Skip to main content

fetch_happen/
native.rs

1//! The native transport: a streaming reqwest client with the same API shape
2//! as the web transport. `send()` resolves once the response headers arrive;
3//! the body is read on demand, either whole (`bytes`, `text`, `json`) or
4//! chunk by chunk (`stream_reader`), and abort signals are honoured while
5//! awaiting headers and every body read.
6use crate::{AbortSignal, Error, Method, Result};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashMap;
10use std::sync::{Mutex, OnceLock};
11use web_sys::RequestMode;
12
13impl From<reqwest::Error> for Error {
14    fn from(err: reqwest::Error) -> Self {
15        Error::Transport(err.to_string())
16    }
17}
18
19/// One shared client so requests reuse connection pools and TLS sessions.
20fn client() -> &'static reqwest::Client {
21    static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
22    CLIENT.get_or_init(reqwest::Client::new)
23}
24
25/// A builder for HTTP requests
26pub struct RequestBuilder {
27    url: String,
28    method: Method,
29    headers: HashMap<String, String>,
30    body: Option<String>,
31    signal: Option<AbortSignal>,
32}
33
34impl RequestBuilder {
35    pub(crate) fn new(method: Method, url: impl Into<String>) -> Self {
36        Self {
37            url: url.into(),
38            method,
39            headers: HashMap::new(),
40            body: None,
41            signal: None,
42        }
43    }
44
45    /// Set a header
46    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
47        self.headers.insert(key.into(), value.into());
48        self
49    }
50
51    /// Set multiple headers
52    pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
53        self.headers.extend(headers);
54        self
55    }
56
57    /// Set the request mode. CORS is a browser concept; accepted for API
58    /// parity and ignored natively.
59    pub fn mode(self, _mode: RequestMode) -> Self {
60        self
61    }
62
63    /// Set the request body as a string
64    pub fn body(mut self, body: impl Into<String>) -> Self {
65        self.body = Some(body.into());
66        self
67    }
68
69    /// Set the request body as JSON
70    pub fn json<T: Serialize>(mut self, json: &T) -> Result<Self> {
71        let body = serde_json::to_string(json)?;
72        self.body = Some(body);
73        self.headers
74            .insert("Content-Type".to_string(), "application/json".to_string());
75        Ok(self)
76    }
77
78    /// Abort pending headers or body reads when the signal fires.
79    pub fn abort_signal(mut self, signal: impl Into<AbortSignal>) -> Self {
80        self.signal = Some(signal.into());
81        self
82    }
83
84    /// Send the request and get a Response once its headers arrive.
85    pub async fn send(mut self) -> Result<Response> {
86        let signal = self.signal.take();
87        let response = until(signal.as_ref(), self.send_inner()).await?;
88        Ok(Response { signal, ..response })
89    }
90
91    async fn send_inner(self) -> Result<Response> {
92        let method = reqwest::Method::from_bytes(self.method.as_str().as_bytes())
93            .expect("Method::as_str is always a valid HTTP method");
94        let mut request = client().request(method, &self.url);
95        for (key, value) in &self.headers {
96            request = request.header(key, value);
97        }
98        if let Some(body) = self.body {
99            request = request.body(body);
100        }
101
102        let response = request.send().await?;
103        Ok(Response {
104            status: response.status().as_u16(),
105            ok: response.status().is_success(),
106            headers: response.headers().clone(),
107            body: Body::new(response),
108            signal: None,
109        })
110    }
111}
112
113/// Run `future`, failing with [`Error::Aborted`] if `signal` fires first.
114async fn until<T>(
115    signal: Option<&AbortSignal>,
116    future: impl std::future::Future<Output = Result<T>>,
117) -> Result<T> {
118    match signal {
119        Some(signal) => signal.until(future).await.map_err(|_| Error::Aborted)?,
120        None => future.await,
121    }
122}
123
124/// The unread remainder of a response body. Like the browser's, a body can
125/// be consumed once; reading it again is an error rather than empty data.
126struct Body(Mutex<Option<reqwest::Response>>);
127
128impl Body {
129    fn new(response: reqwest::Response) -> Self {
130        Self(Mutex::new(Some(response)))
131    }
132
133    fn take(&self) -> Result<reqwest::Response> {
134        self.0
135            .lock()
136            .unwrap_or_else(|poisoned| poisoned.into_inner())
137            .take()
138            .ok_or_else(|| Error::Transport("body already consumed".to_string()))
139    }
140
141    fn put_back(&self, response: reqwest::Response) {
142        *self
143            .0
144            .lock()
145            .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(response);
146    }
147}
148
149/// A response from a fetch request. Headers are available immediately; the
150/// body streams from the socket as it is read.
151pub struct Response {
152    status: u16,
153    ok: bool,
154    headers: reqwest::header::HeaderMap,
155    body: Body,
156    signal: Option<AbortSignal>,
157}
158
159impl Response {
160    /// Get the status code
161    pub fn status(&self) -> u16 {
162        self.status
163    }
164
165    /// Check if the response was successful (status 200-299)
166    pub fn ok(&self) -> bool {
167        self.ok
168    }
169
170    /// Get a header value
171    pub fn header(&self, name: &str) -> Result<Option<String>> {
172        match self.headers.get(name) {
173            Some(value) => Ok(Some(
174                value
175                    .to_str()
176                    .map_err(|e| Error::Transport(format!("non-UTF-8 header value: {e}")))?
177                    .to_string(),
178            )),
179            None => Ok(None),
180        }
181    }
182
183    /// Get the response body as text. Malformed UTF-8 is replaced rather
184    /// than rejected, matching the browser's `Response.text()`.
185    pub async fn text(&self) -> Result<String> {
186        Ok(String::from_utf8_lossy(&self.bytes().await?).into_owned())
187    }
188
189    /// Get the response body as JSON
190    pub async fn json<T: for<'de> Deserialize<'de>>(&self) -> Result<T> {
191        Ok(serde_json::from_slice(&self.bytes().await?)?)
192    }
193
194    /// Get the response body as a dynamic JSON value
195    pub async fn json_value(&self) -> Result<Value> {
196        self.json().await
197    }
198
199    /// Read the whole response body
200    pub async fn bytes(&self) -> Result<Vec<u8>> {
201        let response = self.body.take()?;
202        let bytes = until(self.signal.as_ref(), async { Ok(response.bytes().await?) }).await?;
203        Ok(bytes.to_vec())
204    }
205
206    /// Ensure the response was successful, returning an error if not
207    pub fn error_for_status(self) -> Result<Self> {
208        if self.ok() {
209            Ok(self)
210        } else {
211            let status = self.status();
212            let text = format!("HTTP Error {}", status);
213            Err(Error::HttpError(status, text))
214        }
215    }
216
217    /// Get a stream reader for reading chunks from the response as they
218    /// arrive. Takes over the body, so it can't be read from `self` again.
219    pub fn stream_reader(&self) -> Result<StreamReader> {
220        Ok(StreamReader {
221            body: Body::new(self.body.take()?),
222            signal: self.signal.clone(),
223        })
224    }
225}
226
227/// A reader for streaming response bodies chunk by chunk
228pub struct StreamReader {
229    body: Body,
230    signal: Option<AbortSignal>,
231}
232
233impl StreamReader {
234    /// Read the next chunk from the stream
235    /// Returns Ok(Some(bytes)) if a chunk is available
236    /// Returns Ok(None) if the stream is finished
237    pub async fn read_chunk(&self) -> Result<Option<Vec<u8>>> {
238        let mut response = self.body.take()?;
239        let chunk = until(self.signal.as_ref(), async { Ok(response.chunk().await?) }).await?;
240        // Kept after the end too, so reading past it stays `None` as in the browser.
241        self.body.put_back(response);
242        Ok(chunk.map(|chunk| chunk.to_vec()))
243    }
244
245    /// Release the reader; the connection is dropped with it.
246    pub fn cancel(self) -> Result<()> {
247        Ok(())
248    }
249}