v2.0: Modernization (M1-M6, 44 tasks)#374
Draft
etr wants to merge 483 commits into
Draft
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #374 +/- ##
==========================================
+ Coverage 68.03% 68.81% +0.78%
==========================================
Files 34 64 +30
Lines 1730 4057 +2327
Branches 697 1489 +792
==========================================
+ Hits 1177 2792 +1615
- Misses 80 355 +275
- Partials 473 910 +437
... and 21 files with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
etr
added a commit
that referenced
this pull request
May 7, 2026
…rueFalse, exclude specs/ Codacy was reporting 2018 new issues on the v2.0 PR (#374). Resolve as follows: * Add .codacy.yaml excluding specs/** — the product spec, architecture notes, task records, and review notes are internal groundwork artifacts, not user-facing docs, and should not be subject to README markdownlint rules. Removes 2003 markdownlint findings. * src/webserver.cpp:499 — drop the redundant `blocking &&` from the wait loop condition. `blocking` is a function parameter never reassigned inside the loop body, so the conjunct was tautological (cppcheck knownConditionTrueFalse). * src/webserver.cpp:946 — replace the C-style `(struct detail::modded_request*)` cast on the MHD `cls` void* with `static_cast<detail::modded_request*>` (cppcheck cstyleCast). Mirrors the existing static_cast usage elsewhere in the file. * detail/webserver_impl.hpp, detail/http_request_impl.hpp, iovec_entry.hpp — add `// cppcheck-suppress-file unusedStructMember` with a one-line rationale comment. Every flagged member is in fact heavily used from the corresponding .cpp translation unit (registered_resources*, route_cache_*, bans, allowances, files_, path_pieces_public_, iovec_entry::base/len, etc.); cppcheck analyses each TU in isolation and cannot see those uses, so the warning is a known pimpl/POD false positive. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
etr
added a commit
that referenced
this pull request
May 7, 2026
… clash Two unrelated CI regressions on PR #374, both falling out of TASK-020: 1. Lint job (gcc-14, ubuntu): cpplint flagged src/http_utils.cpp:30 with build/include_order, because the matching public header ("httpserver/http_utils.hpp") came AFTER a non-matching project header ("httpserver/constants.hpp"), and <microhttpd.h> (a C system header in cpplint's view) followed both. cpplint's expected order is: matching header, C system, C++ system, other. Reorder so the matching header comes first and the project headers ("constants.hpp" / "string_utilities.hpp") move to the bottom of the include block. 2. Windows MSYS2 build: src/httpserver/http_utils.hpp failed with error: expected identifier before numeric constant at the line `ERROR = 0,` inside the digest_auth_result enum. <wingdi.h> (pulled in via <windows.h> via <winsock2.h> via <microhttpd.h> on MinGW) unconditionally `#define`s ERROR to 0, and the preprocessor expands macros inside scoped-enum bodies just like anywhere else. Pre-TASK-020 the enum was inside `#ifdef HAVE_DAUTH`, so MSYS2 builds without digest auth never compiled it; PRD-FLG-REQ-001 then made the enum unconditional and exposed the latent collision. v2.0 is unreleased, so renaming is safe: ERROR -> GENERIC_ERROR (matches MHD_DAUTH_ERROR's "general error" docs). Static-assert pin in src/http_utils.cpp updated to match. Verified locally: - python3 -m cpplint on both touched files: exit 0. - `make check` on macOS: 32/32 PASS, all check-hygiene / check-headers gates PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
etr
added a commit
that referenced
this pull request
May 11, 2026
Codacy's "26 new issues (0 max.)" gate was failing on PR #374. Two classes of finding, addressed at root: - 21 markdownlint findings on test/REGRESSION.md (MD013 line-length, MD040 fenced-code language, MD043 heading structure). REGRESSION.md is an internal test-gate document (the v2.0 routing parity gate), conceptually peer to the already-excluded specs/ artifacts and not in the user-facing README/ChangeLog/CONTRIBUTING category. Extend .codacy.yaml exclude_paths with `test/**/*.md`. - 5 cppcheck findings that are all single-TU false positives: * iovec_entry.hpp: `cppcheck-suppress-file unusedStructMember` was not at the top of the file (preprocessorErrorDirective), so the file-level suppression was ignored and `base`/`len` were both flagged unused. Replaced with per-member inline suppressions. * route_cache.hpp: `cache_value::captured_params` is read in src/webserver.cpp at the cache-hit replay site; cppcheck does not follow the cross-TU read. Inline-suppress. * header_hygiene_test.cpp: cppcheck statically assumes none of the forbidden-header guard macros are defined and reports `leaks > 0` as always-false; the comparison is load-bearing at runtime under any actual leak. Inline-suppress. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
etr
added a commit
that referenced
this pull request
May 20, 2026
Three CI failures on feature/v2.0 PR #374 run 26183259463: 1. cpplint: examples/hello_world.cpp was missing the copyright line. Added single-line copyright header (the file is the deliberately minimal lambda-form example, so the full LGPL block would defeat its purpose). 2. tsan ws_start_stop: webserver::stop() and is_running() read impl_->running with no lock while start() writes it from the blocking-server thread. Made the field std::atomic<bool> — fixes the genuine race without changing the mutex/cond_var discipline that gates the blocking wait. 3. tsan route_table_concurrency + threadsafety_stress: libstdc++'s std::ctype<char>::narrow lazily fills a 256-byte cache; the guard flag is not atomic so concurrent std::regex compiles inside http_endpoint::http_endpoint look like a race even though every initialiser computes the same bytes. Added test/tsan.supp scoped to that one libstdc++ symbol pair, plumbed via TSAN_OPTIONS only on the tsan matrix lane, and shipped via test/Makefile.am EXTRA_DIST. Libhttpserver-internal races stay fatal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
etr
added a commit
that referenced
this pull request
May 21, 2026
Planning-only commit. No code yet; subsequent task-branch PRs implement TASK-045..052 in order against feature/v2.0. Adds a multi-subscriber lifecycle hook system to v2.0, replacing v1's patchwork of single-slot callbacks (log_access, not_found_handler, method_not_allowed_handler, internal_error_handler, auth_handler) with one uniform webserver::add_hook(phase, callable) surface plus a per-route http_resource::add_hook(...) variant. Existing v1 setters survive as documented aliases (PRD-HOOK-REQ-009). Eleven phases spanning the connection -> request -> routing -> handler -> response -> cleanup lifecycle: connection_opened, accept_decision, request_received, body_chunk, route_resolved, before_handler, handler_exception, after_handler, response_sent, request_completed, connection_closed. Short-circuit allowed at four pre-handler phases (request_received, body_chunk, before_handler, handler_exception) and at the after_handler post-handler phase. Throwing hooks route through DR-9 §5.2. Closes (once TASK-046, 047, 050 land): #332 banned-IP log entry (accept_decision hook) #281 response-aware access log (response_sent context) #69 Common Log Format w/ time-taken (response_sent context) #273 early 413 on oversize body (request_received short-circuit) Partially addresses #272 (body_chunk observation; the buffer-steal half remains a v2.1 candidate needing a streaming-body API). Files added: specs/architecture/11-decisions/DR-012.md specs/architecture/04-components/hooks.md (§4.10) specs/tasks/M5-routing-lifecycle/TASK-045.md .. TASK-052.md Files updated: specs/product_specs.md - new §3.8 with PRD-HOOK-REQ-001..009 - §4 traceability line for API-HOOK specs/architecture/05-cross-cutting.md - new §5.6 hook lifecycle contract - four new public headers added to §5.5 header tree specs/tasks/_index.md - M5 milestone row updated - 8 task-status rows (045..052) - dependency-graph branch - PRD-HOOK coverage rows - DR-012 coverage row Per-route hooks (TASK-051) are restricted to phases that fire after route resolution. v1 alias retention is covered in TASK-048 (404/405/auth), TASK-049 (internal_error_handler), TASK-050 (log_access), and re-documented in TASK-052. TASK-052 explicitly touches back into the already-Done TASK-040 (examples), TASK-041 (README), TASK-042 (RELEASE_NOTES), TASK-043 (Doxygen) — the planned M6 touch-back called out when this scope was approved for inclusion in PR #374. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add = nullptr default initializer to modded_request::callback field, eliminating UB per the C++ standard (findings 1 & 5: code-quality- reviewer + code-simplifier). The is_allowed guard already prevents the pointer from being invoked for unrecognized methods; this makes the contract explicit at the declaration site. - Update the comment in resolve_method_callback to remove the stale "pre-existing latent bug" note now that callback has a safe default. - Add per-test counter reset at the start of the three deferred tests that share the static counter (finding 12: test-quality-reviewer). tear_down() also resets but cannot guarantee ordering when tests run in non-default order or under skip conditions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace busy-poll (100×10 ms) in deferred_producer_destroyed_in_ request_completed with a condition_variable-based wait (finding 11: test-quality-reviewer). The destruction_sentinel now holds a mutex/cv pair and signals on destruction; the test does a timed_wait with a 5-second upper bound — fast when the invariant holds, bounded when it does not. - Trim the 6-line block comment on modded_request::response_ to a one-line cross-reference (finding 28: code-simplifier). The full DR-010 lifetime contract is documented canonically in webserver_impl.hpp and the architecture doc; repeating it in the struct adds noise. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code changes (12 files):
- src/httpserver/detail/radix_tree.hpp: trim radix_match/radix_node comments
to WHY-only; add match_root_terminus() helper eliminating empty-path
duplication; add NOTE on remove() pattern-vs-value traversal asymmetry
- src/httpserver/detail/route_cache.hpp: remove noexcept from size()/clear();
introduce kHashMix constexpr replacing magic literal; add max_entries==0
precondition check (throws std::invalid_argument, CWE-754)
- src/httpserver/detail/route_tier.hpp: wrap std::regex construction in
try/catch(std::regex_error) → rethrows as std::invalid_argument (CWE-248)
- src/httpserver/detail/webserver_impl.hpp: rename route_cache_mutex →
route_cache_mutex_; add v1/v2 section banners; add TODO(Cycle K) rename
note on route_cache_v2; add CWE-284 guard comment on lookup_v2 declaration;
regex_routes_ now stores regex_route{url_complete, compiled_re, entry}
(pre-compiled at registration time, eliminating per-miss recompile)
- src/detail/webserver_dispatch.cpp: rename route_cache_mutex_; use
std::move(result.entry/captured_params) on cache-miss path (avoids
second shared_ptr bump + vector copy)
- src/detail/webserver_register.cpp: fix lock order in unregister_impl_ and
unregister_resource — registered_resources_mutex released before table lock
and cache cleared; delegate cache clear to invalidate_route_cache() matching
register_impl_ pattern (CWE-833)
- src/detail/webserver_request.cpp: move normalize_path inside namespace
detail immediately before should_skip_auth; add NOTE documenting
pre-unescaped-path invariant and double-slash collapsing behaviour (CWE-22)
- src/detail/webserver_routes.cpp: extract upsert_v2_param_route() helper
encapsulating radix read-merge-reinsert; add __builtin_unreachable() to
unreachable radix branch in insert_fresh_v2_entry; remove trailing
comment on set_all() line; regex_routes_ push_back now moves pre-compiled
regex from classify_route_tier result
- test/unit/http_method_test.cpp: remove redundant runtime
to_string_returns_uppercase_wire_tokens (duplicate of static_asserts)
- test/unit/lookup_pipeline_test.cpp: remove redundant
cache_duplicate_insert_replaces_in_place; add lambda_route_hits_exact_tier,
lambda_parameterized_route_hits_radix_tier,
plain_path_with_regex_checking_hits_exact_tier
- test/unit/route_table_test.cpp: add cache_zero_max_entries_throws;
add radix_tree_remove_parameterized_path, radix_tree_remove_prefix_path,
radix_tree_remove_one_terminus_does_not_clear_sibling
- test/integ/route_table_concurrency.cpp: replace 500ms sleep with
kOpThreshold=10000 operation-count gate (first test); rename second test
to concurrent_wildcard_node_alloc_and_lookup_no_data_race
Review file updated (specs/unworked_review_issues/2026-05-10_230348_task-027.md):
Items #4-62 annotated with *Status:* lines.
30 fixed-in-batch, 7 already-addressed, 22 deferred.
Key fixes: pre-compiled regex tier (#4,#53), lock-order deadlock (#17,#21),
CWE-284 shadow-table documentation (#15), CWE-248 regex_error guard (#48),
CWE-754 max_entries=0 check (#46), double-slash normalization note (#16),
route_cache_mutex_ rename (#20), kHashMix constexpr (#37), noexcept removal
(#25), v1/v2 boundary comments (#26), __builtin_unreachable (#22).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add a TODO comment on auth_handler_ptr in create_webserver.hpp documenting the deferred migration to std::optional<http_response> (findings 4 & 7: code-quality-reviewer + code-simplifier). Changing the public typedef mid-milestone would cascade into centralized_authentication.cpp and authentication.cpp; scoped out. - Rename local variable auth_resp -> auth_rejection_response in the auth_handler alias hook (webserver_aliases.cpp) to clarify that null means "allow" and non-null means "reject with this response" — the minimum improvement from finding 7 recommendation (b). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use aggregate initialization {methods, handler, is_prefix=false} to
construct route_entry in insert_fresh_v2_entry, replacing two identical
3-line field-assignment blocks with a single-expression construct per
case arm. No behavior change; structure is already correct.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ings 6, 9) - Rename modded_request::response_ to response throughout all 16 call-site files (finding 6: code-simplifier). The trailing underscore incorrectly implied a private-class-member convention; modded_request is an all-public struct. Affected files: webserver_aliases.cpp, webserver_body_pipeline.cpp, webserver_callbacks.cpp, webserver_dispatch.cpp, webserver_error_pages.cpp, webserver_finalize.cpp, webserver_request.cpp, hook_handle.cpp, lambda_resource.hpp, modded_request.hpp, webserver_impl_dispatch.hpp, hook_context.hpp, http_resource.hpp, test/Makefile.am, deferred.cpp, hooks_after_handler_replaces_response.cpp. All comment references updated. - Add CWE-209 WARNING comment to internal_error_page() in webserver_error_pages.cpp (finding 9: security-reviewer). Documents that the informative default (surfacing e.what() in the 500 body) is intended for development only; operators must wire a constant-returning internal_error_handler before production deployment. - Mark findings 1-12 (all major) in the review file with fix/deferral status. 10 fixed across this and prior commits (22d8f07, 5d49d42, a11e5ee, 5d21408); finding 2 already addressed by AC-1/AC-2 in deferred.cpp; finding 10 deferred (test split has low ROI given sentinel infrastructure). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…indings Source fixes (major findings): - Extract local_map_for(MHD_ValueKind) helper on http_request_impl, eliminating verbatim switch duplication in get_connection_value() and ensure_headerlike_cache() (code-simplifier #3/#4, code-quality #15/#16) - Add mutable bool user_pass_fetched to http_request_impl (under HAVE_BAUTH); guard get_user()/get_pass() on the flag instead of username/password.empty(), matching args_populated pattern and fixing redundant MHD round-trips (code-simplifier #6) - Add mutable bool requestor_ip_cached to http_request_impl; consolidate get_requestor() to a single early-return covering cache-hit and test-request paths (code-simplifier #7 + #8/#9) - Explicit = default for http_request_impl move ctor and move-assign (code-simplifier #33) Architecture/doc fixes: - http-request.md: add historical note that TASK-015 used std::make_unique and TASK-016 wired the arena (housekeeper #34/#35/#36) - DR-003b.md: add Implementation phasing section recording the two-step landing (housekeeper #1/#2 from major section) - specs/unworked_review_issues/2026-05-04_013108_task-013.md: mark finding #4 [x] (microhttpd.h leak resolved by TASK-015) (housekeeper #38/#39) Review file closeout: - Mark all 18 resolved findings [x] with Status notes - Add deferred Status notes to all 46 remaining minor findings Findings fixed this batch: 7 (code-simplifier #3/#4/#6/#7/#8/#9/#33 + code-quality #15/#16 + housekeeper #34/#35/#36/#38/#39 = 14 unique findings across both review-file and source changes) Findings deferred (minor, all annotated): 46 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- scripts/lib/resolve-prefix.sh: new shared helper sourced by both gate scripts; POSIX-portable sed (no GNU \?), absolute-path guard, split pipeline for readability. Eliminates DRY violation (items 2, 3, 10, 16, 22). - check-soversion.sh: move resolve_symlink_chain() to top alongside fail/pass (item 4); add set -uo pipefail comment (item 9); tee install for streaming CI output (item 11); strict SONAME equality in A5 via sed bracket-extraction (item 27); narrow A7 grep to ^library_names= field (item 28). - check-parallel-install.sh: fix header comment Darwin name (item 6); add -e-omission comment (item 9); extract remove_master_worktree() helper for EXIT trap and stale-cleanup (item 14); rename V1_PATTERN to V1_SONAME_PATTERN with per-platform comments (items 13, 24); gate Homebrew flags on Darwin, use brew --prefix dynamically (items 8, 15, 26); dynamic make -j parallelism (item 19); Phase 3 strict exact-filename check from .la library_names field, SONAME collision detection, while IFS= read -r for v1_hits (items 1, 5, 21). - configure.ac + src/Makefile.am: move -version-number from global LDFLAGS to libhttpserver_la_LDFLAGS via LHT_LDF_VERSION AC_SUBST (item 25). - Makefile.am: add scripts/lib/resolve-prefix.sh to EXTRA_DIST. - specs/tasks/_index.md: bump Last updated to 2026-05-20 (items 12, 18). - Mark 27/28 items in review file; item 23 deferred (design decision). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eview Major fixes (all 5 addressed): - Replaced bare catch(...) on unregister_path with catch(invalid_argument) + catch(exception) with stderr logging — surface unexpected exceptions. - Added 5 ms inter-request sleep to curl client loop — rate-limits to ~200 req/s under TSan, keeping wall-clock within CI budget. - Added null check after curl_easy_init() — skip thread gracefully on resource exhaustion rather than crash. - Added early return after fork() failure (LT_CHECK(child >= 0)) — prevents waitpid(-1,...) from reaping unrelated processes. - TASK-032.md status already Done; assertions already individual (TASK-052). Minor fixes (cosmetic + docs): - Added kDynSlots, kIpRange, kExitCodeStopReturned, kExitCodeCurlCompleted named constants; replaced all hex magic literals (0x7, 0xff, 42, 43). - Reduced CURLOPT_TIMEOUT_MS in stop-from-handler child to 3000L (< parent's 5 s window) so timeout ordering is consistent. - Renamed deadline → child_deadline in stop_from_handler test. - Replaced std::string(run) with std::string_view(run) for env-var check. - Added upper bound (v <= 3600) to stress_seconds() (CWE-1284). - Added comment documenting why register_prefix is intentionally excluded. - Added @see stop() cross-reference to ~webserver() Doxygen. - Updated specs/architecture/09-testing.md §9 item 6 to use v2 API names. - Corrected DR-008.md Verification: exit codes are regression sentinels, not positive observations; WIFSIGNALED paths are the positive observations. - Added ~webserver() note to DR-008.md Verification section. - Added PRD-CON-REQ-001/002 EARS concurrency requirements to product_specs.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 majors: 4 fixed in this batch, 1 already addressed. 33 minors: 24 fixed/already addressed, 4 deferred (low-value churn). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Major (item 1): replace redundant `catches_as_feature_unavailable_directly`
test (which only proved pointer upcast already guaranteed by static_assert)
with `catches_as_std_exception`, a distinct test verifying the full
std::exception -> std::runtime_error -> feature_unavailable chain
and that what() is not sliced.
Other fixes:
- src/httpserver.hpp: bump C++ guard from 201703L (C++17) to 202002L
(C++20) per DR-001 (item 2)
- feature_unavailable.hpp: replace magic literal 32 with
`k_fixed_overhead` constexpr computed from the actual string literal
at compile time, keeping reserve() in sync with future wording
changes (items 3, 15); inline compose_message helper into an
immediately-invoked lambda in the constructor initializer, removing
private surface (item 9); add @param lifetime doc comments for the
string_view arguments (item 16)
- feature_unavailable_test.cpp: add `LT_CHECK(!msg.empty())` guard
before find-checks in all tests (item 6); add
`empty_args_produce_non_empty_message` test for ("", "") edge case
(items 7, 19); add comments to empty set_up/tear_down bodies (items
4, 5, 11, 12)
Deferred: item 14 (_index.md status, handled by merge step),
item 17 (pre-existing HAVE_* guards, future task per PRD-FLG-REQ-001),
item 10 (reviewer says no change needed).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Items 1-3 from 2026-05-02_230828_task-005.md: - set_all_then_contains_every_method: replace loop with explicit per-method LT_CHECK calls for all 9 methods (GET through PATCH) - clear_all_makes_empty: same explicit enumeration, drop loop - complement_of_singleton_contains_every_other_method: split into two tests: complement_of_singleton_excludes_that_method (asserts absent) complement_of_singleton_contains_every_other_method (asserts each of 8) All 13 runtime tests pass; static_asserts compile clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the std::memset call in connection_state::reset_arena() with a
new httpserver::detail::secure_zero helper that the optimizer is
forbidden to elide as a dead store. The helper dispatches to
explicit_bzero (glibc/musl/BSD), RtlSecureZeroMemory (Windows), or a
portable volatile-pointer loop + asm("" ::: "memory") clobber fallback
(macOS and any other lane without explicit_bzero). The full 8 KiB
initial_buffer_ is scrubbed unconditionally, closing the residual-
credential window across keep-alive requests for credentials that fit
within the arena (overflow blocks remain an accepted residue, documented
in the rewritten comment block).
The DR-008 sanity gate (a DEADBEEFCRED sentinel carried as a basic-auth
username on request 1 must not be observable to request 2 on the same
MHD connection) is pinned by a new integ test that reaches the
underlying MHD_Connection through a new HTTPSERVER_COMPILATION-gated
http_request::underlying_connection_for_testing() accessor and probes
connection_state::initial_buffer_ directly. A second integ-signal hook
(connection_opened) asserts curl keep-alive engaged exactly once across
both requests.
Two additional unit tests guard the helper itself: secure_zero_dce_test
is compiled at -O2 -DNDEBUG and reads every byte through a volatile
sink to verify the writes are not dead-store eliminated; and
connection_state_sentinel_test pins that reset_arena() zeros the entire
arena, not just the bump-pointer prefix.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adjust the body-residue integ test to force HTTP/1.1 (HTTP/2 stream multiplexing would invalidate the keep-alive precondition) and gate the headline assertion behind LT_ASSERT_EQ on connection_opened_count so a fresh connection cannot produce a vacuously-passing result. Switch the secure_zero DCE test's buffer to `volatile unsigned char[]` so the optimizer cannot propagate the pre-zeroing sentinel directly into the post-zeroing read (closes a real DCE loophole the prior sink-only approach left open). Extend the nullptr-with-zero-size test with an adjacent-sentinel guard so an accidental out-of-bounds write is caught. Flip TASK-068 in M7 _index.md to Done, and persist the 35 unworked review findings (0 critical, 2 major, 33 minor) from the final review cycle for future follow-up tasks. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The two-arg http_request_impl(MHD_Connection*, unescaper_ptr) overload
was preserved during the M3 PIMPL split (TASK-015 / TASK-016) "for
source compatibility" with callers not yet ported to the allocator-
taking form. The v2 cutover is complete and detail/ headers do not
require a deprecation cycle, so the overload comes out now.
Changes:
- src/httpserver/detail/http_request_impl.hpp: delete the two-arg
delegating constructor and its comment block. The default ctor
(test-request path) and the canonical three-arg allocator-aware
ctor remain.
- src/http_request.cpp: migrate the sole live caller (the
heap-fallback branch when pick_resource returns the default
resource) to pass std::pmr::polymorphic_allocator<>(get_default_
resource()) explicitly. Semantically identical to what the
delegating ctor did internally; byte-for-byte heap behaviour is
unchanged.
- test/unit/http_request_pimpl_test.cpp: add three static_asserts
pinning the construction surface:
1. default-constructible (test-request path);
2. canonical three-arg (MHD_Connection*, unescaper_ptr, alloc)
remains constructible;
3. negative: !is_constructible from (MHD_Connection*,
unescaper_ptr) -- the TASK-069 sentinel. If the two-arg
overload ever returns, the build breaks here.
Verification:
- `make check -j1` (release): 98/98 pass.
- `make check -j1` (--enable-debug, -Werror -Wextra): 98/98 pass.
- Header-only cross-flag sweep compiles the sentinel TU under each
of -UHAVE_BAUTH / -UHAVE_DAUTH / -UHAVE_GNUTLS / -UHAVE_WEBSOCKET
cleanly (proves the canonical ctor's #ifdef-conditioned member-
init list still works under every flag-off permutation, per the
plan's Step B).
Pre-existing on feature/v2.0, NOT introduced by this task:
- check-doxygen.sh fails with 7 warnings (4 @security + start()
refs + stale arguments_accumulator refs) in
create_webserver.hpp / hook_context.hpp / webserver_websocket.hpp.
None of those files are in this diff; tracked separately for a
future cleanup.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds Sub-test D (per_resource_add_hook_first_call_cas_no_data_race) and
extends Sub-test A's driver_body switch from 6 ops to 8 ops to cover the
per-resource (middle-tier) hook bus that the existing test header was
claiming but not actually exercising.
Sub-test D structurally proves the contended-null window in
http_resource::ensure_table() (src/http_resource.cpp:93-110) is entered:
64 iterations x 8 racing threads, each iteration synchronised on a
std::latch so workers enter add_hook concurrently against a freshly-null
hook_table_ slot. The HTTPSERVER_COMPILATION-gated hook_table_raw_()
accessor witnesses pre-race null (none installed) -> post-race non-null
(exactly one CAS-arbitrated table installed). A per-iteration
"entered-after-latch" counter pins the structural CAS-contention proof
(>=2 workers visible inside the contended-null window). Total CAS races
per run: 512 (shape-bounded; ~5ms warm, ~25ms TSan).
Sub-test A's driver_body now also fires:
- op 6: fresh stack-local cas_witness_resource per call -> contended-null
branch of ensure_table() driven from inside an MHD handler thread while
route_table_mutex_ is held shared by dispatch (the documented 3-tier
lock order in a single moment).
- op 7: shared cas_witness_resource captured by reference -> load-acquire
short-circuit branch ("if (existing) return existing;") complementing
op 6's contended-null branch.
Two new counters (cas_resource_hook_ok, cas_existing_hook_ok) are pinned
by LT_CHECK_GT alongside the existing TASK-032/052 counters.
The Sub-test A header comment is corrected to stop overclaiming
per-resource coverage; an explicit Sub-test D header describes the new
coverage. grep -n "resource hook_table" returns three comment lines
accurately describing the lock-order claim (AC 3).
src/http_resource.cpp is bit-identical (AC 4). No library rebuild
required (HTTPSERVER_COMPILATION already in test/Makefile.am AM_CPPFLAGS).
No instrumentation macro added; the witness is purely test-side.
Manual TSan validation on macOS (Apple clang) confirmed zero data-race
warnings; the CI Linux libstdc++ TSan lane re-runs this binary via the
existing build-type: tsan matrix entry in .github/workflows/verify-build.yml
without workflow changes.
Local: HTTPSERVER_STRESS_SECONDS=2 make check -j1 -> 98/98 PASS.
Sub-test D INFO line: iters=64 threads_per_iter=8
total_post_race_nonnull=64 contended_window_iters=64.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ct and mark Done - Add CONTRACT comment to hook_table_raw_() in http_resource.hpp making the synchronisation requirement explicit: the accessor is a non-atomic .get() on the shared_ptr and is safe only when called after a happens-before edge over any concurrent ensure_table() writer (e.g. after joining all threads). Addresses security-reviewer-iter1-1. - Add companion comment at the Sub-test D call site (line 993) explaining why the post-join read is safe. Addresses security-reviewer-iter1-1. - Create TASK-094.md in worktree and mark Status: Done with all six action item checkboxes checked. Addresses housekeeper-iter1-1 and housekeeper-iter1-2. - Add TASK-094 row (Done) to _index.md and bump Last updated to 2026-06-07. Addresses housekeeper-iter1-3. - Add cross-reference note to TASK-070.md under the TSan acceptance criterion and in the Dependencies section. Addresses housekeeper-iter1-4. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… CAS race Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The install_not_found_alias_ hook body was previously labelled "structurally deferred (TASK-048)". TASK-048 shipped; the deferral comment was a misread of the design pinned in DR-012 §4.10 — route_resolved is observation-only, so the on-wire 404 body intentionally flows through webserver_impl::not_found_page (the v1 call site), and the alias seat at this phase exists as the architectural anchor required by PRD-HOOK-REQ-009 (v1 setters register as hook-bus subscriptions). The body is upgraded from a stub-with-debt-comment to a documented no-op observation marker that explicitly does NOT re-invoke the user handler (avoiding double-counting of v1 observed call rates). Pin the wire contract end-to-end with hooks_not_found_alias_test: - default branch (no explicit handler) returns 404 "Not Found", - custom branch returns the user handler's body once per miss, - a user route_resolved observer co-fires alongside the alias seat (DR-012 §4.10 ordering — observation phase is not short-circuit- capable). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…da_handler arm route_entry::handler was originally `std::variant<lambda_handler, shared_ptr<http_resource>>`. The lambda_handler arm has been dead code since TASK-053: every writer of route_entry (on_*/route entry points, register_path, register_prefix) funnels lambdas through a lambda_resource shim and stores the shim as shared_ptr<http_resource>. No call site stored a lambda directly, and no v2 task introduced a writer that would. Both readers (resolve_resource_for_request, prepare_or_create_lambda_shim) already treated the lambda arm as unreachable. Collapse handler to a bare shared_ptr<http_resource>: - route_entry.hpp drops std::variant + the lambda_handler typedef; - the lambda_handler typedef relocates to detail/lambda_resource.hpp where its remaining uses live (the per-method slot signature); - the dispatch site (webserver_dispatch.cpp::resolve_resource_for_request) reads result.entry.handler directly with a defensive null check; - prepare_or_create_lambda_shim and update_existing_v2_entry shed the std::get_if indirection; - the routing_regression unit pin updates likewise. Pin the new shape with a static_assert in webserver_on_methods_test: TASK-071 history is captured in the assertion message so a future reintroduction of variant storage trips an early-warning canary at compile time. The webserver::route(...) and on_*(...) public signatures take std::function (not std::variant) and were unaffected — no Doxygen update needed on the public surface. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nd_alias_test Ports 8211-8213 collided with webserver_on_methods_test (PORT+21..23); shift to 8214-8216. Replace sleep_for(50ms) with a CURLOPT_CONNECT_ONLY retry loop (2 s deadline) so tests survive loaded CI hosts without sending spurious HTTP traffic that would skew not-found-handler counts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mark TASK-071 Status Done and tick all four action-item checkboxes. Update _index.md TASK-071 row from Backlog to Done. Update route-table.md to reflect that route_entry::handler is now a bare std::shared_ptr<http_resource> (lambda_handler variant arm removed); append TASK-071 note to the Implementation status paragraph. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…handler variant arm Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Route the GET-argument unescape output through the per-connection arena so the warm-path zero-global-allocation guarantee holds unconditionally -- previously the v1 code path materialised the unescape result in a std::string temporary, paying one global-heap allocation per argument when the value exceeded std::string's SSO threshold (TASK-018 acknowledged deferral). build_request_args now allocates the destination pmr::string directly in the arena domain and runs the standard %HH / '+' decode in place via a TU-local http_unescape_raw helper. When a user unescaper is registered the ABI-locked `void(std::string&)` callback is fed a thread_local std::string scratch buffer whose capacity amortises across requests on the same worker thread, so the steady-state per-request global-heap cost is zero on the user path too. Tests pinned in test/unit/http_request_unescape_arena_test.cpp: the headline pair use a global operator-new counter (RAII window guard) to assert that build_request_args makes zero global-heap allocations on the warm cycle for both the default and the user-registered unescaper paths. Correctness + lifetime tests cover %2F decode, invalid-hex passthrough, empty input, view-outlives-subsequent-inserts, and the user-callback grow path. bench_warm_path gains two variants: (5) %2F-containing value through the arena-routed unescape and (6) no-escape baseline. The two medians land within noise (99 ns vs 102 ns on the debug build), satisfying the "matches the no-unescape baseline within noise" acceptance criterion. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The old bench accumulated all 1M values under a single key without resetting the arena between outer rounds. The 65536-byte arena was exhausted after ~819 iterations (65536 / ~80 bytes per pmr::string), so every subsequent emplace_back spilled to the upstream heap — the bench was measuring heap-allocation cost in steady state, the exact overhead TASK-072 was supposed to eliminate. Restructure bench sections (5) and (6) to mirror the production per-request lifecycle: each measured call creates a fresh arena-backed impl, inserts one arg, and destroys the impl. With a 65536-byte arena and a single insert per call, all pmr::string allocations stay inside the arena and the bench now proves the zero-heap-alloc guarantee. (5) and (6) now report indistinguishable medians (~1815 ns vs ~1817 ns) confirming that the %2F-decode path adds no heap overhead over the no-escape baseline. Addresses: performance-reviewer-iter1-1 (critical) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both src/http_utils.cpp and src/detail/http_request_impl.cpp kept
private copies of hex_digit_value and the core %HH/'+' decode loop,
each with an independent comment acknowledging the duplication.
Promote both helpers to src/httpserver/detail/unescape_helpers.hpp:
- hex_digit_value: constexpr int, inlined
- unescape_buf_raw: inline raw-buffer unescape (char*, size_t) -> size_t
http_unescape in http_utils.cpp is now a thin wrapper that calls
unescape_buf_raw and adds the trailing '\0' expected by callers of
the std::string* variant.
http_request_impl.cpp's anonymous http_unescape_raw is removed; its
sole caller (unescape_in_arena) now calls unescape_buf_raw directly.
Also addresses:
- Rename unescape_in_arena parameter 'sink' -> 'value' and
thread_local 'user_scratch' -> 'thread_value' for naming
consistency. (code-simplifier-iter1-5)
- Document the per-thread amortisation contract and arena-waste
behaviour of the user-callback path. (performance-reviewer-iter1-2,
performance-reviewer-iter1-3)
- Collapse the long call-site comment in build_request_args to a
single cross-reference line. (code-simplifier-iter1-7)
Addresses: code-simplifier-iter1-1 (critical), code-simplifier-iter1-2
(critical), code-simplifier-iter1-5 (major), performance-reviewer-iter1-2
(major), performance-reviewer-iter1-3 (major)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three improvements to http_request_unescape_arena_test.cpp: 1. decode_via_arena(const char*) helper: extracts the shared setup boilerplate (arena+impl+aa+build_request_args+teardown) from correctness tests (3)-(5). Each test body is now a single LT_CHECK_EQ assertion. (code-simplifier-iter1-3) 2. run_alloc_pin(unescaper_ptr, int warmup_rounds) helper: extracts the shared setup from the two headline zero-alloc-pin tests (1)/(2). The warmup_rounds parameter makes the reason for the different cold- call counts between the default-unescaper (1 warmup) and user- unescaper (2 warmups) paths explicit. (code-simplifier-iter1-6) 3. Suite comment: annotate the empty set_up/tear_down stubs as intentional (the per-test lifecycle lives in the helpers), removing the structural ambiguity. (code-simplifier-iter1-4) All 8 tests pass; 12 assertions (down from 16 because tests (3)-(5) are now single-assertion calls to decode_via_arena). Addresses: code-simplifier-iter1-3 (major), code-simplifier-iter1-4 (major), code-simplifier-iter1-6 (major) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The header was missing from the Autotools dist list, meaning make dist would omit it from the released tarball and break out-of-tree builds. All in-tree builds were unaffected (masking the gap). Verified via make dist + tar tzf that the file now appears in the tarball. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Snapshot of the 35 findings (0 critical, 3 major, 32 minor) surfaced by the validation-loop reviewers that were deemed out of scope for this task. Mirrors the TASK-071 persistence pattern so they remain visible for future follow-up without blocking the merge. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Routes GET-argument unescape into the per-connection arena so the warm-path zero-allocation criterion holds even when a user unescaper is registered. Adds shared unescape_helpers.hpp, an arena-backed sink in build_request_args, a per-thread scratch buffer for the ABI-locked callback path, unit tests pinning zero global operator new calls, and bench_warm_path variants (5)/(6). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ral rationale
The MHD_OPTION_UNESCAPE_CALLBACK registration in webserver_setup.cpp
installs webserver_impl::unescaper_func as a no-op pass-through. The
existing comment framed this purely as a libmicrohttpd v0.99 workaround
for an unescape bug where the internal MHD percent-decoder could produce
strings containing embedded NULs (e.g. from %00), which then broke
MHD_get_connection_values / MHD_lookup_connection_value lookups
downstream.
Verified against the upstream libmicrohttpd ChangeLog
(/opt/homebrew/Cellar/libmicrohttpd/1.0.5/ChangeLog) that upstream
resolved the embedded-NUL-in-key/value-storage class of bug by adding
explicit binary-zero-aware key/value storage and the size-carrying
MHD_KeyValueIteratorN callback. The relevant ChangeLog entries are:
Wed 20 Mar 2019 Adding additional "value_length" argument to
MHD_KeyValueIterator callback to support binary zeros
in values. -CG
Wed May 1 2019 Implemented MHD_KeyValueIteratorN with sizes for
connection's key and value to get keys and values
with binary zeros. -EG
Fri May 3 2019 Added functions for working with keys and values
with binary zeros. -EG
Sun Jun 09 2019 Releasing libmicrohttpd 0.9.64. -EG
configure.ac requires libmicrohttpd >= 1.0.0 (released 2024-02), which
is five years and dozens of releases past 0.9.64, so the v0.99 bug is
no longer reachable on any supported MHD version.
However, the no-op callback registration must stay. Per microhttpd.h
1849-1872 (MHD_OPTION_UNESCAPE_CALLBACK doc), registering a custom
unescape callback suppresses MHD's internal "%HH" decode. libhttpserver
relies on that suppression for two independent reasons:
1. Honouring a user-registered unescaper hook
(create_webserver::unescaper(...)). If MHD pre-decoded behind our
back, the user callback would run on already-decoded input — the
custom_unescaper integ test (test/integ/basic.cpp:2179) pins this.
2. Routing GET-arg decoding through the per-connection PMR arena
(unescape_in_arena() in src/detail/http_request_impl.cpp:265, wired
by TASK-072). Internal MHD decode would heap-allocate and double-
decode.
So this task does not remove the registration; it rewrites the comment
to make the architectural justification durable and demote the v0.99
bug-fix framing to a historical footnote with a verified version
cutoff. No #if MHD_VERSION guard is added: configure.ac already
enforces the floor at build time, so a runtime guard would be dead
code.
No behavioural change. The three pinning suites called out in the
plan stay green:
- test/unit/http_request_unescape_arena_test.cpp (8 tests / 12 checks)
- test/unit/http_request_arena_test.cpp
- test/integ/basic.cpp (custom_unescaper at line 2179, plus 96 others)
Full test/ suite: 100/100 PASS.
The task file already shows Status: Complete; bring the _index.md task table into sync by flipping the TASK-073 row from Backlog to Done. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All findings are minor (0 critical, 0 major); they describe documentation nits in the new comment block and pre-existing out-of-diff inconsistencies in webserver_request.cpp comments. No behavioural concerns. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Documents the architectural rationale for keeping the no-op MHD_OPTION_UNESCAPE_CALLBACK registration — needed to suppress MHD's internal %HH decode so the per-connection PMR arena (TASK-072) and any user-registered unescaper hook own decoding end-to-end. The v0.99 bug framing is demoted to a historical footnote with a verified version cutoff (libmicrohttpd 0.9.64, 2019-06-09). No behavioural change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the #ifdef DEBUG guard around the raw request-body stdout
dump in src/detail/webserver_body_pipeline.cpp with a runtime opt-in
keyed on LIBHTTPSERVER_DEBUG_DUMP_REQUEST_BODY. Default behaviour is
now silent on RELEASE *and* DEBUG builds alike -- the env var is the
only gate, so a debug build accidentally shipped to production cannot
leak credentials/PII unless the operator explicitly opted in.
When the env var is observed at webserver::start(), the library emits
a single SECURITY WARNING line to stderr (and to the user's log_error
callback when wired) naming the env var, the credential/PII risk, and
the docs pointer. Process-wide std::atomic<bool> flag means multiple
webservers in the same process produce exactly one stderr emission.
The runtime gate is cached in a function-local static (Magic Static)
so getenv() is read at most once per process; subsequent setenv()
calls are intentionally ignored (debug-knob semantics).
New documentation:
- docs/debug-env-vars.md - canonical home for LIBHTTPSERVER_*
debug knobs; describes effect, risk, startup signal, disable
procedure, and the spot-check that release-with-DDEBUG still
behaves correctly. Includes a checklist for future debug knobs.
- specs/architecture/10-observability.md cross-references the new
doc.
New tests (test/integ/):
- debug_dump_request_body_unset_test.cpp - asserts silence on both
stdout and stderr when the env var is unset; passes identically
on DEBUG and RELEASE builds.
- debug_dump_request_body_set_test.cpp - asserts the body dump
and one-shot SECURITY WARNING fire when the env var is set, and
pins the process-wide idempotence with a second webserver.
The two binaries are split because the function-local-static cache
means the first observation in the process locks in for the rest;
splitting gives each scenario a deterministic fresh process.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… issues Mark TASK-074 status as Done in the task file and milestone index, and record the 28 review findings (6 major, 22 minor) that were not actioned during this task so they can be picked up by a follow-up cleanup pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Integration branch for the v2.0 modernization effort. Tasks land here individually (one merge commit per task) so the full v2.0 ships as a single reviewable PR.
This PR will remain draft until all milestones are complete.
Milestones
Specs live under
specs/(product_specs, architecture, tasks).Merged tasks
Test plan
Per-task validation runs through the groundwork validation loop on each task branch before merging here. Pre-merge of v2.0 to
master:./configure && makeclean on macOS (Apple Clang) and Linux (recent GCC)make checkgreen-std=c++(11|14|17)regressions in tree🤖 Generated with Claude Code