mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-23 11:54:42 +00:00
Answer 500 to an exception the exception handler gives no status
An exception handler that set no status sent its response with 200, or with whatever status the throwing handler had set. The status is now set to 500 before the exception handler runs, as ASP.NET Core does, so it stays 500 unless the exception handler sets another one. On a WebSocket upgrade, an exception from the pre-routing handler, the pre-request handler or the sub-protocol selector skipped the exception handler and dropped the connection without a response. Everything before the protocol switches now goes through the same exception handling as routing, so it is answered over HTTP. The WebSocket handler itself still runs outside it.
This commit is contained in:
1 parent
64b5c68a46
commit
b8ae3e43d5
3 files changed
+215
-88
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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<Stream>(new detail::WebSocketSSLStream(
|
||||
strm.socket(), const_cast<tls::session_t>(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<Stream>(new detail::WebSocketSSLStream(
|
||||
strm.socket(), const_cast<tls::session_t>(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) {
|
||||
|
||||
+119
-7
@@ -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> &) -> 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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user