入门教程

curl 能连通,Python API 却失败?HTTPX 代理环境变量与 NO_PROXY 排查

核对 HTTPX 的环境代理、显式 proxy 与 NO_PROXY 优先关系,区分目标协议、代理协议和 CA 来源,并用本机标记服务验证实际路由,避免把客户端差异误判为服务商故障。

发布:2026年11月6日
更新:2026/11/6

同一台电脑上,curl 能拿到 HTTP 响应,Python 却出现连接拒绝、代理错误或证书验证失败,先核对两者实际经过的网络路径。浏览器、终端、IDE、后台服务和容器未必读取同一份代理设置;成功访问首页也不能证明模型调用正常。

本文限定 Python HTTPX 0.28.1 的默认传输配置,资料核对于 2026-11-06。自定义 transport、mounts 或 SDK 包装可能改变路由,需检查实际创建客户端的位置。下文实验只访问本机回环地址,不发送 API Key,不调用付费模型,也不把本地结果当成服务商评测。

1. 先比较同一次检查的条件

把 curl 与 Python 的目标协议、主机、端口、路径和 HTTP 方法写在同一行,再记录运行位置、版本、启动方式、是否自动跳转,以及代理来自环境还是代码。只比较同一个已确认安全、允许访问的无凭证检查入口,不用生成请求反复试错。

现象可以说明什么还不能说明什么
curl 返回 401 或 404,HTTPX 没有 HTTP 状态两次请求到达的阶段不同curl 已验证模型可用,或 API Key 有问题
HTTPX 连接被拒绝,配置中存在本地代理端口本地代理地址与监听状态值得核对一定是服务商宕机
HTTPX 报 ProxyError,或代理交互出现 407应检查代理握手、认证与访问规则给模型 API 换 Key 就能修好
HTTPX 报证书验证失败应核对实际对端、信任库与企业证书配置必须关闭 TLS 验证
终端成功,后台服务失败启动环境可能不同同机器上的所有进程共享最新环境

保留脱敏后的异常类型和时间,不直接复制整份异常文本或调试日志;其中可能包含带凭证的代理 URL。连接失败只描述这一尝试,不能据此断言整个业务任务从未重试或不会计费。需要整理工单时,使用最小复现与脱敏反馈模板。

2. 把目标协议与代理协议分开

HTTPX 默认使用环境配置。HTTP_PROXY 面向 HTTP 目标,HTTPS_PROXY 面向 HTTPS 目标,ALL_PROXY 可提供通用代理,NO_PROXY 指定无需经过环境代理的目标。[1]

HTTPS_PROXY 中的值并不必然以 https:// 开头。目标 API 使用 HTTPS,描述的是客户端到目标的加密要求;代理 URL 的 scheme 描述的是如何连接代理本身。对于普通 HTTP 代理,访问 HTTPS 目标通常先向代理建立 CONNECT 隧道,再在隧道中与目标进行 TLS 握手,因此代理地址常是 http://代理主机:端口。[2] 应按管理员或代理服务的真实协议配置,不能仅因为目标是 HTTPS 就改写代理 URL。

还要核对地址所在的网络环境。容器中的 127.0.0.1 指向容器自身,远程服务器中的 127.0.0.1 指向远程服务器;它们都不是你笔记本上的代理。不要复制某个“常用端口”后假定相应代理一定存在。

代理账号与 API Key 是两种凭证。代理 URL 可以包含用户名和密码,不要把完整值贴入工单、截图或仓库。先在本地核对是否设置、协议和经过脱敏的端点,再通过既有安全渠道确认代理认证。

3. 环境变量存在,不等于客户端采用了它

对 HTTPX 0.28.1 的普通 Client,源码中的配置关系如下:[3]

创建方式或设置应如何理解
未传 proxy,保留默认 trust_env=True读取环境代理及其 NO_PROXY 规则
proxy=None,保留默认 trust_env=True仍可能采用环境代理,不是强制直连
显式传 proxy=某地址使用显式代理,不自动合并环境中的 NO_PROXY 例外
trust_env=False,且没有显式代理或自定义路由不采用环境代理
trust_env=False,同时显式传 proxy显式代理仍然生效
自定义 transport 或 mounts需要单独检查;不能直接套用上表的默认路由结论

