Skip to content

Commit 6d73c15

Browse files
committed
Merge TASK-017: http_request container getters return const&
2 parents 54cfe91 + 6352488 commit 6d73c15

11 files changed

Lines changed: 406 additions & 83 deletions

File tree

examples/args_processing.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ class args_resource : public httpserver::http_resource {
4040

4141
response_body << "=== Using get_args() (supports multiple values per key) ===\n\n";
4242

43-
// get_args() returns a map where each key maps to an http_arg_value.
44-
// http_arg_value contains a vector of values for parameters like "?id=1&id=2&id=3"
45-
auto args = req.get_args();
43+
// get_args() returns a const reference to a map where each key
44+
// maps to an http_arg_value. http_arg_value contains a vector of
45+
// values for parameters like "?id=1&id=2&id=3". The reference
46+
// remains valid for the duration of this handler call.
47+
const auto& args = req.get_args();
4648
for (const auto& [key, arg_value] : args) {
4749
response_body << "Key: " << key << "\n";
4850
// Use get_all_values() to get all values for this key

specs/tasks/M3-request/TASK-017.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
Stop copying maps/vectors out of `http_request` on every getter call.
99

1010
**Action Items:**
11-
- [ ] Change return types of `get_args`, `get_path_pieces`, `get_files`, `get_headers`, `get_footers`, `get_cookies` from by-value to `const ContainerType&`.
12-
- [ ] Mark each getter `const`.
13-
- [ ] If a v1 caller relied on copy semantics (modifying the returned value), update it to copy explicitly at the call site.
14-
- [ ] Document in the header that the returned reference is valid until the request object is destroyed (typically until handler return).
11+
- [x] Change return types of `get_args`, `get_path_pieces`, `get_files`, `get_headers`, `get_footers`, `get_cookies` from by-value to `const ContainerType&`.
12+
- [x] Mark each getter `const`.
13+
- [x] If a v1 caller relied on copy semantics (modifying the returned value), update it to copy explicitly at the call site.
14+
- [x] Document in the header that the returned reference is valid until the request object is destroyed (typically until handler return).
1515

1616
**Dependencies:**
1717
- Blocked by: TASK-015
@@ -27,4 +27,4 @@ Stop copying maps/vectors out of `http_request` on every getter call.
2727
**Related Requirements:** PRD-REQ-REQ-001
2828
**Related Decisions:** §4.2
2929

30-
**Status:** Not Started
30+
**Status:** Done

specs/tasks/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ Nominally: **13 sequential tasks**, each S–XL. Most other tasks parallelize of
9999
| TASK-014 | `webserver_impl` skeleton (PIMPL prep) | M3 | In Progress | TASK-002 |
100100
| TASK-015 | `http_request_impl` skeleton (PIMPL split) | M3 | In Progress | TASK-002, TASK-014 |
101101
| TASK-016 | Per-connection arena for `http_request_impl` | M3 | Done | TASK-014, TASK-015 |
102-
| TASK-017 | `http_request` container getters return `const&` | M3 | Not Started | TASK-015 |
102+
| TASK-017 | `http_request` container getters return `const&` | M3 | Done | TASK-015 |
103103
| TASK-018 | `http_request` single-key getters return `string_view`, all const | M3 | Not Started | TASK-015, TASK-016 |
104104
| TASK-019 | High-level GnuTLS accessors replacing `gnutls_session_t` | M3 | Not Started | TASK-015 |
105105
| TASK-020 | Final public-header backend-include sweep | M3 | Not Started | TASK-014, TASK-015, TASK-019 |

specs/unworked_review_issues/2026-05-05_203613_task-017.md

Lines changed: 103 additions & 0 deletions
Large diffs are not rendered by default.

src/http_request.cpp

Lines changed: 84 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -168,31 +168,61 @@ MHD_Result http_request_impl::build_request_header(void* cls, MHD_ValueKind kind
168168
return MHD_YES;
169169
}
170170

171-
http::header_view_map http_request_impl::get_headerlike_values(MHD_ValueKind kind) const {
172-
http::header_view_map headers;
171+
const http::header_view_map& http_request_impl::ensure_headerlike_cache(MHD_ValueKind kind) const {
172+
// Pick the cache slot and build-flag matching `kind`. We resolve them
173+
// up front so the cold (build) and warm (return) paths share a single
174+
// reference without re-switching.
175+
http::header_view_map* cache = nullptr;
176+
bool* built = nullptr;
177+
const http::header_map* local_fallback = nullptr;
178+
switch (kind) {
179+
case MHD_HEADER_KIND:
180+
cache = &headers_cached_;
181+
built = &headers_cache_built_;
182+
local_fallback = &headers_local;
183+
break;
184+
case MHD_FOOTER_KIND:
185+
cache = &footers_cached_;
186+
built = &footers_cache_built_;
187+
local_fallback = &footers_local;
188+
break;
189+
case MHD_COOKIE_KIND:
190+
cache = &cookies_cached_;
191+
built = &cookies_cache_built_;
192+
local_fallback = &cookies_local;
193+
break;
194+
default:
195+
// Unsupported kind: hand back the headers cache (kept empty)
196+
// as a safe fallback; the public API never reaches here.
197+
cache = &headers_cached_;
198+
built = &headers_cache_built_;
199+
local_fallback = &headers_local;
200+
break;
201+
}
202+
203+
if (*built) {
204+
return *cache;
205+
}
173206

174-
// Test-request path: connection_ is null, build view map from local storage.
207+
// Test-request path: connection_ is null, build the view map from
208+
// local owning storage (the create_test_request builder populated it).
175209
if (connection_ == nullptr) {
176-
const auto* map = [&]() -> const http::header_map* {
177-
switch (kind) {
178-
case MHD_HEADER_KIND: return &headers_local;
179-
case MHD_FOOTER_KIND: return &footers_local;
180-
case MHD_COOKIE_KIND: return &cookies_local;
181-
default: return nullptr;
182-
}
183-
}();
184-
if (map != nullptr) {
185-
for (const auto& [k, v] : *map) {
186-
headers[k] = v;
210+
if (local_fallback != nullptr) {
211+
for (const auto& [k, v] : *local_fallback) {
212+
(*cache)[k] = v;
187213
}
188214
}
189-
return headers;
215+
*built = true;
216+
return *cache;
190217
}
191218

219+
// Live-request path: ask MHD to enumerate values for this kind into
220+
// the cache. The string_view keys/values alias MHD-owned storage that
221+
// outlives the request handler.
192222
MHD_get_connection_values(connection_, kind, &http_request_impl::build_request_header,
193-
reinterpret_cast<void*>(&headers));
194-
195-
return headers;
223+
reinterpret_cast<void*>(cache));
224+
*built = true;
225+
return *cache;
196226
}
197227

198228
MHD_Result http_request_impl::build_request_args(void* cls, MHD_ValueKind kind,
@@ -652,18 +682,21 @@ void http_request::set_method(const std::string& method) {
652682
this->method = method;
653683
}
654684

655-
const std::vector<std::string> http_request::get_path_pieces() const {
685+
const std::vector<std::string>& http_request::get_path_pieces() const {
686+
// TASK-017: lazily populate the public-typed mirror cache from the
687+
// (already-built) pmr-backed `path_pieces` and return it by const&.
688+
// Two caches in lockstep -- the pmr one stays arena-friendly for any
689+
// future internal consumer; the public one is what the API exposes.
656690
impl_->ensure_path_pieces_cached(path);
657-
// path_pieces is now pmr-backed; copy element-wise back into a default-
658-
// allocator std::vector<std::string> for the public return type. The
659-
// copy is intrinsic to the v1 API contract; TASK-017 narrows this to
660-
// a const& return that aliases the impl-side storage.
661-
std::vector<std::string> out;
662-
out.reserve(impl_->path_pieces.size());
663-
for (const auto& p : impl_->path_pieces) {
664-
out.emplace_back(p.data(), p.size());
691+
if (!impl_->path_pieces_public_built_) {
692+
impl_->path_pieces_public_.clear();
693+
impl_->path_pieces_public_.reserve(impl_->path_pieces.size());
694+
for (const auto& p : impl_->path_pieces) {
695+
impl_->path_pieces_public_.emplace_back(p.data(), p.size());
696+
}
697+
impl_->path_pieces_public_built_ = true;
665698
}
666-
return out;
699+
return impl_->path_pieces_public_;
667700
}
668701

669702
const std::string http_request::get_path_piece(int index) const {
@@ -725,24 +758,24 @@ std::string_view http_request::get_header(std::string_view key) const {
725758
return impl_->get_connection_value(key, MHD_HEADER_KIND);
726759
}
727760

728-
const http::header_view_map http_request::get_headers() const {
729-
return impl_->get_headerlike_values(MHD_HEADER_KIND);
761+
const http::header_view_map& http_request::get_headers() const {
762+
return impl_->ensure_headerlike_cache(MHD_HEADER_KIND);
730763
}
731764

732765
std::string_view http_request::get_footer(std::string_view key) const {
733766
return impl_->get_connection_value(key, MHD_FOOTER_KIND);
734767
}
735768

736-
const http::header_view_map http_request::get_footers() const {
737-
return impl_->get_headerlike_values(MHD_FOOTER_KIND);
769+
const http::header_view_map& http_request::get_footers() const {
770+
return impl_->ensure_headerlike_cache(MHD_FOOTER_KIND);
738771
}
739772

740773
std::string_view http_request::get_cookie(std::string_view key) const {
741774
return impl_->get_connection_value(key, MHD_COOKIE_KIND);
742775
}
743776

744-
const http::header_view_map http_request::get_cookies() const {
745-
return impl_->get_headerlike_values(MHD_COOKIE_KIND);
777+
const http::header_view_map& http_request::get_cookies() const {
778+
return impl_->ensure_headerlike_cache(MHD_COOKIE_KIND);
746779
}
747780

748781
http_arg_value http_request::get_arg(std::string_view key) const {
@@ -772,17 +805,25 @@ std::string_view http_request::get_arg_flat(std::string_view key) const {
772805
return impl_->get_connection_value(key, MHD_GET_ARGUMENT_KIND);
773806
}
774807

775-
const http::arg_view_map http_request::get_args() const {
808+
const http::arg_view_map& http_request::get_args() const {
809+
// TASK-017: lazily populate the args view-map cache from the pmr-backed
810+
// `unescaped_args` (which is itself populated lazily by populate_args()).
776811
impl_->populate_args();
777-
778-
http::arg_view_map arguments;
779-
for (const auto& [key, value] : impl_->unescaped_args) {
780-
auto& arg_values = arguments[key];
781-
for (const auto& v : value) {
782-
arg_values.values.push_back(v);
812+
if (!impl_->args_view_cache_built_) {
813+
impl_->args_view_cached_.clear();
814+
for (const auto& [key, value] : impl_->unescaped_args) {
815+
// The string_view keys/values alias the pmr-backed strings owned
816+
// by `unescaped_args` -- same lifetime as the request.
817+
auto& arg_values = impl_->args_view_cached_[
818+
std::string_view(key.data(), key.size())];
819+
arg_values.values.reserve(value.size());
820+
for (const auto& v : value) {
821+
arg_values.values.emplace_back(v.data(), v.size());
822+
}
783823
}
824+
impl_->args_view_cache_built_ = true;
784825
}
785-
return arguments;
826+
return impl_->args_view_cached_;
786827
}
787828

788829
const std::map<std::string_view, std::string_view, http::arg_comparator> http_request::get_args_flat() const {
@@ -798,7 +839,7 @@ http::file_info& http_request::get_or_create_file_info(const std::string& key, c
798839
return impl_->files_[key][upload_file_name];
799840
}
800841

801-
const std::map<std::string, std::map<std::string, http::file_info>> http_request::get_files() const {
842+
const std::map<std::string, std::map<std::string, http::file_info>>& http_request::get_files() const noexcept {
802843
return impl_->files_;
803844
}
804845

src/httpserver/detail/http_request_impl.hpp

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,40 @@ class http_request_impl {
176176
mutable bool args_populated = false;
177177
mutable bool path_pieces_cached = false;
178178

179+
// TASK-017: per-request caches for the six container getters. These
180+
// are typed in the public-API container types (default-allocator) so
181+
// http_request::get_*() can return `const ContainerType&` aliasing
182+
// impl-owned storage. Each is built lazily on the first getter call
183+
// and reused on subsequent calls.
184+
//
185+
// Allocator note: the public container types embed std::string_view
186+
// (header_view_map / arg_view_map) or std::string (path_pieces_public_)
187+
// and use the default allocator. They cannot be made PMR without
188+
// changing the public surface (TASK-017 plan, "Storage strategy").
189+
// The first call therefore allocates on the global heap; subsequent
190+
// calls are O(1) and zero-allocating -- a strict win over v1, which
191+
// paid the allocation on every call.
192+
//
193+
// The header/footer/cookie caches alias MHD-owned strings via
194+
// string_view, so they share the request's lifetime; the arg-view
195+
// cache aliases the impl's pmr-backed `unescaped_args`, same lifetime.
196+
mutable http::header_view_map headers_cached_;
197+
mutable http::header_view_map footers_cached_;
198+
mutable http::header_view_map cookies_cached_;
199+
mutable bool headers_cache_built_ = false;
200+
mutable bool footers_cache_built_ = false;
201+
mutable bool cookies_cache_built_ = false;
202+
203+
mutable http::arg_view_map args_view_cached_;
204+
mutable bool args_view_cache_built_ = false;
205+
206+
// Public-typed mirror of `path_pieces` (the pmr::vector<pmr::string>
207+
// already kept above). Two caches in lockstep: the pmr version stays
208+
// arena-friendly for any future internal consumer; the public version
209+
// is what get_path_pieces() returns by const&.
210+
mutable std::vector<std::string> path_pieces_public_;
211+
mutable bool path_pieces_public_built_ = false;
212+
179213
#ifdef HAVE_GNUTLS
180214
mutable bool client_cert_fields_cached = false;
181215
mutable std::pmr::string client_cert_dn;
@@ -189,7 +223,11 @@ class http_request_impl {
189223

190224
// --- helpers (moved out of http_request public header) ---
191225
std::string_view get_connection_value(std::string_view key, MHD_ValueKind kind) const;
192-
http::header_view_map get_headerlike_values(MHD_ValueKind kind) const;
226+
// TASK-017: ensures the cache for `kind` (HEADER / FOOTER / COOKIE) is
227+
// populated and returns a const reference to it. First call fills the
228+
// map (test-request fallback or MHD scan); subsequent calls return
229+
// the same reference in O(1).
230+
const http::header_view_map& ensure_headerlike_cache(MHD_ValueKind kind) const;
193231
void populate_args() const;
194232
void ensure_path_pieces_cached(std::string_view path) const;
195233

src/httpserver/http_request.hpp

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,25 @@ struct http_request_impl_deleter {
110110
* get_pass(), get_digested_user(), get_header(), get_footer(),
111111
* get_cookie(), get_requestor().
112112
* (security-reviewer-iter1-1, CWE-416 Use After Free.)
113+
*
114+
* ### Container reference lifetime contract (TASK-017)
115+
*
116+
* The container getters `get_headers()`, `get_footers()`, `get_cookies()`,
117+
* `get_args()`, `get_path_pieces()`, and `get_files()` all return a
118+
* `const ContainerType&` rather than a by-value copy. The reference and
119+
* any iterators / pointers / element references derived from it remain
120+
* valid until the `http_request` object is destroyed (typically when the
121+
* handler invocation returns).
122+
*
123+
* In particular, the `std::string_view` keys and values held inside the
124+
* `header_view_map` and `arg_view_map` returned by these getters carry the
125+
* same lifetime restriction as the standalone `std::string_view` accessors
126+
* above: do not store them past the handler call frame. Copy explicitly
127+
* if a longer lifetime is required.
128+
*
129+
* Implementation note: the first call to get_headers / get_footers /
130+
* get_cookies / get_args / get_path_pieces lazily populates a per-request
131+
* cache; subsequent calls are O(1) and return the same reference.
113132
**/
114133
class http_request {
115134
public:
@@ -155,9 +174,10 @@ class http_request {
155174

156175
/**
157176
* Method used to get all pieces of the path requested; considering an url splitted by '/'.
158-
* @return a vector of strings containing all pieces
177+
* @return a vector of strings containing all pieces. The reference
178+
* remains valid until the http_request is destroyed.
159179
**/
160-
const std::vector<std::string> get_path_pieces() const;
180+
[[nodiscard]] const std::vector<std::string>& get_path_pieces() const;
161181

162182
/**
163183
* Method used to obtain a specified piece of the path; considering an url splitted by '/'.
@@ -176,30 +196,35 @@ class http_request {
176196

177197
/**
178198
* Method used to get all headers passed with the request.
179-
* @param result a map<string, string> > that will be filled with all headers
180-
* @result the size of the map
199+
* @return a const reference to a map<string_view, string_view>
200+
* containing all headers. The reference (and the views it
201+
* holds) remain valid until the http_request is destroyed.
181202
**/
182-
const http::header_view_map get_headers() const;
203+
[[nodiscard]] const http::header_view_map& get_headers() const;
183204

184205
/**
185206
* Method used to get all footers passed with the request.
186-
* @param result a map<string, string> > that will be filled with all footers
187-
* @result the size of the map
207+
* @return a const reference to a map<string_view, string_view>
208+
* containing all footers. The reference (and the views it
209+
* holds) remain valid until the http_request is destroyed.
188210
**/
189-
const http::header_view_map get_footers() const;
211+
[[nodiscard]] const http::header_view_map& get_footers() const;
190212

191213
/**
192214
* Method used to get all cookies passed with the request.
193-
* @param result a map<string, string> > that will be filled with all cookies
194-
* @result the size of the map
215+
* @return a const reference to a map<string_view, string_view>
216+
* containing all cookies. The reference (and the views it
217+
* holds) remain valid until the http_request is destroyed.
195218
**/
196-
const http::header_view_map get_cookies() const;
219+
[[nodiscard]] const http::header_view_map& get_cookies() const;
197220

198221
/**
199222
* Method used to get all args passed with the request.
200-
* @result the size of the map
223+
* @return a const reference to the args map. The reference (and the
224+
* string_view keys/values it holds) remain valid until the
225+
* http_request is destroyed.
201226
**/
202-
const http::arg_view_map get_args() const;
227+
[[nodiscard]] const http::arg_view_map& get_args() const;
203228

204229
/**
205230
* Method used to get all args passed with the request. If any key has multiple
@@ -218,9 +243,11 @@ class http_request {
218243

219244
/**
220245
* Method used to get all files passed with the request.
221-
* @result result a map<std::string, map<std::string, http::file_info> > that will be filled with all files
246+
* @return a const reference to a map<std::string, map<std::string,
247+
* http::file_info>> containing all files. The reference
248+
* remains valid until the http_request is destroyed.
222249
**/
223-
const std::map<std::string, std::map<std::string, http::file_info>> get_files() const;
250+
[[nodiscard]] const std::map<std::string, std::map<std::string, http::file_info>>& get_files() const noexcept;
224251

225252
/**
226253
* Method used to get a specific header passed with the request.

0 commit comments

Comments
 (0)