scion_http3/response.rs
1// Copyright 2026 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Responses and body collection.
16
17use std::{sync::Arc, time::Duration};
18
19use bytes::Bytes;
20use scion_quic::h3::client::{H3ResponseBody, Http3Client};
21
22use crate::error::{Error, TimeoutPhase};
23
24/// A response whose head has arrived and whose body is still on the wire.
25///
26/// [`bytes`](Self::bytes) and [`text`](Self::text) collect the body (and
27/// trailers, if any) under the remainder of the request deadline, so "request
28/// timeout" covers head *and* body.
29///
30/// The deadline is absolute, fixed when the request started, and it keeps
31/// running while this value is held. Time spent between receiving the head and
32/// collecting the body comes out of the same budget, so a response parked while
33/// the caller does other work can fail with
34/// [`Error::Timeout`] `{ phase: `[`Body`](TimeoutPhase::Body)` }` even though
35/// its bytes have already arrived. Collect promptly, or give such requests a
36/// longer [`request_timeout`](crate::RequestBuilder::request_timeout).
37pub struct Response {
38 parts: http::response::Parts,
39 body: H3ResponseBody,
40 deadline: tokio::time::Instant,
41 timeout: Duration,
42 /// Keeps the connection the body streams over alive until the body is
43 /// collected: the pool may evict the origin at any time, and dropping the
44 /// last reference to the transport client would fault the body mid-read.
45 _connection: Arc<Http3Client>,
46}
47
48impl Response {
49 pub(crate) fn new(
50 response: http::Response<H3ResponseBody>,
51 deadline: tokio::time::Instant,
52 timeout: Duration,
53 connection: Arc<Http3Client>,
54 ) -> Self {
55 let (parts, body) = response.into_parts();
56 Response {
57 parts,
58 body,
59 deadline,
60 timeout,
61 _connection: connection,
62 }
63 }
64
65 /// The response status code.
66 #[must_use]
67 pub fn status(&self) -> http::StatusCode {
68 self.parts.status
69 }
70
71 /// The response headers.
72 #[must_use]
73 pub fn headers(&self) -> &http::HeaderMap {
74 &self.parts.headers
75 }
76
77 /// Whether the status code is in the 2xx range.
78 #[must_use]
79 pub fn is_success(&self) -> bool {
80 self.parts.status.is_success()
81 }
82
83 /// Collects the response body, returning it together with the trailers
84 /// (HTTP/3 trailing header section), if the server sent any.
85 ///
86 /// `max_size` bounds how much is buffered; a larger body fails with
87 /// [`Error::BodyTooLarge`]. `None` means unbounded — prefer a limit
88 /// whenever the server is not fully trusted. Collection runs under the
89 /// remainder of the request deadline.
90 pub async fn bytes(
91 self,
92 max_size: Option<usize>,
93 ) -> Result<(Bytes, Option<http::HeaderMap>), Error> {
94 let collected = tokio::time::timeout_at(self.deadline, self.body.bytes(max_size))
95 .await
96 .map_err(|_| {
97 Error::Timeout {
98 phase: TimeoutPhase::Body,
99 timeout: self.timeout,
100 }
101 })?
102 .map_err(|e| Error::from_collect_error(e, max_size))?;
103 let (body, trailers) = collected;
104 Ok((body.freeze(), trailers))
105 }
106
107 /// Collects the response body as a UTF-8 string, returning it together
108 /// with the trailers, if the server sent any.
109 ///
110 /// Same limits and deadline as [`bytes`](Self::bytes); a body that is not
111 /// valid UTF-8 fails with [`Error::InvalidBody`].
112 pub async fn text(
113 self,
114 max_size: Option<usize>,
115 ) -> Result<(String, Option<http::HeaderMap>), Error> {
116 tokio::time::timeout_at(self.deadline, self.body.text(max_size))
117 .await
118 .map_err(|_| {
119 Error::Timeout {
120 phase: TimeoutPhase::Body,
121 timeout: self.timeout,
122 }
123 })?
124 .map_err(|e| Error::from_collect_to_string_error(e, max_size))
125 }
126}
127
128impl std::fmt::Debug for Response {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 f.debug_struct("Response")
131 .field("status", &self.parts.status)
132 .field("headers", &self.parts.headers)
133 .finish_non_exhaustive()
134 }
135}