Skip to content

Commit bed6ad3

Browse files
etrclaude
andcommitted
TASK-050: fire after_handler / response_sent / request_completed + log_access alias
Wires the three tail-end lifecycle hook phases and converts log_access into a documented response_sent alias slot. Closes the structural holes left open in TASK-045: issues #281 and #69 (CLF / time-taken access logging) are now writable entirely in user code via add_hook(response_sent, ...). Hook firing sites: - after_handler: fires in finalize_answer between dispatch_resource_handler (or 404 synthesis) and materialize_and_queue_response. Short-circuit- capable -- hook_action::respond_with(r) REPLACES mr->response_; a pass()-returning hook may have mutated *mr->response_ in place via ctx.response->with_header(...). - response_sent: fires in materialize_and_queue_response immediately after MHD_queue_response and BEFORE MHD_destroy_response. Carries the structured ctx fields {status, bytes_queued, elapsed} that issues #281 and #69 asked for. - request_completed: fires from webserver_impl::request_completed BEFORE the modded_request is destroyed. ctx carries {resp (nullable), succeeded, duration}. A request_received short-circuit (e.g., a 413 rejection) still reports succeeded == true because MHD drove the request to ordinary completion. log_access(fn) alias: - Dedicated webserver_impl::log_access_alias_ single-slot member, mirroring TASK-049's handler_exception_alias_ contract (single-writer- at-construction). fire_response_sent invokes the slot AFTER the user vector so user hooks observe the response before the legacy logger formats it. - The v1 inline access_log call site in answer_to_connection is removed; the alias now emits "<path> METHOD: <method>" from a response_sent hook, preserving the existing log_access_callback integ test's substring assertions. Misc structural changes: - modded_request::start_time -- captured once in answer_to_connection's fresh-request branch; consumed by response_sent.elapsed and request_completed.duration. - response_sent_ctx + request_completed_ctx grown with the spec'd fields (status, bytes_queued, elapsed, resp, succeeded). - webserver_request.cpp split: validate_websocket_handshake / complete_websocket_upgrade / try_handle_websocket_upgrade moved to webserver_websocket.cpp, and the new fire_*_gated helpers live in a new TU src/detail/webserver_finalize.cpp, both to stay under the FILE_LOC_MAX gate. Tests: - hooks_after_handler_replaces_response: respond_with replaces wire. - hooks_after_handler_mutates_response_in_place: with_header pass() puts the header on the wire. - hooks_response_sent_carries_status_bytes_timing: ctx fields populated. - hooks_request_completed_fires_on_early_failure: 413-short-circuit request_completed reports succeeded == true + non-null resp. - hooks_response_sent_ctx_shape / hooks_request_completed_ctx_shape: compile-time pins for the new ctx fields. - hooks_log_access_alias_slot: log_access(fn) populates the alias slot and does NOT push into hooks_response_sent_ (mirrors TASK-049's handler_exception_alias_slot_test). - hooks_no_firing updated: after_handler / response_sent / request_completed are now wired and removed from not_yet_wired. Example: - examples/clf_access_log.cpp -- a Common Log Format access logger written as a response_sent hook, demonstrating the closure of #281 and #69. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 102f073 commit bed6ad3

22 files changed

Lines changed: 1340 additions & 97 deletions

examples/Makefile.am

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
LDADD = $(top_builddir)/src/libhttpserver.la
2020
AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/
2121
METASOURCES = AUTO
22-
noinst_PROGRAMS = hello_world shared_state service custom_error allowing_disallowing_methods handlers hello_with_get_arg args_processing setting_headers custom_access_log minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban banned_ip_log early_413 benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback empty_response_example iovec_response_example pipe_response_example daemon_info external_event_loop turbo_mode binary_buffer_response
22+
noinst_PROGRAMS = hello_world shared_state service custom_error allowing_disallowing_methods handlers hello_with_get_arg args_processing setting_headers custom_access_log clf_access_log minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban banned_ip_log early_413 benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback empty_response_example iovec_response_example pipe_response_example daemon_info external_event_loop turbo_mode binary_buffer_response
2323

2424
hello_world_SOURCES = hello_world.cpp
2525
shared_state_SOURCES = shared_state.cpp
@@ -31,6 +31,12 @@ hello_with_get_arg_SOURCES = hello_with_get_arg.cpp
3131
args_processing_SOURCES = args_processing.cpp
3232
setting_headers_SOURCES = setting_headers.cpp
3333
custom_access_log_SOURCES = custom_access_log.cpp
34+
# TASK-050: CLF-format access logger written as a response_sent hook.
35+
# Demonstrates the resolution of issues #281 and #69 -- with the
36+
# structured response_sent_ctx (status / bytes_queued / elapsed), users
37+
# can write a real CLF / time-taken log line in user code, without a
38+
# library change.
39+
clf_access_log_SOURCES = clf_access_log.cpp
3440
minimal_https_SOURCES = minimal_https.cpp
3541
minimal_file_response_SOURCES = minimal_file_response.cpp
3642
minimal_deferred_SOURCES = minimal_deferred.cpp

