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}