Skip to content

Commit 8fa2c6b

Browse files
etrclaude
andcommitted
TASK-010: http_response static factory functions
Adds the seven canonical factories on http_response (string, file, iovec, pipe, empty, deferred, unauthorized) plus a public kind() accessor. Each factory placement-news the corresponding detail::body subclass into the SBO buffer through a single private emplace_body<T> helper, so the matched ::operator new(sizeof(T)) / ::operator delete pairing the destructor relies on (TASK-009 OQ-4) lives in exactly one place — a stray plain `new T(...)` in any factory would mismatch and trip ASan. Status-code defaults match v1: 200 for content-bearing bodies, 204 for empty(), 401 for unauthorized(). The unauthorized() factory replaces v1's basic_auth_fail_response and digest_auth_fail_response with a single scheme-parameterised entry; the digest path emits a static WWW-Authenticate challenge and does NOT participate in libmicrohttpd's nonce/opaque state machine (documented contract gap). Tests: 17 LT_BEGIN_AUTO_TESTs in test/unit/http_response_factories_test.cpp exercise kind(), default and overridden Content-Type, file-missing non-throw semantics, iovec span deep-copy, pipe fd ownership transfer (destructor closes), deferred capture lifetime, and the AC-mandated byte-for-byte WWW-Authenticate header. All 27 testsuite entries pass locally with -j1; cpplint clean on the three changed files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 798fa38 commit 8fa2c6b

4 files changed

Lines changed: 578 additions & 1 deletion

File tree

src/http_response.cpp

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,25 @@
2121
#include "httpserver/http_response.hpp"
2222

2323
#include <microhttpd.h>
24+
#include <sys/types.h> // ssize_t (for the deferred() producer)
2425

26+
#include <cassert>
2527
#include <cstddef>
28+
#include <cstdint>
29+
#include <functional>
2630
#include <iostream>
2731
#include <map>
2832
#include <new>
33+
#include <span>
2934
#include <string>
35+
#include <string_view>
3036
#include <type_traits>
3137
#include <utility>
38+
#include <vector>
3239

3340
#include "httpserver/detail/body.hpp" // complete type for body_->~body()
3441
#include "httpserver/http_utils.hpp"
42+
#include "httpserver/iovec_entry.hpp"
3543

