Rust in Everything
基于 tower-http 0.7.1

there are extra bytes after body has been decompressed

主线位置:作用点主线的第三个时刻。解码器报告压缩流结束以后,WrapBody 继续 poll 内层 body,又收到一个非空数据帧,就报这个错。三个版本里这段结尾逻辑各不相同,在 响应解压 里用内存里的帧序列跑过;这一页换成真实的 TCP 连接,看同样的字节在网络上什么时候报错、什么时候静默截断。

flowchart TB
  up["上游:两段 gzip 首尾相接"] --> how{"第二段和第一段在同一帧吗"}
  how -->|"同一个 chunk"| same["解码器读完第一段就结束,缓冲里剩下的没人看"]
  same --> trunc["静默:只得到 hello"]
  how -->|"单独一个 chunk"| sep["解码器结束后又来一个非空帧"]
  sep --> err["报错:there are extra bytes ..."]

复现

一个手写的 HTTP/1.1 服务器,回 Content-Encoding: gzip、分块传输,body 是两段 gzip(hello 和 world)首尾相接。/one 把两段放进一个 chunk,/split 分两个 chunk、中间停 50 ms。客户端两个:hyper-util 的 legacy Client 外面套 tower-http 0.7.1 的 DecompressionLayer,以及打开了 gzip 的 reqwest 0.12。

tower-http = { version = "=0.7.1", features = ["decompression-gzip"] }
tower = { version = "=0.5.3", features = ["util"] }
hyper-util = { version = "=0.1.21", features = ["client-legacy", "http1", "tokio"] }
http = "1"
http-body-util = "0.1"
bytes = "1"
tokio = { version = "=1.54.1", features = ["macros", "rt", "net", "io-util", "time"] }
reqwest = { version = "0.12", default-features = false, features = ["gzip"] }
flate2 = "1"
use bytes::Bytes;
use http_body_util::{BodyExt, Empty};
use hyper_util::{client::legacy::Client, rt::TokioExecutor};
use std::{error::Error, io::Write, time::Duration};
use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::TcpListener};
use tower::{ServiceBuilder, ServiceExt};
use tower_http::decompression::DecompressionLayer;

fn gzip(data: &[u8]) -> Vec<u8> {
  let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
  e.write_all(data).unwrap();
  e.finish().unwrap()
}

fn chunk(data: &[u8]) -> Vec<u8> {
  let mut v = format!("{:x}\r\n", data.len()).into_bytes();
  v.extend_from_slice(data);
  v.extend_from_slice(b"\r\n");
  v
}

// 每个请求都回两段拼接的 gzip;/split 时两段各占一个 chunk,中间停一下
async fn serve(listener: TcpListener) {
  loop {
    let (mut sock, _) = listener.accept().await.unwrap();
    tokio::spawn(async move {
      let mut buf = [0u8; 4096];
      let n = sock.read(&mut buf).await.unwrap();
      let split = String::from_utf8_lossy(&buf[..n]).contains("GET /split");
      let (a, b) = (gzip(b"hello "), gzip(b"world"));
      sock.write_all(b"HTTP/1.1 200 OK\r\ncontent-encoding: gzip\r\ntransfer-encoding: chunked\r\nconnection: close\r\n\r\n").await.unwrap();
      if split {
        sock.write_all(&chunk(&a)).await.unwrap();
        sock.flush().await.unwrap();
        tokio::time::sleep(Duration::from_millis(50)).await;
        sock.write_all(&chunk(&b)).await.unwrap();
      } else {
        sock.write_all(&chunk(&[a, b].concat())).await.unwrap();
      }
      sock.write_all(b"0\r\n\r\n").await.unwrap();
    });
  }
}

