LCOV - code coverage report
Current view: top level - libs/http/src/server - flat_router.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 95.9 % 147 141
Test Date: 2026-02-02 17:02:49 Functions: 100.0 % 12 12

            Line data    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          165 :         if(methods == ~0ULL)
      74           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         5032 :         for(auto const& [m, name] : known)
     114              :         {
     115         4884 :             if(methods & (1ULL << static_cast<unsigned>(m)))
     116              :             {
     117          111 :                 if(!result.empty())
     118            7 :                     result += ", ";
     119          111 :                 result += name;
     120              :             }
     121              :         }
     122              :         // Append custom verbs
     123          152 :         for(auto const& v : custom)
     124              :         {
     125            4 :             if(!result.empty())
     126            0 :                 result += ", ";
     127            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          132 :         if(child & 2)
     141            4 :             result = (result & ~6) | 2;
     142          128 :         else if(child & 4)
     143            2 :             result = (result & ~6) | 4;
     144              : 
     145              :         // strict: bits 3-4 (8=true, 16=false)
     146          132 :         if(child & 8)
     147            1 :             result = (result & ~24) | 8;
     148          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          248 :         for(auto& m : matchers)
     166              :         {
     167          150 :             if(m.end_)
     168           65 :                 m.allow_header_ = build_allow_header(
     169           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          196 :         global_custom_verbs_.erase(
     175           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           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          282 :         for(auto& layer : src.layers)
     190              :         {
     191              :             // Move matcher, set flat router fields
     192          150 :             std::size_t matcher_idx = matchers.size();
     193          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          312 :             for(auto& e : layer.entries)
     203              :             {
     204          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           34 :                     if(nested && nested->impl_)
     209           34 :                         flatten_recursive(*nested->impl_, eff, depth + 1);
     210              :                 }
     211              :                 else
     212              :                 {
     213              :                     // Collect methods for OPTIONS (only for end routes)
     214          128 :                     if(m.end_)
     215              :                     {
     216              :                         // Per-matcher collection
     217           68 :                         if(e.all)
     218            8 :                             m.allowed_methods_ = ~0ULL;
     219           60 :                         else if(e.verb != http::method::unknown)
     220           58 :                             m.allowed_methods_ |= (1ULL << static_cast<unsigned>(e.verb));
     221            2 :                         else if(!e.verb_str.empty())
     222            2 :                             m.custom_verbs_.push_back(e.verb_str);
     223              : 
     224              :                         // Global collection (for OPTIONS *)
     225           68 :                         if(e.all)
     226            8 :                             global_methods_ = ~0ULL;
     227           60 :                         else if(e.verb != http::method::unknown)
     228           58 :                             global_methods_ |= (1ULL << static_cast<unsigned>(e.verb));
     229            2 :                         else if(!e.verb_str.empty())
     230            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          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           30 :         auto const path_len = p.decoded_path_.size() - (p.addedSlash_ ? 1 : 0);
     251           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          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           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          104 :     if(verb == http::method::unknown)
     500            1 :         detail::throw_invalid_argument();
     501              : 
     502              :     // Handle OPTIONS * before normal dispatch
     503          107 :     if(verb == http::method::options &&
     504          107 :        url.encoded_path() == "*")
     505              :     {
     506            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          102 :     p.decoded_path_ = detail::pct_decode_path(url.encoded_path());
     522          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          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            6 :     if(verb.empty())
     548            1 :         detail::throw_invalid_argument();
     549              : 
     550            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            5 :     if(is_options && url.encoded_path() == "*")
     555              :     {
     556            0 :         if(impl_->options_handler_)
     557              :         {
     558            0 :             return impl_->options_handler_->invoke(
     559            0 :                 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            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            5 :     p.decoded_path_ = detail::pct_decode_path(url.encoded_path());
     575            5 :     if(p.decoded_path_.empty() || p.decoded_path_.back() != '/')
     576              :     {
     577            0 :         p.decoded_path_.push_back('/');
     578            0 :         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            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              : 
        

Generated by: LCOV version 2.3