3644
namespace httpserver {
3745

@@ -200,4 +208,132 @@ std::ostream &operator<< (std::ostream& os, const http_response& r) {
200208
return os;
201209
}
202210

211+
// -----------------------------------------------------------------------
212+
// emplace_body — single placement-new entry point shared by all
213+
// factories (TASK-010). Centralising the SBO-vs-heap decision here means
214+
// the matched ::operator new(sizeof(T)) / ::operator delete pairing the
215+
// destructor relies on (TASK-009 OQ-4) lives in exactly one place; a
216+
// stray plain `new T(...)` in any factory would mismatch the
217+
// destructor's ::operator delete and trip ASan immediately.
218+
//
219+
// Defined out-of-line in this TU because every factory in this file
220+
// instantiates it (so no separate-TU instantiation is needed) and the
221+
// template body needs the complete type detail::body. Per-T size+align
222+
// guards duplicate the SBO budget asserts in detail/body.hpp so an
223+
// over-sized future body subclass fails to compile at the factory site
224+
// rather than silently triggering the heap fallback.
225+
// -----------------------------------------------------------------------
226+
template <typename T, typename... Args>
227+
void http_response::emplace_body(body_kind k, Args&&... args) {
228+
static_assert(std::is_base_of_v<detail::body, T>,
229+
"emplace_body: T must derive from detail::body");
230+
assert(body_ == nullptr &&
231+
"emplace_body: body slot already populated");
232+
if constexpr (sizeof(T) <= body_buf_size && alignof(T) <= 16) {
233+
// SBO inline path.
234+
body_ = ::new (body_storage_) T(std::forward<Args>(args)...);
235+
body_inline_ = true;
236+
} else {
237+
// Heap fallback. ::operator new(sizeof(T)) is paired exactly
238+
// with the destructor's ::operator delete(body_); a plain
239+
// `new T(...)` here would mismatch.
240+
void* mem = ::operator new(sizeof(T));
241+
try {
242+
body_ = ::new (mem) T(std::forward<Args>(args)...);
243+
} catch (...) {
244+
::operator delete(mem);
245+
throw;
246+
}
247+
body_inline_ = false;
248+
}
249+
kind_ = k;
250+
}
251+
252+
// -----------------------------------------------------------------------
253+
// Static factories (TASK-010). Each factory:
254+
// 1. constructs a default http_response (status_code_ = -1, no body),
255+
// 2. sets the status code and any per-kind headers,
256+
// 3. emplaces the appropriate detail::body subclass via emplace_body.
257+
//
258+
// The status-code defaults match v1: 200 for content-bearing bodies,
259+
// 204 for empty(), 401 for unauthorized().
260+
// -----------------------------------------------------------------------
261+
262+
http_response http_response::empty() {
263+
http_response r;
264+
r.status_code_ = http::http_utils::http_no_content; // 204
265+
r.emplace_body<detail::empty_body>(body_kind::empty);
266+
return r;
267+
}
268+
269+
http_response http_response::string(std::string body,
270+
std::string content_type) {
271+
http_response r;
272+
r.status_code_ = http::http_utils::http_ok; // 200
273+
r.with_header(http::http_utils::http_header_content_type,
274+
std::move(content_type));
275+
r.emplace_body<detail::string_body>(body_kind::string,
276+
std::move(body));
277+
return r;
278+
}
279+
280+
http_response http_response::file(std::string path) {
281+
http_response r;
282+
r.status_code_ = http::http_utils::http_ok;
283+
r.emplace_body<detail::file_body>(body_kind::file, std::move(path));
284+
return r;
285+
}
286+
287+
http_response http_response::iovec(std::span<const iovec_entry> entries) {
288+
// Deep-copy into the body's owned vector so the caller's span need
289+
// not outlive the response. The buffers each entry's `base` points
290+
// at remain BORROWED — see detail::iovec_body's lifetime contract.
291+
std::vector<iovec_entry> v(entries.begin(), entries.end());
292+
http_response r;
293+
r.status_code_ = http::http_utils::http_ok;
294+
r.emplace_body<detail::iovec_body>(body_kind::iovec, std::move(v));
295+
return r;
296+
}
297+
298+
http_response http_response::pipe(int fd, std::size_t size_hint) {
299+
(void)size_hint; // reserved for future use
300+
http_response r;
301+
r.status_code_ = http::http_utils::http_ok;
302+
r.emplace_body<detail::pipe_body>(body_kind::pipe, fd);
303+
return r;
304+
}
305+
306+
http_response http_response::deferred(
307+
std::function<ssize_t(std::uint64_t, char*, std::size_t)> producer) {
308+
http_response r;
309+
r.status_code_ = http::http_utils::http_ok;
310+
r.emplace_body<detail::deferred_body>(body_kind::deferred,
311+
std::move(producer));
312+
return r;
313+
}
314+
315+
http_response http_response::unauthorized(std::string_view scheme,
316+
std::string_view realm,
317+
std::string body) {
318+
http_response r;
319+
r.status_code_ = http::http_utils::http_unauthorized; // 401
320+
// Build `<scheme> realm="<realm>"`. AC #3 requires byte-for-byte
321+
// `Basic realm="myrealm"` for the canonical case.
322+
std::string challenge;
323+
challenge.reserve(scheme.size() + realm.size() + 10);
324+
challenge.append(scheme.data(), scheme.size());
325+
challenge.append(" realm=\"", 8);
326+
challenge.append(realm.data(), realm.size());
327+
challenge.push_back('"');
328+
r.with_header(http::http_utils::http_header_www_authenticate,
329+
challenge);
330+
// The body slot literally holds a string_body (possibly empty), so
331+
// kind() reports body_kind::string. Switching to body_kind::empty
332+
// for the empty-body case would fork the construction path and
333+
// break the invariant that kind() reflects the placed-new body.
334+
r.emplace_body<detail::string_body>(body_kind::string,
335+
std::move(body));
336+
return r;
337+
}
338+
203339
} // namespace httpserver

