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