Rust in Everything
基于 reqwest 0.13.5

请求构建

主线位置:请求主线的第一步。client.get(url) 造出一个 RequestBuilder,链式调用往里填方法、URL、头和 body,send() 把它交给 Client::execute_request。这一页讲交出去之前发生的事;交出去之后见 执行与 Pending。

flowchart TB
  get["client.get url"] --> into["IntoUrl"]
  into -->|Ok| rb["RequestBuilder Ok"]
  into -->|Err| rbe["RequestBuilder Err"]
  rb --> chain["header json form"]
  chain -->|全部成功| rb2["仍是 Ok"]
  chain -->|某一步失败| rbe
  rbe --> rbe2["之后的调用全跳过"]
  rb2 --> send["send"]
  rbe2 --> send
  send -->|Ok| exec["execute_request"]
  send -->|Err| perr["Pending new_err"]

文件是 async_impl/request.rs(1151 行,其中 #[cfg(test)] 从第 667 行开始),认证头的编码在 util.rs。实验在 exp-reqwest/req-lab(M2 Max、macOS 27、rustc 1.99.0):builder_err 构造各种出错的请求,wire 把请求发到一个自己写的 HTTP/1.1 服务器,服务器把收到的请求头原样打印出来。这台机器配了系统代理(scutil --proxy 显示 HTTP/HTTPS 代理 127.0.0.1:7890),所有客户端都调了 .no_proxy()。

两个结构

Request 就是一个请求的全部数据:

pub struct Request {
    method: Method,
    url: Url,
    headers: HeaderMap,
    body: Option<Body>,
    version: Version,
    extensions: Extensions,
}

url 是 url::Url,不是 http::Uri,转换在执行时才做。超时没有单独的字段,timeout_mut 把它存在 extensions 里(RequestConfig<TotalTimeout>),执行时客户端从这里取每个请求自己的超时,取不到就用 ClientBuilder::timeout 的值。

RequestBuilder 只多一个 Client:

#[must_use = "RequestBuilder does nothing until you 'send' it"]
pub struct RequestBuilder {
    client: Client,
    request: crate::Result<Request>,
}

Client 内部是一个 Arc,克隆进来只是加一次引用计数(见 一个 Client)。关键是第二个字段:它不是 Request,而是 crate::Result<Request>。

错误先存着,send 时才报

Client::get、post 等都转到 Client::request:

    pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
        let req = url.into_url().map(move |url| Request::new(method, url));
        RequestBuilder::new(self.clone(), req)
    }

URL 解析失败,req 就是 Err,照样包进 RequestBuilder 返回,不 panic,也不让 get 返回 Result。之后每个方法都是同一个套路,以 header_sensitive 为例:

        let mut error = None;
        if let Ok(ref mut req) = self.request {
            match <HeaderName as TryFrom<K>>::try_from(key) {
                Ok(key) => match <HeaderValue as TryFrom<V>>::try_from(value) {
                    Ok(mut value) => {
                        // ...
                        req.headers_mut().append(key, value);
                    }
                    Err(e) => error = Some(crate::error::builder(e.into())),
                },
                Err(e) => error = Some(crate::error::builder(e.into())),
            };
        }
        if let Some(err) = error {
            self.request = Err(err);
        }
        self
  • 已经是 Err,什么也不做,直接返回 self。所以错误是「粘住」的:第一个错误之后的调用全部跳过,后面的调用也没法把它改回 Ok。
  • 这一步出错,把 Ok(Request) 整个换成 Err,已经填好的方法、URL、头都丢了。
  • 错误统一用 crate::error::builder 包装,Kind::Builder。

send 才看结果:

    pub fn send(self) -> impl Future<Output = Result<Response, crate::Error>> {
        match self.request {
            Ok(req) => self.client.execute_request(req),
            Err(err) => Pending::new_err(err),
        }
    }

Err 分支返回的 Pending 第一次被 poll 就交出这个错误,不碰网络。build 直接把 self.request 交出来,想早点知道错误可以用它。

