Skip to content

v2.0: Modernization (M1-M6, 44 tasks)#374

Draft
etr wants to merge 80 commits intomasterfrom
feature/v2.0
Draft

v2.0: Modernization (M1-M6, 44 tasks)#374
etr wants to merge 80 commits intomasterfrom
feature/v2.0

Conversation

@etr
Copy link
Copy Markdown
Owner

@etr etr commented Apr 30, 2026

Summary

Integration branch for the v2.0 modernization effort. Tasks land here individually (one merge commit per task) so the full v2.0 ships as a single reviewable PR.

This PR will remain draft until all milestones are complete.

Milestones

  • M1 — Foundation (TASK-001 … TASK-007): toolchain + build-system prerequisites
  • M2 — Response (TASK-008 … TASK-013)
  • M3 — Request (TASK-014 … TASK-020)
  • M4 — Handlers (TASK-021 … TASK-026)
  • M5 — Routing & Lifecycle (TASK-027 … TASK-036)
  • M6 — Release (TASK-037 … TASK-044)

Specs live under specs/ (product_specs, architecture, tasks).

Merged tasks

  • TASK-001 — Bump C++ standard floor to C++20

Test plan

Per-task validation runs through the groundwork validation loop on each task branch before merging here. Pre-merge of v2.0 to master:

  • ./configure && make clean on macOS (Apple Clang) and Linux (recent GCC)
  • make check green
  • CI matrix green across all supported toolchains
  • No -std=c++(11|14|17) regressions in tree
  • ChangeLog and README reflect v2.0 changes

🤖 Generated with Claude Code

etr and others added 3 commits April 30, 2026 23:24
Raises the project's C++ standard floor from C++17 to C++20 so that
subsequent v2.0 work can rely on concepts, std::span, <bit>,
designated initializers, and std::pmr without per-feature gates.

- m4/ax_cxx_compile_stdcxx.m4: replaced with upstream serial 25
  (autoconf-archive). The vendored serial 12 only accepted [11], [14],
  [17] and m4_fatals on anything else; serial 25 adds [20] and [23]
  alternatives plus the C++20 feature-test bodies.
- configure.ac:47: AX_CXX_COMPILE_STDCXX([17]) -> ([20], [noext],
  [mandatory]). [noext] keeps -std=c++20 (no gnu++20 extensions in
  ABI surface); [mandatory] aborts cleanly on too-old toolchains.
- configure.ac:224: dropped redundant -std=c++17 from the
  --enable-debug AM_CXXFLAGS branch. AX_CXX_COMPILE_STDCXX already
  appends -std=c++20 to $CXX, so leaving the override in would
  silently downgrade debug builds.
- Verified Makefile.am, src/Makefile.am, test/Makefile.am, and
  examples/Makefile.am: no per-subdirectory -std= overrides exist.
- .github/workflows/verify-build.yml:
  - Pruned gcc-9, clang-11, clang-12 matrix rows (incomplete C++20
    support: missing concepts/<bit>/<span> in libstdc++/libc++).
  - Bumped IWYU CXXFLAGS from -std=c++11 to -std=c++20.
- README.md: bumped Requirements to "g++ >= 10 or clang >= 13
  (Apple Clang from Xcode 15+)" and "C++20 or newer". Added a
  one-liner about gcc-toolset-14 on RHEL 9.
- README.CentOS-7: updated to reflect the C++20 floor and the
  gcc-toolset-14 workaround.
- ChangeLog: noted the standard bump under 0.20.0.

Verification (Apple Clang 21 on macOS):
- ./configure && make: succeeds with -std=c++20.
- make check: 17/17 tests pass.
- ./configure --enable-debug && make: clean under
  -Wall -Wextra -Werror -pedantic -std=c++20.
- make check (debug): 17/17 tests pass.
- grep -RE '-std=(c\+\+11|c\+\+14|c\+\+17|gnu\+\+(11|14|17))'
    configure.ac Makefile.am src test  -> zero matches.

Refs: PRD §2 NFR (modern C++ idioms), DR-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First task in the v2.0 milestone series (M1-Foundation). Raises the
project's C++ standard floor from C++17 to C++20.
Local planning artifacts from groundwork task scaffolding shouldn't be tracked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 30, 2026

Codecov Report

❌ Patch coverage is 73.75887% with 185 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.30%. Comparing base (d8b055e) to head (d2e137f).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/http_request.cpp 66.40% 53 Missing and 75 partials ⚠️
src/http_response.cpp 82.31% 1 Missing and 28 partials ⚠️
src/create_test_request.cpp 48.00% 0 Missing and 13 partials ⚠️
src/detail/body.cpp 89.06% 1 Missing and 6 partials ⚠️
src/httpserver/detail/http_request_impl.hpp 75.00% 3 Missing and 2 partials ⚠️
src/httpserver/detail/body.hpp 93.47% 2 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #374      +/-   ##
==========================================
+ Coverage   68.03%   69.30%   +1.27%     
==========================================
  Files          34       26       -8     
  Lines        1730     2258     +528     
  Branches      697      826     +129     
==========================================
+ Hits         1177     1565     +388     
- Misses         80      148      +68     
- Partials      473      545      +72     
Files with missing lines Coverage Δ
src/detail/http_endpoint.cpp 60.00% <ø> (ø)
src/http_resource.cpp 25.00% <100.00%> (+8.33%) ⬆️
src/http_utils.cpp 66.97% <100.00%> (ø)
src/httpserver/create_webserver.hpp 96.98% <ø> (ø)
src/httpserver/detail/http_endpoint.hpp 85.00% <ø> (ø)
src/httpserver/detail/modded_request.hpp 100.00% <ø> (ø)
src/httpserver/detail/webserver_impl.hpp 100.00% <ø> (ø)
src/httpserver/feature_unavailable.hpp 40.00% <ø> (ø)
src/httpserver/file_info.hpp 100.00% <ø> (ø)
src/httpserver/http_method.hpp 93.75% <ø> (ø)
... and 11 more

... and 5 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d8b055e...d2e137f. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

etr and others added 26 commits May 1, 2026 00:49
Tighten the public/private header split so detail headers and the
HTTPSERVER_COMPILATION macro cannot leak to downstream consumers, and
add make-check assertions that protect the surface going forward.

Changes:
  - src/httpserver.hpp: #undef _HTTPSERVER_HPP_INSIDE_ after all child
    includes so the macro does not survive into a consumer's TU.
  - src/Makefile.am: move httpserver/details/http_endpoint.hpp out of
    nobase_include_HEADERS into noinst_HEADERS — distributed in the
    tarball but never installed under $prefix/include. Add
    -DHTTPSERVER_COMPILATION to AM_CPPFLAGS so the lib's own TUs see it.
  - test/Makefile.am: add -DHTTPSERVER_COMPILATION to AM_CPPFLAGS so
    first-party unit tests that legitimately include detail headers
    still compile.
  - configure.ac: stop injecting -DHTTPSERVER_COMPILATION into global
    CXXFLAGS. Scope is now per-directory (lib + tests only); examples
    build as true consumers via <httpserver.hpp>.
  - Makefile.am: new check-headers target with four sub-checks
    (A.1 direct public include must fail, A.2 direct detail include
    must fail, A.3 umbrella must compile cleanly, A.4 post-umbrella
    direct include must still fail) and a new check-install-layout
    target that runs `make install DESTDIR=...` to a stage and asserts
    no `details/` directory or `*_impl.hpp` file leaks. Both wired into
    check-local.
  - test/headers/: four one-line consumer TUs driving the checks.

