error decoding response body
主线位置:响应主线的最后一步,
json()把 body 读完交给 serde_json。json经do_bytes读 body 的代码、三种典型失败的错误链见 错误 的「json 解码失败,和留住原文」;Display为什么只打一层见同一页。这一页从日志里那一行报错出发。
flowchart TB r["Response"] --> j["json"] j --> b["do_bytes 读完 body"] b --> sj["serde 解析"] sj --> e["Decode 错误"] e --> d["Display 一层"] e --> src["source serde 原因"] b --> gone["状态 类型 原文 丢掉"] r --> fix["bytes 留住原文"] fix --> own["自己解析 带上下文"]
实验 crate:exp-reqwest/pita-lab,src/bin/p10_json.rs。本机服务器三个地址:/gateway 回 502、text/html 的网关错误页;/login 回 200、text/html 的登录页(会话过期时常见);/drift 回 200、application/json,但 year 从数字变成了字符串。客户端期望 struct Paper { id: String, year: u16 }。M2 Max、macOS 27、rustc 1.99.0,release。
现象
== /gateway
{e}: error decoding response body for url (http://127.0.0.1:52433/gateway)
chain: error decoding response body for url (http://127.0.0.1:52433/gateway)
<- expected value at line 1 column 1
== /login
{e}: error decoding response body for url (http://127.0.0.1:52433/login)
chain: error decoding response body for url (http://127.0.0.1:52433/login)
<- expected value at line 1 column 1
== /drift
{e}: error decoding response body for url (http://127.0.0.1:52433/drift)
chain: error decoding response body for url (http://127.0.0.1:52433/drift)
<- invalid type: string "2024", expected u16 at line 1 column 32
- 日志里通常只有
{e}那一行:三种完全不同的问题,同一句话。 - 沿
source()往下一层能看到 serde_json 的原因。字段类型变了这种,原因已经够用;另外两种只有expected value at line 1 column 1,只说明第一个字节不是 JSON。上游在报 502、会话过期了,这些都不在错误里。
原因
json() 先读完 body,再交给 serde_json,失败时只带走 serde 的错误。 Response::json:
pub async fn json<T: DeserializeOwned>(self) -> crate::Result<T> {
let (full, url) = self.do_bytes().await?;
serde_json::from_slice(&full).map_err(|err| crate::error::decode(err).with_url(*url))
}
do_bytes 把 body 收集成一块 Bytes,同时交出 URL;self 在这里被消费,状态码和响应头没有传出来。full 是局部变量,错误造好以后它就被丢掉。造出来的是 Kind::Decode 加 serde 的错误加 URL,原文、状态码、Content-Type 都不在里面。json() 也不检查状态码和 Content-Type,502 的 HTML 页面照样交给 serde_json 去解析。
Display 只打一层。 impl Display for Error 只写种类的固定短句和 URL,不写 source。{e}、tracing::error!(%e) 都只能看到第一行。
URL 本身也是最近才有的:0.13.4 之前,json 先调 bytes()、再 map_err(crate::error::decode),错误里没有 URL,#3026(「fix: do not lose the url in error when decoding json」,首次发布在 v0.13.4)加了 do_bytes 才补上。还在 0.12 上的项目,连上面 for url (...) 那半句都没有。
修法
先把 body 读到自己手里,再解析。 失败时,状态码、Content-Type、body 开头都还在:
async fn json_or_explain<T: serde::de::DeserializeOwned>(r: reqwest::Response) -> Result<T, String> {
let status = r.status();
let ct = r.headers().get(reqwest::header::CONTENT_TYPE).and_then(|v| v.to_str().ok()).unwrap_or("-").to_string();
let url = r.url().clone();
let body = r.bytes().await.map_err(|e| chain(&e))?;
serde_json::from_slice(&body).map_err(|e| {
let head = String::from_utf8_lossy(&body[..body.len().min(80)]);
format!("{status} {ct} from {url}: {e}; body starts {head:?}")
})
}
== /gateway
fix: 502 Bad Gateway text/html from http://127.0.0.1:52433/gateway: expected value at line 1 column 1; body starts "<html><body><h1>502 Bad Gateway</h1></body></html>"
== /login
fix: 200 OK text/html; charset=utf-8 from http://127.0.0.1:52433/login: expected value at line 1 column 1; body starts "<!doctype html><title>Sign in</title>"
== /drift
fix: 200 OK application/json from http://127.0.0.1:52433/drift: invalid type: string "2024", expected u16 at line 1 column 32; body starts "{\"id\":\"2410.01234\",\"year\":\"2024\"}"
== /ok
fix: Ok(Paper { id: "2410.01234", year: 2024 })
一眼就能分出三种情况:上游挂了、会话过期、接口字段变了。这和 json() 内部做的事一样(读完再 from_slice),代价相同,只是原文留在了自己手里。实际代码里换成自己的错误类型,留下状态码和截断的 body;截多长自己定,body 进日志之前要想清楚里面有没有不该记下的数据。
状态码可以更早判断。 先 error_for_status_ref(),失败时读 text() 当错误信息,成功了再解析,见 4xx、5xx 也是 Ok。这样 /gateway 这一类在解析之前就被拦下,剩下的 Decode 错误基本都是接口格式问题。
日志打印整条链。 至少把 source() 链打出来(用 anyhow 的话是 {:#}),不要只写 {e}。
怎么检查
- 搜日志:
error decoding response body后面没有原因,说明日志只打了Display。 - 搜代码:
.json::</.json().await前面没有状态码判断的调用点,502、登录页、限流页都会走到解码这一步。 - 写测试: 让测试服务器对同一个接口分别回 HTML 错误页和字段类型不对的 JSON,断言错误信息里能看到状态码和 body 开头。
小结
json()失败时,{e}只有「error decoding response body」和 URL;serde 的原因在下一层source()。- 状态码、
Content-Type、body 原文在json()里就丢掉了;HTML 错误页和登录页报的都是expected value at line 1 column 1。 - 0.13.4 起错误里才带 URL(#3026),0.12 上连 URL 都没有。
- 要诊断就自己
bytes()再serde_json::from_slice,失败时带上状态码、类型和 body 开头;最好先判断状态码。