src/client.cpp

82.4% Lines (61/74) 93.8% List of functions (15/16) 62.7% Branches (32/51)
client.cpp
f(x) 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