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::burl::request_head&, boost::burl::client::config const&) :45 28x 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 :50 4x 100.0% 100.0% 100.0% boost::burl::(anonymous namespace)::set_target(boost::burl::request_head&, boost::urls::url_view const&) :74 34x 100.0% 100.0% 62.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% 64.0% boost::burl::client::bearer_auth(std::basic_string_view<char, std::char_traits<char> >) :125 1x 100.0% 100.0% 67.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 #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/content_coding.hpp"
18 #include "detail/decoders.hpp"
19 #include "detail/drain_body.hpp"
20 #include "detail/redirect.hpp"
21 #include "detail/serializer.hpp"
22
23 #include <boost/capy/buffers/make_buffer.hpp>
24 #include <boost/capy/ex/execution_context.hpp>
25 #include <boost/capy/io/any_write_stream.hpp>
26 #include <boost/capy/write.hpp>
27 #include <boost/corosio/timeout.hpp>
28 #include <boost/burl/message_reader.hpp>
29 #include <boost/burl/response_parser.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 request_head& head,
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 head.set(http::field::accept_encoding, accept_encoding);
71 28x }
72
73 void
74 34x set_target(request_head& head, 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 head.set_target("/" + std::string(target));
79 else
80
1/1
✓ Branch 2 taken 32 times.
32x head.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 request_head head(request.method, "/", config_.version);
196
197 for(auto f : headers_)
198 if(!request.headers.contains(f.name))
199 head.append(f.name, f.value);
200
201 for(auto f : request.headers)
202 head.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(!head.contains(field::content_type))
208 {
209 if(auto ct = request.body.content_type())
210 head.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 head.set_content_length(cl.value());
216 else
217 head.set_chunked(true);
218 }
219
220 auto const is_head = head.method() == http::method::head;
221 auto const auto_decode = !head.contains(field::accept_encoding);
222 if(auto_decode)
223 set_accept_encoding(head, config_);
224
225 response_parser parser(
226 {
227 .hdr_limits = {},
228 .in_buffer = config_.response_inplace_buffer,
229 .dec_buffer = config_.response_inplace_buffer,
230 .body_limit = config_.response_body_limit
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(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 [cec, conn] = co_await pool_->acquire(url);
259 if(cec)
260 co_return { cec, {} };
261
262 // TODO: expect100timeout
263
264 capy::any_write_stream stream(&conn);
265 sr.reset(&stream, &head);
266 if(request.body.has_value())
267 {
268 http::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();
279 parser.start(is_head);
280
281 auto [rec] = co_await message_reader{
282 &conn, &parser }.read_header();
283 if(rec)
284 co_return { rec, {} };
285
286 // extract cookies
287 if(config_.cookies)
288 {
289 for(auto sv : parser.get().find_all(field::set_cookie))
290 {
291 auto rs = parse_cookie(sv);
292 if(rs.has_value())
293 cookie_jar_.add(url, rs.value());
294 }
295 }
296
297 auto [is_redirect, need_method_change] =
298 detail::is_redirect(parser.get().status(), config_);
299
300 if(!is_redirect || !followlocation)
301 {
302 auto ec = std::error_code{};
303 auto status_int = parser.get().status_int();
304 if(status_int >= 400)
305 ec = std::error_code(status_int, burl_category());
306
307 std::unique_ptr<parser::decoder> dec;
308 if(auto_decode && !is_head)
309 {
310 dec = detail::make_decoder(
311 detail::content_coding(parser.get()));
312 parser.set_decoder(dec.get());
313 }
314
315 co_return {
316 ec,
317 response{ url, std::move(conn), std::move(parser),
318 std::move(dec), deadline }
319 };
320 }
321
322 // Read and discard small bodies so the connection can be reused
323 auto [dec, drained] = co_await corosio::timeout(
324 detail::drain_body(conn, parser, 3),
325 std::chrono::seconds(2));
326 if(drained && detail::can_reuse_conn(parser))
327 conn.return_to_pool();
328
329 if(maxredirs-- == 0)
330 co_return { error::too_many_redirects, {} };
331
332 // Set the Referer header to the URL we are leaving.
333 if(config_.autoreferer)
334 {
335 auto referer = url;
336 referer.remove_userinfo();
337 referer.remove_fragment();
338 head.set(field::referer, referer.buffer());
339 }
340
341 // Prepare the next request to follow the redirect
342 url = detail::resolve_location(parser.get(), url);
343 if(url.empty())
344 co_return { error::bad_redirect_response, {} };
345
346 // Change the method according to RFC 9110, Section 15.4.4.
347 if(need_method_change && head.method() != http::method::head)
348 {
349 head.set_method(http::method::get);
350 head.erase(field::content_length);
351 head.erase(field::transfer_encoding);
352 head.erase(field::content_encoding);
353 head.erase(field::content_type);
354 head.erase(field::expect);
355 request.body = {}; // drop the body
356 }
357
358 trusted = (request.url.encoded_origin() == url.encoded_origin()) ||
359 config_.unrestricted_auth;
360
361 if(!trusted)
362 {
363 head.erase(field::authorization);
364 head.erase(field::proxy_authorization);
365 // cookies are removed on each iteration
366 }
367 }
368 56x }
369
370 } // namespace burl
371 } // namespace boost
372