Where every error status comes from, how handler exceptions become responses, and what you can customize. Decision record:
specs/architecture/11-decisions/DR-009.md· cross-cutting §5.2:05-cross-cutting.md.
DR-009 chose Option 1 — no throw-as-status idiom. There is no http_error type. To return a 404/400 you build a response by value:
return http_response::empty().with_status(404);Any uncaught exception from a handler becomes a 500, routed through an optional internal_error_handler escape hatch. The six-point rule:
- Handler throws
std::exception→ caught, logged vialog_error,internal_error_handlerinvoked withe.what(). Default body is the fixed string"Internal Server Error"(CWE-209) — the verbatim message reaches only the log and a configured handler. - Handler throws non-
std::exception→catch (...), message sentinel"unknown exception", same fixed default body. - Library-internal dispatch exception (allocation, body materialization) → same path.
internal_error_handleritself throws → logged, hardcoded empty-body 500.feature_unavailableis a plainstd::runtime_error— no special status mapping.- No throw-as-status.
No-throw boundary: the exception→status translation is entirely internal C++; nothing new crosses the ABI. A response always reaches MHD — the four terminal producers are noexcept or exception-contained.
| Status | Producer | Trigger | Customize with |
|---|---|---|---|
| 404 | error_pages::not_found_page |
route lookup miss in finalize_answer (also the invalid_argument fallback arm of get_raw_response_with_fallback) |
not_found_handler |
| 405 | error_pages::method_not_allowed_page |
is_allowed(method) false in dispatch_resource_handler; Allow: header from hrm->get_allow_header() |
method_not_allowed_handler |
500 (default "Internal Server Error") |
error_pages::internal_error_page (force_our=false) |
handler/dispatch exception, null-materialize, -1 sentinel response |
internal_error_handler, expose_exception_messages |
500 (body = e.what()) |
same, when expose_exception_messages==true |
dev opt-in only | — |
| 500 (hardcoded empty body) | internal_error_page(force_our=true) |
double-fault: user handler threw, or belt-and-suspenders sites | — |
| 401 (Basic / scheme) | http_response::unauthorized(scheme, realm, body) |
user-returned; builds WWW-Authenticate: <scheme> realm="…" (control-char rejection, quoted-string escaping) |
your handler |
| 401 (Digest) | http_response::unauthorized(digest_challenge) → MHD_queue_auth_required_response3 |
user-returned; body_kind::digest_challenge (needs HAVE_DAUTH) |
your handler |
| 400 (bad WS handshake) | websocket_upgrader::try_handle |
Upgrade: websocket present but validate_websocket_handshake fails (bad Connection / Sec-WebSocket-Version ≠ 13 / empty key) |
— |
| 101 (success, not an error) | complete_websocket_upgrade |
successful WebSocket upgrade | — |
Default bodies (constants.hpp): "Not Found", "Method not Allowed", "Internal Server Error".
flowchart TD
H["handler throws"] --> C{"catch in<br/>dispatch_resource_handler"}
C --> L["log_dispatch_error · verbatim to log_error"]
L --> HDE{"handler_exception hooks<br/>or alias present?"}
HDE -- yes --> FS["fire_handler_exception<br/>server-wide, then per-route"]
FS --> R{"a hook returned<br/>respond_with?"}
R -- yes --> OUT["that response is sent"]
R -- no --> FORCE["internal_error_page force_our<br/>empty-body 500"]
HDE -- no --> SAFE["run_internal_error_handler_safely"]
SAFE --> IEH{"internal_error_handler set?"}
IEH -- yes --> UH["user handler(request, message)"]
IEH -- no --> EXP{"expose_exception_messages?"}
EXP -- yes --> BODYW["body = message"]
EXP -- no --> BODYD["body = Internal Server Error"]
UH -. throws .-> FORCE
Notable details:
-1sentinel:http_response::status_code_defaults to-1. A returned response withget_status()==-1is treated as null and rerouted throughrun_internal_error_handler_safely(…, "handler returned null response").- A throwing
handler_exceptionhook does not abort the chain — it is logged and the next hook runs (the chain's job is recovery). Every other phase's throwing hook is routed back through the normal DR-009 500 path. run_internal_error_handler_safelywraps the userinternal_error_handlerin try/catch; if it throws, it logs and falls back to the hardcoded empty-body 500. No exception escapes.log_dispatch_error(noexcept) is the single internal logging channel: no-op iflog_erroris unset, otherwise forwards the message verbatim (flag-independent) inside atry/catchthat swallows a misbehaving logger.
| Knob | Signature / default | Effect |
|---|---|---|
not_found_handler |
std::function<http_response(const http_request&)>, default none |
replaces the 404 body |
method_not_allowed_handler |
std::function<http_response(const http_request&)>, default none |
replaces the 405 body (Allow: still added) |
internal_error_handler |
std::function<http_response(const http_request&, std::string_view message)>, default none |
invoked with the originating message for 500 |
expose_exception_messages |
bool, default false |
dev only — default-500 body becomes the originating message instead of "Internal Server Error" (CWE-209) |
expose_credentials_in_logs |
bool, default false |
when true, http_request::operator<< streams passwords / Authorization / cookies verbatim instead of <redacted> (CWE-312/532) |
Message-exposure matrix: the verbatim message always goes to log_error and to a configured internal_error_handler; it reaches the wire only under expose_exception_messages(true).
class feature_unavailable : public std::runtime_error — always compiled, regardless of HAVE_*. Thrown when a feature-gated API is used on a build without that feature (see features & build matrix). Its reach depends on the call site:
- Thrown at
webserverconstruction or from setup / registration APIs → propagates to the caller (your application code). - Thrown from inside a handler (e.g.
unauthorized(digest_challenge)on a non-digest build, or awebsocket_sessionsend) → it is a normalstd::exceptionon the dispatch path → generic 500 to the client, unless aninternal_error_handlerspecial-cases it.
See also: request-flow (where 404/405/500 sit in the dispatch sequence) · hooks cookbook (the handler_exception phase).