|
| 1 | +// |
| 2 | +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) |
| 3 | +// |
| 4 | +// Distributed under the Boost Software License, Version 1.0. (See accompanying |
| 5 | +// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) |
| 6 | +// |
| 7 | +// Official repository: https://github.com/cppalliance/http_proto |
| 8 | +// |
| 9 | + |
| 10 | += Server |
| 11 | + |
| 12 | +This library provides an Express.js-style request dispatcher for HTTP servers. |
| 13 | +The interface is Sans-I/O: it handles routing and response generation without |
| 14 | +performing network operations. A separate I/O framework such as Boost.Beast2 |
| 15 | +manages connections and drives the protocol. |
| 16 | + |
| 17 | +== Router |
| 18 | + |
| 19 | +cpp:router[] is a class template that implements request routing. It stores a |
| 20 | +collection of routes, each with a path pattern, HTTP method, and one or more |
| 21 | +handlers. Callers (typically a framework) use the router to dispatch an HTTP |
| 22 | +request to a handler. |
| 23 | + |
| 24 | +=== Route Handler |
| 25 | + |
| 26 | +Route handlers have this signature: |
| 27 | +[source,cpp] |
| 28 | +---- |
| 29 | +route_result handler( route_params& rp ); |
| 30 | +---- |
| 31 | + |
| 32 | +After this chapter you can: dispatch HTTP requests to handlers based on method |
| 33 | +and path, chain handlers together, and control request flow. |
| 34 | + |
| 35 | +== Overview |
| 36 | + |
| 37 | +The router is an Express.js-style request dispatcher. You register handlers |
| 38 | +for path patterns and HTTP methods, then dispatch incoming requests. The |
| 39 | +router matches the request against registered routes and invokes the |
| 40 | +appropriate handlers in order. |
| 41 | + |
| 42 | +[source,cpp] |
| 43 | +---- |
| 44 | +#include <boost/http_proto.hpp> |
| 45 | +
|
| 46 | +using namespace boost::http_proto; |
| 47 | +
|
| 48 | +basic_router<route_params> router; |
| 49 | +
|
| 50 | +router.add(method::get, "/hello", |
| 51 | + [](route_params& p) |
| 52 | + { |
| 53 | + p.status(status::ok); |
| 54 | + p.set_body("Hello, world!"); |
| 55 | + return route::send; |
| 56 | + }); |
| 57 | +---- |
| 58 | + |
| 59 | +The library provides `route_params` as the standard parameters type. It |
| 60 | +contains the request, response, URL, and other context needed by handlers. |
| 61 | + |
| 62 | +== Handlers |
| 63 | + |
| 64 | +A handler is any callable that accepts a reference to the params object and |
| 65 | +returns a `route_result`: |
| 66 | + |
| 67 | +[source,cpp] |
| 68 | +---- |
| 69 | +route_result handler(route_params& p); |
| 70 | +---- |
| 71 | + |
| 72 | +The return value tells the router what to do next: |
| 73 | + |
| 74 | +[cols="1,3"] |
| 75 | +|=== |
| 76 | +|Value |Meaning |
| 77 | + |
| 78 | +|`route::send` |
| 79 | +|Response is ready. Send it to the client. |
| 80 | + |
| 81 | +|`route::next` |
| 82 | +|Continue to the next handler in the chain. |
| 83 | + |
| 84 | +|`route::next_route` |
| 85 | +|Skip remaining handlers in this route, try the next route. |
| 86 | + |
| 87 | +|`route::close` |
| 88 | +|Close the connection after sending any response. |
| 89 | + |
| 90 | +|`route::complete` |
| 91 | +|Request fully handled; no response to send. |
| 92 | + |
| 93 | +|`route::detach` |
| 94 | +|Handler took ownership of the session (advanced). |
| 95 | +|=== |
| 96 | + |
| 97 | +Most handlers return `route::send` when they produce a response, or |
| 98 | +`route::next` when they perform setup work and defer to later handlers. |
| 99 | + |
| 100 | +== Adding Routes |
| 101 | + |
| 102 | +Use `add()` to register a handler for a specific HTTP method and path: |
| 103 | + |
| 104 | +[source,cpp] |
| 105 | +---- |
| 106 | +router.add(method::get, "/users", get_users); |
| 107 | +router.add(method::post, "/users", create_user); |
| 108 | +router.add(method::get, "/users/:id", get_user); |
| 109 | +router.add(method::put, "/users/:id", update_user); |
| 110 | +router.add(method::delete_, "/users/:id", delete_user); |
| 111 | +---- |
| 112 | + |
| 113 | +Use `all()` to match any HTTP method: |
| 114 | + |
| 115 | +[source,cpp] |
| 116 | +---- |
| 117 | +router.all("/status", check_status); |
| 118 | +---- |
| 119 | + |
| 120 | +== Fluent Route Interface |
| 121 | + |
| 122 | +The `route()` method returns a fluent interface for registering multiple |
| 123 | +handlers on the same path: |
| 124 | + |
| 125 | +[source,cpp] |
| 126 | +---- |
| 127 | +router.route("/users/:id") |
| 128 | + .add(method::get, get_user) |
| 129 | + .add(method::put, update_user) |
| 130 | + .add(method::delete_, delete_user) |
| 131 | + .all(log_access); |
| 132 | +---- |
| 133 | + |
| 134 | +This is equivalent to calling `add()` separately for each method, but more |
| 135 | +concise when a path has multiple method handlers. |
| 136 | + |
| 137 | +== Dispatching Requests |
| 138 | + |
| 139 | +Call `dispatch()` to route a request: |
| 140 | + |
| 141 | +[source,cpp] |
| 142 | +---- |
| 143 | +route_params p; |
| 144 | +// ... populate p.req, p.url from parsed request ... |
| 145 | +
|
| 146 | +route_result rv = router.dispatch(method::get, p.url, p); |
| 147 | +
|
| 148 | +if(rv == route::send) |
| 149 | +{ |
| 150 | + // p.res contains the response to send |
| 151 | +} |
| 152 | +else if(rv == route::next) |
| 153 | +{ |
| 154 | + // No handler matched; send 404 |
| 155 | +} |
| 156 | +---- |
| 157 | + |
| 158 | +The router tries each matching route in registration order. If a handler |
| 159 | +returns `route::next`, the router continues to the next handler. If all |
| 160 | +handlers return `route::next`, dispatch returns `route::next` to indicate |
| 161 | +no handler produced a response. |
| 162 | + |
| 163 | +== Handler Chaining |
| 164 | + |
| 165 | +Multiple handlers can be registered for the same route. They execute in |
| 166 | +order until one returns something other than `route::next`: |
| 167 | + |
| 168 | +[source,cpp] |
| 169 | +---- |
| 170 | +router.add(method::get, "/admin", |
| 171 | + [](route_params& p) |
| 172 | + { |
| 173 | + // Authentication check |
| 174 | + if(!is_authenticated(p)) |
| 175 | + { |
| 176 | + p.status(status::unauthorized); |
| 177 | + p.set_body("Unauthorized"); |
| 178 | + return route::send; |
| 179 | + } |
| 180 | + return route::next; |
| 181 | + }, |
| 182 | + [](route_params& p) |
| 183 | + { |
| 184 | + // Authorization check |
| 185 | + if(!is_admin(p)) |
| 186 | + { |
| 187 | + p.status(status::forbidden); |
| 188 | + p.set_body("Forbidden"); |
| 189 | + return route::send; |
| 190 | + } |
| 191 | + return route::next; |
| 192 | + }, |
| 193 | + [](route_params& p) |
| 194 | + { |
| 195 | + // Actual handler |
| 196 | + p.status(status::ok); |
| 197 | + p.set_body("Admin panel"); |
| 198 | + return route::send; |
| 199 | + }); |
| 200 | +---- |
| 201 | + |
| 202 | +This pattern separates concerns: authentication, authorization, and business |
| 203 | +logic each have their own handler. |
| 204 | + |
| 205 | +== Path Patterns |
| 206 | + |
| 207 | +Route paths support named parameters and wildcards: |
| 208 | + |
| 209 | +[cols="1,2,2"] |
| 210 | +|=== |
| 211 | +|Pattern |Example URL |Matches |
| 212 | + |
| 213 | +|`/users` |
| 214 | +|`/users` |
| 215 | +|Exact match |
| 216 | + |
| 217 | +|`/users/:id` |
| 218 | +|`/users/42` |
| 219 | +|Named parameter `id` = `"42"` |
| 220 | + |
| 221 | +|`/files/*` |
| 222 | +|`/files/docs/readme.txt` |
| 223 | +|Wildcard suffix |
| 224 | +|=== |
| 225 | + |
| 226 | +Path matching is case-insensitive by default. Use `router_options` to change |
| 227 | +this behavior. |
| 228 | + |
| 229 | +== Router Options |
| 230 | + |
| 231 | +Configure matching behavior when constructing the router: |
| 232 | + |
| 233 | +[source,cpp] |
| 234 | +---- |
| 235 | +basic_router<route_params> router( |
| 236 | + router_options() |
| 237 | + .case_sensitive(true) // Paths are case-sensitive |
| 238 | + .strict(true)); // Trailing slash matters |
| 239 | +---- |
| 240 | + |
| 241 | +[cols="1,1,3"] |
| 242 | +|=== |
| 243 | +|Option |Default |Description |
| 244 | + |
| 245 | +|`case_sensitive` |
| 246 | +|`false` |
| 247 | +|When true, `/Users` and `/users` are different routes. |
| 248 | + |
| 249 | +|`strict` |
| 250 | +|`false` |
| 251 | +|When true, `/api` and `/api/` are different routes. |
| 252 | + |
| 253 | +|`merge_params` |
| 254 | +|`false` |
| 255 | +|When true, inherit parameters from parent routers. |
| 256 | +|=== |
| 257 | + |
| 258 | +== Complete Example |
| 259 | + |
| 260 | +[source,cpp] |
| 261 | +---- |
| 262 | +#include <boost/http_proto.hpp> |
| 263 | +
|
| 264 | +using namespace boost::http_proto; |
| 265 | +
|
| 266 | +int main() |
| 267 | +{ |
| 268 | + basic_router<route_params> router; |
| 269 | +
|
| 270 | + // Health check endpoint |
| 271 | + router.add(method::get, "/health", |
| 272 | + [](route_params& p) |
| 273 | + { |
| 274 | + p.status(status::ok); |
| 275 | + p.set_body("OK"); |
| 276 | + return route::send; |
| 277 | + }); |
| 278 | +
|
| 279 | + // API routes |
| 280 | + router.route("/api/echo") |
| 281 | + .add(method::post, |
| 282 | + [](route_params& p) |
| 283 | + { |
| 284 | + p.status(status::ok); |
| 285 | + // Echo back the request body |
| 286 | + return route::send; |
| 287 | + }) |
| 288 | + .add(method::get, |
| 289 | + [](route_params& p) |
| 290 | + { |
| 291 | + p.status(status::method_not_allowed); |
| 292 | + return route::send; |
| 293 | + }); |
| 294 | +
|
| 295 | + // Dispatch a request |
| 296 | + route_params p; |
| 297 | + auto rv = router.dispatch( |
| 298 | + method::get, |
| 299 | + urls::url_view("/health"), |
| 300 | + p); |
| 301 | +
|
| 302 | + // rv == route::send, p.res contains "OK" |
| 303 | +} |
| 304 | +---- |
| 305 | + |
| 306 | +== See Also |
| 307 | + |
| 308 | +* xref:server/middleware.adoc[Middleware] - Path-based handler chains |
| 309 | +* xref:server/errors.adoc[Error Handling] - Error and exception handlers |
| 310 | +* xref:server/params.adoc[Route Parameters] - The `route_params` object |
0 commit comments