Skip to main content

better_fetch/
request.rs

1//! Per-request fluent builder.
2//!
3//! Obtain a [`RequestBuilder`] from [`Client::get`](crate::Client::get) (or other verbs), chain
4//! path/query/body options, then call [`RequestBuilder::send`] or [`RequestBuilder::send_json`].
5
6use std::collections::HashMap;
7use std::time::Duration;
8
9use bytes::Bytes;
10use http::{HeaderMap, Method};
11use indexmap::IndexMap;
12
13use crate::auth::Auth;
14use crate::backend::HttpBody;
15use crate::cancel::CancellationToken;
16use crate::client::Client;
17use crate::error::Error;
18use crate::response::Response;
19use crate::retry::RetryPolicy;
20use crate::streaming::StreamingResponse;
21use crate::url_build::QueryValue;
22use crate::Result;
23
24#[cfg(feature = "json")]
25use crate::json_parser::JsonParserFn;
26
27/// Fluent builder for a single HTTP request.
28///
29/// By default [`send`](Self::send) returns [`Response`] even on non-2xx status. Use
30/// [`throw_on_error`](Self::throw_on_error)(`true`) to get `Err` from `send`, or use
31/// [`send_json`](Self::send_json) which checks status before deserializing.
32pub struct RequestBuilder<'a> {
33    pub(crate) client: &'a Client,
34    pub(crate) method: Method,
35    pub(crate) path: String,
36    pub(crate) params: HashMap<String, String>,
37    pub(crate) query: IndexMap<String, QueryValue>,
38    pub(crate) headers: HeaderMap,
39    pub(crate) body: HttpBody,
40    #[cfg(feature = "multipart")]
41    pub(crate) multipart: Option<crate::multipart::Form>,
42    pub(crate) timeout: Option<Duration>,
43    pub(crate) retry: Option<RetryPolicy>,
44    pub(crate) auth: Option<Auth>,
45    pub(crate) cancellation: Option<CancellationToken>,
46    pub(crate) throw_on_error: bool,
47    pub(crate) max_response_bytes: Option<u64>,
48    pub(crate) retry_body_peek_bytes: Option<u64>,
49    #[cfg(feature = "json")]
50    pub(crate) json_parser: Option<JsonParserFn>,
51    #[cfg(feature = "validate")]
52    pub(crate) validate_response: bool,
53}
54
55impl<'a> RequestBuilder<'a> {
56    /// Sets a path template parameter (`:key` in the path).
57    pub fn param(mut self, key: impl Into<String>, value: impl ToString) -> Self {
58        self.params.insert(key.into(), value.to_string());
59        self
60    }
61
62    /// Merges path parameters from a map.
63    pub fn params(mut self, params: HashMap<String, String>) -> Self {
64        self.params.extend(params);
65        self
66    }
67
68    /// Merges path parameters from an iterator.
69    pub fn params_iter(
70        mut self,
71        params: impl IntoIterator<Item = (impl Into<String>, impl ToString)>,
72    ) -> Self {
73        for (k, v) in params {
74            self.params.insert(k.into(), v.to_string());
75        }
76        self
77    }
78
79    /// Adds a query string parameter.
80    pub fn query(mut self, key: impl Into<String>, value: impl ToString) -> Self {
81        self.query
82            .insert(key.into(), QueryValue::Scalar(value.to_string()));
83        self
84    }
85
86    /// Sets multiple query parameters preserving insertion order.
87    pub fn queries(mut self, query: IndexMap<String, QueryValue>) -> Self {
88        for (k, v) in query {
89            self.query.insert(k, v);
90        }
91        self
92    }
93
94    /// Serializes `value` as JSON and uses it as a query parameter (feature `json`).
95    #[cfg(feature = "json")]
96    pub fn query_json<T: serde::Serialize>(
97        mut self,
98        key: impl Into<String>,
99        value: &T,
100    ) -> Result<Self> {
101        self.query
102            .insert(key.into(), QueryValue::from_serializable(value)?);
103        Ok(self)
104    }
105
106    /// Adds a request header.
107    pub fn header(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> Result<Self> {
108        let name = http::HeaderName::from_bytes(key.as_ref().as_bytes())
109            .map_err(|e| Error::Other(format!("invalid header name: {e}")))?;
110        let value = http::HeaderValue::from_str(value.as_ref())
111            .map_err(|e| Error::Other(format!("invalid header value: {e}")))?;
112        self.headers.insert(name, value);
113        Ok(self)
114    }
115
116    /// Sets a JSON request body (feature `json`).
117    #[cfg(feature = "json")]
118    pub fn json<T: serde::Serialize>(mut self, body: &T) -> Result<Self> {
119        let bytes = serde_json::to_vec(body).map_err(|e| Error::Other(e.to_string()))?;
120        self.body = HttpBody::Bytes(Bytes::from(bytes));
121        if !self.headers.contains_key(http::header::CONTENT_TYPE) {
122            self.headers.insert(
123                http::header::CONTENT_TYPE,
124                http::HeaderValue::from_static("application/json"),
125            );
126        }
127        Ok(self)
128    }
129
130    /// Sets a raw request body.
131    pub fn body(mut self, body: impl Into<Bytes>) -> Self {
132        self.body = HttpBody::Bytes(body.into());
133        self
134    }
135
136    /// URL-encoded form body (`application/x-www-form-urlencoded`).
137    pub fn form<I, K, V>(mut self, fields: I) -> Self
138    where
139        I: IntoIterator<Item = (K, V)>,
140        K: AsRef<str>,
141        V: AsRef<str>,
142    {
143        let mut serializer = url::form_urlencoded::Serializer::new(String::new());
144        for (k, v) in fields {
145            serializer.append_pair(k.as_ref(), v.as_ref());
146        }
147        self.body = HttpBody::Bytes(Bytes::from(serializer.finish()));
148        if !self.headers.contains_key(http::header::CONTENT_TYPE) {
149            self.headers.insert(
150                http::header::CONTENT_TYPE,
151                http::HeaderValue::from_static("application/x-www-form-urlencoded"),
152            );
153        }
154        self
155    }
156
157    /// Multipart form body (requires the `multipart` feature).
158    ///
159    /// Automatic retry is not supported when multipart bodies are used.
160    #[cfg(feature = "multipart")]
161    pub fn multipart(mut self, form: crate::multipart::Form) -> Self {
162        self.multipart = Some(form);
163        self.body = HttpBody::Empty;
164        self
165    }
166
167    /// Overrides the client default timeout for this request.
168    pub fn timeout(mut self, timeout: Duration) -> Self {
169        self.timeout = Some(timeout);
170        self
171    }
172
173    /// Overrides the client default retry policy for this request.
174    pub fn retry(mut self, policy: RetryPolicy) -> Self {
175        self.retry = Some(policy);
176        self
177    }
178
179    /// Overrides authentication for this request.
180    pub fn auth(mut self, auth: Auth) -> Self {
181        self.auth = Some(auth);
182        self
183    }
184
185    /// Sets bearer authentication for this request.
186    pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
187        self.auth = Some(Auth::bearer(token));
188        self
189    }
190
191    /// Cancels the in-flight request and retry sleeps when this token is triggered.
192    ///
193    /// # Examples
194    ///
195    /// ```no_run
196    /// # use better_fetch::{CancellationToken, Client, Result};
197    /// # use std::time::Duration;
198    /// # #[tokio::main]
199    /// # async fn main() -> Result<()> {
200    /// let client = Client::new("https://api.example.com")?;
201    /// let token = CancellationToken::new();
202    /// let token_clone = token.clone();
203    /// tokio::spawn(async move {
204    ///     tokio::time::sleep(Duration::from_millis(10)).await;
205    ///     token_clone.cancel();
206    /// });
207    /// let err = client
208    ///     .get("/slow")
209    ///     .cancellation_token(token)
210    ///     .send()
211    ///     .await
212    ///     .unwrap_err();
213    /// assert!(err.is_cancelled());
214    /// # Ok(())
215    /// # }
216    /// ```
217    pub fn cancellation_token(mut self, token: CancellationToken) -> Self {
218        self.cancellation = Some(token);
219        self
220    }
221
222    /// When `true`, [`send`](Self::send) returns `Err` on non-2xx HTTP status (like upstream `throw: true`).
223    pub fn throw_on_error(mut self, throw: bool) -> Self {
224        self.throw_on_error = throw;
225        self
226    }
227
228    /// Overrides the client's JSON parser for this request only.
229    ///
230    /// See [`crate::json_parser`] for fast path vs two-step parsing.
231    #[cfg(feature = "json")]
232    pub fn json_parser<F>(mut self, f: F) -> Self
233    where
234        F: Fn(&Bytes) -> std::result::Result<serde_json::Value, String> + Send + Sync + 'static,
235    {
236        self.json_parser = Some(crate::json_parser::json_parser(f));
237        self
238    }
239
240    /// Overrides the client's JSON parser for this request only.
241    #[cfg(feature = "json")]
242    pub fn json_parser_fn(mut self, parser: JsonParserFn) -> Self {
243        self.json_parser = Some(parser);
244        self
245    }
246
247    /// Executes the request and returns the [`Response`].
248    ///
249    /// Non-2xx responses are returned as `Ok(Response)` unless [`throw_on_error`](Self::throw_on_error)
250    /// is `true`. Deserialize JSON with [`Response::json`](crate::Response::json) or use
251    /// [`send_json`](Self::send_json) for a one-step typed result.
252    ///
253    /// # Examples
254    ///
255    /// ```no_run
256    /// # use better_fetch::{Client, Result};
257    /// # #[tokio::main]
258    /// # async fn main() -> Result<()> {
259    /// let client = Client::new("https://api.example.com")?;
260    /// let response = client.get("/users/1").send().await?;
261    /// if response.is_success() {
262    ///     println!("status {}", response.status());
263    /// }
264    /// # Ok(())
265    /// # }
266    /// ```
267    pub async fn send(self) -> Result<Response> {
268        self.client.execute(self).await
269    }
270
271    /// Maximum response body size in bytes for this streaming request.
272    ///
273    /// Applies to [`send_stream`](Self::send_stream) only. When a chunk would exceed the limit,
274    /// the stream yields [`Error::BodyTooLarge`](crate::Error::BodyTooLarge).
275    pub fn max_response_bytes(mut self, limit: u64) -> Self {
276        self.max_response_bytes = Some(limit);
277        self
278    }
279
280    /// Overrides the client default for how many bytes may be read when a custom retry predicate runs on a stream.
281    pub fn retry_body_peek_bytes(mut self, limit: u64) -> Self {
282        self.retry_body_peek_bytes = Some(limit);
283        self
284    }
285
286    /// Executes the request and returns a [`StreamingResponse`] without buffering the full body.
287    ///
288    /// Uses [`Hooks::on_request`](crate::Hooks::on_request), [`Hooks::on_response_stream`](crate::Hooks::on_response_stream),
289    /// and [`Hooks::on_success_stream`](crate::Hooks::on_success_stream) (2xx). Buffered
290    /// [`Hooks::on_response`](crate::Hooks::on_response) / [`on_success`](crate::Hooks::on_success) are not called.
291    /// Custom retry predicates may peek up to [`ClientBuilder::retry_body_peek_bytes`](crate::ClientBuilder::retry_body_peek_bytes).
292    /// Cancellation wakes pending body reads via the cancellation token (checked on each stream poll).
293    ///
294    /// # Examples
295    ///
296    /// ```no_run
297    /// # use better_fetch::{Client, Result};
298    /// # use futures_util::StreamExt;
299    /// # #[tokio::main]
300    /// # async fn main() -> Result<()> {
301    /// let client = Client::new("https://api.example.com")?;
302    /// let mut response = client.get("/export").send_stream().await?;
303    /// while let Some(chunk) = response.bytes_stream().next().await {
304    ///     let _chunk = chunk?;
305    /// }
306    /// # Ok(())
307    /// # }
308    /// ```
309    pub async fn send_stream(self) -> Result<StreamingResponse> {
310        self.client.execute_stream(self).await
311    }
312
313    /// Executes the request and deserializes JSON on success (feature `json`).
314    ///
315    /// Fails with [`Error::Http`](crate::Error::Http) or [`Error::Deserialize`](crate::Error::Deserialize)
316    /// on non-2xx or invalid JSON.
317    ///
318    /// # Examples
319    ///
320    /// ```no_run
321    /// # use better_fetch::{Client, Result};
322    /// # use serde::Deserialize;
323    /// # #[derive(Deserialize)]
324    /// # struct User { id: u64 }
325    /// # #[tokio::main]
326    /// # async fn main() -> Result<()> {
327    /// let client = Client::new("https://api.example.com")?;
328    /// let user: User = client.get("/users/:id").param("id", 1).send_json().await?;
329    /// # Ok(())
330    /// # }
331    /// ```
332    #[cfg(feature = "json")]
333    #[must_use = "send the request with `.await` and handle the result"]
334    pub async fn send_json<T: serde::de::DeserializeOwned>(self) -> Result<T> {
335        self.send().await?.json::<T>().await
336    }
337
338    /// When `false`, [`send_json_validated`](Self::send_json_validated) only deserializes (no garde).
339    #[cfg(feature = "validate")]
340    pub fn validate_response(mut self, validate: bool) -> Self {
341        self.validate_response = validate;
342        self
343    }
344
345    /// `send` + [`Response::json_validated`](crate::Response::json_validated) (feature `validate`).
346    #[cfg(feature = "validate")]
347    pub async fn send_json_validated<T>(self) -> Result<T>
348    where
349        T: serde::de::DeserializeOwned + garde::Validate,
350        T::Context: Default,
351    {
352        if !self.validate_response {
353            return self.send_json().await;
354        }
355        self.send().await?.json_validated().await
356    }
357}