1 | use std::io::Read; |
2 | use std::{fmt, time}; |
3 | |
4 | use url::{form_urlencoded, ParseError, Url}; |
5 | |
6 | use crate::agent::Agent; |
7 | use crate::body::Payload; |
8 | use crate::error::{Error, ErrorKind}; |
9 | use crate::header::{self, Header}; |
10 | use crate::middleware::MiddlewareNext; |
11 | use crate::unit::{self, Unit}; |
12 | use crate::Response; |
13 | |
14 | pub type Result<T> = std::result::Result<T, Error>; |
15 | |
16 | /// Request instances are builders that creates a request. |
17 | /// |
18 | /// ``` |
19 | /// # fn main() -> Result<(), ureq::Error> { |
20 | /// # ureq::is_test(true); |
21 | /// let response = ureq::get("http://example.com/get" ) |
22 | /// .query("foo" , "bar baz" ) // add ?foo=bar+baz |
23 | /// .call()?; // run the request |
24 | /// # Ok(()) |
25 | /// # } |
26 | /// ``` |
27 | #[derive (Clone)] |
28 | #[must_use = "Requests do nothing until consumed by `call()`" ] |
29 | pub struct Request { |
30 | agent: Agent, |
31 | method: String, |
32 | url: String, |
33 | pub(crate) headers: Vec<Header>, |
34 | timeout: Option<time::Duration>, |
35 | } |
36 | |
37 | impl fmt::Debug for Request { |
38 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
39 | write!( |
40 | f, |
41 | "Request( {} {}, {:?})" , |
42 | self.method, self.url, self.headers |
43 | ) |
44 | } |
45 | } |
46 | |
47 | impl Request { |
48 | pub(crate) fn new(agent: Agent, method: String, url: String) -> Request { |
49 | Request { |
50 | agent, |
51 | method, |
52 | url, |
53 | headers: vec![], |
54 | timeout: None, |
55 | } |
56 | } |
57 | |
58 | #[inline (always)] |
59 | /// Sets overall timeout for the request, overriding agent's configuration if any. |
60 | pub fn timeout(mut self, timeout: time::Duration) -> Self { |
61 | self.timeout = Some(timeout); |
62 | self |
63 | } |
64 | |
65 | /// Sends the request with no body and blocks the caller until done. |
66 | /// |
67 | /// Use this with GET, HEAD, OPTIONS or TRACE. It sends neither |
68 | /// Content-Length nor Transfer-Encoding. |
69 | /// |
70 | /// ``` |
71 | /// # fn main() -> Result<(), ureq::Error> { |
72 | /// # ureq::is_test(true); |
73 | /// let resp = ureq::get("http://example.com/" ) |
74 | /// .call()?; |
75 | /// # Ok(()) |
76 | /// # } |
77 | /// ``` |
78 | pub fn call(self) -> Result<Response> { |
79 | self.do_call(Payload::Empty) |
80 | } |
81 | |
82 | fn parse_url(&self) -> Result<Url> { |
83 | Ok(self.url.parse().and_then(|url: Url| |
84 | // No hostname is fine for urls in general, but not for website urls. |
85 | if url.host_str().is_none() { |
86 | Err(ParseError::EmptyHost) |
87 | } else { |
88 | Ok(url) |
89 | } |
90 | )?) |
91 | } |
92 | |
93 | /// Add Accept-Encoding header with supported values, unless user has |
94 | /// already set this header or is requesting a specific byte-range. |
95 | #[cfg (any(feature = "gzip" , feature = "brotli" ))] |
96 | fn add_accept_encoding(&mut self) { |
97 | let should_add = !self.headers.iter().map(|h| h.name()).any(|name| { |
98 | name.eq_ignore_ascii_case("accept-encoding" ) || name.eq_ignore_ascii_case("range" ) |
99 | }); |
100 | if should_add { |
101 | const GZ: bool = cfg!(feature = "gzip" ); |
102 | const BR: bool = cfg!(feature = "brotli" ); |
103 | const ACCEPT: &str = match (GZ, BR) { |
104 | (true, true) => "gzip, br" , |
105 | (true, false) => "gzip" , |
106 | (false, true) => "br" , |
107 | (false, false) => "identity" , // unreachable due to cfg feature on this fn |
108 | }; |
109 | self.headers.push(Header::new("accept-encoding" , ACCEPT)); |
110 | } |
111 | } |
112 | |
113 | #[cfg_attr (not(any(feature = "gzip" , feature = "brotli" )), allow(unused_mut))] |
114 | fn do_call(mut self, payload: Payload) -> Result<Response> { |
115 | for h in &self.headers { |
116 | h.validate()?; |
117 | } |
118 | let url = self.parse_url()?; |
119 | |
120 | #[cfg (any(feature = "gzip" , feature = "brotli" ))] |
121 | self.add_accept_encoding(); |
122 | |
123 | let deadline = match self.timeout.or(self.agent.config.timeout) { |
124 | None => None, |
125 | Some(timeout) => { |
126 | let now = time::Instant::now(); |
127 | match now.checked_add(timeout) { |
128 | Some(dl) => Some(dl), |
129 | None => { |
130 | return Err(Error::new( |
131 | ErrorKind::Io, |
132 | Some("Request deadline overflowed" .to_string()), |
133 | )) |
134 | } |
135 | } |
136 | } |
137 | }; |
138 | |
139 | let request_fn = |req: Request| { |
140 | let reader = payload.into_read(); |
141 | let unit = Unit::new( |
142 | &req.agent, |
143 | &req.method, |
144 | &url, |
145 | req.headers, |
146 | &reader, |
147 | deadline, |
148 | ); |
149 | |
150 | unit::connect(unit, true, reader).map_err(|e| e.url(url.clone())) |
151 | }; |
152 | |
153 | let response = if !self.agent.state.middleware.is_empty() { |
154 | // Clone agent to get a local copy with same lifetime as Payload |
155 | let agent = self.agent.clone(); |
156 | let chain = &mut agent.state.middleware.iter().map(|mw| mw.as_ref()); |
157 | |
158 | let request_fn = Box::new(request_fn); |
159 | |
160 | let next = MiddlewareNext { chain, request_fn }; |
161 | |
162 | // // Run middleware chain |
163 | next.handle(self)? |
164 | } else { |
165 | // Run the request_fn without any further indirection. |
166 | request_fn(self)? |
167 | }; |
168 | |
169 | if response.status() >= 400 { |
170 | Err(Error::Status(response.status(), response)) |
171 | } else { |
172 | Ok(response) |
173 | } |
174 | } |
175 | |
176 | /// Send data a json value. |
177 | /// |
178 | /// The `Content-Length` header is implicitly set to the length of the serialized value. |
179 | /// |
180 | /// ``` |
181 | /// # fn main() -> Result<(), ureq::Error> { |
182 | /// # ureq::is_test(true); |
183 | /// let resp = ureq::post("http://httpbin.org/post" ) |
184 | /// .send_json(ureq::json!({ |
185 | /// "name" : "martin" , |
186 | /// "rust" : true, |
187 | /// }))?; |
188 | /// # Ok(()) |
189 | /// # } |
190 | /// ``` |
191 | #[cfg (feature = "json" )] |
192 | pub fn send_json(mut self, data: impl serde::Serialize) -> Result<Response> { |
193 | if self.header("Content-Type" ).is_none() { |
194 | self = self.set("Content-Type" , "application/json" ); |
195 | } |
196 | |
197 | let json_bytes = serde_json::to_vec(&data) |
198 | .expect("Failed to serialize data passed to send_json into JSON" ); |
199 | |
200 | self.do_call(Payload::Bytes(&json_bytes)) |
201 | } |
202 | |
203 | /// Send data as bytes. |
204 | /// |
205 | /// The `Content-Length` header is implicitly set to the length of the serialized value. |
206 | /// |
207 | /// ``` |
208 | /// # fn main() -> Result<(), ureq::Error> { |
209 | /// # ureq::is_test(true); |
210 | /// let resp = ureq::put("http://httpbin.org/put" ) |
211 | /// .send_bytes(&[0; 1000])?; |
212 | /// # Ok(()) |
213 | /// # } |
214 | /// ``` |
215 | pub fn send_bytes(self, data: &[u8]) -> Result<Response> { |
216 | self.do_call(Payload::Bytes(data)) |
217 | } |
218 | |
219 | /// Send data as a string. |
220 | /// |
221 | /// The `Content-Length` header is implicitly set to the length of the serialized value. |
222 | /// Defaults to `utf-8` |
223 | /// |
224 | /// ## Charset support |
225 | /// |
226 | /// Requires feature `ureq = { version = "*", features = ["charset"] }` |
227 | /// |
228 | /// If a `Content-Type` header is present and it contains a charset specification, we |
229 | /// attempt to encode the string using that character set. If it fails, we fall back |
230 | /// on utf-8. |
231 | /// |
232 | /// ``` |
233 | /// // this example requires features = ["charset"] |
234 | /// |
235 | /// # fn main() -> Result<(), ureq::Error> { |
236 | /// # ureq::is_test(true); |
237 | /// let resp = ureq::post("http://httpbin.org/post" ) |
238 | /// .set("Content-Type" , "text/plain; charset=iso-8859-1" ) |
239 | /// .send_string("Hällo Wörld!" )?; |
240 | /// # Ok(()) |
241 | /// # } |
242 | /// ``` |
243 | pub fn send_string(self, data: &str) -> Result<Response> { |
244 | let charset = |
245 | crate::response::charset_from_content_type(self.header("content-type" )).to_string(); |
246 | self.do_call(Payload::Text(data, charset)) |
247 | } |
248 | |
249 | /// Send a sequence of (key, value) pairs as form-urlencoded data. |
250 | /// |
251 | /// The `Content-Type` header is implicitly set to application/x-www-form-urlencoded. |
252 | /// The `Content-Length` header is implicitly set to the length of the serialized value. |
253 | /// |
254 | /// ``` |
255 | /// # fn main() -> Result<(), ureq::Error> { |
256 | /// # ureq::is_test(true); |
257 | /// let resp = ureq::post("http://httpbin.org/post" ) |
258 | /// .send_form(&[ |
259 | /// ("foo" , "bar" ), |
260 | /// ("foo2" , "bar2" ), |
261 | /// ])?; |
262 | /// # Ok(()) |
263 | /// # } |
264 | /// ``` |
265 | pub fn send_form(mut self, data: &[(&str, &str)]) -> Result<Response> { |
266 | if self.header("Content-Type" ).is_none() { |
267 | self = self.set("Content-Type" , "application/x-www-form-urlencoded" ); |
268 | } |
269 | let encoded = form_urlencoded::Serializer::new(String::new()) |
270 | .extend_pairs(data) |
271 | .finish(); |
272 | self.do_call(Payload::Bytes(&encoded.into_bytes())) |
273 | } |
274 | |
275 | /// Send data from a reader. |
276 | /// |
277 | /// If no Content-Length and Transfer-Encoding header has been set, it uses the [chunked transfer encoding](https://tools.ietf.org/html/rfc7230#section-4.1). |
278 | /// |
279 | /// The caller may set the Content-Length header to the expected byte size of the reader if is |
280 | /// known. |
281 | /// |
282 | /// The input from the reader is buffered into chunks of size 16,384, the max size of a TLS fragment. |
283 | /// |
284 | /// ``` |
285 | /// use std::io::Cursor; |
286 | /// # fn main() -> Result<(), ureq::Error> { |
287 | /// # ureq::is_test(true); |
288 | /// let read = Cursor::new(vec![0x20; 100]); |
289 | /// let resp = ureq::post("http://httpbin.org/post" ) |
290 | /// .send(read)?; |
291 | /// # Ok(()) |
292 | /// # } |
293 | /// ``` |
294 | pub fn send(self, reader: impl Read) -> Result<Response> { |
295 | self.do_call(Payload::Reader(Box::new(reader))) |
296 | } |
297 | |
298 | /// Set a header field. |
299 | /// |
300 | /// ``` |
301 | /// # fn main() -> Result<(), ureq::Error> { |
302 | /// # ureq::is_test(true); |
303 | /// let resp = ureq::get("http://httpbin.org/bytes/1000" ) |
304 | /// .set("Accept" , "text/plain" ) |
305 | /// .set("Range" , "bytes=500-999" ) |
306 | /// .call()?; |
307 | /// # Ok(()) |
308 | /// # } |
309 | /// ``` |
310 | pub fn set(mut self, header: &str, value: &str) -> Self { |
311 | header::add_header(&mut self.headers, Header::new(header, value)); |
312 | self |
313 | } |
314 | |
315 | /// Returns the value for a set header. |
316 | /// |
317 | /// ``` |
318 | /// let req = ureq::get("/my_page" ) |
319 | /// .set("X-API-Key" , "foobar" ); |
320 | /// assert_eq!("foobar" , req.header("x-api-Key" ).unwrap()); |
321 | /// ``` |
322 | pub fn header(&self, name: &str) -> Option<&str> { |
323 | header::get_header(&self.headers, name) |
324 | } |
325 | |
326 | /// A list of the set header names in this request. Lowercased to be uniform. |
327 | /// |
328 | /// ``` |
329 | /// let req = ureq::get("/my_page" ) |
330 | /// .set("X-API-Key" , "foobar" ) |
331 | /// .set("Content-Type" , "application/json" ); |
332 | /// assert_eq!(req.header_names(), vec!["x-api-key" , "content-type" ]); |
333 | /// ``` |
334 | pub fn header_names(&self) -> Vec<String> { |
335 | self.headers |
336 | .iter() |
337 | .map(|h| h.name().to_ascii_lowercase()) |
338 | .collect() |
339 | } |
340 | |
341 | /// Tells if the header has been set. |
342 | /// |
343 | /// ``` |
344 | /// let req = ureq::get("/my_page" ) |
345 | /// .set("X-API-Key" , "foobar" ); |
346 | /// assert_eq!(true, req.has("x-api-Key" )); |
347 | /// ``` |
348 | pub fn has(&self, name: &str) -> bool { |
349 | header::has_header(&self.headers, name) |
350 | } |
351 | |
352 | /// All headers corresponding values for the give name, or empty vector. |
353 | /// |
354 | /// ``` |
355 | /// let req = ureq::get("/my_page" ) |
356 | /// .set("X-Forwarded-For" , "1.2.3.4" ) |
357 | /// .set("X-Forwarded-For" , "2.3.4.5" ); |
358 | /// |
359 | /// assert_eq!(req.all("x-forwarded-for" ), vec![ |
360 | /// "1.2.3.4" , |
361 | /// "2.3.4.5" , |
362 | /// ]); |
363 | /// ``` |
364 | pub fn all(&self, name: &str) -> Vec<&str> { |
365 | header::get_all_headers(&self.headers, name) |
366 | } |
367 | |
368 | /// Set a query parameter. |
369 | /// |
370 | /// For example, to set `?format=json&dest=/login` |
371 | /// |
372 | /// ``` |
373 | /// # fn main() -> Result<(), ureq::Error> { |
374 | /// # ureq::is_test(true); |
375 | /// let resp = ureq::get("http://httpbin.org/get" ) |
376 | /// .query("format" , "json" ) |
377 | /// .query("dest" , "/login" ) |
378 | /// .call()?; |
379 | /// # Ok(()) |
380 | /// # } |
381 | /// ``` |
382 | pub fn query(mut self, param: &str, value: &str) -> Self { |
383 | if let Ok(mut url) = self.parse_url() { |
384 | url.query_pairs_mut().append_pair(param, value); |
385 | |
386 | // replace url |
387 | self.url = url.to_string(); |
388 | } |
389 | self |
390 | } |
391 | |
392 | /// Set multi query parameters. |
393 | /// |
394 | /// For example, to set `?format=json&dest=/login` |
395 | /// |
396 | /// ``` |
397 | /// # fn main() -> Result<(), ureq::Error> { |
398 | /// # ureq::is_test(true); |
399 | /// |
400 | /// let query = vec![ |
401 | /// ("format" , "json" ), |
402 | /// ("dest" , "/login" ), |
403 | /// ]; |
404 | /// |
405 | /// let resp = ureq::get("http://httpbin.org/get" ) |
406 | /// .query_pairs(query) |
407 | /// .call()?; |
408 | /// # Ok(()) |
409 | /// # } |
410 | /// ``` |
411 | pub fn query_pairs<'a, P>(mut self, pairs: P) -> Self |
412 | where |
413 | P: IntoIterator<Item = (&'a str, &'a str)>, |
414 | { |
415 | if let Ok(mut url) = self.parse_url() { |
416 | { |
417 | let mut query_pairs = url.query_pairs_mut(); |
418 | for (param, value) in pairs { |
419 | query_pairs.append_pair(param, value); |
420 | } |
421 | } |
422 | |
423 | // replace url |
424 | self.url = url.to_string(); |
425 | } |
426 | self |
427 | } |
428 | |
429 | /// Returns the value of the request method. Something like `GET`, `POST`, `PUT` etc. |
430 | /// |
431 | /// ``` |
432 | /// let req = ureq::put("http://httpbin.org/put" ); |
433 | /// |
434 | /// assert_eq!(req.method(), "PUT" ); |
435 | /// ``` |
436 | pub fn method(&self) -> &str { |
437 | &self.method |
438 | } |
439 | |
440 | /// Get the url str that will be used for this request. |
441 | /// |
442 | /// The url might differ from that originally provided when constructing the |
443 | /// request if additional query parameters have been added using [`Request::query()`]. |
444 | /// |
445 | /// In case the original url provided to build the request is not possible to |
446 | /// parse to a Url, this function returns the original, and it will error once the |
447 | /// Request object is used. |
448 | /// |
449 | /// ``` |
450 | /// # fn main() -> Result<(), ureq::Error> { |
451 | /// # ureq::is_test(true); |
452 | /// let req = ureq::get("http://httpbin.org/get" ) |
453 | /// .query("foo" , "bar" ); |
454 | /// |
455 | /// assert_eq!(req.url(), "http://httpbin.org/get?foo=bar" ); |
456 | /// # Ok(()) |
457 | /// # } |
458 | /// ``` |
459 | /// |
460 | /// ``` |
461 | /// # fn main() -> Result<(), ureq::Error> { |
462 | /// # ureq::is_test(true); |
463 | /// let req = ureq::get("SO WRONG" ) |
464 | /// .query("foo" , "bar" ); // does nothing |
465 | /// |
466 | /// assert_eq!(req.url(), "SO WRONG" ); |
467 | /// # Ok(()) |
468 | /// # } |
469 | /// ``` |
470 | pub fn url(&self) -> &str { |
471 | &self.url |
472 | } |
473 | |
474 | /// Get the parsed url that will be used for this request. The parsed url |
475 | /// has functions to inspect the parts of the url further. |
476 | /// |
477 | /// The url might differ from that originally provided when constructing the |
478 | /// request if additional query parameters have been added using [`Request::query()`]. |
479 | /// |
480 | /// Returns a `Result` since a common use case is to construct |
481 | /// the [`Request`] using a `&str` in which case the url needs to be parsed |
482 | /// to inspect the parts. If the Request url is not possible to parse, this |
483 | /// function produces the same error that would otherwise happen when |
484 | /// `call` or `send_*` is called. |
485 | /// |
486 | /// ``` |
487 | /// # fn main() -> Result<(), ureq::Error> { |
488 | /// # ureq::is_test(true); |
489 | /// let req = ureq::get("http://httpbin.org/get" ) |
490 | /// .query("foo" , "bar" ); |
491 | /// |
492 | /// assert_eq!(req.request_url()?.host(), "httpbin.org" ); |
493 | /// # Ok(()) |
494 | /// # } |
495 | /// ``` |
496 | pub fn request_url(&self) -> Result<RequestUrl> { |
497 | Ok(RequestUrl::new(self.parse_url()?)) |
498 | } |
499 | } |
500 | |
501 | /// Parsed result of a request url with handy inspection methods. |
502 | #[derive (Debug, Clone)] |
503 | pub struct RequestUrl { |
504 | url: Url, |
505 | query_pairs: Vec<(String, String)>, |
506 | } |
507 | |
508 | impl RequestUrl { |
509 | fn new(url: Url) -> Self { |
510 | // This is needed to avoid url::Url Cow<str>. We want ureq API to work with &str. |
511 | let query_pairs = url |
512 | .query_pairs() |
513 | .map(|(k, v)| (k.to_string(), v.to_string())) |
514 | .collect(); |
515 | |
516 | RequestUrl { url, query_pairs } |
517 | } |
518 | |
519 | /// Handle the request url as a standard [`url::Url`]. |
520 | pub fn as_url(&self) -> &Url { |
521 | &self.url |
522 | } |
523 | |
524 | /// Get the scheme of the request url, i.e. "https" or "http". |
525 | pub fn scheme(&self) -> &str { |
526 | self.url.scheme() |
527 | } |
528 | |
529 | /// Host of the request url. |
530 | pub fn host(&self) -> &str { |
531 | // this unwrap() is ok, because RequestUrl is tested for empty host |
532 | // urls in Request::parse_url(). |
533 | self.url.host_str().unwrap() |
534 | } |
535 | |
536 | /// Port of the request url, if available. Ports are only available if they |
537 | /// are present in the original url. Specifically the scheme default ports, |
538 | /// 443 for `https` and and 80 for `http` are `None` unless explicitly |
539 | /// set in the url, i.e. `https://my-host.com:443/some/path`. |
540 | pub fn port(&self) -> Option<u16> { |
541 | self.url.port() |
542 | } |
543 | |
544 | /// Path of the request url. |
545 | pub fn path(&self) -> &str { |
546 | self.url.path() |
547 | } |
548 | |
549 | /// Returns all query parameters as a vector of key-value pairs. |
550 | /// |
551 | /// ``` |
552 | /// # fn main() -> Result<(), ureq::Error> { |
553 | /// # ureq::is_test(true); |
554 | /// let req = ureq::get("http://httpbin.org/get" ) |
555 | /// .query("foo" , "42" ) |
556 | /// .query("foo" , "43" ); |
557 | /// |
558 | /// assert_eq!(req.request_url()?.query_pairs(), vec![ |
559 | /// ("foo" , "42" ), |
560 | /// ("foo" , "43" ) |
561 | /// ]); |
562 | /// # Ok(()) |
563 | /// # } |
564 | /// ``` |
565 | pub fn query_pairs(&self) -> Vec<(&str, &str)> { |
566 | self.query_pairs |
567 | .iter() |
568 | .map(|(k, v)| (k.as_str(), v.as_str())) |
569 | .collect() |
570 | } |
571 | } |
572 | |
573 | #[cfg (test)] |
574 | mod tests { |
575 | use super::*; |
576 | |
577 | #[test ] |
578 | fn request_implements_send_and_sync() { |
579 | let _request: Box<dyn Send> = Box::new(Request::new( |
580 | Agent::new(), |
581 | "GET" .to_string(), |
582 | "https://example.com/" .to_string(), |
583 | )); |
584 | let _request: Box<dyn Sync> = Box::new(Request::new( |
585 | Agent::new(), |
586 | "GET" .to_string(), |
587 | "https://example.com/" .to_string(), |
588 | )); |
589 | } |
590 | |
591 | #[test ] |
592 | fn send_byte_slice() { |
593 | let bytes = vec![1, 2, 3]; |
594 | crate::agent() |
595 | .post("http://example.com" ) |
596 | .send(&bytes[1..2]) |
597 | .ok(); |
598 | } |
599 | |
600 | #[test ] |
601 | fn disallow_empty_host() { |
602 | let req = crate::agent().get("file:///some/path" ); |
603 | |
604 | // Both request_url and call() must surface the same error. |
605 | assert_eq!( |
606 | req.request_url().unwrap_err().kind(), |
607 | crate::ErrorKind::InvalidUrl |
608 | ); |
609 | |
610 | assert_eq!(req.call().unwrap_err().kind(), crate::ErrorKind::InvalidUrl); |
611 | } |
612 | } |
613 | |