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}