Skip to content

Commit 6c2eec3

Browse files
etrclaude
andcommitted
refactor(dr-014): extract hook_dispatcher behavior service
Move the dispatch-time hook firing off webserver_impl into detail::hook_dispatcher: the eleven per-phase forwarders (formerly hook_phase_dispatch.cpp) and the four gated helpers (fire_before_handler_gated / _after_handler_gated / _response_sent_gated / _request_completed_gated, formerly webserver_hook_firing.cpp). It holds hook_bus& (the phase state) and const webserver_config& (only to reach the log_dispatch_error free function), owns no state, and takes no locks. hook_dispatcher is granted friend access to http_response (like webserver_impl) for the response_sent bytes_queued = body_->size() read. webserver_impl gains a hook_dispatcher hooks_dispatch_ member; the two former TUs become thin webserver_impl forwarders during the migration. 113/113 tests pass; cpplint/complexity/file-size gates green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ef48ed2 commit 6c2eec3

8 files changed

Lines changed: 496 additions & 313 deletions

File tree

src/Makefile.am

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ 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 webserver_add_hook.cpp http_utils.cpp file_info.cpp http_request.cpp http_request_auth.cpp http_response.cpp http_response_factories.cpp http_resource.cpp create_webserver.cpp create_test_request.cpp websocket_handler.cpp hook_handle.cpp peer_address.cpp resource_hook_table.cpp cookie.cpp detail/http_endpoint.cpp detail/body.cpp detail/ip_representation.cpp detail/ip_access_control.cpp detail/ws_registry.cpp detail/hook_bus.cpp detail/route_table.cpp detail/daemon_lifecycle.cpp detail/dispatch_util.cpp detail/error_pages.cpp detail/http_request_impl.cpp detail/http_request_impl_args.cpp detail/http_request_impl_tls.cpp detail/webserver_lifecycle.cpp detail/webserver_register.cpp detail/webserver_routes.cpp detail/webserver_routes_upsert.cpp detail/webserver_callbacks.cpp detail/webserver_callbacks_lifecycle.cpp detail/webserver_websocket.cpp detail/webserver_dispatch.cpp detail/webserver_request.cpp detail/webserver_response_queue.cpp detail/webserver_body_pipeline.cpp detail/webserver_error_pages.cpp detail/webserver_aliases.cpp detail/webserver_hook_firing.cpp detail/hook_phase_dispatch.cpp
28+
libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp webserver_add_hook.cpp http_utils.cpp file_info.cpp http_request.cpp http_request_auth.cpp http_response.cpp http_response_factories.cpp http_resource.cpp create_webserver.cpp create_test_request.cpp websocket_handler.cpp hook_handle.cpp peer_address.cpp resource_hook_table.cpp cookie.cpp detail/http_endpoint.cpp detail/body.cpp detail/ip_representation.cpp detail/ip_access_control.cpp detail/ws_registry.cpp detail/hook_bus.cpp detail/route_table.cpp detail/daemon_lifecycle.cpp detail/dispatch_util.cpp detail/error_pages.cpp detail/hook_dispatcher.cpp detail/http_request_impl.cpp detail/http_request_impl_args.cpp detail/http_request_impl_tls.cpp detail/webserver_lifecycle.cpp detail/webserver_register.cpp detail/webserver_routes.cpp detail/webserver_routes_upsert.cpp detail/webserver_callbacks.cpp detail/webserver_callbacks_lifecycle.cpp detail/webserver_websocket.cpp detail/webserver_dispatch.cpp detail/webserver_request.cpp detail/webserver_response_queue.cpp detail/webserver_body_pipeline.cpp detail/webserver_error_pages.cpp detail/webserver_aliases.cpp detail/webserver_hook_firing.cpp detail/hook_phase_dispatch.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>.
32-
noinst_HEADERS = httpserver/string_utilities.hpp httpserver/detail/modded_request.hpp httpserver/detail/http_endpoint.hpp httpserver/detail/body.hpp httpserver/detail/webserver_impl.hpp httpserver/detail/webserver_impl_dispatch.hpp httpserver/detail/connection_state.hpp httpserver/detail/ip_access_control.hpp httpserver/detail/ws_registry.hpp httpserver/detail/hook_bus.hpp httpserver/detail/route_table.hpp httpserver/detail/daemon_lifecycle.hpp httpserver/detail/dispatch_util.hpp httpserver/detail/error_pages.hpp httpserver/detail/secure_zero.hpp httpserver/detail/http_request_impl.hpp httpserver/detail/resource_hook_table.hpp httpserver/detail/route_entry.hpp httpserver/detail/lambda_resource.hpp httpserver/detail/segment_trie.hpp httpserver/detail/route_cache.hpp httpserver/detail/route_tier.hpp httpserver/detail/unescape_helpers.hpp gettext.h
32+
noinst_HEADERS = httpserver/string_utilities.hpp httpserver/detail/modded_request.hpp httpserver/detail/http_endpoint.hpp httpserver/detail/body.hpp httpserver/detail/webserver_impl.hpp httpserver/detail/webserver_impl_dispatch.hpp httpserver/detail/connection_state.hpp httpserver/detail/ip_access_control.hpp httpserver/detail/ws_registry.hpp httpserver/detail/hook_bus.hpp httpserver/detail/route_table.hpp httpserver/detail/daemon_lifecycle.hpp httpserver/detail/dispatch_util.hpp httpserver/detail/error_pages.hpp httpserver/detail/hook_dispatcher.hpp httpserver/detail/secure_zero.hpp httpserver/detail/http_request_impl.hpp httpserver/detail/resource_hook_table.hpp httpserver/detail/route_entry.hpp httpserver/detail/lambda_resource.hpp httpserver/detail/segment_trie.hpp httpserver/detail/route_cache.hpp httpserver/detail/route_tier.hpp httpserver/detail/unescape_helpers.hpp gettext.h
3333
nobase_include_HEADERS = httpserver.hpp httpserver/body_kind.hpp httpserver/cookie.hpp httpserver/constants.hpp httpserver/create_webserver.hpp httpserver/create_webserver_setters.hpp httpserver/create_test_request.hpp httpserver/webserver.hpp httpserver/webserver_routes.hpp httpserver/webserver_runtime.hpp httpserver/webserver_websocket.hpp httpserver/webserver_hooks.hpp httpserver/websocket_handler.hpp httpserver/http_utils.hpp httpserver/http_utils_helpers.hpp httpserver/ip_representation.hpp httpserver/file_info.hpp httpserver/http_request.hpp httpserver/http_response.hpp httpserver/http_resource.hpp httpserver/feature_unavailable.hpp httpserver/iovec_entry.hpp httpserver/http_arg_value.hpp httpserver/http_method.hpp httpserver/hook_phase.hpp httpserver/hook_action.hpp httpserver/hook_handle.hpp httpserver/hook_context.hpp
3434

