@@ -95,6 +95,7 @@ The block above is reproduced byte-for-byte from
9595* [ WebSocket support] ( #websocket-support )
9696* [ Daemon introspection and external event loops] ( #daemon-introspection-and-external-event-loops )
9797* [ HTTP utils] ( #http-utils )
98+ * [ Lifecycle hooks] ( #lifecycle-hooks )
9899* [ Threading contract] ( #threading-contract )
99100* [ Error propagation] ( #error-propagation )
100101* [ Feature availability] ( #feature-availability )
@@ -1510,6 +1511,93 @@ The following utility functions are available on `http::http_utils`:
15101511
15111512[ Back to TOC] ( #table-of-contents )
15121513
1514+ ## Lifecycle hooks
1515+
1516+ The hook bus is the single extension surface for observing or
1517+ short-circuiting the request lifecycle. It replaces v1's single-slot
1518+ patchwork (one ` log_access ` callback, one ` not_found_handler ` , one
1519+ ` method_not_allowed_handler ` , one ` internal_error_handler ` , one
1520+ ` auth_handler ` ) with eleven distinct phases spanning connection,
1521+ request, routing, handler, and response. Each phase accepts multiple
1522+ subscribers and is observable both server-wide (via ` webserver::add_hook ` )
1523+ and, for the five post-route-resolution phases, per-route (via
1524+ ` http_resource::add_hook ` ). The contract is captured in DR-012 and
1525+ [ ` specs/architecture/04-components/hooks.md ` ] ( specs/architecture/04-components/hooks.md ) .
1526+
1527+ Each call returns a move-only ` hook_handle ` . The handle's destructor
1528+ removes the registration; ` hook_handle::detach() ` disarms the destructor
1529+ so the registration persists for the webserver's lifetime.
1530+
1531+ ### Phases
1532+
1533+ | Phase | Fires at | Short-circuit | Per-route eligible |
1534+ | -------| ----------| ---------------| --------------------|
1535+ | ` connection_opened ` | New TCP / TLS connection accepted by MHD | No | No |
1536+ | ` accept_decision ` | After the default-policy / block-list verdict; the connection has been accepted or denied | No | No |
1537+ | ` connection_closed ` | Connection torn down (peer close or server close) | No | No |
1538+ | ` request_received ` | Request line and headers parsed, body not yet consumed | Yes (` hook_action ` ) | No |
1539+ | ` body_chunk ` | Each upload-body chunk delivered by MHD | Yes (` hook_action ` ) | No |
1540+ | ` route_resolved ` | After URL → resource resolution; carries the matched route or "no match" | No | No |
1541+ | ` before_handler ` | After route resolution and method check, immediately before the handler runs | Yes (` hook_action ` ) | Yes |
1542+ | ` handler_exception ` | Exception escapes the handler, before ` internal_error_handler ` is consulted | Yes (` hook_action ` ) | Yes |
1543+ | ` after_handler ` | Handler returned a response, before it is queued on the wire (mutation point) | Yes (` hook_action ` ) | Yes |
1544+ | ` response_sent ` | Response handed to ` MHD_queue_response ` ; carries ` status ` , ` bytes_queued ` , ` elapsed ` | No | Yes |
1545+ | ` request_completed ` | Request lifecycle finished (success or failure); last hook to fire per request | No | Yes |
1546+
1547+ ### Short-circuit semantics
1548+
1549+ Phases marked "Short-circuit" return a ` hook_action ` :
1550+ ` hook_action::pass() ` lets the chain continue;
1551+ ` hook_action::respond_with(response) ` aborts the chain at that
1552+ position. The wrapped response is sent on the wire in place of any
1553+ handler output. Subsequent hooks in the same phase are not invoked.
1554+ Observation-only phases (` connection_opened ` , ` accept_decision ` ,
1555+ ` connection_closed ` , ` route_resolved ` , ` response_sent ` ,
1556+ ` request_completed ` ) ignore the return; the chain always runs to
1557+ completion.
1558+
1559+ ### Per-route hooks
1560+
1561+ ` http_resource::add_hook(phase, fn) ` accepts only the five
1562+ post-route-resolution phases: ` before_handler ` , ` handler_exception ` ,
1563+ ` after_handler ` , ` response_sent ` , ` request_completed ` . Per-route hooks
1564+ fire after the server-wide chain at the same phase, and only if that
1565+ server-wide chain did not short-circuit. Passing any other phase
1566+ throws ` std::invalid_argument ` naming the rejected phase. See
1567+ [ ` examples/per_route_auth.cpp ` ] ( examples/per_route_auth.cpp ) for a
1568+ worked example.
1569+
1570+ ### Examples
1571+
1572+ * [ ` examples/banned_ip_log.cpp ` ] ( examples/banned_ip_log.cpp ) — log every
1573+ banned-IP rejection via a single ` accept_decision ` hook.
1574+ * [ ` examples/early_413.cpp ` ] ( examples/early_413.cpp ) — return 413 from
1575+ a ` request_received ` hook before the body is consumed.
1576+ * [ ` examples/clf_access_log.cpp ` ] ( examples/clf_access_log.cpp ) — write
1577+ a Common Log Format access line from a ` response_sent ` hook.
1578+ * [ ` examples/per_route_auth.cpp ` ] ( examples/per_route_auth.cpp ) —
1579+ per-route HTTP Basic auth via ` http_resource::add_hook(before_handler, ...) ` .
1580+
1581+ ### v1 aliases
1582+
1583+ Each of the v1 single-slot setters is an alias for an ` add_hook ` call
1584+ at the corresponding phase. The aliases survive for ergonomic call
1585+ sites; new code can use either form.
1586+
1587+ | v1 setter | Equivalent ` add_hook ` call |
1588+ | -----------| ----------------------------|
1589+ | ` log_access(fn) ` | ` ws.add_hook(hook_phase::response_sent, fn) ` |
1590+ | ` not_found_handler(fn) ` | ` ws.add_hook(hook_phase::route_resolved, fn) ` |
1591+ | ` method_not_allowed_handler(fn) ` | ` ws.add_hook(hook_phase::before_handler, fn) ` |
1592+ | ` internal_error_handler(fn) ` | ` ws.add_hook(hook_phase::handler_exception, fn) ` |
1593+ | ` auth_handler(fn) ` | ` ws.add_hook(hook_phase::before_handler, fn) ` |
1594+
1595+ The aliases install observation-stub hooks under the same dispatch
1596+ plumbing, so the on-the-wire behaviour is identical regardless of
1597+ which form the caller used.
1598+
1599+ [ Back to TOC] ( #table-of-contents )
1600+
15131601## Threading contract
15141602
15151603Distilled from
@@ -1717,6 +1805,21 @@ to the canonical example for each topic covered in this manual.
17171805* [ ` examples/client_cert_auth.cpp ` ] ( examples/client_cert_auth.cpp ) —
17181806 mutual TLS with X.509 client certificates.
17191807
1808+ ### Lifecycle hooks
1809+
1810+ * [ ` examples/banned_ip_log.cpp ` ] ( examples/banned_ip_log.cpp ) — log every
1811+ banned-IP rejection from a single ` accept_decision ` hook (issue #332 ).
1812+ * [ ` examples/early_413.cpp ` ] ( examples/early_413.cpp ) — short-circuit
1813+ oversized uploads with 413 before any body bytes are consumed via a
1814+ ` request_received ` hook (issue #273 ).
1815+ * [ ` examples/clf_access_log.cpp ` ] ( examples/clf_access_log.cpp ) —
1816+ Common Log Format access logger written as a ` response_sent ` hook
1817+ using the structured ` status ` / ` bytes_queued ` / ` elapsed ` context
1818+ fields (issues #281 and #69 ).
1819+ * [ ` examples/per_route_auth.cpp ` ] ( examples/per_route_auth.cpp ) —
1820+ per-route HTTP Basic auth via `http_resource::add_hook(before_handler,
1821+ ...)` that does not touch sibling routes (DR-012).
1822+
17201823### Operations
17211824
17221825* [ ` examples/daemon_info.cpp ` ] ( examples/daemon_info.cpp ) — introspect
0 commit comments