Routing¶
A Router maps a method and a path to a handler. Build it before creating the server, then hand it over:
#include <utility>
Router router;
router.setPath(http::Method::GET, "/hello", [](const HttpRequestView&) { return HttpResponse(200).body("hello\n"); });
router.setPath(http::Method::GET | http::Method::POST, "/echo", [](const HttpRequestView& req) {
return HttpResponse(200).body(req.body());
});
SingleHttpServer server(HttpServerConfig{}.withPort(8080), std::move(router));
server.run();
Handlers¶
A route takes one of three kinds of handlers, all registered with setPath():
| Handler | Signature | Use it when |
|---|---|---|
| Synchronous | HttpResponse(const HttpRequestView&) |
The response is computed quickly, without blocking. The common case. |
| Streaming | void(const HttpRequestView&, HttpResponseWriter&) |
The body is too large to build in memory, and is written piece by piece. See Streaming responses. |
| Async | RequestTask<HttpResponse>(HttpRequestView&) |
The handler waits for a database, another service, or any blocking call. See Async handlers. |
All handlers of a server run on its event-loop thread: a handler that blocks delays every other connection of that thread.
Registering routes¶
setPath(methods, path, handler)registers a handler for one method or a set of methods, combined with|. Each method of a path has its own handler: registeringPOSTon a path that has aGEThandler keeps both, and registering the same method again replaces its handler.setDefault(handler)registers a fallback for requests that match no path. A streaming fallback can be registered next to a synchronous one; see Dispatch precedence.- A request whose path matches no route, without fallback, receives
404 Not Found. A request whose path exists but not for its method receives405 Method Not Allowed, with anAllowheader. HEADrequests use theGEThandler when noHEADhandler is registered; the server sends the headers without the body.OPTIONS(CORS preflight,OPTIONS *),TRACE, andCONNECTare handled by the server, see HTTP/1.1 conformance.
setPath() returns the route entry, to attach per-route options (see Per-route options).
Path patterns¶
A path starts with / and is split into segments at each /. Empty segments (//) are not allowed. A segment is a literal, a pattern mixing literals and parameters, or a final wildcard.
| Pattern | Matches | Parameters |
|---|---|---|
/users/{id} |
/users/42 |
id = 42 |
/users/{id}/posts/{post} |
/users/42/posts/7 |
id = 42, post = 7 |
/api/v{}/search-{} |
/api/v3/search-books |
0 = 3, 1 = books |
/users/{id:[0-9]+} |
/users/42, not /users/me |
id = 42 |
/assets/* |
/assets/app.js, /assets/css/site.css |
none |
/api/{{version}}/data |
/api/{version}/data |
none |
Parameters. {name} captures part of a segment under name. {} captures it under a numeric key, "0", "1", and so on in order. A path uses either named or unnamed parameters, not both. A segment may mix literals and parameters (foo{}bar), but two parameters must be separated by a literal.
Constraints. {name:pattern} only matches values accepted by a regular expression. Simple character classes with repetitions, such as [0-9]+, \d+, or [a-zA-Z0-9_-]{3,32}, use a fast built-in matcher; other expressions (groups, alternation, anchors) fall back to std::regex. Constraints are compiled when the route is registered, and an invalid one throws std::regex_error. A value that fails a constraint lets matching continue with the other routes.
Wildcard. A final segment that is exactly * matches any remaining suffix, slashes included. Elsewhere, and inside a longer segment (seg*), * is a literal asterisk. The suffix matched by the wildcard is not exposed as a parameter: read it from req.path().
Literal braces. {{ and }} stand for literal { and }.
setPath() throws std::invalid_argument on an invalid pattern: a path not starting with /, an empty segment, an unterminated {, two consecutive parameters, an invalid parameter name, mixed named and unnamed parameters, or a pattern conflicting with an existing wildcard route. Registering a streaming handler for a method that already has a synchronous one, or the reverse, throws std::logic_error.
Matching order¶
When several routes match a path, the most specific wins:
- a literal segment, over
- a parameter with a constraint, over
- a parameter without constraint, over
- the final wildcard.
/users/me is thus served by a /users/me route even when /users/{id} exists. When several constrained parameters accept the same segment, the first registered wins.
Reading parameters¶
#include <string_view>
Router router;
router.setPath(http::Method::GET, "/users/{id:[0-9]+}/posts/{post}", [](const HttpRequestView& req) {
const std::string_view userId = req.pathParamValueOrEmpty("id");
const auto postId = req.pathParamValue("post"); // std::optional, empty if absent
return HttpResponse(200).body(userId);
});
pathParams() returns all of them as a map. Like every view of the request, parameter values point into the connection buffer and are valid until the handler returns: copy them to keep them longer (see Request lifetime).
Trailing slashes¶
RouterConfig::trailingSlashPolicy decides how /foo and /foo/ relate. An exact match is always served first; the policy only applies when the request differs from a registered route by a final slash. The root path / is never rewritten.
| Policy | Request /foo/, route /foo |
Request /foo, route /foo/ |
Routes /foo and /foo/ |
|---|---|---|---|
Normalize (default) |
served by /foo |
served by /foo/ |
one canonical route: the first registration is kept |
Redirect |
301 to /foo |
301 to /foo/ |
one canonical route: the first registration is kept |
Strict |
404 |
404 |
two distinct routes |
RouterConfig routerConfig;
routerConfig.withTrailingSlashPolicy(RouterConfig::TrailingSlashPolicy::Strict);
Router router(routerConfig);
Use Strict for APIs where the two forms mean different resources, Redirect to enforce canonical public URLs, and Normalize to accept both without duplicate registrations.
Dispatch precedence¶
A path can have synchronous and streaming handlers for different methods (for instance a streaming GET and a synchronous POST), but a given method of a path has only one handler: registering a streaming handler for a method that already has a synchronous one throws std::logic_error. For a request, the server picks, in this order:
- the streaming handler of the matched path and method,
- the synchronous or async handler of the matched path and method,
- the streaming fallback (
setDefault(StreamingHandler)), - the synchronous or async fallback.
Route groups¶
router.group(prefix) registers routes under a common prefix, with shared options:
#include <chrono>
Router router;
auto api = router.group("/api/v1");
api.withMaxBodyBytes(64 * 1024).withTimeout(std::chrono::seconds{5});
api.addRequestMiddleware([](HttpRequestView&) { return MiddlewareResult::Continue(); });
api.setPath(http::Method::GET, "/users", [](const HttpRequestView&) { return HttpResponse(200); }); // /api/v1/users
api.setPath(http::Method::POST, "/users", [](const HttpRequestView&) { return HttpResponse(201); }); // /api/v1/users
auto admin = api.group("/admin"); // /api/v1/admin/..., inherits the options and middleware of api
admin.setPath(http::Method::DELETE, "/cache", [](const HttpRequestView&) { return HttpResponse(204); });
A group applies its options when a route is registered through it, so set them before registering routes. Options set on a route afterwards override the group's. A group does not own the router, which must outlive it.
Per-route options¶
The entry returned by setPath() carries options for that route:
#include <chrono>
Router router;
router.setPath(http::Method::POST, "/login", [](const HttpRequestView&) { return HttpResponse(204); })
.maxBodyBytes(4096) // a login form needs far less than the server-wide limit
.timeout(std::chrono::seconds{5})
.before([](HttpRequestView&) { return MiddlewareResult::Continue(); });
| Option | Effect |
|---|---|
maxHeaderBytes(n), maxBodyBytes(n) |
Lower limits for this route. A route can only tighten the server limits (HttpServerConfig::maxHeaderBytes and maxBodyBytes): a larger value is capped to the server limit, with a warning, also after a config update lowering it. A route without its own limit follows the server limit. Set the server limit to the largest value any route needs. |
timeout(duration) |
Deadline for the request, from the start of its head: when it expires, the server answers 408 Request Timeout and closes the connection. Enforced for async and streaming handlers; a synchronous handler cannot be interrupted. |
before(middleware), after(middleware) |
Middleware of this route only, see Middleware. |
cors(policy) |
CORS policy of this route, see CORS. |
http2Enable(mode) |
Force HTTP/2 on or off for this route, regardless of the global setting. |
Updating routes at runtime¶
A router passed to the server constructor is ready when the server starts, which is the simplest model. To change routes while the server runs, use server.router(), which returns a proxy forwarding each change to the event-loop thread, or postRouterUpdate() to apply several changes at once:
SingleHttpServer server(HttpServerConfig{});
auto handle = server.startDetached();
// From any thread:
server.router().setPath(http::Method::GET, "/feature", [](const HttpRequestView&) { return HttpResponse(200); });
server.postRouterUpdate([](Router& router) {
router.setPath(http::Method::GET, "/a", [](const HttpRequestView&) { return HttpResponse(200); });
router.setPath(http::Method::GET, "/b", [](const HttpRequestView&) { return HttpResponse(200); });
});
An update is applied by the event loop at its next iteration (immediately when the server is not running). Requests that already started keep their handler: an async handler suspended during an update completes with the handler it started with.
Tests¶
- Patterns, constraints, wildcard, escapes, trailing slashes, and precedence: router_test.cpp.
- Per-route limits capped by the server limits, and following their changes:
ClampConfigs*in router_test.cpp,GlobalLimitsUpdateAppliesToRoutesandRouteLimitAboveGlobalLimitIsCappedin tests/http-routing_test.cpp. - End-to-end routing,
404/405, and trailing slash redirects: tests/http-routing_test.cpp, and over HTTP/1.1 and HTTP/2: tests/http-protocol-parity_test.cpp.