builder error
主线位置:请求主线的第一步之前。
RequestBuilder的每个方法出错时不返回Result,而是把错误存进自己,send()才交出来;这套「错误先存着」的机制和七种出错方式见 请求构建。本页讲它最常见的来路:一个带换行的密钥。
flowchart TB file["token.txt 末尾换行"] --> bearer["bearer_auth"] file --> basic["basic_auth"] bearer --> hv["HeaderValue 校验"] hv -->|失败| stash["Err 存进 builder"] stash --> send["send"] send --> err["builder error"] basic --> b64["base64 编码"] b64 --> wire["照常发出"] wire --> srv["服务端认证失败"]
实验 crate:exp-reqwest/pitb-lab(src/bin/token.rs)。M2 Max、macOS 27、rustc 1.99.0,reqwest 0.13.5,release 构建,客户端 .no_proxy(),服务器是 testsrv 的 /echo。
现象
密钥放在文件里,echo sk-test-123 > token.txt 写进去,std::fs::read_to_string 读出来是 "sk-test-123\n"。拿它发请求:
let token = String::from("sk-test-123\n");
let e = c.get(srv.url("/echo")).bearer_auth(&token).send().await.unwrap_err();
println!("bearer_auth: {e}");
println!(" {e:?}");
println!(" is_builder={} url={:?}", e.is_builder(), e.url());
println!(" chain: {}", pitb_lab::chain(&e));
let e = c.get(srv.url("/echo")).header("x-api-key", &token).send().await.unwrap_err();
println!("x-api-key: {e}");
let r = c.get(srv.url("/echo")).basic_auth("app", Some(&token)).send().await.unwrap();
// ...
bearer_auth: builder error
reqwest::Error { kind: Builder, source: http::Error(InvalidHeaderValue) }
is_builder=true url=None
chain: builder error <- failed to parse header value
x-api-key: builder error
basic_auth: sent, server saw `authorization: Basic YXBwOnNrLXRlc3QtMTIzCg==`
server accepted 1 connection(s), 1 request(s)
- 日志里只有
builder error。source再往下一层是failed to parse header value,到底了。没有头名,没有 URL,也看不出是换行。一个请求设了十几个头时,只能一个个试。 - 错误出在
send().await,离写错的那一行很远:bearer_auth那一行没有任何返回值可以检查。 - 前两个请求没有发出去,服务器收到的唯一一个请求来自
basic_auth。 basic_auth不报错。YXBwOnNrLXRlc3QtMTIzCg==解出来是app:sk-test-123\n,换行被编进了密码。真正校验密码的服务器会回认证失败,客户端这一侧什么也看不出来。
原因
bearer_auth 把 token 拼成字符串,交给 header_sensitive:
pub fn bearer_auth<T>(self, token: T) -> RequestBuilder
where
T: fmt::Display,
{
let header_value = format!("Bearer {token}");
self.header_sensitive(crate::header::AUTHORIZATION, header_value, true)
}
header_sensitive 里 HeaderValue::try_from 拒绝控制字符,\n 不合法,crate::error::builder(e.into()) 被存进 self.request,替换掉原来的 Ok(Request)。HTTP/1.1 里换行是头的分隔符,放进头值就能伪造出下一个头,所以 http crate 在构造 HeaderValue 时就挡掉它。header() 走的是同一个函数。
错误的 Display 对 Kind::Builder 只写 builder error;source 是 http::Error,它的 Display 是 failed to parse header value,同样不知道是哪个头。头名在 header_sensitive 里明明拿得到,但没有放进错误。send 看到 Err 就返回一个立刻失败的 Pending,不碰网络。
basic_auth 先调 util::basic_auth,把 "{username}:{password}" 整个写进 base64 编码器,再 HeaderValue::from_maybe_shared(...).expect("base64 is always valid HeaderValue")。base64 的输出只有字母、数字、+、/、=,永远是合法头值,所以换行不会被发现,只会被编码。
修法
读进来就 trim(),并且在启动时用头名校验一次。 配置加载的地方是唯一知道「这个值从哪来、要放进哪个头」的地方:
let check = |t: &str| {
HeaderValue::from_str(&format!("Bearer {t}")).map_err(|e| format!("Authorization (from token.txt): {e}"))
};
println!("startup check, raw: {:?}", check(&token).map(|_| ()));
let token = token.trim();
println!("startup check, trimmed: {:?}", check(token).map(|_| ()));
let r = c.get(srv.url("/echo")).bearer_auth(token).send().await.unwrap();
println!("trimmed: {}", r.status());
startup check, raw: Err("Authorization (from token.txt): failed to parse header value")
startup check, trimmed: Ok(())
trimmed: 200 OK
trim() 同时去掉 \r\n(Windows 上写的文件)和首尾空格。对 basic_auth 的用户名和密码也要 trim(),因为它不会报错。
发送之前就要拿到错误,用 build()。 build 返回 crate::Result<Request>,再用 Client::execute 发出去:
build(): is_err=true
错误内容和 send() 时一样,好处只是位置:可以紧挨着设头的代码处理,或者在请求模板建好时就失败。
怎么检查
e.is_builder()为真:请求根本没发出去,不用去服务器日志里找。- 看
{e:?}:InvalidHeaderValue是值的问题(换行、回车、其他控制字符),InvalidHeaderName是名字的问题(比如名字里有空格)。 - 非法头的错误
e.url()是None。URL 解析失败、.query()传了元组、json()序列化失败也都是builder error,Debug里的source能区分它们(请求构建 一页列了七种)。 - 认证失败但客户端没报错时,看看用的是不是
basic_auth:把服务端收到的Authorization头 base64 解码,末尾多出的\n一眼就能看到。
小结
bearer_auth/header拿到带换行的值,不报错,把错误存进 builder,send()时返回builder error;Display和source里都没有头名和 URL。basic_auth不会因为换行失败:base64 后总是合法头值,错误的密码被照常发出。- 修法:读入时
trim(),启动时带着头名用HeaderValue::from_str校验;需要就近处理时用build()。 is_builder()为真说明请求没有发出;Debug里的InvalidHeaderValue/InvalidHeaderName区分值和名。