Skip to content

Commit 86195d6

Browse files
etrclaude
andcommitted
TASK-062: RFC-7616-compliant Digest auth response factory
Adds `http_response::unauthorized(digest_challenge)` factory and a dispatch-time switch on `body_kind::digest_challenge` that routes through libmicrohttpd's `MHD_queue_auth_required_response3`, so the authoritative `WWW-Authenticate: Digest ...` challenge with nonce/opaque/algorithm/qop is written into the wire response. The legacy `unauthorized("Digest", realm, body)` overload remains source-compatible; its Doxygen now points new code at the new overload. Key pieces: * `digest_challenge` struct (`src/httpserver/http_response.hpp`, HAVE_DAUTH-gated) — public RFC-7616 challenge-parameter container. * `body_kind::digest_challenge` enumerator + `detail::digest_challenge_body` subclass (`src/httpserver/body_kind.hpp`, `src/httpserver/detail/body.hpp`, `src/detail/body.cpp`) — heap-allocated params inside a unique_ptr to keep the body's inline footprint under the 64-byte SBO budget. * `webserver_impl::queue_response_dispatching_kind` (`src/detail/webserver_request.cpp`) — branches on `body_kind::digest_challenge` and calls `MHD_queue_auth_required_response3` with the user-supplied params; every other body kind goes through the standard `MHD_queue_response` path. * Per-`webserver_impl` opaque (`digest_opaque_`) seeded once at construction from `std::random_device`, substituted when the factory leaves the opaque field empty (RFC 7616 §5.10: opaque is an identifier, not a secret). * Validation: realm/opaque/domain/body fields are rejected with `std::invalid_argument` on CR/LF/NUL (CWE-113 header injection). DR-013 ("Digest auth simplified to static WWW-Authenticate challenge") is marked Superseded by TASK-062 — the value-type/DR-005 constraint is preserved by doing the kind-switch at dispatch time (the response itself stays a movable value); only the dispatch path knows about MHD_Connection. http-response.md updated to drop the "non-RFC-compliant" sentence and point at the new overload. Tests: * New unit test `http_response_digest_factory_test.cpp` (12 tests): status/kind/SBO/algorithm/opaque/domain round-trip + CR/LF/NUL injection guard on each text field. * New integ test `digest_challenge_format_test.cpp`: spins up a webserver wired to digest_auth_random + nonce_nc_size, hits with plain curl (no --digest), asserts the WWW-Authenticate header carries every RFC 7616 §3.3-mandated token. * `digest_resource` in `test/integ/authentication.cpp` rewritten to emit the new `digest_challenge` body; `digest_auth` test flipped from expecting 401/FAIL to 200/SUCCESS (AC1). Acceptance criteria: 1. curl --digest negotiates: digest_auth test passes 200/SUCCESS. 2. New integ test pins WWW-Authenticate format against RFC 7616 §3.3. 3. Six placeholder integ tests now drive real nonce/opaque handshake end-to-end (wrong-password arms still resolve to 401/FAIL but through MHD_digest_auth_check3 validation). 4. libmicrohttpd's MD5/SHA-256 helpers remain the underlying primitive (we delegate to MHD's nonce HMAC machinery; no crypto here). 5. Typecheck and lint gates pass (check-complexity, check-file-size, check-warning-suppressions, check-duplication, check-hygiene). 6. Tests pass (digest_challenge_format and http_response_digest_factory in isolation; the pre-existing `basic` cascade flake on feature/v2.0 is unchanged). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent e448c35 commit 86195d6

17 files changed

Lines changed: 942 additions & 38 deletions

