src/client.cpp
100.0% Lines (71/71)
100.0% List of functions (16/16)
95.9% Branches (47/49)
Functions (16)
Function
Calls
Lines
Branches
Blocks
boost::burl::(anonymous namespace)::set_accept_encoding(boost::burl::request_head&, boost::burl::client::config const&)
:46
29x
100.0%
–
89.0%
boost::burl::(anonymous namespace)::set_accept_encoding(boost::burl::request_head&, boost::burl::client::config const&)::{lambda(char const*)#1}::operator()(char const*) const
:51
5x
100.0%
100.0%
100.0%
boost::burl::(anonymous namespace)::set_target(boost::burl::request_head&, boost::urls::url_view const&)
:75
35x
100.0%
100.0%
62.0%
boost::burl::client::client(boost::capy::executor_ref, boost::corosio::tls_context)
:86
13x
100.0%
60.0%
60.0%
boost::burl::client::client(boost::capy::executor_ref, boost::corosio::tls_context, boost::burl::client::config)
:91
37x
100.0%
100.0%
71.0%
boost::burl::client::basic_auth(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >)
:111
1x
100.0%
100.0%
64.0%
boost::burl::client::bearer_auth(std::basic_string_view<char, std::char_traits<char> >)
:124
1x
100.0%
100.0%
67.0%
boost::burl::client::get(boost::urls::url_view)
:133
40x
100.0%
–
100.0%
boost::burl::client::head(boost::urls::url_view)
:139
2x
100.0%
–
100.0%
boost::burl::client::post(boost::urls::url_view)
:145
6x
100.0%
–
100.0%
boost::burl::client::put(boost::urls::url_view)
:151
1x
100.0%
–
100.0%
boost::burl::client::patch(boost::urls::url_view)
:157
1x
100.0%
–
100.0%
boost::burl::client::delete_(boost::urls::url_view)
:163
1x
100.0%
–
100.0%
boost::burl::client::request(boost::http::method, boost::urls::url_view)
:169
53x
100.0%
100.0%
100.0%
boost::burl::client::execute(boost::burl::request)
:175
29x
100.0%
100.0%
42.0%
boost::burl::client::execute_impl(boost::burl::request, std::optional<std::chrono::time_point<std::chrono::_V2::steady_clock, std::chrono::duration<long, std::ratio<1l, 1000000000l> > > >)
:188
29x
100.0%
100.0%
42.0%
| Line | Branch | TLA | Hits | Source Code |
|---|---|---|---|---|
| 1 | // | |||
| 2 | // Copyright (c) 2026 Mohammad Nejati | |||
| 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/burl | |||
| 8 | // | |||
| 9 | ||||
| 10 | #include <boost/burl/client.hpp> | |||
| 11 | #include <boost/burl/error.hpp> | |||
| 12 | #include <boost/burl/request_head.hpp> | |||
| 13 | ||||
| 14 | #include "detail/base64.hpp" | |||
| 15 | #include "detail/can_reuse_conn.hpp" | |||
| 16 | #include "detail/connection_pool.hpp" | |||
| 17 | #include "detail/drain_body.hpp" | |||
| 18 | #include "detail/redirect.hpp" | |||
| 19 | ||||
| 20 | #include <boost/capy/buffers/make_buffer.hpp> | |||
| 21 | #include <boost/capy/ex/execution_context.hpp> | |||
| 22 | #include <boost/capy/ex/system_context.hpp> | |||
| 23 | #include <boost/corosio/timeout.hpp> | |||
| 24 | #include <boost/burl/message_reader.hpp> | |||
| 25 | #include <boost/burl/message_writer.hpp> | |||
| 26 | #include <boost/burl/response_parser.hpp> | |||
| 27 | #include <boost/burl/serializer.hpp> | |||
| 28 | #include <boost/http/brotli/decode.hpp> | |||
| 29 | #include <boost/http/zlib/inflate.hpp> | |||
| 30 | #include <boost/http/zstd/decompress.hpp> | |||
| 31 | ||||
| 32 | #include <chrono> | |||
| 33 | #include <optional> | |||
| 34 | #include <string> | |||
| 35 | #include <utility> | |||
| 36 | ||||
| 37 | namespace boost | |||
| 38 | { | |||
| 39 | namespace burl | |||
| 40 | { | |||
| 41 | ||||
| 42 | namespace | |||
| 43 | { | |||
| 44 | ||||
| 45 | void | |||
| 46 | 29x | set_accept_encoding( | ||
| 47 | request_head& head, | |||
| 48 | client::config const& cfg) | |||
| 49 | { | |||
| 50 | 29x | std::string accept_encoding; | ||
| 51 | 5x | auto const accept = [&](char const* coding) | ||
| 52 | { | |||
| 53 |
2/2✓ Branch 1 taken 1 time.
✓ Branch 2 taken 4 times.
|
5x | if(!accept_encoding.empty()) | |
| 54 | 1x | accept_encoding += ", "; | ||
| 55 | 5x | accept_encoding += coding; | ||
| 56 | 34x | }; | ||
| 57 | ||||
| 58 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 28 times.
|
29x | if(cfg.brotli) | |
| 59 |
1/1✓ Branch 1 taken 1 time.
|
1x | accept("br"); | |
| 60 | ||||
| 61 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 28 times.
|
29x | if(cfg.deflate) | |
| 62 |
1/1✓ Branch 1 taken 1 time.
|
1x | accept("deflate"); | |
| 63 | ||||
| 64 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 28 times.
|
29x | if(cfg.gzip) | |
| 65 |
1/1✓ Branch 1 taken 1 time.
|
1x | accept("gzip"); | |
| 66 | ||||
| 67 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 27 times.
|
29x | if(cfg.zstd) | |
| 68 |
1/1✓ Branch 1 taken 2 times.
|
2x | accept("zstd"); | |
| 69 | ||||
| 70 |
2/2✓ Branch 1 taken 4 times.
✓ Branch 2 taken 25 times.
|
29x | if(!accept_encoding.empty()) | |
| 71 |
1/1✓ Branch 2 taken 4 times.
|
4x | head.set(http::field::accept_encoding, accept_encoding); | |
| 72 | 29x | } | ||
| 73 | ||||
| 74 | void | |||
| 75 | 35x | set_target(request_head& head, const urls::url_view& url) | ||
| 76 | { | |||
| 77 | 35x | auto target = url.encoded_target(); | ||
| 78 |
3/3✓ Branch 2 taken 35 times.
✓ Branch 7 taken 2 times.
✓ Branch 8 taken 33 times.
|
35x | if(url.path().empty()) | |
| 79 |
3/3✓ Branch 1 taken 2 times.
✓ Branch 4 taken 2 times.
✓ Branch 8 taken 2 times.
|
6x | head.set_target("/" + std::string(target)); | |
| 80 | else | |||
| 81 |
1/1✓ Branch 2 taken 33 times.
|
33x | head.set_target(target); | |
| 82 | 35x | } | ||
| 83 | ||||
| 84 | } // namespace | |||
| 85 | ||||
| 86 | 13x | client::client(capy::executor_ref exec, corosio::tls_context tls_ctx) | ||
| 87 |
3/5✓ Branch 3 taken 13 times.
✓ Branch 8 taken 13 times.
✓ Branch 15 taken 13 times.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
|
13x | : client(exec, std::move(tls_ctx), config{}) | |
| 88 | { | |||
| 89 | 13x | } | ||
| 90 | ||||
| 91 | 37x | client::client( | ||
| 92 | capy::executor_ref exec, | |||
| 93 | corosio::tls_context tls_ctx, | |||
| 94 | 37x | config cfg) | ||
| 95 | 37x | : config_(cfg) | ||
| 96 |
1/1✓ Branch 1 taken 37 times.
|
37x | , pool_( | |
| 97 | std::make_shared<detail::connection_pool>( | |||
| 98 | 74x | exec, std::move(tls_ctx), cfg)) | ||
| 99 | { | |||
| 100 | // Disable codings whose decoder service is unavailable. | |||
| 101 |
1/1✓ Branch 1 taken 37 times.
|
37x | auto const& ctx = capy::get_system_context(); | |
| 102 |
2/2✓ Branch 1 taken 29 times.
✓ Branch 2 taken 8 times.
|
37x | if(!ctx.has_service<http::brotli::decode_service>()) | |
| 103 | 29x | config_.brotli = false; | ||
| 104 |
2/2✓ Branch 1 taken 28 times.
✓ Branch 2 taken 9 times.
|
37x | if(!ctx.has_service<http::zlib::inflate_service>()) | |
| 105 | 28x | config_.deflate = config_.gzip = false; | ||
| 106 |
2/2✓ Branch 1 taken 30 times.
✓ Branch 2 taken 7 times.
|
37x | if(!ctx.has_service<http::zstd::decompress_service>()) | |
| 107 | 30x | config_.zstd = false; | ||
| 108 | 37x | } | ||
| 109 | ||||
| 110 | void | |||
| 111 | 1x | client::basic_auth(std::string_view user, std::string_view pass) | ||
| 112 | { | |||
| 113 |
1/1✓ Branch 1 taken 1 time.
|
1x | std::string credentials{ user }; | |
| 114 |
1/1✓ Branch 1 taken 1 time.
|
1x | credentials += ':'; | |
| 115 |
1/1✓ Branch 1 taken 1 time.
|
1x | credentials += pass; | |
| 116 | ||||
| 117 |
1/1✓ Branch 1 taken 1 time.
|
1x | std::string value = "Basic "; | |
| 118 |
1/1✓ Branch 2 taken 1 time.
|
1x | detail::base64_encode(value, credentials); | |
| 119 | ||||
| 120 |
1/1✓ Branch 2 taken 1 time.
|
1x | headers_.set(http::field::authorization, value); | |
| 121 | 1x | } | ||
| 122 | ||||
| 123 | void | |||
| 124 | 1x | client::bearer_auth(std::string_view token) | ||
| 125 | { | |||
| 126 |
1/1✓ Branch 1 taken 1 time.
|
1x | std::string value = "Bearer "; | |
| 127 |
1/1✓ Branch 1 taken 1 time.
|
1x | value += token; | |
| 128 | ||||
| 129 |
1/1✓ Branch 2 taken 1 time.
|
1x | headers_.set(http::field::authorization, value); | |
| 130 | 1x | } | ||
| 131 | ||||
| 132 | request_builder | |||
| 133 | 40x | client::get(urls::url_view url) | ||
| 134 | { | |||
| 135 | 40x | return request(http::method::get, url); | ||
| 136 | } | |||
| 137 | ||||
| 138 | request_builder | |||
| 139 | 2x | client::head(urls::url_view url) | ||
| 140 | { | |||
| 141 | 2x | return request(http::method::head, url); | ||
| 142 | } | |||
| 143 | ||||
| 144 | request_builder | |||
| 145 | 6x | client::post(urls::url_view url) | ||
| 146 | { | |||
| 147 | 6x | return request(http::method::post, url); | ||
| 148 | } | |||
| 149 | ||||
| 150 | request_builder | |||
| 151 | 1x | client::put(urls::url_view url) | ||
| 152 | { | |||
| 153 | 1x | return request(http::method::put, url); | ||
| 154 | } | |||
| 155 | ||||
| 156 | request_builder | |||
| 157 | 1x | client::patch(urls::url_view url) | ||
| 158 | { | |||
| 159 | 1x | return request(http::method::patch, url); | ||
| 160 | } | |||
| 161 | ||||
| 162 | request_builder | |||
| 163 | 1x | client::delete_(urls::url_view url) | ||
| 164 | { | |||
| 165 | 1x | return request(http::method::delete_, url); | ||
| 166 | } | |||
| 167 | ||||
| 168 | request_builder | |||
| 169 | 53x | client::request(http::method method, urls::url_view url) | ||
| 170 | { | |||
| 171 |
1/1✓ Branch 1 taken 53 times.
|
53x | return { *this, method, url }; | |
| 172 | } | |||
| 173 | ||||
| 174 | capy::io_task<response> | |||
| 175 |
1/1✓ Branch 1 taken 29 times.
|
29x | client::execute(burl::request request) | |
| 176 | { | |||
| 177 | auto timeout = | |||
| 178 | request.options.timeout ? request.options.timeout : config_.timeout; | |||
| 179 | if(!timeout) | |||
| 180 | co_return co_await execute_impl(std::move(request), std::nullopt); | |||
| 181 | ||||
| 182 | auto deadline = config::clock::now() + *timeout; | |||
| 183 | co_return co_await corosio::timeout( | |||
| 184 | execute_impl(std::move(request), deadline), *timeout); | |||
| 185 | 58x | } | ||
| 186 | ||||
| 187 | capy::io_task<response> | |||
| 188 |
1/1✓ Branch 1 taken 29 times.
|
29x | client::execute_impl( | |
| 189 | burl::request request, | |||
| 190 | std::optional<config::clock::time_point> deadline) | |||
| 191 | { | |||
| 192 | using field = http::field; | |||
| 193 | ||||
| 194 | request_head head(request.method, "/", config_.version); | |||
| 195 | ||||
| 196 | for(auto f : headers_) | |||
| 197 | if(!request.headers.contains(f.name)) | |||
| 198 | head.append(f.name, f.value); | |||
| 199 | ||||
| 200 | for(auto f : request.headers) | |||
| 201 | head.append(f.name, f.value); | |||
| 202 | ||||
| 203 | if(request.body.has_value()) | |||
| 204 | { | |||
| 205 | // Use the body's content type only if the caller did not set one. | |||
| 206 | if(!head.contains(field::content_type)) | |||
| 207 | { | |||
| 208 | if(auto ct = request.body.content_type()) | |||
| 209 | head.set(field::content_type, ct.value()); | |||
| 210 | } | |||
| 211 | ||||
| 212 | // Content length is always derived from the body. | |||
| 213 | if(auto cl = request.body.content_length()) | |||
| 214 | head.set_content_length(cl.value()); | |||
| 215 | else | |||
| 216 | head.set_chunked(true); | |||
| 217 | } | |||
| 218 | ||||
| 219 | auto const is_head = head.method() == http::method::head; | |||
| 220 | auto const auto_decode = !head.contains(field::accept_encoding); | |||
| 221 | if(auto_decode) | |||
| 222 | set_accept_encoding(head, config_); | |||
| 223 | ||||
| 224 | response_parser parser( | |||
| 225 | { | |||
| 226 | .hdr_limits = {}, | |||
| 227 | .in_buffer = config_.response_inplace_buffer, | |||
| 228 | .dec_buffer = config_.response_inplace_buffer, | |||
| 229 | .body_limit = config_.response_body_limit, | |||
| 230 | .decode = auto_decode | |||
| 231 | }); | |||
| 232 | serializer sr({}); | |||
| 233 | ||||
| 234 | auto url = request.url; | |||
| 235 | auto trusted = true; | |||
| 236 | auto followlocation = request.options.followlocation.value_or(config_.followlocation); | |||
| 237 | auto maxredirs = config_.maxredirs; | |||
| 238 | auto request_cookies = request.headers.value_or(field::cookie, ""); | |||
| 239 | for(;;) | |||
| 240 | { | |||
| 241 | set_target(head, url); | |||
| 242 | head.set(field::host, url.encoded_host_and_port()); | |||
| 243 | ||||
| 244 | // set cookies | |||
| 245 | head.erase(field::cookie); | |||
| 246 | if(!request_cookies.empty()) | |||
| 247 | { | |||
| 248 | if(trusted) | |||
| 249 | head.set(field::cookie, request_cookies); | |||
| 250 | } | |||
| 251 | else if(config_.cookies) | |||
| 252 | { | |||
| 253 | auto cookies = cookie_jar_.cookie_header(url); | |||
| 254 | if(!cookies.empty()) | |||
| 255 | head.set(field::cookie, cookies); | |||
| 256 | } | |||
| 257 | ||||
| 258 | auto [ec, conn] = co_await pool_->acquire(url); | |||
| 259 | if(ec) | |||
| 260 | co_return { ec, response{} }; | |||
| 261 | ||||
| 262 | // TODO: expect100timeout | |||
| 263 | ||||
| 264 | sr.start(&head); | |||
| 265 | message_writer writer(&conn, &sr); | |||
| 266 | if(request.body.has_value()) | |||
| 267 | { | |||
| 268 | http::any_buffer_sink sink(&writer); | |||
| 269 | std::tie(ec) = co_await request.body.write(sink); | |||
| 270 | if(ec) | |||
| 271 | co_return { ec, response{} }; | |||
| 272 | } | |||
| 273 | if(!sr.is_done()) | |||
| 274 | { | |||
| 275 | std::tie(ec) = co_await writer.write_eof(); | |||
| 276 | if(ec) | |||
| 277 | co_return { ec, response{} }; | |||
| 278 | } | |||
| 279 | ||||
| 280 | parser.reset(); | |||
| 281 | parser.start(is_head); | |||
| 282 | ||||
| 283 | std::tie(ec) = co_await message_reader{ | |||
| 284 | &conn, &parser }.read_header(); | |||
| 285 | if(ec) | |||
| 286 | co_return { ec, response{} }; | |||
| 287 | ||||
| 288 | // extract cookies | |||
| 289 | if(config_.cookies) | |||
| 290 | { | |||
| 291 | for(auto sv : parser.get().find_all(field::set_cookie)) | |||
| 292 | { | |||
| 293 | auto rs = parse_cookie(sv); | |||
| 294 | if(rs.has_value()) | |||
| 295 | cookie_jar_.add(url, rs.value()); | |||
| 296 | } | |||
| 297 | } | |||
| 298 | ||||
| 299 | auto [is_redirect, need_method_change] = | |||
| 300 | detail::is_redirect(parser.get().status(), config_); | |||
| 301 | ||||
| 302 | if(!is_redirect || !followlocation) | |||
| 303 | { | |||
| 304 | auto status_int = parser.get().status_int(); | |||
| 305 | ec = status_int >= 400 | |||
| 306 | ? std::error_code(status_int, burl_category()) | |||
| 307 | : std::error_code(); | |||
| 308 | ||||
| 309 | co_return { | |||
| 310 | ec, | |||
| 311 | response{ url, std::move(conn), std::move(parser), deadline } | |||
| 312 | }; | |||
| 313 | } | |||
| 314 | ||||
| 315 | // Read and discard small bodies so the connection can be reused | |||
| 316 | auto [dec, drained] = co_await corosio::timeout( | |||
| 317 | detail::drain_body(conn, parser, 3), | |||
| 318 | std::chrono::seconds(2)); | |||
| 319 | if(drained && detail::can_reuse_conn(parser)) | |||
| 320 | conn.return_to_pool(); | |||
| 321 | ||||
| 322 | if(maxredirs-- == 0) | |||
| 323 | co_return { error::too_many_redirects, response{} }; | |||
| 324 | ||||
| 325 | // Set the Referer header to the URL we are leaving. | |||
| 326 | if(config_.autoreferer) | |||
| 327 | { | |||
| 328 | auto referer = url; | |||
| 329 | referer.remove_userinfo(); | |||
| 330 | referer.remove_fragment(); | |||
| 331 | head.set(field::referer, referer.buffer()); | |||
| 332 | } | |||
| 333 | ||||
| 334 | // Prepare the next request to follow the redirect | |||
| 335 | url = detail::resolve_location(parser.get(), url); | |||
| 336 | if(url.empty()) | |||
| 337 | co_return { error::bad_redirect_response, response{} }; | |||
| 338 | ||||
| 339 | // Change the method according to RFC 9110, Section 15.4.4. | |||
| 340 | if(need_method_change && head.method() != http::method::head) | |||
| 341 | { | |||
| 342 | head.set_method(http::method::get); | |||
| 343 | head.erase(field::content_length); | |||
| 344 | head.erase(field::transfer_encoding); | |||
| 345 | head.erase(field::content_encoding); | |||
| 346 | head.erase(field::content_type); | |||
| 347 | head.erase(field::expect); | |||
| 348 | request.body = {}; // drop the body | |||
| 349 | } | |||
| 350 | ||||
| 351 | trusted = (request.url.encoded_origin() == url.encoded_origin()) || | |||
| 352 | config_.unrestricted_auth; | |||
| 353 | ||||
| 354 | if(!trusted) | |||
| 355 | { | |||
| 356 | head.erase(field::authorization); | |||
| 357 | head.erase(field::proxy_authorization); | |||
| 358 | // cookies are removed on each iteration | |||
| 359 | } | |||
| 360 | } | |||
| 361 | 58x | } | ||
| 362 | ||||
| 363 | } // namespace burl | |||
| 364 | } // namespace boost | |||
| 365 |