diff --git a/README.md b/README.md index e1b63296..85afb62d 100644 --- a/README.md +++ b/README.md @@ -522,7 +522,7 @@ svr.set_error_handler([](const auto& req, auto& res) { ``` ### Exception handler -The exception handler gets called if a user routing handler throws an error. This covers the pre-routing handler, the file request handler, the pre-request handler, route handlers and content readers. An exception from the post-routing handler, the error handler, the `Expect: 100-continue` handler, a content provider, a logger or a WebSocket handler does not reach it: the connection is dropped, and the error logger receives `Error::UserCallbackException`. +The exception handler gets called if a user routing handler throws an error. This covers the pre-routing handler, the file request handler, the pre-request handler, route handlers, content readers and, on a WebSocket upgrade, the sub-protocol selector. The response is a 500 unless the exception handler sets another status. An exception from the post-routing handler, the error handler, the `Expect: 100-continue` handler, a content provider, a logger or a WebSocket handler does not reach it: the connection is dropped, and the error logger receives `Error::UserCallbackException`. ```cpp svr.set_exception_handler([](const auto& req, auto& res, std::exception_ptr ep) { diff --git a/httplib.h b/httplib.h index b92252b3..7eec7a85 100644 --- a/httplib.h +++ b/httplib.h @@ -2394,10 +2394,11 @@ protected: // 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. + // process_request() wraps only routing() and the hooks before a WebSocket + // upgrade 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 @@ -2484,6 +2485,9 @@ private: bool routing(Request &req, Response &res, Stream &strm, bool skip_pre_routing); +#ifndef CPPHTTPLIB_NO_EXCEPTIONS + bool handle_exception(Request &req, Response &res); +#endif bool handle_file_request(Request &req, Response &res); bool check_if_not_modified(const Request &req, Response &res, const std::string &etag, time_t mtime) const; @@ -14660,6 +14664,17 @@ inline bool Server::listen_internal() { return ret; } +#ifndef CPPHTTPLIB_NO_EXCEPTIONS +// Call from a catch block. The response is a 500 unless the exception handler +// sets another status. Returns whether the exception handler ran. +inline bool Server::handle_exception(Request &req, Response &res) { + res.status = StatusCode::InternalServerError_500; + if (!exception_handler_) { return false; } + exception_handler_(req, res, std::current_exception()); + return true; +} +#endif + inline bool Server::routing(Request &req, Response &res, Stream &strm, bool skip_pre_routing) { if (!skip_pre_routing && pre_routing_handler_ && @@ -15233,27 +15248,22 @@ Server::process_request(Stream &strm, const std::string &remote_addr, auto close_if_answered = close_after_response || detail::has_framed_body(req); - if (pre_routing_handler_ && - pre_routing_handler_(req, res) == HandlerResponse::Handled) { - if (res.status == -1) { res.status = StatusCode::OK_200; } - return write_response_with_content(strm, close_if_answered, req, res); - } - // Find matching WebSocket handler - for (const auto &entry : websocket_handlers_) { - if (entry.matcher->match(req)) { + const WebSocketHandlerEntry *ws_route = nullptr; + std::string selected_subprotocol; + auto answer_before_upgrade = [&]() -> bool { + if (pre_routing_handler_ && + pre_routing_handler_(req, res) == HandlerResponse::Handled) { + return true; + } + for (const auto &entry : websocket_handlers_) { + if (!entry.matcher->match(req)) { continue; } req.matched_route = entry.matcher->pattern(); if (pre_request_handler_ && pre_request_handler_(req, res) == HandlerResponse::Handled) { - if (res.status == -1) { res.status = StatusCode::OK_200; } - return write_response_with_content(strm, close_if_answered, req, res); + return true; } + ws_route = &entry; - // Compute accept key - auto client_key = req.get_header_value("Sec-WebSocket-Key"); - auto accept_key = detail::websocket_accept_key(client_key); - - // Negotiate subprotocol - std::string selected_subprotocol; if (entry.sub_protocol_selector) { auto protocol_header = detail::get_combined_header_value( req.headers, "Sec-WebSocket-Protocol"); @@ -15273,57 +15283,78 @@ Server::process_request(Stream &strm, const std::string &remote_addr, } } } + return false; + } + return false; + }; - // Send 101 Switching Protocols - std::string handshake_response = "HTTP/1.1 101 Switching Protocols\r\n" - "Upgrade: websocket\r\n" - "Connection: Upgrade\r\n" - "Sec-WebSocket-Accept: " + - accept_key + "\r\n"; - if (!selected_subprotocol.empty()) { - if (!detail::fields::is_field_value(selected_subprotocol)) { - return false; - } - handshake_response += - "Sec-WebSocket-Protocol: " + selected_subprotocol + "\r\n"; - } - handshake_response += "\r\n"; - if (strm.write(handshake_response.data(), handshake_response.size()) < - 0) { + auto answered = false; +#ifdef CPPHTTPLIB_NO_EXCEPTIONS + answered = answer_before_upgrade(); +#else + try { + answered = answer_before_upgrade(); + } catch (...) { + if (!handle_exception(req, res)) { + return write_response(strm, close_if_answered, req, res); + } + answered = true; + } +#endif + if (answered) { + if (res.status == -1) { res.status = StatusCode::OK_200; } + return write_response_with_content(strm, close_if_answered, req, res); + } + + if (ws_route) { + auto client_key = req.get_header_value("Sec-WebSocket-Key"); + auto accept_key = detail::websocket_accept_key(client_key); + + // Send 101 Switching Protocols + std::string handshake_response = "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: " + + accept_key + "\r\n"; + if (!selected_subprotocol.empty()) { + if (!detail::fields::is_field_value(selected_subprotocol)) { return false; } + handshake_response += + "Sec-WebSocket-Protocol: " + selected_subprotocol + "\r\n"; + } + handshake_response += "\r\n"; + if (strm.write(handshake_response.data(), handshake_response.size()) < + 0) { + return false; + } - connection_closed = true; - if (websocket_upgraded) { *websocket_upgraded = true; } + connection_closed = true; + if (websocket_upgraded) { *websocket_upgraded = true; } - { #ifdef CPPHTTPLIB_SSL_ENABLED - if (req.ssl) { - // wss: the heartbeat ping thread and the read path enter the same - // TLS session from different threads. Hand the WebSocket a stream - // that serializes every TLS call, so the shared SSLSocketStream on - // the plain HTTP/HTTPS paths stays untouched. - auto ws_strm = - std::unique_ptr(new detail::WebSocketSSLStream( - strm.socket(), const_cast(req.ssl), - CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, 0, - write_timeout_sec_, write_timeout_usec_)); - ws::WebSocket ws(std::move(ws_strm), req, true, - websocket_ping_interval_sec_, - websocket_max_missed_pongs_); - entry.handler(req, ws); - return true; - } -#endif - // Use WebSocket-specific read timeout instead of HTTP timeout - strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, - 0); - ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_, - websocket_max_missed_pongs_); - entry.handler(req, ws); - } + if (req.ssl) { + // wss: the heartbeat ping thread and the read path enter the same + // TLS session from different threads. Hand the WebSocket a stream + // that serializes every TLS call, so the shared SSLSocketStream on + // the plain HTTP/HTTPS paths stays untouched. + auto ws_strm = std::unique_ptr(new detail::WebSocketSSLStream( + strm.socket(), const_cast(req.ssl), + CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, 0, + write_timeout_sec_, write_timeout_usec_)); + ws::WebSocket ws(std::move(ws_strm), req, true, + websocket_ping_interval_sec_, + websocket_max_missed_pongs_); + ws_route->handler(req, ws); return true; } +#endif + // Use WebSocket-specific read timeout instead of HTTP timeout + strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, 0); + ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_, + websocket_max_missed_pongs_); + ws_route->handler(req, ws); + return true; } // No WebSocket route: route it as a plain HTTP request } @@ -15335,23 +15366,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr, #else try { routed = routing(req, res, strm, is_upgrade); - } catch (std::exception &) { - if (exception_handler_) { - auto ep = std::current_exception(); - exception_handler_(req, res, ep); - routed = true; - } else { - res.status = StatusCode::InternalServerError_500; - } - } catch (...) { - if (exception_handler_) { - auto ep = std::current_exception(); - exception_handler_(req, res, ep); - routed = true; - } else { - res.status = StatusCode::InternalServerError_500; - } - } + } catch (...) { routed = handle_exception(req, res); } #endif auto ret = false; if (routed) { diff --git a/test/test.cc b/test/test.cc index f792f17f..8d34661f 100644 --- a/test/test.cc +++ b/test/test.cc @@ -5342,6 +5342,38 @@ TEST(ExceptionTest, WithExceptionHandler) { } } +TEST(ExceptionTest, StatusIs500UnlessExceptionHandlerSetsIt) { + Server svr; + svr.set_exception_handler( + [](const Request &, Response &res, std::exception_ptr) { + res.set_content("oops", "text/plain"); + }); + svr.Get("/throw", [](const Request &, Response &) { + throw std::runtime_error("throw"); + }); + svr.Get("/created-then-throw", [](const Request &, Response &res) { + res.status = StatusCode::Created_201; + throw std::runtime_error("created-then-throw"); + }); + + auto port = svr.bind_to_any_port(HOST); + ASSERT_GT(port, 0); + 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(); + + Client cli(HOST, port); + for (auto path : {"/throw", "/created-then-throw"}) { + auto res = cli.Get(path); + ASSERT_TRUE(res) << path << ": " << to_string(res.error()); + EXPECT_EQ(StatusCode::InternalServerError_500, res->status) << path; + } +} + TEST(ExceptionTest, AndErrorHandler) { Server svr; @@ -5404,13 +5436,12 @@ 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. +// An exception from a user callback outside process_request()'s try/catch +// 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); }; @@ -28121,6 +28152,87 @@ TEST(WebSocketPreRequestTest, RejectWithoutAuth) { t.join(); } +#ifndef CPPHTTPLIB_NO_EXCEPTIONS +static Headers websocket_upgrade_headers() { + return {{"Upgrade", "websocket"}, + {"Connection", "Upgrade"}, + {"Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ=="}, + {"Sec-WebSocket-Version", "13"}, + {"Sec-WebSocket-Protocol", "chat"}}; +} + +TEST(WebSocketExceptionTest, HooksBeforeUpgradeReachExceptionHandler) { + Server svr; + svr.set_pre_routing_handler([](const Request &req, Response &) { + if (req.path == "/pre-routing") { throw std::runtime_error(req.path); } + return Server::HandlerResponse::Unhandled; + }); + svr.set_pre_request_handler([](const Request &req, Response &) { + if (req.path == "/pre-request") { throw std::runtime_error(req.path); } + return Server::HandlerResponse::Unhandled; + }); + svr.set_exception_handler( + [](const Request &, Response &res, std::exception_ptr ep) { + try { + std::rethrow_exception(ep); + } catch (std::exception &e) { res.set_content(e.what(), "text/plain"); } + }); + + auto handler = [](const Request &, ws::WebSocket &) {}; + svr.WebSocket("/pre-routing", handler); + svr.WebSocket("/pre-request", handler); + svr.WebSocket("/sub-protocol", handler, + [](const std::vector &) -> std::string { + throw std::runtime_error("/sub-protocol"); + }); + + auto port = svr.bind_to_any_port(HOST); + ASSERT_GT(port, 0); + 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(); + + Client cli(HOST, port); + for (auto path : {"/pre-routing", "/pre-request", "/sub-protocol"}) { + auto res = cli.Get(path, websocket_upgrade_headers()); + if (!res) { + ADD_FAILURE() << path << ": " << to_string(res.error()); + continue; + } + EXPECT_EQ(StatusCode::InternalServerError_500, res->status) << path; + EXPECT_EQ(path, res->body); + } +} + +TEST(WebSocketExceptionTest, HookBeforeUpgradeWithoutExceptionHandler) { + Server svr; + svr.set_pre_routing_handler( + [](const Request &, Response &) -> Server::HandlerResponse { + throw std::runtime_error("pre-routing"); + }); + svr.WebSocket("/ws", [](const Request &, ws::WebSocket &) {}); + + auto port = svr.bind_to_any_port(HOST); + ASSERT_GT(port, 0); + 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(); + + Client cli(HOST, port); + auto res = cli.Get("/ws", websocket_upgrade_headers()); + ASSERT_TRUE(res) << to_string(res.error()); + EXPECT_EQ(StatusCode::InternalServerError_500, res->status); +} +#endif + TEST(WebSocketServerTimeoutTest, HandlerSendsWhileNothingArrives) { Server svr;