Skip to content

Commit fff3d62

Browse files
etrclaude
andcommitted
TASK-013: review-pass fixes (DRY forbidden-char constant, dispatch null-safety, digest-test contract)
- Reuse anonymous-namespace kForbiddenFieldChars in unauthorized() instead of the duplicate local kForbidden (code-quality-iter1-8, DRY). - Move the operator<< friend declaration out of the now-misleading protected: section in http_response.hpp; http_response is final, so the section was never reachable by subclasses. - Collapse the duplicate catch(const std::exception&)/catch(...) block in finalize_answer to a single catch(...). Both branches did the same work. - Add LT_CHECK_EQ(http_code, 401) to the canonical digest_auth integration test and TODO(v2-digest) markers to the four sibling digest_auth_* variants documenting why they are currently behaviourally redundant under the v2 static-challenge path (test-quality-iter1-10/28). - Migrate examples/service.cpp render_* methods from shared_ptr(new http_response(...)) to make_shared<http_response>(...) (code-quality-iter1-6, idiomatic v2 form). - Add iovec_factory_empty_span and iovec_factory_single_entry edge-case tests, with an expanded comment block on iovec_factory_deep_copies_span spelling out the span deep-copy / caller-buffer lifetime contract (test-quality-iter1-29). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f4a5530 commit fff3d62

6 files changed

Lines changed: 83 additions & 29 deletions

File tree

examples/service.cpp

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ std::shared_ptr<httpserver::http_response> service_resource::render_GET(const ht
5252
std::cout << "service_resource::render_GET()" << std::endl;
5353

5454
if (verbose) std::cout << req;
55-
httpserver::http_response* res = new httpserver::http_response(httpserver::http_response::string("GET response"));
55+
auto res = std::make_shared<httpserver::http_response>(httpserver::http_response::string("GET response"));
5656

5757
if (verbose) std::cout << *res;
5858

59-
return std::shared_ptr<httpserver::http_response>(res);
59+
return res;
6060
}
6161

6262

@@ -65,83 +65,83 @@ std::shared_ptr<httpserver::http_response> service_resource::render_PUT(const ht
6565

6666
if (verbose) std::cout << req;
6767

68-
httpserver::http_response* res = new httpserver::http_response(httpserver::http_response::string("PUT response"));
68+
auto res = std::make_shared<httpserver::http_response>(httpserver::http_response::string("PUT response"));
6969

7070
if (verbose) std::cout << *res;
7171

72-
return std::shared_ptr<httpserver::http_response>(res);
72+
return res;
7373
}
7474

7575
std::shared_ptr<httpserver::http_response> service_resource::render_POST(const httpserver::http_request &req) {
7676
std::cout << "service_resource::render_POST()" << std::endl;
7777

7878
if (verbose) std::cout << req;
7979

80-
httpserver::http_response* res = new httpserver::http_response(httpserver::http_response::string("POST response"));
80+
auto res = std::make_shared<httpserver::http_response>(httpserver::http_response::string("POST response"));
8181

8282
if (verbose) std::cout << *res;
8383

84-
return std::shared_ptr<httpserver::http_response>(res);
84+
return res;
8585
}
8686

8787
std::shared_ptr<httpserver::http_response> service_resource::render(const httpserver::http_request &req) {
8888
std::cout << "service_resource::render()" << std::endl;
8989

9090
if (verbose) std::cout << req;
9191

92-
httpserver::http_response* res = new httpserver::http_response(httpserver::http_response::string("generic response"));
92+
auto res = std::make_shared<httpserver::http_response>(httpserver::http_response::string("generic response"));
9393

9494
if (verbose) std::cout << *res;
9595

96-
return std::shared_ptr<httpserver::http_response>(res);
96+
return res;
9797
}
9898

9999
std::shared_ptr<httpserver::http_response> service_resource::render_HEAD(const httpserver::http_request &req) {
100100
std::cout << "service_resource::render_HEAD()" << std::endl;
101101

102102
if (verbose) std::cout << req;
103103

104-
httpserver::http_response* res = new httpserver::http_response(httpserver::http_response::string("HEAD response"));
104+
auto res = std::make_shared<httpserver::http_response>(httpserver::http_response::string("HEAD response"));
105105

106106
if (verbose) std::cout << *res;
107107

108-
return std::shared_ptr<httpserver::http_response>(res);
108+
return res;
109109
}
110110

