From bae89e037c14dabb571ea850a48627a9ac745c13 Mon Sep 17 00:00:00 2001 From: Adam Winstanley Date: Thu, 26 Mar 2026 10:14:39 -0700 Subject: [PATCH 1/2] fix(http1): allow keep-alive for chunked requests with trailers When a chunked request body included trailers, poll_read_body incorrectly transitioned to Reading::Closed instead of Reading::KeepAlive. This prevented connection reuse for any request that sent trailers, even though trailers signal body completion just like a final data frame at EOF. Closes #4039 --- src/proto/h1/conn.rs | 3 ++- tests/server.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/proto/h1/conn.rs b/src/proto/h1/conn.rs index 3d71ed5bc5..3373021bf7 100644 --- a/src/proto/h1/conn.rs +++ b/src/proto/h1/conn.rs @@ -393,7 +393,8 @@ where }; (reading, Poll::Ready(maybe_frame)) } else if frame.is_trailers() { - (Reading::Closed, Poll::Ready(Some(Ok(frame)))) + debug!("incoming body completed with trailers"); + (Reading::KeepAlive, Poll::Ready(Some(Ok(frame)))) } else { trace!("discarding unknown frame"); (Reading::Closed, Poll::Ready(None)) diff --git a/tests/server.rs b/tests/server.rs index 1cd0eba570..9570171a73 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -2967,6 +2967,61 @@ fn http1_trailer_recv_fields() { ); } +#[test] +fn http1_trailer_recv_keep_alive() { + let server = serve(); + server + .reply() + .header("content-length", "2") + .body(b"ok"); + let mut req = connect(server.addr()); + + // First request: chunked POST with trailers + req.write_all( + b"\ + POST / HTTP/1.1\r\n\ + trailer: chunky-trailer\r\n\ + host: example.domain\r\n\ + transfer-encoding: chunked\r\n\ + \r\n\ + 5\r\n\ + hello\r\n\ + 0\r\n\ + chunky-trailer: header data\r\n\ + \r\n\ + ", + ) + .expect("writing 1"); + + assert_eq!(server.body(), b"hello"); + + let trailers = server.trailers(); + assert_eq!( + trailers.get("chunky-trailer"), + Some(&"header data".parse().unwrap()) + ); + + read_until(&mut req, |buf| buf.ends_with(b"ok")).expect("reading 1"); + + // Second request: reuse the same connection to verify keep-alive + let quux = b"zar quux"; + server + .reply() + .header("content-length", quux.len().to_string()) + .body(quux); + req.write_all( + b"\ + GET /quux HTTP/1.1\r\n\ + Host: example.domain\r\n\ + Connection: close\r\n\ + \r\n\ + ", + ) + .expect("writing 2"); + + read_until(&mut req, |buf| buf.ends_with(quux)).expect("reading 2"); +} + // ------------------------------------------------- // the Server that is used to run all the tests with // ------------------------------------------------- From 4962fa2d4c307c5568453556fe193eaab88d3b86 Mon Sep 17 00:00:00 2001 From: Adam Winstanley Date: Thu, 26 Mar 2026 10:17:18 -0700 Subject: [PATCH 2/2] style(http1): fix rustfmt formatting in trailer keep-alive test --- tests/server.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/server.rs b/tests/server.rs index 9570171a73..651fbdf40d 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -2970,10 +2970,7 @@ fn http1_trailer_recv_fields() { #[test] fn http1_trailer_recv_keep_alive() { let server = serve(); - server - .reply() - .header("content-length", "2") - .body(b"ok"); + server.reply().header("content-length", "2").body(b"ok"); let mut req = connect(server.addr()); // First request: chunked POST with trailers