include/boost/burl/response.hpp

88.9% Lines (8/9) 100.0% List of functions (4/4) 50.0% Branches (1/2)
response.hpp
f(x) Functions (4)
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_RESPONSE_HPP
11 #define BOOST_BURL_RESPONSE_HPP
12
13 #include <boost/burl/conversion.hpp>
14 #include <boost/burl/detail/config.hpp>
15 #include <boost/burl/detail/connection_pool.hpp>
16 #include <boost/burl/error.hpp>
17 #include <boost/capy/io/any_buffer_source.hpp>
18 #include <boost/capy/io/any_read_source.hpp>
19 #include <boost/capy/io_task.hpp>
20 #include <boost/http/fields_base.hpp>
21 #include <boost/http/metadata.hpp>
22 #include <boost/http/response_parser.hpp>
23 #include <boost/http/status.hpp>
24 #include <boost/http/version.hpp>
25 #include <boost/url/url.hpp>
26 #include <boost/url/url_view.hpp>
27
28 #include <chrono>
29 #include <optional>
30 #include <string_view>
31 #include <system_error>
32 #include <utility>
33
34 namespace boost
35 {
36 namespace burl
37 {
38
39 /** The response to an HTTP request.
40
41 Objects of this type provide access to the
42 status, headers, and body of a response. The
43 status line and headers have already been read
44 when the response is obtained; the body remains
45 unread on the connection and is consumed through
46 the body functions.
47
48 The response owns the connection it was received
49 on. Upon destruction, the connection is returned
50 to the pool for reuse when it can be kept alive
51 and the entire message has arrived.
52
53 A response remains usable after the client which
54 produced it is destroyed; in that case the
55 connection is closed upon destruction instead of
56 being returned to the pool.
57
58 @par Example
59 @code
60 auto [ec, r] = co_await c.get("https://example.com").send();
61
62 if(ec)
63 throw std::system_error(ec);
64
65 std::cout << "status: " << r.status_int() << '\n';
66 std::cout << "headers: " << r.headers() << '\n';
67 std::cout << "body: " << co_await r.as<std::string>() << '\n';
68 @endcode
69
70 @see
71 @ref client::execute,
72 @ref request_builder::send.
73 */
74 class response
75 {
76 friend class client;
77 using clock = std::chrono::steady_clock;
78
79 urls::url url_;
80 detail::pooled_connection conn_;
81 http::response_parser parser_;
82 std::optional<clock::time_point> deadline_;
83
84 response(
85 urls::url url,
86 detail::pooled_connection conn,
87 http::response_parser parser,
88 std::optional<clock::time_point> deadline);
89
90 public:
91 /** Constructor.
92
93 A default-constructed response is not
94 associated with any request, and is intended
95 only as a target for assignment.
96 */
97 6x response() = default;
98
99 /** Copy constructor (deleted).
100 */
101 response(response const&) = delete;
102
103 /** Copy assignment (deleted).
104 */
105 response&
106 operator=(response const&) = delete;
107
108 /** Move constructor.
109
110 Constructs a response by taking ownership of
111 the contents of another response, including
112 the underlying connection. The moved-from
113 response no longer owns a connection.
114
115 @param other The response to move from.
116 */
117 BOOST_BURL_DECL
118 response(response&& other) noexcept;
119
120 /** Move assignment.
121
122 Takes ownership of the contents of another
123 response, including the underlying
124 connection. The previously owned connection,
125 if any, is returned to the pool or closed,
126 as if by destruction. The moved-from
127 response no longer owns a connection.
128
129 @param other The response to move from.
130
131 @return A reference to this object.
132 */
133 BOOST_BURL_DECL
134 response&
135 operator=(response&& other) noexcept;
136
137 /** Destructor.
138
139 Returns the connection to the pool for reuse
140 when it can be kept alive and the entire
141 message has arrived.
142 */
143 BOOST_BURL_DECL
144 ~response();
145
146 /** Return the status code.
147 */
148 http::status
149 9x status() const noexcept
150 {
151 9x return parser_.get().status();
152 }
153
154 /** Return the status code as an integer.
155 */
156 unsigned short
157 status_int() const noexcept
158 {
159 return parser_.get().status_int();
160 }
161
162 /** Return true if the status code indicates success.
163 */
164 bool
165 ok() const noexcept
166 {
167 return http::to_status_class(status()) ==
168 http::status_class::successful;
169 }
170
171 /** Return the reason phrase of the status code.
172 */
173 std::string_view
174 reason() const noexcept
175 {
176 return parser_.get().reason();
177 }
178
179 /** Throw an exception for 4xx and 5xx status codes.
180
181 If the status code is 400 or above, throws
182 an exception whose code value is the status
183 code and whose category is
184 @ref burl_category. Otherwise, this function
185 has no effect.
186
187 @par Example
188 @code
189 r.raise_for_status(); // throws on 4XX and 5XX status codes
190 @endcode
191
192 @throw std::system_error
193 The status code is 400 or above.
194 */
195 void
196 raise_for_status() const
197 {
198 if(status_int() >= 400)
199 throw std::system_error(
200 std::error_code(status_int(), burl_category()));
201 }
202
203 /** Return the HTTP version of the response.
204 */
205 http::version
206 version() const noexcept
207 {
208 return parser_.get().version();
209 }
210
211 /** Return the final URL of the response.
212 */
213 urls::url_view
214 1x url() const noexcept
215 {
216 1x return url_;
217 }
218
219 /** Return the response headers.
220 */
221 const http::fields_base&
222 headers() const noexcept
223 {
224 return parser_.get();
225 }
226
227 /** Return the payload size, if known.
228
229 Returns the size of the message payload when
230 it is determined by the message metadata.
231 Otherwise returns an empty optional, such as
232 for chunked messages.
233 */
234 std::optional<std::uint64_t>
235 1x content_length() const noexcept
236 {
237
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
1x if(parser_.get().payload() == http::payload::size)
238 1x return parser_.get().payload_size();
239 return std::nullopt;
240 }
241
242 /** Asynchronously read the entire body in place.
243
244 Reads the remainder of the body into the
245 internal buffer of the parser and returns a
246 view of the complete body. The buffer is
247 sized by
248 @ref client::config::response_inplace_buffer;
249 a body which does not fit fails with
250 `http::error::in_place_overflow`. If the
251 body has already been read to completion,
252 the body is returned without performing I/O.
253
254 The returned view references memory owned by
255 the response, and remains valid until the
256 response is destroyed or moved from.
257
258 The remaining time of the request timeout,
259 when one was set, applies to this operation.
260
261 @par Example
262 @code
263 auto [ec, body] = co_await r.try_as_view();
264 @endcode
265
266 @return An awaitable yielding
267 `(error_code,std::string_view)`.
268
269 @see @ref as_view.
270 */
271 BOOST_BURL_DECL
272 capy::io_task<std::string_view>
273 try_as_view() &;
274
275 /** Asynchronously read the entire body in place.
276
277 Equivalent to @ref try_as_view, except
278 that an exception is thrown upon failure.
279
280 @par Example
281 @code
282 std::cout << co_await r.as_view() << '\n';
283 @endcode
284
285 @throw std::system_error
286 The operation failed.
287
288 @return An awaitable yielding a view of the
289 body.
290 */
291 BOOST_BURL_DECL
292 capy::task<std::string_view>
293 as_view() &;
294
295 /** Asynchronously convert the body.
296
297 Reads the body and converts it to `T` by
298 calling `tag_invoke` with @ref body_to_tag.
299
300 The remaining time of the request timeout,
301 when one was set, applies to this operation.
302
303 @par Example
304 @code
305 auto [ec, v] = co_await r.try_as<json::value>();
306 @endcode
307
308 @tparam T The type to convert the body to.
309
310 @param args Additional arguments forwarded
311 to the conversion.
312
313 @return An awaitable yielding
314 `(error_code,T)`.
315
316 @see
317 @ref as,
318 @ref body_to_tag.
319 */
320 template<class T, class... Args>
321 requires requires(response& resp, Args&&... args) {
322 tag_invoke(body_to_tag<T>{}, resp, std::forward<Args>(args)...);
323 }
324 capy::io_task<T>
325 try_as(Args&&... args) &
326 {
327 if(deadline_)
328 {
329 auto dur = *deadline_ - clock::now();
330 if(dur <= clock::duration::zero())
331 return []() -> capy::io_task<T>
332 {
333 co_return { capy::error::timeout, {} };
334 }();
335 return capy::timeout(
336 tag_invoke(
337 body_to_tag<T>{}, *this, std::forward<Args>(args)...),
338 dur);
339 }
340 return tag_invoke(body_to_tag<T>{}, *this, std::forward<Args>(args)...);
341 }
342
343 /** Asynchronously convert the body.
344
345 Equivalent to @ref try_as, except that an
346 exception is thrown upon failure.
347
348 @par Example
349 @code
350 auto v = co_await r.as<json::value>();
351 @endcode
352
353 @throw std::system_error
354 The operation failed.
355
356 @tparam T The type to convert the body to.
357
358 @param args Additional arguments forwarded
359 to the conversion.
360
361 @return An awaitable yielding the converted
362 body.
363
364 @see
365 @ref try_as,
366 @ref body_to_tag.
367 */
368 template<class T, class... Args>
369 capy::task<T>
370 as(Args... args) &
371 {
372 auto [ec, body] = co_await try_as<T>(std::move(args)...);
373
374 if(ec)
375 throw std::system_error(ec);
376
377 co_return std::move(body);
378 }
379
380 /** Return a buffer source for reading the body.
381
382 The returned source pulls the body
383 incrementally, exposing the internal buffers
384 of the parser directly instead of buffering
385 the whole body in memory. The response must
386 remain valid until the source is no longer
387 used.
388
389 @par Example
390 @code
391 auto source = r.as_buffer_source();
392 for(;;)
393 {
394 capy::const_buffer arr[8];
395 auto [ec, bufs] = co_await source.pull(arr);
396 if(ec == capy::cond::eof)
397 break;
398 if(ec)
399 throw std::system_error(ec);
400 for(auto const& buf : bufs)
401 {
402 consume_data(buf.data(), buf.size());
403 source.consume(buf.size());
404 }
405 }
406 @endcode
407
408 @return A buffer source for the body.
409
410 @see @ref as_read_source.
411 */
412 BOOST_BURL_DECL
413 capy::any_buffer_source
414 as_buffer_source() &;
415
416 /** Return a read source for reading the body.
417
418 The returned source reads the body
419 incrementally into caller-provided buffers.
420 The response must remain valid until the
421 source is no longer used.
422
423 @return A read source for the body.
424
425 @see @ref as_buffer_source.
426 */
427 BOOST_BURL_DECL
428 capy::any_read_source
429 as_read_source() &;
430 };
431
432 } // namespace burl
433 } // namespace boost
434
435 #endif
436