Per the plan's Phase 3a-i, the detail-header gate stays dual-mode
(_HTTPSERVER_HPP_INSIDE_ || HTTPSERVER_COMPILATION) because
webserver.hpp still transitively includes details/http_endpoint.hpp;
TASK-014's PIMPL split will let a future change tighten that gate to
HTTPSERVER_COMPILATION-only.

Acceptance criteria verified:
  - 17/17 existing tests pass under release and --enable-debug.
  - check-headers A.1 fires with the gate error string.
  - check-install-layout: staged install has no details/ and no
    *_impl.hpp; httpserver.hpp + httpserverpp symlink installed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… for MSYS

TASK-001 raised the C++ floor to C++20, which broke matrix entries
running gcc-10, clang-14, and clang-15 (the autoconf C++20 feature
test rejects them). Drop those entries from extra/none, and bump the
lint and performance jobs (which were pinned to gcc-10) to gcc-14 so
they still exercise an older-but-supported toolchain.

The MSYS native job started failing with "microhttpd.h not found"
because the runner image no longer ships libmicrohttpd transitively.
Add libmicrohttpd-devel to the explicit pacman install line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…set check

libmicrohttpd's <microhttpd.h> hard-asserts that _SYS_TYPES_FD_SET is
defined on Cygwin/MSYS, otherwise emitting `#error Cygwin with winsock
fd_set is not supported`. newlib defines that macro via <sys/select.h>,
included from <sys/types.h> only when __BSD_VISIBLE -- which in turn is
gated on _DEFAULT_SOURCE. Strict ANSI C++ (-std=c++NN, the floor we
adopted in TASK-001 with AX_CXX_COMPILE_STDCXX noext) suppresses
newlib's auto-define of _DEFAULT_SOURCE, so the macro never lands and
microhttpd.h refuses to compile.

