include/boost/burl/request_builder.hpp

100.0% Lines (18/18) 100.0% List of functions (7/7) 100.0% Branches (2/2)
request_builder.hpp
f(x) Functions (7)
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 #ifndef BOOST_BURL_REQUEST_BUILDER_HPP
11 #define BOOST_BURL_REQUEST_BUILDER_HPP
12
13 #include <boost/burl/conversion.hpp>
14 #include <boost/burl/detail/config.hpp>
15 #include <boost/burl/request.hpp>
16 #include <boost/burl/response.hpp>
17
18 #include <boost/capy/io_task.hpp>
19 #include <boost/http/field.hpp>
20 #include <boost/http/method.hpp>
21 #include <boost/url/url.hpp>
22
23 #include <chrono>
24 #include <string_view>
25 #include <type_traits>
26 #include <utility>
27
28 namespace boost
29 {
30 namespace burl
31 {
32
33 class client;
34
35 /** A builder for configuring and sending a request.
36
37 Objects of this type are created by the verb
38 functions of @ref client, such as
39 @ref client::get and @ref client::post. Member
40 functions configure the request and return the
41 builder by rvalue reference, allowing calls to
42 be chained. The chain is finished with
43 @ref send, @ref as, or @ref build, which
44 consume the builder.
45
46 @par Example
47 @code
48 auto r = co_await c.get("https://example.com/get")
49 .query("category", "shoes")
50 .header(http::field::accept_language, "en")
51 .as<json::value>();
52 @endcode
53
54 @see
55 @ref client,
56 @ref request,
57 @ref response.
58 */
59 class request_builder
60 {
61 client& client_;
62 request request_;
63
64 public:
65 /** Constructor.
66
67 Constructs a builder for a request with the
68 provided method and URL. Ownership of the
69 client is not transferred; it must remain
70 valid until the request has been sent.
71
72 @param client The client used to send the
73 request.
74
75 @param method The method of the request.
76
77 @param url The URL of the request.
78 */
79 57x request_builder(client& client, http::method method, urls::url url)
80 57x : client_(client)
81 57x , request_{ method, std::move(url), {}, {}, {} }
82 {
83 57x }
84
85 /** Append a parameter to the URL query.
86
87 The key and value are percent-encoded.
88
89 @par Example
90 @code
91 auto r = co_await c.get("https://example.com/get")
92 .query("category", "shoes")
93 .query("color", "blue")
94 .send();
95 @endcode
96
97 @param key The key of the parameter.
98
99 @param value The value of the parameter.
100
101 @return The builder, for chaining.
102 */
103 BOOST_BURL_DECL
104 request_builder&&
105 query(std::string_view key, std::string_view value) &&;
106
107 /** Set a request header.
108
109 Any existing values for the same field are
110 replaced. Headers set on the request take
111 precedence over the default headers of the
112 client with the same name.
113
114 @param field The field name constant.
115
116 @param value The value of the field.
117
118 @return The builder, for chaining.
119 */
120 BOOST_BURL_DECL
121 request_builder&&
122 header(http::field field, std::string_view value) &&;
123
124 /** Set a request header.
125
126 Any existing values for the same field are
127 replaced. Headers set on the request take
128 precedence over the default headers of the
129 client with the same name.
130
131 @param name The name of the field.
132
133 @param value The value of the field.
134
135 @return The builder, for chaining.
136 */
137 BOOST_BURL_DECL
138 request_builder&&
139 header(std::string_view name, std::string_view value) &&;
140
141 /** Set credentials for HTTP Basic authentication.
142
143 Sets the `Authorization` header of this
144 request to the Basic scheme with the
145 provided credentials, overriding any default
146 set on the client.
147
148 Credentials are not sent when a redirect
149 leads to a different origin, unless
150 @ref client::config::unrestricted_auth is
151 enabled.
152
153 @param user The username.
154
155 @param pass The password.
156
157 @return The builder, for chaining.
158 */
159 BOOST_BURL_DECL
160 request_builder&&
161 basic_auth(std::string_view user, std::string_view pass) &&;
162
163 /** Set a token for HTTP Bearer authentication.
164
165 Sets the `Authorization` header of this
166 request to the Bearer scheme with the
167 provided token, overriding any default set
168 on the client.
169
170 The token is not sent when a redirect leads
171 to a different origin, unless
172 @ref client::config::unrestricted_auth is
173 enabled.
174
175 @param token The bearer token.
176
177 @return The builder, for chaining.
178 */
179 BOOST_BURL_DECL
180 request_builder&&
181 bearer_auth(std::string_view token) &&;
182
183 /** Set a timeout for the entire operation.
184
185 Overrides @ref client::config::timeout for
186 this request.
187
188 @param dur The timeout duration.
189
190 @return The builder, for chaining.
191 */
192 request_builder&&
193 2x timeout(request::options::clock::duration dur) &&
194 {
195 2x request_.options.timeout = dur;
196 2x return std::move(*this);
197 }
198
199 /** Set whether redirect responses are followed.
200
201 Overrides
202 @ref client::config::followlocation for
203 this request. When disabled, the redirect
204 response is returned as-is; its `Location`
205 header can be inspected through the
206 @ref response.
207
208 @par Example
209 @code
210 auto [ec, r] = co_await c.get("https://example.com/moved")
211 .followlocation(false)
212 .send();
213 @endcode
214
215 @param enable `true` to follow redirects.
216
217 @return The builder, for chaining.
218 */
219 request_builder&&
220 3x followlocation(bool enable) &&
221 {
222 3x request_.options.followlocation = enable;
223 3x return std::move(*this);
224 }
225
226 /** Set the request body.
227
228 The value is converted to a request body by
229 calling `tag_invoke` with
230 @ref body_from_tag.
231
232 The `Content-Type` of the body is used
233 unless the header is set explicitly on the
234 request. The `Content-Length`, or chunked
235 transfer encoding, is always derived from
236 the body.
237
238 @par Example
239 @code
240 auto r = co_await c.post("https://example.com/post")
241 .body(json::value({ "key", "value" }))
242 .send();
243 @endcode
244
245 @param value The value to convert into the
246 body.
247
248 @param args Additional arguments forwarded
249 to the conversion.
250
251 @return The builder, for chaining.
252
253 @see
254 @ref body_from_tag,
255 @ref RequestBody.
256 */
257 template<class T, class... Args>
258 requires requires(T&& value, Args&&... args) {
259 tag_invoke(
260 body_from_tag<std::remove_cvref_t<T>>{},
261 std::forward<T>(value),
262 std::forward<Args>(args)...);
263 }
264 request_builder&&
265 4x body(T&& value, Args&&... args) &&
266 {
267
2/2
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 2 times.
4x request_.body = tag_invoke(
268 body_from_tag<std::remove_cvref_t<T>>{},
269 std::forward<T>(value),
270 std::forward<Args>(args)...);
271 4x return std::move(*this);
272 }
273
274 /** Set the request body.
275
276 @param body The body to set.
277
278 @return The builder, for chaining.
279 */
280 request_builder&&
281 1x body(any_request_body body) &&
282 {
283 1x request_.body = std::move(body);
284 1x return std::move(*this);
285 }
286
287 /** Return the configured request.
288
289 The returned request can be stored and
290 executed later with @ref client::execute.
291
292 @par Example
293 @code
294 burl::request req = c.post("https://example.com/post")
295 .header("X-Debug", "1")
296 .body("payload")
297 .build();
298
299 auto [ec, r] = co_await c.execute(std::move(req));
300 @endcode
301
302 @return The configured request.
303 */
304 request
305 24x build() &&
306 {
307 24x return std::move(request_);
308 }
309
310 /** Asynchronously send the request.
311
312 Sends the request and reads the response
313 status line and headers; the body is left
314 unread and can be consumed through the
315 returned @ref response. Equivalent to
316 passing the built request to
317 @ref client::execute.
318
319 A status code of 400 or above yields an error
320 code equal to @ref condition::client_error or
321 @ref condition::server_error. The @ref response
322 is still returned, so the body can be retrieved.
323
324 @par Example
325 @code
326 auto [ec, r] = co_await c.get("https://example.com").send();
327 @endcode
328
329 @return An awaitable yielding
330 `(error_code,response)`.
331 */
332 BOOST_BURL_DECL
333 capy::io_task<response>
334 send() &&;
335
336 /** Asynchronously send the request and convert the body.
337
338 Sends the request and converts the response
339 body to `T` by calling `tag_invoke` with
340 @ref body_to_tag.
341
342 A status code of 400 or above yields an error
343 code equal to @ref condition::client_error or
344 @ref condition::server_error. The body is
345 still read and converted, but conversion errors
346 are masked by the status error.
347
348 To examine the status code before consuming the
349 body, use @ref send instead.
350
351 @par Example
352 @code
353 auto [ec, body] = co_await c.get("https://example.com")
354 .try_as<std::string>();
355
356 if(ec == burl::condition::client_error)
357 {
358 std::cerr << ec.message() << '\n'; // e.g. HTTP 404 Not Found
359 std::cerr << body << '\n';
360 }
361 @endcode
362
363 @tparam T The type to convert the body to.
364
365 @param args Additional arguments forwarded
366 to the conversion.
367
368 @return An awaitable yielding
369 `(error_code,T)`.
370
371 @see
372 @ref body_to_tag,
373 @ref response::try_as.
374 */
375 template<class T, class... Args>
376 capy::io_task<T>
377 try_as(Args&&... args) &&
378 {
379 return try_as_impl<T>(std::move(*this), std::forward<Args>(args)...);
380 }
381
382 /** Asynchronously send the request and convert the body.
383
384 Sends the request and converts the response
385 body to `T` by calling `tag_invoke` with
386 @ref body_to_tag.
387
388 A status code of 400 or above throws without
389 reading the body. Use @ref try_as to inspect
390 an error response.
391
392 @par Example
393 @code
394 auto r = co_await c.get("https://example.com")
395 .as<std::string>();
396 @endcode
397
398 @throw std::system_error
399 The request, status code, or conversion failed.
400
401 @tparam T The type to convert the body to.
402
403 @param args Additional arguments forwarded
404 to the conversion.
405
406 @return An awaitable yielding the converted
407 body.
408
409 @see
410 @ref body_to_tag,
411 @ref response::as.
412 */
413 template<class T, class... Args>
414 capy::task<T>
415 as(Args&&... args) &&
416 {
417 return as_impl<T>(std::move(*this), std::forward<Args>(args)...);
418 }
419
420 private:
421 template<class T, class... Args>
422 static capy::task<T>
423 as_impl(request_builder rb, Args... args)
424 {
425 auto [ec, resp] = co_await std::move(rb).send();
426 if(ec)
427 throw std::system_error(ec);
428
429 auto [bec, body] = co_await resp.template try_as<T>(std::move(args)...);
430 if(bec)
431 throw std::system_error(bec);
432
433 co_return std::move(body);
434 }
435
436 template<class T, class... Args>
437 static capy::io_task<T>
438 try_as_impl(request_builder rb, Args... args)
439 {
440 auto [ec1, resp] = co_await std::move(rb).send();
441 if(ec1 && ec1 != condition::client_error && ec1 != condition::server_error)
442 co_return { ec1, {} };
443
444 // On a status error keep ec1; otherwise surface the conversion error.
445 auto [ec2, body] = co_await resp.template try_as<T>(std::move(args)...);
446 co_return { ec1 ? ec1 : ec2, std::move(body) };
447 }
448 };
449
450 } // namespace burl
451 } // namespace boost
452
453 #endif
454