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 = if query_string.is_empty() {
156                path.to_owned()
157            } else {
158                [path, "?", &query_string].concat()
159            };
160            // serde_urlencoded always produces valid ASCII, so this can never fail.
161            PathAndQuery::try_from(pq_str).expect("invalid path and query after encoding")
162        };
163
164        parts.path_and_query = Some(new_path_and_query);
165        // The parts came from a valid URI with only path_and_query replaced, so this can never fail.
166        let uri = http::Uri::from_parts(parts).expect("invalid URI parts after setting query");
167
168        Ok(self.uri(uri))
169    }
170}
171
172mod private {
173    pub trait Sealed {}
174
175    impl Sealed for http::request::Builder {}
176}
177
178#[cfg(all(test, feature = "query"))]
179mod query_tests {
180    use pretty_assertions::assert_eq;
181    use tower_http::BoxError;
182
183    use super::*;
184
185    #[test]
186    fn test_query_basic() -> Result<(), BoxError> {
187        let request = http::Request::builder()
188            .uri("http://example.com/path")
189            .query(&[("key", "value")])?
190            .body(())?;
191
192        assert_eq!(request.uri().query(), Some("key=value"));
193        Ok(())
194    }
195
196    #[test]
197    fn test_query_without_uri() -> Result<(), BoxError> {
198        let request = http::Request::builder()
199            .query(&[("key", "value")])?
200            .body(())?;
201
202        assert_eq!(request.uri().query(), Some("key=value"));
203        Ok(())
204    }
205
206    #[test]
207    fn test_query_overwrites_existing() -> Result<(), BoxError> {
208        let request = http::Request::builder()
209            .uri("http://example.com/path?existing=1")
210            .query(&[("key", "value")])?
211            .body(())?;
212
213        // "existing=1" must be gone
214        assert_eq!(request.uri().query(), Some("key=value"));
215        Ok(())
216    }
217
218    #[test]
219    fn test_query_last_call_wins() -> Result<(), BoxError> {
220        let request = http::Request::builder()
221            .uri("http://example.com/path")
222            .query(&[("first", "1")])?
223            .query(&[("second", "2")])?
224            .body(())?;
225
226        // Only the last call survives
227        assert_eq!(request.uri().query(), Some("second=2"));
228        Ok(())
229    }
230
231    #[test]
232    fn test_query_duplicate_keys() -> Result<(), BoxError> {
233        let request = http::Request::builder()
234            .uri("http://example.com/path")
235            .query(&[("foo", "a"), ("foo", "b")])?
236            .body(())?;
237
238        assert_eq!(request.uri().query(), Some("foo=a&foo=b"));
239        Ok(())
240    }
241
242    #[test]
243    fn test_query_struct() -> Result<(), BoxError> {
244        #[derive(serde::Serialize)]
245        struct Params {
246            page: u32,
247            limit: u32,
248        }
249
250        let request = http::Request::builder()
251            .uri("http://example.com/path")
252            .query(&Params { page: 2, limit: 10 })?
253            .body(())?;
254
255        assert_eq!(request.uri().query(), Some("page=2&limit=10"));
256        Ok(())
257    }
258
259    #[test]
260    fn test_query_special_characters_are_encoded() -> Result<(), BoxError> {
261        let request = http::Request::builder()
262            .uri("http://example.com/path")
263            .query(&[("key", "hello world")])?
264            .body(())?;
265
266        assert_eq!(request.uri().query(), Some("key=hello+world"));
267        Ok(())
268    }
269
270    #[test]
271    fn test_query_encode_error() {
272        // Scalars (e.g. integers) are not supported by serde_urlencoded
273        let _error: serde_urlencoded::ser::Error = http::Request::builder().query(&42).unwrap_err();
274    }
275
276    #[test]
277    fn test_query_empty_serialization_clears_query() -> Result<(), BoxError> {
278        #[derive(serde::Serialize)]
279        struct Empty {}
280
281        let request = http::Request::builder()
282            .uri("/hello?old=1")
283            .query(&Empty {})?
284            .body(())?;
285
286        // The URI should be "/hello", not "/hello?"
287        assert_eq!(request.uri().path(), "/hello");
288        assert_eq!(request.uri().query(), None);
289        Ok(())
290    }
291}