Skip to content

Commit d5e03f4

Browse files
committed
Merge TASK-015 review cleanup (18/64 fixed, 46 deferred)
2 parents 4f583fe + bd64fbc commit d5e03f4

8 files changed

Lines changed: 173 additions & 50 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
**Responsibility:** Carry per-request inputs from MHD's worker thread to the user handler. Lazily-cache derived data (path pieces, parsed args, basic-auth credentials, client cert fields).
44

5-
**Implementation:** PIMPL via `std::unique_ptr<http_request_impl>`. The impl is **arena-allocated** from a `std::pmr::monotonic_buffer_resource` that lives on the connection (one arena per MHD connection, reset between requests on the same keep-alive connection). The arena also backs the impl's owned strings and lazy-cache containers where practical, eliminating per-request `malloc` on the hot path.
5+
**Implementation:** PIMPL via `std::unique_ptr<http_request_impl>` (with a custom deleter that supports both heap and arena lifetimes). The impl is **arena-allocated** from a `std::pmr::monotonic_buffer_resource` that lives on the connection (one arena per MHD connection, reset between requests on the same keep-alive connection). The arena also backs the impl's owned strings and lazy-cache containers where practical, eliminating per-request `malloc` on the hot path. *[Historical note: the PIMPL boundary was introduced in TASK-015 using `std::make_unique` as a temporary heap allocation; the per-connection arena plumbing was wired in TASK-016, completing the DR-003b Option 2 decision.]*
66

77
**Interfaces:**
88
- Exposes (from PRD §3.6):

specs/architecture/11-decisions/DR-003b.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@
2121
- `webserver` constructor takes the arena allocator out of band (request constructor receives it implicitly via the dispatch path; not a public API surface).
2222
- `std::pmr::polymorphic_allocator` plumbed through `webserver_impl` → connection state → request ctor.
2323

24+
**Implementation phasing:**
25+
- *TASK-015* introduced the PIMPL boundary with `std::make_unique<http_request_impl>()` (plain heap allocation) as an incremental step to split the public header from the backend-coupled impl. This was an intentional temporary deviation from the full Option 2 design.
26+
- *TASK-016* wired the per-connection `std::pmr::monotonic_buffer_resource` arena through `webserver_impl::connection_notify` and the `pick_resource()` helper in `http_request.cpp`, completing the DR-003b Option 2 decision. The `http_request_impl` constructor and all its container members are PMR-aware as of TASK-016.
27+
2428
---

specs/unworked_review_issues/2026-05-04_013108_task-013.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@
2020
The unauthorized() factory drops the v1 digest-auth nonce/opaque state-machine (previously using MHD_queue_auth_required_response3) in favour of a static WWW-Authenticate challenge. This is a meaningful behavioural regression for callers relying on full MHD digest-auth. The commit message and the http_response.hpp doc-comment (line 183-188) acknowledge the trade-off, but no Decision Record captures it. DR-005 covers body representation; no DR covers the auth-flow simplification. The architecture doc §4.3 lists unauthorized(scheme, realm, ...) as the factory without mentioning the state-machine loss.
2121
*Recommendation:* Create a new DR (e.g. DR-012) documenting: (a) the decision to replace MHD_queue_auth_required_response3 with a static WWW-Authenticate challenge, (b) the rationale (removes dependency on connection-time state / MHD internal nonce tracking from the response value type), and (c) the consequence that callers wanting full RFC 7616 digest auth must reach MHD APIs directly. Reference PRD-RSP-REQ-005. This brings the arch record into sync with the implementation without changing any code.
2222