specs/architecture/04-components/http-response.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ The body subclasses (`detail::string_body`, `file_body`, `iovec_body`, `pipe_bod
1919

2020
**Interfaces:**
2121
- Exposes (from PRD §3.5):
22-
- Factories: `http_response::string(...)`, `::file(...)`, `::iovec(std::span<const httpserver::iovec_entry>)`, `::pipe(...)`, `::empty(...)`, `::deferred(...)`, `::unauthorized(scheme, realm, ...)` — all return `http_response` by value. **For the `"Digest"` scheme**, `unauthorized()` produces only a static `WWW-Authenticate: Digest realm="<realm>"` challenge; it does NOT set `nonce`, `opaque`, `algorithm`, or `qop`, which RFC 7616 §3.3 requires. The response provides no replay protection and will be rejected by strict RFC 7616 parsers. This is a known limitation of the value-typed response model (DR-013). Callers needing fully RFC-compliant Digest auth must call MHD APIs directly; use `"Basic"` for a compliant challenge.
22+
- Factories: `http_response::string(...)`, `::file(...)`, `::iovec(std::span<const httpserver::iovec_entry>)`, `::pipe(...)`, `::empty(...)`, `::deferred(...)`, `::unauthorized(scheme, realm, ...)`, `::unauthorized(digest_challenge)` — all return `http_response` by value. The `unauthorized(digest_challenge)` overload (TASK-062, declared behind `HAVE_DAUTH`) produces a fully RFC 7616 §3.3-compliant `WWW-Authenticate: Digest ` challenge with `nonce`, `opaque`, `algorithm`, and `qop` parameters; the dispatch path detects `body_kind::digest_challenge` and routes through libmicrohttpd's `MHD_queue_auth_required_response3`, which drives the per-connection nonce state machine. The legacy `unauthorized("Digest", realm, body)` string overload remains for source compatibility and emits only `WWW-Authenticate: Digest realm="<realm>"`; for new code prefer the `digest_challenge` overload. See DR-013 (Superseded by TASK-062) for the supersession note.
2323
- **`httpserver::iovec_entry`** is a library-defined POD declared in `<httpserver/iovec_entry.hpp>` (a dedicated public header installed alongside `http_response.hpp` and included by it): `struct iovec_entry { const void* base; std::size_t len; };`. It mirrors POSIX `struct iovec` exactly in layout but does not require `<sys/uio.h>` in any installed header. The internal dispatch path uses the user-supplied span to build a `struct iovec` array inside `iovec_body`. The implementation file `src/detail/body.cpp` carries `static_assert`s pinning the layout assumption: `static_assert(sizeof(iovec_entry) == sizeof(struct iovec))`, `static_assert(offsetof(iovec_entry, base) == offsetof(struct iovec, iov_base))`, `static_assert(offsetof(iovec_entry, len) == offsetof(struct iovec, iov_len))`. When the asserts hold, conversion is a `reinterpret_cast`; when they fail (a hypothetical platform with divergent layout), the build fails loudly at compile time and we fall back to memcpy. This keeps the public header free of system headers and makes the API uniformly available on platforms where `<sys/uio.h>` is not standard (e.g., MSVC builds).
2424
- Fluent setters: `with_header`, `with_footer`, `with_cookie`, `with_status` — each has two ref-qualified overloads: `& → http_response&` (mutate-in-place on an lvalue) and `&& → http_response&&` (return the object by rvalue-reference for zero-copy rvalue factory chains, e.g. `http_response::string("body").with_header("X-Foo", "bar").with_status(201)`).
2525
- `const` accessors: `get_header`, `get_footer`, `get_cookie` returning `string_view` (empty on miss; do not insert).

specs/architecture/11-decisions/DR-013.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
### DR-013: Digest auth simplified to static WWW-Authenticate challenge
22

3-
**Status:** Accepted
3+
**Status:** Superseded by TASK-062 (2026-06-04)
44
**Date:** 2026-05-04
55
**Context:** v1's `basic_auth_fail_response` and `digest_auth_fail_response` each produced their 401 challenges via dedicated response subclasses; `digest_auth_fail_response` in particular delegated to `MHD_queue_auth_required_response3`, which sets the full RFC 7616 nonce, opaque, algorithm, and qop fields and wires the challenge into libmicrohttpd's per-connection Digest state machine. v2.0 removes all `*_response` subclasses and the `get_raw_response` / `decorate_response` / `enqueue_response` virtuals in favour of the unified `http_response::unauthorized(scheme, realm, body)` factory. The factory returns an `http_response` by value with no MHD connection handle available, so it cannot call `MHD_queue_auth_required_response3` — that API requires a live connection pointer, which is incompatible with the value-typed response model.
66

@@ -22,4 +22,21 @@
2222

2323
**Related:** PRD-RSP-REQ-005, TASK-013 §2/§10, http-response.md §4.3.
2424

25+
**Supersession note (TASK-062, 2026-06-04):**
26+
The "value-type response model cannot reach `MHD_queue_auth_required_response3`"
27+
constraint was lifted by adding a kind-dispatched queueing helper in
28+
`webserver_impl`. The dispatch path now branches on
29+
`body_kind::digest_challenge` and routes through
30+
`MHD_queue_auth_required_response3` instead of `MHD_queue_response`, while the
31+
response value itself remains a movable value-typed `http_response` (DR-005
32+
preserved). The new factory `http_response::unauthorized(digest_challenge)`
33+
(declared in `src/httpserver/http_response.hpp` behind `HAVE_DAUTH`) emits a
34+
fully RFC-7616 §3.3-compliant challenge with libmicrohttpd-owned nonce,
35+
server-owned opaque, and user-selectable algorithm (MD5/SHA-256/SHA-512-256)
36+
and qop (auth) parameters. The legacy `unauthorized("Digest", realm, body)`
37+
overload remains for source-compatibility but now its Doxygen points callers
38+
at the new overload as the RFC-compliant path. See TASK-062 plan §1, §2.3,
39+
§2.4 for the architecture; the dispatch-time switch lives in
40+
`src/detail/webserver_request.cpp::queue_response_dispatching_kind`.
41+
2542
---

specs/tasks/M7-v2-cleanup/TASK-062.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
Make `http_response::unauthorized(...)` produce an RFC-7616-compliant `WWW-Authenticate: Digest …` challenge with `nonce`, `opaque`, `algorithm`, and `qop` parameters, and drive the matching nonce/opaque server-side state machine so strict clients negotiate a real Digest session. The current implementation (`src/httpserver/http_response.hpp:184-196`) is documented as a non-RFC-compliant stub that strict parsers reject.
99

1010
**Action Items:**
11-
- [ ] Audit current `http_response::unauthorized(...)` overloads and document the gap against RFC 7616 §3 (challenge format) and §3.4 (`Authorization` validation) in a short header comment.
12-
- [ ] Add a nonce/opaque generator to `webserver_impl` (CSPRNG-backed, with replay/expiry tracking). Reuse the existing `dauth` plumbing where possible.
13-
- [ ] Extend `http_response::unauthorized(...)` to accept (or auto-derive) `nonce`, `opaque`, `algorithm` (default `MD5`, support `SHA-256` and `SHA-256-sess`), `qop` (default `auth`).
14-
- [ ] Wire the dispatch path to validate incoming `Authorization: Digest …` against the issued nonce/opaque pair and route to `dauth` handlers.
15-
- [ ] Convert the six v2 digest placeholder integ tests (`test/integ/authentication.cpp:42-60, 245-613`) to drive the nonce/opaque state machine end-to-end. Coordinated with TASK-079 (test work).
16-
- [ ] Update Doxygen on `unauthorized(...)` to remove the "non-RFC-compliant stub" disclaimer and add RFC references.
11+
- [x] Audit current `http_response::unauthorized(...)` overloads and document the gap against RFC 7616 §3 (challenge format) and §3.4 (`Authorization` validation) in a short header comment.
12+
- [x] Add a nonce/opaque generator to `webserver_impl` (CSPRNG-backed, with replay/expiry tracking). Reuse the existing `dauth` plumbing where possible.*delegated to libmicrohttpd via `MHD_queue_auth_required_response3` (nonce HMAC keyed by `MHD_OPTION_DIGEST_AUTH_RANDOM`, replay window by `MHD_OPTION_NONCE_NC_SIZE`); opaque generated once at `webserver_impl` construction from `std::random_device`.*
13+
- [x] Extend `http_response::unauthorized(...)` to accept (or auto-derive) `nonce`, `opaque`, `algorithm` (default `MD5`, support `SHA-256` and `SHA-256-sess`), `qop` (default `auth`).*new `unauthorized(digest_challenge)` overload supports MD5 (default), SHA-256, SHA-512-256, qop="auth". SHA-256-sess deferred (out of v2 scope, see plan §7).*
14+
- [x] Wire the dispatch path to validate incoming `Authorization: Digest …` against the issued nonce/opaque pair and route to `dauth` handlers.*dispatch branches on `body_kind::digest_challenge` in `webserver_impl::queue_response_dispatching_kind`; existing `req.check_digest_auth(...)` validates the returning header.*
15+
- [x] Convert the six v2 digest placeholder integ tests (`test/integ/authentication.cpp:42-60, 245-613`) to drive the nonce/opaque state machine end-to-end. Coordinated with TASK-079 (test work).*`digest_resource` updated to emit the new `digest_challenge` body; all six tests now exercise the real handshake path end-to-end (the wrong-password tests remain 401/FAIL but now go through `MHD_digest_auth_check3` validation rather than failing because no nonce was issued).*
16+
- [x] Update Doxygen on `unauthorized(...)` to remove the "non-RFC-compliant stub" disclaimer and add RFC references.
1717

1818
**Dependencies:**
1919
- Blocked by: None
@@ -30,4 +30,4 @@ Make `http_response::unauthorized(...)` produce an RFC-7616-compliant `WWW-Authe
3030
**Related Requirements:** PRD-RSP-REQ-005 (`unauthorized` factory completeness)
3131
**Related Decisions:** None new (RFC 7616)
3232

33-
**Status:** Backlog
33+
**Status:** Done

specs/tasks/M7-v2-cleanup/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ TASK-093).
2626
|---|---|---|---|---|
2727
| TASK-060 | Scope or remove file-scoped `-Warray-bounds` suppressions | HIGH | S | Done |
2828
| TASK-061 | Mechanical cleanup sweep — unfinished prose, orphan comments, stale doc refs | HIGH | S | Done |
29-
| TASK-062 | RFC-7616-compliant Digest auth response factory | HIGH | L | Backlog |
29+
| TASK-062 | RFC-7616-compliant Digest auth response factory | HIGH | L | Done |
3030
| TASK-063 | Honor or remove `http_response::pipe` `size_hint` parameter | HIGH | S | Backlog |
3131
| TASK-064 | Structured cookie type | HIGH | M | Backlog |
3232
| TASK-065 | RFC 5952 IPv6 zero-compression in `peer_address` | HIGH | S | Backlog |

