Skip to content

Commit 873359e

Browse files
committed
Merge TASK-050 (after_handler + response_sent + request_completed + log_access alias) into feature/v2.0
2 parents 102f073 + b278f8a commit 873359e

27 files changed

Lines changed: 1676 additions & 106 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: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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+
#include <string_view>
56+
57+
#include <httpserver.hpp>
58+
59+
namespace hs = httpserver;
60+
61+
namespace {
62+
63+
class hello_resource : public hs::http_resource {
64+
public:
65+
hs::http_response render_get(const hs::http_request&) override {
66+
return hs::http_response::string("Hello, World!");
67+
}
68+
};
69+
70+
// SECURITY (CWE-117): sanitize a string_view for CLF output.
71+
// Replace ASCII control characters (< 0x20 or == 0x7F) with '-' to
72+
// prevent a client from injecting additional log lines by embedding
73+
// newlines or carriage-returns in the request path or method.
74+
std::string sanitize_clf(std::string_view sv) {
75+
std::string out;
76+
out.reserve(sv.size());
77+
for (unsigned char c : sv) {
78+
out += (c < 0x20 || c == 0x7f) ? '-' : static_cast<char>(c);
79+
}
80+
return out;
81+
}
82+
83+
void emit_clf_line(const hs::response_sent_ctx& ctx) {
84+
std::time_t now = std::time(nullptr);
85+
char ts[32];
86+
std::tm tm_buf;
87+
#if defined(_WIN32) && !defined(__CYGWIN__)
88+
localtime_s(&tm_buf, &now);
89+
#else
90+
localtime_r(&now, &tm_buf);
91+
#endif
92+
std::strftime(ts, sizeof(ts), "%d/%b/%Y:%H:%M:%S %z", &tm_buf);
93+
94+
int64_t ms = std::chrono::duration_cast<std::chrono::milliseconds>(
95+
ctx.elapsed).count();
96+
std::string method, path;
97+
if (ctx.request != nullptr) {
98+
// Sanitize before embedding in the CLF line to prevent log injection.
99+
method = sanitize_clf(ctx.request->get_method());
100+
path = sanitize_clf(ctx.request->get_path());
101+
}
102+
std::printf("- - - [%s] \"%s %s HTTP/1.1\" %d %zu %lld\n",
103+
ts,
104+
method.c_str(),
105+
path.c_str(),
106+
ctx.status,
107+
ctx.bytes_queued,
108+
static_cast<long long>(ms)); // NOLINT(runtime/int)
109+
std::fflush(stdout);
110+
}
111+
112+
} // namespace
113+
114+
int main() {
115+
hs::webserver ws{hs::create_webserver(8080)};
116+
117+
auto h = ws.add_hook(hs::hook_phase::response_sent,
118+
std::function<void(const hs::response_sent_ctx&)>(emit_clf_line));
119+
120+
auto resource = std::make_shared<hello_resource>();
121+
ws.register_path("/hello", resource);
122+
123+
ws.start(true);
124+
return 0;
125+
}

specs/architecture/04-components/hooks.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
| `before_handler` | `webserver.cpp:dispatch_resource_handler` — start | **yes** | yes |
1515
| `handler_exception` | `webserver.cpp:dispatch_resource_handler` — each catch arm | **yes** (maps exception to response) | yes |
1616
| `after_handler` | between handler return and `materialize_and_queue_response` | **yes** (replaces response) | yes |
17-
| `response_sent` | `webserver.cpp:materialize_and_queue_response` — post `MHD_queue_response` | no | yes |
18-
| `request_completed` | `webserver.cpp:request_completed` — NOTIFY_COMPLETED | no | yes |
17+
| `response_sent` | `webserver_request.cpp:materialize_and_queue_response` — post `MHD_queue_response` | no | yes |
18+
| `request_completed` | `webserver_callbacks.cpp:request_completed` — NOTIFY_COMPLETED | no | yes |
1919
| `connection_closed` | `webserver.cpp:connection_notify` — NOTIFY_CLOSED | no | no |
2020

2121
**Implementation.** Each phase has its own `std::vector<std::function<...>>` in `webserver_impl`, guarded by a single `std::shared_mutex hook_table_mutex_`. A per-phase `std::atomic<bool> any_hooks_[hook_phase::count_]` flag short-circuits the dispatch site to a relaxed atomic load and a compare-with-zero when no subscribers exist — the only hook-related cost on the hot request path for a server with zero hooks registered.

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
Wire the three tail-end phases. `after_handler` is the post-handler short-circuit (it can REPLACE the in-flight response). `response_sent` is the observation point that finally has the response, status, byte-count, and timing — the data #281 and #69 have been asking for. `request_completed` is the unconditional final hook. Convert `log_access` into the documented alias.
99

