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