Skip to content

Commit fd8b090

Browse files
committed
Merge TASK-026: Generic webserver::route(method, path, handler)
2 parents 39c3c0d + c1f1f25 commit fd8b090

8 files changed

Lines changed: 737 additions & 56 deletions

File tree

specs/tasks/M4-handlers/TASK-026.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
Provide the table-driven escape hatch for registering handlers when the HTTP method is a runtime value.
99

1010
**Action Items:**
11-
- [ ] Add `webserver::route(http_method m, const std::string& path, std::function<http_response(const http_request&)> handler);`.
12-
- [ ] Implementation dispatches to the same internal registration path used by `on_*`.
13-
- [ ] Document the call-site convention: `route()` is the escape hatch; `on_*` is preferred when the method is known statically.
14-
- [ ] Add `webserver::route(method_set methods, const std::string& path, handler)` if a single handler should serve multiple methods (e.g., GET and HEAD).
11+
- [x] Add `webserver::route(http_method m, const std::string& path, std::function<http_response(const http_request&)> handler);`.
12+
- [x] Implementation dispatches to the same internal registration path used by `on_*`.
13+
- [x] Document the call-site convention: `route()` is the escape hatch; `on_*` is preferred when the method is known statically.
14+
- [x] Add `webserver::route(method_set methods, const std::string& path, handler)` if a single handler should serve multiple methods (e.g., GET and HEAD).
1515

1616
**Dependencies:**
1717
- Blocked by: TASK-005, TASK-025
@@ -26,4 +26,4 @@ Provide the table-driven escape hatch for registering handlers when the HTTP met
2626
**Related Requirements:** PRD-HDL-REQ-006
2727
**Related Decisions:** §4.7, OQ-003 resolution
2828

29-
**Status:** Not Started
29+
**Status:** Done

specs/tasks/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ Nominally: **13 sequential tasks**, each S–XL. Most other tasks parallelize of
108108
| TASK-023 | Smart-pointer `register_resource` overloads | M4 | Done | TASK-014 |
109109
| TASK-024 | `register_path` and `register_prefix` (replace `bool family`) | M4 | Done | TASK-023 |
110110
| TASK-025 | Lambda handler entry points `on_*` | M4 | Done | TASK-005, TASK-009, TASK-014 |
111-
| TASK-026 | Generic `webserver::route(method, path, handler)` | M4 | Not Started | TASK-005, TASK-025 |
111+
| TASK-026 | Generic `webserver::route(method, path, handler)` | M4 | Done | TASK-005, TASK-025 |
112112
| TASK-027 | 3-tier route table with LRU cache | M5 | Not Started | TASK-005, TASK-014, TASK-021, TASK-024, TASK-025, TASK-026 |
113113
| TASK-028 | Routing-semantics regression gate | M5 | Not Started | TASK-027 |
114114
| TASK-029 | Naming consistency — `stop_and_wait`, `block_ip`/`unblock_ip` | M5 | Not Started | TASK-014 |

specs/unworked_review_issues/2026-05-10_195020_task-026.md

Lines changed: 129 additions & 0 deletions
Large diffs are not rendered by default.

src/httpserver/detail/lambda_resource.hpp

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,19 @@
1818
USA
1919
*/
2020