// 把错误链拼成一行
fn chain(e: &dyn Error) -> String {
  let mut s = e.to_string();
  let mut cur = e.source();
  while let Some(c) = cur {
    s.push_str(&format!(" <- {c}"));
    cur = c.source();
  }
  s
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
  let addr = listener.local_addr().unwrap();
  tokio::spawn(serve(listener));

  let hyper = Client::builder(TokioExecutor::new()).build_http::<Empty<Bytes>>();
  let reqwest = reqwest::Client::builder().no_proxy().build().unwrap();
  for path in ["/one", "/split"] {
    let url = format!("http://{addr}{path}");
    let svc = ServiceBuilder::new().layer(DecompressionLayer::new()).service(hyper.clone());
    let res = svc.oneshot(http::Request::get(&url).body(Empty::new()).unwrap()).await.unwrap();
    match res.into_body().collect().await {
      Ok(b) => println!("tower-http 0.7.1 {path:<6} -> {:?}", b.to_bytes()),
      Err(e) => println!("tower-http 0.7.1 {path:<6} -> error: {}", chain(&*e)),
    }
    let res = reqwest.get(&url).send().await.unwrap();
    match res.text().await {
      Ok(t) => println!("reqwest 0.12    {path:<6} -> {t:?}"),
      Err(e) => println!("reqwest 0.12    {path:<6} -> error: {}", chain(&e)),
    }
  }
}

M2 Max、rustc 1.99.0,flate2 1.1.10,跑 3 次输出相同:

tower-http 0.7.1 /one   -> b"hello "
reqwest 0.12    /one   -> "hello "
tower-http 0.7.1 /split -> error: there are extra bytes after body has been decompressed
reqwest 0.12    /split -> error: error decoding response body <- there are extra bytes after body has been decompressed
  • 同样的字节,两种结局。/one 只拿到 hello ,没有任何错误;/split 报错。hyper 把每个 chunk 交成一个数据帧,第二段 gzip 是落在第一帧里还是单独成帧,决定了它是被解码器的缓冲吞掉,还是在解码器结束后作为「多出来的帧」被发现。网络上的分帧调用方控制不了,所以同一个上游可能时好时坏。
  • reqwest 报的是另一句话。reqwest 0.12.28 的 gzip feature 就是打开 tower-http/decompression-gzip,它内部用的正是这段代码(依赖树里是 tower-http 0.6.11,这个检查 0.6.11 也有)。reqwest 把 body 错误包成 Kind::Decode,Display 只写 error decoding response body,原文要顺着 source() 才看得到。只打印 {e} 的日志里看不到 gzip 的字样,见 reqwest 卷 error sending request for url。

出处

compression_utils.rs 第 249 行,在 WrapBody::poll_frame 解码器读完以后的分支里:

            Some(Ok(frame)) => match frame.into_data() {
                // Payload after the decompressor reported end-of-stream means
                // the body and the compressed stream disagree about where the
                // content ends.
                Ok(data) if data.has_remaining() => Poll::Ready(Some(Err(
                    "there are extra bytes after body has been decompressed".into(),
                ))),

gzip 解码器没开 multiple_members(0.6.8 的 #621 关掉的),读完第一段就报告结束。这个检查只能看到「解码器结束之后才到达的帧」,同一帧里剩下的字节已经被读进了解码器的缓冲,WrapBody 不再检查它们。0.7.0 曾经把这个检查弄丢(非空帧原样转发),0.7.1 的 #712 加回来,经过见 响应解压 的「结尾:三次改动」。

怎么办

  • 先确认是不是上游的问题:把原始字节存下来(关掉客户端的自动解压),用 zcat 之类的命令行工具解一遍,它会把拼接的几段都解出来,和客户端拿到的长度一比就知道。HTTP 里一个 Content-Encoding: gzip 的 body 通常只有一段(#621 关掉多段支持的理由就是「HTTP context only uses one member」),出现多段多半是上游把几份压缩数据直接拼在了一起。修上游最干净。
  • 改不了上游、又确实要多段:在客户端关掉自动解压(tower-http 不套 DecompressionLayer;reqwest 用 .no_gzip()),拿到原始字节后用支持多段的解码器(flate2 的 MultiGzDecoder)自己解。
  • 静默截断那一半最危险:只有第二段恰好单独成帧时才有错误。对完整性有要求的下载,比对长度或校验和。

小结

  • 解码器读完第一段 gzip 后,内层 body 又交出非空数据帧,就报 there are extra bytes after body has been decompressed;同一帧里的多余字节则被静默丢掉。
  • 在真实连接上,结果取决于上游怎么分 chunk;reqwest 0.12 内部是同一段代码,错误显示为 error decoding response body,原文在 source() 里。
  • 修上游,或者关掉自动解压、自己用多段解码器。

💬 关联讨论

发起讨论
加载中...