Skip to content

Commit 07af54a

Browse files
committed
Merge TASK-011: http_response const-correct accessors
2 parents 21f1ab8 + b7d9138 commit 07af54a

9 files changed

Lines changed: 394 additions & 46 deletions

File tree

specs/tasks/M2-response/TASK-011.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
Make read accessors callable on `const http_response&`, returning views without inserting on miss.
99

1010
**Action Items:**
11-
- [ ] `std::string_view get_header(std::string_view key) const;` returns empty view on miss; does NOT insert.
12-
- [ ] Same for `get_footer(std::string_view) const;` and `get_cookie(std::string_view) const;`.
13-
- [ ] `const header_map& get_headers() const noexcept;` (and `get_footers`, `get_cookies`).
14-
- [ ] `int get_status() const noexcept;`
15-
- [ ] `body_kind kind() const noexcept;`
16-
- [ ] Remove any v1 accessor that inserted on miss (e.g., `headers[key]` patterns).
17-
- [ ] Audit `string_view` returns: the storage must outlive the view. Document lifetime contract on each accessor (views invalidated by mutation of the response, e.g., `with_header` may rehash the map).
11+
- [x] `std::string_view get_header(std::string_view key) const;` returns empty view on miss; does NOT insert.
12+
- [x] Same for `get_footer(std::string_view) const;` and `get_cookie(std::string_view) const;`.
13+
- [x] `const header_map& get_headers() const noexcept;` (and `get_footers`, `get_cookies`).
14+
- [x] `int get_status() const noexcept;`
15+
- [x] `body_kind kind() const noexcept;`
16+
- [x] Remove any v1 accessor that inserted on miss (e.g., `headers[key]` patterns).
17+
- [x] Audit `string_view` returns: the storage must outlive the view. Document lifetime contract on each accessor (views invalidated by mutation of the response, e.g., `with_header` may rehash the map).
1818

1919
**Dependencies:**
2020
- Blocked by: TASK-009
@@ -30,4 +30,4 @@ Make read accessors callable on `const http_response&`, returning views without
3030
**Related Requirements:** PRD-RSP-REQ-002, PRD-RSP-REQ-003
3131
**Related Decisions:** §2.2 (const correctness), §4.3
3232

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

specs/tasks/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Nominally: **13 sequential tasks**, each S–XL. Most other tasks parallelize of
9393
| TASK-008 | Internal `detail::body` hierarchy | M2 | Done | TASK-002 |
9494
| TASK-009 | `http_response` value type with SBO buffer | M2 | Done | TASK-008 |
9595
| TASK-010 | `http_response` factory functions | M2 | Done | TASK-008, TASK-009, TASK-004 |
96-
| TASK-011 | `http_response` const-correct accessors | M2 | Not Started | TASK-009 |
96+
| TASK-011 | `http_response` const-correct accessors | M2 | Done | TASK-009 |
9797
| TASK-012 | `http_response` fluent `with_*` setters | M2 | Not Started | TASK-009 |
9898
| TASK-013 | Remove `*_response` subclasses and dispatch virtuals | M2 | Not Started | TASK-009, TASK-010, TASK-011, TASK-012 |
9999
| TASK-014 | `webserver_impl` skeleton (PIMPL prep) | M3 | Not Started | TASK-002 |
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Unworked Review Issues
2+
3+
**Run:** 2026-05-03 21:10:00
4+
**Task:** TASK-011
5+
**Total:** 0 (0 critical, 0 major, 0 minor)
6+
7+
No unworked findings. All critical and major findings (task status Not Started → Done, seven action-item checkboxes) were resolved in the housekeeping pass.

specs/unworked_review_issues/2026-05-03_213725_task-011.md

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

src/http_response.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,39 @@ void http_response::shoutCAST() {
189189
status_code_ |= http::http_utils::shoutcast_response;
190190
}
191191

