Skip to content

Commit 6388623

Browse files
etrclaude
andcommitted
specs: add planning scaffolding and review records
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>
1 parent 454e3d4 commit 6388623

87 files changed

Lines changed: 3613 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ libtool
6161
.claude
6262
CLAUDE.md
6363
.groundwork-plans/
64+
.DS_Store
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
## 1) Executive Summary
2+
3+
libhttpserver is a C++ HTTP server library wrapping libmicrohttpd. v2.0 is a clean breaking release whose architectural goal is to **hide the C backend from the public ABI** and **fit 2026 C++ idioms** without requiring users to subclass, manage raw pointers, or mirror the library's build flags.
4+
5+
The design rests on five load-bearing choices: a **C++20 floor**; **PIMPL on `webserver` and `http_request`** with a backend-free public surface; a **non-PIMPL value-typed `http_response`** with a polymorphic body held in a 64-byte SBO buffer that falls back to heap; **handler-returns-by-value** as the canonical signature; and a **route table with three structures** (hash for exact paths, radix for parameterized + prefix, regex chain for fallback). The remaining decisions — thread-safety contract, error propagation, deferred/websocket lifecycle, ABI versioning — are documentation and consistency rather than novel mechanism.
6+
7+
The architecture preserves libmicrohttpd as the only backend (no pluggable backends in scope) but makes its presence invisible in `<httpserver.hpp>`. It commits to value semantics where they fit and PIMPL where they don't, refusing to apply either uniformly.
8+
9+
---
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## 2) Architectural Drivers
2+
3+
### 2.1 Business Drivers (from PRD §1)
4+
- **Vision:** A modern, ergonomic C++ HTTP server library that hides its libmicrohttpd backend, fits 2026 C++ idioms, and is safe to use without reading the source.
5+
- **JTBD: 30-line endpoint without subclassing.** Drives the lambda-first handler model and value-typed response.
6+
- **JTBD: Build flags must not leak.** Drives the build-flag-independent ABI and unconditional declarations.
7+
- **JTBD: No transitive C-header inclusion.** Drives PIMPL and forward declarations on backend types.
8+
- **North-star: hello world ≤10 LOC**, zero public-header dependencies on backend C types.
9+
10+
### 2.2 Quality Attributes (from PRD §2)
11+
12+
| Attribute | Requirement | Architecture response |
13+
|---|---|---|
14+
| Public-header decoupling | No `<microhttpd.h>` / `<gnutls/gnutls.h>` / `<pthread.h>` / `<sys/socket.h>` / `<sys/uio.h>` in installed headers | PIMPL on `webserver` and `http_request`; forward-declared `detail::body` for `http_response`; high-level accessors (cert DN, fingerprint) replacing raw GnuTLS handles; library-defined `httpserver::iovec_entry` POD replacing `struct iovec` in the public `http_response::iovec(...)` factory |
15+
| Build-flag stability | Public API surface invariant under `HAVE_BAUTH` / `HAVE_DAUTH` / `HAVE_GNUTLS` / `HAVE_WEBSOCKET` | Unconditional declarations; runtime sentinels or `feature_unavailable` throws when backends disabled; `webserver::features()` reports availability |
16+
| Const correctness | Pure accessors `const`; lazy caches OK via `mutable`; daemon-driving methods exempt | Request-side caches in `mutable` storage (or unique_ptr); `is_running` / `get_fdset` / `get_timeout` documented as exempt operations |
17+
| Hot-path performance | Per-request getters do not allocate or copy containers | Container-returning getters change to `const&` / `string_view`; per-request impl arena-allocated from a per-connection `std::pmr::monotonic_buffer_resource`; method-state held as a `uint32_t` bitmask, not a `std::map` |
18+
| Naming | Snake_case + one canonical verb per concept | `block_ip` / `unblock_ip` (replacing four ban/allow synonyms); `_handler` suffix (replacing `_resource` for function-shaped setters); `shoutCAST` grandfathered as a protocol identifier |
19+
| Documentation | v2.0 ships rewritten README, examples, RELEASE_NOTES.md | Out of architecture scope; flagged in §13 as a documentation-track deliverable |
20+
21+
### 2.3 Constraints
22+
23+
**Technical:**
24+
- libmicrohttpd is the only backend; pluggable backends are explicitly out of scope (PRD §3.1).
25+
- Distro packagers are a named target user segment (PRD §1) — system-toolchain compatibility on Debian stable, RHEL, FreeBSD ports must be respected.
26+
- The library is currently autoconf-built; v2.0 keeps that toolchain.
27+
28+
**Team:**
29+
- Single maintainer (Sebastiano Merlino) plus drive-by contributors. Architecture choices favor maintainability over novelty.
30+
31+
**Release:**
32+
- v2.0 is a hard cutover. No v1.x maintenance branch. SOVERSION bump (PRD §1, OQ-007).
33+
34+
---
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
## 3) System Overview
2+
3+
### 3.1 High-level shape
4+
5+
```
6+
┌──────────────────────────────────────────────────────────────────────┐
7+
│ Consumer translation unit │
8+
│ #include <httpserver.hpp> │
9+
│ │
10+
│ webserver ──→ http_request ──→ http_resource / lambda handler │
11+
│ │ ↓ │
12+
│ ↓ http_response │
13+
│ (PIMPL) (value type, SBO body) │
14+
└──────────┬───────────────────────────────────────────────────────────┘
15+
│ (no backend types crossed)
16+
17+
┌──────────┴───────────────────────────────────────────────────────────┐
18+
│ libhttpserver.so internals │
19+
│ │
20+
│ webserver::impl (MHD_Daemon, route table, mutex, bans set) │
21+
│ ├── route table: { exact: hash, param/prefix: radix, regex: chain} │
22+
│ ├── per-connection arena (std::pmr::monotonic_buffer_resource) │
23+
│ └── http_request::impl (allocated from connection's arena) │
24+
│ │
25+
│ detail::body (polymorphic; subclasses string/file/iovec/pipe/ │
26+
│ deferred/empty live in details/body.hpp) │
27+
└──────────┬───────────────────────────────────────────────────────────┘
28+
29+
┌──────────┴───────────────────────────────────────────────────────────┐
30+
│ libmicrohttpd (C backend) │
31+
│ MHD_Daemon, MHD_Connection, MHD_Response │
32+
└────────────────────────────────────────────────────────────────────────┘
33+
```
34+
35+
### 3.2 Component summary
36+
37+
| Component | Responsibility | Implementation |
38+
|---|---|---|
39+
| `webserver` | Lifecycle, route registration, IP block list, MHD daemon ownership | PIMPL via `std::unique_ptr<webserver_impl>` |
40+
| `http_request` | Per-request inputs (path, method, headers, args, body, TLS metadata) | PIMPL via `std::unique_ptr<http_request_impl>`; impl allocated from per-connection arena |
41+
| `http_response` | Response value: status, headers, footers, cookies, body | Non-PIMPL value type; polymorphic body in 64-byte SBO buffer with heap fallback |
42+
| `http_resource` | Class-form handler (state shared across HTTP methods of one resource) | Public abstract base; allow-mask held as `method_set` (`uint32_t` bitmask) |
43+
| `websocket_handler` | Per-endpoint WebSocket protocol handler | Public abstract base; registered via `unique_ptr` / `shared_ptr` overloads |
44+
| `detail::body` | Polymorphic body kinds (string / file / iovec / pipe / deferred / empty) | Internal hierarchy in `src/httpserver/details/body.hpp` |
45+
| Route table | Path → (method_set, handler) lookup | `unordered_map` (exact) + radix tree (parameterized + prefix) + regex chain (fallback) |
46+
47+
---
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
## 4) Component Details
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
### 4.8 `detail::body` hierarchy
2+
3+
**Responsibility:** Polymorphic body representation backing `http_response`'s SBO buffer. Each subclass carries the data needed for one body kind and knows how to stream itself into an MHD response.
4+
5+
**Implementation:** Abstract base in `src/httpserver/details/body.hpp` (not installed):
6+
7+
```cpp
8+
namespace httpserver::detail {
9+
class body {
10+
public:
11+
virtual ~body() = default;
12+
virtual body_kind kind() const noexcept = 0;
13+
virtual std::size_t size() const noexcept = 0;
14+
virtual MHD_Response* materialize(/* dispatch context */) = 0; // builds the MHD response on demand
15+
};
16+
17+
class string_body : public body { /* std::string content; */ };
18+
class file_body : public body { /* std::string path; std::size_t size_cached; */ };
19+
class iovec_body : public body { /* std::vector<iovec> iov; (iovec from <sys/uio.h>, included only in this private header) */ };
20+
class pipe_body : public body { /* int fd; std::size_t hint; */ };
21+
class deferred_body: public body { /* std::function<ssize_t(uint64_t pos, char* buf, std::size_t max)> producer; */ };
22+
class empty_body : public body { /* nothing */ };
23+
}
24+
```
25+
26+
**SBO storage:** factories use placement-new into the response's `body_storage_` buffer when the subclass fits (always true for v2.0's set). New body kinds added in v2.x check at compile time (`static_assert`) whether they fit; if they don't, the factory falls back to `new`-allocating and storing the heap pointer.
27+
28+
**Materialization timing:** `materialize()` is called from `webserver`'s dispatch, not from the handler. The body holds whatever data it needs (strings, paths, callables) until that point; resources owned by the body (file handles, pipe FDs) are opened lazily during materialize where appropriate.
29+
30+
**Related requirements:** PRD-RSP-REQ-006, PRD-HDR-REQ-005.
31+
32+
---
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
### 4.9 `create_webserver` (builder)
2+
3+
**Responsibility:** Configuration builder for `webserver`.
4+
5+
**Implementation:** Single-class builder, ~half the v1 line count. Each paired `foo()/no_foo()` collapses to `foo(bool = true)` (PRD-CFG-REQ-001). All `#define` constants (`DEFAULT_WS_PORT`, `DEFAULT_WS_TIMEOUT`, `NOT_FOUND_ERROR`) move to `constexpr` in `httpserver::constants` (PRD-CFG-REQ-002). Out-of-range setters throw `std::invalid_argument` (PRD-CFG-REQ-003).
6+
7+
The builder remains non-PIMPL (it's a pure value carrier; PIMPL would buy nothing).
8+
9+
**Related requirements:** PRD-CFG-REQ-001..004.
10+
11+
---
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
### 4.6 `http_method` and `method_set`
2+
3+
**Responsibility:** Type-safe representation of HTTP methods and method-allow masks.
4+
5+
**Implementation:**
6+
7+
```cpp
8+
enum class http_method : std::uint8_t {
9+
get, head, post, put, del, connect, options, trace, patch, count_
10+
};
11+
// `del` rather than `delete` (C++ keyword); `count_` sentinel for compile-time iteration.
12+
13+
struct method_set {
14+
std::uint32_t bits = 0;
15+
constexpr bool contains(http_method m) const noexcept;
16+
constexpr method_set& set(http_method m) noexcept;
17+
constexpr method_set& clear(http_method m) noexcept;
18+
constexpr method_set& set_all() noexcept;
19+
constexpr method_set& clear_all() noexcept;
20+
// bitwise free operators on http_method and method_set, all constexpr noexcept
21+
};
22+
```
23+
24+
`uint32_t` carries 32 method slots — 23 bits of growth headroom beyond the 9 standard methods (room for WebDAV verbs if ever added).
25+
26+
**Related requirements:** PRD-REQ-REQ-003, PRD-HDL-REQ-006.
27+
28+
---
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
### 4.2 `http_request`
2+
3+
**Responsibility:** Carry per-request inputs from MHD's worker thread to the user handler. Lazily-cache derived data (path pieces, parsed args, basic-auth credentials, client cert fields).
4+
5+
**Implementation:** PIMPL via `std::unique_ptr<http_request_impl>`. The impl is **arena-allocated** from a `std::pmr::monotonic_buffer_resource` that lives on the connection (one arena per MHD connection, reset between requests on the same keep-alive connection). The arena also backs the impl's owned strings and lazy-cache containers where practical, eliminating per-request `malloc` on the hot path.
6+
7+
**Interfaces:**
8+
- Exposes (from PRD §3.6):
9+
- `get_path()`, `get_method()`, `get_version()`, `get_content()`, `get_querystring()` returning `string_view`
10+
- `get_headers()`, `get_footers()`, `get_cookies()`, `get_args()`, `get_path_pieces()`, `get_files()` returning `const ContainerType&`
11+
- `get_header(key)`, `get_cookie(key)`, `get_footer(key)`, `get_arg(key)`, `get_arg_flat(key)` returning `string_view` (empty on miss; never insert)
12+
- `get_user()`, `get_pass()`, `get_digested_user()` returning `string_view` (empty when basic/digest auth disabled at build)
13+
- `has_tls_session()`, `has_client_certificate()`, `get_client_cert_dn()`, `get_client_cert_issuer_dn()`, `get_client_cert_cn()`, `get_client_cert_fingerprint_sha256()`, `is_client_cert_verified()`, `get_client_cert_not_before()`, `get_client_cert_not_after()` (all returning sentinels when GnuTLS disabled)
14+
- `check_digest_auth(...)` family
15+
- `get_requestor()`, `get_requestor_port()`
16+
- All getters are `const`. Lazy caches use `mutable` (or unique_ptr indirection); the const-correctness NFR's exemption for daemon-driving methods does not apply to request — every request getter is logically const.
17+
- Move-only (preserves identity; rules out shared ownership). PRD §3.6 out-of-scope: not changing the move-only identity.
18+
19+
**Key design notes:**
20+
- The arena allocator is plumbed through `webserver_impl` → connection state → `http_request` constructor. The user does not see it; it is an internal optimization.
21+
- Containers returned by `get_*()` reference impl-owned storage; the request must outlive any view derived from it. Documented as a lifetime contract.
22+
- `gnutls_session_t` (raw GnuTLS handle) is not exposed publicly. Users wanting custom TLS introspection use the high-level `get_client_cert_*` accessors. The handle remains accessible via friend access from internal code.
23+
24+
**Related requirements:** PRD-HDR-REQ-001..004, PRD-FLG-REQ-001..002, PRD-REQ-REQ-001, PRD-RSP-REQ-* (for the response side of the request/response cycle).
25+
26+
---
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
### 4.4 `http_resource` (class-form handler)
2+
3+
**Responsibility:** Stateful handler base for cases where state is shared across HTTP methods of one resource (counter, cache, DB handle, auth context).
4+
5+
**Implementation:** Public abstract base. Subclasses override one of `render_get / render_post / render_put / render_delete / render_patch / render_options / render_head` (renamed from v1's `render_GET` etc., to comply with PRD-NAM-REQ-001 snake_case). The default `render(...)` falls back when the method-specific override is not provided.
6+
7+
The allow-mask (formerly `std::map<std::string, bool> method_state`) becomes `method_set methods_allowed_;` — a `uint32_t` bitmask wrapper (DR-6). `is_allowed(http_method)` and `get_allowed_methods()` are `const` and return without allocation.
8+
9+
**Lifetime:** owned by the `webserver` via `unique_ptr` or `shared_ptr` (PRD-HDL-REQ-003). Raw-pointer registration is gone (PRD-HDL-REQ-005).
10+
11+
**Related requirements:** PRD-HDL-REQ-003, PRD-HDL-REQ-005, PRD-REQ-REQ-002, PRD-REQ-REQ-003.
12+
13+
---

0 commit comments

Comments
 (0)