This is unrelated to the C++ language mode -- _DEFAULT_SOURCE only
controls feature-test gating in system headers -- so defining it here
preserves DR-001's "noext" portability promise while fixing the build
on every Cygwin/MSYS consumer (not just our CI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n PPA, revert MSYS libmicrohttpd-devel

Three small follow-ups now that the _DEFAULT_SOURCE Cygwin/MSYS fix has
landed:

1. The four test/headers/consumer_*.cpp gate tests added in TASK-002
   were missing the project's standard LGPL/copyright header, tripping
   the lint job once gcc-14 was running cpplint over them.

2. The "Install Ubuntu test sources" step was running
   add-apt-repository ppa:ubuntu-toolchain-r/test which talks to
   launchpad and has been hitting 504 Gateway Time-out across runs.
   With the C++20 floor we no longer need the toolchain PPA -- gcc-11
   through gcc-14 ship in stock ubuntu-22.04/24.04 repos, and
   clang-13/16-18 likewise. Keep just apt-get update.

3. The earlier "add libmicrohttpd-devel to MSYS pacman" attempt was
   wrong -- there is no such MSYS native package. The actual fix was
   the configure.ac _DEFAULT_SOURCE define landed in 5b78014; revert
   the bogus pacman entry so the install step stops failing first.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce a new public header `src/httpserver/feature_unavailable.hpp`
defining `class feature_unavailable : public std::runtime_error`. The
constructor takes `(std::string_view feature, std::string_view
build_flag)` and composes a `what()` message that names both, e.g.
`"feature 'tls' unavailable: built without HAVE_GNUTLS"`.

The class is header-only and inline. It has no library dependencies
(only <stdexcept>, <string>, <string_view>), so any TU — including
later tasks like TASK-034 that need to throw it from sites in
build-time-disabled code paths — can include it without circular
header coupling. Keeping it inline also avoids ABI churn for what is
effectively a labelled std::runtime_error and keeps libhttpserver_la
sources untouched.

The header is re-exported from the umbrella `<httpserver.hpp>`
unconditionally (no `#ifdef HAVE_*` wrap): even a build with no
optional features must let consumers name `feature_unavailable` so
they can write `try { ... } catch (const httpserver::feature_unavailable&)`.

The TASK-002 inclusion gate is applied verbatim — direct inclusion of
the header without the umbrella or `HTTPSERVER_COMPILATION` errors
out, and `_HTTPSERVER_HPP_INSIDE_` does not leak post-umbrella (both
verified by the existing check-headers A.1–A.4 recipes).

A new unit test `test/unit/feature_unavailable_test.cpp` provides:
- a TU-scope `static_assert(std::is_base_of_v<std::runtime_error,
  httpserver::feature_unavailable>)` (acceptance criterion 1),
- a test that catches as `std::runtime_error` and asserts both the
  feature name and the build flag appear in `what()` (AC 2),
- a test that catches as the concrete type and confirms it slices to
  `runtime_error` correctly,
- a test with a different (feature, flag) pair to guard against
  hard-coded message text.

Verified locally:
- `make check`: 18/18 PASS (was 17, +1 for feature_unavailable),
- check-headers A.1–A.4 PASS,
- check-install-layout PASS (no details/ leak),
- staged install ships exactly one feature_unavailable.hpp at
  $(prefix)/include/httpserver/feature_unavailable.hpp,
- debug build (--enable-debug, -Werror -Wextra -pedantic) builds and
  tests cleanly.

Refs: PRD-FLG-REQ-004, PRD-FLG-REQ-005; §7 (feature availability).
Introduces a library-defined POD `httpserver::iovec_entry { const void* base;
std::size_t len; }` in a new public header `<httpserver/iovec_entry.hpp>`,
included by `<httpserver/http_response.hpp>` and the umbrella header. The
type replaces POSIX `struct iovec` at the public API surface, keeping
`<sys/uio.h>` out of every public header.

Layout pinning lives in `src/iovec_response.cpp` as six unconditional
static_asserts: three against POSIX `struct iovec` (size + iov_base /
iov_len offsets) per the spec, and three parallel asserts against
libmicrohttpd `MHD_IoVec` because that is the actual cast target on the
dispatch path. The MHD_IoVec asserts are an addition over the spec —
without them the reinterpret_cast bridge is the unsafe one. A TODO
sentinel comment (LIBHTTPSERVER_TODO_TASK004_MEMCPY_FALLBACK) documents
the memcpy fallback strategy that would activate if a divergent-layout
platform ever trips one of the asserts. Today every supported platform
(glibc, musl, macOS, FreeBSD, NetBSD, OpenBSD, illumos) shares the same
layout so the asserts pass and the reinterpret_cast is well-defined.

`iovec_response::get_raw_response()` now builds a contiguous
`std::vector<iovec_entry>` from its owned std::strings and
reinterpret_casts to `const MHD_IoVec*` when calling MHD. This proves
the cast bridge in production code today; TASK-010 will move the same
line into the future `details/body.hpp` factory.

Two new TDD-driven test programs:
- `test/unit/iovec_entry_test.cpp` — verifies POD traits (standard
  layout, trivially copyable), member types, layout equivalence with
  POSIX `struct iovec` from a consumer perspective, and the
  reinterpret_cast bridge round-trip.
- `test/unit/header_hygiene_iovec_test.cpp` — declares a colliding
  `struct iovec` before including `iovec_entry.hpp` directly. The TU
  compiling at all proves the new public header pulls in nothing from
  `<sys/uio.h>`. (The broader umbrella-leak concern — current umbrella
  transitively pulls `<sys/uio.h>` via gnutls and `<sys/socket.h>` —
  is out of scope for TASK-004 and is the remit of TASK-007's
  header-hygiene CI gate.)

Build: 20/20 tests pass under both default and `--enable-debug`
(-Wall -Wextra -Werror -pedantic -O0). `grep -E '#include\s+<sys/uio\.h>'
src/httpserver/*.hpp` returns no results. `make install` ships the new
header at `$prefix/include/httpserver/iovec_entry.hpp`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…vements

- Delete copy constructor and copy assignment on iovec_response to close
  CWE-416 use-after-free: the owning constructor stores entries_ as raw
  void* into owned_buffers_ strings; a defaulted copy would shallow-copy
  entries_ while deep-copying owned_buffers_ to new addresses, making
  entries_ dangle after source destruction. Move semantics are safe and
  kept. Static asserts in iovec_response_test.cpp guard this invariant.

- Remove the spurious '#include "httpserver/iovec_entry.hpp"' from
  http_response.hpp; http_response itself never uses iovec_entry, and
  iovec_response.hpp already includes it directly.

- Add @attention Doxygen contract to the non-owning iovec_response
  constructor documenting that caller buffers must outlive MHD_destroy_response.

- Remove duplicate offsetof/sizeof/alignof layout-pinning static_asserts
  from iovec_entry_test.cpp; authoritative copies live in iovec_response.cpp
  where the reinterpret_cast actually occurs.

- Add iovec_response_test.cpp (was untracked) with content-type forwarding
  tests and move-semantics tests for both constructor variants.

- Commit iovec_response.hpp, iovec_response.cpp, and test/Makefile.am that
  were modified/added in iter-1 but never staged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces the type-safe HTTP-method primitives that http_resource,
the route table, and lambda registration will consume.

- enum class http_method : std::uint8_t { get, head, post, put, del,
  connect, options, trace, patch, count_ }. Identifier `del` avoids
  the C++ keyword; wire token returned by to_string is "DELETE".
- struct method_set { std::uint32_t bits = 0; } with constexpr
  contains/set/clear/set_all/clear_all and defaulted operator==.
- Free constexpr noexcept bitwise operators (|, &, ^, ~, |=, &=, ^=)
  on http_method and method_set, including mixed (set, enum) overloads.
  All operators usable in constant expressions and at runtime ("consteval-
  friendly" without forbidding runtime use, which the route-table writer
  path needs).
- to_string(http_method) returning std::string_view for logging and
  the 405 Allow: header. Total over the 9 enumerators; out-of-range
  returns an empty view so logging stays robust against stale values.
- Layout/width invariants pinned at namespace scope:
  count_ <= 32, standard layout, trivially copyable,
  sizeof(method_set) == sizeof(uint32_t).
- Re-exported from <httpserver.hpp> and installed via
  nobase_include_HEADERS in src/Makefile.am.
- Test driver test/unit/http_method_test.cpp covers both compile-time
  static_asserts (round-trip, layout, bitwise composition, complement
  bounding, to_string totality) and 13 runtime LT_BEGIN_AUTO_TEST
  cases including a contract check that to_string matches
  libmicrohttpd's MHD_HTTP_METHOD_* tokens.

All 22 testsuite entries pass under the default build and under
--enable-debug (-Wall -Wextra -Werror -pedantic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move every value-form #define from public headers into
inline constexpr declarations under httpserver::constants:

- DEFAULT_WS_PORT      -> std::uint16_t (9898)
- DEFAULT_WS_TIMEOUT   -> int           (180 seconds)
- DEFAULT_MASK_VALUE   -> std::uint16_t (0xFFFF)
- NOT_FOUND_ERROR      -> std::string_view ("Not Found")
- METHOD_ERROR         -> std::string_view ("Method not Allowed")
- NOT_METHOD_ERROR     -> std::string_view ("Method not Acceptable")
- GENERIC_ERROR        -> std::string_view ("Internal Error")

The new header src/httpserver/constants.hpp uses the established
two-token gate (_HTTPSERVER_HPP_INSIDE_ + HTTPSERVER_COMPILATION),
is re-exported from <httpserver.hpp>, and is registered in
nobase_include_HEADERS so it ships in the install layout.

Internal callers in webserver.cpp, http_utils.cpp,
create_webserver.hpp, and http_utils.hpp are migrated to the
namespaced names. The string_response call sites materialize a
std::string from the string_view to satisfy the existing ctor
signature.

A new unit test (test/unit/constants_test.cpp) pins the values
and types via static_assert, and uses #ifdef sentinels to
witness that the v1 macro names no longer leak into consumer
namespace after #include <httpserver.hpp>.

NOT_METHOD_ERROR has no in-tree caller; retained for v1 API
parity per the v2.0 mechanical-migration policy.

Acceptance:
- 23/23 tests pass (release + debug -Werror -Wall -Wextra)
- Filtered grep on src/httpserver/*.hpp shows no leftover
  value-constant #defines (include guards, _WINDOWS,
  _WIN32_WINNT, and COMPARATOR are out of scope per plan §2)
- Installed-header layout includes httpserver/constants.hpp

Closes TASK-006.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mark all five action items complete and set task status to Complete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update specs/tasks/_index.md to change TASK-006 status from 'In Progress'
to 'Done', matching the completed state in TASK-006.md and the pattern
used by TASK-003, TASK-004, and TASK-005.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a two-layer header-hygiene gate that locks in the "no backend
headers leak through <httpserver.hpp>" invariant from PRD-HDR-REQ-001..003.

Layer 1 -- compile/runtime sentinel (test/unit/header_hygiene_test.cpp):
  Includes only <httpserver.hpp>, then checks well-known include-guard
  macros (MHD_VERSION, _PTHREAD_H{,_}, GNUTLS_GNUTLS_H, _SYS_SOCKET_H{,_},
  _SYS_UIO_H{,_}). At runtime it prints the leaked headers and exits 1.
  Per-target CPPFLAGS overrides AM_CPPFLAGS so HTTPSERVER_COMPILATION
  and the build-tree -I src/httpserver/ entries are NOT in scope --
  mimics a real consumer translation unit.

Layer 2 -- preprocessor grep against staged install (`make check-hygiene`):
  Stages `make install DESTDIR=$(CHECK_HYGIENE_STAGE)` to a clean tree,
  preprocesses test/headers/consumer_umbrella_no_backend.cpp using ONLY
  -I$(CHECK_HYGIENE_STAGE)$(includedir), then greps cpp line markers
  for forbidden backend headers. HEADER_HYGIENE_STRICT controls
  fatality (default no -> informational; yes -> hard fail at TASK-020).

Both gates are wired into `make check`:
- header_hygiene runs as a check_PROGRAMS test, marked XFAIL_TESTS
  until M5 lands and the umbrella is clean. Automake's XPASS-as-error
  default is the explicit signal for TASK-020 to remove the marker.
- check-hygiene runs via check-local; in non-strict mode it prints an
  EXPECTED-FAIL banner with diagnostics and exits 0 so `make check`
  stays green during M2-M5 while keeping leak progress visible.

CI surface: new header-hygiene matrix entry in verify-build.yml runs
`make check-hygiene` as a focused, named GitHub Actions check.

TASK-020.md updated with explicit M5 close-out steps (delete
XFAIL_TESTS line + flip HEADER_HYGIENE_STRICT default).

Verified locally on macOS/aarch64 with gnutls 3.x, libmicrohttpd 1.0.5,
Apple Clang 15+: 24 tests / 23 PASS / 1 XFAIL (header_hygiene); the
sentinel correctly reports microhttpd, pthread, gnutls, sys/socket,
sys/uio leaks; check-hygiene reports EXPECTED-FAIL on staged install
(webserver.hpp still references private detail header until TASK-014).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… hygiene CI matrix

- check-local: build one DESTDIR=.shared-check-stage and pass it to both
  check-install-layout and check-hygiene via CHECK_*_SHARED=yes, halving
  the install cost of `make check`. Standalone invocations still do their
  own install.
- check-hygiene: gate the staged install behind a $(HYGIENE_STAMP) mtime
  sentinel so repeated standalone runs are no-ops when public headers
  haven't changed; bypassed when CHECK_HYGIENE_SHARED=yes.
- check-hygiene grep: anchor HEADER_HYGIENE_FORBIDDEN to a leading "/"
  so leak detection only matches absolute paths, not arbitrary substrings.
- clean-local: remove the stage directories on `make clean`.
- CI: header-hygiene matrix entry skips the unconditional `make check`
  step (the dedicated `make check-hygiene` step is the gate for that job).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the polymorphic body hierarchy that http_response's SBO buffer will
host (TASK-009) and the public body_kind enum that http_response::kind()
will return (TASK-011). TASK-008 ships only the standalone hierarchy:
each subclass is independently constructible, destructible, and
materializable, mirroring the corresponding v1 *_response::get_raw_response.

New public header (umbrella-included):
- httpserver/body_kind.hpp: enum class body_kind : std::uint8_t {
  empty, string, file, iovec, pipe, deferred }; empty=0 so a
  value-initialised body_kind matches the no-body state.

New private header (HTTPSERVER_COMPILATION-only, never installed):
- httpserver/details/body.hpp: abstract detail::body + 6 final
  subclasses (empty_body, string_body, file_body, iovec_body, pipe_body,
  deferred_body) plus per-subclass static_assert(sizeof <= 64) and
  static_assert(alignof(deferred_body) <= 16) for the SBO budget
  (DR-005).

Out-of-line definitions in src/details/body.cpp:
- materialize() per subclass mirrors v1 byte-for-byte
  (string=PERSISTENT, file=open/fstat/lseek/from_fd, iovec=CWE-190
  guard + reinterpret_cast to MHD_IoVec, pipe=from_pipe, deferred=
  from_callback with a static trampoline).
- Layout-pinning static_asserts duplicated from iovec_response.cpp
  (TASK-013 will remove the originals).
- pipe_body::~pipe_body() closes fd_ only if materialize() was never
  called (MHD owns it after a successful materialise).

New test:
- test/unit/body_test.cpp drives every subclass through MHD's
  daemon-independent inspection APIs (no daemon spun up). 12 tests, 29
  checks; the deferred trampoline is exposed as a public static so it
  can be unit-tested directly. Linked with explicit -lmicrohttpd
  (mirrors uri_log).

Observed sizes on libc++/arm64: empty=16, string=32, file=40, iovec=40,
pipe=16, deferred=40. All well under the 64 B SBO budget — TASK-010
will not need the heap-fallback branch on supported toolchains.

Out of scope (TASK-009/010): http_response wiring, body_inline_
fallback, kind() accessor, removal of v1 *_response subclasses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Applies fixes from the iter1 review pass on the detail::body hierarchy:

file_body (CWE-367 / perf):
- Open + fstat moved to constructor; size() is now accurate immediately.
- Drops lseek(SEEK_END); materialize() uses st_size from fstat.
  Closes the TOCTOU window between size discovery and the fd handed
  to MHD_create_response_from_fd, and removes the side-effect on the
  fd's read position.
- Adds destructor that closes fd_ only when MHD never took ownership
  (materialized_ stays false until from_fd returns non-null).

deferred_body (CWE-476):
- trampoline() guards against null cls and empty producer_ before
  invoking the std::function. MHD's callback path doesn't catch C++
  exceptions, so a bad_function_call would terminate in MHD's IO
  thread; the guard returns MHD_CONTENT_READER_END_WITH_ERROR instead.
- Constructor asserts producer_ is non-empty (debug-only precondition).

Header docs:
- file_body: documents path-canonicalisation contract (O_NOFOLLOW only
  blocks the final component) and fd ownership lifecycle.
- iovec_body: documents the borrowed-pointer lifetime contract
  (iov_base buffers must outlive the MHD_Response*) and the heap
  allocation note from DR-005.
- deferred_body: documents the std::function SBO caveat — capturing
  more than the implementation-defined threshold silently heap-allocates.

Tests:
- file_body_size_known_before_materialize: size() must be correct at
  construction (21 bytes for test_content), not only after materialize.
- deferred_body_trampoline_null_cls_returns_error: trampoline with
  cls==nullptr returns MHD_CONTENT_READER_END_WITH_ERROR rather than
  dereferencing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings in the polymorphic detail::body hierarchy plus iter1 review-pass
fixes (file_body TOCTOU, deferred_body null-callable guard, header
lifetime/ownership docs, and accompanying tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweeps in groundwork-generated planning content that had been left
untracked across recent task work, and adds .DS_Store to .gitignore so
macOS metadata stops appearing as untracked.

Planning content:
- specs/product_specs.md — top-level product spec.
- specs/architecture/ — system overview, architectural drivers,
  per-component specs (body-hierarchy, create-webserver, http-method,
  http-request, http-resource, http-response, route-table, webserver,
  websocket-handler), cross-cutting concerns, integration, feature
  availability, build/packaging, testing, observability, the DR-001..011
  decision records, open questions, documentation, and appendices.
- specs/tasks/M{1..6}-*/TASK-*.md — task definitions for the v2.0
  milestones (M1 foundation through M6 release). Pre-existing tasks
  TASK-006/007 were already tracked from prior commits; this adds the
  rest, including the M2 response, M3 request, M4 handlers, and M5
  routing-lifecycle definitions.

Review records:
- specs/unworked_review_issues/2026-04-30..2026-05-03_*.md — outputs
  from the iter1 review passes on TASK-001 through TASK-008. Captured
  for traceability; "unworked" denotes issues not yet folded back into
  task scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When TASK-003..008 were merged into feature/v2.0 they were not pushed
individually, so the cumulative push surfaced regressions across the
matrix. This sweeps them up.

Build error (basic ubuntu / valgrind / windows-IWYU):
- test/unit/body_test.cpp:56-60: static_cast<int>(uint8_t-enum) >= 0
  is always-true, breaking -Werror=type-limits. Replace with
  enumerator != body_kind::empty so the compile-time reference still
  guards against a missing enumerator without the bogus comparison.

cpplint (17 errors → 0):
- Include order:
  - src/details/body.cpp, src/iovec_response.cpp,
    src/httpserver/details/body.hpp,
    test/unit/{body_test,header_hygiene_test,http_method_test,
    iovec_entry_test}.cpp: move <microhttpd.h> and <sys/uio.h> into
    the C-system-header group so the layout is primary, c, c++, other.
- Missing includes:
  - src/details/body.cpp, src/iovec_response.cpp: add <string> for
    std::string in the file_body / iovec_response signatures.
  - src/iovec_response.cpp: add <utility> for std::move.
- Header guard:
  - src/httpserver/details/body.hpp: cpplint expects #ifndef GUARD as
    the first non-comment line. Move the SRC_HTTPSERVER_DETAILS_BODY_HPP_
    guard above the HTTPSERVER_COMPILATION #error block (which now
    lives inside the guard).
- Misc:
  - body_kind.hpp: NOLINT(build/include_what_you_use) on the `string`
    enumerator (cpplint mistook it for std::string).
  - body_test.cpp:251: split single-line if-with-multiple-statements.
  - http_method_test.cpp:121: add space between [] and { in lambda.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
etr and others added 30 commits May 4, 2026 10:52
- Mark TASK-013 status as Done (matches the convention used by all other
  M2 tasks; addresses housekeeper-iter1-16) and tick off the six action
  items now satisfied by the implementation commit.
- Sync specs/tasks/_index.md row from "Not Started" → "Done".
- Sync specs/architecture/04-components/http-response.md to reflect the
  sealed `final` http_response and the ref-qualified `& / &&` `with_*`
  setter overloads added in TASK-009/TASK-012.
- Record the 33 unworked findings (1 major, 32 minor) from the validation
  pass in specs/unworked_review_issues/2026-05-04_013108_task-013.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves webserver's backend-coupled state (MHD_Daemon*, pthread mutexes,
ban set, allowance set, route table, route cache, websocket registry,
optional GnuTLS SNI cache) into a new internal class
detail::webserver_impl, leaving the public httpserver/webserver.hpp
carrying only the const config bag plus a single std::unique_ptr<impl>.

Acceptance criteria:
- grep '#include <microhttpd.h>' src/httpserver/webserver.hpp returns
  nothing.
- webserver is now non-copyable and non-movable (PIMPL ABI lock-down,
  asserted at compile time by the new webserver_pimpl test).
- sizeof(webserver) shrank by ~half (the backend state moved behind
  impl_); the new test bounds it at 144 * sizeof(void*) so future
  regressions surface immediately.
- All 26 v1 integration + unit tests still pass; header_hygiene stays
  XFAIL until TASK-020 finishes scrubbing the umbrella.

Implementation notes:
- detail/webserver_impl.hpp is gated #if !defined(HTTPSERVER_COMPILATION)
  -- strict one-mode form, since it is reachable only from the library
  TUs and never from the public umbrella.
- Dispatch helpers (request_completed, answer_to_connection,
  post_iterator, finalize_answer, complete_request, materialize_response,
  decorate_mhd_response, etc.) and the auxiliary MHD callbacks
  (policy_callback, error_log, access_log, uri_log, unescaper_func)
  moved to webserver_impl as static members so their MHD-typed
  signatures stay off the public header.
- webserver_impl carries a back-pointer `parent` to the owning webserver
  so dispatch helpers can read the const config (tcp_nodelay, unescaper,
  regex_checking, auth_handler, ...).
- http_response, http_request, http_resource, websocket_handler, and
  http::file_info gain `friend class detail::webserver_impl;` next to
  the existing `friend class webserver;` so the migrated dispatch
  helpers retain the same private access they had before.
- webserver.hpp drops <microhttpd.h>, <pthread.h>, <gnutls/gnutls.h>,
  <list>, <map>, <mutex>, <set>, <shared_mutex>, <unordered_map>, and
  the transitive include of detail/http_endpoint.hpp.
- detail/http_endpoint.hpp had a stale forward declaration of
  `class http_resource;` inside namespace httpserver::detail; removed
  because (a) it shadowed the real httpserver::http_resource for any
  caller doing name lookup from inside namespace detail, and (b) it
  was unused.
- test/unit/webserver_pimpl_test.cpp -- TASK-014 sentinel asserting the
  PIMPL-shaped invariants (non-copyable, non-movable, bounded size).
- test/unit/uri_log_test.cpp updated to call the migrated
  detail::webserver_impl::uri_log static through a thin trampoline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Set TASK-014 status to "In Progress" in TASK-014.md and _index.md row.
  Structural action items are checked off but the validation pass surfaced
  3 major and 24 minor unworked findings, so the task is not yet Done.
- Record the 27 unworked findings (0 critical, 3 major, 24 minor) from the
  validation pass in specs/unworked_review_issues/2026-05-04_115707_task-014.md
  for follow-up work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move webserver's backend state (MHD_Daemon*, mutexes, ban set, connection
table, route cache) into detail/webserver_impl.hpp behind a unique_ptr
impl_ pointer. Public webserver.hpp no longer includes <microhttpd.h> or
<pthread.h>; <sys/socket.h> residual is tracked under TASK-020.