192+
// -----------------------------------------------------------------------
193+
// Const single-key accessors (TASK-011).
194+
//
195+
// All three share the same shape: heterogeneous lookup into the
196+
// corresponding header_map (transparent header_comparator), returning an
197+
// empty std::string_view on miss. NEVER inserts (PRD-RSP-REQ-003); the
198+
// previous v1 accessors used `headers_[key]`, which silently inserted
199+
// an empty entry on miss and consequently could not be const.
200+
//
201+
// View lifetime is documented in the class-level contract block in
202+
// http_response.hpp.
203+
// -----------------------------------------------------------------------
204+
namespace {
205+
inline std::string_view header_map_find_view(const http::header_map& m,
206+
std::string_view key) {
207+
auto it = m.find(key);
208+
if (it == m.end()) return {};
209+
return std::string_view(it->second);
210+
}
211+
} // namespace
212+
213+
std::string_view http_response::get_header(std::string_view key) const {
214+
return header_map_find_view(headers_, key);
215+
}
216+
217+
std::string_view http_response::get_footer(std::string_view key) const {
218+
return header_map_find_view(footers_, key);
219+
}
220+
221+
std::string_view http_response::get_cookie(std::string_view key) const {
222+
return header_map_find_view(cookies_, key);
223+
}
224+
192225
namespace {
193226
static inline http::header_view_map to_view_map(const http::header_map& hdr_map) {
194227
http::header_view_map view_map;

src/httpserver/http_response.hpp

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -183,53 +183,87 @@ class http_response {
183183
std::string_view realm,
184184
std::string body = {});
185185

186-
/**
187-
* Method used to get a specified header defined for the response
188-
* @param key The header identification
189-
* @return a string representing the value assumed by the header
190-
**/
191-
const std::string& get_header(const std::string& key) {
192-
return headers_[key];
193-
}
186+
// -----------------------------------------------------------------
187+
// Read accessors (TASK-011, PRD-RSP-REQ-002 / PRD-RSP-REQ-003).
188+
//
189+
// Lifetime contract for the string_view-returning accessors:
190+
//
191+
// The returned view points into storage owned by *this. The view is
192+
// valid until ANY of the following happen:
193+
// 1. *this is destroyed.
194+
// 2. *this is moved-from (move ctor / move-assign target).
195+
// 3. The corresponding map is mutated for the SAME key
196+
// (with_header(key, ...) replacing an existing value
197+
// invalidates a view obtained from a prior get_header(key)).
198+
//
199+
// std::map's node-stability guarantee means that adding or removing
200+
// OTHER keys does NOT invalidate views of unrelated keys; only
201+
// same-key re-assignment, erase, or whole-response destruction
202+
// does. Multi-value headers are not modelled in v2.0 — header_map
203+
// is single-valued per key.
204+
//
205+
// Callers MUST NOT keep the view past the next non-const operation
206+
// on the response, and MUST NOT keep it past the response's
207+
// destruction. If a longer lifetime is required, copy into a
208+
// std::string.
209+
//
210+
// No noexcept on the single-key accessors: std::map::find can in
211+
// principle propagate a comparator exception. The map-returning
212+
// accessors and the trivial scalar accessors (get_status, kind) are
213+
// noexcept (they only return a reference / scalar member).
214+
// -----------------------------------------------------------------
194215

195-
/**
196-
* Method used to get a specified footer defined for the response
197-
* @param key The footer identification
198-
* @return a string representing the value assumed by the footer
199-
**/
200-
const std::string& get_footer(const std::string& key) {
201-
return footers_[key];
202-
}
216+
/// Returns the value of header `key`, or an empty view if absent.
217+
/// Does NOT insert on miss (PRD-RSP-REQ-003).
218+
/// View lifetime: see lifetime contract above.
219+
[[nodiscard]] std::string_view get_header(std::string_view key) const;
203220

204-
const std::string& get_cookie(const std::string& key) {
205-
return cookies_[key];
206-
}
221+
/// Returns the value of footer `key`, or an empty view if absent.
222+
/// Does NOT insert on miss. View lifetime: see lifetime contract.
223+
[[nodiscard]] std::string_view get_footer(std::string_view key) const;
224+
225+
/// Returns the value of cookie `key`, or an empty view if absent.
226+
/// Does NOT insert on miss. View lifetime: see lifetime contract.
227+
[[nodiscard]] std::string_view get_cookie(std::string_view key) const;
207228

208229
/**
209230
* Method used to get all headers passed with the request.
210231
* @return a map<string,string> containing all headers.
211232
**/
212-
const std::map<std::string, std::string, http::header_comparator>& get_headers() const {
233+
[[nodiscard]] const http::header_map& get_headers() const noexcept {
213234
return headers_;
214235
}
215236

216237
/**
217238
* Method used to get all footers passed with the request.
218239
* @return a map<string,string> containing all footers.
219240
**/
220-
const std::map<std::string, std::string, http::header_comparator>& get_footers() const {
241+
[[nodiscard]] const http::header_map& get_footers() const noexcept {
221242
return footers_;
222243
}
223244

224-
const std::map<std::string, std::string, http::header_comparator>& get_cookies() const {
245+
[[nodiscard]] const http::header_map& get_cookies() const noexcept {
225246
return cookies_;
226247
}
227248

228249
/**
229-
* Method used to get the response code from the response
250+
* Method used to get the response status code.
251+
* Spelled `get_status` to match the v2 vocabulary (TASK-011);
252+
* `get_response_code` survives as a compatibility alias while the
253+
* v1 subclass hierarchy still inherits from http_response
254+
* (TASK-013 removes both the subclasses and the alias together
255+
* with the dispatch path in webserver.cpp:1336).
230256
* @return The response code
231257
**/
232-
int get_response_code() const {
258+
[[nodiscard]] int get_status() const noexcept {
259+
return status_code_;
260+
}
261+
262+
// Compatibility shim retained while v1 subclasses still inherit
263+
// (TASK-013 removes them). Internal dispatch (webserver.cpp:1336)
264+
// reaches through a base pointer; that call site flips to
265+
// get_status() when TASK-013 lands.
266+
[[nodiscard]] int get_response_code() const noexcept {
233267
return status_code_;
234268
}
235269

src/httpserver/http_utils.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,12 @@ class http_utils {
312312

313313
class header_comparator {
314314
public:
315+
// is_transparent enables heterogeneous lookup against header_map
316+
// (std::map<std::string, std::string, header_comparator>): callers
317+
// can pass std::string_view directly to find()/count() without
318+
// constructing a std::string. Required by TASK-011's
319+
// string_view-returning const accessors on http_response.
320+
using is_transparent = std::true_type;
315321
/**
316322
* Operator used to compare strings.
317323
* @param first string

test/integ/basic.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2472,9 +2472,14 @@ class response_footer_resource : public http_resource {
24722472
response->with_footer("X-Checksum", "abc123");
24732473
response->with_footer("X-Processing-Time", "42ms");
24742474

2475-
// Test get_footer and get_footers on response
2475+
// Test get_footer and get_footers on response. The returned
2476+
// string_view points into the response's storage; we only
2477+
// read it before returning so the response (and thus the
2478+
// backing string) outlives any read.
24762479
auto checksum = response->get_footer("X-Checksum");
24772480
auto all_footers = response->get_footers();
2481+
(void)checksum;
2482+
(void)all_footers;
24782483

24792484
return response;
24802485
}

0 commit comments

Comments
 (0)