Rust in Everything
基于 hyper 1.12.0

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 就够。
📝NOTE
没有蠢问题

问: 同样是 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_sizemax_header_size
默认DEFAULT_MAX_BUFFER_SIZE = 8192 + 4096 × 100 = 417 792未设(None)
下限8192,小于它 builder 直接 panic无
管什么整个读缓冲;同时也是写缓冲的上限起始行加所有头的字节数;也是 chunked trailers 的上限
服务端设置http1::Builder::max_buf_sizehttp1::Builder::max_header_size
客户端设置http1::Builder::max_buf_sizehttp1::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,差了二十多倍。

❗IMPORTANT
动脑筋:同一个 20 KiB 的头,换成 HTTP/2 会怎样?

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)、legacy Client、reqwest 都设不了 max_header_size。
  • HTTP/2 的头部上限是 h2 的 MAX_HEADER_LIST_SIZE,hyper 两端默认 16 KiB,比 HTTP/1 小得多;超了由 h2 回 431 并拒掉这个流,连接继续可用。

💬 关联讨论

发起讨论
加载中...