Status: In Progress — 27 unworked validation findings (3 major, 24 minor)
recorded in specs/unworked_review_issues/2026-05-04_115707_task-014.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move every backend-coupled member of http_request behind a
unique_ptr<detail::http_request_impl>. The public header
src/httpserver/http_request.hpp no longer includes <microhttpd.h>
or <gnutls/gnutls.h>; the only backend-typed names that remain
visible at the public surface are forward-declared (MHD_Connection*
in the HTTPSERVER_COMPILATION-gated private ctor, and a narrow
typedef forward-decl of gnutls_session_t for the still-public
get_tls_session() return type that TASK-019 will remove).

Outer keeps: path, method, content, version, content_size_limit,
plus the impl_ unique_ptr. Everything else (the live MHD_Connection*,
unescaper, file table, parsed-args / cookies / cert caches, the
MHD trampolines build_request_*, fetch_user_pass(),
populate_all_cert_fields()) lives on http_request_impl. Public
methods are one-line forwarders.

The dtor is out-of-line in http_request.cpp so unique_ptr<impl>
sees the complete impl type. Move ctor/assign remain defaulted
and operate on the unique_ptr.

A new sentinel test/unit/http_request_pimpl_test.cpp asserts that
http_request appears non-copy/non-move-constructible from external
scope (private moves), and locks sizeof(http_request) at <= 24
pointer widths. Currently 14 pointers (112 B on LP64).

