Coverage Report

Created: 2026-09-14 06:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/glaze/include/glaze/net/http_router.hpp
Line
Count
Source
1
// Glaze Library
2
// For the license information refer to glaze.hpp
3
4
#pragma once
5
6
#include <algorithm>
7
#include <functional>
8
#include <future>
9
#include <iostream>
10
#include <memory>
11
#include <optional>
12
#include <source_location>
13
#include <string>
14
#include <string_view>
15
#include <unordered_map>
16
#include <vector>
17
18
#include "glaze/json/generic.hpp"
19
#include "glaze/net/http.hpp"
20
#include "glaze/net/http_headers.hpp"
21
#include "glaze/net/url.hpp"
22
#include "glaze/util/key_transformers.hpp"
23
24
// To deconflict Windows.h, which defines a DELETE macro that collides with the
25
// http_method::DELETE used in the route helpers below. http.hpp's own undef is
26
// include-guarded, so it does not re-run when a later include (e.g. asio via
27
// http_client.hpp) pulls in Windows.h before this header is parsed.
28
#ifdef DELETE
29
#undef DELETE
30
#endif
31
32
namespace glz
33
{
34
   namespace detail
35
   {
36
      struct request_line
37
      {
38
         http_method method;
39
         std::string_view target;
40
         bool is_http11;
41
      };
42
   }
43
44
   // Request context object
45
   struct request
46
   {
47
      http_method method{};
48
      std::string target{}; // Full request target (path + query string)
49
      std::string path{}; // Path component only (without query string)
50
      std::unordered_map<std::string, std::string> params{}; // Path parameters (e.g., :id)
51
      std::unordered_map<std::string, std::string> query{}; // Query parameters (e.g., ?limit=10)
52
      glz::http_headers headers{};
53
      std::string body{};
54
      std::string remote_ip{};
55
      uint16_t remote_port{};
56
   };
57
58
   // Serialized to the response body when response::body<Opts>(value) fails
59
   // to write `value`. Declared at namespace scope so glaze's reflection can
60
   // name the type; function-local types are not reflectable on GCC.
61
   struct glz_write_error
62
   {
63
      std::string_view error{"glaze write failure"};
64
      uint32_t code{};
65
      std::string_view message{};
66
   };
67
68
   // Response builder
69
   struct response
70
   {
71
      enum header_flag : uint8_t {
72
         has_content_length = 1,
73
         has_date = 2,
74
         has_server = 4,
75
         has_connection = 8,
76
      };
77
78
      int status_code = 200;
79
      glz::http_headers response_headers{};
80
      std::string response_body{};
81
      uint8_t user_headers_set{};
82
83
      inline response& status(int code)
84
0
      {
85
0
         status_code = code;
86
0
         return *this;
87
0
      }
88
89
      // Replaces any existing field with this name.
90
      //
91
      // A field-name or field-value carrying CR or LF would terminate the field on
92
      // the wire, letting attacker-influenced data inject extra headers or a body
93
      // (CWE-113); header_field_has_crlf carries the full rationale. Rejecting it
94
      // here keeps it out of the container and, crucially, skips the
95
      // mark_user_supplied bookkeeping below - see that function for why the order
96
      // of these two lines is load-bearing. The wire serializers keep an
97
      // independent drop as a backstop for fields that bypass this setter.
98
      inline response& header(std::string_view name, std::string_view value)
99
0
      {
100
0
         if (header_field_has_crlf(name, value)) [[unlikely]] {
101
0
            return *this;
102
0
         }
103
0
104
0
         mark_user_supplied(name);
105
0
         response_headers.set(std::string(name), std::string(value));
106
0
         return *this;
107
0
      }
108
109
      // Appends instead of replacing, for names that can repeat like Set-Cookie.
110
      // Content-Length and Transfer-Encoding are replaced regardless: a second one leaves
111
      // the body length ambiguous and opens response smuggling (RFC 9112 6.3).
112
      inline response& add_header(std::string_view name, std::string_view value)
113
0
      {
114
0
         if (header_field_has_crlf(name, value)) [[unlikely]] {
115
0
            return *this;
116
0
         }
117
0
118
0
         mark_user_supplied(name);
119
0
120
0
         if (header_field_frames_body(name)) [[unlikely]] {
121
0
            response_headers.set(std::string(name), std::string(value));
122
0
         }
123
0
         else {
124
0
            response_headers.add(std::string(name), std::string(value));
125
0
         }
126
0
         return *this;
127
0
      }
128
129
      inline response& body(std::string_view content)
130
0
      {
131
0
         response_body.assign(content.data(), content.size());
132
0
         return *this;
133
0
      }
134
135
      // Use glz::opts for format deduction and serialization
136
      // my_response.res<Opts>(value);
137
      template <auto Opts, class T>
138
      response& body(T&& value)
139
      {
140
         if constexpr (Opts.format == JSON) {
141
            content_type("application/json");
142
         }
143
         else if constexpr (Opts.format == BEVE) {
144
            content_type("application/beve");
145
         }
146
         auto ec = glz::write<Opts>(std::forward<T>(value), response_body);
147
         if (ec) {
148
            // The body may have been partially written before the error
149
            // fired; clear it so the response is never a mix of a partial
150
            // success payload and the error report.
151
            response_body.clear();
152
153
            // Serialize a structured glz_write_error object instead of the
154
            // legacy hardcoded placeholder. The original error_code and
155
            // any custom_error_message are surfaced so callers can debug
156
            // failed writes without having to reproduce them locally.
157
            glz_write_error info{
158
               .code = uint32_t(ec.ec),
159
               .message = ec.custom_error_message,
160
            };
161
            if (auto write_ec = glz::write_json(info, response_body); write_ec) {
162
               // Re-erroring on a tiny struct is essentially impossible,
163
               // but keep a safe static fallback so the response body is
164
               // never left empty after a failed write attempt.
165
               response_body = R"({"error":"glaze write failure"})";
166
            }
167
         }
168
         return *this;
169
      }
170
171
      // Reset response for reuse, preserving allocated capacity
172
      void clear()
173
0
      {
174
0
         status_code = 200;
175
0
         response_headers.clear();
176
0
         response_body.clear(); // preserves capacity
177
0
         user_headers_set = 0;
178
0
      }
179
180
0
      inline response& content_type(std::string_view type) { return header("Content-Type", type); }
181
182
      // JSON response helper using Glaze
183
      template <class T = glz::generic>
184
      response& json(T&& value)
185
      {
186
         content_type("application/json");
187
         auto ec = glz::write_json(std::forward<T>(value), response_body);
188
         if (ec) {
189
            response_body = R"({"error":"glz::write_json error"})"; // rare that this would ever happen
190
         }
191
         return *this;
192
      }
193
194
     private:
195
      // Records that the handler supplied one of the headers the wire serializer
196
      // would otherwise generate, so the serializer leaves it alone. Only reached
197
      // once the field has passed the CR/LF check: a field dropped there must not
198
      // set its flag, or a rejected Content-Length or Connection would suppress
199
      // the auto-generated counterpart and leave the response unframed.
200
      void mark_user_supplied(std::string_view name) noexcept
201
0
      {
202
0
         if (glz::striequal(name, "content-length"))
203
0
            user_headers_set |= has_content_length;
204
0
         else if (glz::striequal(name, "date"))
205
0
            user_headers_set |= has_date;
206
0
         else if (glz::striequal(name, "server"))
207
0
            user_headers_set |= has_server;
208
0
         else if (glz::striequal(name, "connection"))
209
0
            user_headers_set |= has_connection;
210
0
      }
211
   };
212
213
   using handler = std::function<void(const request&, response&)>;
214
   using async_handler = std::function<std::future<void>(const request&, response&)>;
215
   using error_handler = std::function<void(std::error_code, std::source_location)>;
216
217
   // Forward declarations for streaming and WebSocket support.
218
   // Full definitions live in glaze/net/http_server.hpp and glaze/net/websocket_connection.hpp.
219
   // Forward declarations are sufficient here because streaming_handler holds streaming_response
220
   // by reference and websocket_handler holds websocket_server through std::shared_ptr.
221
   struct streaming_response;
222
   struct websocket_server;
223
224
   // Streaming handler signature. Streaming routes take over the connection lifecycle
225
   // (no keep-alive) and write chunked responses through streaming_response.
226
   using streaming_handler = std::function<void(request&, streaming_response&)>;
227
228
   // WebSocket handler value: a websocket_server instance is bound to a path. The HTTP
229
   // server detects the upgrade handshake (Upgrade: websocket) and dispatches to the
230
   // matching server entry.
231
   using websocket_handler = std::shared_ptr<websocket_server>;
232
233
   /**
234
    * @brief Parameter constraint for route validation
235
    *
236
    * Defines validation rules for route parameters using a validation function.
237
    */
238
   struct param_constraint
239
   {
240
      /**
241
       * @brief Human-readable description of the constraint
242
       *
243
       * Used for OpenAPI parameter descriptions and debugging output.
244
       */
245
      std::string description{};
246
247
      /**
248
       * @brief Validation function for parameter values
249
       *
250
       * This function is used to validate parameter values. It should return true if the parameter value is valid,
251
       * false otherwise.
252
       */
253
      std::function<bool(std::string_view)> validation = [](std::string_view) { return true; };
254
   };
255
256
   /**
257
    * @brief An entry for a registered route.
258
    */
259
   struct route_spec
260
   {
261
      std::string description{};
262
      std::vector<std::string> tags{};
263
      std::unordered_map<std::string, param_constraint> constraints{};
264
265
      // Type information for schema generation
266
      std::optional<std::string> request_body_schema{};
267
      std::optional<std::string> response_schema{};
268
      std::optional<std::string> request_body_type_name{};
269
      std::optional<std::string> response_type_name{};
270
   };
271
272
   /**
273
    * @brief Generic radix-tree route table parameterized over the stored handler type.
274
    *
275
    * route_table provides path matching with support for static, parameterized (":param"),
276
    * and wildcard ("*name") segments. It is used as a building block by basic_http_router
277
    * to store normal routes (typed by Handler), streaming routes (streaming_handler), and
278
    * WebSocket routes (websocket_handler).
279
    *
280
    * @tparam H The stored handler type. Must be default-constructible (the empty value
281
    *           returned by match() when no route matches is H{}).
282
    */
283
   template <class H>
284
   struct route_table
285
   {
286
      /**
287
       * @brief Stored route entry: handler plus optional spec for schema/constraints.
288
       */
289
      struct route_entry
290
      {
291
         H handle{};
292
         route_spec spec{};
293
      };
294
295
      /**
296
       * @brief Node in the radix tree routing structure
297
       *
298
       * Each node represents a segment of a path, which can be a static string,
299
       * a parameter (prefixed with ":"), or a wildcard (prefixed with "*").
300
       */
301
      struct radix_node
302
      {
303
         std::string segment;
304
         bool is_parameter = false;
305
         bool is_wildcard = false;
306
         std::string parameter_name;
307
         std::unordered_map<std::string, std::unique_ptr<radix_node>> static_children;
308
         std::unique_ptr<radix_node> parameter_child;
309
         std::unique_ptr<radix_node> wildcard_child;
310
         std::unordered_map<http_method, H> handlers;
311
         std::unordered_map<http_method, std::unordered_map<std::string, param_constraint>> constraints;
312
         bool is_endpoint = false;
313
         std::string full_path;
314
315
         std::string to_string() const
316
         {
317
            std::string result;
318
            result.reserve(80 + segment.size() + full_path.size());
319
320
            result.append("Node[");
321
            result.append(is_parameter ? "PARAM:" : (is_wildcard ? "WILD:" : ""));
322
            result.append(segment);
323
            result.append(", endpoint=");
324
            result.append(is_endpoint ? "true" : "false");
325
            result.append(", children=");
326
            result.append(std::to_string(static_children.size()));
327
            result.append(parameter_child ? "+param" : "");
328
            result.append(wildcard_child ? "+wild" : "");
329
            result.append(", full_path=");
330
            result.append(full_path);
331
            result.append("]");
332
            return result;
333
         }
334
      };
335
336
      /**
337
       * @brief Map of registered routes, keyed by full path then HTTP method.
338
       *
339
       * Public for backward compatibility and to support introspection (e.g., the
340
       * OpenAPI generator iterates this map).
341
       */
342
      std::unordered_map<std::string, std::unordered_map<http_method, route_entry>> routes;
343
344
      /**
345
       * @brief Split a path into segments
346
       *
347
       * Splits a path like "/users/:id/profile" into ["users", ":id", "profile"].
348
       * This is the canonical implementation; basic_http_router::split_path is a
349
       * thin backward-compatible wrapper that delegates here.
350
       *
351
       * @param path The path to split
352
       * @return Vector of path segments
353
       */
354
      static std::vector<std::string> split_path(std::string_view path)
355
      {
356
         std::vector<std::string> segments;
357
         segments.reserve(std::count(path.begin(), path.end(), '/') + 1);
358
359
         size_t start = 0;
360
         while (start < path.size()) {
361
            if (path[start] == '/') {
362
               start++;
363
               continue;
364
            }
365
366
            size_t end = path.find('/', start);
367
            if (end == std::string::npos) end = path.size();
368
369
            segments.push_back(std::string(path.substr(start, end - start)));
370
            start = end;
371
         }
372
373
         return segments;
374
      }
375
376
      /**
377
       * @brief Detect a ".." path-traversal component in a decoded capture.
378
       *
379
       * Returns true when `path` contains a ".." segment delimited by a
380
       * separator or a string boundary. Percent-decoding happens after the
381
       * target is split on literal '/', so a "%2e%2e%2f" sequence only becomes
382
       * a "../" here; a capture carrying such a segment can climb out of a base
383
       * directory once a handler resolves it as a filesystem path.
384
       *
385
       * Both '/' and '\\' are treated as separators: on Windows the backslash
386
       * is a path separator too, so a "%2e%2e%5c" ("..\") capture traverses the
387
       * same way and an encoded backslash would otherwise slip past a '/'-only
388
       * check. A backslash is a legal byte in a POSIX filename, but a capture
389
       * bound for a filesystem join is exactly the case guarded here, so the
390
       * traversal reading wins over the rare literal-backslash name.
391
       */
392
      static bool has_dot_dot_segment(std::string_view path) noexcept
393
      {
394
         size_t start = 0;
395
         while (true) {
396
            const size_t sep = path.find_first_of("/\\", start);
397
            const size_t seg_len = (sep == std::string_view::npos ? path.size() : sep) - start;
398
            if (seg_len == 2 && path[start] == '.' && path[start + 1] == '.') {
399
               return true;
400
            }
401
            if (sep == std::string_view::npos) {
402
               return false;
403
            }
404
            start = sep + 1;
405
         }
406
      }
407
408
      /**
409
       * @brief Register a route in the table.
410
       *
411
       * @param method The HTTP method (GET, POST, etc.)
412
       * @param path The route path, may contain ":param" or trailing "*name" wildcards.
413
       * @param handle The handler to associate with the route.
414
       * @param spec Optional spec for the route.
415
       */
416
      void add(http_method method, std::string_view path, H handle, const route_spec& spec = {})
417
      {
418
         try {
419
            // Install in the radix tree first. add_route can throw on conflict
420
            // (duplicate :param or *wildcard names at the same position); if it
421
            // throws we want the routes map to stay in sync with the tree, not
422
            // to silently retain an entry that iteration sees but match() never
423
            // reaches. Pass handle by value so we still own a copy to move into
424
            // the routes entry below.
425
            add_route(method, path, handle, spec.constraints);
426
427
            auto& entry = routes[std::string(path)][method];
428
            entry.handle = std::move(handle);
429
            entry.spec = spec;
430
         }
431
         catch (const std::exception& e) {
432
            std::fprintf(stderr, "Error adding route '%.*s': %s\n", static_cast<int>(path.length()), path.data(),
433
                         e.what());
434
         }
435
      }
436
437
      /**
438
       * @brief Match a target against the registered routes.
439
       *
440
       * @param method The HTTP method of the request.
441
       * @param target The request target (may include a query string).
442
       * @return Pair of (matched handler, extracted path parameters). The handler is
443
       *         a default-constructed H if no route matched.
444
       */
445
      std::pair<H, std::unordered_map<std::string, std::string>> match(http_method method,
446
                                                                       std::string_view target) const
447
      {
448
         std::unordered_map<std::string, std::string> params;
449
         H result{};
450
451
         // Strip query string from target for matching
452
         const auto [path, query_string] = split_target(target);
453
454
         // First try direct lookup for non-parameterized routes (optimization)
455
         auto direct_it = direct_routes.find(std::string(path));
456
         if (direct_it != direct_routes.end()) {
457
            auto method_it = direct_it->second.find(method);
458
            if (method_it != direct_it->second.end()) {
459
               return {method_it->second, params};
460
            }
461
         }
462
463
         std::vector<std::string> segments = split_path(path);
464
         match_node(&root, segments, 0, method, params, result);
465
466
         return {result, params};
467
      }
468
469
      /**
470
       * @brief Print the entire tree structure for debugging.
471
       *
472
       * Prints only the tree contents. The caller is responsible for any header
473
       * (basic_http_router::print_tree groups three of these under labelled sections).
474
       */
475
      void print_tree() const { print_node(&root, 0); }
476
477
     private:
478
      mutable radix_node root;
479
      std::unordered_map<std::string, std::unordered_map<http_method, H>> direct_routes;
480
481
      void add_route(http_method method, std::string_view path, H handle,
482
                     const std::unordered_map<std::string, param_constraint>& constraints = {})
483
      {
484
         std::string path_str(path);
485
486
         if (path_str.find(':') == std::string::npos && path_str.find('*') == std::string::npos) {
487
            direct_routes[path_str][method] = handle;
488
            return;
489
         }
490
491
         std::vector<std::string> segments = split_path(path);
492
493
         radix_node* current = &root;
494
495
         for (size_t i = 0; i < segments.size(); ++i) {
496
            const std::string& segment = segments[i];
497
498
            if (segment.empty()) continue;
499
500
            if (segment[0] == ':') {
501
               std::string param_name = segment.substr(1);
502
503
               if (!current->parameter_child) {
504
                  current->parameter_child = std::make_unique<radix_node>();
505
                  current->parameter_child->is_parameter = true;
506
                  current->parameter_child->parameter_name = param_name;
507
                  current->parameter_child->segment = segment;
508
                  current->parameter_child->full_path = current->full_path + "/" + segment;
509
               }
510
               else if (current->parameter_child->parameter_name != param_name) {
511
                  throw std::runtime_error("Route conflict: different parameter names at same position: :" +
512
                                           current->parameter_child->parameter_name + " vs :" + param_name);
513
               }
514
515
               current = current->parameter_child.get();
516
            }
517
            else if (segment[0] == '*') {
518
               std::string wildcard_name = segment.substr(1);
519
520
               if (i != segments.size() - 1) {
521
                  throw std::runtime_error("Wildcard must be the last segment in route: " + path_str);
522
               }
523
524
               if (!current->wildcard_child) {
525
                  current->wildcard_child = std::make_unique<radix_node>();
526
                  current->wildcard_child->is_wildcard = true;
527
                  current->wildcard_child->parameter_name = wildcard_name;
528
                  current->wildcard_child->segment = segment;
529
                  current->wildcard_child->full_path = current->full_path + "/" + segment;
530
               }
531
               else if (current->wildcard_child->parameter_name != wildcard_name) {
532
                  throw std::runtime_error("Route conflict: different wildcard names at same position: *" +
533
                                           current->wildcard_child->parameter_name + " vs *" + wildcard_name);
534
               }
535
536
               current = current->wildcard_child.get();
537
               break;
538
            }
539
            else {
540
               if (current->static_children.find(segment) == current->static_children.end()) {
541
                  current->static_children[segment] = std::make_unique<radix_node>();
542
                  current->static_children[segment]->segment = segment;
543
                  current->static_children[segment]->full_path = current->full_path + "/" + segment;
544
               }
545
546
               current = current->static_children[segment].get();
547
            }
548
         }
549
550
         current->is_endpoint = true;
551
         current->handlers[method] = handle;
552
553
         if (!constraints.empty()) {
554
            current->constraints[method] = constraints;
555
         }
556
      }
557
558
      bool match_node(radix_node* node, const std::vector<std::string>& segments, size_t index, http_method method,
559
                      std::unordered_map<std::string, std::string>& params, H& result) const
560
      {
561
         if (index == segments.size()) {
562
            if (node->is_endpoint) {
563
               auto it = node->handlers.find(method);
564
               if (it != node->handlers.end()) {
565
                  bool constraints_passed = true;
566
                  auto constraints_it = node->constraints.find(method);
567
                  if (constraints_it != node->constraints.end()) {
568
                     for (const auto& [param_name, constraint] : constraints_it->second) {
569
                        auto param_it = params.find(param_name);
570
                        if (param_it != params.end()) {
571
                           const std::string& value = param_it->second;
572
                           if (!constraint.validation(value)) {
573
                              constraints_passed = false;
574
                              break;
575
                           }
576
                        }
577
                     }
578
                  }
579
580
                  if (constraints_passed) {
581
                     result = it->second;
582
                     return true;
583
                  }
584
                  return false;
585
               }
586
            }
587
            return false;
588
         }
589
590
         const std::string& segment = segments[index];
591
592
         auto static_it = node->static_children.find(segment);
593
         if (static_it != node->static_children.end()) {
594
            if (match_node(static_it->second.get(), segments, index + 1, method, params, result)) {
595
               return true;
596
            }
597
         }
598
599
         if (node->parameter_child) {
600
            std::string decoded = url_decode(segment);
601
602
            // A ":param" captures a single segment; refuse a decoded ".."
603
            // component so the value cannot escape a base directory when a
604
            // handler treats it as a path.
605
            if (!has_dot_dot_segment(decoded)) {
606
               std::string param_name = node->parameter_child->parameter_name;
607
               params[param_name] = std::move(decoded);
608
609
               if (match_node(node->parameter_child.get(), segments, index + 1, method, params, result)) {
610
                  return true;
611
               }
612
613
               params.erase(param_name);
614
            }
615
         }
616
617
         if (node->wildcard_child) {
618
            std::string full_capture;
619
            for (size_t i = index; i < segments.size(); i++) {
620
               if (i > index) full_capture += "/";
621
               full_capture += url_decode(segments[i]);
622
            }
623
624
            // The capture is joined from decoded segments, so a "%2e%2e%2f" in
625
            // the request only resolves to a ".." here; refuse it so a mount
626
            // like "/files/*path" cannot be walked outside its base directory.
627
            if (has_dot_dot_segment(full_capture)) {
628
               return false;
629
            }
630
631
            const auto& wildcard_name = node->wildcard_child->parameter_name;
632
            params[wildcard_name] = full_capture;
633
634
            if (node->wildcard_child->is_endpoint) {
635
               auto it = node->wildcard_child->handlers.find(method);
636
               if (it != node->wildcard_child->handlers.end()) {
637
                  bool constraints_passed = true;
638
                  auto constraints_it = node->wildcard_child->constraints.find(method);
639
                  if (constraints_it != node->wildcard_child->constraints.end()) {
640
                     for (const auto& [param_name, constraint] : constraints_it->second) {
641
                        auto param_it = params.find(param_name);
642
                        if (param_it != params.end()) {
643
                           const std::string& value = param_it->second;
644
                           if (!constraint.validation(value)) {
645
                              constraints_passed = false;
646
                              break;
647
                           }
648
                        }
649
                     }
650
                  }
651
652
                  if (constraints_passed) {
653
                     result = it->second;
654
                     return true;
655
                  }
656
               }
657
            }
658
659
            // Wildcard match failed (not an endpoint, wrong method, or
660
            // constraint failure). Mirror the parameter-branch behavior and
661
            // erase the capture so the caller's params map reflects only the
662
            // matched route's parameters.
663
            params.erase(wildcard_name);
664
         }
665
666
         return false;
667
      }
668
669
      void print_node(const radix_node* node, int depth) const
670
      {
671
         if (!node) return;
672
673
         std::string indent(depth * 2, ' ');
674
         std::cout << indent << node->to_string() << "\n";
675
676
         if (node->is_endpoint) {
677
            std::cout << indent << "  Handlers: ";
678
            for (const auto& [method, _] : node->handlers) {
679
               std::cout << to_string(method) << " ";
680
            }
681
            std::cout << "\n";
682
683
            for (const auto& [method, method_constraints] : node->constraints) {
684
               std::cout << indent << "  Constraints for " << to_string(method) << ":\n";
685
               for (const auto& [param, constraint] : method_constraints) {
686
                  std::cout << indent << "    " << param << ": (" << constraint.description << ")\n";
687
               }
688
            }
689
         }
690
691
         for (const auto& [segment, child] : node->static_children) {
692
            print_node(child.get(), depth + 1);
693
         }
694
695
         if (node->parameter_child) {
696
            print_node(node->parameter_child.get(), depth + 1);
697
         }
698
699
         if (node->wildcard_child) {
700
            print_node(node->wildcard_child.get(), depth + 1);
701
         }
702
      }
703
   };
704
705
   /**
706
    * @brief Match a value against a pattern with advanced pattern matching features
707
    *
708
    * Supports:
709
    * - Wildcards (*) for matching any number of characters
710
    * - Question marks (?) for matching a single character
711
    * - Character classes ([a-z], [^0-9])
712
    * - Anchors (^ for start of string, $ for end of string)
713
    * - Escape sequences with backslash
714
    *
715
    * @param value The string to match
716
    * @param pattern The pattern to match against
717
    * @return true if the value matches the pattern, false otherwise
718
    */
719
   inline bool match_pattern(std::string_view value, std::string_view pattern)
720
0
   {
721
0
      enum struct State { Literal, Escape, CharClass };
722
0
723
0
      if (pattern.empty()) return true; // Empty pattern matches anything
724
0
725
0
      size_t v_pos = 0;
726
0
      size_t p_pos = 0;
727
0
728
0
      // For backtracking when we encounter *
729
0
      std::optional<size_t> backtrack_pattern;
730
0
      std::optional<size_t> backtrack_value;
731
0
732
0
      // For character classes
733
0
      State state = State::Literal;
734
0
      bool negate_class = false;
735
0
      bool char_class_match = false;
736
0
737
0
      while (v_pos < value.size() || p_pos < pattern.size()) {
738
0
         // Pattern exhausted but value remains
739
0
         if (p_pos >= pattern.size()) {
740
0
            if (backtrack_pattern) {
741
0
               p_pos = *backtrack_pattern;
742
0
               v_pos = ++(*backtrack_value);
743
0
               continue;
744
0
            }
745
0
            return false;
746
0
         }
747
0
748
0
         // Value exhausted but pattern remains
749
0
         if (v_pos >= value.size()) {
750
0
            if (p_pos < pattern.size() && pattern[p_pos] == '*' && p_pos == pattern.size() - 1) return true;
751
0
752
0
            if (backtrack_pattern) {
753
0
               p_pos = *backtrack_pattern;
754
0
               v_pos = ++(*backtrack_value);
755
0
               continue;
756
0
            }
757
0
            return false;
758
0
         }
759
0
760
0
         switch (state) {
761
0
         case State::Literal:
762
0
            if (pattern[p_pos] == '\\') {
763
0
               state = State::Escape;
764
0
               p_pos++;
765
0
               continue;
766
0
            }
767
0
            else if (pattern[p_pos] == '[') {
768
0
               state = State::CharClass;
769
0
               char_class_match = false;
770
0
               p_pos++;
771
0
772
0
               if (p_pos < pattern.size() && pattern[p_pos] == '^') {
773
0
                  negate_class = true;
774
0
                  p_pos++;
775
0
               }
776
0
               else {
777
0
                  negate_class = false;
778
0
               }
779
0
               continue;
780
0
            }
781
0
            else if (pattern[p_pos] == '*') {
782
0
               backtrack_pattern = p_pos;
783
0
               backtrack_value = v_pos;
784
0
               p_pos++;
785
0
               continue;
786
0
            }
787
0
            else if (pattern[p_pos] == '?') {
788
0
               p_pos++;
789
0
               v_pos++;
790
0
               continue;
791
0
            }
792
0
            else if (pattern[p_pos] == '^' && p_pos == 0) {
793
0
               p_pos++;
794
0
               continue;
795
0
            }
796
0
            else if (pattern[p_pos] == '$' && p_pos == pattern.size() - 1) {
797
0
               return v_pos == value.size();
798
0
            }
799
0
            else {
800
0
               if (pattern[p_pos] != value[v_pos]) {
801
0
                  if (backtrack_pattern) {
802
0
                     p_pos = *backtrack_pattern;
803
0
                     v_pos = ++(*backtrack_value);
804
0
                     continue;
805
0
                  }
806
0
                  return false;
807
0
               }
808
0
               p_pos++;
809
0
               v_pos++;
810
0
            }
811
0
            break;
812
0
813
0
         case State::Escape:
814
0
            if (p_pos >= pattern.size() || pattern[p_pos] != value[v_pos]) {
815
0
               if (backtrack_pattern) {
816
0
                  p_pos = *backtrack_pattern;
817
0
                  v_pos = ++(*backtrack_value);
818
0
                  state = State::Literal;
819
0
                  continue;
820
0
               }
821
0
               return false;
822
0
            }
823
0
            p_pos++;
824
0
            v_pos++;
825
0
            state = State::Literal;
826
0
            break;
827
0
828
0
         case State::CharClass:
829
0
            if (pattern[p_pos] == ']') {
830
0
               p_pos++;
831
0
               if (negate_class) {
832
0
                  if (char_class_match) {
833
0
                     return false;
834
0
                  }
835
0
                  v_pos++;
836
0
               }
837
0
               else {
838
0
                  if (!char_class_match) {
839
0
                     return false;
840
0
                  }
841
0
                  v_pos++;
842
0
               }
843
0
               state = State::Literal;
844
0
               continue;
845
0
            }
846
0
            else if (p_pos + 2 < pattern.size() && pattern[p_pos + 1] == '-') {
847
0
               char start = pattern[p_pos];
848
0
               char end = pattern[p_pos + 2];
849
0
               if (value[v_pos] >= start && value[v_pos] <= end) {
850
0
                  char_class_match = true;
851
0
               }
852
0
               p_pos += 3;
853
0
            }
854
0
            else {
855
0
               if (pattern[p_pos] == value[v_pos]) {
856
0
                  char_class_match = true;
857
0
               }
858
0
               p_pos++;
859
0
            }
860
0
            break;
861
0
         }
862
0
      }
863
0
864
0
      return v_pos == value.size() && p_pos == pattern.size();
865
0
   }
866
867
   /**
868
    * @brief HTTP router based on a radix tree for efficient path matching
869
    *
870
    * @tparam Handler The type of the handler that gets invoked upon a route match.
871
    *                 Must be invocable with (const request&, response&).
872
    *                 Note: Middleware registered via use() must also be this type.
873
    *
874
    * The basic_http_router class provides fast route matching for HTTP requests using a radix tree
875
    * data structure. It supports static routes, parameterized routes (e.g., "/users/:id"),
876
    * wildcard routes, and parameter validation via constraints.
877
    *
878
    * In addition to normal request/response handlers, basic_http_router also stores
879
    * streaming routes (registered via stream_get/stream_post/stream) and WebSocket
880
    * routes (registered via websocket()). All three kinds share the same radix-tree
881
    * matching logic, so streaming and WebSocket routes also support ":param" path
882
    * parameters.
883
    *
884
    * Note: http_server::mount() only accepts the default http_router (basic_http_router<>).
885
    * Custom handler routers are intended for standalone use or with custom server implementations.
886
    */
887
   template <class Handler = std::function<void(const request&, response&)>>
888
      requires std::invocable<Handler, const request&, response&>
889
   struct basic_http_router
890
   {
891
      /**
892
       * @brief Function type for request handlers
893
       */
894
      using handler = Handler;
895
896
      /**
897
       * @brief A compile-time boolean indicating whether asynchronous request handlers are enabled.
898
       */
899
      static constexpr bool is_async_enabled =
900
         std::is_constructible_v<Handler, std::function<void(const request&, response&)>>;
901
902
      /**
903
       * @brief Function type for asynchronous request handlers
904
       */
905
      using async_handler = std::function<std::future<void>(const request&, response&)>;
906
907
      /**
908
       * @brief Underlying route table type for normal routes.
909
       */
910
      using normal_route_table = route_table<Handler>;
911
912
      /**
913
       * @brief Backward-compatible alias for the normal route entry type.
914
       */
915
      using route_entry = typename normal_route_table::route_entry;
916
917
      basic_http_router() = default;
918
919
      /**
920
       * @brief Backward-compatible static helper. Equivalent to route_table<Handler>::split_path.
921
       */
922
      static std::vector<std::string> split_path(std::string_view path) { return normal_route_table::split_path(path); }
923
924
      /**
925
       * @brief Register a route with the router
926
       *
927
       * @param method The HTTP method (GET, POST, etc.)
928
       * @param path The route path, can include parameters (":param") and wildcards ("*param")
929
       * @param handle The handler function to call when this route matches
930
       * @param spec Optional spec for the route.
931
       * @return Reference to this router for method chaining
932
       * @throws std::runtime_error if there's a route conflict
933
       */
934
      basic_http_router& route(http_method method, std::string_view path, handler handle, const route_spec& spec = {})
935
      {
936
         normal_routes.add(method, path, std::move(handle), spec);
937
         return *this;
938
      }
939
940
      /**
941
       * @brief Register a GET route
942
       */
943
      basic_http_router& get(std::string_view path, handler handle, const route_spec& spec = {})
944
      {
945
         return route(http_method::GET, path, std::move(handle), spec);
946
      }
947
948
      /**
949
       * @brief Register a POST route
950
       */
951
      basic_http_router& post(std::string_view path, handler handle, const route_spec& spec = {})
952
      {
953
         return route(http_method::POST, path, std::move(handle), spec);
954
      }
955
956
      /**
957
       * @brief Register a PUT route
958
       */
959
      basic_http_router& put(std::string_view path, handler handle, const route_spec& spec = {})
960
      {
961
         return route(http_method::PUT, path, std::move(handle), spec);
962
      }
963
964
      /**
965
       * @brief Register a DELETE route
966
       */
967
      basic_http_router& del(std::string_view path, handler handle, const route_spec& spec = {})
968
      {
969
         return route(http_method::DELETE, path, std::move(handle), spec);
970
      }
971
972
      /**
973
       * @brief Register a PATCH route
974
       */
975
      basic_http_router& patch(std::string_view path, handler handle, const route_spec& spec = {})
976
      {
977
         return route(http_method::PATCH, path, std::move(handle), spec);
978
      }
979
980
      /**
981
       * @brief Register an asynchronous route
982
       */
983
      basic_http_router& route_async(http_method method, std::string_view path, async_handler handle,
984
                                     const route_spec& spec = {})
985
         requires is_async_enabled
986
      {
987
         return route(
988
            method, path,
989
            [handle = std::move(handle)](const request& req, response& res) {
990
               auto future = handle(req, res);
991
               future.get();
992
            },
993
            spec);
994
      }
995
996
      /**
997
       * @brief Register an asynchronous GET route
998
       */
999
      basic_http_router& get_async(std::string_view path, async_handler handle, const route_spec& spec = {})
1000
         requires is_async_enabled
1001
      {
1002
         return route_async(http_method::GET, path, std::move(handle), spec);
1003
      }
1004
1005
      /**
1006
       * @brief Register an asynchronous POST route
1007
       */
1008
      basic_http_router& post_async(std::string_view path, async_handler handle, const route_spec& spec = {})
1009
         requires is_async_enabled
1010
      {
1011
         return route_async(http_method::POST, path, std::move(handle), spec);
1012
      }
1013
1014
      /**
1015
       * @brief Register an asynchronous PUT route
1016
       */
1017
      basic_http_router& put_async(std::string_view path, async_handler handle, const route_spec& spec = {})
1018
         requires is_async_enabled
1019
      {
1020
         return route_async(http_method::PUT, path, std::move(handle), spec);
1021
      }
1022
1023
      /**
1024
       * @brief Register an asynchronous DELETE route
1025
       */
1026
      basic_http_router& del_async(std::string_view path, async_handler handle, const route_spec& spec = {})
1027
         requires is_async_enabled
1028
      {
1029
         return route_async(http_method::DELETE, path, std::move(handle), spec);
1030
      }
1031
1032
      /**
1033
       * @brief Register an asynchronous PATCH route
1034
       */
1035
      basic_http_router& patch_async(std::string_view path, async_handler handle, const route_spec& spec = {})
1036
         requires is_async_enabled
1037
      {
1038
         return route_async(http_method::PATCH, path, std::move(handle), spec);
1039
      }
1040
1041
      /**
1042
       * @brief Register a streaming route. Streaming handlers take over the connection
1043
       * lifecycle and write chunked responses through streaming_response.
1044
       *
1045
       * Supports the same path-parameter syntax as normal routes (":param" and "*name").
1046
       */
1047
      basic_http_router& stream(http_method method, std::string_view path, streaming_handler handle,
1048
                                const route_spec& spec = {})
1049
      {
1050
         streaming_routes.add(method, path, std::move(handle), spec);
1051
         return *this;
1052
      }
1053
1054
      /**
1055
       * @brief Register a streaming GET route.
1056
       */
1057
      basic_http_router& stream_get(std::string_view path, streaming_handler handle, const route_spec& spec = {})
1058
      {
1059
         return stream(http_method::GET, path, std::move(handle), spec);
1060
      }
1061
1062
      /**
1063
       * @brief Register a streaming POST route.
1064
       */
1065
      basic_http_router& stream_post(std::string_view path, streaming_handler handle, const route_spec& spec = {})
1066
      {
1067
         return stream(http_method::POST, path, std::move(handle), spec);
1068
      }
1069
1070
      /**
1071
       * @brief Register a WebSocket handler for a path.
1072
       *
1073
       * Supports the same path-parameter syntax as normal routes. The HTTP server
1074
       * detects the upgrade handshake (Upgrade: websocket) and dispatches to the
1075
       * matching server, populating request.params from the path.
1076
       */
1077
      basic_http_router& websocket(std::string_view path, websocket_handler server, const route_spec& spec = {})
1078
      {
1079
         websocket_routes.add(http_method::GET, path, std::move(server), spec);
1080
         return *this;
1081
      }
1082
1083
      /**
1084
       * @brief Register middleware to be executed before route handlers
1085
       *
1086
       * Middleware functions are executed in the order they are registered.
1087
       *
1088
       * @param middleware The middleware function
1089
       * @return Reference to this router for method chaining
1090
       */
1091
      basic_http_router& use(handler middleware)
1092
      {
1093
         middlewares.push_back(std::move(middleware));
1094
         return *this;
1095
      }
1096
1097
      /**
1098
       * @brief Match a normal request against registered routes
1099
       *
1100
       * @param method The HTTP method of the request
1101
       * @param target The target path of the request (may include query string)
1102
       * @return A pair containing the matched handler and extracted parameters
1103
       */
1104
      std::pair<handler, std::unordered_map<std::string, std::string>> match(http_method method,
1105
                                                                             std::string_view target) const
1106
      {
1107
         return normal_routes.match(method, target);
1108
      }
1109
1110
      /**
1111
       * @brief Match a streaming request against registered streaming routes.
1112
       */
1113
      std::pair<streaming_handler, std::unordered_map<std::string, std::string>> match_streaming(
1114
         http_method method, std::string_view target) const
1115
      {
1116
         return streaming_routes.match(method, target);
1117
      }
1118
1119
      /**
1120
       * @brief Match a WebSocket upgrade request against registered WebSocket routes.
1121
       *
1122
       * WebSocket upgrades are HTTP GET requests by definition, so the lookup uses
1123
       * http_method::GET internally.
1124
       */
1125
      std::pair<websocket_handler, std::unordered_map<std::string, std::string>> match_websocket(
1126
         std::string_view target) const
1127
      {
1128
         return websocket_routes.match(http_method::GET, target);
1129
      }
1130
1131
      /**
1132
       * @brief Print the router structure for debugging.
1133
       */
1134
      void print_tree() const
1135
      {
1136
         std::cout << "[normal routes]\n";
1137
         normal_routes.print_tree();
1138
         std::cout << "[streaming routes]\n";
1139
         streaming_routes.print_tree();
1140
         std::cout << "[websocket routes]\n";
1141
         websocket_routes.print_tree();
1142
      }
1143
1144
      /**
1145
       * @brief Storage for normal request/response routes.
1146
       *
1147
       * Iterate `router.normal_routes.routes` for path -> method -> entry inspection
1148
       * (used by the OpenAPI generator and mount()).
1149
       *
1150
       * Migration note: prior to the streaming/WebSocket unification, this field was
1151
       * exposed as `router.routes`. Code that iterated `router.routes` should now
1152
       * iterate `router.normal_routes.routes` (or use the `routes()` accessor below).
1153
       */
1154
      normal_route_table normal_routes;
1155
1156
      /**
1157
       * @brief Backward-compatible accessor for the registered normal routes map.
1158
       *
1159
       * Equivalent to `normal_routes.routes`. Provided as a function (not a
1160
       * reference data member) because a reference member would be copied verbatim
1161
       * by the implicitly-defined move constructor and dangle into the moved-from
1162
       * source.
1163
       */
1164
      auto& routes() noexcept { return normal_routes.routes; }
1165
      const auto& routes() const noexcept { return normal_routes.routes; }
1166
1167
      /**
1168
       * @brief Storage for streaming routes.
1169
       */
1170
      route_table<streaming_handler> streaming_routes;
1171
1172
      /**
1173
       * @brief Storage for WebSocket routes.
1174
       */
1175
      route_table<websocket_handler> websocket_routes;
1176
1177
      /**
1178
       * @brief Vector of middleware handlers
1179
       */
1180
      std::vector<handler> middlewares;
1181
   };
1182
1183
   /**
1184
    * @brief Default HTTP router using std::function handlers
1185
    *
1186
    * This is a type alias for backward compatibility. Use basic_http_router<Handler>
1187
    * if you need to customize the handler type for coroutines, different futures
1188
    * implementations, or callback-based architectures.
1189
    */
1190
   using http_router = basic_http_router<>;
1191
}