3535
AM_CXXFLAGS += -fPIC -Wall

src/detail/hook_dispatcher.cpp

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
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+
// hook_dispatcher behavior service (DR-014 §4.11). Logic moved verbatim
22+
// out of the former hook_phase_dispatch.cpp (the eleven forwarders) and
23+
// webserver_hook_firing.cpp (the four gated helpers). Those TUs now hold
24+
// only the thin webserver_impl forwarders during the migration. The one
25+
// behavioral change is that the per-call error logger now routes through
26+
// the detail::log_dispatch_error free function (config bag) instead of the
27+
// webserver_impl member.
28+
29+
#include "httpserver/detail/hook_dispatcher.hpp"
30+
31+
#include <microhttpd.h>
32+
33+
#include <chrono>
34+
#include <cstddef>
35+
#include <memory>
36+
#include <optional>
37+
#include <string_view>
38+
#include <utility>
39+
40+
#include "httpserver/hook_action.hpp"
41+
#include "httpserver/hook_context.hpp"
42+
#include "httpserver/hook_phase.hpp"
43+
#include "httpserver/http_request.hpp"
44+
#include "httpserver/http_resource.hpp"
45+
#include "httpserver/http_response.hpp"
46+
#include "httpserver/detail/body.hpp"
47+
#include "httpserver/detail/dispatch_util.hpp"
48+
#include "httpserver/detail/hook_bus.hpp"
49+
#include "httpserver/detail/modded_request.hpp"
50+
#include "httpserver/detail/resource_hook_table.hpp"
51+
52+
namespace httpserver {
53+
namespace detail {
54+
55+
namespace {
56+
57+
// Fetch the per-route hook table (if any) from the request's
58+
// resource_weak_ slot, keeping the shared_ptr alive in res_out so the
59+
// caller can hold the table pointer valid. Returns nullptr when no
60+
// per-route table exists. Uses a direct lock() (atomic) rather than
61+
// expired()+lock() to avoid a TOCTOU window.
62+
resource_hook_table* per_route_table(detail::modded_request* mr,
63+
std::shared_ptr<http_resource>& res_out) {
64+
res_out = mr->resource_weak_.lock();
65+
if (!res_out) return nullptr;
66+
return res_out->hook_table_raw_();
67+
}
68+
69+
} // namespace
70+
71+
// ---- eleven per-phase forwarders -----------------------------------------
72+
73+
void hook_dispatcher::fire_connection_opened(
74+
const connection_open_ctx& ctx) noexcept {
75+
hooks_.fire_connection_opened(ctx,
76+
[this](std::string_view m) { log_dispatch_error(config_, m); });
77+
}
78+
79+
void hook_dispatcher::fire_accept_decision(const accept_ctx& ctx) noexcept {
80+
hooks_.fire_accept_decision(ctx,
81+
[this](std::string_view m) { log_dispatch_error(config_, m); });
82+
}
83+
84+
void hook_dispatcher::fire_connection_closed(
85+
const connection_close_ctx& ctx) noexcept {
86+
hooks_.fire_connection_closed(ctx,
87+
[this](std::string_view m) { log_dispatch_error(config_, m); });
88+
}
89+
90+
std::optional<http_response> hook_dispatcher::fire_request_received(
91+
request_received_ctx& ctx) noexcept {
92+
return hooks_.fire_request_received(ctx,
93+
[this](std::string_view m) { log_dispatch_error(config_, m); });
94+
}
95+
96+
std::optional<http_response> hook_dispatcher::fire_body_chunk(
97+
body_chunk_ctx& ctx) noexcept {
98+
return hooks_.fire_body_chunk(ctx,
99+
[this](std::string_view m) { log_dispatch_error(config_, m); });
100+
}
101+
102+
void hook_dispatcher::fire_route_resolved(
103+
const route_resolved_ctx& ctx) noexcept {
104+
hooks_.fire_route_resolved(ctx,
105+
[this](std::string_view m) { log_dispatch_error(config_, m); });
106+
}
107+
108+
std::optional<http_response> hook_dispatcher::fire_before_handler(
109+
before_handler_ctx& ctx) noexcept {
110+
return hooks_.fire_before_handler(ctx,
111+
[this](std::string_view m) { log_dispatch_error(config_, m); });
112+
}
113+
114+
std::optional<http_response> hook_dispatcher::fire_handler_exception(
115+
const handler_exception_ctx& ctx) noexcept {
116+
return hooks_.fire_handler_exception(ctx,
117+
[this](std::string_view m) { log_dispatch_error(config_, m); });
118+
}
119+
120+
std::optional<http_response> hook_dispatcher::fire_after_handler(
121+
after_handler_ctx& ctx) noexcept {
122+
return hooks_.fire_after_handler(ctx,
123+
[this](std::string_view m) { log_dispatch_error(config_, m); });
124+
}
125+
126+
void hook_dispatcher::fire_response_sent(
127+
const response_sent_ctx& ctx) noexcept {
128+
hooks_.fire_response_sent(ctx,
129+
[this](std::string_view m) { log_dispatch_error(config_, m); });
130+
}
131+
132+
void hook_dispatcher::fire_request_completed(
133+
const request_completed_ctx& ctx) noexcept {
134+
hooks_.fire_request_completed(ctx,
135+
[this](std::string_view m) { log_dispatch_error(config_, m); });
136+
}
137+
138+
// ---- gated: before_handler (server-wide + per-route, short-circuiting) ---
139+
140+
bool hook_dispatcher::fire_before_handler_gated(
141+
detail::modded_request* mr,
142+
const std::shared_ptr<http_resource>& hrm) {
143+
const bool server_gate = hooks_.has_hooks_for(hook_phase::before_handler);
144+
// rtable comes from hrm directly (already resolved under the route
145+
// table lock); hrm keeps the resource alive for this function. The
146+
// acquire barrier from that shared_lock lets hook_table_raw_() observe
147+
// any prior add_hook() store.
148+
auto* rtable = hrm->hook_table_raw_();
149+
const bool per_route_gate = rtable != nullptr &&
150+
rtable->any_hooks(hook_phase::before_handler);
151+
if (!server_gate && !per_route_gate) return false;
152+
153+
std::optional<route_descriptor> desc;
154+
if (!mr->matched_path_template.empty()) {
155+
desc = route_descriptor{
156+
/*path_template=*/std::string_view{mr->matched_path_template},
157+
/*methods=*/hrm->get_allowed_methods(),
158+
/*is_prefix=*/mr->matched_is_prefix};
159+
}
160+
before_handler_ctx ctx{
161+
/*request=*/mr->request.get(),
162+
/*matched=*/std::move(desc),
163+
/*method=*/mr->method_enum,
164+
/*resource=*/hrm.get()};
165+
if (server_gate) {
166+
if (auto sc = fire_before_handler(ctx)) {
167+
mr->response.emplace(std::move(*sc));
168+
return true;
169+
}
170+
}
171+
if (per_route_gate) {
172+
if (auto sc = rtable->fire_before_handler(ctx,
173+
[this](std::string_view m) {
174+
log_dispatch_error(config_, m);
175+
})) {
176+
mr->response.emplace(std::move(*sc));
177+
return true;
178+
}
179+
}
180+
return false;
181+
}
182+
183+
// ---- gated: after_handler (server-wide + per-route, replace-response) ----
184+
185+
void hook_dispatcher::fire_after_handler_gated(detail::modded_request* mr,
186+
http_resource* resource) {
187+
const bool server_gate = hooks_.has_hooks_for(hook_phase::after_handler);
188+
// resource is borrowed from finalize_answer's live shared_ptr; no
189+
// weak_ptr lock() needed. nullptr on the 404 path -- no per-route table.
190+
auto* rtable = resource != nullptr ? resource->hook_table_raw_() : nullptr;
191+
const bool route_gate = rtable != nullptr &&
192+
rtable->any_hooks(hook_phase::after_handler);
193+
194+
if (!server_gate && !route_gate) return;
195+
if (!mr->response) return; // defensive: never fire without a response
196+
197+
after_handler_ctx ctx{mr->request.get(), &*mr->response};
198+
if (server_gate) {
199+
if (auto sc = fire_after_handler(ctx)) {
200+
// Short-circuit: REPLACE mr->response (emplace destroys the old
201+
// response, releasing deferred captures now). The per-route
202+
// chain ALSO sees the replaced response, so refresh ctx.
203+
mr->response.emplace(std::move(*sc));
204+
ctx.response = &*mr->response;
205+
}
206+
}
207+
if (route_gate) {
208+
if (auto sc = rtable->fire_after_handler(ctx,
209+
[this](std::string_view m) {
210+
log_dispatch_error(config_, m);
211+
})) {
212+
mr->response.emplace(std::move(*sc));
213+
}
214+
}
215+
}
216+
217+
// ---- gated: response_sent (observation, server-wide + per-route) ---------
218+
219+
void hook_dispatcher::fire_response_sent_gated(detail::modded_request* mr,
220+
http_resource* resource) {
221+
const bool server_gate = hooks_.has_hooks_for(hook_phase::response_sent);
222+
// resource is borrowed from finalize_answer's live shared_ptr, so read
223+
// the per-route table directly instead of locking the weak_ptr. nullptr
224+
// on the skip_handler / 404 paths.
225+
auto* rtable = resource != nullptr ? resource->hook_table_raw_() : nullptr;
226+
const bool route_gate = rtable != nullptr &&
227+
rtable->any_hooks(hook_phase::response_sent);
228+
// Whether any user hook (server-wide or per-route) will observe this
229+
// firing. Distinct from the log_access alias slot, which fires
230+
// regardless but never reads ctx.elapsed (see the elapsed gate below).
231+
const bool user_gate = server_gate || route_gate;
232+
233+
if (!user_gate && !hooks_.has_log_access_alias()) return;
234+
// mr->response is null only if materialize_and_queue_response's
235+
// belt-and-suspenders fallback also failed; fire nothing rather than crash.
236+
if (!mr->response) return;
237+
238+
// 0 for deferred/pipe bodies -- see response_sent_ctx docs.
239+
const std::size_t bytes_queued = (mr->response->body_ != nullptr)
240+
? mr->response->body_->size() : 0;
241+
// elapsed is consumed by user hooks, not by the log_access alias.
242+
// Skip the steady_clock::now() syscall when only the alias slot fires.
243+
// NOTE: the alias body MUST NOT read ctx.elapsed; any future change that
244+
// needs elapsed in the alias must also remove this optimisation.
245+
const auto elapsed = user_gate
246+
? std::chrono::duration_cast<std::chrono::nanoseconds>(
247+
std::chrono::steady_clock::now() - mr->start_time)
248+
: std::chrono::nanoseconds::zero();
249+
response_sent_ctx ctx{mr->request.get(), &*mr->response,
250+
mr->response->get_status(), bytes_queued, elapsed};
251+
fire_response_sent(ctx);
252+
// Per-route chain AFTER server-wide + its alias slot.
253+
// response_sent is observation-only; no short-circuit logic.
254+
if (route_gate) {
255+
rtable->fire_response_sent(ctx,
256+
[this](std::string_view m) { log_dispatch_error(config_, m); });
257+
}
258+
}
259+
260+
// ---- gated: request_completed (fires from MHD completion callback) -------
261+
262+
void hook_dispatcher::fire_request_completed_gated(
263+
detail::modded_request* mr,
264+
enum MHD_RequestTerminationCode toe) {
265+
const bool server_gate =
266+
hooks_.has_hooks_for(hook_phase::request_completed);
267+
268+
// Per-route gate. This fires from the MHD completion callback --
269+
// finalize_answer's owning shared_ptr is gone -- so lock() the weak_ptr
270+
// to keep the resource alive while rtable is in use. Skip the lock()
271+
// entirely on the common path: route_has_hook_table_ is a snapshot of
272+
// whether the resource carried any per-route hook table; when it's false
273+
// and no server-wide hook is registered, no control-block atomics are
274+
// touched.
275+
std::shared_ptr<http_resource> res;
276+
resource_hook_table* rtable = nullptr;
277+
if (server_gate || mr->route_has_hook_table_) {
278+
rtable = per_route_table(mr, res);
279+
}
280+
const bool per_route_present = rtable != nullptr &&
281+
rtable->any_hooks(hook_phase::request_completed);
282+
283+
if (!server_gate && !per_route_present) {
284+
return;
285+
}
286+
const http_response* resp_ptr =
287+
mr->response ? &*mr->response : nullptr;
288+
// mr->start_time may be epoch if answer_to_connection never ran (e.g. a
289+
// port scan: uri_log created mr but MHD closed before dispatch). Emit
290+
// nanoseconds{-1} as a sentinel so hook authors can distinguish the
291+
// degenerate case from a real (but very slow) request.
292+
const auto raw_duration =
293+
std::chrono::steady_clock::now() - mr->start_time;
294+
const auto duration =
295+
(mr->start_time == std::chrono::steady_clock::time_point{})
296+
? std::chrono::nanoseconds{-1}
297+
: std::chrono::duration_cast<std::chrono::nanoseconds>(
298+
raw_duration);
299+
request_completed_ctx ctx{
300+
/*request=*/mr->request.get(),
301+
/*resp=*/resp_ptr,
302+
/*succeeded=*/(toe == MHD_REQUEST_TERMINATED_COMPLETED_OK),
303+
/*duration=*/duration,
304+
};
305+
if (server_gate) {
306+
fire_request_completed(ctx);
307+
}
308+
if (per_route_present) {
309+
rtable->fire_request_completed(ctx,
310+
[this](std::string_view m) { log_dispatch_error(config_, m); });
311+
}
312+
}
313+
314+
} // namespace detail
315+
} // namespace httpserver

0 commit comments

Comments
 (0)