src/detail/body.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,29 @@ MHD_Response* deferred_body::materialize() {
253253
MHD_SIZE_UNKNOWN, 1024, &deferred_body::trampoline, this, nullptr);
254254
}
255255

256+
// ---------------------------------------------------------------------------
257+
// digest_challenge_body — TASK-062 / RFC 7616.
258+
//
259+
// Returns a body-only MHD_Response carrying the "access denied" payload.
260+
// The WWW-Authenticate header itself is NOT attached here -- the dispatch
261+
// path branches on body_kind::digest_challenge and routes through
262+
// MHD_queue_auth_required_response3, which writes the authoritative
263+
// challenge with libmicrohttpd's MHD_OPTION_DIGEST_AUTH_RANDOM-keyed nonce
264+
// machinery (this satisfies the "MHD MD5/SHA-256 helpers remain the
265+
// underlying primitive" acceptance criterion).
266+
// ---------------------------------------------------------------------------
267+
MHD_Response* digest_challenge_body::materialize() {
268+
if (!params_) {
269+
// Defence in depth: a moved-from body should never reach the
270+
// dispatch path, but if a regression causes it, hand MHD an
271+
// empty body rather than a null pointer.
272+
return MHD_create_response_from_buffer_static(0, nullptr);
273+
}
274+
return MHD_create_response_from_buffer_static(
275+
params_->body_text.size(),
276+
static_cast<const void*>(params_->body_text.data()));
277+
}
278+
256279
} // namespace detail
257280

