Skip to main content

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}