builder_err 对同一个本地服务器构造了 7 种出错的请求,第一种是头的值里带换行:

  let rb = c
    .get(srv.url("/a"))
    .header("x-note", "line1\nline2")
    .header("x-after", "still called");
  println!("builder Debug: {rb:?}");
  match rb.send().await {
    // ...

输出:

builder Debug: RequestBuilder { error: reqwest::Error { kind: Builder, source: http::Error(InvalidHeaderValue) } }
send() Display: builder error
send() Debug:   reqwest::Error { kind: Builder, source: http::Error(InvalidHeaderValue) }
is_builder=true url=None
  source: failed to parse header value
bad name: builder error / reqwest::Error { kind: Builder, source: http::Error(InvalidHeaderName) }
query tuple: builder error / source: Some("unsupported pair")
bad url: builder error / reqwest::Error { kind: Builder, source: RelativeUrlWithoutBase }
ftp build(): Some(true)
ftp: builder error for url (ftp://127.0.0.1/x) / is_builder=true
json: builder error / source: Some("key must be a string")
sticky: build() is_err=true
server accepted 0 connection(s)
  • Display 只有一句 builder error,不带 URL,也不说是哪个头。顺着 source() 走一层是 failed to parse header value,还是没有头名。一个请求设了十几个头时,只能逐个排查。
  • .query(&("k", "v")) 传的是元组,serde_urlencoded 不接受顶层的单个键值对,错误是 unsupported pair。文档里写了要传切片:.query(&[("k", "v")])。
  • http//missing-colon(少了冒号)在 Client::get 里解析失败,错误是 RelativeUrlWithoutBase。
  • ftp:// 能解析成 Url,build() 是 Ok;它在 execute_request 里被拒(执行与 Pending 的第一步),错误种类同样是 Builder,但这次带了 URL。
  • HashMap<(i32, i32), _> 的键不是字符串,serde_json 报 key must be a string,也是到 send 才看到。
  • 7 个请求,服务器收到 0 个连接。
📝NOTE
没有蠢问题

问: 为什么不让 header() 直接返回 Result,出错当场就知道?

答: 那样每一步都得写 ?,链式调用就断了:client.get(u).header(a, b)?.json(&x)?.send()。reqwest 把「构造」和「执行」放在同一个返回 Result 的地方,也就是 send().await,调用方只处理一次错误。代价是错误离出错的那一行远了,而且 Display 里没有头名。要在发送前检查,用 build() 拿到 Result<Request>,再用 Client::execute 发出去。

头:append、replace 与 entry

几个改头的方法,写法各不相同:

方法写法已有同名头时
headerappend两个都留
headersreplace_headers换成传入的值(传入的多值都保留)
basic_auth / bearer_auth经 header_sensitive,append两个都留
form / json 的 Content-Typeentry().or_insert不动已有的
multipart 的 Content-Typeself.header(..),append两个都留

wire 里的一个请求:

    let mut m = HeaderMap::new();
    m.insert("x-a", HeaderValue::from_static("from-map"));
    c.get(srv.url("/h"))
      .header("x-a", "one")
      .header("x-a", "two")
      .header("x-b", "one")
      .header("x-b", "two")
      .headers(m)
      .header("accept", "text/plain")
      .send()

服务器收到:

    GET /h HTTP/1.1
    x-a: from-map
    x-b: one
    x-b: two
    accept: text/plain
    accept-encoding: gzip
    host: 127.0.0.1:61730

x-a 的两个值被 headers() 换成了一个,x-b 没被 headers() 提到,两个都在。accept: text/plain 是请求自己设的,客户端默认的 accept: */* 就不再加(默认头只填空位,见 执行与 Pending)。

json 在请求已有 Content-Type 时不覆盖:先设 application/vnd.api+json 再调 .json(),线上就是 content-type: application/vnd.api+json。form 从 0.12.13 起也这样(#2527,「stop RequestBuilder::form() from overwriting previously set CONTENT_TYPE」,之前是 insert)。multipart 没跟着改,先手设一个 Content-Type 再调 .multipart(),线上会出现两行,实测见 请求体与 multipart。

查询、表单、JSON

query 直接在 Url 上追加,不覆盖 URL 里原有的参数:

            let url = req.url_mut();
            let mut pairs = url.query_pairs_mut();
            let serializer = serde_urlencoded::Serializer::new(&mut pairs);

            if let Err(err) = query.serialize(serializer) {
                error = Some(crate::error::builder(err));
            }

序列化完如果查询串是空的(传了空切片),再 set_query(None) 去掉孤零零的 ?。form 用 serde_urlencoded::to_string 生成整个 body,json 用 serde_json::to_vec,两者都把结果变成 Body(String 和 Vec<u8> 都转成完整的 Bytes,见 请求体与 multipart)。wire 的结果:

== query
[  16ms server] request head:
    GET /q?x=1&a=1&a=2&page=2&tag=a+b%26c HTTP/1.1
...
== form
[  16ms server] request head:
    POST /form HTTP/1.1
    content-type: application/x-www-form-urlencoded
    accept: */*
    accept-encoding: gzip
    host: 127.0.0.1:61730
    content-length: 17
    -- body: content-length 17 (17 bytes)
== json
[  17ms server] request head:
    POST /json HTTP/1.1
    content-type: application/json
    accept: */*
    accept-encoding: gzip
    host: 127.0.0.1:61730
    content-length: 7
    -- body: content-length 7 (7 bytes)
  • URL 里原有的 x=1 留着,两次 .query() 依次追加;同名键 a 出现两次;结构体 Q { page, tag } 按字段展开,空格编成 +,& 编成 %26。
  • 表单 [("k", "v w"), ("z", "中")] 编成 17 字节(k=v+w&z=%E4%B8%AD)。
  • body 都是完整的字节,所以有 content-length。这个头不是 reqwest 加的,是 hyper 按 body 的 size_hint 加的。
📝NOTE
冷知识:0.13 起 query() 和 form() 要自己开 feature

0.12 里 query()、form() 一直在,serde 和 serde_urlencoded 是必选依赖。提交 9fa9067(「feat: add query and form crate features」,#2858)把它们放到 #[cfg(feature = "query")]、#[cfg(feature = "form")] 后面,提交说明写着「Make serde optional by introducing query, form features」和「Breaking change since query() and form() are no longer available by default, but must be enabled.」。现在 Cargo.toml 里是 query = ["dep:serde", "dep:serde_urlencoded"],form 相同,json 是 ["dep:serde", "dep:serde_json"],都不在 default 里。首次发布在 v0.13.0,CHANGELOG 的 Breaking changes 写的是「query and form are now crate features, disabled by default.」

只开 json 编译 .query(..) 和 .form(..)(exp-reqwest/req-nofeat),编译器不提 feature,只说没有这个方法:

error[E0599]: no method named `query` found for struct `RequestBuilder` in the current scope
 --> src/main.rs:5:40
  |
5 |   let _ = c.get("http://127.0.0.1:9/").query(&[("a", "1")]);
  |                                        ^^^^^ method not found in `RequestBuilder`

error[E0599]: no method named `form` found for struct `RequestBuilder` in the current scope
 --> src/main.rs:6:41
  |
6 |   let _ = c.post("http://127.0.0.1:9/").form(&[("a", "1")]);
  |                                         ^^^^
  |
help: there is a method `or` with a similar name

认证:basic、bearer 和 URL 里的用户名

basic_auth 调 util::basic_auth:往 b"Basic " 后面用 base64 的 EncoderWriter 写 {username}:{password}(没有密码就只写 {username}:),生成的 HeaderValue 标成敏感(set_sensitive(true))。bearer_auth 就是 format!("Bearer {token}"),也标敏感。敏感的值在 Debug 里显示成 Sensitive,HTTP/2 下 hpack 不把它放进动态表;线上内容不变。

URL 里写了用户名密码(http://bob:s3cret@host/)时,RequestBuilder::new 用 extract_authority 把它们从 URL 里拿掉(百分号解码),转手调 basic_auth:

        let auth = builder
            .request
            .as_mut()
            .ok()
            .and_then(|req| extract_authority(&mut req.url));

        if let Some((username, password)) = auth {
            builder.basic_auth(username, password)
        } else {
            builder
        }

wire 打印的 builder 里,URL 的 username 已经是空串,password 是 None,头里多了一个 "authorization": Sensitive。用户名密码不会出现在请求行里,也不会出现在错误信息的 URL 里。

认证头都走 append,RequestBuilder 不检查已经有没有 Authorization。

❗IMPORTANT
动脑筋:URL 写成 http://bob:s3cret@127.0.0.1:port/u,再调 .bearer_auth("tok"),服务器收到几行 authorization?

try_clone:只有完整的 body 能复制

RequestBuilder::try_clone 在 request 是 Err 时返回 None(错误不能克隆),否则转到 Request::try_clone:

    pub fn try_clone(&self) -> Option<Request> {
        let body = match self.body.as_ref() {
            Some(body) => Some(body.try_clone()?),
            None => None,
        };
        // ...

Body::try_clone 只在 body 是完整的 Bytes 时成功,流式 body 返回 None。body 实验里四种 body 的结果:

try_clone: bytes=true stream=false wrap=false multipart_text=false
  • .body("hello") 是完整字节,能复制。
  • Body::wrap_stream(..) 是流,不能。
  • Body::wrap(String::from("hello")) 内容是个 String,长度也知道,但 wrap 一律走流式那一支,不能。
  • 只有文本字段的 multipart::Form 也不能:.multipart() 总是把表单变成流。

这个 None 不只影响用户自己调 try_clone。重试策略的 clone_request 和重定向策略的 clone_body 用的都是同一个 Body::try_clone:流式 body 的请求不会被重试;遇到要求原样重发 body 的 307 / 308,FollowRedirect 不跟,调用方拿到的就是这个 3xx 响应。细节在 重试 和 重定向,两种 body 的区别在 请求体与 multipart。

动脑筋答案

== userinfo + bearer_auth
[  18ms server] request head:
    GET /u HTTP/1.1
    authorization: Basic Ym9iOnMzY3JldA==
    authorization: Bearer tok
    accept: */*
    accept-encoding: gzip
    host: 127.0.0.1:61730
    -- body: no body (0 bytes)

两行。RequestBuilder::new 从 URL 里拿到 bob:s3cret,已经 append 了一个 Basic Ym9iOnMzY3JldA==(bob:s3cret 的 base64);bearer_auth 再 append 一个 Bearer tok。服务端拿到两个 Authorization,用哪个由服务端决定,很多实现只读第一个,于是 token 被无视。同一个请求连调两次 basic_auth 也是两行。要换掉已有的认证头,用 headers()(替换语义),或者别把用户名写进 URL。

小结

  • RequestBuilder 存的是 crate::Result<Request>:URL 解析、非法头、query / form / json 序列化失败都记成 Kind::Builder 错误,后续调用全部跳过,send() 返回的 Pending 第一次 poll 就交出它,不连网络。
  • 错误的 Display 只有 builder error,非法头的错误不带头名也不带 URL;build() 可以在发送前拿到它。
  • header 追加,headers 替换,json / form 不覆盖已有 Content-Type,multipart 会多出一行 Content-Type。
  • URL 里的用户名密码被移出 URL、变成 basic 认证头;认证头都是追加,再调 bearer_auth 会出现两行 Authorization。
  • 0.13 起 query()、form() 要开同名 feature,否则是 E0599,编译器不提示 feature。
  • try_clone 只对完整 Bytes body 成功;wrap、wrap_stream、multipart 都是流,复制、重试、307 / 308 重发都做不到。

💬 关联讨论

发起讨论
加载中...