258281
} // namespace httpserver

src/detail/webserver_request.cpp

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,91 @@ struct MHD_Response* webserver_impl::get_raw_response_with_fallback(detail::modd
329329
// to keep this TU under the FILE_LOC_MAX gate after adding the
330330
// after_handler / response_sent firing sites.
331331

332+
// TASK-062: kind-dispatched queueing.
333+
//
334+
// For `body_kind::digest_challenge`, delegate to
335+
// MHD_queue_auth_required_response3 so libmicrohttpd writes the
336+
// authoritative RFC-7616 WWW-Authenticate header with its
337+
// HMAC-keyed nonce, our opaque, and the requested
338+
// algorithm/qop/charset/userhash bits. For every other body kind,
339+
// queue the response through the standard MHD_queue_response path.
340+
// Returning `int` matches the legacy MHD_queue_response signature on
341+
// older MHD versions (the MHD_Result alias upcast happens at the
342+
// call site).
343+
//
344+
// The digest mapping (algorithm enum -> MHD bitfield, opaque
345+
// substitution, domain optional-NULL handling) is factored into the
346+
// `map_to_mhd_digest_args_` anonymous-namespace helper so the
347+
// member-function dispatcher stays under the CCN ceiling. That helper
348+
// only handles fields visible through `const params&`, which is safe
349+
// to expose because get_params() is public.
350+
#ifdef HAVE_DAUTH
351+
namespace {
352+
struct mhd_digest_args {
353+
MHD_DigestAuthMultiAlgo3 algo;
354+
MHD_DigestAuthMultiQOP qop;
355+
const char* opaque_cstr;
356+
const char* domain_cstr;
357+
};
358+
359+
mhd_digest_args map_to_mhd_digest_args_(
360+
const detail::digest_challenge_body::params& p,
361+
const std::string& server_opaque) {
362+
MHD_DigestAuthMultiAlgo3 algo;
363+
switch (p.algorithm) {
364+
case http::http_utils::digest_algorithm::SHA256:
365+
algo = MHD_DIGEST_AUTH_MULT_ALGO3_SHA256;
366+
break;
367+
case http::http_utils::digest_algorithm::SHA512_256:
368+
algo = MHD_DIGEST_AUTH_MULT_ALGO3_SHA512_256;
369+
break;
370+
case http::http_utils::digest_algorithm::MD5:
371+
default:
372+
algo = MHD_DIGEST_AUTH_MULT_ALGO3_MD5;
373+
break;
374+
}
375+
// qop="auth" is the only v2.0-supported variant; auth-int is parked
376+
// (TASK-062 plan §7). qop_auth == false -> RFC-2069 no-qop.
377+
MHD_DigestAuthMultiQOP qop = p.qop_auth
378+
? MHD_DIGEST_AUTH_MULT_QOP_AUTH
379+
: MHD_DIGEST_AUTH_MULT_QOP_NONE;
380+
// Empty user opaque -> substitute per-webserver opaque (plan §2.4).
381+
const char* opaque_cstr =
382+
p.opaque.empty() ? server_opaque.c_str() : p.opaque.c_str();
383+
const char* domain_cstr =
384+
p.domain.empty() ? nullptr : p.domain.c_str();
385+
return {algo, qop, opaque_cstr, domain_cstr};
386+
}
387+
} // namespace
388+
#endif // HAVE_DAUTH
389+
390+
int webserver_impl::queue_response_dispatching_kind(
391+
MHD_Connection* connection,
392+
detail::modded_request* mr,
393+
MHD_Response* raw_response) {
394+
#ifdef HAVE_DAUTH
395+
if (mr->response->kind() == body_kind::digest_challenge) {
396+
auto* dch = static_cast<detail::digest_challenge_body*>(
397+
mr->response->body_);
398+
const auto& p = dch->get_params();
399+
auto args = map_to_mhd_digest_args_(p, digest_opaque_);
400+
return static_cast<int>(MHD_queue_auth_required_response3(
401+
connection,
402+
p.realm.c_str(),
403+
args.opaque_cstr,
404+
args.domain_cstr,
405+
raw_response,
406+
p.signal_stale ? MHD_YES : MHD_NO,
407+
args.qop,
408+
args.algo,
409+
p.userhash_support ? MHD_YES : MHD_NO,
410+
p.prefer_utf8 ? MHD_YES : MHD_NO));
411+
}
412+
#endif // HAVE_DAUTH
413+
return static_cast<int>(MHD_queue_response(
414+
connection, mr->response->get_status(), raw_response));
415+
}
416+
332417
MHD_Result webserver_impl::materialize_and_queue_response(MHD_Connection* connection,
333418
detail::modded_request* mr) {
334419
struct MHD_Response* raw_response = get_raw_response_with_fallback(mr);
@@ -354,7 +439,7 @@ MHD_Result webserver_impl::materialize_and_queue_response(MHD_Connection* connec
354439
}
355440
}
356441
decorate_mhd_response(raw_response, *mr->response);
357-
int to_ret = MHD_queue_response(connection, mr->response->get_status(), raw_response);
442+
int to_ret = queue_response_dispatching_kind(connection, mr, raw_response);
358443
// TASK-050: fire response_sent AFTER MHD_queue_response (so the
359444
// status/bytes ctx fields reflect what was actually queued) and
360445
// BEFORE MHD_destroy_response (so ctx.response is backed by live

src/http_response.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,4 +483,56 @@ http_response http_response::unauthorized(std::string_view scheme,
483483
return r;
484484
}
485485

486+
#ifdef HAVE_DAUTH
487+
// TASK-062: RFC 7616 §3.3-compliant Digest challenge factory.
488+
//
489+
// Validates the user-supplied fields for header-injection control
490+
// characters (CR/LF/NUL) and packs the parameters into a
491+
// detail::digest_challenge_body. No `WWW-Authenticate` header is added
492+
// at the response-value layer; the dispatch path (TASK-062 branch in
493+
// materialize_and_queue_response) calls
494+
// MHD_queue_auth_required_response3 to attach the authoritative
495+
// challenge with nonce/opaque/algorithm/qop/charset/userhash bits.
496+
//
497+
// Empty opaque is preserved: the dispatch path substitutes
498+
// webserver_impl::digest_opaque_ at queue time, so the factory remains
499+
// side-effect-free (no webserver reference required).
500+
http_response http_response::unauthorized(digest_challenge challenge) {
501+
// Same forbidden-character set as validate_http_field above.
502+
auto reject_ctrl_chars = [](std::string_view field,
503+
std::string_view value) {
504+
if (value.find_first_of(kForbiddenFieldChars) !=
505+
std::string_view::npos) {
506+
throw std::invalid_argument(
507+
std::string("http_response::unauthorized(digest_challenge): ") +
508+
std::string(field) +
509+
" contains forbidden control character (CR, LF, or NUL)");
510+
}
511+
};
512+
reject_ctrl_chars("realm", challenge.realm);
513+
reject_ctrl_chars("opaque", challenge.opaque);
514+
reject_ctrl_chars("domain", challenge.domain);
515+
reject_ctrl_chars("body", challenge.body);
516+
517+
detail::digest_challenge_body::params p{
518+
/*realm=*/ std::move(challenge.realm),
519+
/*opaque=*/ std::move(challenge.opaque),
520+
/*domain=*/ std::move(challenge.domain),
521+
/*body_text=*/ std::move(challenge.body),
522+
/*algorithm=*/ challenge.algorithm,
523+
/*qop_auth=*/ challenge.qop_auth,
524+
/*qop_auth_int=*/ challenge.qop_auth_int,
525+
/*signal_stale=*/ challenge.signal_stale,
526+
/*userhash_support=*/ challenge.userhash_support,
527+
/*prefer_utf8=*/ challenge.prefer_utf8,
528+
};
529+
530+
http_response r;
531+
r.status_code_ = http::http_utils::http_unauthorized; // 401
532+
r.emplace_body<detail::digest_challenge_body>(
533+
body_kind::digest_challenge, std::move(p));
534+
return r;
535+
}
536+
#endif // HAVE_DAUTH
537+
486538
} // namespace httpserver

src/httpserver/body_kind.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ enum class body_kind : std::uint8_t {
5050
iovec,
5151
pipe,
5252
deferred,
53+
// TASK-062: RFC-7616 Digest auth challenge body. The body is a body-only
54+
// MHD_Response (the "access denied" payload); the WWW-Authenticate header
55+
// with nonce/opaque/qop/algorithm parameters is attached by the dispatch
56+
// path via MHD_queue_auth_required_response3 rather than by the body's
57+
// own materialize() output. Carrying the kind lets the dispatch hot path
58+
// branch onto the auth-required queueing API without naming any backend
59+
// type from http_response.hpp.
60+
digest_challenge,
5361
};
5462

5563
} // namespace httpserver

0 commit comments

Comments
 (0)