111111
std::shared_ptr<httpserver::http_response> service_resource::render_OPTIONS(const httpserver::http_request &req) {
112112
std::cout << "service_resource::render_OPTIONS()" << std::endl;
113113

114114
if (verbose) std::cout << req;
115115

116-
httpserver::http_response* res = new httpserver::http_response(httpserver::http_response::string("OPTIONS response"));
116+
auto res = std::make_shared<httpserver::http_response>(httpserver::http_response::string("OPTIONS response"));
117117

118118
if (verbose) std::cout << *res;
119119

120-
return std::shared_ptr<httpserver::http_response>(res);
120+
return res;
121121
}
122122

123123
std::shared_ptr<httpserver::http_response> service_resource::render_CONNECT(const httpserver::http_request &req) {
124124
std::cout << "service_resource::render_CONNECT()" << std::endl;
125125

126126
if (verbose) std::cout << req;
127127

128-
httpserver::http_response* res = new httpserver::http_response(httpserver::http_response::string("CONNECT response"));
128+
auto res = std::make_shared<httpserver::http_response>(httpserver::http_response::string("CONNECT response"));
129129

130130
if (verbose) std::cout << *res;
131131

132-
return std::shared_ptr<httpserver::http_response>(res);
132+
return res;
133133
}
134134

135135
std::shared_ptr<httpserver::http_response> service_resource::render_DELETE(const httpserver::http_request &req) {
136136
std::cout << "service_resource::render_DELETE()" << std::endl;
137137

138138
if (verbose) std::cout << req;
139139

140-
httpserver::http_response* res = new httpserver::http_response(httpserver::http_response::string("DELETE response"));
140+
auto res = std::make_shared<httpserver::http_response>(httpserver::http_response::string("DELETE response"));
141141

142142
if (verbose) std::cout << *res;
143143

144-
return std::shared_ptr<httpserver::http_response>(res);
144+
return res;
145145
}
146146

