From 8e702d3837b2164765ca1d98cb6d180ae4711e70 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sun, 9 Aug 2026 19:43:19 -0400 Subject: [PATCH] Gracefully drain socket before close in Server::process_and_close_socket (#2534) * Gracefully drain socket before close in Server::process_and_close_socket Closing a connection while the receive queue still has unread data, or while bytes are still in flight, can make the OS send an abortive RST instead of a graceful FIN. On Windows this surfaces as WSAECONNABORTED/WSAECONNRESET on the peer's read, which can make an otherwise fully-written response look like a failed request -- a likely contributor to the ServerTest.HTTP2Magic flakiness tracked in #2533. Add detail::close_socket_gracefully(), which half-closes the write side, drains any queued/in-flight bytes (bounded to 100ms / 1MB), then performs the final shutdown+close. Use it in Server::process_and_close_socket. Root cause and fix mechanism identified by @Hyukya in #2533. * Rename close_socket_gracefully to drain_and_close_socket 'gracefully' already means something specific in this codebase: whether to send a TLS close_notify before closing (shutdown_ssl's shutdown_gracefully param, ClientImpl::disconnect(gracefully), tls::shutdown(session, graceful)). Reusing the word for an unrelated TCP-level drain-before-close made the new function read as part of that TLS machinery when it isn't. Rename it to describe what it does instead, matching the existing close_socket/shutdown_socket and WebSocketClient::shutdown_and_close naming. --- httplib.h | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/httplib.h b/httplib.h index 8c311403..7e0223e3 100644 --- a/httplib.h +++ b/httplib.h @@ -6321,6 +6321,39 @@ inline int shutdown_socket(socket_t sock) noexcept { #endif } +// Half-closes the write side and drains any in-flight/queued bytes before +// the final shutdown+close. Closing with unread data in the receive queue +// (or bytes arriving after the receive side is closed) makes the stack send +// an abortive RST instead of a graceful FIN, which can make the peer see the +// response as a failed read even though it was fully written. +inline void drain_and_close_socket(socket_t sock) noexcept { +#ifdef _WIN32 + shutdown(sock, SD_SEND); +#else + shutdown(sock, SHUT_WR); +#endif + + char buf[CPPHTTPLIB_RECV_BUFSIZ]; + size_t total = 0; + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::milliseconds(100); // bound #1 + + while (total < size_t(1024u * 1024u)) { // bound #2 + const auto remaining = + std::chrono::duration_cast( + deadline - std::chrono::steady_clock::now()) + .count(); + if (remaining <= 0) { break; } + if (select_read(sock, 0, static_cast(remaining)) <= 0) { break; } + const auto n = read_socket(sock, buf, sizeof(buf), CPPHTTPLIB_RECV_FLAGS); + if (n <= 0) { break; } + total += static_cast(n); + } + + shutdown_socket(sock); + close_socket(sock); +} + inline std::string escape_abstract_namespace_unix_domain(const std::string &s) { if (s.size() > 1 && s[0] == '\0') { auto ret = s; @@ -13608,8 +13641,7 @@ inline bool Server::process_and_close_socket(socket_t sock) { nullptr, &websocket_upgraded); }); - detail::shutdown_socket(sock); - detail::close_socket(sock); + detail::drain_and_close_socket(sock); return ret; }