1
0
mirror of synced 2025-12-11 01:22:25 +03:00

Add error handling for stream read timeouts and connection closures

This commit is contained in:
yhirose
2025-12-03 23:56:04 -05:00
parent 143019a38c
commit dbd5ca4bf2
3 changed files with 324 additions and 50 deletions

View File

@@ -12463,4 +12463,227 @@ TEST_F(SSLStreamApiTest, GetAndPost) {
auto post = httplib::stream::Post(cli, "/echo", "test", "text/plain");
EXPECT_EQ("test", read_body(post));
}
#endif
#endif
// Tests for Error::Timeout and Error::ConnectionClosed error types
// These errors are set in SocketStream/SSLSocketStream and propagated through
// BodyReader
TEST(ErrorHandlingTest, StreamReadTimeout) {
// Test that read timeout during streaming is detected
// Use a large content-length response where server delays mid-stream
Server svr;
svr.Get("/slow-stream", [](const Request &, Response &res) {
// Send a large response with delay in the middle
res.set_content_provider(
1000, // content_length
"text/plain", [](size_t offset, size_t /*length*/, DataSink &sink) {
if (offset < 100) {
// Send first 100 bytes immediately
std::string data(100, 'A');
sink.write(data.c_str(), data.size());
return true;
}
// Then delay longer than client timeout
std::this_thread::sleep_for(std::chrono::seconds(3));
std::string data(900, 'B');
sink.write(data.c_str(), data.size());
return true;
});
});
auto port = 8091;
std::thread t([&]() { svr.listen("localhost", port); });
svr.wait_until_ready();
Client cli("localhost", port);
cli.set_read_timeout(1, 0); // 1 second timeout
auto handle = cli.open_stream("GET", "/slow-stream");
ASSERT_TRUE(handle.is_valid());
char buf[256];
ssize_t total = 0;
ssize_t n;
bool got_error = false;
while ((n = handle.read(buf, sizeof(buf))) > 0) {
total += n;
}
if (n < 0) {
got_error = true;
// Should be timeout or read error
EXPECT_TRUE(handle.get_read_error() == Error::Timeout ||
handle.get_read_error() == Error::Read)
<< "Actual error: " << to_string(handle.get_read_error());
}
// Either we got an error, or we got less data than expected
EXPECT_TRUE(got_error || total < 1000)
<< "Expected timeout but got all " << total << " bytes";
svr.stop();
t.join();
}
TEST(ErrorHandlingTest, StreamConnectionClosed) {
// Test connection closed detection via BodyReader
Server svr;
std::atomic<bool> close_now{false};
svr.Get("/will-close", [&](const Request &, Response &res) {
res.set_content_provider(
10000, // Large content_length that we won't fully send
"text/plain", [&](size_t offset, size_t /*length*/, DataSink &sink) {
if (offset < 100) {
std::string data(100, 'X');
sink.write(data.c_str(), data.size());
return true;
}
// Wait for signal then abort
while (!close_now) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
return false; // Abort - server will close connection
});
});
auto port = 8092;
std::thread t([&]() { svr.listen("localhost", port); });
svr.wait_until_ready();
Client cli("localhost", port);
auto handle = cli.open_stream("GET", "/will-close");
ASSERT_TRUE(handle.is_valid());
char buf[256];
ssize_t n = handle.read(buf, sizeof(buf)); // First read
EXPECT_GT(n, 0) << "First read should succeed";
// Signal server to close
close_now = true;
// Keep reading until error or EOF
while ((n = handle.read(buf, sizeof(buf))) > 0) {
// Keep reading
}
// Should get an error since content_length wasn't satisfied
if (n < 0) {
EXPECT_TRUE(handle.get_read_error() == Error::ConnectionClosed ||
handle.get_read_error() == Error::Read)
<< "Actual error: " << to_string(handle.get_read_error());
}
svr.stop();
t.join();
}
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
TEST(ErrorHandlingTest, SSLStreamReadTimeout) {
// Test that read timeout during SSL streaming is detected
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
svr.Get("/slow-stream", [](const Request &, Response &res) {
res.set_content_provider(
1000, "text/plain",
[](size_t offset, size_t /*length*/, DataSink &sink) {
if (offset < 100) {
std::string data(100, 'A');
sink.write(data.c_str(), data.size());
return true;
}
std::this_thread::sleep_for(std::chrono::seconds(3));
std::string data(900, 'B');
sink.write(data.c_str(), data.size());
return true;
});
});
auto port = 8093;
std::thread t([&]() { svr.listen("localhost", port); });
svr.wait_until_ready();
SSLClient cli("localhost", port);
cli.enable_server_certificate_verification(false);
cli.set_read_timeout(1, 0); // 1 second timeout
auto handle = cli.open_stream("GET", "/slow-stream");
ASSERT_TRUE(handle.is_valid());
char buf[256];
ssize_t total = 0;
ssize_t n;
bool got_error = false;
while ((n = handle.read(buf, sizeof(buf))) > 0) {
total += n;
}
if (n < 0) {
got_error = true;
EXPECT_TRUE(handle.get_read_error() == Error::Timeout ||
handle.get_read_error() == Error::Read)
<< "Actual error: " << to_string(handle.get_read_error());
}
EXPECT_TRUE(got_error || total < 1000)
<< "Expected timeout but got all " << total << " bytes";
svr.stop();
t.join();
}
TEST(ErrorHandlingTest, SSLStreamConnectionClosed) {
// Test SSL connection closed detection
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
std::atomic<bool> close_now{false};
svr.Get("/will-close", [&](const Request &, Response &res) {
res.set_content_provider(
10000, "text/plain",
[&](size_t offset, size_t /*length*/, DataSink &sink) {
if (offset < 100) {
std::string data(100, 'X');
sink.write(data.c_str(), data.size());
return true;
}
while (!close_now) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
return false;
});
});
auto port = 8094;
std::thread t([&]() { svr.listen("localhost", port); });
svr.wait_until_ready();
SSLClient cli("localhost", port);
cli.enable_server_certificate_verification(false);
auto handle = cli.open_stream("GET", "/will-close");
ASSERT_TRUE(handle.is_valid());
char buf[256];
ssize_t n = handle.read(buf, sizeof(buf)); // First read
EXPECT_GT(n, 0);
// Signal server to close
close_now = true;
while ((n = handle.read(buf, sizeof(buf))) > 0) {
// Keep reading
}
if (n < 0) {
EXPECT_TRUE(handle.get_read_error() == Error::ConnectionClosed ||
handle.get_read_error() == Error::Read)
<< "Actual error: " << to_string(handle.get_read_error());
}
svr.stop();
t.join();
}
#endif