21-
// TASK-025: dispatch shim used by webserver::on_method_ to slot lambda
22-
// handlers into the existing v1-shaped route table.
21+
// TASK-025/TASK-026: dispatch shim used by webserver::on_methods_ to
22+
// slot lambda handlers into the existing v1-shaped route table.
2323
//
2424
// The shim is a sub-class of http_resource that holds one slot per
2525
// http_method enumerator. Its render_* virtuals look up the slot for
2626
// the dispatched method and invoke it. The shim starts with EVERY
27-
// method disallowed (`disallow_all()`); each on_* call enables exactly
28-
// the matching bit via `set_allowing(method, true)`. The existing
29-
// finalize_answer dispatch glue therefore returns 405 for unregistered
30-
// methods automatically — no edit to webserver.cpp's dispatch path is
31-
// needed.
27+
// method disallowed (`disallow_all()`); each on_*/route call enables
28+
// exactly the matching bits via `set_allowing(method, true)`. The
29+
// existing finalize_answer dispatch glue therefore returns 405 for
30+
// unregistered methods automatically — no edit to webserver.cpp's
31+
// dispatch path is needed.
3232
//
33-
// `final` is intentional: the conflict check in webserver::on_method_
33+
// `final` is intentional: the conflict check in webserver::on_methods_
3434
// uses dynamic_pointer_cast<lambda_resource>(...) to distinguish
3535
// lambda-owned routes from class-owned routes. A subclass would hide
3636
// in that test and break the invariant.
@@ -77,7 +77,7 @@ class lambda_resource final : public ::httpserver::http_resource {
7777

7878
// Install (or replace) the slot for `method`. Caller must have
7979
// already verified that no slot is currently set for `method`
80-
// (webserver::on_method_ enforces this and throws on conflict).
80+
// (webserver::on_methods_ enforces this and throws on conflict).
8181
void set_slot(http_method method, lambda_handler h) {
8282
slots_[static_cast<std::size_t>(method)] = std::move(h);
8383
set_allowing(method, true);

src/httpserver/webserver.hpp

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,59 @@ class webserver {
227227
void on_head(const std::string& path,
228228
std::function<http_response(const http_request&)> handler);
229229

230+
/**
231+
* Generic table-driven lambda registration.
232+
*
233+
* `on_get`, `on_post`, ... are the preferred call-site form when
234+
* the HTTP method is known statically; `route()` is the escape
235+
* hatch for cases where the method is a runtime value
236+
* (config-driven route tables, programmatic registration loops).
237+
*
238+
* Both forms share the same internal registration path: a single
239+
* `route(http_method, path, h)` is exactly equivalent to the
240+
* matching `on_*(path, h)`, and `route(method_set, path, h)` is
241+
* exactly equivalent to one `on_*` call per set bit, applied
242+
* atomically -- if any one of the bits would conflict with an
243+
* existing registration, no slot is mutated and the call throws.
244+
*
245+
* Throws std::invalid_argument if @p handler is empty, if @p m
246+
* is `http_method::count_` (sentinel), if the path conflicts
247+
* with single_resource mode, if a class-based resource is
248+
* already registered at the path, or if a lambda is already
249+
* registered for any of the requested methods on this path.
250+
*
251+
* @param m HTTP method to register the handler under.
252+
* @param path URL path; may be parameterized as /foo/{id}.
253+
* @param handler invoked per request; returns http_response by value.
254+
**/
255+
void route(http_method m,
256+
const std::string& path,
257+
std::function<http_response(const http_request&)> handler);
258+
259+
/**
260+
* Multi-method form of route(): register the same handler for
261+
* every method bit set in @p methods, atomically (all-or-nothing).
262+
*
263+
* Useful when one handler should serve more than one method
264+
* (e.g., GET and HEAD on the same path). Equivalent to one
265+
* `on_*` call per set bit, but if any one of those would conflict
266+
* with an existing registration, no slot is mutated and the call
267+
* throws std::invalid_argument.
268+
*
269+
* Throws std::invalid_argument if @p methods is empty, if
270+
* @p handler is empty, if the path conflicts with single_resource
271+
* mode, if a class-based resource is already registered at the
272+
* path, or if a lambda is already registered for any of the
273+
* requested methods on this path.
274+
*
275+
* @param methods bitmask of methods to register the handler for.
276+
* @param path URL path; may be parameterized as /foo/{id}.
277+
* @param handler invoked per request; returns http_response by value.
278+
**/
279+
void route(method_set methods,
280+
const std::string& path,
281+
std::function<http_response(const http_request&)> handler);
282+
230283
/**
231284
* Unregister an exact-match (register_path) registration.
232285
* No-op if no exact registration exists at @p path.
@@ -433,18 +486,23 @@ class webserver {
433486
// registration of the requested kind.
434487
void unregister_impl_(const std::string& path, bool family);
435488

436-
// TASK-025: shared lambda-registration helper. Builds-or-merges a
437-
// hidden detail::lambda_resource shim at @p path, sets the @p method
438-
// bit on it, and stores @p handler into that method's slot. All
439-
// seven public on_* overloads forward to this single entry point so
440-
// the merge-and-conflict logic lives in one place. Throws
441-
// std::invalid_argument if @p handler is empty, if the path
442-
// conflicts with single_resource mode, if a class-based resource
443-
// is already registered at the path, or if a lambda is already
444-
// registered for (method, path).
445-
void on_method_(http_method method,
446-
const std::string& path,
447-
std::function<http_response(const http_request&)> handler);
489+
// TASK-025/TASK-026: shared lambda-registration helper. Builds-or-
490+
// merges a hidden detail::lambda_resource shim at @p path, sets every
491+
// bit in @p methods on it, and stores @p handler into each of those
492+
// method slots. All seven public on_* overloads and both public
493+
// route() overloads forward to this single entry point so the
494+
// merge-and-conflict logic lives in one place. Validation is
495+
// atomic: if any requested method already has a slot on the path,
496+
// no slot is mutated and the call throws -- callers therefore see
497+
// either a fully-installed registration or no change at all.
498+
// Throws std::invalid_argument if @p methods is empty, if @p
499+
// handler is empty, if the path conflicts with single_resource
500+
// mode, if a class-based resource is already registered at the
501+
// path, or if a lambda is already registered for any requested
502+
// (method, path).
503+
void on_methods_(method_set methods,
504+
const std::string& path,
505+
std::function<http_response(const http_request&)> handler);
448506

449507
// PIMPL: backend-coupled state (MHD daemon, pthread mutexes, route
450508
// table, ban set, route cache, websocket registry, GnuTLS SNI cache,

src/webserver.cpp

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -306,40 +306,48 @@ void webserver::register_resource(const std::string& resource,
306306
register_path(resource, std::move(res));
307307
}
308308

309-
// TASK-025: lambda registration plumbing.
309+
// TASK-025/TASK-026: lambda registration plumbing.
310310
//
311-
// All seven public on_* overloads forward to on_method_, which:
312-
// 1. Validates the handler and the path (single_resource constraints).
311+
// All seven public on_* overloads and both public route() overloads
312+
// forward to on_methods_, which:
313+
// 1. Validates the handler, the method set (non-empty, no count_
314+
// sentinel bit), and the path (single_resource constraints).
313315
// 2. Looks up any existing entry at the path. If it's a class-based
314316
// http_resource, throw -- lambda and class registrations cannot
315-
// share a path. If it's an existing lambda_resource shim, merge by
316-
// checking the per-method slot is empty (else throw) and writing
317-
// the new handler into that slot.
317+
// share a path. If it's an existing lambda_resource shim, check
318+
// that EVERY requested method slot is empty before mutating any
319+
// of them (atomic all-or-nothing); otherwise throw.
318320
// 3. If no entry exists, build a fresh lambda_resource shim and
319321
// insert it into the same three storage maps used by
320322
// register_impl_ (master ordered map; the str fast-path map iff
321323
// exact non-parameterized; the regex map iff parameterized).
322-
// 4. Invalidate the LRU route cache.
324+
// 4. Write @p handler into each requested method slot.
325+
// 5. Invalidate the LRU route cache.
323326
//
324327
// The dispatch path in finalize_answer is not modified: it already
325328
// looks up via shared_ptr<http_resource>, calls is_allowed(method)
326329
// gating the 405 path, then dispatches via the per-method member-
327330
// function pointer set in answer_to_connection. The lambda_resource
328331
// shim's render_* overrides invoke the stored slot.
329-
void webserver::on_method_(http_method method,
330-
const std::string& path,
331-
std::function<http_response(const http_request&)> handler) {
332+
void webserver::on_methods_(method_set methods,
333+
const std::string& path,
334+
std::function<http_response(const http_request&)> handler) {
335+
if (methods.bits == 0u) {
336+
throw std::invalid_argument(
337+
"route(method_set, ...) requires at least one method bit set");
338+
}
332339
if (!handler) {
333340
throw std::invalid_argument(
334-
"The handler function passed to on_* must be non-empty");
341+
"The handler function passed to on_*/route must be non-empty");
335342
}
336343

337344
// Same single-resource constraint as register_path: only "" or "/"
338-
// is acceptable, and the matching mode must be exact (which on_* is).
345+
// is acceptable, and the matching mode must be exact (which on_*/
346+
// route are).
339347
if (single_resource && path != "" && path != "/") {
340348
throw std::invalid_argument(
341-
"When using a single_resource server, on_* requires the "
342-
"path to be '' or '/'");
349+
"When using a single_resource server, on_*/route requires "
350+
"the path to be '' or '/'");
343351
}
344352

345353
detail::http_endpoint idx(path, /*family=*/false,
@@ -360,20 +368,38 @@ void webserver::on_method_(http_method method,
360368
if (!shim) {
361369
throw std::invalid_argument(
362370
"A non-lambda http_resource is already registered at "
363-
"this path; on_* cannot share a path with "
371+
"this path; on_*/route cannot share a path with "
364372
"register_path/register_prefix");
365373
}
366-
if (shim->has_slot(method)) {
367-
throw std::invalid_argument(
368-
"A handler is already registered for this method on "
369-
"this path");
374+
// Atomicity pre-check: every requested slot must be empty
375+
// BEFORE we mutate any of them. Iterates in enum order (get,
376+
// head, post, ...) matching the `Allow:` header serialization.
377+
for (std::uint8_t i = 0;
378+
i < static_cast<std::uint8_t>(http_method::count_);
379+
++i) {
380+
auto m = static_cast<http_method>(i);
381+
if (!methods.contains(m)) continue;
382+
if (shim->has_slot(m)) {
383+
throw std::invalid_argument(
384+
"A handler is already registered for one of the "
385+
"requested methods on this path");
386+
}
370387
}
371388
} else {
372389
shim = std::make_shared<detail::lambda_resource>();
373390
fresh = true;
374391
}
375392

376-
shim->set_slot(method, std::move(handler));
393+
// Commit phase: install the handler into every requested slot.
394+
// The shared std::function copies cheaply (type-erased callable),
395+
// so each slot owns its own copy.
396+
for (std::uint8_t i = 0;
397+
i < static_cast<std::uint8_t>(http_method::count_);
398+
++i) {
399+
auto m = static_cast<http_method>(i);
400+
if (!methods.contains(m)) continue;
401+
shim->set_slot(m, handler);
402+
}
377403

378404
if (fresh) {
379405
impl_->registered_resources.insert({idx, shim});
@@ -392,42 +418,64 @@ void webserver::on_method_(http_method method,
392418

393419
// The seven named forwarders below are the only place that maps the
394420
// method name to its http_method enum constant. Each is a thin alias
395-
// for on_method_; all validation and insertion logic lives there.
421+
// for on_methods_; all validation and insertion logic lives there.
396422
// on_delete uses http_method::del because `delete` is a C++ keyword;
397423
// the wire token is "DELETE" (see http_method::to_string).
398424
void webserver::on_get(const std::string& path,
399425
std::function<http_response(const http_request&)> handler) {
400-
on_method_(http_method::get, path, std::move(handler));
426+
on_methods_(method_set{}.set(http_method::get), path, std::move(handler));
401427
}
402428

403429
void webserver::on_post(const std::string& path,
404430
std::function<http_response(const http_request&)> handler) {
405-
on_method_(http_method::post, path, std::move(handler));
431+
on_methods_(method_set{}.set(http_method::post), path, std::move(handler));
406432
}
407433

408434
void webserver::on_put(const std::string& path,
409435
std::function<http_response(const http_request&)> handler) {
410-
on_method_(http_method::put, path, std::move(handler));
436+
on_methods_(method_set{}.set(http_method::put), path, std::move(handler));
411437
}
412438

413439
void webserver::on_delete(const std::string& path,
414440
std::function<http_response(const http_request&)> handler) {
415-
on_method_(http_method::del, path, std::move(handler));
441+
on_methods_(method_set{}.set(http_method::del), path, std::move(handler));
416442
}
417443

418444
void webserver::on_patch(const std::string& path,
419445
std::function<http_response(const http_request&)> handler) {
420-
on_method_(http_method::patch, path, std::move(handler));
446+
on_methods_(method_set{}.set(http_method::patch), path, std::move(handler));
421447
}
422448

423449
void webserver::on_options(const std::string& path,
424450
std::function<http_response(const http_request&)> handler) {
425-
on_method_(http_method::options, path, std::move(handler));
451+
on_methods_(method_set{}.set(http_method::options), path, std::move(handler));
426452
}
427453

428454
void webserver::on_head(const std::string& path,
429455
std::function<http_response(const http_request&)> handler) {
430-
on_method_(http_method::head, path, std::move(handler));
456+
on_methods_(method_set{}.set(http_method::head), path, std::move(handler));
457+
}
458+
459+
// TASK-026: generic table-driven entry points. The single-method form
460+
// rejects http_method::count_ explicitly because the public route()
461+
// overload accepts a runtime value (and so the sentinel is reachable);
462+
// the on_* forwarders never pass count_, so the on_methods_ helper
463+
// itself does not guard against it.
464+
void webserver::route(http_method m,
465+
const std::string& path,
466+
std::function<http_response(const http_request&)> handler) {
467+
if (m == http_method::count_) {
468+
throw std::invalid_argument(
469+
"http_method::count_ is a sentinel and may not be "
470+
"registered as a route");
471+
}
472+
on_methods_(method_set{}.set(m), path, std::move(handler));
473+
}
474+
475+
void webserver::route(method_set methods,
476+
const std::string& path,
477+
std::function<http_response(const http_request&)> handler) {
478+
on_methods_(methods, path, std::move(handler));
431479
}
432480

433481
#ifdef HAVE_WEBSOCKET

0 commit comments

Comments
 (0)