examples/clf_access_log.cpp

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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+
// clf_access_log.cpp -- Common Log Format access logger written as a
22+
// response_sent lifecycle hook, demonstrating the resolution of issues
23+
// #281 and #69. libhttpserver v1's log_access(fn) callback handed the
24+
// user a single string and was invoked at request-arrival time, so the
25+
// status code, byte count, and request duration were not yet known.
26+
// Issues #281 and #69 asked for a logger that could emit a real CLF /
27+
// `time-taken` line.
28+
//
29+
// In v2.0 the response_sent hook (DR-012 §4.10) fires immediately after
30+
// MHD_queue_response and carries the structured context users have been
31+
// asking for:
32+
// - ctx.status -- HTTP status code
33+
// - ctx.bytes_queued -- logical body size (Content-Length, when known)
34+
// - ctx.elapsed -- steady_clock nanoseconds from
35+
// answer_to_connection's first invocation
36+
//
37+
// This example uses those fields to emit a Common Log Format line on
38+
// stdout. The library no longer hard-codes a logging format -- you do.
39+
//
40+
// Run:
41+
// ./clf_access_log # blocks; serves on :8080
42+
// curl http://localhost:8080/hello
43+
// curl http://localhost:8080/hello?name=world
44+
//
45+
// Expected output (per request):
46+
// - - - [26/May/2026:14:01:23 +0000] "GET /hello HTTP/1.1" 200 13 1
47+
48+
#include <chrono>
49+
#include <cstdint>
50+
#include <cstdio>
51+
#include <ctime>
52+
#include <functional>
53+
#include <memory>
54+
#include <string>
55+
56+
#include <httpserver.hpp>
57+
58+
namespace hs = httpserver;
59+
60+
namespace {
61+
62+
class hello_resource : public hs::http_resource {
63+
public:
64+
hs::http_response render_get(const hs::http_request&) override {
65+
return hs::http_response::string("Hello, World!");
66+
}
67+
};
68+
69+
void emit_clf_line(const hs::response_sent_ctx& ctx) {
70+
std::time_t now = std::time(nullptr);
71+
char ts[32];
72+
std::tm tm_buf;
73+
#if defined(_WIN32) && !defined(__CYGWIN__)
74+
localtime_s(&tm_buf, &now);
75+
#else
76+
localtime_r(&now, &tm_buf);
77+
#endif
78+
std::strftime(ts, sizeof(ts), "%d/%b/%Y:%H:%M:%S %z", &tm_buf);
79+
80+
int64_t ms = std::chrono::duration_cast<std::chrono::milliseconds>(
81+
ctx.elapsed).count();
82+
std::string method, path;
83+
if (ctx.request != nullptr) {
84+
method = std::string(ctx.request->get_method());
85+
path = std::string(ctx.request->get_path());
86+
}
87+
std::printf("- - - [%s] \"%s %s HTTP/1.1\" %d %zu %lld\n",
88+
ts,
89+
method.c_str(),
90+
path.c_str(),
91+
ctx.status,
92+
ctx.bytes_queued,
93+
static_cast<long long>(ms)); // NOLINT(runtime/int)
94+
std::fflush(stdout);
95+
}
96+
97+
} // namespace
98+
99+
int main() {
100+
hs::webserver ws{hs::create_webserver(8080)};
101+
102+
auto h = ws.add_hook(hs::hook_phase::response_sent,
103+
std::function<void(const hs::response_sent_ctx&)>(emit_clf_line));
104+
105+
auto resource = std::make_shared<hello_resource>();
106+
ws.register_path("/hello", resource);
107+
108+
ws.start(true);
109+
return 0;
110+
}

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 detail/webserver_error_pages.cpp detail/webserver_aliases.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 detail/webserver_finalize.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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
#include <string_view>
6363
#include <utility>
6464

65+
#include "httpserver/create_webserver.hpp"
6566
#include "httpserver/hook_action.hpp"
6667
#include "httpserver/hook_context.hpp"
6768
#include "httpserver/hook_handle.hpp"
@@ -107,6 +108,28 @@ void install_internal_error_alias_(
107108
make_internal_error_alias_(std::move(user_handler));
108109
}
109110

