请求构建
主线位置:请求主线的第一步。
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 个连接。
问: 为什么不让 header() 直接返回 Result,出错当场就知道?
答: 那样每一步都得写 ?,链式调用就断了:client.get(u).header(a, b)?.json(&x)?.send()。reqwest 把「构造」和「执行」放在同一个返回 Result 的地方,也就是 send().await,调用方只处理一次错误。代价是错误离出错的那一行远了,而且 Display 里没有头名。要在发送前检查,用 build() 拿到 Result<Request>,再用 Client::execute 发出去。
头:append、replace 与 entry
几个改头的方法,写法各不相同:
| 方法 | 写法 | 已有同名头时 |
|---|---|---|
header | append | 两个都留 |
headers | replace_headers | 换成传入的值(传入的多值都保留) |
basic_auth / bearer_auth | 经 header_sensitive,append | 两个都留 |
form / json 的 Content-Type | entry().or_insert | 不动已有的 |
multipart 的 Content-Type | self.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加的。
query() 和 form() 要自己开 feature0.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。
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只对完整Bytesbody 成功;wrap、wrap_stream、multipart都是流,复制、重试、307 / 308 重发都做不到。