HTTPX 在创建客户端时建立代理路由。只在同一进程中修改 NO_PROXY,不应期待已有 Client 自动重新读取;完成旧请求、关闭旧客户端,再按新配置创建并验收。对于服务或容器,修改文件也不等于已运行进程的配置改变。不要为排错直接关闭承载生产请求的共享客户端。

此外,HTTPX 0.28.1 通过 Python 的 urllib.request.getproxies 获取代理信息;在 Windows、macOS 的特定条件下还可能回退到系统配置。[4] 因此“没有看到这几个环境变量”并不足以证明正在直连。只检查变量是否存在也不能证明最终路由,应结合显式参数和实际代理访问记录。

curl 有自己的规则和配置来源。例如官方文档规定 http_proxy 使用小写名,并说明 NO_PROXY 的域名和 CIDR 匹配规则。[6] Windows 环境变量名通常不区分大小写,不要照搬 Unix 上的大小写对照实验。更不要假定一个客户端接受的通配符、前导点、CIDR 或端口写法,在另一个客户端中含义完全相同。跨客户端排查先用精确主机名或 IP 做窄范围对照;localhost 与 127.0.0.1 也应分别核对。

4. 用本机标记服务验证四种路由

下面脚本需要已经安装 HTTPX;本文实际验证版本为 0.28.1。保存为 proxy_demo.py 后,用安装了该版本的 Python 运行。脚本启动两个只监听 127.0.0.1 随机端口的服务:一个返回 origin,另一个扮演代理入口并返回 proxy,后者不转发任何请求。

它只在自己的独立进程中临时替换相关环境项,并在结束或异常时恢复。不要把这种修改进程环境的实验嵌入多线程生产服务。请求设有各阶段 2 秒超时、不跟随跳转,读取响应最多接受 32 字节并关闭响应;该超时不是严格的端到端总预算,字节限制也不是底层网络缓冲的内存上限。

import os
from contextlib import contextmanager
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread
import httpx

NAMES = {"http_proxy", "https_proxy", "all_proxy", "no_proxy",
         "ssl_cert_file", "ssl_cert_dir", "request_method"}

@contextmanager
def isolated_env(proxy):
    saved = {k: v for k, v in os.environ.items() if k.lower() in NAMES}
    try:
        for k in list(os.environ):
            if k.lower() in NAMES:
                del os.environ[k]
        for k in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"):
            os.environ[k] = proxy
        os.environ["NO_PROXY"] = ""
        yield
    finally:
        for k in list(os.environ):
            if k.lower() in NAMES:
                del os.environ[k]
        os.environ.update(saved)

@contextmanager
def endpoint(marker):
    class Handler(BaseHTTPRequestHandler):
        def setup(self):
            super().setup()
            self.connection.settimeout(2)
        def do_GET(self):
            body = marker.encode("ascii")
            self.send_response(200)
            self.send_header("Content-Length", str(len(body)))
            self.send_header("Connection", "close")
            self.end_headers()
            self.wfile.write(body)
        def log_message(self, *_):
            pass
    server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
    thread = Thread(target=server.serve_forever, daemon=True)
    thread.start()
    try:
        yield f"http://127.0.0.1:{server.server_port}"
    finally:
        server.shutdown()
        server.server_close()
        thread.join(timeout=3)

def read_marker(client, url):
    with client.stream("GET", url) as response:
        response.raise_for_status()
        data = bytearray()
        for chunk in response.iter_raw():
            if len(data) + len(chunk) > 32:
                raise ValueError("Unexpected response size")
            data.extend(chunk)
        return data.decode("ascii")

