Python异步请求遇httpx.UnsupportedProtocol错误,仅调试模式可运行
我尝试用Python的xts_api_client异步客户端xts_connect_async连接XTS Market Data API,代码如下:
from xts_api_client.xts_connect_async import XTSConnect from dotenv import load_dotenv import os import asyncio load_dotenv() async def main(): market_data_API_key = os.getenv("market_data_API_key") market_data_API_secret = os.getenv("market_data_API_secret") API_source = os.getenv("API_source") API_root = os.getenv("API_root") connect = XTSConnect(market_data_API_key, market_data_API_secret, API_source, API_root) response = await connect.marketdata_login() print(response) asyncio.run(main())
在UV(Astral)或标准pip环境运行时,持续出现以下错误:
PS U:\xts> uv run .\xts.py Traceback (most recent call last): File "U:\xts\.venv\Lib\site-packages\httpx\_transports\default.py", line 101, in map_httpcore_exceptions yield File "U:\xts\.venv\Lib\site-packages\httpx\_transports\default.py", line 394, in handle_async_request resp = await self._pool.handle_async_request(req) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "U:\xts\.venv\Lib\site-packages\httpcore\_async\connection_pool.py", line 207, in handle_async_request raise UnsupportedProtocol( "Request URL is missing an 'http://' or 'https://' protocol." ) httpcore.UnsupportedProtocol: Request URL is missing an 'http://' or 'https://' protocol. The above exception was the direct cause of the following exception: Traceback (most recent call last): File "U:\xts\.venv\Lib\site-packages\xts_api_client\xts_connect_async.py", line 633, in marketdata_login response = await self._post("market.login", params) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "U:\xts\.venv\Lib\site-packages\xts_api_client\xts_connect_async.py", line 805, in _post return await self._request(route, "POST", params) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "U:\xts\.venv\Lib\site-packages\xts_api_client\xts_connect_async.py", line 841, in _request raise e File "U:\xts\.venv\Lib\site-packages\xts_api_client\xts_connect_async.py", line 836, in _request r = await self.reqsession.request(method = method, url = url, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...<2 lines>... headers=headers) ^^^^^^^^^^^^^^^^ File "U:\xts\.venv\Lib\site-packages\httpx\_client.py", line 1540, in request return await self.send(request, auth=auth, follow_redirects=follow_redirects) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "U:\xts\.venv\Lib\site-packages\httpx\_client.py", line 1629, in send response = await self._send_handling_auth( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...<4 lines>... ) ^ File "U:\xts\.venv\Lib\site-packages\httpx\_client.py", line 1657, in _send_handling_auth response = await self._send_handling_redirects( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...<3 lines>... ) ^ File "U:\xts\.venv\Lib\site-packages\httpx\_client.py", line 1694, in _send_handling_redirects response = await self._send_single_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "U:\xts\.venv\Lib\site-packages\httpx\_client.py", line 1730, in _send_single_request response = await transport.handle_async_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "U:\xts\.venv\Lib\site-packages\httpx\_transports\default.py", line 393, in handle_async_request with map_httpcore_exceptions(): ~~~~~~~~~~~~~~~~~~~~~~~^^ File "C:\Users\parul.kakade\AppData\Local\Programs\Python\Python313\Lib\contextlib.py", line 162, in __exit__ self.gen.throw(value) ~~~~~~~~~~~~~~^^^^^^^ File "U:\xts\.venv\Lib\site-packages\httpx\_transports\default.py", line 118, in map_httpcore_exceptions raise mapped_exc(message) from exc httpx.UnsupportedProtocol: Request URL is missing an 'http://' or 'https://' protocol. During handling of the above exception, another exception occurred: Traceback (most recent call last): File "U:\xts\xts.py", line 16, in <module> asyncio.run(main()) ~~~~~~~~~~~^^^^^^^^ File "C:\Users\parul.kakade\AppData\Local\Programs\Python\Python313\Lib\asyncio\runners.py", line 195, in run return runner.run(main) ~~~~~~~~~~^^^^^^ File "C:\Users\parul.kakade\AppData\Local\Programs\Python\Python313\Lib\asyncio\runners.py", line 118, in run return self._loop.run_until_complete(task) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^ File "C:\Users\parul.kakade\AppData\Local\Programs\Python\Python313\Lib\asyncio\base_events.py", line 725, in run_until_complete return future.result() ~~~~~~~~~~~~~^^ File "U:\xts\xts.py", line 14, in main response = await connect.marketdata_login() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "U:\xts\.venv\Lib\site-packages\xts_api_client\xts_connect_async.py", line 640, in marketdata_login return response['description'] ^^^^^^^^ UnboundLocalError: cannot access local variable 'response' where it is not associated with a value
我预期XTSConnect.marketdata_login()能成功认证并返回包含登录状态的JSON响应,且已确认所有环境变量(API密钥、密钥、来源、根地址)均正确设置,尤其是API_root以https://开头。
已尝试的操作:
- 创建
XTSConnect实例前打印所有环境变量,确认均正确填充; - 硬编码变量值,排除
.env加载问题; - 在pip和uv环境分别运行脚本;
- 尝试不同版本的
xts-api-client、httpcore和httpx; - 用Trio替代asyncio;
- 在库代码中添加调试打印追踪请求URL;
- VSCode调试器运行能持续正常,但普通运行报错。
提出以下问题:
- 为何错误间歇性出现?调试与普通运行环境的行为差异原因是什么?
XTSConnect或httpx中是否存在未注意到的竞态条件或懒加载行为?- 如何确保
API_root始终被正确格式化并使用?
1. 错误间歇性出现及调试/普通环境差异的原因
调试模式下代码执行速度慢,掩盖了异步环境中的资源初始化问题。xts_api_client内部大概率存在懒加载的HTTP客户端初始化逻辑:普通运行时客户端还没完成初始化就发起请求,导致API_root未被正确拼接;而调试时的断点、慢执行给了初始化足够时间,所以能正常工作。另外,UnboundLocalError是因为marketdata_login方法里,当请求抛出异常时,response变量未被定义就尝试返回,属于库本身的异常处理漏洞。
2. 竞态条件或懒加载行为的确认
是的,xts_api_client的异步客户端存在懒加载:比如reqsession(httpx异步客户端)可能在第一次请求时才初始化,而初始化过程和请求发起之间没有同步机制,导致竞态——请求已经开始拼接URL,但API_root还没被正确赋值到客户端配置里。httpx本身的连接池异步初始化也可能存在类似问题,但核心原因还是客户端库的实现缺陷。
3. 确保API_root正确格式化并使用的方法
临时修复方案(无需修改库代码)
在创建XTSConnect实例后,手动触发客户端初始化,或者强制校验API_root格式:
async def main(): # 加载环境变量代码 market_data_API_key = os.getenv("market_data_API_key") market_data_API_secret = os.getenv("market_data_API_secret") API_source = os.getenv("API_source") API_root = os.getenv("API_root") # 强制校验API_root格式 if not API_root.startswith(('http://', 'https://')): raise ValueError("API_root必须以http://或https://开头") connect = XTSConnect(market_data_API_key, market_data_API_secret, API_source, API_root) # 手动触发客户端初始化,发起一个轻量请求预热 try: await connect.reqsession.get(f"{API_root}/health") except Exception: pass # 执行登录 response = await connect.marketdata_login() print(response)
修复库代码的异常处理漏洞
找到xts_api_client/xts_connect_async.py的marketdata_login方法,修改异常处理逻辑,确保response变量始终有定义:
async def marketdata_login(self): params = { "userID": self.userID, "password": self.password, "source": self.source } try: response = await self._post("market.login", params) return response['description'] except Exception as e: # 返回错误信息或重新抛出异常,避免UnboundLocalError return f"登录失败: {str(e)}" # 或者选择重新抛出异常:raise e
长期方案
向xts_api_client提交issue,反馈两个问题:
- 异步客户端初始化的竞态条件导致URL拼接错误;
marketdata_login方法的异常处理漏洞引发UnboundLocalError。
内容的提问来源于stack exchange,提问作者Parul Kakade

