Procházet zdrojové kódy

Add `Expect: 100-continue` support

yhirose před 2 týdny
rodič
revize
02dfb97fd6
2 změnil soubory, kde provedl 118 přidání a 18 odebrání
  1. 93 17
      httplib.h
  2. 25 1
      test/test.cc

+ 93 - 17
httplib.h

@@ -98,6 +98,22 @@
 #define CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND 0
 #endif
 
+#ifndef CPPHTTPLIB_EXPECT_100_THRESHOLD
+#define CPPHTTPLIB_EXPECT_100_THRESHOLD 1024
+#endif
+
+#ifndef CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND
+#define CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND 1000
+#endif
+
+#ifndef CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD
+#define CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD (1024 * 1024)
+#endif
+
+#ifndef CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND
+#define CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND 50
+#endif
+
 #ifndef CPPHTTPLIB_IDLE_INTERVAL_SECOND
 #define CPPHTTPLIB_IDLE_INTERVAL_SECOND 0
 #endif
@@ -1849,10 +1865,11 @@ private:
   Result send_(Request &&req);
 
   socket_t create_client_socket(Error &error) const;
-  bool read_response_line(Stream &strm, const Request &req,
-                          Response &res) const;
+  bool read_response_line(Stream &strm, const Request &req, Response &res,
+                          bool skip_100_continue = true) const;
   bool write_request(Stream &strm, Request &req, bool close_connection,
-                     Error &error);
+                     Error &error, bool skip_body = false);
+  bool write_request_body(Stream &strm, Request &req, Error &error);
   void prepare_default_headers(Request &r, bool for_stream,
                                const std::string &ct);
   bool redirect(Request &req, Response &res, Error &error);
