From 5d10305218cf8e236442e93a4a7f419391b38af0 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 early answer to Expect: 100-continue When the final response came before 100 Continue, the body was never sent but the connection was kept, so the server could read the next request as that body. Close it after the response. --- httplib.h | 7 ++++++- test/test.cc | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/httplib.h b/httplib.h index c01c852a..b7b2104e 100644 --- a/httplib.h +++ b/httplib.h @@ -16596,8 +16596,13 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req, output_error_log(error, &req); return false; } + } else { + // A final response came first, so the body this request declared was + // never sent, and the server may still be waiting for it. The + // connection cannot carry another request. + std::lock_guard guard(socket_mutex_); + socket_should_be_closed_when_request_is_done_ = true; } - // If not 100 Continue, server returned an error; proceed with that response } // Body diff --git a/test/test.cc b/test/test.cc index f2c8f921..061823b1 100644 --- a/test/test.cc +++ b/test/test.cc @@ -13535,6 +13535,63 @@ TEST(ClientPayloadTest, DoesNotReserveTheClaimedLength) { EXPECT_FALSE(cli.Get("/")); } +TEST(ExpectContinueTest, EarlyFinalResponseClosesConnection) { + std::atomic closed_by_client{false}; + auto server_thread = std::thread([&] { + auto srv = ::socket(AF_INET, SOCK_STREAM, 0); + default_socket_options(srv); + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(static_cast(PORT)); + ::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + int opt = 1; + ::setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, +#ifdef _WIN32 + reinterpret_cast(&opt), +#else + &opt, +#endif + sizeof(opt)); + ::bind(srv, reinterpret_cast(&addr), sizeof(addr)); + ::listen(srv, 1); + + auto cli = ::accept(srv, nullptr, nullptr); + if (cli != INVALID_SOCKET) { + detail::set_socket_opt_time(cli, SOL_SOCKET, SO_RCVTIMEO, 3, 0); + + // Answer the head without asking for the body, and keep the connection. + std::string head; + char c; + while (head.find("\r\n\r\n") == std::string::npos && + ::recv(cli, &c, 1, 0) == 1) { + head += c; + } + std::string res = + "HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\n\r\n"; + ::send(cli, res.data(), static_cast(res.size()), 0); + + // Bytes here would be read as the body the first request declared. + char buf[256]; + closed_by_client = ::recv(cli, buf, sizeof(buf), 0) == 0; + detail::close_socket(cli); + } + detail::close_socket(srv); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + Client cli("127.0.0.1", PORT); + cli.set_keep_alive(true); + cli.set_read_timeout(1, 0); + // Long enough for the client to send Expect: 100-continue. + auto res = cli.Post("/upload", std::string(2048, 'B'), "text/plain"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ(StatusCode::Unauthorized_401, res->status); + cli.Get("/second"); + + server_thread.join(); + EXPECT_TRUE(closed_by_client); +} + #ifdef CPPHTTPLIB_ZLIB_SUPPORT // `payload_max_length` is not enforced on decompressed body in ContentReader // path.