Acceptance criteria all green:
- grep -E '#include <(microhttpd|gnutls/gnutls)\.h>' on the public
  header returns nothing.
- All v1 request-side tests pass (basic, file_upload,
  authentication, http_resource, threaded, nodelay, ws_start_stop,
  uri_log, etc. -- 27 PASS + 1 XFAIL header_hygiene umbrella,
  unchanged from baseline; the umbrella sweep is TASK-020).
- sizeof(http_request) = 14 * sizeof(void*); asserted in the new
  sentinel.
- noinst_HEADERS lists the impl header so it ships in the source
  tarball but never installs to $prefix/include.
- No test reaches across the boundary into http_request_impl.
create_test_request now allocates the impl_ and stores headers/footers/
cookies/args/querystring/auth/requestor/tls flags on it, instead of on
http_request directly. The MHD-touching accessors in http_request_impl
(get_connection_value, get_headerlike_values, populate_args, fetch_user_pass,
has_tls_session) and in http_request (get_querystring, get_digested_user,
get_requestor) now branch on connection_ == nullptr to read from the
local maps when running through the test-request path.

Also adds a string_body::get_content() helper used by the new
create_test_request unit suite, friend-grants create_test_request access
to http_request::impl_, and ships the create_test_request unit binary
from the autotools build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Persist the validation-loop reviewer findings (0 critical / 10 major /
54 minor) under specs/unworked_review_issues/. Status flag and index
sync were already part of the skeleton commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nly)

