diff --git a/httplib.h b/httplib.h index 0bda8477..e22f2f15 100644 --- a/httplib.h +++ b/httplib.h @@ -1834,6 +1834,7 @@ enum class Error { InvalidRangeHeader, UnsupportedContentEncoding, WebSocketHandshake, + UserCallbackException, // For internal use only SSLPeerCouldBeClosed_, @@ -2224,6 +2225,35 @@ protected: const std::function &setup_request, 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 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 svr_sock_{INVALID_SOCKET}; std::vector trusted_proxies_; @@ -10740,6 +10770,7 @@ inline std::string to_string(const Error error) { case Error::InvalidRangeHeader: return "Invalid Range header"; case Error::UnsupportedContentEncoding: return "Unsupported Content-Encoding"; case Error::WebSocketHandshake: return "WebSocket handshake failed"; + case Error::UserCallbackException: return "User callback threw an exception"; 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); bool websocket_upgraded = false; - auto ret = detail::process_server_socket( - svr_sock_, sock, keep_alive_max_count_, keep_alive_timeout_sec_, - read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, - write_timeout_usec_, - [&](Stream &strm, bool close_connection, bool &connection_closed) { - return process_request(strm, remote_addr, remote_port, local_addr, - local_port, close_connection, connection_closed, - nullptr, &websocket_upgraded); - }); + auto ret = serve_guarded([&]() { + return detail::process_server_socket( + svr_sock_, sock, keep_alive_max_count_, keep_alive_timeout_sec_, + read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, + write_timeout_usec_, + [&](Stream &strm, bool close_connection, bool &connection_closed) { + return process_request(strm, remote_addr, remote_port, local_addr, + local_port, close_connection, + connection_closed, nullptr, + &websocket_upgraded); + }); + }); detail::drain_and_close_socket(sock); return ret; @@ -17567,16 +17601,18 @@ inline bool SSLServer::process_and_close_socket(socket_t sock) { int local_port = 0; detail::get_local_ip_and_port(sock, local_addr, local_port); - ret = detail::process_server_socket_ssl( - svr_sock_, session, sock, keep_alive_max_count_, keep_alive_timeout_sec_, - read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, - write_timeout_usec_, - [&](Stream &strm, bool close_connection, bool &connection_closed) { - return process_request( - strm, remote_addr, remote_port, local_addr, local_port, - close_connection, connection_closed, - [&](Request &req) { req.ssl = session; }, &websocket_upgraded); - }); + ret = serve_guarded([&]() { + return detail::process_server_socket_ssl( + svr_sock_, session, sock, keep_alive_max_count_, + keep_alive_timeout_sec_, read_timeout_sec_, read_timeout_usec_, + write_timeout_sec_, write_timeout_usec_, + [&](Stream &strm, bool close_connection, bool &connection_closed) { + return process_request( + strm, remote_addr, remote_port, local_addr, local_port, + close_connection, connection_closed, + [&](Request &req) { req.ssl = session; }, &websocket_upgraded); + }); + }); return ret; } diff --git a/test/test.cc b/test/test.cc index a80719e3..ead102ac 100644 --- a/test/test.cc +++ b/test/test.cc @@ -4348,6 +4348,279 @@ TEST(ExceptionTest, AndErrorHandler) { } #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 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 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 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 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 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) { Server svr;