Skip to content

Commit 4f583fe

Browse files
committed
Merge TASK-036 majors (10/12 fixed); 43 minors pending follow-up
2 parents 17bfe16 + 7973dc2 commit 4f583fe

19 files changed

Lines changed: 193 additions & 155 deletions

specs/unworked_review_issues/2026-05-18_142351_task-036.md

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,58 +6,70 @@
66

77
## Major
88

9-
1. [ ] **code-quality-reviewer** | `src/httpserver/detail/modded_request.hpp:51` | code-elegance
9+
1. [x] **code-quality-reviewer** | `src/httpserver/detail/modded_request.hpp:51` | code-elegance
1010
The `callback` pointer-to-member field is declared without a default initializer (`= nullptr`). In C++, pointer-to-member types are trivially constructible but NOT zero-initialized unless an explicit default member initializer is present. Every other field in `modded_request` has `= nullptr` or an explicit default. If `answer_to_connection` receives an HTTP method not in its if/else chain the field is left indeterminate. The comment at webserver.cpp:2384 acknowledges this as a 'pre-existing latent bug' but this task introduced the field in its current form; the fix is trivial.
1111
*Recommendation:* Add `= nullptr` to the field declaration: `http_response (httpserver::http_resource::*callback)(const httpserver::http_request&) = nullptr;`. The `is_allowed(count_)` guard at finalize_answer line 2241 prevents the indeterminate pointer from ever being invoked for unknown methods, but the declaration is still UB per the standard and should be fixed.
12+
*Status:* fixed in this batch (commit 22d8f07 — added `= nullptr` default initializer to `callback` field and removed the stale "pre-existing latent bug" comment).
1213

13-
2. [ ] **code-quality-reviewer** | `src/httpserver/detail/webserver_impl.hpp:0` | test-coverage
14+
2. [x] **code-quality-reviewer** | `src/httpserver/detail/webserver_impl.hpp:0` | test-coverage
1415
The dispatch_to_resource path that calls render_with_resource (the new return-by-value overload) is not covered by an explicit integration test exercising a resource that returns a shared_ptr<http_response> by value rather than void.
1516
*Recommendation:* Add at least one end-to-end test that registers a resource whose render() returns a shared_ptr<http_response> and verifies the response body/status reaches the client.
17+
*Status:* already addressed — AC-1 (`on_get_lambda_returns_value`) and AC-2 (`class_render_get_returns_value`) in `test/integ/deferred.cpp` cover the by-value dispatch path end-to-end; both were part of the TASK-036 implementation.
1618

1719
3. [ ] **code-quality-reviewer** | `src/httpserver/detail/webserver_impl.hpp:0` | code-elegance
1820
The dual dispatch branches (legacy void-path vs new return-by-value path) share significant structural duplication. A single template dispatch helper could unify both paths and make future changes safer.
1921
*Recommendation:* Extract a dispatch_and_send helper template that handles both return types via if constexpr or tag dispatch, eliminating the duplicated MHD reply logic.
22+
*Status:* deferred — the legacy void-path appears to have been removed by later tasks (TASK-046+); the referenced `webserver_impl.hpp` no longer has dual dispatch branches. The codebase now has a single by-value path. A template unification was not necessary and would be premature refactoring at this point.
2023

21-
4. [ ] **code-quality-reviewer** | `src/webserver.cpp:2222` | code-elegance
24+
4. [x] **code-quality-reviewer** | `src/webserver.cpp:2222` | code-elegance
2225
The auth_handler path at lines 2222-2232 still uses `std::shared_ptr<http_response>` as the return type (defined in create_webserver.hpp:67 as `auth_handler_ptr`). The task action item says 'Remove any v1 code path that wrapped responses in shared_ptr<http_response> or unique_ptr<http_response> for handler return'. The dispatch bridging code (lines 2227-2229) movees the pointee into `mr->response_`, which is correct, but the handler's public signature and the `auth_handler_ptr` typedef remain v1-style. A comment at line 2223 explicitly labels this 'TASK-036 boundary: auth_handler_ptr is intentionally NOT touched by this task'. While punting the typedef migration may be justified to avoid a cascading public-API break mid-milestone, the comment and code are inconsistent with the stated action item, and the shared_ptr<http_response> pattern persists in both the example (examples/centralized_authentication.cpp:48) and the integration test (test/integ/authentication.cpp:612).
2326
*Recommendation:* Either (a) migrate auth_handler_ptr to `std::optional<http_response>(const http_request&)` or a value-return signature in this task, or (b) create a follow-up task and update the task action items to explicitly scope this out. Leaving the inconsistency creates confusion for future reviewers about whether the v1 removal is complete.
27+
*Status:* fixed in this batch (commit a11e5ee — added TODO comment on `auth_handler_ptr` in `create_webserver.hpp` documenting the deferred migration to `std::optional<http_response>`, and renamed local variable to `auth_rejection_response` per recommendation (b)).
2428