Brings in:
- e9175c1 TASK-015: http_request_impl skeleton (PIMPL split, structural only)
- fd6c984 TASK-015: wire test-request fallback through http_request_impl
- 01c425b TASK-015: housekeeping (review record)
Allocate http_request_impl from a std::pmr::monotonic_buffer_resource
owned by connection_state so the warm path stops touching the global
heap on every request. The arena is wired in via MHD's
NOTIFY_CONNECTION callback (new on STARTED, delete on CLOSED) and
release()-rewound by request_completed once modded_request -- and
therefore the impl's destructor -- has run. A keep-alive connection
reuses the same buffer for every subsequent request.

Internal pmr-aware containers (header_local/footer_local/cookies_local
stay default-allocated for the test path; querystring, requestor_ip,
client_cert_*, unescaped_args, path_pieces, and the auth strings move
to std::pmr) propagate the arena allocator through scoped construction.
files_ stays default-allocated by design: file_info owns disk-side
state and decoupling its lifecycle from the arena keeps that reasoning
local.

The public header keeps <memory_resource> hidden: the impl deleter is a
small forward-declared struct holding only a function pointer, so
sizeof(unique_ptr<impl, deleter>) is two pointers regardless of where
the impl was allocated. The deleter dispatches between operator-delete
(heap fallback / test-request path) and destructor-only (arena path).

Tests:
- New unit test test/unit/http_request_arena_test.cpp covers the three
  acceptance facts: (a) arena_.release() rewinds the bump pointer,
  (b) warm-path http_request_impl construction does not touch the
  upstream resource (custom counting upstream verifies zero allocs),
  (c) two consecutive impls land at the same address across release().
- Sequential make check passes except for the pre-existing
  create_test_request::method_uppercase failure (unrelated to TASK-016;
  reproduces on the baseline).
- AddressSanitizer reports no use-after-free / heap-use-after-free
  across any of basic/threaded/http_request_arena/http_request_pimpl/
  authentication/deferred under -fsanitize=address.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- security: document arena lifetime contract on http_request public API
- security: cap headers/args via max_args_count + max_args_bytes
- security: zero arena memory on reset_arena() to prevent disclosure
- perf: bump arena initial buffer from 4 KiB to 8 KiB
- perf: add debug fallback counter + stderr warning when pick_resource
  spills to heap so undersized arenas surface in test runs
- tests: add coverage for arg-size guards, arena zeroing, and warm-path
  zero-upstream-allocs with PMR containers populated

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six container getters on http_request now return `const ContainerType&`
aliasing impl-owned storage instead of copying maps/vectors on every
call:

  - get_path_pieces() -> const std::vector<std::string>&
  - get_headers()     -> const http::header_view_map&
  - get_footers()     -> const http::header_view_map&
  - get_cookies()     -> const http::header_view_map&
  - get_args()        -> const http::arg_view_map&
  - get_files()       -> const std::map<std::string,
                                        std::map<std::string,
                                                 http::file_info>>&

Implementation
--------------
http_request_impl carries six new mutable caches (one per getter):

  * headers_cached_ / footers_cached_ / cookies_cached_ -- view-maps
    populated lazily on the first call to get_headers/footers/cookies.
    The new ensure_headerlike_cache(MHD_ValueKind) helper replaces
    get_headerlike_values(), populating the matching slot once and
    returning a const& on subsequent calls. The string_view keys/values
    alias MHD-owned storage on the live-request path and the impl's
    own headers_local/footers_local/cookies_local maps on the
    test-request path -- both share the request lifetime.

  * args_view_cached_ -- view-map built once from the pmr-backed
    `unescaped_args` after populate_args(). The string_views alias the
    pmr::strings owned by unescaped_args, same lifetime.

  * path_pieces_public_ -- public-typed mirror of the existing
    pmr-backed `path_pieces`. Two caches in lockstep: the pmr one stays
    arena-friendly for any future internal consumer; the public one is
    what get_path_pieces() returns by const&.

get_files() returns impl_->files_ directly (already the exact public
type, no extra cache); marked noexcept since the body is now a pure
member dereference.

Allocator note
--------------
The cached view-maps and path_pieces_public_ are default-allocator-
typed because the public API's container types are -- they cannot be
PMR without changing the public surface. First call therefore allocates
on the global heap; subsequent calls are O(1) and zero-allocating, a
strict win over v1 which paid the build cost on every call. This is
the trade-off PRD §3.6 anticipates and TASK-039 will measure.

Lifetime contract
-----------------
http_request.hpp gains a "Container reference lifetime contract"
section in the class-level docblock: the returned references and any
iterators / element references / string_view keys/values derived from
them remain valid until the http_request is destroyed (typically when
the handler invocation returns). This mirrors the TASK-016 string_view
contract.

Tests
-----
* test/unit/http_request_pimpl_test.cpp -- twelve compile-time asserts
  added: six is_lvalue_reference_v assertions (matching the literal
  acceptance criterion) plus six is_same_v assertions locking the
  exact `const ContainerType&` return type. Drift back to by-value or
  to a non-const reference fails compilation now.

* test/unit/create_test_request_test.cpp -- new
  `getters_return_const_ref_stable` test verifying that two calls to
  the same getter on a const http_request return the same address (the
  cache reference is stable across calls).

Caller sweep
------------
Callers that took the return by value have been updated to bind by
const reference where the original semantics were read-only:
test/integ/basic.cpp (args_caching, footer_test_resource),
test/integ/file_upload.cpp (print_file_upload_resource render_POST/PUT
-- args_view bound by ref; the `files = req.get_files()` copies are
deliberate and now copy-from-const& with comments explaining intent),
test/unit/create_test_request_test.cpp (build_headers /
build_footers_cookies / build_path_pieces), and
examples/args_processing.cpp.

Known pre-existing failure unrelated to TASK-017:
test/unit/create_test_request_test::method_uppercase. It expects
set_method() to uppercase its argument; commit a2afe2b removed that
uppercasing, and the test was added in 755ecc1 after that, so it has
been failing on feature/v2.0 since well before this task. Not fixed
here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/create_test_request.cpp: uppercase method via to_upper_copy so
  create_test_request_suite/method_uppercase passes (`.method("post")`
  -> `POST`); fix cpplint header order.
