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;
23use url::Url;
24
25#[cfg(feature = "json")]
26use crate::json_parser::JsonParserFn;
27
28/// Parses a header name/value pair for request or client default headers.
29pub(crate) fn parse_request_header(
30    key: impl AsRef<str>,
31    value: impl AsRef<str>,
32) -> Result<(http::HeaderName, http::HeaderValue)> {
33    let name = http::HeaderName::from_bytes(key.as_ref().as_bytes())
34        .map_err(|e| Error::InvalidHeaderName(e.to_string()))?;
35    let value = http::HeaderValue::from_str(value.as_ref())
36        .map_err(|e| Error::InvalidHeaderValue(e.to_string()))?;
37    Ok((name, value))
38}
39
40/// Fluent builder for a single HTTP request.
41///
42/// By default [`send`](Self::send) returns [`Response`] even on non-2xx status. Use
43/// [`throw_on_error`](Self::throw_on_error)(`true`) to get `Err` from `send`, or use
44/// [`send_json`](Self::send_json) which checks status before deserializing.
45#[must_use = "request builders do nothing until you call `.send().await`, `.send_stream().await`, or similar"]
46pub struct RequestBuilder<'a> {
47    pub(crate) client: &'a Client,
48    pub(crate) method: Method,
49    pub(crate) path: String,
50    pub(crate) base_url: Option<url::Url>,
51    pub(crate) params: HashMap<String, String>,
52    pub(crate) query: IndexMap<String, QueryValue>,
53    pub(crate) headers: HeaderMap,
54    pub(crate) body: HttpBody,
55    #[cfg(feature = "multipart")]
56    pub(crate) multipart: Option<crate::multipart::Form>,
57    pub(crate) timeout: Option<Duration>,
58    pub(crate) retry: Option<RetryPolicy>,
59    pub(crate) auth: Option<Auth>,
60    pub(crate) cancellation: Option<CancellationToken>,
61    pub(crate) throw_on_error: bool,
62    pub(crate) max_response_bytes: Option<u64>,
63    pub(crate) retry_body_peek_bytes: Option<u64>,
64    #[cfg(feature = "json")]
65    pub(crate) json_parser: Option<JsonParserFn>,
66    #[cfg(feature = "validate")]
67    pub(crate) validate_response: bool,
68}
69
70impl<'a> RequestBuilder<'a> {
71    /// Sets a path template parameter (`:key` in the path).
72    pub fn param(mut self, key: impl Into<String>, value: impl ToString) -> Self {
73        self.params.insert(key.into(), value.to_string());
74        self
75    }
76
77    /// Merges path parameters from a map.
78    pub fn params(mut self, params: HashMap<String, String>) -> Self {
79        self.params.extend(params);
80        self
81    }
82
83    /// Merges path parameters from an iterator.
84    pub fn params_iter(
85        mut self,
86        params: impl IntoIterator<Item = (impl Into<String>, impl ToString)>,
87    ) -> Self {
88        for (k, v) in params {
89            self.params.insert(k.into(), v.to_string());
90        }
91        self
92    }
93
94    /// Merges path parameters in iterator order (substitution follows `:segment` order in the path).
95    ///
96    /// Alias for [`params_iter`](Self::params_iter); prefer this name when documenting ordered routes.
97    pub fn params_ordered(
98        self,
99        params: impl IntoIterator<Item = (impl Into<String>, impl ToString)>,
100    ) -> Self {
101        self.params_iter(params)
102    }
103
104    /// Overrides the client base URL for this request only.
105    ///
106    /// # Examples
107    ///
108    /// ```no_run
109    /// # use better_fetch::{Client, Result};
110    /// # #[tokio::main]
111    /// # async fn main() -> Result<()> {
112    /// let client = Client::new("https://api.example.com")?;
113    /// let _ = client
114    ///     .get("/health")
115    ///     .base_url("https://status.example.com")?
116    ///     .send()
117    ///     .await?;
118    /// # Ok(())
119    /// # }
120    /// ```
121    pub fn base_url(mut self, base_url: impl AsRef<str>) -> Result<Self> {
122        self.base_url = Some(Url::parse(base_url.as_ref()).map_err(Error::InvalidBaseUrl)?);
123        Ok(self)
124    }
125
126    /// Adds a query string parameter.
127    pub fn query(mut self, key: impl Into<String>, value: impl ToString) -> Self {
128        self.query
129            .insert(key.into(), QueryValue::Scalar(value.to_string()));
130        self
131    }
132
133    /// Sets multiple query parameters preserving insertion order.
134    pub fn queries(mut self, query: IndexMap<String, QueryValue>) -> Self {
135        for (k, v) in query {
136            self.query.insert(k, v);
137        }
138        self
139    }
140
141    /// Serializes `value` as JSON and uses it as a query parameter (feature `json`).
142    #[cfg(feature = "json")]
143    pub fn query_json<T: serde::Serialize>(
144        mut self,
145        key: impl Into<String>,
146        value: &T,
147    ) -> Result<Self> {
148        self.query
149            .insert(key.into(), QueryValue::from_serializable(value)?);
150        Ok(self)
151    }
152
153    /// Adds a request header.
154    pub fn header(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> Result<Self> {
155        let (name, value) = parse_request_header(key, value)?;
156        self.headers.insert(name, value);
157        Ok(self)
158    }
159
160    /// Sets a JSON request body (feature `json`).
161    #[cfg(feature = "json")]
162    pub fn json<T: serde::Serialize>(mut self, body: &T) -> Result<Self> {
163        let bytes = serde_json::to_vec(body).map_err(|e| Error::Config(e.to_string()))?;
164        self.body = HttpBody::Bytes(Bytes::from(bytes));
165        if !self.headers.contains_key(http::header::CONTENT_TYPE) {
166            self.headers.insert(
167                http::header::CONTENT_TYPE,
168                http::HeaderValue::from_static("application/json"),
169            );
170        }
171        Ok(self)
172    }
173
174    /// Sets a raw request body.
175    pub fn body(mut self, body: impl Into<Bytes>) -> Self {
176        self.body = HttpBody::Bytes(body.into());
177        self
178    }
179
180    /// Sets `Content-Type` when not already present.
181    pub fn content_type(mut self, value: impl AsRef<str>) -> Result<Self> {
182        self.headers.insert(
183            http::header::CONTENT_TYPE,
184            http::HeaderValue::from_str(value.as_ref())
185                .map_err(|e| Error::InvalidHeaderValue(e.to_string()))?,
186        );
187        Ok(self)
188    }
189
190    /// Sets a streaming request body (not replayable with automatic retry).
191    ///
192    /// Sets `Content-Type` to `application/octet-stream` when not already set.
193    pub fn body_stream(mut self, stream: crate::BodyStream) -> Self {
194        self.body = HttpBody::Stream(stream);
195        if !self.headers.contains_key(http::header::CONTENT_TYPE) {
196            self.headers.insert(
197                http::header::CONTENT_TYPE,
198                http::HeaderValue::from_static("application/octet-stream"),
199            );
200        }
201        self
202    }
203
204    /// URL-encoded form body (`application/x-www-form-urlencoded`).
205    pub fn form<I, K, V>(mut self, fields: I) -> Self
206    where
207        I: IntoIterator<Item = (K, V)>,
208        K: AsRef<str>,
209        V: AsRef<str>,
210    {
211        let mut serializer = url::form_urlencoded::Serializer::new(String::new());
212        for (k, v) in fields {
213            serializer.append_pair(k.as_ref(), v.as_ref());
214        }
215        self.body = HttpBody::Bytes(Bytes::from(serializer.finish()));
216        if !self.headers.contains_key(http::header::CONTENT_TYPE) {
217            self.headers.insert(
218                http::header::CONTENT_TYPE,
219                http::HeaderValue::from_static("application/x-www-form-urlencoded"),
220            );
221        }
222        self
223    }
224
225    /// Multipart form body (requires the `multipart` feature).
226    ///
227    /// Automatic retry is not supported when multipart bodies are used.
228    #[cfg(feature = "multipart")]
229    pub fn multipart(mut self, form: crate::multipart::Form) -> Self {
230        self.multipart = Some(form);
231        self.body = HttpBody::Empty;
232        self
233    }
234
235    /// Overrides the client default timeout for this request.
236    pub fn timeout(mut self, timeout: Duration) -> Self {
237        self.timeout = Some(timeout);
238        self
239    }
240
241    /// Overrides the client default retry policy for this request.
242    pub fn retry(mut self, policy: RetryPolicy) -> Self {
243        self.retry = Some(policy);
244        self
245    }
246
247    /// Overrides authentication for this request.
248    pub fn auth(mut self, auth: Auth) -> Self {
249        self.auth = Some(auth);
250        self
251    }
252
253    /// Sets bearer authentication for this request.
254    pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
255        self.auth = Some(Auth::bearer(token));
256        self
257    }
258
259    /// Cancels the in-flight request and retry sleeps when this token is triggered.
260    ///
261    /// # Examples
262    ///
263    /// ```no_run
264    /// # use better_fetch::{CancellationToken, Client, Result};
265    /// # use std::time::Duration;
266    /// # #[tokio::main]
267    /// # async fn main() -> Result<()> {
268    /// let client = Client::new("https://api.example.com")?;
269    /// let token = CancellationToken::new();
270    /// let token_clone = token.clone();
271    /// tokio::spawn(async move {
272    ///     tokio::time::sleep(Duration::from_millis(10)).await;
273    ///     token_clone.cancel();
274    /// });
275    /// let err = client
276    ///     .get("/slow")
277    ///     .cancellation_token(token)
278    ///     .send()
279    ///     .await
280    ///     .unwrap_err();
281    /// assert!(err.is_cancelled());
282    /// # Ok(())
283    /// # }
284    /// ```
285    pub fn cancellation_token(mut self, token: CancellationToken) -> Self {
286        self.cancellation = Some(token);
287        self
288    }
289
290    /// When `true`, [`send`](Self::send) returns `Err` on non-2xx HTTP status (like upstream `throw: true`).
291    pub fn throw_on_error(mut self, throw: bool) -> Self {
292        self.throw_on_error = throw;
293        self
294    }
295
296    /// Overrides the client's JSON parser for this request only.
297    ///
298    /// See [`crate::json_parser`] for fast path vs two-step parsing.
299    #[cfg(feature = "json")]
300    pub fn json_parser<F>(mut self, f: F) -> Self
301    where
302        F: Fn(&Bytes) -> std::result::Result<serde_json::Value, String> + Send + Sync + 'static,
303    {
304        self.json_parser = Some(crate::json_parser::json_parser(f));
305        self
306    }
307
308    /// Overrides the client's JSON parser for this request only.
309    #[cfg(feature = "json")]
310    pub fn json_parser_fn(mut self, parser: JsonParserFn) -> Self {
311        self.json_parser = Some(parser);
312        self
313    }
314
315    /// Executes the request and returns the [`Response`].
316    ///
317    /// Non-2xx responses are returned as `Ok(Response)` unless [`throw_on_error`](Self::throw_on_error)
318    /// is `true`. Deserialize JSON with [`Response::json`](crate::Response::json) or use
319    /// [`send_json`](Self::send_json) for a one-step typed result.
320    ///
321    /// # Examples
322    ///
323    /// ```no_run
324    /// # use better_fetch::{Client, Result};
325    /// # #[tokio::main]
326    /// # async fn main() -> Result<()> {
327    /// let client = Client::new("https://api.example.com")?;
328    /// let response = client.get("/users/1").send().await?;
329    /// if response.is_success() {
330    ///     println!("status {}", response.status());
331    /// }
332    /// # Ok(())
333    /// # }
334    /// ```
335    pub async fn send(self) -> Result<Response> {
336        self.client.execute(self).await
337    }
338
339    /// Maximum response body size in bytes for this request.
340    ///
341    /// Applies to [`send`](Self::send), [`send_json`](Self::send_json), [`send_stream`](Self::send_stream),
342    /// and [`StreamingResponse::collect`](crate::StreamingResponse::collect). When the body would exceed
343    /// the limit, returns [`Error::BodyTooLarge`](crate::Error::BodyTooLarge). On the streaming path,
344    /// the limit is also enforced incrementally on each chunk.
345    pub fn max_response_bytes(mut self, limit: u64) -> Self {
346        self.max_response_bytes = Some(limit);
347        self
348    }
349
350    /// Overrides the client default for how many bytes may be read when a custom retry predicate runs on a stream.
351    pub fn retry_body_peek_bytes(mut self, limit: u64) -> Self {
352        self.retry_body_peek_bytes = Some(limit);
353        self
354    }
355
356    /// Executes the request and returns a [`StreamingResponse`] without buffering the full body.
357    ///
358    /// Uses [`Hooks::on_request`](crate::Hooks::on_request), [`Hooks::on_response_stream`](crate::Hooks::on_response_stream),
359    /// and [`Hooks::on_success_stream`](crate::Hooks::on_success_stream) (2xx). Buffered
360    /// [`Hooks::on_response`](crate::Hooks::on_response) / [`on_success`](crate::Hooks::on_success) are not called.
361    /// Custom retry predicates may peek up to [`ClientBuilder::retry_body_peek_bytes`](crate::ClientBuilder::retry_body_peek_bytes)
362    /// without consuming the body returned to the caller. Non-2xx responses keep the full streaming
363    /// body unless [`Self::throw_on_error`](Self::throw_on_error)(`true`) or a retry discards it.
364    /// Cancellation wakes pending body reads via the cancellation token (checked on each stream poll).
365    ///
366    /// # Examples
367    ///
368    /// ```no_run
369    /// # use better_fetch::{Client, Result};
370    /// # use futures_util::StreamExt;
371    /// # #[tokio::main]
372    /// # async fn main() -> Result<()> {
373    /// let client = Client::new("https://api.example.com")?;
374    /// let mut response = client.get("/export").send_stream().await?;
375    /// while let Some(chunk) = response.bytes_stream().next().await {
376    ///     let _chunk = chunk?;
377    /// }
378    /// # Ok(())
379    /// # }
380    /// ```
381    pub async fn send_stream(self) -> Result<StreamingResponse> {
382        self.client.execute_stream(self).await
383    }
384
385    /// Executes the request and deserializes JSON on success (feature `json`).
386    ///
387    /// Fails with [`Error::Http`](crate::Error::Http) or [`Error::Deserialize`](crate::Error::Deserialize)
388    /// on non-2xx or invalid JSON.
389    ///
390    /// # Examples
391    ///
392    /// ```no_run
393    /// # use better_fetch::{Client, Result};
394    /// # use serde::Deserialize;
395    /// # #[derive(Deserialize)]
396    /// # struct User { id: u64 }
397    /// # #[tokio::main]
398    /// # async fn main() -> Result<()> {
399    /// let client = Client::new("https://api.example.com")?;
400    /// let user: User = client.get("/users/:id").param("id", 1).send_json().await?;
401    /// # Ok(())
402    /// # }
403    /// ```
404    #[cfg(feature = "json")]
405    #[must_use = "send the request with `.await` and handle the result"]
406    pub async fn send_json<T: serde::de::DeserializeOwned>(self) -> Result<T> {
407        self.send().await?.json::<T>().await
408    }
409
410    /// When `false`, [`send_json_validated`](Self::send_json_validated) only deserializes (no garde).
411    #[cfg(feature = "validate")]
412    pub fn validate_response(mut self, validate: bool) -> Self {
413        self.validate_response = validate;
414        self
415    }
416
417    /// `send` + [`Response::json_validated`](crate::Response::json_validated) (feature `validate`).
418    #[cfg(feature = "validate")]
419    pub async fn send_json_validated<T>(self) -> Result<T>
420    where
421        T: serde::de::DeserializeOwned + garde::Validate,
422        T::Context: Default,
423    {
424        if !self.validate_response {
425            return self.send_json().await;
426        }
427        self.send().await?.json_validated().await
428    }
429
430    /// Serializes and validates `body` with [`garde::Validate`] before sending (feature `validate`).
431    #[cfg(feature = "validate")]
432    pub fn json_validated<T>(mut self, body: &T) -> Result<Self>
433    where
434        T: serde::Serialize + garde::Validate,
435        T::Context: Default,
436    {
437        body.validate().map_err(|report| Error::RequestValidation {
438            message: report.to_string(),
439        })?;
440        let bytes = serde_json::to_vec(body).map_err(|e| Error::Config(e.to_string()))?;
441        self.body = HttpBody::Bytes(Bytes::from(bytes));
442        if !self.headers.contains_key(http::header::CONTENT_TYPE) {
443            self.headers.insert(
444                http::header::CONTENT_TYPE,
445                http::HeaderValue::from_static("application/json"),
446            );
447        }
448        Ok(self)
449    }
450}