Rust in Everything
基于 reqwest 0.13.5

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 开头;最好先判断状态码。

💬 关联讨论

发起讨论
加载中...