Skip to content

Commit 72614c0

Browse files
etrclaude
andcommitted
TASK-036 minors: address 43 unchecked minor review items
Fix or document all 43 pending minor findings from the 2026-05-18 review pass. No behavioral changes — cosmetic, documentation, and low-risk structural improvements only. Key changes: - Fix typo in webserver_callbacks.cpp (finding 22: 'Asssume/enought') - Add `*~` to .gitignore to suppress editor/autoconf backup files (34) - Add class-level comment to modded_request (18) - Trim verbose DR-010 comment on response field to one line (28) - Update lambda_resource: note render_connect/render_trace fallback (27), add slot-type doc comment (17), add invoke_() doc comment (26) - Trim duplicate inline comment from http_resource::render() (29) - Add MHD refcounting comment near MHD_destroy_response for deferred responses explaining why the immediate destroy is safe (15) - Rename materialize_current -> try_materialize (23) - Extract emplace_and_materialize helper in get_raw_response_with_fallback to fold repeated emplace+materialize pattern (30) - Wrap ws_upgrade_data in unique_ptr RAII guard in websocket upgrade path; release to MHD after queue_response (42 / CWE-401) - Document CWE-532 contract on log_dispatch_error (44) - Document auth oracle design choice (found==true gate, 43 / CWE-200) - Reduce thread_safety test sleep from 10s to 2s and add liveness comment on tautological assertion (49) - Rename deferred_response_suite test -> deferred_response_with_prefix_content (50) - Add on_post_lambda_returns_value integration test to guard non-GET lambda dispatch path (51) - Condense lengthy AC-3 comment to observable contract (52) - Add double-stop idempotency note in AC-3 test (25) - Replace default_render_returns_empty unit test with default_render_returns_sentinel which tests the actual -1 sentinel contract introduced by TASK-036 (53) - Add route_entry::lambda_handler dispatch note for PRD-RSP-REQ-007 (47) - Update http-resource.md arch doc: render_* return by value, DR-004 (36) - Add per-slot copy behaviour comment in commit_handlers_to_shim (39) Deferred (unchanged): 13, 37, 38, 40, 41, 45, 46 (already documented or design-level decisions); 20, 21, 31, 32, 33 (already addressed by code refactoring in prior commits); 16, 35 (no action needed). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4f583fe commit 72614c0

15 files changed

Lines changed: 135 additions & 46 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
*.sw*
22
*.*~
3+
*~
34
*.in
45
*.php
56
*.pm

specs/architecture/04-components/http-resource.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
**Responsibility:** Stateful handler base for cases where state is shared across HTTP methods of one resource (counter, cache, DB handle, auth context).
44