23-
4. [ ] **architecture-alignment-checker** | `src/httpserver/http_request.hpp:28` | interface-contract
23+
4. [x] **architecture-alignment-checker** | `src/httpserver/http_request.hpp:28` | interface-contract
2424
http_request.hpp still includes <microhttpd.h> directly (line 28), leaking MHD types into the public umbrella header via httpserver.hpp. Like the webserver.hpp case this predates TASK-013 (M3 / TASK-015 is assigned to fix it). TASK-013's stated acceptance check was narrowly scoped to http_response.hpp, and that file is now clean — no struct MHD_Connection or struct MHD_Response forward declarations remain in http_response.hpp (confirmed). No regression was introduced by this commit.
2525
*Recommendation:* Track alongside finding #2 as a pre-M3 accepted deviation. Verify that the M3 PIMPL tasks (TASK-014, TASK-015) clear both headers.
26+
*Status:* fixed — TASK-015 removed `#include <microhttpd.h>` from http_request.hpp; confirmed absent (grep returns nothing for the include in the public header, which now carries only a comment at lines 28-38 explaining the deliberate absence). Closed by TASK-015 review cleanup batch (2026-05-27).
2627

2728
5. [ ] **architecture-alignment-checker** | `src/httpserver/webserver.hpp:28` | component-boundary
2829
webserver.hpp includes <microhttpd.h>, <pthread.h>, and <sys/socket.h> in the public header, which conflicts with the documented architecture goal in §4.1: 'Public header <httpserver/webserver.hpp> includes only <httpserver/create_webserver.hpp> and standard library, never <microhttpd.h> or <pthread.h>.' This pre-dates TASK-013 and is tracked as deferred work (TASK-014 PIMPL split, M3 milestone per specs/tasks/_index.md), but the constraint remains unmet in the post-TASK-013 state.

specs/unworked_review_issues/2026-05-04_211034_task-015.md

Lines changed: 82 additions & 18 deletions
Large diffs are not rendered by default.

