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}