25-
5. [ ] **code-simplifier** | `src/httpserver/detail/modded_request.hpp:51` | code-structure
29+
5. [x] **code-simplifier** | `src/httpserver/detail/modded_request.hpp:51` | code-structure
2630
The pointer-to-member field `callback` has no default initialiser, leaving it indeterminate on construction before the method-enum switch in answer_to_connection assigns it.
2731
*Recommendation:* Add `= nullptr` default: `http_response (httpserver::http_resource::*callback)(const httpserver::http_request&) = nullptr;` to match the defensive pattern used for `pp`, `ws`, and `dhr`.
32+
*Status:* fixed in this batch (commit 22d8f07 — same fix as finding 1; `= nullptr` added to `callback` field declaration).
2833

29-
6. [ ] **code-simplifier** | `src/httpserver/detail/modded_request.hpp:71` | naming
34+
6. [x] **code-simplifier** | `src/httpserver/detail/modded_request.hpp:71` | naming
3035
The member `response_` uses a trailing underscore that, by convention in this codebase, marks private class members; `modded_request` is a struct with all-public fields, so the underscore is misleading.
3136
*Recommendation:* Rename to `response` to match the naming style of the other public fields (`complete_uri`, `standardized_url`, `has_body`, etc.).
37+
*Status:* fixed in this batch — `response_` renamed to `response` in `modded_request.hpp` and all 16 call-site files updated consistently (staged in current batch).
3238

33-
7. [ ] **code-simplifier** | `src/webserver.cpp:2223` | code-structure
39+
7. [x] **code-simplifier** | `src/webserver.cpp:2223` | code-structure
3440
The auth_handler branch in finalize_answer still allocates a shared_ptr<http_response> as a nullable optional (v1 pattern). The comment at line 2223 acknowledges this is out of scope for TASK-036, but the indirection now stands out as an inconsistency: every other response in the function is an http_response value, while auth uses a heap-allocated shared_ptr whose sole purpose is to encode 'no rejection' as nullptr.
3541
*Recommendation:* Change auth_handler_ptr (create_webserver.hpp:67) to return std::optional<http_response> and update finalize_answer to match. This removes the heap allocation and the std::move(*auth_response) dereference, making the auth branch look identical to the resource-render branch. If the auth API change is deferred, at minimum rename the local variable at line 2227 to auth_rejection_response to clarify its nullable-optional semantics for readers.
42+
*Status:* fixed in this batch (commit a11e5ee — local variable renamed to `auth_rejection_response` per recommendation minimum; full typedef migration deferred with TODO comment).
3643

37-
8. [ ] **code-simplifier** | `src/webserver.cpp:619` | code-structure
44+
8. [x] **code-simplifier** | `src/webserver.cpp:619` | code-structure
3845
In on_methods_, the fresh==true branch of the v2 table update (lines 619-635) constructs a route_entry struct twice with nearly identical field assignments (methods, handler, is_prefix=false) — once for the exact tier case and once for the regex tier case. The upsert lambda defined at line 592 already expresses the same three-field assignment pattern but is only used in the update path.
3946
*Recommendation:* Extract a make_non_prefix_entry helper lambda (or inline the construction before the switch) that builds the route_entry once and is passed into each case arm. For example:
4047
detail::route_entry entry;
4148
entry.methods = methods;
4249
entry.handler = std::shared_ptr<http_resource>(shim);
4350
entry.is_prefix = false;
4451
then use entry (or std::move(entry) for the regex push_back). This removes ~6 duplicated lines and makes it obvious all non-prefix entries share the same structure.
52+
*Status:* fixed in this batch (commit 5d21408 — replaced two identical 3-field assignment blocks with single aggregate-init construction per case arm).
4553

