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