From 58957ae4dabb487a7cf534946f4e660506c3e83a Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 16 Oct 2026 16:39:00 -0400 Subject: [PATCH] Close the connection after an ambiguous Transfer-Encoding RFC 9112 6.1 has a server close the connection after answering a request with both Transfer-Encoding and Content-Length, or with Transfer-Encoding in HTTP/1.0. Close it, and send Connection: close whenever the connection closes after a response. --- httplib.h | 17 +++++++++++++---- test/test.cc | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/httplib.h b/httplib.h index 9d15c9f8..ca2913e5 100644 --- a/httplib.h +++ b/httplib.h @@ -14761,6 +14761,13 @@ Server::process_request(Stream &strm, const std::string &remote_addr, return write_response(strm, close_connection, req, res); } + // RFC 9112 §6.1: answer a request that also has a Content-Length, or an + // HTTP/1.0 one, by its Transfer-Encoding, but close the connection after. + if (req.has_header("Transfer-Encoding") && + (req.has_header("Content-Length") || req.version == "HTTP/1.0")) { + connection_closed = true; + } + // RFC 9112 §3.2: Reject more than one Host field line or an invalid Host // value, so that every recipient of the request sees the same authority. if (req.get_header_value_count("Host") > 1 || @@ -14990,6 +14997,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr, } } #endif + // The response says so when this connection closes after it. + auto close = close_connection || connection_closed; auto ret = false; if (routed) { if (res.status == -1) { @@ -15023,17 +15032,17 @@ Server::process_request(Stream &strm, const std::string &remote_addr, } if (file_open_error) { - ret = write_response(strm, close_connection, req, res); + ret = write_response(strm, close, req, res); } else if (detail::range_error(req, res)) { detail::clear_content(res); res.status = StatusCode::RangeNotSatisfiable_416; - ret = write_response(strm, close_connection, req, res); + ret = write_response(strm, close, req, res); } else { - ret = write_response_with_content(strm, close_connection, req, res); + ret = write_response_with_content(strm, close, req, res); } } else { if (res.status == -1) { res.status = StatusCode::NotFound_404; } - ret = write_response(strm, close_connection, req, res); + ret = write_response(strm, close, req, res); } // Drain any unconsumed framed body to prevent request smuggling on diff --git a/test/test.cc b/test/test.cc index e5acf7f4..70d10354 100644 --- a/test/test.cc +++ b/test/test.cc @@ -10836,6 +10836,29 @@ TEST(ServerRequestParsingTest, ExcessiveWhitespaceInUnparsableHeaderLine) { "\r\n"); } +TEST(ServerRequestParsingTest, AmbiguousTransferEncodingClosesConnection) { + // RFC 9112 §6.1 + const char *requests[] = { + "PUT /put_hi HTTP/1.1\r\n" + "Content-Length: 0\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "0\r\n\r\n", + "PUT /put_hi HTTP/1.0\r\n" + "Connection: keep-alive\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "0\r\n\r\n", + }; + + for (auto request : requests) { + std::string out; + test_raw_request(request, &out); + EXPECT_EQ("HTTP/1.1 200 OK", out.substr(0, 15)) << request; + EXPECT_NE(std::string::npos, out.find("Connection: close")) << request; + } +} + TEST(ServerRequestParsingTest, InvalidFirstChunkLengthInRequest) { std::string out;