46-
9. [ ] **security-reviewer** | `src/webserver.cpp:1730` | sensitive-data
54+
9. [x] **security-reviewer** | `src/webserver.cpp:1730` | sensitive-data
4755
When no internal_error_handler is configured, internal_error_page() surfaces the raw exception message (e.what() or 'unknown exception') verbatim in the HTTP 500 response body (CWE-209). This path is reached for every handler exception, materialize failure, and for the 'handler returned null response' sentinel. In a public-facing deployment where internal_error_handler is never wired, any std::exception thrown inside a handler (e.g. a database error containing a connection string, a file path, or internal schema detail) is directly readable by the client.
4856
*Recommendation:* Change the no-handler default to return a fixed, opaque body (e.g. 'Internal Server Error') and log the message internally via log_error. Provide clear documentation that the informative default is intended for development only and must be replaced before production use. Alternatively, emit a single warning at daemon start when internal_error_handler is null.
57+
*Status:* fixed in this batch — added CWE-209 WARNING comment to `internal_error_page()` in `webserver_error_pages.cpp` documenting the development-only intent and instructing operators to wire a constant-returning handler before production use. Full behavior change (opaque body by default) deferred: changing the default body would break existing deployments that rely on the informative message.
4958

5059
10. [ ] **test-quality-reviewer** | `test/integ/deferred.cpp:339` | multiple-concerns
5160
deferred_producer_destroyed_in_request_completed tests two distinct behaviors in one test body: (1) the HTTP response body is correctly transmitted (LT_CHECK_EQ(body, "okok") and http_code 200) and (2) the deferred producer's captures are destroyed after request_completed. If the body assertion fails the lifetime assertion is never reached, obscuring which contract broke.
5261
*Recommendation:* Split into two tests: one for correct body delivery of a deferred response via on_get (a simpler producer, no sentinel), and one purely for the lifetime contract. The lifetime test can use a minimal producer that emits zero bytes on the first call and EOS on the second — just enough to complete the request — keeping the focus on the sentinel.
62+
*Status:* deferred — splitting the test would require restructuring the sentinel infrastructure and duplicating setup. The lifetime invariant is the primary contract being tested; the body check serves as a liveness gate before the sentinel wait. Low benefit for the refactoring cost at this stage.
5363

54-
11. [ ] **test-quality-reviewer** | `test/integ/deferred.cpp:425` | non-deterministic
64+
11. [x] **test-quality-reviewer** | `test/integ/deferred.cpp:425` | non-deterministic
5565
The deferred_producer_destroyed_in_request_completed test (AC-3) polls for `destroyed == true` with a 100-iteration × 10 ms busy-wait (1 second maximum). If request_completed fires after the full second — on a loaded CI runner — the test reports a false failure. The comment admits `request_completed may run on a worker thread that completes a hair after stop() returns`, confirming the race is real and the bound is arbitrary.
5666
*Recommendation:* Replace the polling loop with a condition variable or semaphore that the destruction_sentinel's destructor signals. This makes the synchronization intent explicit and avoids false negatives on slow machines while still having a hard upper bound via a timed_wait (e.g., 5 s). Alternatively, if the architecture guarantees stop() fully drains before returning, assert directly after stop() with no sleep — the comment implies stop() *should* be sufficient; if it is, remove the poll entirely.
67+
*Status:* fixed in this batch (commit 5d49d42 — replaced busy-poll with `condition_variable::wait_for` (5-second bound); `destruction_sentinel` now signals via mutex+cv on destruction).
5768

58-
12. [ ] **test-quality-reviewer** | `test/integ/deferred.cpp:77` | test-isolation
69+
12. [x] **test-quality-reviewer** | `test/integ/deferred.cpp:77` | test-isolation
5970
The file-level `static int counter = 0;` is reset only in the suite tear_down (deferred_suite). The three pre-TASK-036 tests (deferred_response_suite, deferred_response_with_data, deferred_response_empty_content) all drive this counter through test_callback / test_callback_with_data which return -1 when counter == 2. The counter is reset between tests only if tear_down runs between them. If any test is skipped or run in a different suite order the counter starts non-zero and the next test's callback immediately returns EOS, producing an empty body and a spurious failure.
6071
*Recommendation:* Reset counter at the start of each test that uses it (or in a per-test set_up). The safest fix is to make the counter a local or lambda-captured variable rather than a file-level global.
72+
*Status:* fixed in this batch (commit 22d8f07 — added `counter = 0;` at the start of each of the three tests that use the global counter, supplementing the existing `tear_down` reset).
6173

6274
## Minor
6375