- src/http_request.cpp: reorder includes to match cpplint convention
  (matching header, C system, C++ system, project) and add <utility>
  for std::move at line 363.
- examples/empty_response_example.cpp: move <microhttpd.h> before
  <memory> to satisfy cpplint header ordering.
- examples/url_registration.cpp: split a 215-char render() body to
  satisfy cpplint line-length cap (200).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-key getters (get_header / get_cookie / get_footer / get_arg /
get_arg_flat) and the always-present getters (get_path / get_method /
get_version / get_content / get_querystring) were already const-callable
and (post-TASK-015/016/017) empty-on-miss without insertion. This task
finalizes the contract:

- Mark the five always-present getters `noexcept`. The four trivial
  string-returning getters (path/method/version/content) are reduced
  noexcept because string_view's conversion from std::string is itself
  noexcept since C++17. `get_querystring()` becomes noexcept by moving
  its lazy MHD enumeration to the http_request ctor on the live-MHD
  path; the public reader is now a trivial member access.
- Add Doxygen `@note` blocks reiterating the per-request lifetime
  contract on each of the five.
- Lock the entire surface (10 getters) at compile time via a new
  pure-static_assert TU `test/unit/http_request_const_getters_test.cpp`,
  asserting `is_invocable_v<..., const http_request&, ...>`, exact
  return-type identity (string_view for nine; http_arg_value for
  get_arg, deliberately preserved for multi-value semantics), and
  `noexcept` on the five always-present getters.
- Add behavioral tests in create_test_request_test.cpp: a
  missing-key-no-insert assertion that hammers each per-key getter
  with five misses and verifies the underlying container sizes don't
  grow, plus a hit-returns-correct-value assertion.
- Wire the new TU into test/Makefile.am.

