libs/http/src/server/flat_router.cpp
95.9% Lines (141/147)
100.0% Functions (12/12)
83.2% Branches (94/113)
libs/http/src/server/flat_router.cpp
| Line | Branch | Hits | Source Code |
|---|---|---|---|
| 1 | // | ||
| 2 | // Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot 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 | #include <boost/http/server/flat_router.hpp> | ||
| 11 | #include <boost/http/server/basic_router.hpp> | ||
| 12 | #include <boost/http/detail/except.hpp> | ||
| 13 | |||
| 14 | #include "src/server/detail/router_base.hpp" | ||
| 15 | #include "src/server/detail/pct_decode.hpp" | ||
| 16 | #include "src/server/detail/route_match.hpp" | ||
| 17 | |||
| 18 | #include <algorithm> | ||
| 19 | |||
| 20 | namespace boost { | ||
| 21 | namespace http { | ||
| 22 | |||
| 23 | //------------------------------------------------ | ||
| 24 | |||
| 25 | struct flat_router::impl | ||
| 26 | { | ||
| 27 | using entry = detail::router_base::entry; | ||
| 28 | using layer = detail::router_base::layer; | ||
| 29 | using handler = detail::router_base::handler; | ||
| 30 | using matcher = detail::router_base::matcher; | ||
| 31 | using opt_flags = detail::router_base::opt_flags; | ||
| 32 | using handler_ptr = detail::router_base::handler_ptr; | ||
| 33 | using options_handler_ptr = detail::router_base::options_handler_ptr; | ||
| 34 | using match_result = route_params_base::match_result; | ||
| 35 | |||
| 36 | std::vector<entry> entries; | ||
| 37 | std::vector<matcher> matchers; | ||
| 38 | |||
| 39 | std::uint64_t global_methods_ = 0; | ||
| 40 | std::vector<std::string> global_custom_verbs_; | ||
| 41 | std::string global_allow_header_; | ||
| 42 | options_handler_ptr options_handler_; | ||
| 43 | |||
| 44 | // RAII scope tracker sets matcher's skip_ when scope ends | ||
| 45 | struct scope_tracker | ||
| 46 | { | ||
| 47 | std::vector<matcher>& matchers_; | ||
| 48 | std::vector<entry>& entries_; | ||
| 49 | std::size_t matcher_idx_; | ||
| 50 | |||
| 51 | 150 | scope_tracker( | |
| 52 | std::vector<matcher>& m, | ||
| 53 | std::vector<entry>& e, | ||
| 54 | std::size_t idx) | ||
| 55 | 150 | : matchers_(m) | |
| 56 | 150 | , entries_(e) | |
| 57 | 150 | , matcher_idx_(idx) | |
| 58 | { | ||
| 59 | 150 | } | |
| 60 | |||
| 61 | 150 | ~scope_tracker() | |
| 62 | { | ||
| 63 | 150 | matchers_[matcher_idx_].skip_ = entries_.size(); | |
| 64 | 150 | } | |
| 65 | }; | ||
| 66 | |||
| 67 | // Build Allow header string from bitmask and custom verbs | ||
| 68 | static std::string | ||
| 69 | 165 | build_allow_header( | |
| 70 | std::uint64_t methods, | ||
| 71 | std::vector<std::string> const& custom) | ||
| 72 | { | ||
| 73 |
2/2✓ Branch 0 taken 17 times.
✓ Branch 1 taken 148 times.
|
165 | if(methods == ~0ULL) |
| 74 |
1/1✓ Branch 1 taken 17 times.
|
34 | return "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT"; |
| 75 | |||
| 76 | 148 | std::string result; | |
| 77 | // Methods in alphabetical order | ||
| 78 | static constexpr std::pair<http::method, char const*> known[] = { | ||
| 79 | {http::method::acl, "ACL"}, | ||
| 80 | {http::method::bind, "BIND"}, | ||
| 81 | {http::method::checkout, "CHECKOUT"}, | ||
| 82 | {http::method::connect, "CONNECT"}, | ||
| 83 | {http::method::copy, "COPY"}, | ||
| 84 | {http::method::delete_, "DELETE"}, | ||
| 85 | {http::method::get, "GET"}, | ||
| 86 | {http::method::head, "HEAD"}, | ||
| 87 | {http::method::link, "LINK"}, | ||
| 88 | {http::method::lock, "LOCK"}, | ||
| 89 | {http::method::merge, "MERGE"}, | ||
| 90 | {http::method::mkactivity, "MKACTIVITY"}, | ||
| 91 | {http::method::mkcalendar, "MKCALENDAR"}, | ||
| 92 | {http::method::mkcol, "MKCOL"}, | ||
| 93 | {http::method::move, "MOVE"}, | ||
| 94 | {http::method::msearch, "M-SEARCH"}, | ||
| 95 | {http::method::notify, "NOTIFY"}, | ||
| 96 | {http::method::options, "OPTIONS"}, | ||
| 97 | {http::method::patch, "PATCH"}, | ||
| 98 | {http::method::post, "POST"}, | ||
| 99 | {http::method::propfind, "PROPFIND"}, | ||
| 100 | {http::method::proppatch, "PROPPATCH"}, | ||
| 101 | {http::method::purge, "PURGE"}, | ||
| 102 | {http::method::put, "PUT"}, | ||
| 103 | {http::method::rebind, "REBIND"}, | ||
| 104 | {http::method::report, "REPORT"}, | ||
| 105 | {http::method::search, "SEARCH"}, | ||
| 106 | {http::method::subscribe, "SUBSCRIBE"}, | ||
| 107 | {http::method::trace, "TRACE"}, | ||
| 108 | {http::method::unbind, "UNBIND"}, | ||
| 109 | {http::method::unlink, "UNLINK"}, | ||
| 110 | {http::method::unlock, "UNLOCK"}, | ||
| 111 | {http::method::unsubscribe, "UNSUBSCRIBE"}, | ||
| 112 | }; | ||
| 113 |
2/2✓ Branch 2 taken 4884 times.
✓ Branch 3 taken 148 times.
|
5032 | for(auto const& [m, name] : known) |
| 114 | { | ||
| 115 |
2/2✓ Branch 0 taken 111 times.
✓ Branch 1 taken 4773 times.
|
4884 | if(methods & (1ULL << static_cast<unsigned>(m))) |
| 116 | { | ||
| 117 |
2/2✓ Branch 1 taken 7 times.
✓ Branch 2 taken 104 times.
|
111 | if(!result.empty()) |
| 118 |
1/1✓ Branch 1 taken 7 times.
|
7 | result += ", "; |
| 119 |
1/1✓ Branch 1 taken 111 times.
|
111 | result += name; |
| 120 | } | ||
| 121 | } | ||
| 122 | // Append custom verbs | ||
| 123 |
2/2✓ Branch 5 taken 4 times.
✓ Branch 6 taken 148 times.
|
152 | for(auto const& v : custom) |
| 124 | { | ||
| 125 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
|
4 | if(!result.empty()) |
| 126 | ✗ | result += ", "; | |
| 127 |
1/1✓ Branch 1 taken 4 times.
|
4 | result += v; |
| 128 | } | ||
| 129 | 148 | return result; | |
| 130 | 148 | } | |
| 131 | |||
| 132 | static opt_flags | ||
| 133 | 132 | compute_effective_opts( | |
| 134 | opt_flags parent, | ||
| 135 | opt_flags child) | ||
| 136 | { | ||
| 137 | 132 | opt_flags result = parent; | |
| 138 | |||
| 139 | // case_sensitive: bits 1-2 (2=true, 4=false) | ||
| 140 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 128 times.
|
132 | if(child & 2) |
| 141 | 4 | result = (result & ~6) | 2; | |
| 142 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 126 times.
|
128 | else if(child & 4) |
| 143 | 2 | result = (result & ~6) | 4; | |
| 144 | |||
| 145 | // strict: bits 3-4 (8=true, 16=false) | ||
| 146 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 131 times.
|
132 | if(child & 8) |
| 147 | 1 | result = (result & ~24) | 8; | |
| 148 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 130 times.
|
131 | else if(child & 16) |
| 149 | 1 | result = (result & ~24) | 16; | |
| 150 | |||
| 151 | 132 | return result; | |
| 152 | } | ||
| 153 | |||
| 154 | void | ||
| 155 | 98 | flatten(detail::router_base::impl& src) | |
| 156 | { | ||
| 157 | 98 | flatten_recursive(src, opt_flags{}, 0); | |
| 158 | 98 | build_allow_headers(); | |
| 159 | 98 | } | |
| 160 | |||
| 161 | void | ||
| 162 | 98 | build_allow_headers() | |
| 163 | { | ||
| 164 | // Build per-matcher Allow header strings | ||
| 165 |
2/2✓ Branch 5 taken 150 times.
✓ Branch 6 taken 98 times.
|
248 | for(auto& m : matchers) |
| 166 | { | ||
| 167 |
2/2✓ Branch 0 taken 65 times.
✓ Branch 1 taken 85 times.
|
150 | if(m.end_) |
| 168 | 65 | m.allow_header_ = build_allow_header( | |
| 169 |
1/1✓ Branch 1 taken 65 times.
|
65 | m.allowed_methods_, m.custom_verbs_); |
| 170 | } | ||
| 171 | |||
| 172 | // Deduplicate global custom verbs and build global Allow header | ||
| 173 | 98 | std::sort(global_custom_verbs_.begin(), global_custom_verbs_.end()); | |
| 174 |
1/1✓ Branch 3 taken 98 times.
|
196 | global_custom_verbs_.erase( |
| 175 |
1/1✓ Branch 3 taken 98 times.
|
98 | std::unique(global_custom_verbs_.begin(), global_custom_verbs_.end()), |
| 176 | 98 | global_custom_verbs_.end()); | |
| 177 | 98 | global_allow_header_ = build_allow_header( | |
| 178 |
1/1✓ Branch 1 taken 98 times.
|
98 | global_methods_, global_custom_verbs_); |
| 179 | 98 | } | |
| 180 | |||
| 181 | void | ||
| 182 | 132 | flatten_recursive( | |
| 183 | detail::router_base::impl& src, | ||
| 184 | opt_flags parent_opts, | ||
| 185 | std::uint32_t depth) | ||
| 186 | { | ||
| 187 | 132 | opt_flags eff = compute_effective_opts(parent_opts, src.opt); | |
| 188 | |||
| 189 |
2/2✓ Branch 5 taken 150 times.
✓ Branch 6 taken 132 times.
|
282 | for(auto& layer : src.layers) |
| 190 | { | ||
| 191 | // Move matcher, set flat router fields | ||
| 192 | 150 | std::size_t matcher_idx = matchers.size(); | |
| 193 |
1/1✓ Branch 2 taken 150 times.
|
150 | matchers.emplace_back(std::move(layer.match)); |
| 194 | 150 | auto& m = matchers.back(); | |
| 195 | 150 | m.first_entry_ = entries.size(); | |
| 196 | 150 | m.effective_opts_ = eff; | |
| 197 | 150 | m.depth_ = depth; | |
| 198 | // m.skip_ set by scope_tracker dtor | ||
| 199 | |||
| 200 | 150 | scope_tracker scope(matchers, entries, matcher_idx); | |
| 201 | |||
| 202 |
2/2✓ Branch 5 taken 162 times.
✓ Branch 6 taken 150 times.
|
312 | for(auto& e : layer.entries) |
| 203 | { | ||
| 204 |
2/2✓ Branch 1 taken 34 times.
✓ Branch 2 taken 128 times.
|
162 | if(e.h->kind == detail::router_base::is_router) |
| 205 | { | ||
| 206 | // Recurse into nested router | ||
| 207 | 34 | auto* nested = e.h->get_router(); | |
| 208 |
2/4✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34 times.
✗ Branch 3 not taken.
|
34 | if(nested && nested->impl_) |
| 209 |
1/1✓ Branch 1 taken 34 times.
|
34 | flatten_recursive(*nested->impl_, eff, depth + 1); |
| 210 | } | ||
| 211 | else | ||
| 212 | { | ||
| 213 | // Collect methods for OPTIONS (only for end routes) | ||
| 214 |
2/2✓ Branch 0 taken 68 times.
✓ Branch 1 taken 60 times.
|
128 | if(m.end_) |
| 215 | { | ||
| 216 | // Per-matcher collection | ||
| 217 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 60 times.
|
68 | if(e.all) |
| 218 | 8 | m.allowed_methods_ = ~0ULL; | |
| 219 |
2/2✓ Branch 0 taken 58 times.
✓ Branch 1 taken 2 times.
|
60 | else if(e.verb != http::method::unknown) |
| 220 | 58 | m.allowed_methods_ |= (1ULL << static_cast<unsigned>(e.verb)); | |
| 221 |
1/2✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
|
2 | else if(!e.verb_str.empty()) |
| 222 |
1/1✓ Branch 1 taken 2 times.
|
2 | m.custom_verbs_.push_back(e.verb_str); |
| 223 | |||
| 224 | // Global collection (for OPTIONS *) | ||
| 225 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 60 times.
|
68 | if(e.all) |
| 226 | 8 | global_methods_ = ~0ULL; | |
| 227 |
2/2✓ Branch 0 taken 58 times.
✓ Branch 1 taken 2 times.
|
60 | else if(e.verb != http::method::unknown) |
| 228 | 58 | global_methods_ |= (1ULL << static_cast<unsigned>(e.verb)); | |
| 229 |
1/2✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
|
2 | else if(!e.verb_str.empty()) |
| 230 |
1/1✓ Branch 1 taken 2 times.
|
2 | global_custom_verbs_.push_back(e.verb_str); |
| 231 | } | ||
| 232 | |||
| 233 | // Set matcher_idx, then move entire entry | ||
| 234 | 128 | e.matcher_idx = matcher_idx; | |
| 235 |
1/1✓ Branch 2 taken 128 times.
|
128 | entries.emplace_back(std::move(e)); |
| 236 | } | ||
| 237 | } | ||
| 238 | // ~scope_tracker sets matchers[matcher_idx].skip_ | ||
| 239 | 150 | } | |
| 240 | 132 | } | |
| 241 | |||
| 242 | // Restore path to a given base_path length | ||
| 243 | static void | ||
| 244 | 30 | restore_path( | |
| 245 | route_params_base& p, | ||
| 246 | std::size_t base_len) | ||
| 247 | { | ||
| 248 | 30 | p.base_path = { p.decoded_path_.data(), base_len }; | |
| 249 | // Account for the addedSlash_ when computing path length | ||
| 250 |
2/2✓ Branch 1 taken 25 times.
✓ Branch 2 taken 5 times.
|
30 | auto const path_len = p.decoded_path_.size() - (p.addedSlash_ ? 1 : 0); |
| 251 |
2/2✓ Branch 0 taken 29 times.
✓ Branch 1 taken 1 time.
|
30 | if(base_len < path_len) |
| 252 | 29 | p.path = { p.decoded_path_.data() + base_len, | |
| 253 | path_len - base_len }; | ||
| 254 | else | ||
| 255 | 2 | p.path = { p.decoded_path_.data() + | |
| 256 | 1 | p.decoded_path_.size() - 1, 1 }; // soft slash | |
| 257 | 30 | } | |
| 258 | |||
| 259 | route_task | ||
| 260 |
1/1✓ Branch 1 taken 107 times.
|
107 | dispatch_loop(route_params_base& p, bool is_options) const |
| 261 | { | ||
| 262 | // All checks happen BEFORE co_await to minimize coroutine launches. | ||
| 263 | // Avoid touching p.ep_ (expensive atomic on Windows) - use p.kind_ for mode checks. | ||
| 264 | |||
| 265 | std::size_t last_matched = SIZE_MAX; | ||
| 266 | std::uint32_t current_depth = 0; | ||
| 267 | |||
| 268 | // Collect methods from all matching end-route matchers for OPTIONS | ||
| 269 | std::uint64_t options_methods = 0; | ||
| 270 | std::vector<std::string> options_custom_verbs; | ||
| 271 | |||
| 272 | // Stack of base_path lengths at each depth level. | ||
| 273 | // path_stack[d] = base_path.size() before any matcher at depth d was tried. | ||
| 274 | std::size_t path_stack[detail::router_base::max_path_depth]; | ||
| 275 | path_stack[0] = 0; | ||
| 276 | |||
| 277 | // Track which matcher index is matched at each depth level. | ||
| 278 | // matched_at_depth[d] = matcher index that successfully matched at depth d. | ||
| 279 | std::size_t matched_at_depth[detail::router_base::max_path_depth]; | ||
| 280 | for(std::size_t d = 0; d < detail::router_base::max_path_depth; ++d) | ||
| 281 | matched_at_depth[d] = SIZE_MAX; | ||
| 282 | |||
| 283 | for(std::size_t i = 0; i < entries.size(); ) | ||
| 284 | { | ||
| 285 | auto const& e = entries[i]; | ||
| 286 | auto const& m = matchers[e.matcher_idx]; | ||
| 287 | auto const target_depth = m.depth_; | ||
| 288 | |||
| 289 | //-------------------------------------------------- | ||
| 290 | // Pre-invoke checks (no coroutine yet) | ||
| 291 | //-------------------------------------------------- | ||
| 292 | |||
| 293 | // For nested routes: verify ancestors at depths 0..target_depth-1 are matched. | ||
| 294 | // For siblings: if moving to same depth with different matcher, restore path. | ||
| 295 | bool ancestors_ok = true; | ||
| 296 | |||
| 297 | // Check if we need to match new ancestor matchers for this entry. | ||
| 298 | // We iterate through matchers from last_matched+1 to e.matcher_idx, | ||
| 299 | // but ONLY process those that are at depths we need (ancestors or self). | ||
| 300 | std::size_t start_idx = (last_matched == SIZE_MAX) ? 0 : last_matched + 1; | ||
| 301 | |||
| 302 | for(std::size_t check_idx = start_idx; | ||
| 303 | check_idx <= e.matcher_idx && ancestors_ok; | ||
| 304 | ++check_idx) | ||
| 305 | { | ||
| 306 | auto const& cm = matchers[check_idx]; | ||
| 307 | |||
| 308 | // Only check matchers that are: | ||
| 309 | // 1. Ancestors (depth < target_depth) that we haven't matched yet, or | ||
| 310 | // 2. The entry's own matcher | ||
| 311 | bool is_needed_ancestor = (cm.depth_ < target_depth) && | ||
| 312 | (matched_at_depth[cm.depth_] == SIZE_MAX); | ||
| 313 | bool is_self = (check_idx == e.matcher_idx); | ||
| 314 | |||
| 315 | if(!is_needed_ancestor && !is_self) | ||
| 316 | continue; | ||
| 317 | |||
| 318 | // Restore path if moving to same or shallower depth | ||
| 319 | if(cm.depth_ <= current_depth && current_depth > 0) | ||
| 320 | { | ||
| 321 | restore_path(p, path_stack[cm.depth_]); | ||
| 322 | } | ||
| 323 | |||
| 324 | // In error/exception mode, skip end routes | ||
| 325 | if(cm.end_ && p.kind_ != detail::router_base::is_plain) | ||
| 326 | { | ||
| 327 | i = cm.skip_; | ||
| 328 | ancestors_ok = false; | ||
| 329 | break; | ||
| 330 | } | ||
| 331 | |||
| 332 | // Apply effective_opts for this matcher | ||
| 333 | p.case_sensitive = (cm.effective_opts_ & 2) != 0; | ||
| 334 | p.strict = (cm.effective_opts_ & 8) != 0; | ||
| 335 | |||
| 336 | // Save path state before trying this matcher | ||
| 337 | if(cm.depth_ < detail::router_base::max_path_depth) | ||
| 338 | path_stack[cm.depth_] = p.base_path.size(); | ||
| 339 | |||
| 340 | match_result mr; | ||
| 341 | if(!cm(p, mr)) | ||
| 342 | { | ||
| 343 | // Clear matched_at_depth for this depth and deeper | ||
| 344 | for(std::size_t d = cm.depth_; d < detail::router_base::max_path_depth; ++d) | ||
| 345 | matched_at_depth[d] = SIZE_MAX; | ||
| 346 | i = cm.skip_; | ||
| 347 | ancestors_ok = false; | ||
| 348 | break; | ||
| 349 | } | ||
| 350 | |||
| 351 | // Copy captured params to route_params_base | ||
| 352 | if(!mr.params_.empty()) | ||
| 353 | { | ||
| 354 | for(auto& param : mr.params_) | ||
| 355 | p.params.push_back(std::move(param)); | ||
| 356 | } | ||
| 357 | |||
| 358 | // Mark this depth as matched | ||
| 359 | if(cm.depth_ < detail::router_base::max_path_depth) | ||
| 360 | matched_at_depth[cm.depth_] = check_idx; | ||
| 361 | |||
| 362 | last_matched = check_idx; | ||
| 363 | current_depth = cm.depth_ + 1; | ||
| 364 | |||
| 365 | // Save state for next depth level | ||
| 366 | if(current_depth < detail::router_base::max_path_depth) | ||
| 367 | path_stack[current_depth] = p.base_path.size(); | ||
| 368 | } | ||
| 369 | |||
| 370 | if(!ancestors_ok) | ||
| 371 | continue; | ||
| 372 | |||
| 373 | // Collect methods from matching end-route matchers for OPTIONS | ||
| 374 | if(is_options && m.end_) | ||
| 375 | { | ||
| 376 | options_methods |= m.allowed_methods_; | ||
| 377 | for(auto const& v : m.custom_verbs_) | ||
| 378 | options_custom_verbs.push_back(v); | ||
| 379 | } | ||
| 380 | |||
| 381 | // Check method match (only for end routes) | ||
| 382 | if(m.end_ && !e.match_method( | ||
| 383 | const_cast<route_params_base&>(p))) | ||
| 384 | { | ||
| 385 | ++i; | ||
| 386 | continue; | ||
| 387 | } | ||
| 388 | |||
| 389 | // Check kind match (cheap char comparison) | ||
| 390 | if(e.h->kind != p.kind_) | ||
| 391 | { | ||
| 392 | ++i; | ||
| 393 | continue; | ||
| 394 | } | ||
| 395 | |||
| 396 | //-------------------------------------------------- | ||
| 397 | // Invoke handler (coroutine starts here) | ||
| 398 | //-------------------------------------------------- | ||
| 399 | |||
| 400 | route_result rv; | ||
| 401 | try | ||
| 402 | { | ||
| 403 | rv = co_await e.h->invoke( | ||
| 404 | const_cast<route_params_base&>(p)); | ||
| 405 | } | ||
| 406 | catch(...) | ||
| 407 | { | ||
| 408 | // Only touch ep_ when actually catching | ||
| 409 | p.ep_ = std::current_exception(); | ||
| 410 | p.kind_ = detail::router_base::is_exception; | ||
| 411 | ++i; | ||
| 412 | continue; | ||
| 413 | } | ||
| 414 | |||
| 415 | //-------------------------------------------------- | ||
| 416 | // Handle result | ||
| 417 | // | ||
| 418 | // Coroutines invert control - handler does the send. | ||
| 419 | // route_what::done = handler completed request | ||
| 420 | // route_what::next = continue to next handler | ||
| 421 | // route_what::next_route = skip to next route | ||
| 422 | // route_what::close = close connection | ||
| 423 | // route_what::error = enter error mode | ||
| 424 | //-------------------------------------------------- | ||
| 425 | |||
| 426 | if(rv.what() == route_what::next) | ||
| 427 | { | ||
| 428 | ++i; | ||
| 429 | continue; | ||
| 430 | } | ||
| 431 | |||
| 432 | if(rv.what() == route_what::next_route) | ||
| 433 | { | ||
| 434 | // next_route only valid for end routes, not middleware | ||
| 435 | if(!m.end_) | ||
| 436 | // VFALCO this is a logic error | ||
| 437 | co_return route_error(error::invalid_route_result); | ||
| 438 | i = m.skip_; | ||
| 439 | continue; | ||
| 440 | } | ||
| 441 | |||
| 442 | if(rv.what() == route_what::done || | ||
| 443 | rv.what() == route_what::close) | ||
| 444 | { | ||
| 445 | // Handler completed or requested close | ||
| 446 | co_return rv; | ||
| 447 | } | ||
| 448 | |||
| 449 | // Error - transition to error mode | ||
| 450 | p.ec_ = rv.error(); | ||
| 451 | p.kind_ = detail::router_base::is_error; | ||
| 452 | |||
| 453 | if(m.end_) | ||
| 454 | { | ||
| 455 | // End routes don't have error handlers | ||
| 456 | i = m.skip_; | ||
| 457 | continue; | ||
| 458 | } | ||
| 459 | |||
| 460 | ++i; | ||
| 461 | } | ||
| 462 | |||
| 463 | // Final state | ||
| 464 | if(p.kind_ == detail::router_base::is_exception) | ||
| 465 | co_return route_error(error::unhandled_exception); | ||
| 466 | if(p.kind_ == detail::router_base::is_error) | ||
| 467 | co_return route_error(p.ec_); | ||
| 468 | |||
| 469 | // OPTIONS fallback: path matched but no explicit OPTIONS handler | ||
| 470 | if(is_options && options_methods != 0 && options_handler_) | ||
| 471 | { | ||
| 472 | // Build Allow header from collected methods | ||
| 473 | std::string allow = build_allow_header(options_methods, options_custom_verbs); | ||
| 474 | co_return co_await options_handler_->invoke(p, allow); | ||
| 475 | } | ||
| 476 | |||
| 477 | co_return route_next; // no handler matched | ||
| 478 | 214 | } | |
| 479 | }; | ||
| 480 | |||
| 481 | //------------------------------------------------ | ||
| 482 | |||
| 483 | 98 | flat_router:: | |
| 484 | flat_router( | ||
| 485 | 98 | detail::router_base&& src) | |
| 486 | 98 | : impl_(std::make_shared<impl>()) | |
| 487 | { | ||
| 488 |
1/1✓ Branch 2 taken 98 times.
|
98 | impl_->flatten(*src.impl_); |
| 489 | 98 | impl_->options_handler_ = std::move(src.options_handler_); | |
| 490 | 98 | } | |
| 491 | |||
| 492 | route_task | ||
| 493 | 104 | flat_router:: | |
| 494 | dispatch( | ||
| 495 | http::method verb, | ||
| 496 | urls::url_view const& url, | ||
| 497 | route_params_base& p) const | ||
| 498 | { | ||
| 499 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 103 times.
|
104 | if(verb == http::method::unknown) |
| 500 | 1 | detail::throw_invalid_argument(); | |
| 501 | |||
| 502 | // Handle OPTIONS * before normal dispatch | ||
| 503 |
4/4✓ Branch 0 taken 4 times.
✓ Branch 1 taken 99 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 3 times.
|
107 | if(verb == http::method::options && |
| 504 |
2/2✓ Branch 2 taken 1 time.
✓ Branch 3 taken 102 times.
|
107 | url.encoded_path() == "*") |
| 505 | { | ||
| 506 |
1/2✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
1 | if(impl_->options_handler_) |
| 507 | { | ||
| 508 | 1 | return impl_->options_handler_->invoke( | |
| 509 | 1 | p, impl_->global_allow_header_); | |
| 510 | } | ||
| 511 | // No handler, let it fall through to 404 | ||
| 512 | } | ||
| 513 | |||
| 514 | // Initialize params | ||
| 515 | 102 | p.kind_ = detail::router_base::is_plain; | |
| 516 | 102 | p.verb_ = verb; | |
| 517 | 102 | p.verb_str_.clear(); | |
| 518 | 102 | p.ec_.clear(); | |
| 519 | 102 | p.ep_ = nullptr; | |
| 520 | 102 | p.params.clear(); | |
| 521 |
1/1✓ Branch 2 taken 102 times.
|
102 | p.decoded_path_ = detail::pct_decode_path(url.encoded_path()); |
| 522 |
6/6✓ Branch 1 taken 101 times.
✓ Branch 2 taken 1 time.
✓ Branch 4 taken 68 times.
✓ Branch 5 taken 33 times.
✓ Branch 6 taken 69 times.
✓ Branch 7 taken 33 times.
|
102 | if(p.decoded_path_.empty() || p.decoded_path_.back() != '/') |
| 523 | { | ||
| 524 | 69 | p.decoded_path_.push_back('/'); | |
| 525 | 69 | p.addedSlash_ = true; | |
| 526 | } | ||
| 527 | else | ||
| 528 | { | ||
| 529 | 33 | p.addedSlash_ = false; | |
| 530 | } | ||
| 531 | // Set path views after potential reallocation from push_back | ||
| 532 | // Exclude added trailing slash from visible path, but keep "/" if empty | ||
| 533 | 102 | p.base_path = { p.decoded_path_.data(), 0 }; | |
| 534 |
4/4✓ Branch 0 taken 69 times.
✓ Branch 1 taken 33 times.
✓ Branch 3 taken 68 times.
✓ Branch 4 taken 1 time.
|
102 | auto const subtract = (p.addedSlash_ && p.decoded_path_.size() > 1) ? 1 : 0; |
| 535 | 102 | p.path = { p.decoded_path_.data(), p.decoded_path_.size() - subtract }; | |
| 536 | |||
| 537 | 102 | return impl_->dispatch_loop(p, verb == http::method::options); | |
| 538 | } | ||
| 539 | |||
| 540 | route_task | ||
| 541 | 6 | flat_router:: | |
| 542 | dispatch( | ||
| 543 | std::string_view verb, | ||
| 544 | urls::url_view const& url, | ||
| 545 | route_params_base& p) const | ||
| 546 | { | ||
| 547 |
2/2✓ Branch 1 taken 1 time.
✓ Branch 2 taken 5 times.
|
6 | if(verb.empty()) |
| 548 | 1 | detail::throw_invalid_argument(); | |
| 549 | |||
| 550 |
1/1✓ Branch 2 taken 5 times.
|
5 | auto const method = http::string_to_method(verb); |
| 551 | 5 | bool const is_options = (method == http::method::options); | |
| 552 | |||
| 553 | // Handle OPTIONS * before normal dispatch | ||
| 554 |
2/6✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 5 times.
|
5 | if(is_options && url.encoded_path() == "*") |
| 555 | { | ||
| 556 | ✗ | if(impl_->options_handler_) | |
| 557 | { | ||
| 558 | ✗ | return impl_->options_handler_->invoke( | |
| 559 | ✗ | p, impl_->global_allow_header_); | |
| 560 | } | ||
| 561 | // No handler, let it fall through to 404 | ||
| 562 | } | ||
| 563 | |||
| 564 | // Initialize params | ||
| 565 | 5 | p.kind_ = detail::router_base::is_plain; | |
| 566 | 5 | p.verb_ = method; | |
| 567 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 time.
|
5 | if(p.verb_ == http::method::unknown) |
| 568 | 4 | p.verb_str_ = verb; | |
| 569 | else | ||
| 570 | 1 | p.verb_str_.clear(); | |
| 571 | 5 | p.ec_.clear(); | |
| 572 | 5 | p.ep_ = nullptr; | |
| 573 | 5 | p.params.clear(); | |
| 574 |
1/1✓ Branch 2 taken 5 times.
|
5 | p.decoded_path_ = detail::pct_decode_path(url.encoded_path()); |
| 575 |
3/6✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 5 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 5 times.
|
5 | if(p.decoded_path_.empty() || p.decoded_path_.back() != '/') |
| 576 | { | ||
| 577 | ✗ | p.decoded_path_.push_back('/'); | |
| 578 | ✗ | p.addedSlash_ = true; | |
| 579 | } | ||
| 580 | else | ||
| 581 | { | ||
| 582 | 5 | p.addedSlash_ = false; | |
| 583 | } | ||
| 584 | // Set path views after potential reallocation from push_back | ||
| 585 | // Exclude added trailing slash from visible path, but keep "/" if empty | ||
| 586 | 5 | p.base_path = { p.decoded_path_.data(), 0 }; | |
| 587 |
1/4✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
|
5 | auto const subtract = (p.addedSlash_ && p.decoded_path_.size() > 1) ? 1 : 0; |
| 588 | 5 | p.path = { p.decoded_path_.data(), p.decoded_path_.size() - subtract }; | |
| 589 | |||
| 590 | 5 | return impl_->dispatch_loop(p, is_options); | |
| 591 | } | ||
| 592 | |||
| 593 | } // http | ||
| 594 | } // boost | ||
| 595 | |||
| 596 |