From 28741cb9d1058c41d39451cc6f9a3437f9d81243 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 01:54:23 +0000 Subject: [PATCH] Keep the TLS error behind a failed read or write in the client's Result A request that fails after the TLS handshake reports Error::Read or Error::Write, and SSLSocketStream dropped the TLS error behind it. On TLS 1.3, where a server rejects a client without a certificate only after the handshake, ssl_error() and ssl_backend_error() were 0 instead of showing, for example, OpenSSL's "tlsv13 alert certificate required". SSLSocketStream now keeps the error from its last failed TLS read or write, and SSLClient copies it into the values Result reports. Error itself stays Read or Write. Those values are also reset at the start of each request, so a later successful request no longer reports the TLS error of an earlier one. A client whose TLS context failed to set up keeps reporting that error. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WGAYpQgXt95LdDQcJYgjB8 --- httplib.h | 43 ++++++++++++++++++++++++++++++++++++++----- test/test.cc | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/httplib.h b/httplib.h index d36ab8e7..107b0988 100644 --- a/httplib.h +++ b/httplib.h @@ -10494,6 +10494,10 @@ public: // See SocketStream::set_readable_hint(). void set_readable_hint() { readable_hint_ = true; } + // The error from the last read() or write() that failed in the TLS layer, + // with code Success if none did + const tls::TlsError &tls_error() const { return tls_error_; } + private: bool ensure_readable(); @@ -10506,6 +10510,7 @@ private: time_t max_timeout_msec_; const std::chrono::time_point start_time_; bool readable_hint_ = false; + tls::TlsError tls_error_; }; // A TLS stream for WebSocket connections, where the receive path and the @@ -10720,11 +10725,14 @@ inline bool process_client_socket_ssl( tls::session_t session, socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec, - std::chrono::time_point start_time, T callback) { + std::chrono::time_point start_time, T callback, + tls::TlsError *tls_error = nullptr) { SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec, max_timeout_msec, start_time); - return callback(strm); + auto ret = callback(strm); + if (tls_error) { *tls_error = strm.tls_error(); } + return ret; } inline std::pair make_digest_authentication_header( @@ -12791,6 +12799,7 @@ inline SSLSocketStream::SSLSocketStream( write_timeout_sec_(write_timeout_sec), write_timeout_usec_(write_timeout_usec), max_timeout_msec_(max_timeout_msec), start_time_(start_time) { + tls_error_.code = tls::ErrorCode::Success; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT // Clear AUTO_RETRY for proper non-blocking I/O timeout handling // Note: create_session() also clears this, but SSLClient currently @@ -12843,6 +12852,8 @@ inline ssize_t SSLSocketStream::read(char *ptr, size_t size) { auto ret = tls::read(session_, ptr, size, err); if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) { error_ = Error::ConnectionClosed; + } else if (ret < 0) { + tls_error_ = err; } return ret; } else if (ensure_readable()) { @@ -12858,7 +12869,9 @@ inline ssize_t SSLSocketStream::read(char *ptr, size_t size) { while (--n >= 0 && err.code == tls::ErrorCode::WantRead) { #endif if (tls::pending(session_) > 0) { - return tls::read(session_, ptr, size, err); + ret = tls::read(session_, ptr, size, err); + if (ret >= 0) { return ret; } + break; } else if (wait_readable()) { std::this_thread::sleep_for(std::chrono::microseconds{10}); ret = tls::read(session_, ptr, size, err); @@ -12868,6 +12881,7 @@ inline ssize_t SSLSocketStream::read(char *ptr, size_t size) { } } assert(ret < 0); + tls_error_ = err; } else if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) { error_ = Error::ConnectionClosed; } @@ -12903,6 +12917,7 @@ inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) { } } assert(ret < 0); + tls_error_ = err; } return ret; } @@ -15209,6 +15224,14 @@ inline bool ClientImpl::read_response_line(Stream &strm, const Request &req, inline bool ClientImpl::send(Request &req, Response &res, Error &error) { std::lock_guard request_mutex_guard(request_mutex_); +#ifdef CPPHTTPLIB_SSL_ENABLED + // Don't let an earlier request's TLS error show up in this one's Result. A + // client that failed to set up its TLS context keeps reporting why. + if (is_valid()) { + last_ssl_error_ = 0; + last_backend_error_ = 0; + } +#endif auto ret = send_(req, res, error); if (error == Error::SSLPeerCouldBeClosed_) { assert(!ret); @@ -18555,10 +18578,20 @@ inline bool SSLClient::process_socket( std::chrono::time_point start_time, std::function callback) { assert(socket.ssl); - return detail::process_client_socket_ssl( + tls::TlsError tls_error; + auto ret = detail::process_client_socket_ssl( socket.ssl, socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time, - std::move(callback)); + std::move(callback), &tls_error); + + // A request that failed after the handshake reports Error::Read or + // Error::Write; keep the TLS error behind it, such as a fatal alert from + // the server, for Result::ssl_error() and ssl_backend_error() + if (!ret && tls_error.code != tls::ErrorCode::Success) { + last_ssl_error_ = static_cast(tls_error.code); + last_backend_error_ = tls_error.backend_code; + } + return ret; } inline bool SSLClient::is_ssl() const { return true; } diff --git a/test/test.cc b/test/test.cc index 65e69c1e..d37ab081 100644 --- a/test/test.cc +++ b/test/test.cc @@ -14612,6 +14612,8 @@ void ClientCertMissing(const ClientCertServerSetup &setup = nullptr) { if (res.error() == Error::SSLConnection) { EXPECT_NE(0UL, res.ssl_backend_error()); } + // A rejection noticed on the write keeps the TLS error the write hit + if (res.error() == Error::Write) { EXPECT_NE(0, res.ssl_error()); } } TEST(SSLClientServerTest, ClientCertMissing) { ClientCertMissing(); } @@ -14639,6 +14641,39 @@ TEST(SSLClientServerTest, ClientCertStillRequiredAfterVerifyCallbackCleared) { ClientCertStillRequired(true); } +// A TLS error from one request must not show up in a later request's Result +TEST(SSLClientServerTest, SSLErrorNotCarriedToLaterRequest) { + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(svr.is_valid()); + svr.Get("/test", [](const Request &, Response &res) { + res.set_content("test", "text/plain"); + }); + + auto port = svr.bind_to_any_port(HOST); + ASSERT_LT(0, port); + thread t = thread([&]() { svr.listen_after_bind(); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + SSLClient cli(HOST, port); + cli.set_connection_timeout(30); + cli.enable_server_hostname_verification(false); + + // cert.pem is not trusted, so the handshake fails + ASSERT_FALSE(cli.Get("/test")); + + cli.enable_server_certificate_verification(false); + auto res = cli.Get("/test"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ(0, res.ssl_error()); + EXPECT_EQ(0UL, res.ssl_backend_error()); +} + // PEM memory-based constructor tests (works with all TLS backends) void PemMemoryClientCertPresent( const std::string &client_cert_file,