111+
// TASK-050: install the log_access alias into the dedicated response_sent
112+
// alias slot on webserver_impl. Extracted from install_default_alias_hooks_
113+
// for the same reason as install_internal_error_alias_: keeping the host
114+
// function under the project CCN gate. See webserver_impl::log_access_alias_
115+
// for the lifetime contract.
116+
void install_log_access_alias_(
117+
detail::webserver_impl* impl,
118+
log_access_ptr user_logger) {
119+
if (user_logger == nullptr) return;
120+
impl->log_access_alias_ =
121+
[user_logger = std::move(user_logger)](
122+
const response_sent_ctx& ctx) {
123+
if (ctx.request == nullptr) return;
124+
std::string line;
125+
line.reserve(64);
126+
line += std::string(ctx.request->get_path());
127+
line += " METHOD: ";
128+
line += std::string(ctx.request->get_method());
129+
user_logger(line);
130+
};
131+
}
132+
110133
// Serialize an allowed-method set into the comma-separated value
111134
// expected by the HTTP `Allow:` header. Enum-declaration order:
112135
// GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH.
@@ -264,6 +287,31 @@ void webserver::install_default_alias_hooks_() {
264287
// calling it a second time would observably invoke the user code
265288
// twice for one logical exception). See webserver_dispatch.cpp.
266289
install_internal_error_alias_(impl_.get(), internal_error_handler);
290+
291+
// ----------------------------------------------------------------
292+
// log_access -> response_sent alias slot. [TASK-050]
293+
//
294+
// This is an alias. Calling log_access(fn) on the create_webserver
295+
// builder wires `fn` into the dedicated single-slot member
296+
// webserver_impl::log_access_alias_, which fire_response_sent
297+
// invokes AFTER the user-added response_sent vector. Users who want
298+
// the structured ctx (status, bytes_queued, elapsed -- the data
299+
// issues #281 and #69 asked for) should call
300+
// add_hook(hook_phase::response_sent, ...) directly.
301+
//
302+
// Format compatibility: the pre-TASK-050 access log was emitted by
303+
// webserver_impl::access_log(parent, complete_uri + " METHOD: " +
304+
// method) at request-arrival time from inside answer_to_connection.
305+
// The existing log_access_callback integ test (test/integ/basic.cpp)
306+
// asserts the substrings "/logtest" AND "METHOD" appear in the
307+
// logged line. To keep that test passing without modification we
308+
// emit a line that contains both, formatted as:
309+
// <path> METHOD: <method>
310+
// using ctx.request->get_path() and ctx.request->get_method(). The
311+
// alias body is null-safe (early-returns if ctx.request is null),
312+
// which is structurally impossible at the fire site but the guard
313+
// costs nothing and documents the contract.
314+
install_log_access_alias_(impl_.get(), log_access);
267315
}
268316

269317
} // namespace httpserver

src/detail/webserver_callbacks.cpp

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,23 @@ namespace detail {
9595
void webserver_impl::request_completed(void *cls, struct MHD_Connection *connection, void **con_cls, enum MHD_RequestTerminationCode toe) {
9696
// These parameters are passed to respect the MHD interface, but are not needed here.
9797
std::ignore = cls;
98-
std::ignore = toe;
98+
99+
// TASK-050: fire request_completed BEFORE the modded_request is
100+
// destroyed so the ctx pointers remain backed by live storage. The
101+
// gate-and-fire helper reads any_hooks_[request_completed] and
102+
// builds the ctx from mr->dhr, mr->response_, and the MHD
103+
// termination code. The fire site is the very first thing this
104+
// callback does, while mr is still untouched.
105+
auto* mr = static_cast<detail::modded_request*>(*con_cls);
106+
if (mr != nullptr) {
107+
// mr->ws is the parent webserver -- set in answer_to_connection
108+
// (hoisted there at TASK-050 time). For paths where
109+
// answer_to_connection never ran (e.g., very early MHD failures),
110+
// mr->ws may be null; skip the fire site in that degenerate case.
111+
if (mr->ws != nullptr && mr->ws->impl_ != nullptr) {
112+
mr->ws->impl_->fire_request_completed_gated(mr, toe);
113+
}
114+
}
99115

100116
// (1) Destroy the modded_request first. This runs ~http_request,
101117
// which calls the arena_deleter on the impl's unique_ptr (a
@@ -303,9 +319,9 @@ void webserver_impl::error_log(void* cls, const char* fmt, va_list ap) {
303319
if (dws->log_error != nullptr) dws->log_error(msg);
304320
}
305321

306-
void webserver_impl::access_log(webserver* dws, const string& uri) {
307-
if (dws->log_access != nullptr) dws->log_access(uri);
308-
}
322+
// TASK-050: webserver_impl::access_log removed. log_access is now a
323+
// response_sent hook alias installed in webserver_aliases.cpp; the v1
324+
// inline call site in answer_to_connection has been removed.
309325

310326
size_t webserver_impl::unescaper_func(void * cls, struct MHD_Connection *c, char *s) {
311327
// Parameter needed to respect MHD interface, but not needed here.

0 commit comments

Comments
 (0)