src/client.cpp
100.0% Lines (74/74)
100.0% List of functions (16/16)
96.1% Branches (49/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
37x
100.0%
–
83.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
9x
100.0%
100.0%
100.0%
boost::burl::(anonymous namespace)::set_target(boost::http::request&, boost::urls::url_view const&)
:84
43x
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
45x
100.0%
100.0%
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
48x
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
61x
100.0%
100.0%
100.0%
boost::burl::client::execute(boost::burl::request)
:182
37x
100.0%
100.0%
70.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
37x
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 | #include "detail/serializer.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/capy/io/any_stream.hpp> | |||
| 24 | #include <boost/capy/timeout.hpp> | |||
| 25 | #include <boost/capy/write.hpp> | |||
| 26 | #include <boost/http/brotli/decode.hpp> | |||
| 27 | #include <boost/http/field.hpp> | |||
| 28 | #include <boost/http/request.hpp> | |||
| 29 | #include <boost/http/response_base.hpp> | |||
| 30 | #include <boost/http/response_parser.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 | 37x | set_accept_encoding( | ||
| 49 | http::parser_config& parser_cfg, | |||
| 50 | http::request& headers, | |||
| 51 | client::config const& cfg) | |||
| 52 | { | |||
| 53 | 37x | std::string accept_encoding; | ||
| 54 | 9x | auto const accept = [&](char const* coding) | ||
| 55 | { | |||
| 56 |
2/2✓ Branch 1 taken 5 times.
✓ Branch 2 taken 4 times.
|
9x | if(!accept_encoding.empty()) | |
| 57 | 5x | accept_encoding += ", "; | ||
| 58 | 9x | accept_encoding += coding; | ||
| 59 | 46x | }; | ||
| 60 | ||||
| 61 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 34 times.
|
37x | if(cfg.brotli) | |
| 62 | { | |||
| 63 | 3x | parser_cfg.apply_brotli_decoder = true; | ||
| 64 |
1/1✓ Branch 1 taken 3 times.
|
3x | accept("br"); | |
| 65 | } | |||
| 66 | ||||
| 67 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 34 times.
|
37x | if(cfg.deflate) | |
| 68 | { | |||
| 69 | 3x | parser_cfg.apply_deflate_decoder = true; | ||
| 70 |
1/1✓ Branch 1 taken 3 times.
|
3x | accept("deflate"); | |
| 71 | } | |||
| 72 | ||||
| 73 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 34 times.
|
37x | if(cfg.gzip) | |
| 74 | { | |||
| 75 | 3x | parser_cfg.apply_gzip_decoder = true; | ||
| 76 |
1/1✓ Branch 1 taken 3 times.
|
3x | accept("gzip"); | |
| 77 | } | |||
| 78 | ||||
| 79 |
2/2✓ Branch 1 taken 4 times.
✓ Branch 2 taken 33 times.
|
37x | if(!accept_encoding.empty()) | |
| 80 |
1/1✓ Branch 2 taken 4 times.
|
4x | headers.set(http::field::accept_encoding, accept_encoding); | |
| 81 | 37x | } | ||
| 82 | ||||
| 83 | void | |||
| 84 | 43x | set_target(http::request& headers, const urls::url_view& url) | ||
| 85 | { | |||
| 86 | 43x | auto target = url.encoded_target(); | ||
| 87 |
3/3✓ Branch 2 taken 43 times.
✓ Branch 7 taken 4 times.
✓ Branch 8 taken 39 times.
|
43x | if(url.path().empty()) | |
| 88 |
3/3✓ Branch 1 taken 4 times.
✓ Branch 4 taken 4 times.
✓ Branch 8 taken 4 times.
|
16x | headers.set_target("/" + std::string(target)); | |
| 89 | else | |||
| 90 |
1/1✓ Branch 2 taken 39 times.
|
39x | headers.set_target(target); | |
| 91 | 43x | } | ||
| 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 | 45x | client::client( | ||
| 101 | capy::executor_ref exec, | |||
| 102 | corosio::tls_context tls_ctx, | |||
| 103 | 45x | config cfg) | ||
| 104 | 45x | : config_(cfg) | ||
| 105 |
1/1✓ Branch 1 taken 45 times.
|
45x | , pool_( | |
| 106 | std::make_shared<detail::connection_pool>( | |||
| 107 | 90x | exec, std::move(tls_ctx), cfg)) | ||
| 108 | { | |||
| 109 | // Disable codings whose decoder service is unavailable. | |||
| 110 |
1/1✓ Branch 1 taken 45 times.
|
45x | auto const& ctx = capy::get_system_context(); | |
| 111 |
2/2✓ Branch 1 taken 29 times.
✓ Branch 2 taken 16 times.
|
45x | if(!ctx.has_service<http::brotli::decode_service>()) | |
| 112 | 29x | config_.brotli = false; | ||
| 113 |
2/2✓ Branch 1 taken 28 times.
✓ Branch 2 taken 17 times.
|
45x | if(!ctx.has_service<http::zlib::inflate_service>()) | |
| 114 | 28x | config_.deflate = config_.gzip = false; | ||
| 115 | 45x | } | ||
| 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 | 48x | client::get(urls::url_view url) | ||
| 141 | { | |||
| 142 | 48x | 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 | 61x | client::request(http::method method, urls::url_view url) | ||
| 177 | { | |||
| 178 |
1/1✓ Branch 1 taken 61 times.
|
61x | return { *this, method, url }; | |
| 179 | } | |||
| 180 | ||||
| 181 | capy::io_task<response> | |||
| 182 | 37x | client::execute(burl::request request) | ||
| 183 | { | |||
| 184 | auto timeout = | |||
| 185 |
2/2✓ Branch 1 taken 1 time.
✓ Branch 2 taken 36 times.
|
37x | request.options.timeout ? request.options.timeout : config_.timeout; | |
| 186 |
2/2✓ Branch 1 taken 34 times.
✓ Branch 2 taken 3 times.
|
37x | if(!timeout) | |
| 187 |
1/1✓ Branch 4 taken 34 times.
|
34x | return execute_impl(std::move(request), std::nullopt); | |
| 188 | ||||
| 189 |
1/1✓ Branch 3 taken 3 times.
|
3x | auto deadline = config::clock::now() + *timeout; | |
| 190 |
2/2✓ Branch 5 taken 3 times.
✓ Branch 8 taken 3 times.
|
3x | return capy::timeout(execute_impl(std::move(request), deadline), *timeout); | |
| 191 | } | |||
| 192 | ||||
| 193 | capy::io_task<response> | |||
| 194 |
1/1✓ Branch 1 taken 37 times.
|
37x | 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::response_parser parser( | |||
| 235 | http::make_parser_config(parser_cfg)); | |||
| 236 | ||||
| 237 | auto url = request.url; | |||
| 238 | auto trusted = true; | |||
| 239 | auto followlocation = request.options.followlocation.value_or(config_.followlocation); | |||
| 240 | auto maxredirs = config_.maxredirs; | |||
| 241 | auto request_cookies = request.headers.value_or(field::cookie, ""); | |||
| 242 | for(;;) | |||
| 243 | { | |||
| 244 | set_target(headers, url); | |||
| 245 | headers.set(field::host, url.encoded_host_and_port()); | |||
| 246 | ||||
| 247 | // set cookies | |||
| 248 | headers.erase(field::cookie); | |||
| 249 | if(!request_cookies.empty()) | |||
| 250 | { | |||
| 251 | if(trusted) | |||
| 252 | headers.set(field::cookie, request_cookies); | |||
| 253 | } | |||
| 254 | else if(config_.cookies) | |||
| 255 | { | |||
| 256 | auto cookies = cookie_jar_.cookie_header(url); | |||
| 257 | if(!cookies.empty()) | |||
| 258 | headers.set(field::cookie, cookies); | |||
| 259 | } | |||
| 260 | ||||
| 261 | auto [cec, conn] = co_await pool_->acquire(url); | |||
| 262 | if(cec) | |||
| 263 | co_return { cec, {} }; | |||
| 264 | ||||
| 265 | // TODO: expect100timeout | |||
| 266 | ||||
| 267 | if(request.body.has_value()) | |||
| 268 | { | |||
| 269 | capy::any_write_stream ws(&conn); | |||
| 270 | detail::serializer sr(ws, headers); | |||
| 271 | capy::any_buffer_sink sink(&sr); | |||
| 272 | if(auto [wec] = co_await request.body.write(sink); wec) | |||
| 273 | co_return { wec, {} }; | |||
| 274 | if(!sr.is_done()) | |||
| 275 | { | |||
| 276 | if(auto [wec] = co_await sr.write_eof(); wec) | |||
| 277 | co_return { wec, {} }; | |||
| 278 | } | |||
| 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, drained] = co_await capy::timeout( | |||
| 327 | detail::drain_body(parser, capy::any_stream(&conn), 1024 * 1024), | |||
| 328 | std::chrono::seconds(2)); | |||
| 329 | if(drained && 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 | 74x | } | ||
| 372 | ||||
| 373 | } // namespace burl | |||
| 374 | } // namespace boost | |||
| 375 |