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