Rust in Everything
基于 reqwest 0.13.5

默认不带 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,所以也没有。

📝NOTE
冷知识:0.10 之前是有默认 UA 的

#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。

💬 关联讨论

发起讨论
加载中...