Skip to content

Commit fb875dc

Browse files
etrclaude
andcommitted
docs(architecture): add threading, errors, hooks, and feature deep-dives
Four reference docs behind the two hero diagrams, as GitHub-native Markdown (Mermaid + tables): - threading.md — DR-008 concurrency contract, full mutex inventory, lock ordering, the two threading models, the Helgrind lane rationale. - errors.md — DR-009 error propagation, every 4xx/5xx origin, the handler-exception flow, config knobs, feature_unavailable reach. - hooks.md — the 11 hook phases as a user cookbook: context fields, short-circuit vs observe, server vs per-route, hook_handle, v1 aliases, recipes with example refs. - features.md — HAVE_* detection, gated symbols, features() vs is_feature_supported, feature_unavailable throw sites, build matrix. Indexed and cross-linked from docs/architecture/README.md, grouped by audience (contributors / API users / packagers). Sourced from the current feature/v2.0 tree (DR-008/DR-009, post-DR-014 decomposition). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tAodxYJMEY4VxCX4dk62e
1 parent 973064c commit fb875dc

5 files changed

Lines changed: 380 additions & 3 deletions

File tree

docs/architecture/README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
# libhttpserver v2.0 — architecture diagrams
22

3-
Two visual references for the v2.0 (`feature/v2.0`) codebase. Each exists in two forms:
3+
Architecture references for the v2.0 (`feature/v2.0`) codebase.
44

