diff --git a/httplib.h b/httplib.h index 0e7e2aaceb..66d4c2fd84 100644 --- a/httplib.h +++ b/httplib.h @@ -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); diff --git a/test/test.cc b/test/test.cc index 8c5f5fdcde..03ac254688 100644 --- a/test/test.cc +++ b/test/test.cc @@ -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(port)); + ::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + + ASSERT_EQ(0, ::bind(srv, reinterpret_cast(&addr), sizeof(addr))); + ASSERT_EQ(0, ::listen(srv, 1)); + + std::atomic 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(&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(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(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(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(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 inline void max_timeout_test(S &svr, C &cli, time_t timeout, time_t threshold) { svr.Get("/stream", [&](const Request &, Response &res) {