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}