Procházet zdrojové kódy

Resolve #2262 (#2332)

* Resolve #2262

* Enhance request handling on Windows by adding early response check for large request bodies

* Enhance early response handling for large requests with long URIs on Windows
yhirose před 2 týdny
rodič
revize
a38a076571
2 změnil soubory, kde provedl 91 přidání a 2 odebrání
  1. 44 2
      httplib.h
  2. 47 0
      test/test.cc

+ 44 - 2
httplib.h

@@ -286,6 +286,7 @@ using socket_t = int;
 #include <atomic>
 #include <cassert>
 #include <cctype>
+#include <chrono>
 #include <climits>
 #include <condition_variable>
 #include <cstring>
@@ -11014,6 +11015,44 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
     }
   }
 
+  // After sending request line and headers, wait briefly for an early server
+  // response (e.g. 4xx) and avoid sending a potentially large request body
+  // unnecessarily. This workaround is only enabled on Windows because Unix
+  // platforms surface write errors (EPIPE) earlier; on Windows kernel send
+  // buffering can accept large writes even when the peer already responded.
+  // Check the stream first (which covers SSL via `is_readable()`), then
+  // fall back to select on the socket. Only perform the wait for very large
+  // request bodies to avoid interfering with normal small requests and
+  // reduce side-effects. Poll briefly (up to 50ms) for an early response.
+#if defined(_WIN32)
+  if (req.body.size() > (1u << 20) &&
+      req.path.size() >
+          CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) { // > 1MB && long URI
+    auto start = std::chrono::high_resolution_clock::now();
+    const auto max_wait_ms = 50;
+    for (;;) {
+      auto sock = strm.socket();
+      // Prefer socket-level readiness to avoid SSL_pending() false-positives
+      // from SSL internals. If the underlying socket is readable, assume an
+      // early response may be present.
+      if (sock != INVALID_SOCKET && detail::select_read(sock, 0, 0) > 0) {
+        return false;
+      }
+      // Fallback to stream-level check for non-socket streams or when the
+      // socket isn't reporting readable. Avoid using `is_readable()` for
+      // SSL, since `SSL_pending()` may report buffered records that do not
+      // indicate a complete application-level response yet.
+      if (!is_ssl() && strm.is_readable()) { return false; }
+      auto now = std::chrono::high_resolution_clock::now();
+      auto elapsed =
+          std::chrono::duration_cast<std::chrono::milliseconds>(now - start)
+              .count();
+      if (elapsed >= max_wait_ms) { break; }
+      std::this_thread::sleep_for(std::chrono::milliseconds(1));
+    }
+  }
+#endif
+
   // Body
   if (req.body.empty()) {
     return write_content_with_provider(strm, req, error);
@@ -11191,7 +11230,8 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
                                         Response &res, bool close_connection,
                                         Error &error) {
   // Send request
-  if (!write_request(strm, req, close_connection, error)) { return false; }
+  auto write_request_success =
+      write_request(strm, req, close_connection, error);
 
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
   if (is_ssl()) {
@@ -11209,11 +11249,13 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
   // Receive response and headers
   if (!read_response_line(strm, req, res) ||
       !detail::read_headers(strm, res.headers)) {
-    error = Error::Read;
+    if (write_request_success) { error = Error::Read; }
     output_error_log(error, &req);
     return false;
   }
 
+  if (!write_request_success) { return false; }
+
   // Body
   if ((res.status != StatusCode::NoContent_204) && req.method != "HEAD" &&
       req.method != "CONNECT") {

+ 47 - 0
test/test.cc

@@ -6567,6 +6567,53 @@ TEST_F(ServerTest, PreCompressionLoggingOnlyPreLogger) {
   EXPECT_EQ(test_content, pre_compression_body);
 }
 
+TEST_F(ServerTest, SendLargeBodyAfterRequestLineError) {
+  {
+    Request req;
+    req.method = "POST";
+    req.path = "/post-large?q=" + LONG_QUERY_VALUE;
+    req.body = LARGE_DATA;
+
+    Response res;
+    auto error = Error::Success;
+
+    auto start = std::chrono::high_resolution_clock::now();
+    cli_.set_keep_alive(true);
+    auto ret = cli_.send(req, res, error);
+    auto end = std::chrono::high_resolution_clock::now();
+
+    auto elapsed =
+        std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
+            .count();
+
+    EXPECT_FALSE(ret);
+    EXPECT_EQ(StatusCode::UriTooLong_414, res.status);
+    EXPECT_EQ("close", res.get_header_value("Connection"));
+    EXPECT_FALSE(cli_.is_socket_open());
+    EXPECT_LE(elapsed, 200);
+  }
+
+  {
+    // Send an extra GET request to ensure error recovery without hanging
+    Request req;
+    req.method = "GET";
+    req.path = "/hi";
+
+    auto start = std::chrono::high_resolution_clock::now();
+    auto res = cli_.send(req);
+    auto end = std::chrono::high_resolution_clock::now();
+
+    auto elapsed =
+        std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
+            .count();
+
+    ASSERT_TRUE(res);
+    EXPECT_EQ(StatusCode::OK_200, res->status);
+    EXPECT_EQ("Hello World!", res->body);
+    EXPECT_LE(elapsed, 100);
+  }
+}
+
 TEST(ZstdDecompressor, ChunkedDecompression) {
   std::string data;
   for (size_t i = 0; i < 32 * 1024; ++i) {