请求解压
主线位置:作用点主线的第一个时刻,站在服务端一侧。
call时看请求的Content-Encoding,要么把请求 body 换成解码中的 body 交给内层,要么短路回 415;响应 future 完成时只做一件事:把响应 body 装箱。解码本身发生在 handler 读请求 body 的时候,用的是 共享内核 里的WrapBody。
flowchart TB
req["请求"] --> ce{"Content-Encoding"}
ce -->|没有| id1["Identity"]
ce -->|"identity(字节完全相同)"| id2["Identity,头保留"]
ce -->|"gzip / deflate / br / zstd 且已开启"| dec["删 Content-Encoding、Content-Length,body 套 WrapBody"]
ce -->|其他| pass{"pass_through_unaccepted"}
pass -->|true| id3["Identity,头保留"]
pass -->|false| r415["短路:415 + Accept-Encoding"]
id1 --> inner["内层服务:Request<DecompressionBody<B>>"]
id2 --> inner
id3 --> inner
dec --> inner
inner --> boxed["响应 body 装箱成 UnsyncBoxBody"]
r415 --> boxed
同一套零件,换个方向
RequestDecompression 的字段和响应解压一样,多一个开关:
pub struct RequestDecompression<S> {
pub(super) inner: S,
pub(super) accept: AcceptEncoding,
pub(super) pass_through_unaccepted: bool,
}
内层看到的请求 body 类型是 DecompressionBody<ReqBody>(Service 实现的约束 S: Service<Request<DecompressionBody<ReqBody>>, ...>),和响应解压交给调用方的是同一个类型,里面是同样的 BodyInner 和四个解码器,multiple_members 的设置、结尾逻辑、空 body 的处理都和 响应解压 一致。
call 也是逐字节匹配,只多了两条:
match entry.get().as_bytes() {
#[cfg(feature = "decompression-gzip")]
b"gzip" if self.accept.gzip() => {
entry.remove();
parts.headers.remove(header::CONTENT_LENGTH);
BodyInner::gzip(crate::compression_utils::WrapBody::new(
body,
CompressionLevel::default(),
))
}
// deflate、br、zstd 同样
b"identity" => BodyInner::identity(body),
_ if self.pass_through_unaccepted => BodyInner::identity(body),
_ => return ResponseFuture::unsupported_encoding(self.accept),
}
identity放行,但Content-Encoding: identity这个头留在请求里。- 其余一律视为「不支持」:大写的
GZIP、x-gzip、关掉的算法、gzip, br。响应解压遇到不认识的会原样放过,请求解压默认拒绝;pass_through_unaccepted(true)改成原样放过,头和字节都不动,交给 handler 自己处理。
415
不支持时走 Kind::Unsupported,这是 四件套 里的短路枚举。poll 造响应:
StateProj::Unsupported { accept } => {
let res = Response::builder()
.header(
header::ACCEPT_ENCODING,
accept
.to_header_value()
.unwrap_or(HeaderValue::from_static("identity")),
)
.status(StatusCode::UNSUPPORTED_MEDIA_TYPE)
.body(UnsyncBoxBody::from_inner(
Empty::new().map_err(Into::into).boxed_unsync(),
))
.unwrap();
Poll::Ready(Ok(res))
}
响应头里的 Accept-Encoding 告诉客户端服务端认哪些编码(RFC 7694 的用法),值同样来自 to_header_value;四种都关掉时写 identity。body 是空的。
响应一律装箱
短路的响应要和内层的响应放进同一个类型。别的短路模块要求 ResBody: Default,这里换了个办法,把两边都装进 tower-http 自己的 UnsyncBoxBody:
type Response = Response<UnsyncBoxBody<D, BoxError>>;
于是内层响应 body 要 Send + 'static,错误要能转成 BoxError,每个正常响应都多一次装箱。好处是外层的 CorsLayer、TimeoutLayer 拿到的 body 实现了 Default,不受内层影响;坏处是加不加这一层,响应类型不同,按条件加层时会碰到类型不一致,原因见 body 模块。CatchPanic 也是同样的取舍。
没有解压后上限
RequestDecompression 自己不限制解压出来多少字节。上限要靠 RequestBodyLimitLayer,而两层谁在外面,决定了限的是哪一边:
flowchart LR
subgraph a["Limit 在外"]
a1["Limit:看到压缩后的 Content-Length,额度套在压缩字节上"] --> a2["解压:删掉 Content-Length"] --> a3["handler:读到全部解压字节"]
end
subgraph b["Limit 在里"]
b1["解压:删掉 Content-Length"] --> b2["Limit:没有 Content-Length,额度套在解压后的字节上"] --> b3["handler:读到上限就报错"]
end
ServiceBuilder 里先写的在外层。下面先跑几种编码,再用 10 MiB 的零压成的 gzip 对比两种顺序(上限 1 MiB):
tower-http = { version = "=0.7.1", features = ["decompression-full", "limit"] }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
http = "1"
http-body = "1"
http-body-util = "0.1"
bytes = "1"
flate2 = "1"
use bytes::Bytes;
use http::{header, Request, Response};
use http_body_util::{BodyExt, Full};
use std::{convert::Infallible, io::Write};
use tower::{ServiceBuilder, ServiceExt};
use tower_http::{decompression::RequestDecompressionLayer, limit::RequestBodyLimitLayer};
// handler 读完请求 body,报告读到多少字节,以及它看到的两个头
async fn handler<B>(req: Request<B>) -> Result<Response<Full<Bytes>>, Infallible>
where
B: http_body::Body,
B::Error: std::fmt::Display,
{
let heads = format!("ce={:?} cl={:?}", req.headers().get(header::CONTENT_ENCODING), req.headers().get(header::CONTENT_LENGTH));
let msg = match req.into_body().collect().await {
Ok(c) => format!("{heads} read {} bytes", c.to_bytes().len()),
Err(e) => format!("{heads} error: {e}"),
};
Ok(Response::new(Full::from(msg)))
}
fn gzip(data: &[u8]) -> Bytes {
let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
e.write_all(data).unwrap();
Bytes::from(e.finish().unwrap())
}
fn post(ce: &str, body: Bytes) -> Request<Full<Bytes>> {
Request::post("/").header(header::CONTENT_ENCODING, ce).header(header::CONTENT_LENGTH, body.len()).body(Full::new(body)).unwrap()
}
// show(label, svc, req):发请求,打印状态码、响应的 Accept-Encoding 和响应正文
#[tokio::main(flavor = "current_thread")]
async fn main() {
let hello = gzip(b"hello world");
let dec = || ServiceBuilder::new().layer(RequestDecompressionLayer::new()).service_fn(handler);
show("gzip", dec(), post("gzip", hello.clone())).await;
show("identity", dec(), post("identity", Bytes::from("plain"))).await;
show("GZIP", dec(), post("GZIP", hello.clone())).await;
let no_br = ServiceBuilder::new().layer(RequestDecompressionLayer::new().no_br()).service_fn(handler);
show("br, no_br()", no_br, post("br", Bytes::from("whatever"))).await;
let pass = ServiceBuilder::new().layer(RequestDecompressionLayer::new().pass_through_unaccepted(true)).service_fn(handler);
show("GZIP, pass_through", pass, post("GZIP", hello.clone())).await;
let bomb = gzip(&vec![0u8; 10 * 1024 * 1024]);
println!("bomb: {} bytes compressed", bomb.len());
let outside = ServiceBuilder::new()
.layer(RequestBodyLimitLayer::new(1 << 20))
.layer(RequestDecompressionLayer::new())
.service_fn(handler);
show("limit outside decompression", outside, post("gzip", bomb.clone())).await;
let inside = ServiceBuilder::new()
.layer(RequestDecompressionLayer::new())
.layer(RequestBodyLimitLayer::new(1 << 20))
.service_fn(handler);
show("limit inside decompression", inside, post("gzip", bomb.clone())).await;
}
输出(M2 Max、rustc 1.99.0,flate2 1.1.10,跑 3 次相同):
gzip 200 OK accept-encoding=None "ce=None cl=None read 11 bytes"
identity 200 OK accept-encoding=None "ce=Some(\"identity\") cl=Some(\"5\") read 5 bytes"
GZIP 415 Unsupported Media Type accept-encoding=Some("zstd,gzip,deflate,br") ""
br, no_br() 415 Unsupported Media Type accept-encoding=Some("zstd,gzip,deflate") ""
GZIP, pass_through 200 OK accept-encoding=None "ce=Some(\"GZIP\") cl=Some(\"31\") read 31 bytes"
bomb: 10208 bytes compressed
limit outside decompression 200 OK accept-encoding=None "ce=None cl=None read 10485760 bytes"
limit inside decompression 200 OK accept-encoding=None "ce=None cl=None error: length limit exceeded"
gzip:handler 看不到Content-Encoding和Content-Length,读到解压后的 11 字节。identity:两个头都还在。GZIP:大写不认,handler 没被调用,415 里列出四种编码;关掉 br 后列表少了br。pass_through:handler 拿到原样的 31 字节 gzip 数据和原来的头。- 10 MiB 的零压成 10208 字节,压缩比约一千比一。Limit 在外:
RequestBodyLimit看到Content-Length: 10208,小于 1 MiB,放行并把额度收紧到 10208(额度取上限和Content-Length中较小的那个,见 axum 卷 其余常用中间件);这个额度套在压缩字节上,解压层在它里面,handler 读到了全部 10485760 字节。Limit 在里:解压层先删掉了Content-Length,RequestBodyLimit只能按 1 MiB 的上限给解压后的流套Limited,handler 读到第 1 MiB 之后拿到length limit exceeded。
这里的 200 是 handler 自己把错误写进了正文;在 axum 里,Bytes、Json 这类提取器会把 length limit exceeded 变成 413。
问: 既然 Limit 在里才有用,为什么不两层都放?
答: 可以,两层限的是两件事:外层限网络上收多少压缩字节,Content-Length 超限时当场回 413,handler 和解码器都不会运行;里层限解压后展开多少。只放里层时,声明了一个巨大 Content-Length 的请求也会先进到 handler,直到读出的解压字节超限才失败。
小结
RequestDecompression复用DecompressionBody和WrapBody,内层看到的请求 body 类型是DecompressionBody<B>。Content-Encoding按字节匹配:四种已开启的编码解压并删掉Content-Encoding、Content-Length;identity放行且保留头;其余默认短路 415,响应带Accept-Encoding列出支持的编码,pass_through_unaccepted(true)改成原样放行。- 短路和正常响应都装箱成
UnsyncBoxBody<D, BoxError>,不要求Default,但要求内层 bodySend + 'static。 - 没有解压后大小上限。
RequestBodyLimitLayer在外层时限的是压缩字节,10208 字节的 gzip 让 handler 读到 10 MiB;放在里层才限住解压后的字节。