From 22f9b2afd216f20bd1b61e328c6f6238ba0682b3 Mon Sep 17 00:00:00 2001 From: Chansik Park <16063972+chansikpark@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:29:40 -0500 Subject: [PATCH 1/4] More robust error handling - Check if socket is alive before waiting to write to it to prevent SIGPIPE - Consume SIGPIPE on client - Read response line and headers after bad request --- httplib.h | 12 +++++++++--- test/test.cc | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/httplib.h b/httplib.h index 8411c8c93b..898de337ef 100644 --- a/httplib.h +++ b/httplib.h @@ -7133,7 +7133,7 @@ inline ssize_t SocketStream::read(char *ptr, size_t size) { } inline ssize_t SocketStream::write(const char *ptr, size_t size) { - if (!wait_writable()) { return -1; } + if (!detail::is_socket_alive(sock_) || !wait_writable()) { return -1; } #if defined(_WIN32) && !defined(_WIN64) size = @@ -8618,7 +8618,11 @@ inline ClientImpl::ClientImpl(const std::string &host, int port, const std::string &client_key_path) : host_(detail::escape_abstract_namespace_unix_domain(host)), port_(port), host_and_port_(detail::make_host_and_port_string(host_, port, is_ssl())), - client_cert_path_(client_cert_path), client_key_path_(client_key_path) {} + client_cert_path_(client_cert_path), client_key_path_(client_key_path) { +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); +#endif +} inline ClientImpl::~ClientImpl() { // Wait until all the requests in flight are handled. @@ -9516,7 +9520,7 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req, Response &res, bool close_connection, Error &error) { // Send request - if (!write_request(strm, req, close_connection, error)) { return false; } + bool wrote = write_request(strm, req, close_connection, error); #ifdef CPPHTTPLIB_OPENSSL_SUPPORT if (is_ssl()) { @@ -9539,6 +9543,8 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req, return false; } + if (!wrote) return false; + // Body if ((res.status != StatusCode::NoContent_204) && req.method != "HEAD" && req.method != "CONNECT") { diff --git a/test/test.cc b/test/test.cc index b54c0f0afb..baf341117e 100644 --- a/test/test.cc +++ b/test/test.cc @@ -5898,6 +5898,48 @@ TEST_F(ServerTest, BadRequestLineCancelsKeepAlive) { EXPECT_FALSE(cli_.is_socket_open()); } +TEST_F(ServerTest, SendLargeBodyAfterRequestLineError) { + Request post; + post.method = "POST"; + post.path = "/post-large?q=" + LONG_QUERY_VALUE; + post.body = LARGE_DATA; + + auto start = std::chrono::high_resolution_clock::now(); + + auto resPost = std::make_shared(); + auto error = Error::Success; + cli_.set_keep_alive(true); + auto ret = cli_.send(post, *resPost, error); + + auto end = std::chrono::high_resolution_clock::now(); + auto elapsed = + std::chrono::duration_cast(end - start) + .count(); + + EXPECT_FALSE(ret); + EXPECT_EQ(StatusCode::UriTooLong_414, resPost->status); + EXPECT_EQ("close", resPost->get_header_value("Connection")); + EXPECT_FALSE(cli_.is_socket_open()); + EXPECT_LE(elapsed, 200); + + // Send an extra GET request to ensure error recovery without hanging + Request get; + get.method = "GET"; + get.path = "/hi"; + + start = std::chrono::high_resolution_clock::now(); + auto resGet = cli_.send(get); + end = std::chrono::high_resolution_clock::now(); + elapsed = + std::chrono::duration_cast(end - start) + .count(); + + ASSERT_TRUE(resGet); + EXPECT_EQ(StatusCode::OK_200, resGet->status); + EXPECT_EQ("Hello World!", resGet->body); + EXPECT_LE(elapsed, 100); +} + TEST_F(ServerTest, StartTime) { auto res = cli_.Get("/test-start-time"); } #ifdef CPPHTTPLIB_ZLIB_SUPPORT From ed120dc50164e4d28eb87001ef94dedfc614cbbe Mon Sep 17 00:00:00 2001 From: Chansik Park <16063972+chansikpark@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:25:00 -0500 Subject: [PATCH 2/4] Loosen test in WIN32. Error code will not be available in WIN32. --- test/test.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test.cc b/test/test.cc index baf341117e..3c805e844a 100644 --- a/test/test.cc +++ b/test/test.cc @@ -5916,9 +5916,11 @@ TEST_F(ServerTest, SendLargeBodyAfterRequestLineError) { std::chrono::duration_cast(end - start) .count(); +#ifndef _WIN32 EXPECT_FALSE(ret); EXPECT_EQ(StatusCode::UriTooLong_414, resPost->status); EXPECT_EQ("close", resPost->get_header_value("Connection")); +#endif EXPECT_FALSE(cli_.is_socket_open()); EXPECT_LE(elapsed, 200); From 3485f28dd4658c346836ade06cc2715113fdaad3 Mon Sep 17 00:00:00 2001 From: Chansik Park <16063972+chansikpark@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:26:50 -0500 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test.cc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/test.cc b/test/test.cc index 3c805e844a..e02d857f8a 100644 --- a/test/test.cc +++ b/test/test.cc @@ -5932,9 +5932,8 @@ TEST_F(ServerTest, SendLargeBodyAfterRequestLineError) { start = std::chrono::high_resolution_clock::now(); auto resGet = cli_.send(get); end = std::chrono::high_resolution_clock::now(); - elapsed = - std::chrono::duration_cast(end - start) - .count(); + elapsed = std::chrono::duration_cast(end - start) + .count(); ASSERT_TRUE(resGet); EXPECT_EQ(StatusCode::OK_200, resGet->status); From 39d0acc8de2aa301a0dab8544dea9704ff0c08ae Mon Sep 17 00:00:00 2001 From: Chansik Park <16063972+chansikpark@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:22:58 -0500 Subject: [PATCH 4/4] Move one basic test assertion out of WIN32-only block --- test/test.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.cc b/test/test.cc index e02d857f8a..26b2a89765 100644 --- a/test/test.cc +++ b/test/test.cc @@ -5916,8 +5916,8 @@ TEST_F(ServerTest, SendLargeBodyAfterRequestLineError) { std::chrono::duration_cast(end - start) .count(); -#ifndef _WIN32 EXPECT_FALSE(ret); +#ifndef _WIN32 EXPECT_EQ(StatusCode::UriTooLong_414, resPost->status); EXPECT_EQ("close", resPost->get_header_value("Connection")); #endif