src/client.cpp

100.0% Lines (71/71) 100.0% List of functions (16/16) 95.9% Branches (47/49)
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&) :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