Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions httplib.h
Original file line number Diff line number Diff line change
Expand Up @@ -13810,21 +13810,37 @@ 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)
// Handle Expect: 100-continue.
//
// Wait for an interim/early response by attempting to read the status line
// under a short timeout, instead of trusting raw socket readability. Over
// TLS, post-handshake records (e.g. session tickets) make the socket
// readable without any HTTP response being available; relying on
// `select_read` there caused the body to be withheld forever and the
// request to fail with `Read` (#2458). If no status line arrives within the
// timeout, send the body anyway (matching curl's behavior).
auto status_line_read = false;
if (expect_100_continue && write_request_success) {
if (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;
strm.set_read_timeout(sec, usec);
status_line_read = read_response_line(strm, req, res, false);
strm.set_read_timeout(read_timeout_sec_, read_timeout_usec_);
}

if (!status_line_read) {
// No interim response within the timeout: send the body and handle the
// response as usual.
if (!write_request_body(strm, req, error)) { return false; }
expect_100_continue = false; // Switch to normal response handling
}
}

// Receive response and headers
// When using Expect: 100-continue, don't auto-skip `100 Continue` response
if (!read_response_line(strm, req, res, !expect_100_continue) ||
if ((!status_line_read &&
!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);
Expand Down
104 changes: 104 additions & 0 deletions test/test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13675,6 +13675,110 @@ TEST(Expect100ContinueTest, ServerClosesConnection) {
}
#endif

#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && !defined(_WIN32)
// Regression test for #2458.
//
// A large request body (>= CPPHTTPLIB_EXPECT_100_THRESHOLD) makes the client
// auto-add `Expect: 100-continue`. Over TLS, the server's TLS 1.3 session
// ticket can make the client socket spuriously readable during the
// 100-continue wait. If the readiness is mistaken for an incoming response,
// the client withholds the body and then blocks reading a response that never
// comes, failing with `Failed to read connection`.
//
// A correct client (like curl) sends the body once no `100 Continue` arrives
// within the timeout. This raw OpenSSL server deliberately never sends
// `100 Continue`; the client must still deliver the body and receive 200.
TEST(Expect100ContinueTest, TLSServerOmits100Continue) {
signal(SIGPIPE, SIG_IGN);

const auto port = PORT + 4;

auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
ASSERT_NE(srv, INVALID_SOCKET);

int opt = 1;
::setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(static_cast<uint16_t>(port));
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);

ASSERT_EQ(0, ::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)));
ASSERT_EQ(0, ::listen(srv, 1));

std::atomic<size_t> server_body_bytes{0};

auto server_thread = std::thread([&] {
sockaddr_in cli_addr{};
socklen_t cli_len = sizeof(cli_addr);
auto cli = ::accept(srv, reinterpret_cast<sockaddr *>(&cli_addr), &cli_len);
if (cli == INVALID_SOCKET) { return; }

// Bound the lifetime of the server side so a buggy client (which never
// sends the body) cannot make this thread block forever on join.
detail::set_socket_opt_time(cli, SOL_SOCKET, SO_RCVTIMEO, 4, 0);

SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
SSL_CTX_set_min_proto_version(ctx, TLS1_3_VERSION);
SSL_CTX_use_certificate_file(ctx, SERVER_CERT_FILE, SSL_FILETYPE_PEM);
SSL_CTX_use_PrivateKey_file(ctx, SERVER_PRIVATE_KEY_FILE, SSL_FILETYPE_PEM);

SSL *ssl = SSL_new(ctx);
SSL_set_fd(ssl, static_cast<int>(cli));

if (SSL_accept(ssl) > 0) {
// Read the request headers. Reading here also flushes the TLS 1.3
// session tickets to the client. Deliberately do NOT send
// `100 Continue`.
std::string buf;
char tmp[1024];
while (buf.find("\r\n\r\n") == std::string::npos) {
auto n = SSL_read(ssl, tmp, sizeof(tmp));
if (n <= 0) { break; }
buf.append(tmp, static_cast<size_t>(n));
}

// A correct client sends the body now; count what arrives.
auto pos = buf.find("\r\n\r\n");
size_t body = (pos == std::string::npos) ? 0 : buf.size() - (pos + 4);
while (body < 4096) {
auto n = SSL_read(ssl, tmp, sizeof(tmp));
if (n <= 0) { break; }
body += static_cast<size_t>(n);
}
server_body_bytes = body;

std::string resp = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok";
SSL_write(ssl, resp.data(), static_cast<int>(resp.size()));
SSL_shutdown(ssl);
}

SSL_free(ssl);
detail::close_socket(cli);
SSL_CTX_free(ctx);
});

auto se = detail::scope_exit([&] {
server_thread.join();
detail::close_socket(srv);
});

SSLClient cli("127.0.0.1", port);
cli.enable_server_certificate_verification(false);
cli.set_connection_timeout(5, 0);
cli.set_read_timeout(3, 0); // short, so a hang surfaces quickly

// Body larger than CPPHTTPLIB_EXPECT_100_THRESHOLD (1024) -> auto Expect.
std::string body(4096, 'A');
auto res = cli.Put("/api/test", body, "application/json");

ASSERT_TRUE(res) << "request failed: " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(body.size(), server_body_bytes.load());
}
#endif

template <typename S, typename C>
inline void max_timeout_test(S &svr, C &cli, time_t timeout, time_t threshold) {
svr.Get("/stream", [&](const Request &, Response &res) {
Expand Down
Loading