本机请求被系统代理拦走
主线位置:连接主线的第一步,连接器按代理表决定直连还是走代理。
Proxy怎么变成Matcher、NoProxy规则怎么解析、系统代理每次build读一遍,见 代理;macOS 系统设置那一侧(例外列表为什么不生效)在 hyper 卷的 本站的出站客户端 讲过。这一页从报错出发,走一遍排查和修法。
flowchart TB
url["http 127.0.0.1"] --> no{"NO_PROXY 字面匹配"}
no -->|命中| direct["直连"]
no -->|没命中| px{"有代理配置"}
px -->|环境变量| p1["HTTP_PROXY"]
px -->|macOS 设置| p2["系统代理"]
px -->|都没有| direct
p1 --> drop["代理丢掉请求"]
p2 --> drop
drop --> err["connection closed"]
实验 crate:exp-reqwest/pita-lab,src/bin/p05_proxy.rs 和 src/bin/p05_sys.rs。p05_proxy 里的「代理」是自己写的:接受连接、读走请求头、然后挂断,模拟一个把回环请求丢掉的代理工具;HTTP_PROXY 指向它,所以请求不会离开本机。单线程运行时,set_var 时没有别的线程在读环境变量。M2 Max、macOS 27、rustc 1.99.0,release。
这台 Mac 的状态(scutil --proxy 摘录和 shell 环境):
ExceptionsList : <array> {
0 : captive.apple.com
1 : localhost
...
7 : 192.168.0.0/16
}
ExcludeSimpleHostnames : 1
HTTPEnable : 1
HTTPPort : 7890
HTTPProxy : 127.0.0.1
HTTPSEnable : 1
...
HTTP_PROXY=http://127.0.0.1:7890
NO_PROXY=localhost,127.0.0.1,192.168.64.0/24,.local,::1,.xmpbinfo.cn
现象
测试代码里对本机服务器发请求,用的是 Client::new():
== HTTP_PROXY=http://127.0.0.1:53990 NO_PROXY=None
Client::new() 127.0.0.1 via proxy -> error sending request for url (http://127.0.0.1:53989/)
<- client error (SendRequest)
<- connection closed before message completed
Client::new() localhost via proxy -> error sending request for url (http://localhost:53989/)
<- client error (SendRequest)
<- connection closed before message completed
builder().no_proxy() 127.0.0.1 direct -> 200 OK
builder().no_proxy() localhost direct -> 200 OK
== HTTP_PROXY=http://127.0.0.1:53990 NO_PROXY=Some("localhost")
Client::new() 127.0.0.1 via proxy -> error sending request for url (http://127.0.0.1:53989/)
<- client error (SendRequest)
<- connection closed before message completed
Client::new() localhost direct -> 200 OK
builder().no_proxy() 127.0.0.1 direct -> 200 OK
builder().no_proxy() localhost direct -> 200 OK
== HTTP_PROXY=http://127.0.0.1:53990 NO_PROXY=Some("127.0.0.1")
Client::new() 127.0.0.1 direct -> 200 OK
Client::new() localhost via proxy -> error sending request for url (http://localhost:53989/)
<- client error (SendRequest)
<- connection closed before message completed
builder().no_proxy() 127.0.0.1 direct -> 200 OK
builder().no_proxy() localhost direct -> 200 OK
== HTTP_PROXY=http://127.0.0.1:53990 NO_PROXY=Some("127.0.0.0/8,localhost")
Client::new() 127.0.0.1 direct -> 200 OK
Client::new() localhost direct -> 200 OK
builder().no_proxy() 127.0.0.1 direct -> 200 OK
builder().no_proxy() localhost direct -> 200 OK
== HTTPS_PROXY=http://127.0.0.1:53990 NO_PROXY=None
builder() https://localhost:54013/ -> error sending request for url (https://localhost:54013/)
<- client error (Connect)
<- tunnel error: unexpected end of file
(via proxy / direct 是看代理有没有收到新连接判断的。)
- 报错里没有「proxy」这个词。明文请求报
connection closed before message completed,像是服务器崩了;HTTPS 请求报tunnel error: unexpected end of file,CONNECT 隧道没建起来。 NO_PROXY=localhost放过了localhost,没放过127.0.0.1;反过来也一样。.no_proxy()的客户端在每种环境下都直连。
环境变量清掉以后,请求还是会走代理,这次是 macOS 系统设置里的那个。p05_sys 不发请求,直接问 hyper-util 的 Matcher::from_system()(reqwest 的系统代理用的就是它)每个地址会被交给谁:
http://127.0.0.1:8080/ -> proxy http://127.0.0.1:7890/
http://localhost:8080/ -> proxy http://127.0.0.1:7890/
http://192.168.1.10/ -> proxy http://127.0.0.1:7890/
http://printer.local/ -> proxy http://127.0.0.1:7890/
https://localhost:8443/ -> proxy http://127.0.0.1:7890/
系统例外列表里明写着 localhost、*.local、192.168.0.0/16,一个都没起作用。
原因
默认就读系统代理。 默认 feature 里有 system-proxy,ClientBuilder::new 里 auto_sys_proxy 为真,build 时把 Matcher::system 加进代理表。它调 hyper-util 的 Matcher::from_system:先读 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY、NO_PROXY(大写优先,再试小写),环境变量没给的 scheme 再用 macOS 的系统设置补上。
系统设置只读六个键。 mac::with_system 只取 HTTP、HTTPS 两组的开关、主机、端口,ExceptionsList 和 ExcludeSimpleHostnames 都不读。排除规则只有一个来源:NO_PROXY 环境变量。
回环地址没有特殊待遇。 Matcher::intercept 第一步是 self.no.contains(dst.host()?),没命中就按 scheme 返回代理。没有任何一行检查目标是不是 127.0.0.1 或 localhost。
NO_PROXY 按 URL 里的 host 字面匹配。 NoProxy::contains 先试着把 host 解析成 IP:是 IP 就只查 IP 规则(地址或网段),不是就只查域名规则。不做 DNS,所以 localhost 这条域名规则对 http://127.0.0.1 不生效,127.0.0.1 这条 IP 规则对 http://localhost 也不生效。
报错之所以看不出代理:连接器连的是代理,hyper 在这条连接上写请求、等响应,代理一挂断,hyper 看到的就是「响应还没完连接就关了」。代理工具对回环地址怎么处理各不相同,有的转发、有的丢掉;丢掉时就是上面的报错。
修法
测试和本机调用:.no_proxy()。 它清空代理表并关掉系统代理,这个 Client 一律直连,不受机器上任何代理设置的影响:
let client = reqwest::Client::builder().no_proxy().build()?;
生产代码要保留系统代理、只放过本机: 把 IP 和名字都写进 NO_PROXY,用网段覆盖整个回环:
NO_PROXY=127.0.0.0/8,::1,localhost
上面最后一组 127.0.0.0/8,localhost 两种写法都直连了。macOS 系统设置里的例外列表对 reqwest 无效,不要指望它。
只给某些主机走代理: 显式写 Proxy::all(...) 加 .no_proxy(NoProxy::from_string(...))。注意一条显式的 .proxy(...) 会让系统代理整个不再读取。
怎么检查
看环境:
env | grep -i proxy,macOS 上再scutil --proxy。只要有任何一个代理开着,用默认Client访问本机就要想到这个坑。看连的是谁:
RUST_LOG=hyper_util=debug下,连接日志里的端口是代理的,不是目标的。同一个实验,代理在 53980、目标在 53979:DEBUG hyper_util::client::legacy::connect::http: connecting to 127.0.0.1:53980 DEBUG hyper_util::client::legacy::connect::http: connected to 127.0.0.1:53980 Client::new() 127.0.0.1 via proxy -> error sending request for url (http://127.0.0.1:53979/)一个请求在 shell 里用
curl能通、在 Rust 里不通,两者读的代理配置不一定相同;先换成.no_proxy()试一次,通了就是代理的问题。
小结
- 默认
Client读代理环境变量和 macOS 系统代理;127.0.0.1、localhost没有任何特殊待遇。 - macOS 系统设置里的例外列表不读,排除规则只有
NO_PROXY;它按 URL 里的 host 字面匹配,localhost管不到127.0.0.1。 - 代理丢掉请求时,明文报
connection closed before message completed,HTTPS 报tunnel error: unexpected end of file,错误里不提代理。 - 测试和本机调用用
.no_proxy();要保留系统代理就把127.0.0.0/8,::1,localhost都写进NO_PROXY。