Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 : //
4 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 : //
7 : // Official repository: https://github.com/cppalliance/http
8 : //
9 :
10 : #include "src/server/detail/router_base.hpp"
11 : #include <boost/http/server/detail/router_base.hpp>
12 : #include <boost/http/detail/except.hpp>
13 : #include <boost/http/error.hpp>
14 : #include <boost/http/field.hpp>
15 : #include <boost/http/status.hpp>
16 : #include <boost/url/grammar/ci_string.hpp>
17 : #include <boost/url/grammar/hexdig_chars.hpp>
18 : #include "src/server/detail/pct_decode.hpp"
19 :
20 : #include <algorithm>
21 :
22 : namespace boost {
23 : namespace http {
24 : namespace detail {
25 :
26 : //------------------------------------------------
27 : //
28 : // impl helpers
29 : //
30 : //------------------------------------------------
31 :
32 : std::string
33 300 : router_base::impl::
34 : build_allow_header(
35 : std::uint64_t methods,
36 : std::vector<std::string> const& custom)
37 : {
38 300 : if(methods == ~0ULL)
39 40 : return "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT";
40 :
41 280 : std::string result;
42 : static constexpr std::pair<http::method, char const*> known[] = {
43 : {http::method::acl, "ACL"},
44 : {http::method::bind, "BIND"},
45 : {http::method::checkout, "CHECKOUT"},
46 : {http::method::connect, "CONNECT"},
47 : {http::method::copy, "COPY"},
48 : {http::method::delete_, "DELETE"},
49 : {http::method::get, "GET"},
50 : {http::method::head, "HEAD"},
51 : {http::method::link, "LINK"},
52 : {http::method::lock, "LOCK"},
53 : {http::method::merge, "MERGE"},
54 : {http::method::mkactivity, "MKACTIVITY"},
55 : {http::method::mkcalendar, "MKCALENDAR"},
56 : {http::method::mkcol, "MKCOL"},
57 : {http::method::move, "MOVE"},
58 : {http::method::msearch, "M-SEARCH"},
59 : {http::method::notify, "NOTIFY"},
60 : {http::method::options, "OPTIONS"},
61 : {http::method::patch, "PATCH"},
62 : {http::method::post, "POST"},
63 : {http::method::propfind, "PROPFIND"},
64 : {http::method::proppatch, "PROPPATCH"},
65 : {http::method::purge, "PURGE"},
66 : {http::method::put, "PUT"},
67 : {http::method::rebind, "REBIND"},
68 : {http::method::report, "REPORT"},
69 : {http::method::search, "SEARCH"},
70 : {http::method::subscribe, "SUBSCRIBE"},
71 : {http::method::trace, "TRACE"},
72 : {http::method::unbind, "UNBIND"},
73 : {http::method::unlink, "UNLINK"},
74 : {http::method::unlock, "UNLOCK"},
75 : {http::method::unsubscribe, "UNSUBSCRIBE"},
76 : };
77 9520 : for(auto const& [m, name] : known)
78 : {
79 9240 : if(methods & (1ULL << static_cast<unsigned>(m)))
80 : {
81 274 : if(!result.empty())
82 46 : result += ", ";
83 274 : result += name;
84 : }
85 : }
86 288 : for(auto const& v : custom)
87 : {
88 8 : if(!result.empty())
89 2 : result += ", ";
90 8 : result += v;
91 : }
92 280 : return result;
93 280 : }
94 :
95 : router_base::opt_flags
96 598 : router_base::impl::
97 : compute_effective_opts(
98 : opt_flags parent,
99 : opt_flags child)
100 : {
101 598 : opt_flags result = parent;
102 :
103 : // case_sensitive: bits 1-2 (2=true, 4=false)
104 598 : if(child & 2)
105 4 : result = (result & ~6) | 2;
106 594 : else if(child & 4)
107 4 : result = (result & ~6) | 4;
108 :
109 : // strict: bits 3-4 (8=true, 16=false)
110 598 : if(child & 8)
111 1 : result = (result & ~24) | 8;
112 597 : else if(child & 16)
113 1 : result = (result & ~24) | 16;
114 :
115 598 : return result;
116 : }
117 :
118 : void
119 127 : router_base::impl::
120 : restore_path(
121 : route_params& p,
122 : std::size_t base_len)
123 : {
124 127 : auto& pv = *route_params_access{p};
125 127 : p.base_path = { pv.decoded_path_.data(), base_len };
126 127 : auto const path_len = pv.decoded_path_.size() - (pv.addedSlash_ ? 1 : 0);
127 127 : if(base_len < path_len)
128 126 : p.path = { pv.decoded_path_.data() + base_len,
129 : path_len - base_len };
130 : else
131 2 : p.path = { pv.decoded_path_.data() +
132 1 : pv.decoded_path_.size() - 1, 1 }; // soft slash
133 127 : }
134 :
135 : void
136 111 : router_base::impl::
137 : update_allow_for_entry(
138 : matcher& m,
139 : entry const& e)
140 : {
141 111 : if(!m.end_)
142 0 : return;
143 :
144 : // Per-matcher collection
145 111 : if(e.all)
146 9 : m.allowed_methods_ = ~0ULL;
147 102 : else if(e.verb != http::method::unknown)
148 99 : m.allowed_methods_ |= (1ULL << static_cast<unsigned>(e.verb));
149 3 : else if(!e.verb_str.empty())
150 3 : m.custom_verbs_.push_back(e.verb_str);
151 :
152 : // Rebuild per-matcher Allow header eagerly
153 111 : m.allow_header_ = build_allow_header(
154 111 : m.allowed_methods_, m.custom_verbs_);
155 :
156 : // Global collection (for OPTIONS *)
157 111 : if(e.all)
158 9 : global_methods_ = ~0ULL;
159 102 : else if(e.verb != http::method::unknown)
160 99 : global_methods_ |= (1ULL << static_cast<unsigned>(e.verb));
161 3 : else if(!e.verb_str.empty())
162 3 : global_custom_verbs_.push_back(e.verb_str);
163 : }
164 :
165 : void
166 165 : router_base::impl::
167 : rebuild_global_allow_header()
168 : {
169 165 : std::sort(global_custom_verbs_.begin(), global_custom_verbs_.end());
170 330 : global_custom_verbs_.erase(
171 165 : std::unique(global_custom_verbs_.begin(), global_custom_verbs_.end()),
172 165 : global_custom_verbs_.end());
173 165 : global_allow_header_ = build_allow_header(
174 165 : global_methods_, global_custom_verbs_);
175 165 : }
176 :
177 : void
178 481 : router_base::impl::
179 : finalize_pending()
180 : {
181 481 : if(pending_route_ == SIZE_MAX)
182 378 : return;
183 103 : auto& m = matchers[pending_route_];
184 103 : if(entries.size() == m.first_entry_)
185 : {
186 : // empty route, remove it
187 0 : matchers.pop_back();
188 : }
189 : else
190 : {
191 103 : m.skip_ = entries.size();
192 : }
193 103 : pending_route_ = SIZE_MAX;
194 : }
195 :
196 : //------------------------------------------------
197 : //
198 : // dispatch
199 : //
200 : //------------------------------------------------
201 :
202 : route_task
203 204 : router_base::impl::
204 : dispatch_loop(route_params& p, bool is_options) const
205 : {
206 : auto& pv = *route_params_access{p};
207 :
208 : std::size_t last_matched = SIZE_MAX;
209 : std::uint32_t current_depth = 0;
210 :
211 : std::uint64_t matched_methods = 0;
212 : std::vector<std::string> matched_custom_verbs;
213 :
214 : std::size_t path_stack[router_base::max_path_depth];
215 : path_stack[0] = 0;
216 :
217 : std::size_t matched_at_depth[router_base::max_path_depth];
218 : for(std::size_t d = 0; d < router_base::max_path_depth; ++d)
219 : matched_at_depth[d] = SIZE_MAX;
220 :
221 : for(std::size_t i = 0; i < entries.size(); )
222 : {
223 : auto const& e = entries[i];
224 : auto const& m = matchers[e.matcher_idx];
225 : auto const target_depth = m.depth_;
226 :
227 : bool ancestors_ok = true;
228 :
229 : std::size_t start_idx = (last_matched == SIZE_MAX) ? 0 : last_matched + 1;
230 :
231 : for(std::size_t check_idx = start_idx;
232 : check_idx <= e.matcher_idx && ancestors_ok;
233 : ++check_idx)
234 : {
235 : auto const& cm = matchers[check_idx];
236 :
237 : bool is_needed_ancestor = (cm.depth_ < target_depth) &&
238 : (matched_at_depth[cm.depth_] == SIZE_MAX);
239 : bool is_self = (check_idx == e.matcher_idx);
240 :
241 : if(!is_needed_ancestor && !is_self)
242 : continue;
243 :
244 : if(cm.depth_ <= current_depth && current_depth > 0)
245 : {
246 : restore_path(p, path_stack[cm.depth_]);
247 : }
248 :
249 : if(cm.end_ && pv.kind_ != router_base::is_plain)
250 : {
251 : i = cm.skip_;
252 : ancestors_ok = false;
253 : break;
254 : }
255 :
256 : pv.case_sensitive = (cm.effective_opts_ & 2) != 0;
257 : pv.strict = (cm.effective_opts_ & 8) != 0;
258 :
259 : if(cm.depth_ < router_base::max_path_depth)
260 : path_stack[cm.depth_] = p.base_path.size();
261 :
262 : match_result mr;
263 : if(!cm(p, mr))
264 : {
265 : for(std::size_t d = cm.depth_; d < router_base::max_path_depth; ++d)
266 : matched_at_depth[d] = SIZE_MAX;
267 : i = cm.skip_;
268 : ancestors_ok = false;
269 : break;
270 : }
271 :
272 : if(!mr.params_.empty())
273 : {
274 : for(auto& param : mr.params_)
275 : p.params.push_back(std::move(param));
276 : }
277 :
278 : if(cm.depth_ < router_base::max_path_depth)
279 : matched_at_depth[cm.depth_] = check_idx;
280 :
281 : last_matched = check_idx;
282 : current_depth = cm.depth_ + 1;
283 :
284 : if(current_depth < router_base::max_path_depth)
285 : path_stack[current_depth] = p.base_path.size();
286 : }
287 :
288 : if(!ancestors_ok)
289 : continue;
290 :
291 : // Collect methods from matching end-route matchers
292 : if(m.end_)
293 : {
294 : matched_methods |= m.allowed_methods_;
295 : for(auto const& v : m.custom_verbs_)
296 : matched_custom_verbs.push_back(v);
297 : }
298 :
299 : if(m.end_ && !e.match_method(
300 : const_cast<route_params&>(p)))
301 : {
302 : ++i;
303 : continue;
304 : }
305 :
306 : if(e.h->kind != pv.kind_)
307 : {
308 : ++i;
309 : continue;
310 : }
311 :
312 : //--------------------------------------------------
313 : // Invoke handler
314 : //--------------------------------------------------
315 :
316 : route_result rv;
317 : try
318 : {
319 : rv = co_await e.h->invoke(
320 : const_cast<route_params&>(p));
321 : }
322 : catch(...)
323 : {
324 : pv.ep_ = std::current_exception();
325 : pv.kind_ = router_base::is_exception;
326 : ++i;
327 : continue;
328 : }
329 :
330 : if(rv.what() == route_what::next)
331 : {
332 : ++i;
333 : continue;
334 : }
335 :
336 : if(rv.what() == route_what::next_route)
337 : {
338 : if(!m.end_)
339 : co_return route_error(error::invalid_route_result);
340 : i = m.skip_;
341 : continue;
342 : }
343 :
344 : if(rv.what() == route_what::done ||
345 : rv.what() == route_what::close)
346 : {
347 : co_return rv;
348 : }
349 :
350 : // Error - transition to error mode
351 : pv.ec_ = rv.error();
352 : pv.kind_ = router_base::is_error;
353 :
354 : if(m.end_)
355 : {
356 : i = m.skip_;
357 : continue;
358 : }
359 :
360 : ++i;
361 : }
362 :
363 : if(pv.kind_ == router_base::is_exception)
364 : co_return route_error(error::unhandled_exception);
365 : if(pv.kind_ == router_base::is_error)
366 : co_return route_error(pv.ec_);
367 :
368 : // OPTIONS fallback
369 : if(is_options && matched_methods != 0 && options_handler_)
370 : {
371 : std::string allow = build_allow_header(matched_methods, matched_custom_verbs);
372 : co_return co_await options_handler_->invoke(p, allow);
373 : }
374 :
375 : // 405 fallback: path matched but method didn't
376 : if(!is_options &&
377 : (matched_methods != 0 || !matched_custom_verbs.empty()))
378 : {
379 : std::string allow = build_allow_header(matched_methods, matched_custom_verbs);
380 : p.res.set(field::allow, allow);
381 : p.res.set_status(status::method_not_allowed);
382 : (void)(co_await p.send());
383 : co_return route_done;
384 : }
385 :
386 : co_return route_next;
387 408 : }
388 :
389 : //------------------------------------------------
390 : //
391 : // router_base
392 : //
393 : //------------------------------------------------
394 :
395 222 : router_base::
396 : router_base(
397 222 : opt_flags opt)
398 222 : : impl_(std::make_shared<impl>(opt))
399 : {
400 222 : }
401 :
402 : void
403 107 : router_base::
404 : add_middleware(
405 : std::string_view pattern,
406 : handlers hn)
407 : {
408 107 : impl_->finalize_pending();
409 :
410 107 : if(pattern.empty())
411 76 : pattern = "/";
412 :
413 107 : auto const matcher_idx = impl_->matchers.size();
414 107 : impl_->matchers.emplace_back(pattern, false);
415 107 : auto& m = impl_->matchers.back();
416 107 : if(m.error())
417 0 : throw_invalid_argument();
418 107 : m.first_entry_ = impl_->entries.size();
419 107 : m.effective_opts_ = impl::compute_effective_opts(0, impl_->opt_);
420 107 : m.own_opts_ = impl_->opt_;
421 107 : m.depth_ = 0;
422 :
423 223 : for(std::size_t i = 0; i < hn.n; ++i)
424 : {
425 116 : impl_->entries.emplace_back(std::move(hn.p[i]));
426 116 : impl_->entries.back().matcher_idx = matcher_idx;
427 : }
428 :
429 107 : m.skip_ = impl_->entries.size();
430 107 : }
431 :
432 : void
433 55 : router_base::
434 : inline_router(
435 : std::string_view pattern,
436 : router_base&& sub)
437 : {
438 55 : impl_->finalize_pending();
439 :
440 55 : if(!sub.impl_)
441 0 : return;
442 :
443 55 : sub.impl_->finalize_pending();
444 :
445 55 : if(pattern.empty())
446 0 : pattern = "/";
447 :
448 : // Create parent matcher for the mount point
449 55 : auto const parent_matcher_idx = impl_->matchers.size();
450 55 : impl_->matchers.emplace_back(pattern, false);
451 55 : auto& parent_m = impl_->matchers.back();
452 55 : if(parent_m.error())
453 0 : throw_invalid_argument();
454 55 : parent_m.first_entry_ = impl_->entries.size();
455 :
456 55 : auto parent_eff = impl::compute_effective_opts(0, impl_->opt_);
457 55 : parent_m.effective_opts_ = parent_eff;
458 55 : parent_m.own_opts_ = impl_->opt_;
459 55 : parent_m.depth_ = 0;
460 :
461 : // Check nesting depth
462 55 : std::size_t max_sub_depth = 0;
463 349 : for(auto const& sm : sub.impl_->matchers)
464 588 : max_sub_depth = (std::max)(max_sub_depth,
465 294 : static_cast<std::size_t>(sm.depth_));
466 55 : if(max_sub_depth + 1 >= max_path_depth)
467 1 : throw_length_error(
468 : "router nesting depth exceeds max_path_depth");
469 :
470 : // Compute offsets for re-indexing
471 54 : auto const matcher_offset = impl_->matchers.size();
472 54 : auto const entry_offset = impl_->entries.size();
473 :
474 : // Recompute effective_opts for inlined matchers using depth stack
475 54 : auto sub_root_eff = impl::compute_effective_opts(
476 54 : parent_eff, sub.impl_->opt_);
477 : opt_flags eff_stack[max_path_depth];
478 54 : eff_stack[0] = sub_root_eff;
479 :
480 : // Inline sub's matchers
481 332 : for(auto& sm : sub.impl_->matchers)
482 : {
483 278 : auto d = sm.depth_;
484 278 : opt_flags parent = (d > 0) ? eff_stack[d - 1] : parent_eff;
485 278 : eff_stack[d] = impl::compute_effective_opts(parent, sm.own_opts_);
486 278 : sm.effective_opts_ = eff_stack[d];
487 278 : sm.depth_ += 1; // increase by 1 (parent is at depth 0)
488 278 : sm.first_entry_ += entry_offset;
489 278 : sm.skip_ += entry_offset;
490 278 : impl_->matchers.push_back(std::move(sm));
491 : }
492 :
493 : // Inline sub's entries
494 114 : for(auto& se : sub.impl_->entries)
495 : {
496 60 : se.matcher_idx += matcher_offset;
497 60 : impl_->entries.push_back(std::move(se));
498 : }
499 :
500 : // Set parent matcher's skip
501 : // Need to re-fetch since vector may have reallocated
502 54 : impl_->matchers[parent_matcher_idx].skip_ = impl_->entries.size();
503 :
504 : // Merge global methods
505 54 : impl_->global_methods_ |= sub.impl_->global_methods_;
506 54 : for(auto& v : sub.impl_->global_custom_verbs_)
507 0 : impl_->global_custom_verbs_.push_back(std::move(v));
508 54 : impl_->rebuild_global_allow_header();
509 :
510 : // Move options handler if sub has one and parent doesn't
511 54 : if(sub.impl_->options_handler_ && !impl_->options_handler_)
512 0 : impl_->options_handler_ = std::move(sub.impl_->options_handler_);
513 :
514 54 : sub.impl_.reset();
515 : }
516 :
517 : std::size_t
518 116 : router_base::
519 : new_route(
520 : std::string_view pattern)
521 : {
522 116 : impl_->finalize_pending();
523 :
524 116 : if(pattern.empty())
525 0 : throw_invalid_argument();
526 :
527 116 : auto const idx = impl_->matchers.size();
528 116 : impl_->matchers.emplace_back(pattern, true);
529 116 : auto& m = impl_->matchers.back();
530 116 : if(m.error())
531 12 : throw_invalid_argument();
532 104 : m.first_entry_ = impl_->entries.size();
533 104 : m.effective_opts_ = impl::compute_effective_opts(0, impl_->opt_);
534 104 : m.own_opts_ = impl_->opt_;
535 104 : m.depth_ = 0;
536 :
537 104 : impl_->pending_route_ = idx;
538 104 : return idx;
539 : }
540 :
541 : void
542 99 : router_base::
543 : add_to_route(
544 : std::size_t idx,
545 : http::method verb,
546 : handlers hn)
547 : {
548 99 : if(verb == http::method::unknown)
549 0 : throw_invalid_argument();
550 :
551 99 : auto& m = impl_->matchers[idx];
552 198 : for(std::size_t i = 0; i < hn.n; ++i)
553 : {
554 99 : impl_->entries.emplace_back(verb, std::move(hn.p[i]));
555 99 : impl_->entries.back().matcher_idx = idx;
556 99 : impl_->update_allow_for_entry(m, impl_->entries.back());
557 : }
558 99 : impl_->rebuild_global_allow_header();
559 99 : }
560 :
561 : void
562 12 : router_base::
563 : add_to_route(
564 : std::size_t idx,
565 : std::string_view verb,
566 : handlers hn)
567 : {
568 12 : auto& m = impl_->matchers[idx];
569 :
570 12 : if(verb.empty())
571 : {
572 : // all methods
573 18 : for(std::size_t i = 0; i < hn.n; ++i)
574 : {
575 9 : impl_->entries.emplace_back(std::move(hn.p[i]));
576 9 : impl_->entries.back().matcher_idx = idx;
577 9 : impl_->update_allow_for_entry(m, impl_->entries.back());
578 : }
579 : }
580 : else
581 : {
582 : // specific method string
583 6 : for(std::size_t i = 0; i < hn.n; ++i)
584 : {
585 3 : impl_->entries.emplace_back(verb, std::move(hn.p[i]));
586 3 : impl_->entries.back().matcher_idx = idx;
587 3 : impl_->update_allow_for_entry(m, impl_->entries.back());
588 : }
589 : }
590 12 : impl_->rebuild_global_allow_header();
591 12 : }
592 :
593 : void
594 0 : router_base::
595 : finalize_pending()
596 : {
597 0 : if(impl_)
598 0 : impl_->finalize_pending();
599 0 : }
600 :
601 : void
602 224 : router_base::
603 : set_options_handler_impl(
604 : options_handler_ptr p)
605 : {
606 224 : impl_->options_handler_ = std::move(p);
607 224 : }
608 :
609 : //------------------------------------------------
610 : //
611 : // dispatch
612 : //
613 : //------------------------------------------------
614 :
615 : route_task
616 202 : router_base::
617 : dispatch(
618 : http::method verb,
619 : urls::url_view const& url,
620 : route_params& p) const
621 : {
622 202 : if(verb == http::method::unknown)
623 1 : throw_invalid_argument();
624 :
625 201 : impl_->ensure_finalized();
626 :
627 : // Handle OPTIONS * before normal dispatch
628 247 : if(verb == http::method::options &&
629 247 : url.encoded_path() == "*")
630 : {
631 2 : if(impl_->options_handler_)
632 : {
633 2 : return impl_->options_handler_->invoke(
634 2 : p, impl_->global_allow_header_);
635 : }
636 : }
637 :
638 : // Initialize params
639 199 : auto& pv = *route_params_access{p};
640 199 : pv.kind_ = is_plain;
641 199 : pv.verb_ = verb;
642 199 : pv.verb_str_.clear();
643 199 : pv.ec_.clear();
644 199 : pv.ep_ = nullptr;
645 199 : p.params.clear();
646 199 : pv.decoded_path_ = pct_decode_path(url.encoded_path());
647 199 : if(pv.decoded_path_.empty() || pv.decoded_path_.back() != '/')
648 : {
649 114 : pv.decoded_path_.push_back('/');
650 114 : pv.addedSlash_ = true;
651 : }
652 : else
653 : {
654 85 : pv.addedSlash_ = false;
655 : }
656 199 : p.base_path = { pv.decoded_path_.data(), 0 };
657 199 : auto const subtract = (pv.addedSlash_ && pv.decoded_path_.size() > 1) ? 1 : 0;
658 199 : p.path = { pv.decoded_path_.data(), pv.decoded_path_.size() - subtract };
659 :
660 199 : return impl_->dispatch_loop(p, verb == http::method::options);
661 : }
662 :
663 : route_task
664 6 : router_base::
665 : dispatch(
666 : std::string_view verb,
667 : urls::url_view const& url,
668 : route_params& p) const
669 : {
670 6 : if(verb.empty())
671 1 : throw_invalid_argument();
672 :
673 5 : impl_->ensure_finalized();
674 :
675 5 : auto const method = http::string_to_method(verb);
676 5 : bool const is_options = (method == http::method::options);
677 :
678 : // Handle OPTIONS * before normal dispatch
679 5 : if(is_options && url.encoded_path() == "*")
680 : {
681 0 : if(impl_->options_handler_)
682 : {
683 0 : return impl_->options_handler_->invoke(
684 0 : p, impl_->global_allow_header_);
685 : }
686 : }
687 :
688 : // Initialize params
689 5 : auto& pv = *route_params_access{p};
690 5 : pv.kind_ = is_plain;
691 5 : pv.verb_ = method;
692 5 : if(pv.verb_ == http::method::unknown)
693 4 : pv.verb_str_ = verb;
694 : else
695 1 : pv.verb_str_.clear();
696 5 : pv.ec_.clear();
697 5 : pv.ep_ = nullptr;
698 5 : p.params.clear();
699 5 : pv.decoded_path_ = pct_decode_path(url.encoded_path());
700 5 : if(pv.decoded_path_.empty() || pv.decoded_path_.back() != '/')
701 : {
702 0 : pv.decoded_path_.push_back('/');
703 0 : pv.addedSlash_ = true;
704 : }
705 : else
706 : {
707 5 : pv.addedSlash_ = false;
708 : }
709 5 : p.base_path = { pv.decoded_path_.data(), 0 };
710 5 : auto const subtract = (pv.addedSlash_ && pv.decoded_path_.size() > 1) ? 1 : 0;
711 5 : p.path = { pv.decoded_path_.data(), pv.decoded_path_.size() - subtract };
712 :
713 5 : return impl_->dispatch_loop(p, is_options);
714 : }
715 :
716 : } // detail
717 : } // http
718 : } // boost
|