def main():
    with endpoint("origin") as origin, endpoint("proxy") as proxy:
        with isolated_env(proxy):
            cases = [
                ("env", "", {}, "proxy"),
                ("no_proxy", "127.0.0.1", {}, "origin"),
                ("explicit_proxy", "127.0.0.1", {"proxy": proxy}, "proxy"),
                ("ignore_env", "", {"trust_env": False}, "origin"),
            ]
            for label, bypass, options, expected in cases:
                os.environ["NO_PROXY"] = bypass
                with httpx.Client(timeout=2, follow_redirects=False,
                                  **options) as client:
                    actual = read_marker(client, origin)
                if actual != expected:
                    raise AssertionError(f"{label}: {actual} != {expected}")
                print(label, actual)

if __name__ == "__main__":
    main()

预期依次得到 env proxy、no_proxy origin、explicit_proxy proxy、ignore_env origin。第三项尤其容易误判:已经显式传入 proxy 时,环境里的 NO_PROXY 并没有自动让目标直连。

本地验证还覆盖了旧客户端保留原路由、新客户端读取新配置,proxy=None 仍使用环境代理,显式代理不受 trust_env=False 取消,以及 32/33 字节边界和异常后的环境恢复。共 13 项检查通过,其中证书相关检查仅验证客户端创建时是否读取无效 CA 文件路径。

该实验没有实现 HTTPS CONNECT、TLS 握手、SOCKS、代理认证或真实转发,也没有验证 curl 的网络路径。它只证明此版本 HTTPX 在这些受控条件下选择了哪个本机入口,不能证明企业代理或真实 API 已修复。

5. 别把 trust_env=False 当成单纯的代理开关

HTTPX 0.28.1 的 create_ssl_context 源码显示:在 verify=True 的默认分支中,trust_env=True 时会检查 SSL_CERT_FILE,再检查 SSL_CERT_DIR;trust_env=False 时跳过这些环境来源,使用默认 certifi CA 包。[5] 所以修改 trust_env 可能同时改变代理和 CA 来源,不能把修复或新出现的证书错误全部归因于“换了线路”。

这也不等于关闭证书验证。verify=False 才是另一类危险的改变,本教程不需要它。若企业环境依赖受管 CA,应由维护者确认可信证书来源,必要时显式提供正确的 SSLContext,并保持主机名及证书链验证;已有显式 SSLContext 的行为不能按默认分支推断。

本次查阅发现,0.28.1 标签下旧 SSL 文档对自动读取证书环境变量的文字与同标签源码不一致。本节按已核对源码及本地创建客户端的结果限定结论,不据此推断其他版本。证书过期、域名不匹配和缺失 CA 的处理,见SSL 证书错误排查。

6. 在原故障环境中完成修复验收

  1. 确认组织允许的出口方式,查清代理配置的唯一负责来源:启动环境、应用配置或显式代码。只改必要项目,避免同时改代理、证书、模型和 Key。
  2. 由维护者检查实际进程使用的配置。不要导出整个环境;只记录相关变量的有无、脱敏端点、NO_PROXY 的目标是否匹配、客户端版本和配置生效时间。
  3. 对必要的内部直连目标,设置尽可能窄的例外并验证。NO_PROXY=* 会扩大绕过范围,不应作为生产环境的通用修复;显式代理需要用应用支持的路由配置处理例外,不能只追加环境变量。
  4. 按部署流程让新配置生效,在原来的服务或容器中重新创建客户端。使用同一无凭证入口、相同方法、不自动跳转,记录 HTTP 状态或失败阶段;在获准的范围内结合代理日志确认路径。
  5. 网络与证书层恢复后,再由业务方用受限测试凭证和预算验收必要的模型任务。无凭证 HTTP 响应和本机标记都不能代替权限、流式完成与账单检查。

如果路径已确认、并发时仍出现 PoolTimeout,应转到HTTPX 连接池与流式释放排查,不要继续通过改代理变量掩盖连接池等待。

7. 核对资料与适用范围

以下一手资料于 2026-11-06 核对。版本固定资料用于说明 HTTPX 0.28.1,curl 文档用于提醒客户端差异,未把其匹配语义直接套用到 HTTPX。

标签:HTTPX代理配置NO_PROXYAPI 排错Python
curl 能连通,Python API 却失败?HTTPX 代理环境变量与 NO_PROXY 排查 - API选