默认不带 User-Agent
主线位置:请求主线的第一步。
execute_request把Client的默认头补进请求,只填请求里还没有的头;默认头在ClientBuilder::new里定下,只有一个accept: */*。这一步做的五件事见 执行与 Pending。
flowchart TB
new["ClientBuilder new"] --> acc["默认头 accept"]
ua["user_agent"] -.->|"不调就没有"| def["默认头表"]
acc --> def
def --> exec["execute_request 补空位"]
req["请求自己的头"] --> exec
exec --> wire["发出的请求"]
wire --> srv{"服务器要 UA"}
srv -->|"缺"| deny["403 或限流"]
实验 crate:exp-reqwest/pitb-lab(src/bin/ua.rs)。M2 Max、macOS 27、rustc 1.99.0,reqwest 0.13.5,模板 feature(含 gzip),release 构建,客户端 .no_proxy()。服务器两个:testsrv 的 /echo 回显收到的请求头;另一个模仿「要求 User-Agent」的 API,没有这个头就回 403 missing User-Agent,有就回 hi <UA>。
现象
同一个接口,用 curl 试能通(curl 默认发 user-agent: curl/<版本>,见下面「怎么检查」),写成代码却拿到 403。看看默认客户端到底发了什么:
default Client sends:
GET /echo HTTP/1.1
accept: */*
accept-encoding: gzip
host: 127.0.0.1:52353
body: 0 bytes
blocking Client sends:
GET /echo HTTP/1.1
accept: */*
accept-encoding: gzip
host: 127.0.0.1:52353
body: 0 bytes
异步和阻塞客户端都一样:accept、host,以及因为开了 gzip feature 才有的 accept-encoding(只开默认 feature 时没有这一行)。没有 user-agent。对着要求 UA 的服务器:
default 403 Forbidden "missing User-Agent"
真实服务对缺 UA 的请求可能拒绝,也可能限流,回的错误信息未必提到 User-Agent,排查时容易往认证、IP 上想。
原因
ClientBuilder::new 只放了一个默认头:
pub fn new() -> Self {
let mut headers: HeaderMap<HeaderValue> = HeaderMap::with_capacity(2);
headers.insert(ACCEPT, HeaderValue::from_static("*/*"));
execute_request 把这张表里请求还没有的头补进去,此外不加任何头。host 由 hyper-util 的 legacy Client 加(set_host 默认开着);accept-encoding 由解压层加(执行与 Pending)。阻塞客户端用的是同一个异步 ClientBuilder,所以也没有。
#751(「Don't set User-Agent header by default」,09e7fe6,首次发布在 v0.10.0)。diff 删掉了
static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
和 ClientBuilder::new 里插入它的那一行。在这之前,每个 reqwest 请求都带 User-Agent: reqwest/<版本>。同一个提交新增了 ClientBuilder::user_agent,文档示例建议「Name your user agent after your app?」,用的正是原来那个 concat!,只是 CARGO_PKG_NAME 从 reqwest 变成了你的 crate。v0.10.0 的 CHANGELOG 写作「Change to no longer send a default User-Agent header. Add one via ClientBuilder::user_agent().」提交说明没有写理由。
修法
在 ClientBuilder 上设一次。 user_agent 把值放进默认头表,之后这个 Client 的每个请求都带它:
let named = reqwest::Client::builder()
.no_proxy()
.user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")))
.build()
.unwrap();
say("user_agent(pkg/version)", named.get(&pu)).await;
say("plus per-request header", named.get(&pu).header("user-agent", "nightly-job/2")).await;
user_agent(pkg/version) 200 OK "hi pitb-lab/0.1.0"
plus per-request header 200 OK "hi nightly-job/2"
- UA 里写应用名和版本,最好再加一个能联系到人的网址或邮箱,服务方看到异常流量时能找到你。
- 默认头只填空位,单个请求自己写的
user-agent优先,第二行就是这样。 - 非法的值(比如末尾带换行)在
build()时报错,不像请求上的header()那样拖到send():
user_agent("app/1.0\n") build(): Err("builder error")
user_agent 和 default_headers 写的是同一张表,谁后调用谁生效。走代理时,这个 UA 还会被连接器拿去放进 CONNECT 请求(build() 里的 fn user_agent),细节见 代理。
怎么检查
对着一个回显服务器(
testsrv的/echo,或者任何打印请求头的本地服务)发一次,看有没有user-agent一行。代码里搜
Client::new()和Client::builder(),看后面有没有.user_agent(;Client::new()一定没有。服务端回 403 / 429 而 curl 能通时,用
curl -A ''去掉 UA 再试一次,能复现就是它。对着同一个/echo(curl 8.7.1):$ curl --noproxy '*' -s http://127.0.0.1:52999/echo GET /echo HTTP/1.1 host: 127.0.0.1:52999 user-agent: curl/8.7.1 accept: */* body: 0 bytes $ curl --noproxy '*' -s -A '' http://127.0.0.1:52999/echo GET /echo HTTP/1.1 host: 127.0.0.1:52999 accept: */* body: 0 bytes
小结
- reqwest 0.13.5 的异步和阻塞客户端默认都不发 User-Agent,默认头只有
accept: */*;从 v0.10.0(#751)起就是这样。 - 要求 UA 的服务器会拒绝或限流,错误信息未必提 UA,curl 能通是一个线索。
- 修法:
ClientBuilder::user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")));单个请求用header("user-agent", ..)覆盖。 - 非法 UA 在
build()时就报builder error。