147147
void usage() {

src/http_response.cpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ std::string_view http_response::get_cookie(std::string_view key) const {
311311
}
312312

313313
namespace {
314-
static inline http::header_view_map to_view_map(const http::header_map& hdr_map) {
314+
inline http::header_view_map to_view_map(const http::header_map& hdr_map) {
315315
http::header_view_map view_map;
316316
for (const auto& item : hdr_map) {
317317
view_map[std::string_view(item.first)] = std::string_view(item.second);
@@ -447,13 +447,14 @@ http_response http_response::unauthorized(std::string_view scheme,
447447
// caller error — callers must never pass untrusted user input as scheme
448448
// or realm without first validating it. Throw std::invalid_argument so
449449
// the error is visible and cannot be silently swallowed.
450-
static constexpr std::string_view kForbidden("\r\n\0", 3);
451-
if (scheme.find_first_of(kForbidden) != std::string_view::npos) {
450+
// kForbiddenFieldChars is the same constant used by validate_header_field
451+
// above — reused here to avoid a duplicate definition.
452+
if (scheme.find_first_of(kForbiddenFieldChars) != std::string_view::npos) {
452453
throw std::invalid_argument(
453454
"http_response::unauthorized: scheme contains forbidden control "
454455
"character (CR, LF, or NUL)");
455456
}
456-
if (realm.find_first_of(kForbidden) != std::string_view::npos) {
457+
if (realm.find_first_of(kForbiddenFieldChars) != std::string_view::npos) {
457458
throw std::invalid_argument(
458459
"http_response::unauthorized: realm contains forbidden control "
459460
"character (CR, LF, or NUL)");

src/httpserver/http_response.hpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,11 @@ class http_response final {
367367
template <typename T, typename... Args>
368368
void emplace_body(body_kind k, Args&&... args);
369369

370-
protected:
370+
// Friend declarations belong in private: — friendship is unaffected
371+
// by access specifiers, but placing them here (rather than in a
372+
// misleading protected: section) signals clearly that these names can
373+
// bypass encapsulation; http_response is final so there are no
374+
// subclasses to inherit any protected access anyway.
371375
friend std::ostream &operator<< (std::ostream &os, const http_response &r);
372376

373377
// The TASK-009 SBO unit test exercises the four-case move

src/webserver.cpp

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,7 +1389,7 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct detail:
13891389
mr->dhrs->with_header(http_utils::http_header_allow, header_value);
13901390
}
13911391
}
1392-
} catch(const std::exception& e) {
1392+
} catch(...) {
13931393
// The user-supplied internal_error_resource may itself throw;
13941394
// fall back to the built-in error page in that case (force_our=true)
13951395
// so we never let exceptions escape into libmicrohttpd.
@@ -1398,12 +1398,6 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct detail:
13981398
} catch(...) {
13991399
mr->dhrs = internal_error_page(mr, true);
14001400
}
1401-
} catch(...) {
1402-
try {
1403-
mr->dhrs = internal_error_page(mr);
1404-
} catch(...) {
1405-
mr->dhrs = internal_error_page(mr, true);
1406-
}
14071401
}
14081402
} else if (mr->dhrs == nullptr) {
14091403
mr->dhrs = not_found_page(mr);

test/integ/authentication.cpp

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth)
240240
std::string s;
241241
CURL *curl = curl_easy_init();
242242
CURLcode res;
243+
long http_code = 0; // NOLINT(runtime/int)
243244
curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
244245
#if defined(_WINDOWS)
245246
curl_easy_setopt(curl, CURLOPT_USERPWD, "examplerealm/myuser:mypass");
@@ -256,13 +257,23 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth)
256257
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
257258
res = curl_easy_perform(curl);
258259
LT_ASSERT_EQ(res, 0);
259-
// v2 limitation: digest handshake does not complete — body remains FAIL.
260+
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
261+
// v2 contract: the server issues a static 401 Digest challenge; the
262+
// handshake cannot complete (no nonce/opaque state machine), so the
263+
// body remains FAIL and the status must be 401.
264+
LT_CHECK_EQ(http_code, 401);
260265
LT_CHECK_EQ(s, "FAIL");
261266
curl_easy_cleanup(curl);
262267

263268
ws.stop();
264269
LT_END_AUTO_TEST(digest_auth)
265270

271+
// TODO(v2-digest): digest_auth_wrong_pass is indistinguishable from digest_auth
272+
// under v2 because the handshake never completes regardless of credentials.
273+
// Wrong-pass vs. correct-pass both reach the same static 401 challenge path.
274+
// This test becomes meaningful again when full v2 digest support is added
275+
// (MHD nonce/opaque state machine). Until then it exercises a different
276+
// digest_resource instance only.
266277
LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_wrong_pass)
267278
webserver ws = create_webserver(PORT)
268279
.digest_auth_random("myrandom")
@@ -303,6 +314,10 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_wrong_pass)
303314
ws.stop();
304315
LT_END_AUTO_TEST(digest_auth_wrong_pass)
305316

317+
// TODO(v2-digest): digest_auth_with_ha1_md5 is indistinguishable from
318+
// digest_auth under v2 — check_digest_auth_digest() is never reached because
319+
// get_digested_user() always returns empty string (no nonce roundtrip).
320+
// This test becomes meaningful when full v2 digest support is added.
306321
LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_md5)
307322
webserver ws = create_webserver(PORT)
308323
.digest_auth_random("myrandom")
@@ -345,6 +360,9 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_md5)
345360
ws.stop();
346361
LT_END_AUTO_TEST(digest_auth_with_ha1_md5)
347362

363+
// TODO(v2-digest): digest_auth_with_ha1_md5_wrong_pass is indistinguishable
364+
// from digest_auth_with_ha1_md5 under v2 — wrong-pass vs. correct-pass both
365+
// yield the same static 401 challenge. Becomes meaningful with full v2 digest.
348366
LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_md5_wrong_pass)
349367
webserver ws = create_webserver(PORT)
350368
.digest_auth_random("myrandom")
@@ -385,6 +403,9 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_md5_wrong_pass)
385403
ws.stop();
386404
LT_END_AUTO_TEST(digest_auth_with_ha1_md5_wrong_pass)
387405

