libs/http/include/boost/http/server/route_handler.hpp

92.9% Lines (26/28) 91.7% Functions (11/12) 83.3% Branches (5/6)
libs/http/include/boost/http/server/route_handler.hpp
Line Branch Hits Source Code
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 http://www.boost.org/LICENSE_1_0.txt)
6 //
7 // Official repository: https://github.com/cppalliance/http
8 //
9
10 #ifndef BOOST_HTTP_SERVER_ROUTE_HANDLER_HPP
11 #define BOOST_HTTP_SERVER_ROUTE_HANDLER_HPP
12
13 #include <boost/http/detail/config.hpp>
14 #include <boost/http/method.hpp>
15 #include <boost/http/detail/except.hpp>
16 #include <boost/http/datastore.hpp>
17 #include <boost/http/request.hpp>
18 #include <boost/http/response.hpp>
19 #include <boost/core/detail/string_view.hpp>
20 #include <boost/capy/buffers.hpp>
21 #include <boost/capy/buffers/make_buffer.hpp>
22 #include <boost/capy/io_result.hpp>
23 #include <boost/capy/io_task.hpp>
24 #include <boost/capy/task.hpp>
25 #include <boost/capy/write.hpp>
26 #include <boost/capy/io/any_buffer_source.hpp>
27 #include <boost/capy/io/any_buffer_sink.hpp>
28 #include <boost/url/url_view.hpp>
29 #include <boost/system/error_category.hpp>
30 #include <boost/system/error_code.hpp>
31 #include <concepts>
32 #include <exception>
33 #include <memory>
34 #include <span>
35 #include <string>
36 #include <type_traits>
37 #include <utility>
38 #include <vector>
39
40 namespace boost {
41 namespace http {
42
43 /** Directive values for route handler results.
44
45 These values indicate how the router should proceed
46 after a handler completes. Handlers return one of
47 the predefined constants (@ref route_done, @ref route_next,
48 @ref route_next_route, @ref route_close) or an error code.
49
50 @see route_result, route_task
51 */
52 enum class route_what
53 {
54 /// Handler completed successfully, response was sent
55 done,
56
57 /// Handler declined, try next handler in the route
58 next,
59
60 /// Handler declined, skip to next matching route
61 next_route,
62
63 /// Handler requests connection closure
64 close,
65
66 /// Handler encountered an error
67 error
68 };
69
70 //------------------------------------------------
71
72 /** The result type returned by route handlers.
73
74 This class represents the outcome of a route handler.
75 Handlers return this type to indicate how the router
76 should proceed. Construct from a directive constant
77 or an error code:
78
79 @code
80 route_task my_handler(route_params& p)
81 {
82 if(! authorized(p))
83 co_return route_next; // try next handler
84
85 if(auto ec = process(p); ec)
86 co_return ec; // return error
87
88 co_return route_done; // success
89 }
90 @endcode
91
92 @par Checking Results
93
94 Use @ref what() to determine the directive, and
95 @ref error() to retrieve any error code:
96
97 @code
98 route_result rv = co_await handler(p);
99 if(rv.what() == route_what::error)
100 handle_error(rv.error());
101 @endcode
102
103 @see route_task, route_what, route_done, route_next
104 */
105 class BOOST_HTTP_DECL
106 route_result
107 {
108 system::error_code ec_;
109
110 template<route_what T>
111 struct what_t {};
112
113 route_result(system::error_code ec);
114 void set(route_what w);
115
116 public:
117 59 route_result() = default;
118
119 /** Construct from a directive constant.
120
121 This constructor allows implicit conversion from
122 the predefined constants (@ref route_done, @ref route_next,
123 @ref route_next_route, @ref route_close).
124
125 @code
126 route_task handler(route_params& p)
127 {
128 co_return route_done; // implicitly converts
129 }
130 @endcode
131 */
132 template<route_what W>
133 244 route_result(what_t<W>)
134 244 {
135 static_assert(W != route_what::error);
136 244 set(W);
137 244 }
138
139 /** Return the directive for this result.
140
141 Call this to determine how the router should proceed:
142
143 @code
144 route_result rv = co_await handler(p);
145 switch(rv.what())
146 {
147 case route_what::done:
148 // response sent, done with request
149 break;
150 case route_what::next:
151 // try next handler
152 break;
153 case route_what::error:
154 log_error(rv.error());
155 break;
156 }
157 @endcode
158
159 @return The directive value.
160 */
161 auto
162 what() const noexcept ->
163 route_what;
164
165 /** Return the error code, if any.
166
167 If @ref what() returns `route_what::error`, this
168 returns the underlying error code. Otherwise returns
169 a default-constructed (non-failing) error code.
170
171 @return The error code, or a non-failing code.
172 */
173 auto
174 error() const noexcept ->
175 system::error_code;
176
177 /** Return true if the result indicates an error.
178
179 @return `true` if @ref what() equals `route_what::error`.
180 */
181 66 bool failed() const noexcept
182 {
183 66 return what() == route_what::error;
184 }
185
186 static constexpr route_result::what_t<route_what::done> route_done{};
187 static constexpr route_result::what_t<route_what::next> route_next{};
188 static constexpr route_result::what_t<route_what::next_route> route_next_route{};
189 static constexpr route_result::what_t<route_what::close> route_close{};
190 friend route_result route_error(system::error_code ec) noexcept;
191
192 template<class E>
193 friend auto route_error(E e) noexcept ->
194 std::enable_if_t<
195 system::is_error_code_enum<E>::value,
196 route_result>;
197 };
198
199 //------------------------------------------------
200
201 /** Handler completed successfully.
202
203 Return this from a handler to indicate the response
204 was sent and the request is complete:
205
206 @code
207 route_task handler(route_params& p)
208 {
209 p.res.set(field::content_type, "text/plain");
210 co_await p.send("Hello, World!");
211 co_return route_done;
212 }
213 @endcode
214 */
215 inline constexpr decltype(auto) route_done = route_result::route_done;
216
217 /** Handler declined, try next handler.
218
219 Return this from a handler to decline processing
220 and allow the next handler in the route to try:
221
222 @code
223 route_task auth_handler(route_params& p)
224 {
225 if(! p.req.exists(field::authorization))
226 co_return route_next; // let another handler try
227
228 // process authenticated request...
229 co_return route_done;
230 }
231 @endcode
232 */
233 inline constexpr decltype(auto) route_next = route_result::route_next;
234
235 /** Handler declined, skip to next route.
236
237 Return this from a handler to skip all remaining
238 handlers in the current route and proceed to the
239 next matching route:
240
241 @code
242 route_task version_check(route_params& p)
243 {
244 if(p.req.version() < 11)
245 co_return route_next_route; // skip this route
246
247 co_return route_next; // continue with this route
248 }
249 @endcode
250 */
251 inline constexpr decltype(auto) route_next_route = route_result::route_next_route;
252
253 /** Handler requests connection closure.
254
255 Return this from a handler to immediately close
256 the connection without sending a response:
257
258 @code
259 route_task ban_check(route_params& p)
260 {
261 if(is_banned(p.req.remote_address()))
262 co_return route_close; // drop connection
263
264 co_return route_next;
265 }
266 @endcode
267 */
268 inline constexpr decltype(auto) route_close = route_result::route_close;
269
270 /** Construct from an error code.
271
272 Use this constructor to return an error from a handler.
273 The error code must represent a failure condition.
274
275 @param ec The error code to return.
276
277 @throw std::invalid_argument if `!ec` (non-failing code).
278 */
279 33 inline route_result route_error(system::error_code ec) noexcept
280 {
281 33 return route_result(ec);
282 }
283
284 /** Construct from an error enum.
285
286 Use this overload to return an error from a handler
287 using any type satisfying `is_error_code_enum`.
288
289 @param e The error enum value to return.
290 */
291 template<class E>
292 12 auto route_error(E e) noexcept ->
293 std::enable_if_t<
294 system::is_error_code_enum<E>::value,
295 route_result>
296 {
297 12 return route_result(make_error_code(e));
298 }
299
300 //------------------------------------------------
301
302 /** Convenience alias for route handler return type.
303
304 Route handlers are coroutines that return a @ref route_result
305 indicating how the router should proceed. This alias simplifies
306 handler declarations:
307
308 @code
309 route_task my_handler(route_params& p)
310 {
311 // process request...
312 co_return route_done;
313 }
314
315 route_task auth_middleware(route_params& p)
316 {
317 if(! check_token(p))
318 {
319 p.res.set_status(status::unauthorized);
320 co_await p.send();
321 co_return route_done;
322 }
323 co_return route_next; // continue to next handler
324 }
325 @endcode
326
327 @see route_result, route_params
328 */
329 using route_task = capy::task<route_result>;
330
331 //------------------------------------------------
332
333 template<class, class> class router;
334
335 namespace detail {
336
337 struct route_params_access;
338 class router_base;
339
340 struct route_params_base_privates
341 {
342 std::string verb_str_;
343 std::string decoded_path_;
344 system::error_code ec_;
345 std::exception_ptr ep_;
346 std::size_t pos_ = 0;
347 std::size_t resume_ = 0;
348 http::method verb_ =
349 http::method::unknown;
350 bool addedSlash_ = false;
351 bool case_sensitive = false;
352 bool strict = false;
353 char kind_ = 0;
354 };
355
356 } // detail
357
358 //------------------------------------------------
359
360 /** Parameters object for HTTP route handlers.
361
362 This structure holds all the context needed for a route
363 handler to process an HTTP request and generate a response.
364
365 @par Example
366 @code
367 route_task my_handler(route_params& p)
368 {
369 p.res.set(field::content_type, "text/plain");
370 co_await p.send("Hello, World!");
371 co_return route_done;
372 }
373 @endcode
374
375 @see route_task, route_result
376 */
377 class BOOST_HTTP_SYMBOL_VISIBLE
378 route_params
379 {
380 detail::route_params_base_privates priv_;
381
382 public:
383 struct match_result;
384
385 /** Return true if the request method matches `m`
386 */
387 bool is_method(
388 http::method m) const noexcept
389 {
390 return priv_.verb_ == m;
391 }
392
393 /** Return true if the request method matches `s`
394 */
395 BOOST_HTTP_DECL
396 bool is_method(
397 core::string_view s) const noexcept;
398
399 /** The mount path of the current router
400
401 This is the portion of the request path
402 which was matched to select the handler.
403 The remaining portion is available in
404 @ref path.
405 */
406 core::string_view base_path;
407
408 /** The current pathname, relative to the base path
409 */
410 core::string_view path;
411
412 /** Captured route parameters
413
414 Contains name-value pairs extracted from the path
415 by matching :param and *wildcard tokens.
416 */
417 std::vector<std::pair<std::string, std::string>> params;
418
419 /// The complete request target
420 urls::url_view url;
421
422 /// The HTTP request
423 http::request req;
424
425 /// The HTTP response
426 http::response res;
427
428 /// Provides access to the request body
429 capy::any_buffer_source req_body;
430
431 /// Provides access to the response body
432 capy::any_buffer_sink res_body;
433
434 /// Arbitrary per-route data
435 http::datastore route_data;
436
437 /// Arbitrary per-session data
438 http::datastore session_data;
439
440 BOOST_HTTP_DECL ~route_params();
441 BOOST_HTTP_DECL void reset();
442 BOOST_HTTP_DECL route_params& status(http::status code);
443
444 /** Send the response with an optional body.
445 */
446 BOOST_HTTP_DECL capy::io_task<> send(std::string_view body = {});
447
448 private:
449 template<class, class>
450 friend class router;
451 friend class detail::router_base;
452 friend struct detail::route_params_access;
453
454 route_params& operator=(
455 route_params const&) = delete;
456 };
457
458 struct route_params::
459 match_result
460 {
461 std::vector<std::pair<std::string, std::string>> params_;
462
463 297 void adjust_path(
464 route_params& p,
465 std::size_t n)
466 {
467 297 n_ = n;
468
2/2
✓ Branch 0 taken 133 times.
✓ Branch 1 taken 164 times.
297 if(n_ == 0)
469 133 return;
470 164 p.base_path = {
471 p.base_path.data(),
472 164 p.base_path.size() + n_ };
473
2/2
✓ Branch 1 taken 49 times.
✓ Branch 2 taken 115 times.
164 if(n_ < p.path.size())
474 {
475 49 p.path.remove_prefix(n_);
476 }
477 else
478 {
479 // append a soft slash
480 115 p.path = { p.priv_.decoded_path_.data() +
481 115 p.priv_.decoded_path_.size() - 1, 1};
482
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 115 times.
115 BOOST_ASSERT(p.path == "/");
483 }
484 }
485
486 void restore_path(
487 route_params& p)
488 {
489 if( n_ > 0 &&
490 p.priv_.addedSlash_ &&
491 p.path.data() ==
492 p.priv_.decoded_path_.data() +
493 p.priv_.decoded_path_.size() - 1)
494 {
495 // remove soft slash
496 p.path = {
497 p.base_path.data() +
498 p.base_path.size(), 0 };
499 }
500 p.base_path.remove_suffix(n_);
501 p.path = {
502 p.path.data() - n_,
503 p.path.size() + n_ };
504 }
505
506 private:
507 std::size_t n_ = 0; // chars moved from path to base_path
508 };
509
510 //------------------------------------------------
511
512 namespace detail {
513
514 template<class H, class... Args>
515 concept returns_route_task = std::same_as<
516 std::invoke_result_t<H, Args...>, route_task>;
517
518 struct route_params_access
519 {
520 route_params& rp;
521
522 739 route_params_base_privates& operator*() const noexcept
523 {
524 739 return rp.priv_;
525 }
526
527 122 route_params_base_privates* operator->() const noexcept
528 {
529 122 return &rp.priv_;
530 }
531 };
532
533 } // detail
534
535 } // http
536 } // boost
537
538 #endif
539