5-
**Implementation:** Public abstract base. Subclasses override one of `render_get / render_post / render_put / render_delete / render_patch / render_options / render_head` (renamed from v1's `render_GET` etc., to comply with PRD-NAM-REQ-001 snake_case). The default `render(...)` falls back when the method-specific override is not provided.
5+
**Implementation:** Public abstract base. Subclasses override one of `render_get / render_post / render_put / render_delete / render_patch / render_options / render_head` (renamed from v1's `render_GET` etc., to comply with PRD-NAM-REQ-001 snake_case). Each `render_*` override returns `http_response` **by value** (DR-004 / PRD-RSP-REQ-007); the webserver moves the returned value into the per-connection `modded_request::response` anchor. The default `render(...)` falls back when the method-specific override is not provided; it returns a default-constructed `http_response` whose `status_code_ == -1` sentinel routes through `internal_error_page`.
66

77
The allow-mask (formerly `std::map<std::string, bool> method_state`) becomes `method_set methods_allowed_;` — a `uint32_t` bitmask wrapper (DR-6). `is_allowed(http_method)` and `get_allowed_methods()` are `const` and return without allocation.
88

99
**Lifetime:** owned by the `webserver` via `unique_ptr` or `shared_ptr` (PRD-HDL-REQ-003). Raw-pointer registration is gone (PRD-HDL-REQ-005).
1010

11-
**Related requirements:** PRD-HDL-REQ-003, PRD-HDL-REQ-005, PRD-REQ-REQ-002, PRD-REQ-REQ-003.
11+
**Related requirements:** DR-004, PRD-HDL-REQ-003, PRD-HDL-REQ-005, PRD-REQ-REQ-002, PRD-REQ-REQ-003, PRD-RSP-REQ-007.
1212

1313
---

src/detail/webserver_aliases.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,15 @@ void webserver::install_default_alias_hooks_() {
178178
// webserver_impl::apply_auth_short_circuit inline path: it respects
179179
// auth_skip_paths, calls the user-supplied auth_handler, and
180180
// short-circuits with the returned response when auth fails.
181+
//
182+
// Design note (security-reviewer-iter1-7 / CWE-200): auth_handler
183+
// is registered as a before_handler hook, which fires only for
184+
// matched routes (found==true). Requests to unregistered paths
185+
// receive a 404 without consulting auth_handler. This means 401 vs
186+
// 404 distinguishes registered from unregistered routes (auth oracle).
187+
// Callers who need uniform authentication on all requests — including
188+
// 404s — should add a catch-all fallback route or register a
189+
// not_found_handler that applies equivalent auth logic.
181190
// ----------------------------------------------------------------
182191
if (auth_handler != nullptr) {
183192
// Capture both the webserver* (for auth_handler callable) and the

src/detail/webserver_callbacks.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ void webserver_impl::error_log(void* cls, const char* fmt, va_list ap) {
309309
webserver* dws = static_cast<webserver*>(cls);
310310

311311
std::string msg;
312-
msg.resize(80); // Asssume one line will be enought most of the time.
312+
msg.resize(80); // Assume one line will be enough most of the time.
313313

314314
va_list va;
315315
va_copy(va, ap); // Stash a copy in case we need to try again.

src/detail/webserver_error_pages.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ void webserver_impl::log_dispatch_error(std::string_view msg) const {
9898
if (parent->log_error == nullptr) {
9999
return;
100100
}
101+
// Framework contract (CWE-532): msg may contain e.what() text from
102+
// a handler exception, which could include sensitive data (DB connection
103+
// strings, file paths, user-supplied input that triggered the exception).
104+
// Application code should sanitize or wrap exceptions that might expose
105+
// sensitive information before re-throwing them.
106+
//
101107
// A misbehaving user logger must not poison the catch from inside
102108
// the catch. Swallow any exception it throws; we have no recovery
103109
// beyond dropping the log line.

src/detail/webserver_request.cpp

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -194,41 +194,44 @@ struct MHD_Response* webserver_impl::get_raw_response_with_fallback(detail::modd
194194
// emplace(std::move(...)); the optional owns the value and the
195195
// deferred-body trampoline keeps a pointer into it for the lifetime
196196
// of the modded_request.
197-
auto materialize_current = [&]() -> struct MHD_Response* {
197+
auto try_materialize = [&]() -> struct MHD_Response* {
198198
return materialize_response(mr->response ? &*mr->response : nullptr);
199199
};
200+
// Helper: emplace a new response and immediately materialize it.
201+
// Each catch arm sets mr->response then returns the raw MHD handle.
202+
auto emplace_and_materialize = [&](http_response r) -> struct MHD_Response* {
203+
mr->response.emplace(std::move(r));
204+
return try_materialize();
205+
};
200206
try {
201-
struct MHD_Response* raw = materialize_current();
207+
struct MHD_Response* raw = try_materialize();
202208
if (raw == nullptr) {
203209
// TASK-031: no exception was thrown, but the body materializer
204210
// returned null. Route through the safe internal-error path so
205211
// a misbehaving user handler can't escape.
206-
mr->response.emplace(run_internal_error_handler_safely(
212+
return emplace_and_materialize(run_internal_error_handler_safely(
207213
mr, "materialize_response returned null"));
208-
raw = materialize_current();
209214
}
210215
return raw;
211216
} catch(const std::invalid_argument&) {
212217
try {
213-
mr->response.emplace(not_found_page(mr));
214-
return materialize_current();
218+
return emplace_and_materialize(not_found_page(mr));
215219
} catch(...) {
216220
return nullptr;
217221
}
218222
} catch(const std::exception& e) {
219223
log_dispatch_error(std::string("materialize threw: ") + e.what());
220224
try {
221-
mr->response.emplace(run_internal_error_handler_safely(mr, e.what()));
222-
return materialize_current();
225+
return emplace_and_materialize(
226+
run_internal_error_handler_safely(mr, e.what()));
223227
} catch(...) {
224228
return nullptr;
225229
}
226230
} catch(...) {
227231
log_dispatch_error("materialize threw unknown exception");
228232
try {
229-
mr->response.emplace(run_internal_error_handler_safely(mr,
230-
"unknown exception"));
231-
return materialize_current();
233+
return emplace_and_materialize(run_internal_error_handler_safely(
234+
mr, "unknown exception"));
232235
} catch(...) {
233236
return nullptr;
234237
}
@@ -260,6 +263,14 @@ MHD_Result webserver_impl::materialize_and_queue_response(MHD_Connection* connec
260263
// queued bytes -- only the http_response in mr->response matters
261264
// for the hook ctx pointer.
262265
fire_response_sent_gated(mr);
266+
// MHD reference-counting note: for MHD_create_response_from_callback
267+
// responses (deferred/streaming), MHD increments its own internal
268+
// refcount during MHD_queue_response, so this MHD_destroy_response only
269+
// releases the *caller's* reference. MHD keeps the streaming callback
270+
// (deferred_body::trampoline) alive — and therefore the cls pointer into
271+
// mr->response — until request_completed fires and MHD releases its own
272+
// reference. The modded_request (and mr->response) are destroyed only in
273+
// the request_completed callback, which is after MHD is done streaming.
263274
MHD_destroy_response(raw_response);
264275
return (MHD_Result) to_ret;
265276
}

src/detail/webserver_routes.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,11 @@ void webserver_impl::commit_handlers_to_shim(detail::lambda_resource& shim,
170170
method_set methods,
171171
const std::function<::httpserver::http_response(
172172
const ::httpserver::http_request&)>& handler) {
173-
// The shared std::function copies cheaply (type-erased callable),
174-
// so each slot owns its own copy.
173+
// Each slot gets its own copy of the std::function. For small captures
174+
// (within SBO, ~16-32 bytes on libstdc++/libc++) the copies are cheap.
175+
// For large-capture handlers registered across many methods, each copy
176+
// allocates once. Registration-time only; not a hot-path concern.
177+
// (performance-reviewer-iter1-3: document the per-slot copy behaviour.)
175178
for_each_requested_method(methods, [&](http_method m) {
176179
shim.set_slot(m, handler);
177180
});

src/detail/webserver_websocket.cpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,15 @@ webserver_impl::complete_websocket_upgrade(MHD_Connection* connection,
212212
std::shared_ptr<websocket_handler> handler_sp = ws_it->second;
213213
lock.unlock();
214214

215-
auto* data = new ws_upgrade_data{this, std::move(handler_sp)};
215+
// CWE-401: RAII guard so data is freed if MHD_create_response_for_upgrade
216+
// returns null. Ownership is transferred to MHD (via release()) only after
217+
// the queue call — upgrade_handler receives data_guard.get() as cls and
218+
// wraps it in unique_ptr for cleanup (see upgrade_handler below).
219+
std::unique_ptr<ws_upgrade_data> data_guard(
220+
new ws_upgrade_data{this, std::move(handler_sp)});
216221
struct MHD_Response* response = MHD_create_response_for_upgrade(
217-
&webserver_impl::upgrade_handler, data);
222+
&webserver_impl::upgrade_handler, data_guard.get());
218223
if (response == nullptr) {
219-
delete data;
220224
return std::nullopt;
221225
}
222226
MHD_add_response_header(response, MHD_HTTP_HEADER_UPGRADE, "websocket");
@@ -231,6 +235,7 @@ webserver_impl::complete_websocket_upgrade(MHD_Connection* connection,
231235
MHD_HTTP_SWITCHING_PROTOCOLS,
232236
response);
233237
MHD_destroy_response(response);
238+
data_guard.release(); // MHD now owns; upgrade_handler will delete.
234239
return to_ret;
235240
}
236241

src/httpserver/detail/lambda_resource.hpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ namespace detail {
6464

6565
// Tiny adapter that wraps a single lambda_handler as an http_resource
6666
// virtual override. We keep one slot per method enum and dispatch in
67-
// each render_* override.
67+
// each render_* override. Each slot holds a
68+
// std::function<http_response(const http_request&)>
69+
// (see route_entry.hpp::lambda_handler). Slots are installed via
70+
// set_slot() and invoked by the render_* overrides below.
6871
class lambda_resource final : public ::httpserver::http_resource {
6972
public:
7073
lambda_resource() {
@@ -90,6 +93,9 @@ class lambda_resource final : public ::httpserver::http_resource {
9093
// Each differs only by the http_method enum constant forwarded.
9194
// They are required by the http_resource base-class interface and
9295
// cannot be collapsed further without changing that interface.
96+
// render_connect and render_trace intentionally fall back to the
97+
// base-class default (no slot wired, no set_allowing call); they
98+
// return the -1 sentinel and route through internal_error_page.
9399
::httpserver::http_response
94100
render_get(const ::httpserver::http_request& r) override {
95101
return invoke_(http_method::get, r);
@@ -120,6 +126,9 @@ class lambda_resource final : public ::httpserver::http_resource {
120126
}
121127

122128
private:
129+
// Dispatch to the registered slot for method `m`, returning the
130+
// handler's http_response by value. Callers must ensure is_allowed(m)
131+
// is true before calling (the 405 guard in finalize_answer enforces this).
123132
::httpserver::http_response
124133
invoke_(http_method m, const ::httpserver::http_request& r) {
125134
auto& slot = slots_[static_cast<std::size_t>(m)];

src/httpserver/detail/modded_request.hpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ namespace httpserver {
4141

4242
namespace detail {
4343

44+
// modded_request adapts the raw MHD connection data into the
45+
// libhttpserver request model, accumulating per-connection state
46+
// (parsed headers, upload stream, method callback, staged response)
47+
// across MHD's repeated answer_to_connection invocations for one
48+
// HTTP request until finalize_answer queues the response.
4449
struct modded_request {
4550
struct MHD_PostProcessor *pp = nullptr;
4651
std::string complete_uri;
@@ -63,8 +68,7 @@ struct modded_request {
6368
httpserver::http_method method_enum = httpserver::http_method::count_;
6469

6570
std::unique_ptr<http_request> dhr = nullptr;
66-
// DR-010 / §5.3: anchor kept alive until request_completed.
67-
// See webserver_impl.hpp and specs/architecture for full contract.
71+
// DR-010 / §5.3: anchor kept alive until request_completed. See webserver_impl.hpp for full contract.
6872
std::optional<http_response> response;
6973
bool has_body = false;
7074
// TASK-047: set by a pre-handler hook short-circuit (request_received

0 commit comments

Comments
 (0)