Skip to main content

http_quik/client/
response.rs

1use bytes::Bytes;
2use http::header::{HeaderMap, CONTENT_ENCODING};
3use http::{Response as HttpResponse, StatusCode};
4use http2::RecvStream;
5use std::io::Read;
6
7use crate::error::{Error, Result};
8
9/// A high-level response wrapper providing transparent decompression and body management.
10///
11/// `Response` abstracts away the complexities of HTTP/2 stream management and
12/// automatic decompression of browser-standard encodings.
13pub struct Response {
14    /// The underlying HTTP response containing status and headers.
15    inner: HttpResponse<RecvStream>,
16    /// The final, post-redirect URL that produced this response.
17    url: String,
18}
19
20impl Response {
21    /// Creates a new `Response` from a raw H2 response and origin URL.
22    pub fn new(inner: HttpResponse<RecvStream>, url: String) -> Self {
23        Self { inner, url }
24    }
25
26    /// Returns the HTTP status code.
27    pub fn status(&self) -> StatusCode {
28        self.inner.status()
29    }
30
31    /// Returns a reference to the header map.
32    pub fn headers(&self) -> &HeaderMap {
33        self.inner.headers()
34    }
35
36    /// Returns the final post-redirect URL.
37    pub fn url(&self) -> &str {
38        &self.url
39    }
40
41    /// Internal setter for the final URL (used by the redirect engine).
42    pub(crate) fn set_url(&mut self, url: String) {
43        self.url = url;
44    }
45
46    /// Collects the response body and returns the decompressed bytes.
47    ///
48    /// This method is async as it must wait for all HTTP/2 DATA frames to arrive.
49    /// Supports `gzip`, `br`, and `zstd` encodings.
50    pub async fn bytes(self) -> Result<Bytes> {
51        let (parts, mut body_stream) = self.inner.into_parts();
52        let mut data = Vec::new();
53
54        while let Some(chunk) = body_stream.data().await {
55            let chunk = chunk.map_err(Error::Http2)?;
56            data.extend_from_slice(chunk.as_ref());
57        }
58
59        let encoding = parts
60            .headers
61            .get(CONTENT_ENCODING)
62            .and_then(|v| v.to_str().ok())
63            .unwrap_or("");
64
65        if encoding.contains("br") {
66            let mut decoder = brotli_decompressor::Decompressor::new(&data[..], 4096);
67            let mut decoded = Vec::new();
68            decoder
69                .read_to_end(&mut decoded)
70                .map_err(|e| Error::Connect(std::io::Error::other(e.to_string())))?;
71            Ok(Bytes::from(decoded))
72        } else if encoding.contains("zstd") {
73            let decoded = zstd::decode_all(&data[..])
74                .map_err(|e| Error::Connect(std::io::Error::other(e.to_string())))?;
75            Ok(Bytes::from(decoded))
76        } else if encoding.contains("gzip") {
77            let mut decoder = flate2::read::GzDecoder::new(&data[..]);
78            let mut decoded = Vec::new();
79            decoder
80                .read_to_end(&mut decoded)
81                .map_err(|e| Error::Connect(std::io::Error::other(e.to_string())))?;
82            Ok(Bytes::from(decoded))
83        } else {
84            Ok(Bytes::from(data))
85        }
86    }
87
88    /// Collects the body and decodes it as a UTF-8 string.
89    pub async fn text(self) -> Result<String> {
90        let bytes = self.bytes().await?;
91        String::from_utf8(bytes.to_vec())
92            .map_err(|e| Error::Connect(std::io::Error::other(e.to_string())))
93    }
94
95    /// Collects the body and decodes it as JSON.
96    pub async fn json<T: serde::de::DeserializeOwned>(self) -> Result<T> {
97        let bytes = self.bytes().await?;
98        serde_json::from_slice(&bytes)
99            .map_err(|e| Error::Connect(std::io::Error::other(e.to_string())))
100    }
101}