1010
**Action Items:**
11-
- [ ] Fire `after_handler` between `dispatch_resource_handler` and `materialize_and_queue_response` in `webserver_impl::finalize_answer` (`webserver.cpp:2444``webserver.cpp:2449`). Context: `after_handler_ctx { const http_request& req; http_response& resp; }` — mutable response reference so a hook can `resp.with_header(...)` in place. Short-circuit semantics: a hook returning `hook_action::respond_with(r2)` REPLACES `mr->response_` with `r2` and skips remaining hooks at the phase. Subsequent hooks at later phases (`response_sent`, `request_completed`) still fire on the replaced response.
12-
- [ ] Fire `response_sent` immediately after the `MHD_queue_response` call in `webserver_impl::materialize_and_queue_response` (`webserver.cpp:2421`), BEFORE `MHD_destroy_response`. Context: `response_sent_ctx { const http_request& req; const http_response& resp; int status; std::size_t bytes_queued; std::chrono::nanoseconds elapsed; }`. `elapsed` is measured from `answer_to_connection`'s first invocation for this `mr` (timestamp captured in `modded_request` at TASK-045 time).
13-
- [ ] Fire `request_completed` from `webserver_impl::request_completed` (`webserver.cpp:1253`), BEFORE the `delete static_cast<detail::modded_request*>(*con_cls)` step (otherwise the context fields would dangle). Context: `request_completed_ctx { const http_request& req; const http_response* resp; bool succeeded; }`. `resp` is nullable — on early failures (`accept_decision` rejection, etc.) there may be no response object.
14-
- [ ] **Alias: `log_access(fn)`.** Internally register a `response_sent` hook that formats the request line as today and invokes `fn(line)`. Re-registration replaces. Doxygen notes it as an alias and recommends `add_hook(hook_phase::response_sent, ...)` for users who want the structured context (status, bytes, timing — what #281 and #69 actually asked for).
15-
- [ ] `examples/clf_access_log.cpp`: a Common-Log-Format access logger as a `response_sent` hook, demonstrating that the long-requested CLF / `time-taken` format is now writable in user code without a library change. Closes issues #281 and #69 from the user-facing-doc perspective.
11+
- [x] Fire `after_handler` between `dispatch_resource_handler` and `materialize_and_queue_response` in `webserver_impl::finalize_answer` (implemented in `src/detail/webserver_finalize.cpp:fire_after_handler_gated`, called from `src/detail/webserver_request.cpp:finalize_answer`). Context: `after_handler_ctx { const http_request& req; http_response& resp; }` — mutable response reference so a hook can `resp.with_header(...)` in place. Short-circuit semantics: a hook returning `hook_action::respond_with(r2)` REPLACES `mr->response_` with `r2` and skips remaining hooks at the phase. Subsequent hooks at later phases (`response_sent`, `request_completed`) still fire on the replaced response.
12+
- [x] Fire `response_sent` immediately after the `MHD_queue_response` call in `webserver_impl::materialize_and_queue_response` (`src/detail/webserver_request.cpp:materialize_and_queue_response`), BEFORE `MHD_destroy_response`. Context: `response_sent_ctx { const http_request& req; const http_response& resp; int status; std::size_t bytes_queued; std::chrono::nanoseconds elapsed; }`. `elapsed` is measured from `answer_to_connection`'s first invocation for this `mr` (timestamp captured in `modded_request` at TASK-045 time).
13+
- [x] Fire `request_completed` from `webserver_impl::request_completed` (`src/detail/webserver_callbacks.cpp`), BEFORE the `delete static_cast<detail::modded_request*>(*con_cls)` step (otherwise the context fields would dangle). Context: `request_completed_ctx { const http_request& req; const http_response* resp; bool succeeded; }`. `resp` is nullable — on early failures (`accept_decision` rejection, etc.) there may be no response object.
14+
- [x] **Alias: `log_access(fn)`.** Internally registers a `response_sent` hook via dedicated alias slot (`webserver_impl::log_access_alias_`) that formats the request line and invokes `fn(line)`. Re-registration replaces. Control characters are sanitized (CWE-117). Doxygen block added to `create_webserver::log_access()` noting it as an alias and recommending `add_hook(hook_phase::response_sent, ...)` for structured context (status, bytes, timing — what #281 and #69 actually asked for).
15+
- [x] `examples/clf_access_log.cpp`: a Common-Log-Format access logger as a `response_sent` hook, demonstrating that the long-requested CLF / `time-taken` format is now writable in user code without a library change. Closes issues #281 and #69 from the user-facing-doc perspective.
1616

1717
**Dependencies:**
1818
- Blocked by: TASK-045
@@ -32,4 +32,4 @@ Wire the three tail-end phases. `after_handler` is the post-handler short-circui
3232
**Related Decisions:** DR-012, §4.10
3333
**Resolves:** Issues #281, #69
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
@@ -132,7 +132,7 @@ Nominally: **13 sequential tasks**, each S–XL. Most other tasks parallelize of
132132
| TASK-047 | Fire `request_received` and `body_chunk` (pre-handler short-circuit) | M5 | Done | TASK-045 |
133133
| 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 | Done | TASK-045, TASK-031 |
135-
| TASK-050 | Fire `after_handler` (post-handler short-circuit), `response_sent`, `request_completed`; wire `log_access` alias | M5 | Not Started | TASK-045 |
135+
| TASK-050 | Fire `after_handler` (post-handler short-circuit), `response_sent`, `request_completed`; wire `log_access` alias | M5 | Done | TASK-045 |
136136
| TASK-051 | Per-route hooks (`http_resource::add_hook`) | M5 | Not Started | TASK-045, TASK-048, TASK-049, TASK-050 |
137137
| TASK-052 | Hook bus documentation, examples, benchmark, stress-test extension (touches back into TASK-040/041/042/043) | M5 | Not Started | TASK-045, TASK-046, TASK-047, TASK-048, TASK-049, TASK-050, TASK-051 |
138138
| TASK-037 | CI test for build-flag invariance | M6 | Done | TASK-034 |

0 commit comments

Comments
 (0)