@@ -189,6 +189,119 @@ void http_response::shoutCAST() {
189189 status_code_ |= http::http_utils::shoutcast_response;
190190}
191191
192+ // -----------------------------------------------------------------------
193+ // Fluent with_* setters (TASK-012, PRD-RSP-REQ-004).
194+ //
195+ // Each setter has two ref-qualified overloads that delegate to a private
196+ // do_set_*() helper containing the validation + mutation logic. The
197+ // overloads differ only in their return statement: `& overload` returns
198+ // *this by lvalue reference; `&& overload` returns std::move(*this).
199+ // Centralising the mutation in a single helper means validation and
200+ // insert_or_assign are in exactly one place per setter, not duplicated
201+ // across every overload pair.
202+ //
203+ // Validation (security, TASK-012 review-pass):
204+ // * with_header / with_footer: reject key or value containing CR,
205+ // LF, or NUL — these characters can split an HTTP response and
206+ // inject additional headers (CWE-113).
207+ // * with_cookie: same CRLF/NUL rejection on name and value.
208+ // * with_status: code must be in [100, 599] per RFC 9110 §15.
209+ //
210+ // insert_or_assign — rather than `m[k] = v` — is used so the by-value
211+ // `std::string` parameters can be moved into the map slot directly.
212+ // -----------------------------------------------------------------------
213+
214+ // Shared forbidden-character set for header/footer/cookie field names
215+ // and values. The string_view spans all three bytes including the
216+ // embedded NUL.
217+ namespace {
218+ constexpr std::string_view kForbiddenFieldChars (" \r\n\0 " , 3 );
219+
220+ void validate_header_field (std::string_view context,
221+ std::string_view key,
222+ std::string_view value) {
223+ if (key.find_first_of (kForbiddenFieldChars ) != std::string_view::npos) {
224+ throw std::invalid_argument (
225+ std::string (context) +
226+ " : key contains forbidden control character (CR, LF, or NUL)" );
227+ }
228+ if (value.find_first_of (kForbiddenFieldChars ) != std::string_view::npos) {
229+ throw std::invalid_argument (
230+ std::string (context) +
231+ " : value contains forbidden control character (CR, LF, or NUL)" );
232+ }
233+ }
234+ } // namespace
235+
236+ void http_response::do_set_header (std::string key, std::string value) {
237+ validate_header_field (" with_header" , key, value);
238+ headers_.insert_or_assign (std::move (key), std::move (value));
239+ }
240+
241+ void http_response::do_set_footer (std::string key, std::string value) {
242+ validate_header_field (" with_footer" , key, value);
243+ footers_.insert_or_assign (std::move (key), std::move (value));
244+ }
245+
246+ void http_response::do_set_cookie (std::string key, std::string value) {
247+ validate_header_field (" with_cookie" , key, value);
248+ cookies_.insert_or_assign (std::move (key), std::move (value));
249+ }
250+
251+ void http_response::do_set_status (int code) {
252+ if (code < 100 || code > 599 ) {
253+ throw std::invalid_argument (
254+ " with_status: HTTP status code out of range [100, 599]" );
255+ }
256+ status_code_ = code;
257+ }
258+
259+ http_response& http_response::with_header (std::string key,
260+ std::string value) & {
261+ do_set_header (std::move (key), std::move (value));
262+ return *this ;
263+ }
264+
265+ http_response&& http_response::with_header(std::string key,
266+ std::string value) && {
267+ do_set_header (std::move (key), std::move (value));
268+ return std::move (*this );
269+ }
270+
271+ http_response& http_response::with_footer (std::string key,
272+ std::string value) & {
273+ do_set_footer (std::move (key), std::move (value));
274+ return *this ;
275+ }
276+
277+ http_response&& http_response::with_footer(std::string key,
278+ std::string value) && {
279+ do_set_footer (std::move (key), std::move (value));
280+ return std::move (*this );
281+ }
282+
283+ http_response& http_response::with_cookie (std::string key,
284+ std::string value) & {
285+ do_set_cookie (std::move (key), std::move (value));
286+ return *this ;
287+ }
288+
289+ http_response&& http_response::with_cookie(std::string key,
290+ std::string value) && {
291+ do_set_cookie (std::move (key), std::move (value));
292+ return std::move (*this );
293+ }
294+
295+ http_response& http_response::with_status (int code) & {
296+ do_set_status (code);
297+ return *this ;
298+ }
299+
300+ http_response&& http_response::with_status(int code) && {
301+ do_set_status (code);
302+ return std::move (*this );
303+ }
304+
192305// -----------------------------------------------------------------------
193306// Const single-key accessors (TASK-011).
194307//
0 commit comments