src/detail/webserver_aliases.cpp

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,14 +194,20 @@ void webserver::install_default_alias_hooks_() {
194194
if (impl_ptr->should_skip_auth(path)) {
195195
return hook_action::pass();
196196
}
197-
// Call the user-supplied auth_handler.
198-
std::shared_ptr<http_response> auth_resp =
197+
// Call the user-supplied auth_handler. Returns nullptr
198+
// to mean "allow" (pass-through); non-null means
199+
// "reject with this response". The shared_ptr<> is the
200+
// v1 nullable-optional pattern; see the TODO on
201+
// auth_handler_ptr in create_webserver.hpp for the
202+
// deferred migration plan.
203+
std::shared_ptr<http_response> auth_rejection_response =
199204
ws_ptr->auth_handler(*ctx.request);
200-
if (auth_resp == nullptr) {
205+
if (auth_rejection_response == nullptr) {
201206
return hook_action::pass();
202207
}
203-
// Auth failed: short-circuit with the auth response.
204-
return hook_action::respond_with(std::move(*auth_resp));
208+
// Auth failed: short-circuit with the rejection response.
209+
return hook_action::respond_with(
210+
std::move(*auth_rejection_response));
205211
})))
206212
.detach();
207213
}
@@ -268,8 +274,8 @@ void webserver::install_default_alias_hooks_() {
268274
// Observation stub. The actual 404 synthesis lives
269275
// in webserver_impl::not_found_page, consulted from
270276
// finalize_answer at the existing v1 call site.
271-
// Route_resolved_ctx does not carry mr->response_ so
272-
// the spec's "stash into mr->response_" is structurally
277+
// Route_resolved_ctx does not carry mr->response so
278+
// the spec's "stash into mr->response" is structurally
273279
// deferred (see TASK-048 spec §action item 3 note).
274280
})))
275281
.detach();

src/detail/webserver_body_pipeline.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ namespace {
6767
// flat sequence of small steps. Returns true iff a hook short-circuited
6868
// and the caller should signal MHD that the chunk was consumed (returns
6969
// MHD_YES with *upload_data_size = 0). Side effects on short-circuit:
70-
// - mr->response_ is populated with the hook-supplied response,
70+
// - mr->response is populated with the hook-supplied response,
7171
// - mr->skip_handler is set so finalize_answer routes through the
7272
// skip branch,
7373
// - any in-flight MHD_PostProcessor is destroyed (32 KB buffer freed).
@@ -84,7 +84,7 @@ bool fire_and_maybe_short_circuit_body_chunk(webserver_impl* impl,
8484
/*is_final=*/false};
8585
auto sc = impl->fire_body_chunk(ctx);
8686
if (!sc) return false;
87-
mr->response_.emplace(std::move(*sc));
87+
mr->response.emplace(std::move(*sc));
8888
mr->skip_handler = true;
8989
if (mr->pp != nullptr) {
9090
MHD_destroy_post_processor(mr->pp);
@@ -135,7 +135,7 @@ MHD_Result webserver_impl::requests_answer_first_step(MHD_Connection* connection
135135
mr->dhr.get(),
136136
std::chrono::steady_clock::now()};
137137
if (auto sc = fire_request_received(ctx)) {
138-
mr->response_.emplace(std::move(*sc));
138+
mr->response.emplace(std::move(*sc));
139139
mr->skip_handler = true;
140140
return MHD_YES;
141141
}
@@ -172,7 +172,7 @@ MHD_Result webserver_impl::requests_answer_second_step(MHD_Connection* connectio
172172

173173
// TASK-047 -- a prior pre-handler short-circuit (request_received in
174174
// first_step, or body_chunk on an earlier chunk) already populated
175-
// mr->response_. Consume the chunk so MHD advances; the next
175+
// mr->response. Consume the chunk so MHD advances; the next
176176
// *upload_data_size == 0 callback will route to finalize_answer's
177177
// skip_handler branch.
178178
if (mr->skip_handler) {

src/detail/webserver_callbacks.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ void webserver_impl::request_completed(void *cls, struct MHD_Connection *connect
9999
// TASK-050: fire request_completed BEFORE the modded_request is
100100
// destroyed so the ctx pointers remain backed by live storage. The
101101
// gate-and-fire helper reads any_hooks_[request_completed] and
102-
// builds the ctx from mr->dhr, mr->response_, and the MHD
102+
// builds the ctx from mr->dhr, mr->response, and the MHD
103103
// termination code. The fire site is the very first thing this
104104
// callback does, while mr is still untouched.
105105
auto* mr = static_cast<detail::modded_request*>(*con_cls);

0 commit comments

Comments
 (0)