[go: up one dir, main page]

ureq/
request_ext.rs

1use crate::config::typestate::RequestExtScope;
2use crate::config::{Config, ConfigBuilder, RequestLevelConfig};
3use crate::{http, Agent, AsSendBody, Body, Error};
4use std::ops::Deref;
5use ureq_proto::http::{Request, Response};
6
7/// Extension trait for [`http::Request<impl AsSendBody>`].
8///
9/// Adds additional convenience methods to the `Request` that are not available
10/// in the plain http API.
11pub trait RequestExt<S>
12where
13    S: AsSendBody,
14{
15    /// Allows configuring the request behaviour, starting with the default [`Agent`].
16    ///
17    /// This method allows configuring the request by using the default Agent, and performing
18    /// additional configurations on top.
19    /// This method returns a `WithAgent` struct that it is possible to call `configure()` and `run()`
20    /// on to configure the request behaviour, or run the request.
21    ///
22    /// # Example
23    ///
24    /// ```
25    /// use ureq::{http, RequestExt, Error};
26    ///
27    /// let request: Result<http::Response<_>, Error> = http::Request::builder()
28    ///             .method(http::Method::GET)
29    ///             .uri("http://foo.bar")
30    ///             .body(())
31    ///             .unwrap()
32    ///             .with_default_agent()
33    ///             .configure()
34    ///             .http_status_as_error(false)
35    ///             .run();
36    /// ```
37    fn with_default_agent(self) -> WithAgent<'static, S>
38    where
39        Self: Sized,
40    {
41        let agent = Agent::new_with_defaults();
42        Self::with_agent(self, agent)
43    }
44
45    /// Allows configuring this request behaviour, using a specific [`Agent`].
46    ///
47    /// This method allows configuring the request by using a user-provided `Agent` and performing
48    /// additional configurations on top.
49    /// This method returns a `WithAgent` struct that it is possible to call `configure()` and `run()`
50    /// on to configure the request behaviour, or run the request.
51    ///
52    /// # Example
53    ///
54    /// ```
55    /// use ureq::{http, Agent, RequestExt, Error};
56    /// use std::time::Duration;
57    /// let agent = Agent::config_builder()
58    ///     .timeout_global(Some(Duration::from_secs(30)))
59    ///     .build()
60    ///     .new_agent();
61    ///
62    /// let request: Result<http::Response<_>, Error> = http::Request::builder()
63    ///             .method(http::Method::GET)
64    ///             .uri("http://foo.bar")
65    ///             .body(())
66    ///             .unwrap()
67    ///             .with_agent(&agent)
68    ///             .run();
69    /// ```
70    /// # Example with further customizations
71    ///
72    /// In this example we use a specific agent, but apply a request-specific configuration on top.
73    ///
74    /// ```
75    /// use ureq::{http, Agent, RequestExt, Error};
76    /// use std::time::Duration;
77    /// let mut agent = Agent::config_builder()
78    ///     .timeout_global(Some(Duration::from_secs(30)))
79    ///     .build()
80    ///     .new_agent();
81    ///
82    /// let request: Result<http::Response<_>, Error> = http::Request::builder()
83    ///             .method(http::Method::GET)
84    ///             .uri("http://foo.bar")
85    ///             .body(())
86    ///             .unwrap()
87    ///             .with_agent(&agent)
88    ///             .configure()
89    ///             .http_status_as_error(false)
90    ///             .run();
91    /// ```
92    fn with_agent<'a>(self, agent: impl Into<AgentRef<'a>>) -> WithAgent<'a, S>;
93}
94
95/// Wrapper struct that holds a [`Request`] associated with an [`Agent`].
96pub struct WithAgent<'a, S: AsSendBody> {
97    pub(crate) agent: AgentRef<'a>,
98    pub(crate) request: Request<S>,
99}
100
101impl<'a, S: AsSendBody> WithAgent<'a, S> {
102    /// Returns a [`ConfigBuilder`] for configuring the request.
103    ///
104    /// This allows setting additional request-specific options before sending the request.
105    pub fn configure(self) -> ConfigBuilder<RequestExtScope<'a, S>> {
106        ConfigBuilder(RequestExtScope(self))
107    }
108
109    /// Executes the request using the associated [`Agent`].
110    pub fn run(self) -> Result<Response<Body>, Error> {
111        self.agent.run(self.request)
112    }
113}
114
115impl<'a, S: AsSendBody> WithAgent<'a, S> {
116    pub(crate) fn request_level_config(&mut self) -> &mut Config {
117        let request_level_config = self
118            .request
119            .extensions_mut()
120            .get_mut::<RequestLevelConfig>();
121
122        if request_level_config.is_none() {
123            self.request
124                .extensions_mut()
125                .insert(self.agent.new_request_level_config());
126        }
127
128        // Unwrap is safe because of the above check
129        let req_level: &mut RequestLevelConfig = self
130            .request
131            .extensions_mut()
132            .get_mut::<RequestLevelConfig>()
133            .unwrap();
134
135        &mut req_level.0
136    }
137}
138
139/// Reference type to hold an owned or borrowed [`Agent`].
140pub enum AgentRef<'a> {
141    Owned(Agent),
142    Borrowed(&'a Agent),
143}
144
145impl<S: AsSendBody> RequestExt<S> for http::Request<S> {
146    fn with_agent<'a>(self, agent: impl Into<AgentRef<'a>>) -> WithAgent<'a, S> {
147        WithAgent {
148            agent: agent.into(),
149            request: self,
150        }
151    }
152}
153
154impl From<Agent> for AgentRef<'static> {
155    fn from(value: Agent) -> Self {
156        AgentRef::Owned(value)
157    }
158}
159
160impl<'a> From<&'a Agent> for AgentRef<'a> {
161    fn from(value: &'a Agent) -> Self {
162        AgentRef::Borrowed(value)
163    }
164}
165
166impl Deref for AgentRef<'_> {
167    type Target = Agent;
168
169    fn deref(&self) -> &Self::Target {
170        match self {
171            AgentRef::Owned(agent) => agent,
172            AgentRef::Borrowed(agent) => agent,
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use crate::config::RequestLevelConfig;
181    use std::time::Duration;
182
183    #[test]
184    fn configure_request_with_default_agent() {
185        // Create `http` crate request and configure with trait
186        let request = http::Request::builder()
187            .method(http::Method::GET)
188            .uri("http://foo.bar")
189            .body(())
190            .unwrap()
191            .with_default_agent()
192            .configure()
193            .https_only(true)
194            .build();
195
196        // Assert that the request-level configuration has been set
197        let request_config = request
198            .request
199            .extensions()
200            .get::<RequestLevelConfig>()
201            .cloned()
202            .unwrap();
203
204        assert!(request_config.0.https_only());
205    }
206
207    #[test]
208    fn configure_request_default_agent_2() {
209        // Create `http` crate request and configure with trait
210        let request = http::Request::builder()
211            .method(http::Method::GET)
212            .uri("http://foo.bar")
213            .body(())
214            .unwrap()
215            .with_default_agent()
216            .configure()
217            .https_only(false)
218            .build();
219
220        // Assert that the request-level configuration has been set
221        let request_config = request
222            .request
223            .extensions()
224            .get::<RequestLevelConfig>()
225            .cloned()
226            .unwrap();
227
228        assert!(!request_config.0.https_only());
229    }
230
231    #[test]
232    fn configure_request_default_agent_3() {
233        // Create `http` crate request
234        let request = http::Request::builder()
235            .method(http::Method::POST)
236            .uri("http://foo.bar")
237            .body("Some body")
238            .unwrap();
239
240        // Configure with the trait
241        let request = request
242            .with_default_agent()
243            .configure()
244            .http_status_as_error(true)
245            .build();
246
247        let request_config = request
248            .request
249            .extensions()
250            .get::<RequestLevelConfig>()
251            .cloned()
252            .unwrap();
253
254        assert!(request_config.0.http_status_as_error());
255    }
256
257    #[test]
258    fn configure_request_default_agent_4() {
259        // Create `http` crate request
260        let request = http::Request::builder()
261            .method(http::Method::POST)
262            .uri("http://foo.bar")
263            .body("Some body")
264            .unwrap();
265
266        // Configure with the trait
267        let request = request
268            .with_default_agent()
269            .configure()
270            .http_status_as_error(false)
271            .build();
272
273        let request_config = request
274            .request
275            .extensions()
276            .get::<RequestLevelConfig>()
277            .cloned()
278            .unwrap();
279
280        assert!(!request_config.0.http_status_as_error());
281    }
282
283    #[test]
284    fn configure_request_specified_agent() {
285        // Create `http` crate request
286        let request = http::Request::builder()
287            .method(http::Method::POST)
288            .uri("http://foo.bar")
289            .body("Some body")
290            .unwrap();
291
292        // Configure with the trait
293        let agent = Agent::config_builder()
294            .timeout_per_call(Some(Duration::from_secs(60)))
295            .build()
296            .new_agent();
297
298        let request = request
299            .with_agent(&agent)
300            .configure()
301            .http_status_as_error(false)
302            .build();
303
304        let request_config = request
305            .request
306            .extensions()
307            .get::<RequestLevelConfig>()
308            .cloned()
309            .unwrap();
310
311        // The request-level config is the agent defaults + the explicitly configured stuff
312        assert!(!request_config.0.http_status_as_error());
313        assert_eq!(
314            request_config.0.timeouts().per_call,
315            Some(Duration::from_secs(60))
316        );
317    }
318}