tower_http_client/client/
client_request.rs

1//! Useful utilities for constructing HTTP requests.
2
3use std::{any::Any, future::Future, marker::PhantomData};
4
5use http::{Extensions, HeaderMap, HeaderName, HeaderValue, Method, Uri, Version};
6use tower_service::Service;
7
8use super::{IntoUri, ServiceExt as _};
9
10type EmptyBody = ();
11
12const EMPTY_BODY: EmptyBody = ();
13
14/// An [`http::Request`] builder.
15///
16/// Generally, this builder copies the behavior of the [`http::request::Builder`],
17/// but unlike it, this builder contains a reference to the client and is able to send a
18/// constructed request. Also, this builder borrows most useful methods from the [`reqwest`] one.
19///
20/// [`reqwest`]: https://docs.rs/reqwest/latest/reqwest/struct.RequestBuilder.html
21pub struct ClientRequestBuilder<'a, S, Err, RespBody> {
22    service: &'a mut S,
23    builder: http::request::Builder,
24    _phantom: PhantomData<(Err, RespBody)>,
25}
26
27impl<'a, S, Err, RespBody> ClientRequestBuilder<'a, S, Err, RespBody> {
28    /// Sets the HTTP method for this request.
29    ///
30    /// By default this is `GET`.
31    #[must_use]
32    pub fn method<T>(mut self, method: T) -> Self
33    where
34        Method: TryFrom<T>,
35        <Method as TryFrom<T>>::Error: Into<http::Error>,
36    {
37        self.builder = self.builder.method(method);
38        self
39    }
40
41    /// Sets the URI for this request
42    ///
43    /// By default this is `/`.
44    #[must_use]
45    pub fn uri<U: IntoUri>(mut self, uri: U) -> Self
46    where
47        Uri: TryFrom<U::TryInto>,
48        <Uri as TryFrom<U::TryInto>>::Error: Into<http::Error>,
49    {
50        self.builder = self.builder.uri(uri.into_uri());
51        self
52    }
53
54    /// Set the HTTP version for this request.
55    ///
56    /// By default this is HTTP/1.1.
57    #[must_use]
58    pub fn version(mut self, version: Version) -> Self {
59        self.builder = self.builder.version(version);
60        self
61    }
62
63    /// Appends a header to this request.
64    ///
65    /// This function will append the provided key/value as a header to the
66    /// internal [`HeaderMap`] being constructed.  Essentially this is
67    /// equivalent to calling [`HeaderMap::append`].
68    #[must_use]
69    pub fn header<K, V>(mut self, key: K, value: V) -> Self
70    where
71        HeaderName: TryFrom<K>,
72        HeaderValue: TryFrom<V>,
73        <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
74        <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
75    {
76        self.builder = self.builder.header(key, value);
77        self
78    }
79
80    /// Returns a mutable reference to headers of this request builder.
81    ///
82    /// If builder contains error returns `None`.
83    pub fn headers_mut(&mut self) -> Option<&mut HeaderMap<HeaderValue>> {
84        self.builder.headers_mut()
85    }
86
87    /// Adds an extension to this builder.
88    #[must_use]
89    pub fn extension<T>(mut self, extension: T) -> Self
90    where
91        T: Clone + Any + Send + Sync + 'static,
92    {
93        self.builder = self.builder.extension(extension);
94        self
95    }
96
97    /// Returns a mutable reference to the extensions of this request builder.
98    ///
99    /// If builder contains error returns `None`.
100    #[must_use]
101    pub fn extensions_mut(&mut self) -> Option<&mut Extensions> {
102        self.builder.extensions_mut()
103    }
104
105    /// "Consumes" this builder, using the provided `body` to return a
106    /// constructed [`ClientRequest`].
107    ///
108    /// # Errors
109    ///
110    /// Same as the [`http::request::Builder::body`]
111    pub fn body<NewReqBody>(
112        self,
113        body: impl Into<NewReqBody>,
114    ) -> Result<ClientRequest<'a, S, Err, NewReqBody, RespBody>, http::Error> {
115        Ok(ClientRequest {
116            service: self.service,
117            request: self.builder.body(body.into())?,
118            _phantom: PhantomData,
119        })
120    }
121
122    /// Sets a JSON body for this request.
123    ///
124    /// Additionally this method adds a `CONTENT_TYPE` header for JSON body.
125    /// If you decide to override the request body, keep this in mind.
126    ///
127    /// # Errors
128    ///
129    /// If the given value's implementation of [`serde::Serialize`] decides to fail.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    #[doc = include_str!("../../examples/send_json.rs")]
135    /// ```
136    #[cfg(feature = "json")]
137    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
138    pub fn json<T: serde::Serialize + ?Sized>(
139        self,
140        value: &T,
141    ) -> Result<
142        ClientRequest<'a, S, Err, bytes::Bytes, RespBody>,
143        super::request_ext::SetBodyError<serde_json::Error>,
144    > {
145        use super::RequestBuilderExt as _;
146
147        Ok(ClientRequest {
148            service: self.service,
149            request: self.builder.json(value)?,
150            _phantom: PhantomData,
151        })
152    }
153
154    /// Sets a form body for this request.
155    ///
156    /// Additionally this method adds a `CONTENT_TYPE` header for form body.
157    /// If you decide to override the request body, keep this in mind.
158    ///
159    /// # Errors
160    ///
161    /// If the given value's implementation of [`serde::Serialize`] decides to fail.
162    ///
163    /// # Examples
164    ///
165    /// ```
166    #[doc = include_str!("../../examples/send_form.rs")]
167    /// ```
168    #[cfg(feature = "form")]
169    #[cfg_attr(docsrs, doc(cfg(feature = "form")))]
170    pub fn form<T: serde::Serialize + ?Sized>(
171        self,
172        form: &T,
173    ) -> Result<
174        ClientRequest<'a, S, Err, String, RespBody>,
175        super::request_ext::SetBodyError<serde_urlencoded::ser::Error>,
176    > {
177        use super::RequestBuilderExt as _;
178
179        Ok(ClientRequest {
180            service: self.service,
181            request: self.builder.form(form)?,
182            _phantom: PhantomData,
183        })
184    }
185
186    /// Appends a typed header to this request.
187    ///
188    /// This function will append the provided header as a header to the
189    /// internal [`HeaderMap`] being constructed.  Essentially this is
190    /// equivalent to calling [`headers::HeaderMapExt::typed_insert`].
191    #[must_use]
192    #[cfg(feature = "typed-header")]
193    #[cfg_attr(docsrs, doc(cfg(feature = "typed_header")))]
194    pub fn typed_header<T>(mut self, header: T) -> Self
195    where
196        T: headers::Header,
197    {
198        use super::RequestBuilderExt as _;
199
200        self.builder = self.builder.typed_header(header);
201        self
202    }
203
204    /// Consumes this builder and returns a constructed request without a body.
205    ///
206    /// # Errors
207    ///
208    /// If erroneous data was passed during the query building process.
209    #[allow(clippy::missing_panics_doc)]
210    pub fn build(self) -> ClientRequest<'a, S, Err, EmptyBody, RespBody> {
211        ClientRequest {
212            service: self.service,
213            request: self
214                .builder
215                .body(EMPTY_BODY)
216                .expect("failed to build request without a body"),
217            _phantom: PhantomData,
218        }
219    }
220}
221
222impl<S, Err, RespBody> std::fmt::Debug for ClientRequestBuilder<'_, S, Err, RespBody> {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        f.debug_struct("ClientRequestBuilder")
225            .field("builder", &self.builder)
226            .finish_non_exhaustive()
227    }
228}
229
230impl<S, Err, RespBody> From<ClientRequestBuilder<'_, S, Err, RespBody>> for http::request::Builder {
231    fn from(builder: ClientRequestBuilder<'_, S, Err, RespBody>) -> Self {
232        builder.builder
233    }
234}
235
236/// An [`http::Request`] wrapper with a reference to a client.
237///
238/// This struct is used to send constructed HTTP request by using a client.
239pub struct ClientRequest<'a, S, Err, ReqBody, RespBody> {
240    service: &'a mut S,
241    request: http::Request<ReqBody>,
242    _phantom: PhantomData<(Err, RespBody)>,
243}
244
245impl<'a, S, Err, RespBody> ClientRequest<'a, S, Err, (), RespBody> {
246    /// Creates a client request builder.
247    pub fn builder(service: &'a mut S) -> ClientRequestBuilder<'a, S, Err, RespBody> {
248        ClientRequestBuilder {
249            service,
250            builder: http::Request::builder(),
251            _phantom: PhantomData,
252        }
253    }
254}
255
256/// Workaround for impl trait lifetimes capturing rules:
257/// https://github.com/rust-lang/rust/issues/34511#issuecomment-373423999
258#[doc(hidden)]
259pub trait Captures<U> {}
260
261impl<T: ?Sized, U> Captures<U> for T {}
262
263impl<'a, S, Err, RespBody> ClientRequestBuilder<'a, S, Err, RespBody> {
264    /// Sends the request to the target URI.
265    ///
266    /// # Panics
267    ///
268    /// - if the `ReqBody` is not valid body.
269    pub fn send<ReqBody>(
270        self,
271    ) -> impl Future<Output = Result<http::Response<RespBody>, Err>> + Captures<&'a ()>
272    where
273        S: Service<http::Request<ReqBody>, Response = http::Response<RespBody>, Error = Err>,
274        S::Future: Send + 'static,
275        S::Error: 'static,
276        ReqBody: Default,
277    {
278        let request = self
279            .builder
280            .body(ReqBody::default())
281            .expect("failed to build request without a body");
282        self.service.execute(request)
283    }
284}
285
286impl<'a, S, Err, ReqBody, RespBody> ClientRequest<'a, S, Err, ReqBody, RespBody> {
287    /// Sends the request to the target URI.
288    pub fn send<R>(
289        self,
290    ) -> impl Future<Output = Result<http::Response<RespBody>, Err>> + Captures<&'a ()>
291    where
292        S: Service<http::Request<R>, Response = http::Response<RespBody>, Error = Err>,
293        S::Future: Send + 'static,
294        S::Error: 'static,
295        R: From<ReqBody>,
296    {
297        self.service.execute(self.request)
298    }
299}
300
301impl<S, Err, ReqBody, RespBody> std::fmt::Debug for ClientRequest<'_, S, Err, ReqBody, RespBody>
302where
303    ReqBody: std::fmt::Debug,
304{
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        f.debug_struct("ClientRequest")
307            .field("request", &self.request)
308            .finish_non_exhaustive()
309    }
310}
311
312impl<S, Err, ReqBody, RespBody> From<ClientRequest<'_, S, Err, ReqBody, RespBody>>
313    for http::Request<ReqBody>
314{
315    fn from(request: ClientRequest<'_, S, Err, ReqBody, RespBody>) -> Self {
316        request.request
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use http::Method;
323    use reqwest::Client;
324    use tower::ServiceBuilder;
325    use tower_reqwest::HttpClientLayer;
326
327    use crate::ServiceExt as _;
328
329    // Check that client request builder uses proper methods.
330    #[test]
331    fn test_service_ext_request_builder_methods() {
332        let mut fake_client = ServiceBuilder::new()
333            .layer(HttpClientLayer)
334            .service(Client::new());
335
336        assert_eq!(
337            fake_client.get("http://localhost").build().request.method(),
338            Method::GET
339        );
340        assert_eq!(
341            fake_client
342                .post("http://localhost")
343                .build()
344                .request
345                .method(),
346            Method::POST
347        );
348        assert_eq!(
349            fake_client.put("http://localhost").build().request.method(),
350            Method::PUT
351        );
352        assert_eq!(
353            fake_client
354                .patch("http://localhost")
355                .build()
356                .request
357                .method(),
358            Method::PATCH
359        );
360        assert_eq!(
361            fake_client
362                .delete("http://localhost")
363                .build()
364                .request
365                .method(),
366            Method::DELETE
367        );
368        assert_eq!(
369            fake_client
370                .head("http://localhost")
371                .build()
372                .request
373                .method(),
374            Method::HEAD
375        );
376    }
377}