From 7bf66db6ca4921ddc567c71403e6ca5dc5b50e9e Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 16 Oct 2026 23:52:15 -0400 Subject: [PATCH] Close the connection after a response body with no length A content provider without a length or chunked coding sends a body that only the end of the connection delimits (RFC 9112 6.3), yet the response offered keep-alive, so a keep-alive client waited for the server's idle timeout. Send Connection: close and close after it, as Go's net/http does. A Content-Length set by the handler is still dropped; the overload that takes a length frames such a body. --- httplib.h | 11 +++++++++-- test/test.cc | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/httplib.h b/httplib.h index 3c67adbe..a49cacbb 100644 --- a/httplib.h +++ b/httplib.h @@ -12091,8 +12091,9 @@ inline bool is_partial_content(const Request &req, const Response &res) { } // Sets the Content-Length or Transfer-Encoding framing the body that -// Server::write_response_core() writes. Only the library sets them: one from a -// handler would go out next to it, or describe some other body. +// Server::write_response_core() writes, or closes the connection after a body +// that has neither. Only the library sets them: one from a handler would go +// out next to it, or describe some other body. inline void set_framing_headers(const Request &req, Response &res, const std::string &boundary, const std::string &content_type) { @@ -12109,6 +12110,12 @@ inline void set_framing_headers(const Request &req, Response &res, if (res.content_length_ == 0) { if (res.is_chunked_content_provider_) { res.set_header("Transfer-Encoding", "chunked"); + } else if (req.method != "HEAD") { + // RFC 9112 6.3: a body with neither ends where the connection does. + res.headers.erase("Keep-Alive"); + if (!has_header_token(res.headers, "Connection", "close")) { + res.set_header("Connection", "close"); + } } return; } diff --git a/test/test.cc b/test/test.cc index 8fd9bbc0..6b308168 100644 --- a/test/test.cc +++ b/test/test.cc @@ -5756,6 +5756,43 @@ TEST(ResponseFramingTest, PostRoutingHandlerChangesAreFramed) { EXPECT_FALSE(res->has_header("Transfer-Encoding")); } +TEST(ResponseFramingTest, BodyWithoutLengthEndsWithTheConnection) { + Server svr; + + svr.Get("/", [](const Request & /*req*/, Response &res) { + res.set_header("Content-Length", "3"); + res.set_content_provider("text/plain", [](size_t, DataSink &sink) { + sink.write("abc", 3); + sink.done(); + return true; + }); + }); + + thread t = thread([&] { svr.listen(HOST, PORT); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + Client cli(HOST, PORT); + cli.set_keep_alive(true); + + auto res = cli.Get("/"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ("abc", res->body); + EXPECT_FALSE(res->has_header("Content-Length")); + EXPECT_EQ("close", res->get_header_value("Connection")); + EXPECT_FALSE(res->has_header("Keep-Alive")); + + // No body to delimit + auto head = cli.Head("/"); + ASSERT_TRUE(head) << "Error: " << to_string(head.error()); + EXPECT_FALSE(head->has_header("Connection")); +} + TEST(RoutingHandlerTest, PreAndPostRoutingHandlers) { #ifdef CPPHTTPLIB_SSL_ENABLED SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);