src/detail/http_request_impl.cpp

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,25 @@ namespace httpserver {
4343

4444
namespace detail {
4545

46+
// Map a MHD_ValueKind to the corresponding test-request local storage map.
47+
// Returns nullptr for kinds that have no local-storage counterpart (e.g.
48+
// MHD_GET_ARGUMENT_KIND). Called from both get_connection_value() and
49+
// ensure_headerlike_cache() to keep the kind→map dispatch in one place.
50+
// Adding a new kind (e.g. MHD_POSTDATA_KIND) only requires updating this
51+
// function. (code-simplifier-iter1 findings #3/#4 / major review items)
52+
const http::header_map* http_request_impl::local_map_for(MHD_ValueKind kind) const noexcept {
53+
switch (kind) {
54+
case MHD_HEADER_KIND: return &headers_local;
55+
case MHD_FOOTER_KIND: return &footers_local;
56+
case MHD_COOKIE_KIND: return &cookies_local;
57+
default: return nullptr;
58+
}
59+
}
60+
4661
std::string_view http_request_impl::get_connection_value(std::string_view key, MHD_ValueKind kind) const {
4762
// Test-request path: connection_ is null, fall back to local storage.
4863
if (connection_ == nullptr) {
49-
const auto* map = [&]() -> const http::header_map* {
50-
switch (kind) {
51-
case MHD_HEADER_KIND: return &headers_local;
52-
case MHD_FOOTER_KIND: return &footers_local;
53-
case MHD_COOKIE_KIND: return &cookies_local;
54-
default: return nullptr;
55-
}
56-
}();
64+
const auto* map = local_map_for(kind);
5765
if (map != nullptr) {
5866
// header_comparator is_transparent: find(string_view) works without
5967
// constructing a temporary std::string. (performance-reviewer-iter1-30)
@@ -83,34 +91,32 @@ MHD_Result http_request_impl::build_request_header(void* cls, MHD_ValueKind kind
8391
const http::header_view_map& http_request_impl::ensure_headerlike_cache(MHD_ValueKind kind) const {
8492
// Pick the cache slot and build-flag matching `kind`. We resolve them
8593
// up front so the cold (build) and warm (return) paths share a single
86-
// reference without re-switching.
94+
// reference without re-switching. local_map_for() provides the
95+
// local-storage fallback pointer without duplicating the switch.
96+
// (code-simplifier-iter1 findings #3/#4 / major review items)
8797
http::header_view_map* cache = nullptr;
8898
bool* built = nullptr;
89-
const http::header_map* local_fallback = nullptr;
9099
switch (kind) {
91100
case MHD_HEADER_KIND:
92101
cache = &headers_cached_;
93102
built = &headers_cache_built_;
94-
local_fallback = &headers_local;
95103
break;
96104
case MHD_FOOTER_KIND:
97105
cache = &footers_cached_;
98106
built = &footers_cache_built_;
99-
local_fallback = &footers_local;
100107
break;
101108
case MHD_COOKIE_KIND:
102109
cache = &cookies_cached_;
103110
built = &cookies_cache_built_;
104-
local_fallback = &cookies_local;
105111
break;
106112
default:
107113
// Unsupported kind: hand back the headers cache (kept empty)
108114
// as a safe fallback; the public API never reaches here.
109115
cache = &headers_cached_;
110116
built = &headers_cache_built_;
111-
local_fallback = &headers_local;
112117
break;
113118
}
119+
const http::header_map* local_fallback = local_map_for(kind);
114120

115121
if (*built) {
116122
return *cache;
@@ -350,7 +356,8 @@ void http_request_impl::grow_last_arg(const std::string& key, const std::string&
350356

351357
#ifdef HAVE_BAUTH
352358
void http_request_impl::fetch_user_pass() const {
353-
// Test-request path: connection_ is null, credentials already set.
359+
// Test-request path: connection_ is null, credentials already set
360+
// directly by create_test_request; nothing to fetch.
354361
if (connection_ == nullptr) {
355362
return;
356363
}
@@ -363,6 +370,11 @@ void http_request_impl::fetch_user_pass() const {
363370
}
364371
MHD_free(info);
365372
}
373+
// Mark as fetched so subsequent get_user()/get_pass() calls skip the
374+
// MHD round-trip -- even when the request carries no auth header and
375+
// the credential strings remain empty after this call.
376+
// (code-simplifier-iter1 finding #6 / major review item)
377+
user_pass_fetched = true;
366378
}
367379
#endif // HAVE_BAUTH
368380

src/http_request.cpp

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -309,12 +309,15 @@ std::string_view http_request::get_querystring() const noexcept {
309309

310310

311311
std::string_view http_request::get_requestor() const {
312-
if (!impl_->requestor_ip.empty()) {
313-
return impl_->requestor_ip;
314-
}
315-
316-
// Test-request path: connection_ is null, requestor_ip already set.
317-
if (impl_->connection_ == nullptr) {
312+
// Single consolidated early-return covers both the cache-hit path
313+
// (requestor_ip_cached == true after first call on a live connection)
314+
// and the test-request path (connection_ == nullptr, requestor_ip was
315+
// set directly by create_test_request). Using a dedicated boolean instead
316+
// of checking requestor_ip.empty() is consistent with the args_populated /
317+
// path_pieces_cached / user_pass_fetched pattern and is robust if the
318+
// connection layer ever returns an empty IP string.
319+
// (code-simplifier-iter1 findings #7 + #8/#9 / major+minor review items)
320+
if (impl_->requestor_ip_cached || impl_->connection_ == nullptr) {
318321
return impl_->requestor_ip;
319322
}
320323

@@ -326,6 +329,7 @@ std::string_view http_request::get_requestor() const {
326329

327330
auto ip = http::get_ip_str(conninfo->client_addr);
328331
impl_->requestor_ip.assign(ip.data(), ip.size());
332+
impl_->requestor_ip_cached = true;
329333
return impl_->requestor_ip;
330334
}
331335

src/http_request_auth.cpp

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

4242
std::string_view http_request::get_user() const {
4343
#ifdef HAVE_BAUTH
44-
if (!impl_->username.empty()) {
45-
return impl_->username;
44+
// Use user_pass_fetched instead of !username.empty() as the cache
45+
// guard: an empty username string after a successful MHD call is a
46+
// valid result (no auth header), but would cause a redundant re-fetch
47+
// on every subsequent get_user() call without the boolean.
48+
// (code-simplifier-iter1 finding #6 / major review item)
49+
if (!impl_->user_pass_fetched) {
50+
impl_->fetch_user_pass();
51+
// fetch_user_pass() sets user_pass_fetched = true on the live path.
52+
// On the test-request path (connection_ == nullptr) it returns early
53+
// without setting the flag; set it here so future calls are skipped.
54+
impl_->user_pass_fetched = true;
4655
}
47-
impl_->fetch_user_pass();
4856
return impl_->username;
4957
#else
5058
// TASK-034 §7 sentinel: BAUTH disabled at build time -> empty view.
@@ -54,10 +62,11 @@ std::string_view http_request::get_user() const {
5462

5563
std::string_view http_request::get_pass() const {
5664
#ifdef HAVE_BAUTH
57-
if (!impl_->password.empty()) {
58-
return impl_->password;
65+
// Mirror the get_user() boolean guard (code-simplifier-iter1 #6).
66+
if (!impl_->user_pass_fetched) {
67+
impl_->fetch_user_pass();
68+
impl_->user_pass_fetched = true;
5969
}
60-
impl_->fetch_user_pass();
6170
return impl_->password;
6271
#else
6372
return std::string_view{};

src/httpserver/detail/http_request_impl.hpp

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,14 @@ class http_request_impl {
131131

132132
http_request_impl(const http_request_impl&) = delete;
133133
http_request_impl& operator=(const http_request_impl&) = delete;
134-
// Moves are left implicitly defined (and unused -- the impl is held
135-
// through unique_ptr so http_request's defaulted moves operate on
136-
// the unique_ptr, not on the impl directly).
134+
// Move operations: explicitly defaulted to document intent.
135+
// The impl is held through a custom-deleter unique_ptr, so http_request's
136+
// own move ctor/assign operate on the unique_ptr, never on the impl
137+
// directly. These defaults are unused in practice but explicit = default
138+
// is clearer than relying on the implicit definition.
139+
// (code-simplifier-iter1 finding #33 / minor review item)
140+
http_request_impl(http_request_impl&&) = default;
141+
http_request_impl& operator=(http_request_impl&&) = default;
137142

138143
// --- per-request backend handles ---
139144
MHD_Connection* connection_ = nullptr;
@@ -181,6 +186,23 @@ class http_request_impl {
181186
mutable std::pmr::vector<std::pmr::string> path_pieces;
182187
mutable bool args_populated = false;
183188
mutable bool path_pieces_cached = false;
189+
#ifdef HAVE_BAUTH
190+
// Guard for fetch_user_pass(): once the MHD round-trip has been made
191+
// (whether or not the request carries a Basic-Auth header), further
192+
// calls to get_user()/get_pass() skip the MHD call entirely.
193+
// Using a dedicated boolean (rather than checking username.empty())
194+
// matches the args_populated / path_pieces_cached pattern and avoids
195+
// a redundant MHD call on requests with no auth credentials where the
196+
// credential strings remain empty after the first fetch.
197+
// (code-simplifier finding #6 / major review item)
198+
mutable bool user_pass_fetched = false;
199+
#endif // HAVE_BAUTH
200+
// Cache guard for get_requestor(). Using a dedicated boolean (rather
201+
// than checking requestor_ip.empty()) is consistent with the boolean-
202+
// flag pattern used by args_populated and path_pieces_cached, and is
203+
// robust if the connection layer ever returns an empty IP string.
204+
// (code-simplifier finding #7 / minor review item)
205+
mutable bool requestor_ip_cached = false;
184206

185207
// TASK-017: per-request caches for the six container getters. These
186208
// are typed in the public-API container types (default-allocator) so
@@ -233,6 +255,13 @@ class http_request_impl {
233255
#endif // HAVE_GNUTLS
234256

235257
// --- helpers (moved out of http_request public header) ---
258+
259+
// Map MHD_ValueKind to the corresponding test-request local-storage map
260+
// pointer (nullptr for kinds with no local counterpart). Centralises the
261+
// kind→map dispatch so get_connection_value() and ensure_headerlike_cache()
262+
// share one switch body. (code-simplifier-iter1 findings #3/#4)
263+
const http::header_map* local_map_for(MHD_ValueKind kind) const noexcept;
264+
236265
std::string_view get_connection_value(std::string_view key, MHD_ValueKind kind) const;
237266
// TASK-017: ensures the cache for `kind` (HEADER / FOOTER / COOKIE) is
238267
// populated and returns a const reference to it. First call fills the

0 commit comments

Comments
 (0)