message head is too large
主线位置:两条主线上「读消息头」这一步。服务端主线里是
Conn::poll_read_head读请求头,客户端主线里是同一个函数读响应头;两边都由Buffered::parse一边读一边查长度,超了就是这个错误。
flowchart TB readio["读一次 socket"] --> parse["httparse 解析"] parse -->|"头完整"| full["parsed_len 大于 max_header_size?"] parse -->|"没收全"| part["缓冲 ≥ max_header_size?"] part -->|否| buf["缓冲 ≥ max_buf_size?"] buf -->|否| readio part -->|是| toolarge["Parse::TooLarge"] buf -->|是| toolarge full -->|是| toolarge toolarge -->|服务端| s431["回 431,关连接"] toolarge -->|客户端| cerr["请求返回 Err"]
「message head is too large」是 Parse::TooLarge 的 Display。服务端遇到它会自动回 431 Request Header Fields Too Large;客户端遇到它,这次请求直接失败。机制在读写缓冲和解析与编码里讲过,这一页从两端各复现一次,看清楚哪个上限在起作用、对端看到的是什么。
复现:服务端回 431
用裸 TcpStream 往 hyper 的 HTTP/1 服务端发一个带超长 x-big 头的请求,读回全部字节,换几种配置:
hyper = { version = "=1.12.0", features = ["server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
http-body-util = "=0.1.5"
bytes = "1"
tokio = { version = "=1.54.1", features = ["full"] }
use std::convert::Infallible;
use bytes::Bytes;
use http_body_util::Full;
use hyper::body::Incoming;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
async fn handler(_: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
Ok(Response::new(Full::new(Bytes::from("ok"))))
}
// A request whose head is about `head_kib` KiB: one big `x-big` header.
fn request(head_kib: usize) -> Vec<u8> {
let filler = "a".repeat(head_kib * 1024 - 40);
format!("GET / HTTP/1.1\r\nhost: x\r\nconnection: close\r\nx-big: {filler}\r\n\r\n").into_bytes()
}
async fn run(label: &str, head_kib: usize, cfg: impl Fn(&mut http1::Builder)) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let mut builder = http1::Builder::new();
cfg(&mut builder);
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
builder.serve_connection(TokioIo::new(stream), service_fn(handler)).await.map_err(|e| e.to_string())
});
let mut client = TcpStream::connect(addr).await.unwrap();
let req = request(head_kib);
let write = client.write_all(&req).await;
// read until EOF or error, keeping what arrived before an error
let mut resp = Vec::new();
let mut buf = [0u8; 4096];
let end = loop {
match client.read(&mut buf).await {
Ok(0) => break "EOF".to_string(),
Ok(n) => resp.extend_from_slice(&buf[..n]),
Err(e) => break format!("Err({:?})", e.kind()),
}
};
let text = String::from_utf8_lossy(&resp);
let first = text.lines().next().unwrap_or("");
println!("== {label}: {} byte head", req.len());
println!("client: write {:?}, read {} bytes, then {end}", write.map(|_| "ok"), resp.len());
println!("client: first line {first:?}");
println!("server: {:?}", server.await.unwrap());
}
#[tokio::main]
async fn main() {
run("default", 12, |_| {}).await;
run("default", 500, |_| {}).await;
run("max_header_size(16 KiB)", 20, |b| {
b.max_header_size(16 * 1024);
})
.await;
run("max_header_size(1 MiB)", 500, |b| {
b.max_header_size(1024 * 1024);
})
.await;
run("max_header_size(1 MiB) + max_buf_size(1 MiB)", 500, |b| {
b.max_header_size(1024 * 1024).max_buf_size(1024 * 1024);
})
.await;
}
M2 Max、rustc 1.99.0、--release,其中一次的输出:
== default: 12303 byte head
client: write Ok("ok"), read 96 bytes, then EOF
client: first line "HTTP/1.1 200 OK"
server: Ok(())
== default: 512015 byte head
client: write Ok("ok"), read 123 bytes, then Err(ConnectionReset)
client: first line "HTTP/1.1 431 Request Header Fields Too Large"
server: Err("message head is too large")
== max_header_size(16 KiB): 20495 byte head
client: write Ok("ok"), read 123 bytes, then EOF
client: first line "HTTP/1.1 431 Request Header Fields Too Large"
server: Err("message head is too large")
== max_header_size(1 MiB): 512015 byte head
client: write Ok("ok"), read 123 bytes, then Err(ConnectionReset)
client: first line "HTTP/1.1 431 Request Header Fields Too Large"
server: Err("message head is too large")
== max_header_size(1 MiB) + max_buf_size(1 MiB): 512015 byte head
client: write Ok("ok"), read 96 bytes, then EOF
client: first line "HTTP/1.1 200 OK"
server: Ok(())
又连跑 10 次:状态行、字节数、服务端的结果每次相同;两个 500 KiB 的 431 之后是 ConnectionReset 还是 EOF 会变,20 次里 18 次是 ConnectionReset。
- 默认配置,12 KiB:正常。默认的头部上限其实是读缓冲上限
max_buf_size,417 792 字节(约 408 KiB)。 - 默认配置,500 KiB:431,服务端的
Connection以message head is too large结束。 max_header_size(16 KiB),20 KiB:431。这是 1.12.0 新加的、专门管头部的上限。max_header_size(1 MiB),500 KiB:还是 431。max_header_size设得再大,max_buf_size也照样检查,先碰到哪个算哪个。- 两个都调到 1 MiB:500 KiB 的头通过。想放宽,两个都要放宽;想收紧,设
max_header_size就够。
问: 同样是 431,为什么有时候客户端读完响应是 EOF,有时候是 ConnectionReset?
答: 看服务端关 socket 时接收缓冲里还有没有没读的字节。20 KiB 那一项,整个请求都被读进来了,服务端写完 431 正常关闭,客户端读到 FIN,就是 EOF。500 KiB 那两项,服务端读满 417 792 字节就停了,剩下的八九十 KiB 还在内核的接收缓冲里(或者还在路上),这时关闭 socket,内核发的是 RST 而不是 FIN,于是客户端读完 431 之后接着收到 ConnectionReset。偶尔出现 EOF,是 RST 到达前客户端已经读到了结尾。431 的字节在 RST 之前已经送到,所以读到了;但如果客户端还在写、还没开始读,它可能先在写上收到错误,根本看不到 431,下一节的客户端实验就碰到了这种情况。
源码:两个上限,一个循环
服务端和客户端读消息头走同一个 Buffered::parse(循环的完整代码在读写缓冲)。头没收全的分支里,先查 max_header_size,再查 max_buf_size:
let curr_len = self.read_buf.len();
if let Some(max_header_size) = parse_ctx.h1_max_header_size {
if curr_len >= max_header_size {
debug!("max_header_size ({}) reached, closing", max_header_size);
return Poll::Ready(Err(crate::Error::new_too_large()));
}
}
let max = self.read_buf_strategy.max();
if curr_len >= max {
debug!("max_buf_size ({}) reached, closing", max);
return Poll::Ready(Err(crate::Error::new_too_large()));
}
头一次就收全的时候走不到这里,所以解析成功后还要再查一次:服务端在 Server::parse,客户端在 Client::parse,都是 parsed_len > max_header_size 就 Parse::TooLarge。max_buf_size 不需要第二道检查,因为每次读的大小已经被限制在「上限减去已有字节」以内,缓冲不会在头完整之前越过上限。
两个选项的区别:
max_buf_size | max_header_size | |
|---|---|---|
| 默认 | DEFAULT_MAX_BUFFER_SIZE = 8192 + 4096 × 100 = 417 792 | 未设(None) |
| 下限 | 8192,小于它 builder 直接 panic | 无 |
| 管什么 | 整个读缓冲;同时也是写缓冲的上限 | 起始行加所有头的字节数;也是 chunked trailers 的上限 |
| 服务端设置 | http1::Builder::max_buf_size | http1::Builder::max_header_size |
| 客户端设置 | http1::Builder::max_buf_size | http1::Builder::max_header_size |
实际生效的头部上限是两者中较小的一个。max_buf_size 管的东西多,拿它当头部上限是「顺带」的;1.12.0 加 max_header_size 就是为了把两件事分开(来历见服务端 Builder 与 Connection 的冷知识)。
服务端拿到 Parse::TooLarge 之后,Conn::on_parse_error 问 Server::on_error 要一个响应头,TooLarge 对应 431,写出去以后连接以这个错误结束。服务根本没被调用,所以 431 不经过任何中间件,日志、指标里也看不到这次请求,只能在 serve_connection 返回的错误里看到。
客户端那一边
客户端会在两种情况下碰到它:对方回了一个太大的响应头,这时报错的是客户端自己;或者客户端发了一个太大的请求头,被服务端拒绝。第一种用一个手写的服务端回超长响应头,第二种让 hyper 客户端对着 hyper 服务端发:
hyper = { version = "=1.12.0", features = ["client", "server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["client-legacy", "http1", "tokio"] }
http-body-util = "=0.1.5"
bytes = "1"
tokio = { version = "=1.54.1", features = ["full"] }
use std::convert::Infallible;
use bytes::Bytes;
use http_body_util::{Empty, Full};
use hyper::body::Incoming;
use hyper::client::conn::http1 as client_h1;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::client::legacy::Client;
use hyper_util::rt::{TokioExecutor, TokioIo};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
// Raw server: reads one request head, answers with a `head_kib` KiB head.
async fn big_head_server(head_kib: usize) -> std::net::SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let (mut s, _) = listener.accept().await.unwrap();
tokio::spawn(async move {
let mut buf = vec![0u8; 4096];
let _ = s.read(&mut buf).await;
let filler = "a".repeat(head_kib * 1024 - 60);
let head = format!("HTTP/1.1 200 OK\r\ncontent-length: 2\r\nx-big: {filler}\r\n\r\nok");
let _ = s.write_all(head.as_bytes()).await;
let _ = s.read(&mut buf).await; // keep the socket open until the client goes
});
}
});
addr
}
fn chain(e: &(dyn std::error::Error + 'static)) -> String {
let mut out = format!("{e}");
let mut cur = e.source();
while let Some(s) = cur {
out.push_str(&format!(" <- {s}"));
cur = s.source();
}
out
}
async fn conn_client(label: &str, head_kib: usize, max_header_size: Option<usize>) {
let addr = big_head_server(head_kib).await;
let io = TokioIo::new(TcpStream::connect(addr).await.unwrap());
let mut b = client_h1::Builder::new();
if let Some(m) = max_header_size {
b.max_header_size(m);
}
let (mut tx, conn) = b.handshake::<_, Empty<Bytes>>(io).await.unwrap();
let conn = tokio::spawn(conn);
let r = tx.send_request(Request::get("/").body(Empty::new()).unwrap()).await;
match r {
Ok(res) => println!("{label}, {head_kib} KiB response head: Ok({})", res.status()),
Err(e) => println!(
"{label}, {head_kib} KiB response head: Err({e}), is_parse_too_large={}",
e.is_parse_too_large()
),
}
drop(tx);
println!(" conn task: {:?}", conn.await.unwrap().map_err(|e| e.to_string()));
}
async fn legacy_client(head_kib: usize) {
let addr = big_head_server(head_kib).await;
let client: Client<_, Empty<Bytes>> = Client::builder(TokioExecutor::new()).build_http();
match client.get(format!("http://{addr}/").parse().unwrap()).await {
Ok(res) => println!("legacy Client, {head_kib} KiB response head: Ok({})", res.status()),
Err(e) => println!("legacy Client, {head_kib} KiB response head: Err({})", chain(&e)),
}
}
async fn handler(_: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
Ok(Response::new(Full::new(Bytes::from("ok"))))
}
// A hyper client sends a `head_kib` KiB request head to a hyper server.
async fn send_big_request(label: &str, head_kib: usize, cfg: impl Fn(&mut http1::Builder)) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let mut builder = http1::Builder::new();
cfg(&mut builder);
tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let _ = builder.serve_connection(TokioIo::new(stream), service_fn(handler)).await;
});
let io = TokioIo::new(TcpStream::connect(addr).await.unwrap());
let (mut tx, conn) = client_h1::handshake::<_, Empty<Bytes>>(io).await.unwrap();
tokio::spawn(conn);
let big = "a".repeat(head_kib * 1024);
let req = Request::get("/").header("host", "x").header("x-big", big).body(Empty::new()).unwrap();
match tx.send_request(req).await {
Ok(res) => println!("{label}: client sent {head_kib} KiB head, got Ok({})", res.status()),
Err(e) => println!("{label}: client sent {head_kib} KiB head, got Err({})", chain(&e)),
}
}
#[tokio::main]
async fn main() {
println!("-- part 1: big response heads");
conn_client("conn::http1 default", 12, None).await;
conn_client("conn::http1 default", 500, None).await;
conn_client("conn::http1 max_header_size(8 KiB)", 12, Some(8 * 1024)).await;
legacy_client(500).await;
println!("-- part 2: big request heads");
send_big_request("server max_header_size(16 KiB)", 20, |b| {
b.max_header_size(16 * 1024);
})
.await;
send_big_request("server default", 500, |_| {}).await;
send_big_request("server default", 2048, |_| {}).await;
}
其中一次的输出:
-- part 1: big response heads
conn::http1 default, 12 KiB response head: Ok(200 OK)
conn task: Ok(())
conn::http1 default, 500 KiB response head: Err(message head is too large), is_parse_too_large=true
conn task: Ok(())
conn::http1 max_header_size(8 KiB), 12 KiB response head: Err(message head is too large), is_parse_too_large=true
conn task: Ok(())
legacy Client, 500 KiB response head: Err(client error (SendRequest) <- message head is too large)
-- part 2: big request heads
server max_header_size(16 KiB): client sent 20 KiB head, got Ok(431 Request Header Fields Too Large)
server default: client sent 500 KiB head, got Ok(431 Request Header Fields Too Large)
server default: client sent 2048 KiB head, got Err(error writing a body to connection <- Connection reset by peer (os error 54))
连跑 20 次,只有最后一行会变:12 次是 Ok(431 ...),8 次是上面的 Err。其余各行每次相同。
响应头太大:错误交给发请求的人,is_parse_too_large() 为真;legacy Client 再包一层 client error (SendRequest)(见 hyper::Error)。连接任务本身却以 Ok(()) 结束。原因在 Dispatcher::poll_read_head 的错误分支:
Some(Err(err)) => {
debug!("read_head error: {}", err);
self.dispatch.recv_msg(Err(err))?;
// if here, the dispatcher gave the user the error
// somewhere else. we still need to shutdown, but
// not as a second error.
self.close();
Poll::Ready(Ok(()))
}
客户端的 recv_msg 手里有回调时,把错误交给回调就返回 Ok,连接随后关闭。同一个错误不报两次,所以只盯着连接任务的返回值是看不到它的。
请求头太大:服务端回 431,hyper 客户端大多数时候能拿到这个 431,因为客户端的 Dispatcher 每一圈先读后写,请求还没写完时响应就能被读到并交出去。2 MiB 那一行的请求比服务端读走的多出 1.6 MiB,客户端还在写,服务端已经关了 socket,写操作先碰上 RST,于是请求以写错误结束,431 没被读到。注意错误文字是「error writing a body to connection」,可这次连请求头都没写完:Dispatcher::poll_flush 把所有刷新失败一律包成 BodyWrite,不区分写的是头还是 body。
客户端想调大响应头上限,conn::http1::Builder 两个选项都有;legacy Client 的 Builder 只有 http1_max_buf_size,没有转发 max_header_size。再往上一层,reqwest 0.12.28(本站锁的版本)和 0.13.5 的 ClientBuilder 里,http1_* 系列方法只有大小写、容错、http1_only 这些(0.13.5 多一个 http1_max_headers),读缓冲和头部大小都设不了,响应头的上限就是默认的约 408 KiB。
auto::Builder 设不了 max_header_size
auto::Builder 的 HTTP/1 选项是 Http1Builder 逐个转发给 hyper 的,0.1.21 没有转发 max_header_size(auto 一章对过两边的方法列表)。照着 http1::Builder 写:
use hyper_util::rt::TokioExecutor;
use hyper_util::server::conn::auto;
fn main() {
let mut builder = auto::Builder::new(TokioExecutor::new());
builder.http1().max_header_size(16 * 1024);
}
rustc 1.99.0:
error[E0599]: no method named `max_header_size` found for struct `Http1Builder<'a, E>` in the current scope
--> src/bin/auto_max_header.rs:6:19
|
6 | builder.http1().max_header_size(16 * 1024);
| ^^^^^^^^^^^^^^^
|
help: there is a method `max_headers` with a similar name
|
6 - builder.http1().max_header_size(16 * 1024);
6 + builder.http1().max_headers(16 * 1024);
|
编译器建议的 max_headers 是另一回事:它限制头的个数(默认 100),不是字节数。用 auto 时能动的只有 Http1Builder::max_buf_size:收紧时最小只能到 8192,而且它同时限制写缓冲。本地 hyper-util 仓库在 v0.1.21 之后的最新提交(64855f6)里也还没有这个转发。axum::serve 用的就是 auto::Builder::new,不给用户留配置入口,用 axum 的服务端头部上限就是默认的约 408 KiB;要别的值,得自己写 accept 循环。
HTTP/2:另一套上限
上面全是 HTTP/1。HTTP/2 的头经过 HPACK 压缩,不经过 Buffered,上限是 SETTINGS 里的 MAX_HEADER_LIST_SIZE,由 h2 执行。hyper 给两端设的默认值都是 16 KiB:服务端是 DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE,客户端是 DEFAULT_MAX_HEADER_LIST_SIZE,用 max_header_list_size 修改(auto::Builder 的 http2() 也转发了这一个)。默认的 HTTP/1 上限约 408 KiB,HTTP/2 只有 16 KiB,差了二十多倍。
hyper 的 HTTP/2 服务端和客户端,默认配置,依次发 12 KiB、20 KiB、1 KiB 的 x-big 头:
hyper = { version = "=1.12.0", features = ["client", "server", "http2"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
http-body-util = "=0.1.5"
bytes = "1"
tokio = { version = "=1.54.1", features = ["full"] }
use std::convert::Infallible;
use bytes::Bytes;
use http_body_util::{Empty, Full};
use hyper::body::Incoming;
use hyper::client::conn::http2 as client_h2;
use hyper::server::conn::http2;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::{TokioExecutor, TokioIo};
use tokio::net::{TcpListener, TcpStream};
async fn handler(req: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
println!(" handler called, x-big is {} bytes", req.headers()["x-big"].len());
Ok(Response::new(Full::new(Bytes::from("ok"))))
}
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let r = http2::Builder::new(TokioExecutor::new())
.serve_connection(TokioIo::new(stream), service_fn(handler))
.await;
println!("server conn: {:?}", r.map_err(|e| e.to_string()));
});
let io = TokioIo::new(TcpStream::connect(addr).await.unwrap());
let (mut tx, conn) = client_h2::handshake::<_, _, Empty<Bytes>>(TokioExecutor::new(), io).await.unwrap();
tokio::spawn(conn);
for kib in [12, 20, 1] {
let req = Request::get("http://x/")
.header("x-big", "a".repeat(kib * 1024))
.body(Empty::new())
.unwrap();
match tx.send_request(req).await {
Ok(res) => println!("{kib} KiB header: Ok({})", res.status()),
Err(e) => println!("{kib} KiB header: Err({e:?})"),
}
}
}
20 KiB 那一个请求,客户端拿到的是 Ok 还是 Err?服务端的处理函数会不会被调用?第三个请求还能在这条连接上发吗?答案在本页末尾。
修法
| 现象 | 谁的上限 | 怎么办 |
|---|---|---|
服务端日志里有 message head is too large,客户端收到 431 | 服务端 max_buf_size 或 max_header_size | 先查请求头为什么这么大(常见的是 Cookie 越积越多、把数据塞进 URL 或自定义头);确实要放宽,max_header_size 和 max_buf_size 一起调大 |
| 想收紧服务端头部上限,防止大头部占内存 | — | http1::Builder::max_header_size;用 auto::Builder 时只能用 max_buf_size(下限 8192) |
客户端请求返回 message head is too large,is_parse_too_large() 为真 | 客户端自己的上限 | conn::http1::Builder 两个都能调;legacy Client 用 http1_max_buf_size;reqwest 不能调 |
| 发大请求头时偶尔得到写错误而不是 431 | 服务端提前关了连接 | 把请求头做小,这类错误不要重试 |
| 同样的头 HTTP/1 正常、HTTP/2 被拒 | h2 的 MAX_HEADER_LIST_SIZE,默认 16 KiB | 服务端 http2().max_header_list_size(...);客户端 max_header_list_size / http2_max_header_list_size |
动脑筋答案
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
handler called, x-big is 12288 bytes
12 KiB header: Ok(200 OK)
20 KiB header: Ok(431 Request Header Fields Too Large)
handler called, x-big is 1024 bytes
1 KiB header: Ok(200 OK)
- 12 KiB:在 16 KiB 以内,正常。
- 20 KiB:客户端拿到的是
Ok,一个 431 响应。处理函数没有被调用:h2 0.4.20 在recv_headers里发现头块超过SETTINGS_MAX_HEADER_LIST_SIZE,源码注释写着「So, if peer is a server, we'll send a 431. In either case, an error is recorded, which will send a REFUSED_STREAM」。431 是 h2 自己回的,hyper 和服务都没见到这个请求。 - 1 KiB:同一个
SendRequest、同一条连接,正常。HTTP/2 拒掉的只是这一个流,连接不关;server conn: ...那一行没有打印,服务端连接一直开着。HTTP/1 回完 431 就得关连接,因为没读完的头字节还堵在流里,没法找到下一个请求从哪开始。
小结
- 「message head is too large」是
Parse::TooLarge:HTTP/1 的消息头在收全之前就让读缓冲到了max_header_size或max_buf_size,或者收全后超过max_header_size。服务端回 431 后关连接,服务不会被调用;客户端这次请求返回错误,连接任务本身以Ok(())结束。 - 默认的头部上限就是
max_buf_size,417 792 字节;max_header_size(1.12.0)只能把上限收紧,放宽要两个一起调。 - 服务端拒绝时没读完的字节会让关闭变成 RST:客户端多半读到 431 再收到 reset,请求头特别大时可能只看到写错误,hyper 报的是「error writing a body to connection」。
auto::Builder(因而axum::serve)、legacyClient、reqwest 都设不了max_header_size。- HTTP/2 的头部上限是 h2 的
MAX_HEADER_LIST_SIZE,hyper 两端默认 16 KiB,比 HTTP/1 小得多;超了由 h2 回 431 并拒掉这个流,连接继续可用。