mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-17 10:59:39 +00:00
Stop a throwing user callback from terminating the server (#2564)
Server::process_request() wraps only routing() in a try/catch. Everything else the user supplies runs outside it: - the content provider, from write_response_core() - post_routing_handler_, error_handler_, logger_ - expect_100_continue_handler_ - a WebSocket handler, and pre_routing_handler_ on the upgrade path An exception from any of those unwinds out of process_and_close_socket() into the task queue, which calls the job without a catch, so it reaches the top of a pool thread and terminates the process. One handler that throws takes down every other connection the server is holding. Add Server::serve_guarded() and run the serving loop through it in both process_and_close_socket() overloads. The exception is not turned into a 500: by the time a content provider runs, the status line and headers are already on the wire, so there is nothing left to replace. Report it through the error logger as Error::UserCallbackException and drop the connection, which is what the peer observes regardless. Requests on other connections are unaffected, and the socket is still drained and closed - which unwinding used to skip on the non-SSL path, since drain_and_close_socket() sits after the call rather than in a scope guard. The error logger is a user callback too, so the report inside the guard is itself wrapped: a throwing logger must not be able to open the guard back up. Adds ServerExceptionTest: a throwing content provider, post-routing handler, WebSocket handler and error logger, plus the content provider case against SSLServer, each checking that a later request on a new connection still succeeds. Every test runs the server on a single worker thread, so a guard that catches the exception but still loses the thread shows up as the follow-up request never being served. Note that all of them abort the test binary without this change - which is the bug, but it means a regression here fails the run rather than one test.
This commit is contained in:
1 parent
ae417b405a
commit
2addb41089
2 files changed
+328
-19
No files matched your search
@@ -1834,6 +1834,7 @@ enum class Error {
|
|||||||
InvalidRangeHeader,
|
InvalidRangeHeader,
|
||||||
UnsupportedContentEncoding,
|
UnsupportedContentEncoding,
|
||||||
WebSocketHandshake,
|
WebSocketHandshake,
|
||||||
|
UserCallbackException,
|
||||||
|
|
||||||
// For internal use only
|
// For internal use only
|
||||||
SSLPeerCouldBeClosed_,
|
SSLPeerCouldBeClosed_,
|
||||||
@@ -2224,6 +2225,35 @@ protected:
|
|||||||
const std::function<void(Request &)> &setup_request,
|
const std::function<void(Request &)> &setup_request,
|
||||||
bool *websocket_upgraded = nullptr);
|
bool *websocket_upgraded = nullptr);
|
||||||
|
|
||||||
|
// Runs the per-connection serving loop and stops an exception thrown by a
|
||||||
|
// user callback from escaping the worker thread.
|
||||||
|
//
|
||||||
|
// process_request() wraps only routing() in a try/catch. Content providers,
|
||||||
|
// the post-routing, error, logging and expect-100 handlers and WebSocket
|
||||||
|
// handlers all run outside it, and the task queue calls the job without a
|
||||||
|
// catch, so an exception from any of those would terminate the process.
|
||||||
|
//
|
||||||
|
// No 500 is possible here: by the time a content provider runs, the status
|
||||||
|
// line and headers are already on the wire. Report it through the error
|
||||||
|
// logger and drop the connection, which is what the peer observes either
|
||||||
|
// way. Other connections are unaffected.
|
||||||
|
template <typename Serve> bool serve_guarded(Serve &&serve) const {
|
||||||
|
#ifdef CPPHTTPLIB_NO_EXCEPTIONS
|
||||||
|
return serve();
|
||||||
|
#else
|
||||||
|
try {
|
||||||
|
return serve();
|
||||||
|
} catch (...) {
|
||||||
|
// The error logger is a user callback too, so it must not be able to
|
||||||
|
// throw the guard back open.
|
||||||
|
try {
|
||||||
|
output_error_log(Error::UserCallbackException, nullptr);
|
||||||
|
} catch (...) {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
std::atomic<socket_t> svr_sock_{INVALID_SOCKET};
|
std::atomic<socket_t> svr_sock_{INVALID_SOCKET};
|
||||||
|
|
||||||
std::vector<std::string> trusted_proxies_;
|
std::vector<std::string> trusted_proxies_;
|
||||||
@@ -10740,6 +10770,7 @@ inline std::string to_string(const Error error) {
|
|||||||
case Error::InvalidRangeHeader: return "Invalid Range header";
|
case Error::InvalidRangeHeader: return "Invalid Range header";
|
||||||
case Error::UnsupportedContentEncoding: return "Unsupported Content-Encoding";
|
case Error::UnsupportedContentEncoding: return "Unsupported Content-Encoding";
|
||||||
case Error::WebSocketHandshake: return "WebSocket handshake failed";
|
case Error::WebSocketHandshake: return "WebSocket handshake failed";
|
||||||
|
case Error::UserCallbackException: return "User callback threw an exception";
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14159,15 +14190,18 @@ inline bool Server::process_and_close_socket(socket_t sock) {
|
|||||||
detail::get_local_ip_and_port(sock, local_addr, local_port);
|
detail::get_local_ip_and_port(sock, local_addr, local_port);
|
||||||
|
|
||||||
bool websocket_upgraded = false;
|
bool websocket_upgraded = false;
|
||||||
auto ret = detail::process_server_socket(
|
auto ret = serve_guarded([&]() {
|
||||||
svr_sock_, sock, keep_alive_max_count_, keep_alive_timeout_sec_,
|
return detail::process_server_socket(
|
||||||
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
|
svr_sock_, sock, keep_alive_max_count_, keep_alive_timeout_sec_,
|
||||||
write_timeout_usec_,
|
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
|
||||||
[&](Stream &strm, bool close_connection, bool &connection_closed) {
|
write_timeout_usec_,
|
||||||
return process_request(strm, remote_addr, remote_port, local_addr,
|
[&](Stream &strm, bool close_connection, bool &connection_closed) {
|
||||||
local_port, close_connection, connection_closed,
|
return process_request(strm, remote_addr, remote_port, local_addr,
|
||||||
nullptr, &websocket_upgraded);
|
local_port, close_connection,
|
||||||
});
|
connection_closed, nullptr,
|
||||||
|
&websocket_upgraded);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
detail::drain_and_close_socket(sock);
|
detail::drain_and_close_socket(sock);
|
||||||
return ret;
|
return ret;
|
||||||
@@ -17567,16 +17601,18 @@ inline bool SSLServer::process_and_close_socket(socket_t sock) {
|
|||||||
int local_port = 0;
|
int local_port = 0;
|
||||||
detail::get_local_ip_and_port(sock, local_addr, local_port);
|
detail::get_local_ip_and_port(sock, local_addr, local_port);
|
||||||
|
|
||||||
ret = detail::process_server_socket_ssl(
|
ret = serve_guarded([&]() {
|
||||||
svr_sock_, session, sock, keep_alive_max_count_, keep_alive_timeout_sec_,
|
return detail::process_server_socket_ssl(
|
||||||
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
|
svr_sock_, session, sock, keep_alive_max_count_,
|
||||||
write_timeout_usec_,
|
keep_alive_timeout_sec_, read_timeout_sec_, read_timeout_usec_,
|
||||||
[&](Stream &strm, bool close_connection, bool &connection_closed) {
|
write_timeout_sec_, write_timeout_usec_,
|
||||||
return process_request(
|
[&](Stream &strm, bool close_connection, bool &connection_closed) {
|
||||||
strm, remote_addr, remote_port, local_addr, local_port,
|
return process_request(
|
||||||
close_connection, connection_closed,
|
strm, remote_addr, remote_port, local_addr, local_port,
|
||||||
[&](Request &req) { req.ssl = session; }, &websocket_upgraded);
|
close_connection, connection_closed,
|
||||||
});
|
[&](Request &req) { req.ssl = session; }, &websocket_upgraded);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|||||||
+273
@@ -4348,6 +4348,279 @@ TEST(ExceptionTest, AndErrorHandler) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
|
||||||
|
// process_request() wraps only routing() in a try/catch, so an exception from
|
||||||
|
// any other user callback used to unwind into the task queue - which does not
|
||||||
|
// catch - and terminate the process. Each test below runs the server on a
|
||||||
|
// single worker thread: a guard that catches the exception but still loses the
|
||||||
|
// thread would otherwise be hidden by a spare worker serving the follow-up
|
||||||
|
// request. Note that a regression here aborts the whole test binary rather
|
||||||
|
// than failing one test.
|
||||||
|
TEST(ServerExceptionTest, ThrowingContentProviderDoesNotKillTheServer) {
|
||||||
|
Server svr;
|
||||||
|
svr.new_task_queue = [] { return new ThreadPool(1); };
|
||||||
|
|
||||||
|
std::atomic<int> reported{0};
|
||||||
|
svr.set_error_logger([&](const Error &err, const Request * /*req*/) {
|
||||||
|
if (err == Error::UserCallbackException) { reported++; }
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.Get("/throw", [](const Request & /*req*/, Response &res) {
|
||||||
|
res.set_content_provider(
|
||||||
|
1024, "text/plain",
|
||||||
|
[](size_t /*offset*/, size_t /*length*/, DataSink &sink) -> bool {
|
||||||
|
sink.write("hello", 5);
|
||||||
|
throw std::runtime_error("from the content provider");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.Get("/ok", [](const Request & /*req*/, Response &res) {
|
||||||
|
res.set_content("ok", "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port(HOST);
|
||||||
|
auto listen_thread = std::thread([&svr]() { svr.listen_after_bind(); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
listen_thread.join();
|
||||||
|
ASSERT_FALSE(svr.is_running());
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
{
|
||||||
|
Client cli(HOST, port);
|
||||||
|
cli.set_read_timeout(5, 0);
|
||||||
|
EXPECT_FALSE(cli.Get("/throw"));
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_TRUE(svr.is_running());
|
||||||
|
EXPECT_EQ(1, reported.load());
|
||||||
|
|
||||||
|
// A request on a fresh connection is still served.
|
||||||
|
{
|
||||||
|
Client cli(HOST, port);
|
||||||
|
auto res = cli.Get("/ok");
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
|
EXPECT_EQ("ok", res->body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ServerExceptionTest, ThrowingPostRoutingHandlerDoesNotKillTheServer) {
|
||||||
|
Server svr;
|
||||||
|
svr.new_task_queue = [] { return new ThreadPool(1); };
|
||||||
|
|
||||||
|
std::atomic<bool> should_throw{true};
|
||||||
|
|
||||||
|
svr.Get("/hi", [](const Request & /*req*/, Response &res) {
|
||||||
|
res.set_content("hi", "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.set_post_routing_handler(
|
||||||
|
[&](const Request & /*req*/, Response & /*res*/) {
|
||||||
|
if (should_throw) {
|
||||||
|
throw std::runtime_error("from the post-routing handler");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port(HOST);
|
||||||
|
auto listen_thread = std::thread([&svr]() { svr.listen_after_bind(); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
listen_thread.join();
|
||||||
|
ASSERT_FALSE(svr.is_running());
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
{
|
||||||
|
Client cli(HOST, port);
|
||||||
|
cli.set_read_timeout(5, 0);
|
||||||
|
EXPECT_FALSE(cli.Get("/hi"));
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_TRUE(svr.is_running());
|
||||||
|
|
||||||
|
should_throw = false;
|
||||||
|
|
||||||
|
{
|
||||||
|
Client cli(HOST, port);
|
||||||
|
auto res = cli.Get("/hi");
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
|
EXPECT_EQ("hi", res->body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ServerExceptionTest, ThrowingErrorLoggerDoesNotReopenTheGuard) {
|
||||||
|
// The guard reports the caught exception through the error logger, which is
|
||||||
|
// a user callback too. A logger that throws has to be contained as well, or
|
||||||
|
// the process would terminate from inside the catch.
|
||||||
|
Server svr;
|
||||||
|
svr.new_task_queue = [] { return new ThreadPool(1); };
|
||||||
|
|
||||||
|
std::atomic<int> reported{0};
|
||||||
|
svr.set_error_logger([&](const Error &err, const Request * /*req*/) {
|
||||||
|
if (err == Error::UserCallbackException) {
|
||||||
|
reported++;
|
||||||
|
throw std::runtime_error("from the error logger");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.Get("/throw", [](const Request & /*req*/, Response &res) {
|
||||||
|
res.set_content_provider(
|
||||||
|
1024, "text/plain",
|
||||||
|
[](size_t /*offset*/, size_t /*length*/, DataSink &sink) -> bool {
|
||||||
|
sink.write("hello", 5);
|
||||||
|
throw std::runtime_error("from the content provider");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.Get("/ok", [](const Request & /*req*/, Response &res) {
|
||||||
|
res.set_content("ok", "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port(HOST);
|
||||||
|
auto listen_thread = std::thread([&svr]() { svr.listen_after_bind(); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
listen_thread.join();
|
||||||
|
ASSERT_FALSE(svr.is_running());
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
{
|
||||||
|
Client cli(HOST, port);
|
||||||
|
cli.set_read_timeout(5, 0);
|
||||||
|
EXPECT_FALSE(cli.Get("/throw"));
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_TRUE(svr.is_running());
|
||||||
|
EXPECT_EQ(1, reported.load());
|
||||||
|
|
||||||
|
{
|
||||||
|
Client cli(HOST, port);
|
||||||
|
auto res = cli.Get("/ok");
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
|
EXPECT_EQ("ok", res->body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ServerExceptionTest, ThrowingWebSocketHandlerDoesNotKillTheServer) {
|
||||||
|
// A WebSocket handler runs on the upgrade path after the 101 has been sent,
|
||||||
|
// so the exception unwinds through the ws::WebSocket object on its way to
|
||||||
|
// the guard. None of the HTTP cases above cross that.
|
||||||
|
Server svr;
|
||||||
|
svr.new_task_queue = [] { return new ThreadPool(1); };
|
||||||
|
|
||||||
|
std::atomic<int> reported{0};
|
||||||
|
svr.set_error_logger([&](const Error &err, const Request * /*req*/) {
|
||||||
|
if (err == Error::UserCallbackException) { reported++; }
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.WebSocket("/ws", [](const Request & /*req*/, ws::WebSocket & /*ws*/) {
|
||||||
|
throw std::runtime_error("from the WebSocket handler");
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.Get("/ok", [](const Request & /*req*/, Response &res) {
|
||||||
|
res.set_content("ok", "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port(HOST);
|
||||||
|
auto listen_thread = std::thread([&svr]() { svr.listen_after_bind(); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
listen_thread.join();
|
||||||
|
ASSERT_FALSE(svr.is_running());
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
{
|
||||||
|
ws::WebSocketClient client("ws://localhost:" + std::to_string(port) +
|
||||||
|
"/ws");
|
||||||
|
auto res = client.connect();
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
|
||||||
|
// The server dropped the connection instead of dying.
|
||||||
|
std::string msg;
|
||||||
|
EXPECT_FALSE(client.read(msg));
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_TRUE(svr.is_running());
|
||||||
|
EXPECT_EQ(1, reported.load());
|
||||||
|
|
||||||
|
{
|
||||||
|
Client cli(HOST, port);
|
||||||
|
auto res = cli.Get("/ok");
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
|
EXPECT_EQ("ok", res->body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
|
TEST(ServerExceptionTest, ThrowingContentProviderDoesNotKillTheSSLServer) {
|
||||||
|
// SSLServer::process_and_close_socket() is a separate overload with its own
|
||||||
|
// guard, so it is exercised on its own.
|
||||||
|
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||||
|
ASSERT_TRUE(svr.is_valid());
|
||||||
|
svr.new_task_queue = [] { return new ThreadPool(1); };
|
||||||
|
|
||||||
|
std::atomic<int> reported{0};
|
||||||
|
svr.set_error_logger([&](const Error &err, const Request * /*req*/) {
|
||||||
|
if (err == Error::UserCallbackException) { reported++; }
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.Get("/throw", [](const Request & /*req*/, Response &res) {
|
||||||
|
res.set_content_provider(
|
||||||
|
1024, "text/plain",
|
||||||
|
[](size_t /*offset*/, size_t /*length*/, DataSink &sink) -> bool {
|
||||||
|
sink.write("hello", 5);
|
||||||
|
throw std::runtime_error("from the content provider");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.Get("/ok", [](const Request & /*req*/, Response &res) {
|
||||||
|
res.set_content("ok", "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port(HOST);
|
||||||
|
auto listen_thread = std::thread([&svr]() { svr.listen_after_bind(); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
listen_thread.join();
|
||||||
|
ASSERT_FALSE(svr.is_running());
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
{
|
||||||
|
SSLClient cli(HOST, port);
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
|
cli.set_read_timeout(5, 0);
|
||||||
|
EXPECT_FALSE(cli.Get("/throw"));
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_TRUE(svr.is_running());
|
||||||
|
EXPECT_EQ(1, reported.load());
|
||||||
|
|
||||||
|
{
|
||||||
|
SSLClient cli(HOST, port);
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
|
auto res = cli.Get("/ok");
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
|
EXPECT_EQ("ok", res->body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
TEST(NoContentTest, ContentLength) {
|
TEST(NoContentTest, ContentLength) {
|
||||||
Server svr;
|
Server svr;
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user