Skip to main content

tower_http_client/client/
request_ext.rs

1//! Extensions for the `http::request::Builder`.
2
3use private::Sealed;
4use thiserror::Error;
5
6/// Set body errors.
7#[derive(Debug, Error)]
8#[error(transparent)]
9pub enum SetBodyError<S> {
10    /// An error occurred while setting the body.
11    Body(http::Error),
12    /// An error occurred while encoding the body.
13    Encode(S),
14}
15
16/// Extension trait for the [`http::request::Builder`].
17pub trait RequestBuilderExt: Sized + Sealed {
18    /// Appends a typed header to this request.
19    ///
20    /// This function will append the provided header as a header to the
21    /// internal [`http::HeaderMap`] being constructed.  Essentially this is
22    /// equivalent to calling [`headers::HeaderMapExt::typed_insert`].
23    #[must_use]
24    #[cfg(feature = "typed-header")]
25    #[cfg_attr(docsrs, doc(cfg(feature = "typed-header")))]
26    fn typed_header<T>(self, header: T) -> Self
27    where
28        T: headers::Header;
29
30    /// Sets a JSON body for this request.
31    ///
32    /// Additionally this method adds a `CONTENT_TYPE` header for JSON body.
33    /// If you decide to override the request body, keep this in mind.
34    ///
35    /// # Errors
36    ///
37    /// If the given value's implementation of [`serde::Serialize`] decides to fail.
38    #[cfg(feature = "json")]
39    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
40    fn json<T: serde::Serialize + ?Sized>(
41        self,
42        value: &T,
43    ) -> Result<http::Request<bytes::Bytes>, SetBodyError<serde_json::Error>>;
44
45    /// Sets a form body for this request.
46    ///
47    /// Additionally this method adds a `CONTENT_TYPE` header for form body.
48    /// If you decide to override the request body, keep this in mind.
49    ///
50    /// # Errors
51    ///
52    /// If the given value's implementation of [`serde::Serialize`] decides to fail.
53    #[cfg(feature = "form")]
54    #[cfg_attr(docsrs, doc(cfg(feature = "form")))]
55    fn form<T: serde::Serialize + ?Sized>(
56        self,
57        form: &T,
58    ) -> Result<http::Request<bytes::Bytes>, SetBodyError<serde_urlencoded::ser::Error>>;
59
60    /// Sets the query string of the URL.
61    ///
62    /// Serializes the given value into a query string using [`serde_urlencoded`]
63    /// and replaces the existing query string of the URL entirely. Any previously
64    /// set query parameters are discarded.
65    ///
66    /// # Notes
67    ///
68    /// - Duplicate keys are preserved as-is:
69    ///   `.query(&[("foo", "a"), ("foo", "b")])` produces `"foo=a&foo=b"`.
70    ///
71    /// - This method does not support a single key-value tuple directly.
72    ///   Use a slice like `.query(&[("key", "val")])` instead.
73    ///   Structs and maps that serialize into key-value pairs are also supported.
74    ///
75    /// # Errors
76    ///
77    /// Returns a [`serde_urlencoded::ser::Error`] if the provided value cannot be serialized
78    /// into a query string.
79    #[cfg(feature = "query")]
80    #[cfg_attr(docsrs, doc(cfg(feature = "query")))]
81    fn query<T: serde::Serialize + ?Sized>(
82        self,
83        query: &T,
84    ) -> Result<Self, serde_urlencoded::ser::Error>;
85}
86
87impl RequestBuilderExt for http::request::Builder {
88    #[cfg(feature = "typed-header")]
89    #[cfg_attr(docsrs, doc(cfg(feature = "typed-header")))]
90    fn typed_header<T>(mut self, header: T) -> Self
91    where
92        T: headers::Header,
93    {
94        use headers::HeaderMapExt;
95
96        if let Some(headers) = self.headers_mut() {
97            headers.typed_insert(header);
98        }
99        self
100    }
101
102    #[cfg(feature = "json")]
103    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
104    fn json<T: serde::Serialize + ?Sized>(
105        mut self,
106        value: &T,
107    ) -> Result<http::Request<bytes::Bytes>, SetBodyError<serde_json::Error>> {
108        use http::{HeaderValue, header::CONTENT_TYPE};
109
110        if let Some(headers) = self.headers_mut() {
111            headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
112        }
113
114        let bytes = bytes::Bytes::from(serde_json::to_vec(value).map_err(SetBodyError::Encode)?);
115        self.body(bytes).map_err(SetBodyError::Body)
116    }
117
118    #[cfg(feature = "form")]
119    #[cfg_attr(docsrs, doc(cfg(feature = "form")))]
120    fn form<T: serde::Serialize + ?Sized>(
121        mut self,
122        form: &T,
123    ) -> Result<http::Request<bytes::Bytes>, SetBodyError<serde_urlencoded::ser::Error>> {
124        use http::{HeaderValue, header::CONTENT_TYPE};
125
126        let string = serde_urlencoded::to_string(form).map_err(SetBodyError::Encode)?;
127        if let Some(headers) = self.headers_mut() {
128            headers.insert(
129                CONTENT_TYPE,
130                HeaderValue::from_static("application/x-www-form-urlencoded"),
131            );
132        }
133
134        self.body(bytes::Bytes::from(string))
135            .map_err(SetBodyError::Body)
136    }
137
138    #[cfg(feature = "query")]
139    #[cfg_attr(docsrs, doc(cfg(feature = "query")))]
140    fn query<T: serde::Serialize + ?Sized>(
141        self,
142        query: &T,
143    ) -> Result<Self, serde_urlencoded::ser::Error> {
144        use http::uri::PathAndQuery;
145
146        let mut parts = self.uri_ref().cloned().unwrap_or_default().into_parts();
147        let new_path_and_query = {
148            // If the URI doesn't have a path, we need to set it to "/" so that the query string can be appended correctly.
149            let path = parts
150                .path_and_query
151                .as_ref()
152                .map_or_else(|| "/", |pq| pq.path());
153
154            let query_string = serde_urlencoded::to_string(query)?;
155            let pq_str = [path, "?", &query_string].concat();
156            // serde_urlencoded always produces valid ASCII, so this can never fail.
157            PathAndQuery::try_from(pq_str).expect("invalid path and query after encoding")
158        };
159
160        parts.path_and_query = Some(new_path_and_query);
161        // The parts came from a valid URI with only path_and_query replaced, so this can never fail.
162        let uri = http::Uri::from_parts(parts).expect("invalid URI parts after setting query");
163
164        Ok(self.uri(uri))
165    }
166}
167
168mod private {
169    pub trait Sealed {}
170
171    impl Sealed for http::request::Builder {}
172}
173
174#[cfg(all(test, feature = "query"))]
175mod query_tests {
176    use pretty_assertions::assert_eq;
177    use tower_http::BoxError;
178
179    use super::*;
180
181    #[test]
182    fn test_query_basic() -> Result<(), BoxError> {
183        let request = http::Request::builder()
184            .uri("http://example.com/path")
185            .query(&[("key", "value")])?
186            .body(())?;
187
188        assert_eq!(request.uri().query(), Some("key=value"));
189        Ok(())
190    }
191
192    #[test]
193    fn test_query_without_uri() -> Result<(), BoxError> {
194        let request = http::Request::builder()
195            .query(&[("key", "value")])?
196            .body(())?;
197
198        assert_eq!(request.uri().query(), Some("key=value"));
199        Ok(())
200    }
201
202    #[test]
203    fn test_query_overwrites_existing() -> Result<(), BoxError> {
204        let request = http::Request::builder()
205            .uri("http://example.com/path?existing=1")
206            .query(&[("key", "value")])?
207            .body(())?;
208
209        // "existing=1" must be gone
210        assert_eq!(request.uri().query(), Some("key=value"));
211        Ok(())
212    }
213
214    #[test]
215    fn test_query_last_call_wins() -> Result<(), BoxError> {
216        let request = http::Request::builder()
217            .uri("http://example.com/path")
218            .query(&[("first", "1")])?
219            .query(&[("second", "2")])?
220            .body(())?;
221
222        // Only the last call survives
223        assert_eq!(request.uri().query(), Some("second=2"));
224        Ok(())
225    }
226
227    #[test]
228    fn test_query_duplicate_keys() -> Result<(), BoxError> {
229        let request = http::Request::builder()
230            .uri("http://example.com/path")
231            .query(&[("foo", "a"), ("foo", "b")])?
232            .body(())?;
233
234        assert_eq!(request.uri().query(), Some("foo=a&foo=b"));
235        Ok(())
236    }
237
238    #[test]
239    fn test_query_struct() -> Result<(), BoxError> {
240        #[derive(serde::Serialize)]
241        struct Params {
242            page: u32,
243            limit: u32,
244        }
245
246        let request = http::Request::builder()
247            .uri("http://example.com/path")
248            .query(&Params { page: 2, limit: 10 })?
249            .body(())?;
250
251        assert_eq!(request.uri().query(), Some("page=2&limit=10"));
252        Ok(())
253    }
254
255    #[test]
256    fn test_query_special_characters_are_encoded() -> Result<(), BoxError> {
257        let request = http::Request::builder()
258            .uri("http://example.com/path")
259            .query(&[("key", "hello world")])?
260            .body(())?;
261
262        assert_eq!(request.uri().query(), Some("key=hello+world"));
263        Ok(())
264    }
265
266    #[test]
267    fn test_query_encode_error() {
268        // Scalars (e.g. integers) are not supported by serde_urlencoded
269        let _error: serde_urlencoded::ser::Error = http::Request::builder().query(&42).unwrap_err();
270    }
271}