src/client.cpp

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