Note: `get_arg` keeps its `http_arg_value` return type. The
architecture spec §4.2 reads "string_view get_arg" but
test/integ/basic.cpp depends on `.get_all_values()` (multi-value
semantics that get_arg_flat doesn't cover). Narrowing get_arg to
string_view would either alias get_arg_flat or require renaming the
multi-value method -- both wider than TASK-018's stated scope.
The interpretive deviation is documented in the test's static_assert
comments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ll const

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the public `gnutls_session_t get_tls_session()` accessor and the
HAVE_GNUTLS-gated cert accessor block on `http_request` with an
unconditional, high-level surface:

  bool has_tls_session() const noexcept;
  bool has_client_certificate() const noexcept;
  std::string_view get_client_cert_dn() const;
  std::string_view get_client_cert_issuer_dn() const;
  std::string_view get_client_cert_cn() const;
  std::string_view get_client_cert_fingerprint_sha256() const;
  bool is_client_cert_verified() const noexcept;
  std::int64_t get_client_cert_not_before() const noexcept;
  std::int64_t get_client_cert_not_after() const noexcept;

The four string accessors return string_view aliasing per-request impl
storage (same lifetime contract as TASK-016/017/018). The five noexcept
accessors swallow allocator failures from populate_all_cert_fields() and
return their documented sentinel.  When HAVE_GNUTLS is off at build
time every accessor returns empty / false / -1 without throwing.

The internal `gnutls_session_t get_tls_session()` lookup stays on
`http_request_impl` (reached via friend access from library internals
only). It is now nullptr-safe on the test-request path so the test
builder's `tls_enabled()` flag does not segfault inside MHD.

Header-hygiene: <gnutls/gnutls.h> is removed from src/httpserver/http_utils.hpp;
cred_type_T values are hard-coded to match gnutls_credentials_type_t,
pinned to GNUTLS_CRD_* by static_asserts in src/webserver.cpp where
<gnutls/gnutls.h> is reachable.  Both AC greps now return empty:

  grep -E '#include\s+<gnutls/' src/httpserver/*.hpp
  grep -E 'gnutls_session_t' src/httpserver/*.hpp

Tests:
- New compile-time sentinel test/unit/http_request_tls_accessors_test.cpp
  locks return types, noexcept specifiers, and the absence of
  get_tls_session via SFINAE.
- New build_no_client_cert_returns_sentinels and
  build_tls_enabled_no_peer_cert cases in
  test/unit/create_test_request_test.cpp cover the sentinel contract on
  the test-request path (always-on; runs in both build modes).
- New client_cert_accessors_known_values in test/integ/ws_start_stop.cpp
  exercises the new accessors against the known client_cert.pem trust
  setup (DN/CN/issuer match, fingerprint length+charset, validity
  ordering, verified=yes).

All 32 tests pass; the header_hygiene XFAIL stays XFAIL (TASK-020 owns
the flip).  Release and --enable-debug builds both clean under
-Werror -Wall -Wextra -pedantic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes test/Makefile.am's `XFAIL_TESTS = header_hygiene` line and
flips Makefile.am's `HEADER_HYGIENE_STRICT` default from `no` to `yes`.

Both gates were intentionally informational while M2-M5 were in flight.
Now that the structural pieces are in place, this commit makes the
gates fatal so the umbrella sweep produces a real red->green TDD cycle:

  $ make check          -> FAIL: header_hygiene (3 forbidden header guards
                           defined: MHD_VERSION, _PTHREAD_H, _SYS_SOCKET_H_)
  $ make check-hygiene  -> FAIL: forbidden headers leaked through
                           <httpserver.hpp> (microhttpd.h, sys/socket.h,
                           pthread.h, plus libc++ STL transitive)

Subsequent commits in this task remove the leaks at the source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the last <microhttpd.h>, <microhttpd_ws.h>, and <sys/socket.h>
includes from the three public headers that still carried them, and
fixes the downstream test/library .cpp files that previously relied on
transitive include leakage.

Public-header changes (the sweep):
  * http_utils.hpp -- drops <microhttpd.h> and <sys/socket.h>;
    forward-declares struct sockaddr; hard-codes start_method_T,
    digest_algorithm, and digest_auth_result enum values; converts
    is_feature_supported(MHD_FEATURE) to is_feature_supported(int).
    static_asserts in src/http_utils.cpp pin all enum values to the
    upstream MHD_* / MHD_DIGEST_AUTH_ALGO3_* / MHD_DAUTH_* macros so
    a future libmicrohttpd renumber breaks the build at the right
    place.  Per PRD-FLG-REQ-001, the digest_* enum class declarations
    are unconditional (no #ifdef HAVE_DAUTH guard around them); only
    the static_assert pins remain conditional, since they reference
    the upstream macros that are absent when digest auth is disabled
    at the libmicrohttpd build site.
  * webserver.hpp -- drops <sys/socket.h>; forward-declares struct
    sockaddr and struct sockaddr_storage; converts get_fdset's
    fd_set* parameters to void* and add_connection's socklen_t to
    unsigned int. src/webserver.cpp casts back at the boundary and
    static_asserts that unsigned int is at least as wide as
    socklen_t (POSIX guarantees this on every supported platform).
  * websocket_handler.hpp -- drops <microhttpd.h> and
    <microhttpd_ws.h>; forward-declares MHD_UpgradeResponseHandle and
    MHD_WebSocketStream; converts MHD_socket to std::intptr_t on the
    public surface. src/websocket_handler.cpp casts back to MHD_socket
    at every send_all() call site and static_asserts that intptr_t is
    at least as wide as MHD_socket.
  * http_request.hpp -- forward-declares struct MHD_Connection at
    GLOBAL scope under HTTPSERVER_COMPILATION so the in-namespace
    MHD_Connection*-taking constructor does not silently inject
    httpserver::MHD_Connection and shadow the real (global) type once
    <microhttpd.h> is reached later in src/http_request.cpp.

Companion .cpp changes (transitive-include fallout):
  * src/http_utils.cpp -- now includes <microhttpd.h> directly;
    is_feature_supported's body cast-adapts; static_asserts pin
    enum values (digest pins gated on HAVE_DAUTH).
  * src/webserver.cpp / src/websocket_handler.cpp -- include the
    backend headers directly and adapt at the boundary.
  * src/httpserver/detail/modded_request.hpp -- includes
    <microhttpd.h> directly (its destructor calls
    MHD_destroy_post_processor inline).  detail/ is not in the
    nobase_include_HEADERS install set, so this stays internal.
  * test/integ/daemon_info.cpp -- includes <microhttpd.h> directly
    (uses MHD_FEATURE_* enumerators).
  * test/integ/file_upload.cpp, test/unit/http_utils_test.cpp --
    include <unistd.h> directly (use unlink/rmdir).

Hygiene-gate changes:
  * test/unit/header_hygiene_test.cpp -- skips the _PTHREAD_H /
    _PTHREAD_H_ guards on libc++ (Apple's default STL on macOS), which
    unconditionally pulls in <pthread.h> from any STL container header
    via its <__thread/support/pthread.h> backend. The libstdc++ /
    other-STL path keeps the guards. Documented in the file's header
    comment.
  * Makefile.am -- removes pthread.h from the static
    HEADER_HYGIENE_FORBIDDEN regex used by `make check-hygiene`, for
    the same libc++ reason. The runtime sentinel still enforces the
    invariant on libstdc++ and other STLs.

Verification (this commit):
  * grep -nE '^[[:space:]]*#[[:space:]]*include.*<(microhttpd|pthread|gnutls/gnutls|sys/socket|sys/uio)' src/httpserver/*.hpp
    -> zero results.
  * make check -> 32/32 PASS, header_hygiene PASS (no XFAIL, no XPASS).
  * make check-hygiene HEADER_HYGIENE_STRICT=yes -> PASS: no forbidden
    headers reached the consumer TU.
  * Smoke compile of `#include <httpserver.hpp>\nint main(){}` against
    the staged install (only -I to the libhttpserver prefix, no -I to
    libmicrohttpd / pthread / gnutls) -> clean compile.

The textual grep `grep -lE 'microhttpd\.h|pthread\.h|gnutls\.h|sys/socket\.h' src/httpserver/*.hpp` still flags five files; all matches are inside `// comment` text (TASK-015/TASK-019/TASK-020 explanatory breadcrumbs), not real `#include` directives. The CI gates only see post-preprocessor text and are unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rueFalse, exclude specs/

Codacy was reporting 2018 new issues on the v2.0 PR (#374). Resolve as
follows:

* Add .codacy.yaml excluding specs/** — the product spec, architecture
  notes, task records, and review notes are internal groundwork artifacts,
  not user-facing docs, and should not be subject to README markdownlint
  rules. Removes 2003 markdownlint findings.

* src/webserver.cpp:499 — drop the redundant `blocking &&` from the wait
  loop condition. `blocking` is a function parameter never reassigned
  inside the loop body, so the conjunct was tautological
  (cppcheck knownConditionTrueFalse).

* src/webserver.cpp:946 — replace the C-style `(struct detail::modded_request*)`
  cast on the MHD `cls` void* with `static_cast<detail::modded_request*>`
  (cppcheck cstyleCast). Mirrors the existing static_cast usage elsewhere
  in the file.

* detail/webserver_impl.hpp, detail/http_request_impl.hpp, iovec_entry.hpp —
  add `// cppcheck-suppress-file unusedStructMember` with a one-line
  rationale comment. Every flagged member is in fact heavily used from
  the corresponding .cpp translation unit (registered_resources*,
  route_cache_*, bans, allowances, files_, path_pieces_public_,
  iovec_entry::base/len, etc.); cppcheck analyses each TU in isolation
  and cannot see those uses, so the warning is a known pimpl/POD
  false positive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI on Linux failed `header_hygiene` because libstdc++ in -D_REENTRANT mode
(set unconditionally by configure.ac) routes std::thread support through
<bits/gthr-default.h>, which #includes <pthread.h> directly. That defines
_PTHREAD_H once any STL container header (<memory>, <string>, <vector>, ...)
is reached -- so the runtime sentinel reported a leak even though no
libhttpserver public header pulls <pthread.h> in.

This is the same STL implementation detail we already exempt for libc++
via _LIBCPP_VERSION; the original test comment claimed libstdc++ did not
have this property, which was incorrect for thread-enabled builds.

Extend the existing skip to also fire on libstdc++ in thread mode by
testing _GLIBCXX_HAS_GTHREADS, and update the matching comment in
Makefile.am so the asymmetry between the runtime sentinel and the
preprocessor-grep gate (which already excludes pthread) is documented
consistently. The guard still fires under STLs that don't route
std::thread through pthread (e.g. MSVC's Microsoft STL).

Verified locally:
  - macOS libc++: header_hygiene PASS (unchanged).
  - gcc:14 libstdc++ -D_REENTRANT: pre-fix the test reproduces the exact
    CI failure ("1 forbidden header(s) leaked"); post-fix it PASSes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… clash

Two unrelated CI regressions on PR #374, both falling out of TASK-020:

1. Lint job (gcc-14, ubuntu): cpplint flagged
   src/http_utils.cpp:30 with build/include_order, because the
   matching public header ("httpserver/http_utils.hpp") came AFTER a
   non-matching project header ("httpserver/constants.hpp"), and
   <microhttpd.h> (a C system header in cpplint's view) followed both.
   cpplint's expected order is: matching header, C system, C++ system,
   other. Reorder so the matching header comes first and the project
   headers ("constants.hpp" / "string_utilities.hpp") move to the
   bottom of the include block.

2. Windows MSYS2 build: src/httpserver/http_utils.hpp failed with
       error: expected identifier before numeric constant
   at the line `ERROR = 0,` inside the digest_auth_result enum.
   <wingdi.h> (pulled in via <windows.h> via <winsock2.h> via
   <microhttpd.h> on MinGW) unconditionally `#define`s ERROR to 0,
   and the preprocessor expands macros inside scoped-enum bodies just
   like anywhere else. Pre-TASK-020 the enum was inside
   `#ifdef HAVE_DAUTH`, so MSYS2 builds without digest auth never
   compiled it; PRD-FLG-REQ-001 then made the enum unconditional and
   exposed the latent collision. v2.0 is unreleased, so renaming is
   safe: ERROR -> GENERIC_ERROR (matches MHD_DAUTH_ERROR's "general
   error" docs). Static-assert pin in src/http_utils.cpp updated to
   match.

Verified locally:
  - python3 -m cpplint on both touched files: exit 0.
  - `make check` on macOS: 32/32 PASS, all check-hygiene /
    check-headers gates PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant