Skip to main content

http_quik/client/
response.rs

1use bytes::Bytes;
2use http::header::{HeaderMap, CONTENT_ENCODING};
3use http::StatusCode;
4use http2::RecvStream;
5use std::io::Read;
6
7use crate::error::{Error, Result};
8
9/// Polymorphic container representing the inbound data stream from either H2 or H3 transport.
10///
11/// ### Design Rationale:
12/// - **HTTP/2 Transport**: Operates over a TCP/TLS connection using streaming data frames.
13///   The payload is represented as `ResponseBody::Http2(RecvStream)`, which allows for asynchronous,
14///   non-blocking polling of chunks to conserve memory.
15/// - **HTTP/3 Transport**: Operates over a UDP/QUIC multiplexed connection. Because `quiche`
16///   processes transport and application layers via a unified, single-threaded background event loop,
17///   payload bytes are eagerly read from UDP sockets and compiled into an aggregated memory buffer
18///   (`ResponseBody::Http3(Vec<u8>)`). This isolates the network layer from borrowing or concurrency hazards.
19pub enum ResponseBody {
20    /// Standard H2 receiver stream yielding sequential data frames.
21    Http2(RecvStream),
22    /// Eagerly downloaded and aggregated H3 buffer payload.
23    Http3(Vec<u8>),
24}
25
26/// A high-level response wrapper providing transparent decompression and body management.
27///
28/// `Response` unifies both HTTP/2 and HTTP/3 connection payloads under a single,
29/// public-API-compatible interface, implementing transparent decompression on hot paths.
30///
31/// ### Key Architecture:
32/// - **Encapsulation**: Keeps the underlying transport polymorphic (H2 vs H3) transparent to the client caller.
33/// - **Zero-Copy Decompression**: Employs stack-allocated decompressors (`brotli_decompressor`, `zstd`, `flate2`)
34///   upon `bytes()` invocation, decoding raw buffers directly into final owned storage.
35pub struct Response {
36    /// Status code of the response.
37    status: StatusCode,
38    /// HTTP response headers.
39    headers: HeaderMap,
40    /// The polymorphic payload container.
41    body: ResponseBody,
42    /// The final, post-redirect URL that produced this response.
43    url: String,
44}
45
46impl Response {
47    /// Creates a new `Response` from polymorphic parts.
48    pub fn new(status: StatusCode, headers: HeaderMap, body: ResponseBody, url: String) -> Self {
49        Self {
50            status,
51            headers,
52            body,
53            url,
54        }
55    }
56
57    /// Returns the HTTP status code.
58    pub fn status(&self) -> StatusCode {
59        self.status
60    }
61
62    /// Returns a reference to the header map.
63    pub fn headers(&self) -> &HeaderMap {
64        &self.headers
65    }
66
67    /// Returns the final post-redirect URL.
68    pub fn url(&self) -> &str {
69        &self.url
70    }
71
72    /// Internal setter for the final URL (used by the redirect engine).
73    pub(crate) fn set_url(&mut self, url: String) {
74        self.url = url;
75    }
76
77    /// Collects the response body and returns the decompressed bytes.
78    ///
79    /// This method is async as it must support asynchronous polling of H2 chunks.
80    /// Supports `gzip`, `br`, and `zstd` encodings.
81    ///
82    /// ### Implementation Strategy:
83    /// 1. **Polymorphic Assembly**: Accumulates body frames from TCP/H2 streaming loops,
84    ///    or yields the pre-buffered UDP/H3 vector.
85    /// 2. **Transport Content-Encoding Negotiation**: Inspects the `Content-Encoding` header and routes
86    ///    bytes dynamically through the appropriate decompression block (Brotli, Zstd, or Gzip).
87    pub async fn bytes(self) -> Result<Bytes> {
88        let mut data = Vec::new();
89
90        match self.body {
91            ResponseBody::Http2(mut body_stream) => {
92                while let Some(chunk) = body_stream.data().await {
93                    let chunk = chunk.map_err(Error::Http2)?;
94                    data.extend_from_slice(chunk.as_ref());
95                }
96            }
97            ResponseBody::Http3(body_data) => {
98                data = body_data;
99            }
100        }
101
102        let encoding = self
103            .headers
104            .get(CONTENT_ENCODING)
105            .and_then(|v| v.to_str().ok())
106            .unwrap_or("");
107
108        if encoding.contains("br") {
109            let mut decoder = brotli_decompressor::Decompressor::new(&data[..], 4096);
110            let mut decoded = Vec::new();
111            decoder
112                .read_to_end(&mut decoded)
113                .map_err(|e| Error::Connect(std::io::Error::other(e.to_string())))?;
114            Ok(Bytes::from(decoded))
115        } else if encoding.contains("zstd") {
116            let decoded = zstd::decode_all(&data[..])
117                .map_err(|e| Error::Connect(std::io::Error::other(e.to_string())))?;
118            Ok(Bytes::from(decoded))
119        } else if encoding.contains("gzip") {
120            let mut decoder = flate2::read::GzDecoder::new(&data[..]);
121            let mut decoded = Vec::new();
122            decoder
123                .read_to_end(&mut decoded)
124                .map_err(|e| Error::Connect(std::io::Error::other(e.to_string())))?;
125            Ok(Bytes::from(decoded))
126        } else {
127            Ok(Bytes::from(data))
128        }
129    }
130
131    /// Collects the body and decodes it as a UTF-8 string.
132    pub async fn text(self) -> Result<String> {
133        let bytes = self.bytes().await?;
134        String::from_utf8(bytes.to_vec())
135            .map_err(|e| Error::Connect(std::io::Error::other(e.to_string())))
136    }
137
138    /// Collects the body and decodes it as JSON.
139    pub async fn json<T: serde::de::DeserializeOwned>(self) -> Result<T> {
140        let bytes = self.bytes().await?;
141        serde_json::from_slice(&bytes)
142            .map_err(|e| Error::Connect(std::io::Error::other(e.to_string())))
143    }
144}