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