Fix #2458: send body when no 100 Continue arrives over TLS (#2460)

The auto-added `Expect: 100-continue` (for bodies >= 1024 bytes) decided
whether to withhold the request body based on raw socket readability via
select_read(). Over TLS, post-handshake records such as TLS 1.3 session
tickets make the socket readable without any HTTP response being
available, so the client withheld the body and then blocked reading a
response that never came, failing with `Failed to read connection`.

Decide based on whether a status line can actually be read within the
100-continue timeout instead: temporarily shorten the read timeout, try
to read the status line, and if none arrives, send the body and proceed
as usual (matching curl). This keeps the `100 Continue` and early
final-response paths working while no longer being fooled by TLS records.

Add a regression test using a raw OpenSSL server that never sends
`100 Continue`.
This commit is contained in:
yhirose authored and GitHub committed 2026-05-29 06:19:40 -04:00
1 parent c86c192f3e
commit 91219d4508
2 files changed
+128 -8

No files matched your search

+24 -8
View File
@@ -13810,13 +13810,28 @@ 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
}
@@ -13824,7 +13839,8 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
// 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);