Add Expect: 100-continue support

This commit is contained in:
yhirose committed 2026-01-18 22:38:25 -05:00
1 parent a38a076571
commit 02dfb97fd6
2 files changed
+118 -18

No files matched your search

+93 -17
View File
@@ -98,6 +98,22 @@
#define CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND 0 #define CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND 0
#endif #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 #ifndef CPPHTTPLIB_IDLE_INTERVAL_SECOND
#define CPPHTTPLIB_IDLE_INTERVAL_SECOND 0 #define CPPHTTPLIB_IDLE_INTERVAL_SECOND 0
#endif #endif
@@ -1849,10 +1865,11 @@ private:
Result send_(Request &&req); Result send_(Request &&req);
socket_t create_client_socket(Error &error) const; socket_t create_client_socket(Error &error) const;
bool read_response_line(Stream &strm, const Request &req, bool read_response_line(Stream &strm, const Request &req, Response &res,
Response &res) const; bool skip_100_continue = true) const;
bool write_request(Stream &strm, Request &req, bool close_connection, 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, void prepare_default_headers(Request &r, bool for_stream,
const std::string &ct); const std::string &ct);
bool redirect(Request &req, Response &res, Error &error); 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, 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{}; std::array<char, 2048> buf{};
detail::stream_line_reader line_reader(strm, buf.data(), buf.size()); 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.status = std::stoi(std::string(m[2]));
res.reason = std::string(m[3]); res.reason = std::string(m[3]);
// Ignore '100 Continue' // Ignore '100 Continue' (only when not using Expect: 100-continue explicitly)
while (res.status == StatusCode::Continue_100) { while (skip_100_continue && res.status == StatusCode::Continue_100) {
if (!line_reader.getline()) { return false; } // CRLF if (!line_reader.getline()) { return false; } // CRLF
if (!line_reader.getline()) { return false; } // next response line 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, 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 // Prepare additional headers
if (close_connection) { if (close_connection) {
if (!req.has_header("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 // 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 // fall back to select on the socket. Only perform the wait for very large
// request bodies to avoid interfering with normal small requests and // 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 defined(_WIN32)
if (req.body.size() > (1u << 20) && if (!skip_body &&
req.path.size() > req.body.size() > CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD &&
CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) { // > 1MB && long URI req.path.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) {
auto start = std::chrono::high_resolution_clock::now(); auto start = std::chrono::high_resolution_clock::now();
const auto max_wait_ms = 50;
for (;;) { for (;;) {
auto sock = strm.socket();
// Prefer socket-level readiness to avoid SSL_pending() false-positives // Prefer socket-level readiness to avoid SSL_pending() false-positives
// from SSL internals. If the underlying socket is readable, assume an // from SSL internals. If the underlying socket is readable, assume an
// early response may be present. // early response may be present.
auto sock = strm.socket();
if (sock != INVALID_SOCKET && detail::select_read(sock, 0, 0) > 0) { if (sock != INVALID_SOCKET && detail::select_read(sock, 0, 0) > 0) {
return false; return false;
} }
// Fallback to stream-level check for non-socket streams or when the // Fallback to stream-level check for non-socket streams or when the
// socket isn't reporting readable. Avoid using `is_readable()` for // socket isn't reporting readable. Avoid using `is_readable()` for
// SSL, since `SSL_pending()` may report buffered records that do not // SSL, since `SSL_pending()` may report buffered records that do not
// indicate a complete application-level response yet. // indicate a complete application-level response yet.
if (!is_ssl() && strm.is_readable()) { return false; } if (!is_ssl() && strm.is_readable()) { return false; }
auto now = std::chrono::high_resolution_clock::now(); auto now = std::chrono::high_resolution_clock::now();
auto elapsed = auto elapsed =
std::chrono::duration_cast<std::chrono::milliseconds>(now - start) std::chrono::duration_cast<std::chrono::milliseconds>(now - start)
.count(); .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)); std::this_thread::sleep_for(std::chrono::milliseconds(1));
} }
} }
#endif #endif
// Body // 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()) { if (req.body.empty()) {
return write_content_with_provider(strm, req, error); 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, inline bool ClientImpl::process_request(Stream &strm, Request &req,
Response &res, bool close_connection, Response &res, bool close_connection,
Error &error) { 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 = 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 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
if (is_ssl()) { if (is_ssl()) {
@@ -11246,8 +11290,21 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
} }
#endif #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 // 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)) { !detail::read_headers(strm, res.headers)) {
if (write_request_success) { error = Error::Read; } if (write_request_success) { error = Error::Read; }
output_error_log(error, &req); output_error_log(error, &req);
@@ -11256,6 +11313,25 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
if (!write_request_success) { return false; } 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 // Body
if ((res.status != StatusCode::NoContent_204) && req.method != "HEAD" && if ((res.status != StatusCode::NoContent_204) && req.method != "HEAD" &&
req.method != "CONNECT") { req.method != "CONNECT") {
+25 -1
View File
@@ -6569,9 +6569,32 @@ TEST_F(ServerTest, PreCompressionLoggingOnlyPreLogger) {
TEST_F(ServerTest, SendLargeBodyAfterRequestLineError) { 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; Request req;
req.method = "POST"; req.method = "POST";
req.path = "/post-large?q=" + LONG_QUERY_VALUE; req.path = "/post-large?q=" + LONG_QUERY_VALUE;
req.set_header("Expect", "100-continue");
req.body = LARGE_DATA; req.body = LARGE_DATA;
Response res; Response res;
@@ -6586,7 +6609,8 @@ TEST_F(ServerTest, SendLargeBodyAfterRequestLineError) {
std::chrono::duration_cast<std::chrono::milliseconds>(end - start) std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
.count(); .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(StatusCode::UriTooLong_414, res.status);
EXPECT_EQ("close", res.get_header_value("Connection")); EXPECT_EQ("close", res.get_header_value("Connection"));
EXPECT_FALSE(cli_.is_socket_open()); EXPECT_FALSE(cli_.is_socket_open());