@@ -10089,7 +10106,8 @@ inline void ClientImpl::close_socket(Socket &socket) {
 }
 
 inline bool ClientImpl::read_response_line(Stream &strm, const Request &req,
-                                           Response &res) const {
+                                           Response &res,
+                                           bool skip_100_continue) const {
   std::array<char, 2048> buf{};
 
   detail::stream_line_reader line_reader(strm, buf.data(), buf.size());
@@ -10110,8 +10128,8 @@ inline bool ClientImpl::read_response_line(Stream &strm, const Request &req,
   res.status = std::stoi(std::string(m[2]));
   res.reason = std::string(m[3]);
 
-  // Ignore '100 Continue'
-  while (res.status == StatusCode::Continue_100) {
+  // Ignore '100 Continue' (only when not using Expect: 100-continue explicitly)
+  while (skip_100_continue && res.status == StatusCode::Continue_100) {
     if (!line_reader.getline()) { return false; } // CRLF
     if (!line_reader.getline()) { return false; } // next response line
 
@@ -10896,7 +10914,8 @@ inline bool ClientImpl::write_content_with_provider(Stream &strm,
 }
 
 inline bool ClientImpl::write_request(Stream &strm, Request &req,
-                                      bool close_connection, Error &error) {
+                                      bool close_connection, Error &error,
+                                      bool skip_body) {
   // Prepare additional headers
   if (close_connection) {
     if (!req.has_header("Connection")) {
@@ -11023,37 +11042,51 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
   // 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.
+  // reduce side-effects. Poll briefly (up to 50ms as default) for an early
+  // response. Skip this check when using Expect: 100-continue, as the protocol
+  // handles early responses properly.
 #if defined(_WIN32)
-  if (req.body.size() > (1u << 20) &&
-      req.path.size() >
-          CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) { // > 1MB && long URI
+  if (!skip_body &&
+      req.body.size() > CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD &&
+      req.path.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) {
     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.
+      auto sock = strm.socket();
       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; }
+      if (elapsed >= CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND) {
+        break;
+      }
+
       std::this_thread::sleep_for(std::chrono::milliseconds(1));
     }
   }
 #endif
 
   // Body
+  if (skip_body) { return true; }
+
+  return write_request_body(strm, req, error);
+}
+
+inline bool ClientImpl::write_request_body(Stream &strm, Request &req,
+                                           Error &error) {
   if (req.body.empty()) {
     return write_content_with_provider(strm, req, error);
   }
@@ -11229,9 +11262,20 @@ inline void ClientImpl::output_error_log(const Error &err,
 inline bool ClientImpl::process_request(Stream &strm, Request &req,
                                         Response &res, bool close_connection,
                                         Error &error) {
-  // Send request
+  // Auto-add Expect: 100-continue for large bodies
+  if (CPPHTTPLIB_EXPECT_100_THRESHOLD > 0 && !req.has_header("Expect")) {
+    auto body_size = req.body.empty() ? req.content_length_ : req.body.size();
+    if (body_size >= CPPHTTPLIB_EXPECT_100_THRESHOLD) {
+      req.set_header("Expect", "100-continue");
+    }
+  }
+
+  // Check for Expect: 100-continue
+  auto expect_100_continue = req.get_header_value("Expect") == "100-continue";
+
+  // Send request (skip body if using Expect: 100-continue)
   auto write_request_success =
-      write_request(strm, req, close_connection, error);
+      write_request(strm, req, close_connection, error, expect_100_continue);
 
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
   if (is_ssl()) {
@@ -11246,8 +11290,21 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
   }
 #endif
 
+  // Handle Expect: 100-continue with timeout
+  if (expect_100_continue && CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND > 0) {
+    time_t sec = CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND / 1000;
+    time_t usec = (CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND % 1000) * 1000;
+    auto ret = detail::select_read(strm.socket(), sec, usec);
+    if (ret <= 0) {
+      // Timeout or error: send body anyway (server didn't respond in time)
+      if (!write_request_body(strm, req, error)) { return false; }
+      expect_100_continue = false; // Switch to normal response handling
+    }
+  }
+
   // Receive response and headers
-  if (!read_response_line(strm, req, res) ||
+  // When using Expect: 100-continue, don't auto-skip `100 Continue` response
+  if (!read_response_line(strm, req, res, !expect_100_continue) ||
       !detail::read_headers(strm, res.headers)) {
     if (write_request_success) { error = Error::Read; }
     output_error_log(error, &req);
@@ -11256,6 +11313,25 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
 
   if (!write_request_success) { return false; }
 
+  // Handle Expect: 100-continue response
+  if (expect_100_continue) {
+    if (res.status == StatusCode::Continue_100) {
+      // Server accepted, send the body
+      if (!write_request_body(strm, req, error)) { return false; }
+
+      // Read the actual response
+      res.headers.clear();
+      res.body.clear();
+      if (!read_response_line(strm, req, res) ||
+          !detail::read_headers(strm, res.headers)) {
+        error = Error::Read;
+        output_error_log(error, &req);
+        return false;
+      }
+    }
+    // If not 100 Continue, server returned an error; proceed with that response
+  }
+
   // Body
   if ((res.status != StatusCode::NoContent_204) && req.method != "HEAD" &&
       req.method != "CONNECT") {

+ 25 - 1
test/test.cc

@@ -6569,9 +6569,32 @@ TEST_F(ServerTest, PreCompressionLoggingOnlyPreLogger) {
 
 TEST_F(ServerTest, SendLargeBodyAfterRequestLineError) {
   {
+    // Test with Expect: 100-continue header - success case
+    // Server returns 100 Continue, client sends body, server returns 200 OK
+    Request req;
+    req.method = "POST";
+    req.path = "/post-large";
+    req.set_header("Expect", "100-continue");
+    req.body = LARGE_DATA;
+
+    Response res;
+    auto error = Error::Success;
+
+    cli_.set_keep_alive(true);
+    auto ret = cli_.send(req, res, error);
+
+    EXPECT_TRUE(ret);
+    EXPECT_EQ(StatusCode::OK_200, res.status);
+    EXPECT_EQ(LARGE_DATA, res.body);
+  }
+
+  {
+    // Test with Expect: 100-continue header - error case
+    // Client should not send the body when server returns an error
     Request req;
     req.method = "POST";
     req.path = "/post-large?q=" + LONG_QUERY_VALUE;
+    req.set_header("Expect", "100-continue");
     req.body = LARGE_DATA;
 
     Response res;
@@ -6586,7 +6609,8 @@ TEST_F(ServerTest, SendLargeBodyAfterRequestLineError) {
         std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
             .count();
 
-    EXPECT_FALSE(ret);
+    // With Expect: 100-continue, request completes successfully but with error
+    EXPECT_TRUE(ret);
     EXPECT_EQ(StatusCode::UriTooLong_414, res.status);
     EXPECT_EQ("close", res.get_header_value("Connection"));
     EXPECT_FALSE(cli_.is_socket_open());