406+
// TODO(v2-digest): digest_auth_with_ha1_sha256 is indistinguishable from
407+
// digest_auth under v2 — SHA-256 vs. MD5 path is unreachable (no nonce
408+
// roundtrip). Becomes meaningful when full v2 digest support is added.
388409
LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_sha256)
389410
webserver ws = create_webserver(PORT)
390411
.digest_auth_random("myrandom")
@@ -427,6 +448,10 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_sha256)
427448
ws.stop();
428449
LT_END_AUTO_TEST(digest_auth_with_ha1_sha256)
429450

451+
// TODO(v2-digest): digest_auth_with_ha1_sha256_wrong_pass is
452+
// indistinguishable from digest_auth_with_ha1_sha256 under v2 — wrong-pass
453+
// vs. correct-pass both yield the same static 401 challenge. Becomes
454+
// meaningful when full v2 digest support is added.
430455
LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_sha256_wrong_pass)
431456
webserver ws = create_webserver(PORT)
432457
.digest_auth_random("myrandom")

test/unit/http_response_factories_test.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,16 @@ LT_BEGIN_AUTO_TEST(http_response_factories_suite,
185185
// Build a span over a temporary array; let the array go out of
186186
// scope before we observe r. The factory's deep-copy must keep the
187187
// body's iovec_entry vector valid.
188+
//
189+
// Span deep-copy / caller-buffer lifetime contract:
190+
// http_response::iovec() copies the iovec_entry structs (base+len
191+
// pairs) into an internal std::vector. The *entries* are owned by
192+
// the response, but the *buffers they point to* are NOT copied —
193+
// callers must keep their payload buffers alive until the response
194+
// is dispatched (i.e. until the MHD send callback completes).
195+
// http_response itself is move-only, so copy-prohibition does not
196+
// apply; the invariant to test is that the internal vector survives
197+
// after the caller's span goes out of scope.
188198
auto r = []() {
189199
std::array<httpserver::iovec_entry, 1> entries{{ {"x", 1} }};
190200
return http_response::iovec(entries);
@@ -193,6 +203,26 @@ LT_BEGIN_AUTO_TEST(http_response_factories_suite,
193203
static_cast<int>(body_kind::iovec));
194204
LT_END_AUTO_TEST(iovec_factory_deep_copies_span)
195205

206+
LT_BEGIN_AUTO_TEST(http_response_factories_suite, iovec_factory_empty_span)
207+
// An iovec with zero entries must not crash, must have kind iovec,
208+
// and the default status (200) must be preserved.
209+
std::array<httpserver::iovec_entry, 0> entries{};
210+
auto r = http_response::iovec(std::span<const httpserver::iovec_entry>(entries));
211+
LT_CHECK_EQ(static_cast<int>(r.kind()),
212+
static_cast<int>(body_kind::iovec));
213+
LT_CHECK_EQ(r.get_status(), 200);
214+
LT_END_AUTO_TEST(iovec_factory_empty_span)
215+
216+
LT_BEGIN_AUTO_TEST(http_response_factories_suite, iovec_factory_single_entry)
217+
// A single-entry iovec must produce kind iovec and the default status.
218+
static const char buf[] = "hello";
219+
std::array<httpserver::iovec_entry, 1> entries{{ {buf, 5} }};
220+
auto r = http_response::iovec(entries);
221+
LT_CHECK_EQ(static_cast<int>(r.kind()),
222+
static_cast<int>(body_kind::iovec));
223+
LT_CHECK_EQ(r.get_status(), 200);
224+
LT_END_AUTO_TEST(iovec_factory_single_entry)
225+
196226
// -----------------------------------------------------------------------
197227
// pipe() — owns the fd, destructor closes it when not materialized.
198228
// Gated on !_WIN32 because Windows uses _pipe()/CreatePipe() rather

0 commit comments

Comments
 (0)