5-
| Diagram | GitHub-native (below) | Rich standalone page |
5+
**Two hero diagrams** — spatial maps, best viewed as the rich self-contained HTML (no external assets, light/dark aware); Mermaid quick-views render inline below.
6+
7+
| Diagram | GitHub-native | Rich page |
68
|---|---|---|
79
| **Class, relationship & filesystem map** | [Mermaid ↓](#1-class-relationship--filesystem-map) | [`class-map.html`](class-map.html) |
810
| **Request lifecycle & routing flow** | [Mermaid ↓](#2-request-lifecycle--routing-flow) | [`request-flow.html`](request-flow.html) |
911

10-
The `.html` files are self-contained (no external assets, light/dark aware) — open them directly in a browser for the full, colour-coded detail: every class card, filesystem location, ownership edge, all 28 flow steps, the four-tier route cascade, and the hook rail. The Mermaid versions below render inline on GitHub for a quick orientation.
12+
**Four deep dives** — reference-heavy topics, as Markdown (Mermaid + tables, render inline on GitHub).
13+
14+
| Doc | For | Covers |
15+
|---|---|---|
16+
| [**threading.md**](threading.md) | contributors | DR-008 concurrency contract, full mutex inventory, lock ordering, Helgrind lane |
17+
| [**errors.md**](errors.md) | contributors | DR-009 propagation, every 4xx/5xx origin, the handler-exception path, config knobs |
18+
| [**hooks.md**](hooks.md) | API users | the 11 phases, context fields, short-circuit vs observe, `hook_handle`, recipes |
19+
| [**features.md**](features.md) | packagers / API users | `HAVE_*` detection, gated symbols, `features()`, `feature_unavailable`, build matrix |
1120

1221
> **Colour language** (shared by both diagrams): composition-root = blue · state collaborator = amber · behavior service = teal · MHD C-ABI adapter = purple · domain / value type = slate.
1322

docs/architecture/errors.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Error propagation & status codes (DR-009)
2+
3+
> Where every error status comes from, how handler exceptions become responses, and what you can customize.
4+
> Decision record: [`specs/architecture/11-decisions/DR-009.md`](../../specs/architecture/11-decisions/DR-009.md) · cross-cutting §5.2: [`05-cross-cutting.md`](../../specs/architecture/05-cross-cutting.md).
5+
6+
## The contract
7+
8+
DR-009 chose **Option 1 — no throw-as-status idiom.** There is no `http_error` type. To return a 404/400 you build a response by value:
9+
10+
```cpp
11+
return http_response::empty().with_status(404);
12+
```
13+
14+
Any **uncaught exception** from a handler becomes a **500**, routed through an optional `internal_error_handler` escape hatch. The six-point rule:
15+
16+
1. Handler throws `std::exception` → caught, logged via `log_error`, `internal_error_handler` invoked with `e.what()`. **Default body is the fixed string `"Internal Server Error"`** (CWE-209) — the verbatim message reaches only the log and a configured handler.
17+
2. Handler throws non-`std::exception``catch (...)`, message sentinel `"unknown exception"`, same fixed default body.
18+
3. Library-internal dispatch exception (allocation, body materialization) → same path.
19+
4. `internal_error_handler` itself throws → logged, hardcoded **empty-body 500**.
20+
5. `feature_unavailable` is a plain `std::runtime_error` — no special status mapping.
21+
6. No throw-as-status.
22+
23+
**No-throw boundary:** the exception→status translation is entirely internal C++; nothing new crosses the ABI. A response always reaches MHD — the four terminal producers are `noexcept` or exception-contained.
24+
25+
## Status origins
26+
27+
| Status | Producer | Trigger | Customize with |
28+
|---|---|---|---|
29+
| **404** | `error_pages::not_found_page` | route lookup miss in `finalize_answer` (also the `invalid_argument` fallback arm of `get_raw_response_with_fallback`) | `not_found_handler` |
30+
| **405** | `error_pages::method_not_allowed_page` | `is_allowed(method)` false in `dispatch_resource_handler`; `Allow:` header from `hrm->get_allow_header()` | `method_not_allowed_handler` |
31+
| **500** (default `"Internal Server Error"`) | `error_pages::internal_error_page` (`force_our=false`) | handler/dispatch exception, null-materialize, `-1` sentinel response | `internal_error_handler`, `expose_exception_messages` |
32+
| **500** (body = `e.what()`) | same, when `expose_exception_messages==true` | dev opt-in only ||
33+
| **500** (hardcoded empty body) | `internal_error_page(force_our=true)` | double-fault: user handler threw, or belt-and-suspenders sites ||
34+
| **401** (Basic / scheme) | `http_response::unauthorized(scheme, realm, body)` | user-returned; builds `WWW-Authenticate: <scheme> realm="…"` (control-char rejection, quoted-string escaping) | your handler |
35+
| **401** (Digest) | `http_response::unauthorized(digest_challenge)``MHD_queue_auth_required_response3` | user-returned; `body_kind::digest_challenge` (needs `HAVE_DAUTH`) | your handler |
36+
| **400** (bad WS handshake) | `websocket_upgrader::try_handle` | `Upgrade: websocket` present but `validate_websocket_handshake` fails (bad `Connection` / `Sec-WebSocket-Version` ≠ 13 / empty key) ||
37+
| **101** (success, not an error) | `complete_websocket_upgrade` | successful WebSocket upgrade ||
38+
39+
Default bodies (`constants.hpp`): `"Not Found"`, `"Method not Allowed"`, `"Internal Server Error"`.
40+
41+
## The handler-exception path
42+
43+
```mermaid
44+
flowchart TD
45+
H["handler throws"] --> C{"catch in<br/>dispatch_resource_handler"}
46+
C --> L["log_dispatch_error · verbatim to log_error"]
47+
L --> HDE{"handler_exception hooks<br/>or alias present?"}
48+
HDE -- yes --> FS["fire_handler_exception<br/>server-wide, then per-route"]
49+
FS --> R{"a hook returned<br/>respond_with?"}
50+
R -- yes --> OUT["that response is sent"]
51+
R -- no --> FORCE["internal_error_page force_our<br/>empty-body 500"]
52+
HDE -- no --> SAFE["run_internal_error_handler_safely"]
53+
SAFE --> IEH{"internal_error_handler set?"}
54+
IEH -- yes --> UH["user handler(request, message)"]
55+
IEH -- no --> EXP{"expose_exception_messages?"}
56+
EXP -- yes --> BODYW["body = message"]
57+
EXP -- no --> BODYD["body = Internal Server Error"]
58+
UH -. throws .-> FORCE
59+
```
60+
61+
Notable details:
62+
63+
- **`-1` sentinel:** `http_response::status_code_` defaults to `-1`. A returned response with `get_status()==-1` is treated as null and rerouted through `run_internal_error_handler_safely(…, "handler returned null response")`.
64+
- **A throwing `handler_exception` hook does not abort the chain** — it is logged and the next hook runs (the chain's job is recovery). Every *other* phase's throwing hook is routed back through the normal DR-009 500 path.
65+
- **`run_internal_error_handler_safely`** wraps the user `internal_error_handler` in try/catch; if it throws, it logs and falls back to the hardcoded empty-body 500. No exception escapes.
66+
- **`log_dispatch_error`** (`noexcept`) is the single internal logging channel: no-op if `log_error` is unset, otherwise forwards the message **verbatim** (flag-independent) inside a `try/catch` that swallows a misbehaving logger.
67+
68+
## Configuration
69+
70+
| Knob | Signature / default | Effect |
71+
|---|---|---|
72+
| `not_found_handler` | `std::function<http_response(const http_request&)>`, default none | replaces the 404 body |
73+
| `method_not_allowed_handler` | `std::function<http_response(const http_request&)>`, default none | replaces the 405 body (`Allow:` still added) |
74+
| `internal_error_handler` | `std::function<http_response(const http_request&, std::string_view message)>`, default none | invoked with the originating message for 500 |
75+
| `expose_exception_messages` | `bool`, default `false` | **dev only** — default-500 body becomes the originating message instead of `"Internal Server Error"` (CWE-209) |
76+
| `expose_credentials_in_logs` | `bool`, default `false` | when `true`, `http_request::operator<<` streams passwords / `Authorization` / cookies verbatim instead of `<redacted>` (CWE-312/532) |
77+
78+
**Message-exposure matrix:** the verbatim message always goes to `log_error` and to a configured `internal_error_handler`; it reaches the **wire** only under `expose_exception_messages(true)`.
79+
80+
## `feature_unavailable`
81+
82+
`class feature_unavailable : public std::runtime_error` — always compiled, regardless of `HAVE_*`. Thrown when a feature-gated API is used on a build without that feature (see [features & build matrix](features.md)). Its reach depends on the call site:
83+
84+
- Thrown at **`webserver` construction** or from **setup / registration** APIs → propagates to the **caller** (your application code).
85+
- Thrown from **inside a handler** (e.g. `unauthorized(digest_challenge)` on a non-digest build, or a `websocket_session` send) → it is a normal `std::exception` on the dispatch path → generic **500** to the client, unless an `internal_error_handler` special-cases it.
86+
87+
---
88+
*See also: [request-flow](request-flow.html) (where 404/405/500 sit in the dispatch sequence) · [hooks cookbook](hooks.md) (the `handler_exception` phase).*

docs/architecture/features.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Feature availability & build matrix
2+
3+
> Which capabilities compile in, how they're detected, and what happens when they're absent.
4+
> Build & packaging spec: [`specs/architecture/08-build-and-packaging.md`](../../specs/architecture/08-build-and-packaging.md).
5+
6+
## The design rule
7+
8+
**The public symbol surface is identical on every build.** Every feature-gated method is always declared and linkable — only the method *body* branches on the build macro, either returning a documented sentinel or throwing `feature_unavailable`. The four `HAVE_*` macros are pushed via compiler `-D` flags (not a `config.h`), so no consumer include is required.
9+
10+
## The four features
11+
12+
```mermaid
13+
flowchart LR
14+
subgraph det["Build detection · configure.ac"]
15+
G["gnutls/gnutls.h"] --> HG["HAVE_GNUTLS"]
16+
B["MHD_basic_auth_get_username_password3"] --> HB["HAVE_BAUTH"]
17+
D["MHD_digest_auth_check3"] --> HD["HAVE_DAUTH"]
18+
W["microhttpd_ws.h + MHD_websocket_stream_init"] --> HW["HAVE_WEBSOCKET"]
19+
end
20+
HG --> TLS["TLS · client certs · SNI · PSK"]
21+
HB --> BA["Basic auth"]
22+
HD --> DA["Digest auth"]
23+
HW --> WS["WebSocket"]
24+
```
25+
26+
| Feature | Detected by (`configure.ac`) | Gated symbols | `features()` bool | When absent |
27+
|---|---|---|---|---|
28+
| **TLS / HTTPS** | `AC_CHECK_HEADER(gnutls/gnutls.h)``HAVE_GNUTLS` | `http_request_impl_tls.cpp` (`get_tls_session`, `populate_all_cert_fields`), `http_request` cert accessors, `sni_credentials_cache`, `sni_cert_callback_func`, `psk_cred_handler_func`, GnuTLS MHD options | `tls` | `use_ssl(true)`**ctor throws** `feature_unavailable("tls","HAVE_GNUTLS")`; cert accessors return `false`/empty/`-1`; `cred_type`/PSK/SNI options silently dropped |
29+
| **Basic auth** | `AC_CHECK_LIB(microhttpd, MHD_basic_auth_get_username_password3)``HAVE_BAUTH` | `fetch_user_pass`, `get_user`/`get_pass` bodies | `basic_auth` | `basic_auth(true)`**ctor throws** `("basic_auth","HAVE_BAUTH")`; `get_user`/`get_pass` → empty |
30+
| **Digest auth** | `AC_CHECK_LIB(microhttpd, MHD_digest_auth_check3)``HAVE_DAUTH` | `digest_opaque_`, `check_digest_auth`, `check_digest_auth_digest`, `get_digested_user`, `unauthorized(digest_challenge)`, `digest_challenge_response_body`, digest MHD options | `digest_auth` | `digest_auth(true)`**ctor throws** `("digest_auth","HAVE_DAUTH")`; `unauthorized(digest_challenge)` → throws same; `check_*``WRONG_HEADER`; `get_digested_user` → empty |
31+
| **WebSocket** | `AC_CHECK_HEADER(microhttpd_ws.h)` + `AC_CHECK_LIB(microhttpd_ws, MHD_websocket_stream_init)``HAVE_WEBSOCKET` (adds `-lmicrohttpd_ws`) | `websocket_upgrader`, real `websocket_session`, `websocket_handler` hooks, populated `ws_registry`, `register_ws_resource`/`unregister_ws_resource`, `MHD_ALLOW_UPGRADE` | `websocket` | `register_ws_resource`/`unregister_ws_resource` **throw** `("websocket","HAVE_WEBSOCKET")`; session send/close throw same; handler hooks no-op |
32+
33+
Each macro is injected into `AM_CXXFLAGS`/`AM_CFLAGS` as `-D<MACRO>` (with a matching `AM_CONDITIONAL`); none is written to `config.h`.
34+
35+
### Other build-time switches
36+
37+
| Macro | Detection | Gates |
38+
|---|---|---|
39+
| `MHD_OPTION_HTTPS_CERT_CALLBACK` | MHD-provided | SNI callback wiring (additionally requires `HAVE_GNUTLS`) |
40+
| `MHD_VERSION` | MHD header | `http_utils.cpp` compat shim for pre-0.9.74 status-code spellings |
41+
| `HAVE_EXPLICIT_BZERO` | `AC_CHECK_DECLS([explicit_bzero])` | `secure_zero.hpp` implementation (internal) |
42+
| `USE_FASTOPEN` | kernel `TCP_FASTOPEN` probe (`--enable-fastopen`) | `MHD_USE_TCP_FASTOPEN` daemon flag |
43+
44+
**Minimum libmicrohttpd: 1.0.0**`configure.ac` hard-errors below `MHD_VERSION 0x01000000`.
45+
46+
## Runtime feature reporting
47+
48+
- **`webserver::features()`** (`noexcept`) returns `{basic_auth, digest_auth, tls, websocket}`, each a `constexpr` resolved purely from the build macros. It reflects the **library's** build flags, not the caller's — the body lives in `webserver.cpp` precisely so consumers see the shipped library's capabilities. It does **not** query MHD.
49+
- **`http::http_utils::is_feature_supported(int)`** is the only *runtime* MHD query — wraps `MHD_is_feature_supported((enum MHD_FEATURE)…)`; independent of the four `HAVE_*` macros.
50+
- **`http::http_utils::get_mhd_version()`** returns MHD's version string.
51+
52+
## `feature_unavailable` throw sites
53+
54+
`class feature_unavailable : public std::runtime_error` (always compiled). `what()` = `"feature '<feature>' unavailable: built without <build_flag>"`.
55+
56+
- `webserver::webserver` ctor — validated **lazily at construction** (never at the setter): `use_ssl` w/o GnuTLS, `basic_auth` w/o BAuth, `digest_auth` w/o DAuth.
57+
- `http_response::unauthorized(digest_challenge)` w/o `HAVE_DAUTH`.
58+
- `webserver::register_ws_resource` / `unregister_ws_resource` and every `websocket_session` send/close w/o `HAVE_WEBSOCKET`.
59+
60+
Setters never throw — they only mutate config; validation is deferred. So `use_ssl(true)` on a GnuTLS-off build compiles and returns fine, then throws when you construct the `webserver`.
61+
62+
## Builder setters that depend on a feature
63+
64+
| Setter | Feature | If feature absent |
65+
|---|---|---|
66+
| `use_ssl(bool)` | HAVE_GNUTLS | ctor throws |
67+
| `https_mem_key/cert/trust` (+ `raw_*`), `https_mem_dhparams`, `https_key_password`, `https_priorities[_append]` | HAVE_GNUTLS (via `use_ssl`) | wired only under `use_ssl`; otherwise inert |
68+
| `cred_type(cred_type_T)` | HAVE_GNUTLS | option added only under `HAVE_GNUTLS`; else ignored (ctor throws first if `use_ssl` also set) |
69+
| `psk_cred_handler(cb)` | HAVE_GNUTLS + `use_ssl` | ignored otherwise |
70+
| `sni_callback(cb)` | HAVE_GNUTLS + cert-callback + `use_ssl` | ignored otherwise |
71+
| `digest_auth_random(str)`, `nonce_nc_size` | HAVE_DAUTH | option added only under `HAVE_DAUTH`; else ignored |
72+
| `basic_auth(bool)` | HAVE_BAUTH | ctor throws if enabled |
73+
| `digest_auth(bool)` | HAVE_DAUTH | ctor throws if enabled |
74+
75+
`basic_auth`/`digest_auth` default to `true` **only** when their macro is defined, so an auth-off build defaults the flag off and never throws spuriously.
76+
77+
## Build systems
78+
79+
- **Autotools is the only build with capability detection**: `configure.ac` + `src/Makefile.am`. All four `HAVE_*` and the switches above are probed here.
80+
- **CMake support is consumer-side only**: `cmakemodule/FindLibHttpServer.cmake` is a plain find-module (locates headers + library); it does no capability detection and does not build the library. There is no top-level `CMakeLists.txt`.
81+
82+
---
83+
*See also: [errors](errors.md) (`feature_unavailable`'s reach — caller vs client) · [class map](class-map.html) (the gated collaborators, marked `HAVE_WEBSOCKET`).*

0 commit comments

Comments
 (0)