src/httpserver/http_response.hpp

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,20 @@
2525
#ifndef SRC_HTTPSERVER_HTTP_RESPONSE_HPP_
2626
#define SRC_HTTPSERVER_HTTP_RESPONSE_HPP_
2727

28+
#include <sys/types.h> // ssize_t — for the deferred() producer
29+
2830
#include <cstddef>
31+
#include <cstdint>
32+
#include <functional>
2933
#include <iosfwd>
3034
#include <map>
35+
#include <span>
3136
#include <string>
37+
#include <string_view>
3238
#include "httpserver/body_kind.hpp"
3339
#include "httpserver/http_arg_value.hpp"
3440
#include "httpserver/http_utils.hpp"
41+
#include "httpserver/iovec_entry.hpp"
3542

3643
struct MHD_Connection;
3744
struct MHD_Response;
@@ -94,6 +101,88 @@ class http_response {
94101
// the complete type.
95102
virtual ~http_response();
96103

104+
// Body-kind discriminator (TASK-010 AC). Mirrors the kind reported
105+
// by the underlying detail::body, but answered without a virtual
106+
// call: the kind is recorded into kind_ at factory time and
107+
// preserved across moves. TASK-011's dispatch path will consume
108+
// this for its kind-specific fast paths.
109+
[[nodiscard]] body_kind kind() const noexcept { return kind_; }
110+
111+
// -----------------------------------------------------------------
112+
// Static factories (TASK-010, DR-005).
113+
//
114+
// Each factory placement-news the corresponding detail::body
115+
// subclass into the response's SBO buffer (or, if the body ever
116+
// exceeds 64 bytes, onto the heap via ::operator new(sizeof(T))
117+
// so the destructor's matched ::operator delete pairs cleanly).
118+
// Replaces the v1 polymorphic *_response subclasses.
119+
//
120+
// Status-code defaults match v1: 200 for content-bearing bodies,
121+
// 204 for empty(), 401 for unauthorized().
122+
// -----------------------------------------------------------------
123+
124+
// Construct a response carrying a string body. The Content-Type
125+
// header defaults to "text/plain"; pass a different value (for
126+
// example "application/json") to override. The body string is
127+
// stored by move so callers retain no aliasing.
128+
[[nodiscard]] static http_response string(
129+
std::string body,
130+
std::string content_type = "text/plain");
131+
132+
// Construct a response that streams a file from disk. Does NOT
133+
// throw on a missing or unreadable path — failure is observable at
134+
// dispatch time (the materialized MHD_Response is null and the
135+
// dispatch path renders a 500). Mirrors v1 file_response semantics.
136+
[[nodiscard]] static http_response file(std::string path);
137+
138+
// Construct a response from a span of scatter/gather buffers. The
139+
// entries array is deep-copied into the body so the span need not
140+
// outlive the response, but the buffers each entry's `base` points
141+
// at remain BORROWED — they must outlive the response (and the
142+
// MHD_Response that response materializes).
143+
[[nodiscard]] static http_response iovec(
144+
std::span<const iovec_entry> entries);
145+
146+
// Construct a response that streams from a pipe read-end. The
147+
// factory takes ownership of `fd` immediately. The fd is closed
148+
// when the materialized MHD_Response is destroyed; if the response
149+
// is never materialized, the http_response's destructor closes
150+
// it. Callers MUST NOT close `fd` after handing it off.
151+
// `size_hint` is reserved for forward compatibility — currently
152+
// ignored, may be used to advise libmicrohttpd of payload size in
153+
// a future revision.
154+
[[nodiscard]] static http_response pipe(int fd,
155+
std::size_t size_hint = 0);
156+
157+
// Construct an empty (no-payload) response. Defaults to 204
158+
// No Content, matching v1 empty_response.
159+
[[nodiscard]] static http_response empty();
160+
161+
// Construct a response that streams from a producer callback.
162+
// libmicrohttpd invokes `producer(pos, buf, max)` whenever it
163+
// needs more bytes; the producer should return the number of
164+
// bytes written, MHD_CONTENT_READER_END_OF_STREAM, or
165+
// MHD_CONTENT_READER_END_WITH_ERROR. The producer is stored by
166+
// move; large captures may force std::function to heap-allocate
167+
// internally (independent of http_response's own SBO).
168+
[[nodiscard]] static http_response deferred(
169+
std::function<ssize_t(std::uint64_t, char*, std::size_t)> producer);
170+
171+
// Construct a 401 Unauthorized response with a WWW-Authenticate
172+
// header of the form `<scheme> realm="<realm>"`. Replaces v1's
173+
// basic_auth_fail_response and digest_auth_fail_response.
174+
//
175+
// Note: for "Digest" the response carries a static
176+
// WWW-Authenticate challenge but does NOT participate in
177+
// libmicrohttpd's nonce/opaque digest-auth state machine — that
178+
// was v1's MHD_queue_auth_required_response3-driven path which
179+
// requires connection-time state. Callers needing full digest
180+
// auth should reach for the dedicated MHD APIs directly.
181+
[[nodiscard]] static http_response unauthorized(
182+
std::string_view scheme,
183+
std::string_view realm,
184+
std::string body = {});
185+
97186
/**
98187
* Method used to get a specified header defined for the response
99188
* @param key The header identification
@@ -187,6 +276,19 @@ class http_response {
187276
void destroy_body() noexcept;
188277
void adopt_body_from(http_response& o) noexcept;
189278

279+
// Placement-new a concrete detail::body subclass into the SBO
280+
// buffer (or, if T does not fit, onto the heap via the matched
281+
// ::operator new(sizeof(T))/::operator delete pairing the
282+
// destructor relies on). Defined out-of-line in http_response.cpp
283+
// because it requires the complete type detail::body — it is only
284+
// instantiated from the factory bodies in that TU.
285+
//
286+
// Pre-condition: the response's body slot is empty
287+
// (default-constructed). Factories construct on a fresh
288+
// http_response, so this always holds; an assertion guards it.
289+
template <typename T, typename... Args>
290+
void emplace_body(body_kind k, Args&&... args);
291+
190292
protected:
191293
friend std::ostream &operator<< (std::ostream &os, const http_response &r);
192294

test/Makefile.am

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ LDADD += -lcurl
2626

2727
AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ -DHTTPSERVER_COMPILATION
2828
METASOURCES = AUTO
29-
check_PROGRAMS = basic file_upload http_utils threaded nodelay string_utilities http_endpoint ban_system ws_start_stop authentication deferred http_resource http_response create_webserver new_response_types daemon_info uri_log feature_unavailable header_hygiene_iovec header_hygiene iovec_entry iovec_response http_method constants body http_response_sbo
29+
check_PROGRAMS = basic file_upload http_utils threaded nodelay string_utilities http_endpoint ban_system ws_start_stop authentication deferred http_resource http_response create_webserver new_response_types daemon_info uri_log feature_unavailable header_hygiene_iovec header_hygiene iovec_entry iovec_response http_method constants body http_response_sbo http_response_factories
3030

3131
MOSTLYCLEANFILES = *.gcda *.gcno *.gcov
3232

@@ -85,6 +85,16 @@ body_LDADD = $(LDADD) -lmicrohttpd
8585
http_response_sbo_SOURCES = unit/http_response_sbo_test.cpp
8686
http_response_sbo_LDADD = $(LDADD) -lmicrohttpd
8787

88+
# http_response_factories: TASK-010 unit test for the static factory
89+
# functions on http_response (string/file/iovec/pipe/empty/deferred/
90+
# unauthorized). Each factory placement-news the corresponding
91+
# detail::body subclass into the SBO buffer, so this TU includes the
92+
# private detail/body.hpp via the build-tree -DHTTPSERVER_COMPILATION
93+
# path. Needs -lmicrohttpd for the same transitive reasons as
94+
# http_response_sbo.
95+
http_response_factories_SOURCES = unit/http_response_factories_test.cpp
96+
http_response_factories_LDADD = $(LDADD) -lmicrohttpd
97+
8898
noinst_HEADERS = littletest.hpp
8999
AM_CXXFLAGS += -Wall -fPIC -Wno-overloaded-virtual
90100

0 commit comments

Comments
 (0)