minreq/
request.rs

1use crate::connection::Connection;
2use crate::http_url::{HttpUrl, Port};
3#[cfg(feature = "proxy")]
4use crate::proxy::Proxy;
5use crate::{Error, Response, ResponseLazy};
6use std::collections::HashMap;
7use std::fmt;
8use std::fmt::Write;
9
10/// A URL type for requests.
11pub type URL = String;
12
13/// An HTTP request method.
14#[derive(Clone, PartialEq, Eq, Debug)]
15pub enum Method {
16    /// The GET method
17    Get,
18    /// The HEAD method
19    Head,
20    /// The POST method
21    Post,
22    /// The PUT method
23    Put,
24    /// The DELETE method
25    Delete,
26    /// The CONNECT method
27    Connect,
28    /// The OPTIONS method
29    Options,
30    /// The TRACE method
31    Trace,
32    /// The PATCH method
33    Patch,
34    /// A custom method, use with care: the string will be embedded in
35    /// your request as-is.
36    Custom(String),
37}
38
39impl fmt::Display for Method {
40    /// Formats the Method to the form in the HTTP request,
41    /// ie. Method::Get -> "GET", Method::Post -> "POST", etc.
42    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
43        match *self {
44            Method::Get => write!(f, "GET"),
45            Method::Head => write!(f, "HEAD"),
46            Method::Post => write!(f, "POST"),
47            Method::Put => write!(f, "PUT"),
48            Method::Delete => write!(f, "DELETE"),
49            Method::Connect => write!(f, "CONNECT"),
50            Method::Options => write!(f, "OPTIONS"),
51            Method::Trace => write!(f, "TRACE"),
52            Method::Patch => write!(f, "PATCH"),
53            Method::Custom(ref s) => write!(f, "{}", s),
54        }
55    }
56}
57
58/// An HTTP request.
59///
60/// Generally created by the [`minreq::get`](fn.get.html)-style
61/// functions, corresponding to the HTTP method we want to use.
62///
63/// # Example
64///
65/// ```
66/// let request = minreq::post("http://example.com");
67/// ```
68///
69/// After creating the request, you would generally call
70/// [`send`](struct.Request.html#method.send) or
71/// [`send_lazy`](struct.Request.html#method.send_lazy) on it, as it
72/// doesn't do much on its own.
73#[derive(Clone, PartialEq, Eq, Debug)]
74pub struct Request {
75    pub(crate) method: Method,
76    url: URL,
77    params: String,
78    headers: HashMap<String, String>,
79    body: Option<Vec<u8>>,
80    pub(crate) timeout: Option<u64>,
81    pub(crate) max_headers_size: Option<usize>,
82    pub(crate) max_status_line_len: Option<usize>,
83    max_redirects: usize,
84    pub(crate) follow_redirects: bool,
85    #[cfg(feature = "proxy")]
86    pub(crate) proxy: Option<Proxy>,
87}
88
89impl Request {
90    /// Creates a new HTTP `Request`.
91    ///
92    /// This is only the request's data, it is not sent yet. For
93    /// sending the request, see [`send`](struct.Request.html#method.send).
94    ///
95    /// If `urlencoding` is not enabled, it is the responsibility of the
96    /// user to ensure there are no illegal characters in the URL.
97    ///
98    /// If `urlencoding` is enabled, the resource part of the URL will be
99    /// encoded. Any URL special characters (e.g. &, #, =) are not encoded
100    /// as they are assumed to be meaningful parameters etc.
101    pub fn new<T: Into<URL>>(method: Method, url: T) -> Request {
102        Request {
103            method,
104            url: url.into(),
105            params: String::new(),
106            headers: HashMap::new(),
107            body: None,
108            timeout: None,
109            max_headers_size: None,
110            max_status_line_len: None,
111            max_redirects: 100,
112            follow_redirects: true,
113            #[cfg(feature = "proxy")]
114            proxy: None,
115        }
116    }
117
118    /// Add headers to the request this is called on. Use this
119    /// function to add headers to your requests.
120    pub fn with_headers<T, K, V>(mut self, headers: T) -> Request
121    where
122        T: IntoIterator<Item = (K, V)>,
123        K: Into<String>,
124        V: Into<String>,
125    {
126        let headers = headers.into_iter().map(|(k, v)| (k.into(), v.into()));
127        self.headers.extend(headers);
128        self
129    }
130
131    /// Adds a header to the request this is called on. Use this
132    /// function to add headers to your requests.
133    pub fn with_header<T: Into<String>, U: Into<String>>(mut self, key: T, value: U) -> Request {
134        self.headers.insert(key.into(), value.into());
135        self
136    }
137
138    /// Sets the request body.
139    pub fn with_body<T: Into<Vec<u8>>>(mut self, body: T) -> Request {
140        let body = body.into();
141        let body_length = body.len();
142        self.body = Some(body);
143        self.with_header("Content-Length", format!("{}", body_length))
144    }
145
146    /// Adds given key and value as query parameter to request url
147    /// (resource).
148    ///
149    /// If `urlencoding` is not enabled, it is the responsibility
150    /// of the user to ensure there are no illegal characters in the
151    /// key or value.
152    ///
153    /// If `urlencoding` is enabled, the key and value are both encoded.
154    pub fn with_param<T: Into<String>, U: Into<String>>(mut self, key: T, value: U) -> Request {
155        let key = key.into();
156        #[cfg(feature = "urlencoding")]
157        let key = urlencoding::encode(&key);
158        let value = value.into();
159        #[cfg(feature = "urlencoding")]
160        let value = urlencoding::encode(&value);
161
162        if !self.params.is_empty() {
163            self.params.push('&');
164        }
165        self.params.push_str(&key);
166        self.params.push('=');
167        self.params.push_str(&value);
168        self
169    }
170
171    /// Converts given argument to JSON and sets it as body.
172    ///
173    /// # Errors
174    ///
175    /// Returns
176    /// [`SerdeJsonError`](enum.Error.html#variant.SerdeJsonError) if
177    /// Serde runs into a problem when converting `body` into a
178    /// string.
179    #[cfg(feature = "json-using-serde")]
180    pub fn with_json<T: serde::ser::Serialize>(mut self, body: &T) -> Result<Request, Error> {
181        self.headers.insert(
182            "Content-Type".to_string(),
183            "application/json; charset=UTF-8".to_string(),
184        );
185        match serde_json::to_string(&body) {
186            Ok(json) => Ok(self.with_body(json)),
187            Err(err) => Err(Error::SerdeJsonError(err)),
188        }
189    }
190
191    /// Sets the request timeout in seconds.
192    pub fn with_timeout(mut self, timeout: u64) -> Request {
193        self.timeout = Some(timeout);
194        self
195    }
196
197    /// Sets the max redirects we follow until giving up. 100 by
198    /// default.
199    ///
200    /// Warning: setting this to a very high number, such as 1000, may
201    /// cause a stack overflow if that many redirects are followed. If
202    /// you have a use for so many redirects that the stack overflow
203    /// becomes a problem, please open an issue.
204    pub fn with_max_redirects(mut self, max_redirects: usize) -> Request {
205        self.max_redirects = max_redirects;
206        self
207    }
208
209    /// Enables or disables redirect handling. Defaults to `true`, i.e. enabled.
210    ///
211    /// If `follow` is `true` and the server returns a 301, 302, 303, or 307
212    /// status code, minreq will follow the redirect by making another HTTP
213    /// request to the new location, up to the amount of times specified by
214    /// [`Request::with_max_redirects`], returning an error if that amount is
215    /// reached before getting a non-redirection as a response.
216    ///
217    /// Disabling redirection handling with this function by passing in `false`
218    /// can be used to handle the redirects yourself.
219    pub fn with_follow_redirects(mut self, follow_redirects: bool) -> Request {
220        self.follow_redirects = follow_redirects;
221        self
222    }
223
224    /// Sets the maximum size of all the headers this request will
225    /// accept.
226    ///
227    /// If this limit is passed, the request will close the connection
228    /// and return an [Error::HeadersOverflow] error.
229    ///
230    /// The maximum length is counted in bytes, including line-endings
231    /// and other whitespace. Both normal and trailing headers count
232    /// towards this cap.
233    ///
234    /// `None` disables the cap, and may cause the program to use any
235    /// amount of memory if the server responds with a lot of headers
236    /// (or an infinite amount). In minreq versions 2.x.x, the default
237    /// is None, so setting this manually is recommended when talking
238    /// to untrusted servers.
239    pub fn with_max_headers_size<S: Into<Option<usize>>>(mut self, max_headers_size: S) -> Request {
240        self.max_headers_size = max_headers_size.into();
241        self
242    }
243
244    /// Sets the maximum length of the status line this request will
245    /// accept.
246    ///
247    /// If this limit is passed, the request will close the connection
248    /// and return an [Error::StatusLineOverflow] error.
249    ///
250    /// The maximum length is counted in bytes, including the
251    /// line-ending `\r\n`.
252    ///
253    /// `None` disables the cap, and may cause the program to use any
254    /// amount of memory if the server responds with a long (or
255    /// infinite) status line. In minreq versions 2.x.x, the default
256    /// is None, so setting this manually is recommended when talking
257    /// to untrusted servers.
258    pub fn with_max_status_line_length<S: Into<Option<usize>>>(
259        mut self,
260        max_status_line_len: S,
261    ) -> Request {
262        self.max_status_line_len = max_status_line_len.into();
263        self
264    }
265
266    /// Sets the proxy to use.
267    #[cfg(feature = "proxy")]
268    pub fn with_proxy(mut self, proxy: Proxy) -> Request {
269        self.proxy = Some(proxy);
270        self
271    }
272
273    /// Sends this request to the host.
274    ///
275    /// # Errors
276    ///
277    /// Returns `Err` if we run into an error while sending the
278    /// request, or receiving/parsing the response. The specific error
279    /// is described in the `Err`, and it can be any
280    /// [`minreq::Error`](enum.Error.html) except
281    /// [`SerdeJsonError`](enum.Error.html#variant.SerdeJsonError) and
282    /// [`InvalidUtf8InBody`](enum.Error.html#variant.InvalidUtf8InBody).
283    pub fn send(self) -> Result<Response, Error> {
284        let parsed_request = ParsedRequest::new(self)?;
285        if parsed_request.url.https {
286            #[cfg(any(feature = "rustls", feature = "openssl", feature = "native-tls"))]
287            {
288                let is_head = parsed_request.config.method == Method::Head;
289                let response = Connection::new(parsed_request).send_https()?;
290                Response::create(response, is_head)
291            }
292            #[cfg(not(any(feature = "rustls", feature = "openssl", feature = "native-tls")))]
293            {
294                Err(Error::HttpsFeatureNotEnabled)
295            }
296        } else {
297            let is_head = parsed_request.config.method == Method::Head;
298            let response = Connection::new(parsed_request).send()?;
299            Response::create(response, is_head)
300        }
301    }
302
303    /// Sends this request to the host, loaded lazily.
304    ///
305    /// # Errors
306    ///
307    /// See [`send`](struct.Request.html#method.send).
308    pub fn send_lazy(self) -> Result<ResponseLazy, Error> {
309        let parsed_request = ParsedRequest::new(self)?;
310        if parsed_request.url.https {
311            #[cfg(any(feature = "rustls", feature = "openssl", feature = "native-tls"))]
312            {
313                Connection::new(parsed_request).send_https()
314            }
315            #[cfg(not(any(feature = "rustls", feature = "openssl", feature = "native-tls")))]
316            {
317                Err(Error::HttpsFeatureNotEnabled)
318            }
319        } else {
320            Connection::new(parsed_request).send()
321        }
322    }
323}
324
325pub(crate) struct ParsedRequest {
326    pub(crate) url: HttpUrl,
327    pub(crate) redirects: Vec<HttpUrl>,
328    pub(crate) config: Request,
329}
330
331impl ParsedRequest {
332    #[allow(unused_mut)]
333    fn new(mut config: Request) -> Result<ParsedRequest, Error> {
334        let mut url = HttpUrl::parse(&config.url, None)?;
335
336        if !config.params.is_empty() {
337            if url.path_and_query.contains('?') {
338                url.path_and_query.push('&');
339            } else {
340                url.path_and_query.push('?');
341            }
342            url.path_and_query.push_str(&config.params);
343        }
344
345        #[cfg(feature = "proxy")]
346        // Set default proxy from environment variables
347        //
348        // Curl documentation: https://everything.curl.dev/usingcurl/proxies/env
349        //
350        // Accepted variables are `http_proxy`, `https_proxy`, `HTTPS_PROXY`, `ALL_PROXY`
351        //
352        // Note: https://everything.curl.dev/usingcurl/proxies/env#http_proxy-in-lower-case-only
353        if config.proxy.is_none() {
354            // Set HTTP proxies if request's protocol is HTTPS and they're given
355            if url.https {
356                if let Ok(proxy) =
357                    std::env::var("https_proxy").map_err(|_| std::env::var("HTTPS_PROXY"))
358                {
359                    if let Ok(proxy) = Proxy::new(proxy) {
360                        config.proxy = Some(proxy);
361                    }
362                }
363            }
364            // Set HTTP proxies if request's protocol is HTTP and they're given
365            else if let Ok(proxy) = std::env::var("http_proxy") {
366                if let Ok(proxy) = Proxy::new(proxy) {
367                    config.proxy = Some(proxy);
368                }
369            }
370            // Set any given proxies if neither of HTTP/HTTPS were given
371            else if let Ok(proxy) =
372                std::env::var("all_proxy").map_err(|_| std::env::var("ALL_PROXY"))
373            {
374                if let Ok(proxy) = Proxy::new(proxy) {
375                    config.proxy = Some(proxy);
376                }
377            }
378        }
379
380        Ok(ParsedRequest {
381            url,
382            redirects: Vec::new(),
383            config,
384        })
385    }
386
387    fn get_http_head(&self) -> String {
388        let mut http = String::with_capacity(32);
389
390        // NOTE: As of 2.10.0, the fragment is intentionally left out of the request, based on:
391        // - [RFC 3986 section 3.5](https://datatracker.ietf.org/doc/html/rfc3986#section-3.5):
392        //   "...the fragment identifier is not used in the scheme-specific
393        //   processing of a URI; instead, the fragment identifier is separated
394        //   from the rest of the URI prior to a dereference..."
395        // - [RFC 7231 section 9.5](https://datatracker.ietf.org/doc/html/rfc7231#section-9.5):
396        //   "Although fragment identifiers used within URI references are not
397        //   sent in requests..."
398
399        // Add the request line and the "Host" header
400        write!(
401            http,
402            "{} {} HTTP/1.1\r\nHost: {}",
403            self.config.method, self.url.path_and_query, self.url.host
404        )
405        .unwrap();
406        if let Port::Explicit(port) = self.url.port {
407            write!(http, ":{}", port).unwrap();
408        }
409        http += "\r\n";
410
411        // Add other headers
412        for (k, v) in &self.config.headers {
413            write!(http, "{}: {}\r\n", k, v).unwrap();
414        }
415
416        if self.config.method == Method::Post
417            || self.config.method == Method::Put
418            || self.config.method == Method::Patch
419        {
420            let not_length = |key: &String| {
421                let key = key.to_lowercase();
422                key != "content-length" && key != "transfer-encoding"
423            };
424            if self.config.headers.keys().all(not_length) {
425                // A user agent SHOULD send a Content-Length in a request message when no Transfer-Encoding
426                // is sent and the request method defines a meaning for an enclosed payload body.
427                // refer: https://tools.ietf.org/html/rfc7230#section-3.3.2
428
429                // A client MUST NOT send a message body in a TRACE request.
430                // refer: https://tools.ietf.org/html/rfc7231#section-4.3.8
431                // similar line found for GET, HEAD, CONNECT and DELETE.
432
433                http += "Content-Length: 0\r\n";
434            }
435        }
436
437        http += "\r\n";
438        http
439    }
440
441    /// Returns the HTTP request as bytes, ready to be sent to
442    /// the server.
443    pub(crate) fn as_bytes(&self) -> Vec<u8> {
444        let mut head = self.get_http_head().into_bytes();
445        if let Some(body) = &self.config.body {
446            head.extend(body);
447        }
448        head
449    }
450
451    /// Returns the redirected version of this Request, unless an
452    /// infinite redirection loop was detected, or the redirection
453    /// limit was reached.
454    pub(crate) fn redirect_to(&mut self, url: &str) -> Result<(), Error> {
455        if url.contains("://") {
456            let mut url = HttpUrl::parse(url, Some(&self.url)).map_err(|_| {
457                // TODO: Uncomment this for 3.0
458                // Error::InvalidProtocolInRedirect
459                Error::IoError(std::io::Error::new(
460                    std::io::ErrorKind::Other,
461                    "was redirected to an absolute url with an invalid protocol",
462                ))
463            })?;
464            std::mem::swap(&mut url, &mut self.url);
465            self.redirects.push(url);
466        } else {
467            // The url does not have the protocol part, assuming it's
468            // a relative resource.
469            let mut absolute_url = String::new();
470            self.url.write_base_url_to(&mut absolute_url).unwrap();
471            absolute_url.push_str(url);
472            let mut url = HttpUrl::parse(&absolute_url, Some(&self.url))?;
473            std::mem::swap(&mut url, &mut self.url);
474            self.redirects.push(url);
475        }
476
477        if self.redirects.len() > self.config.max_redirects {
478            Err(Error::TooManyRedirections)
479        } else if self
480            .redirects
481            .iter()
482            .any(|redirect_url| redirect_url == &self.url)
483        {
484            Err(Error::InfiniteRedirectionLoop)
485        } else {
486            Ok(())
487        }
488    }
489}
490
491/// Alias for [Request::new](struct.Request.html#method.new) with `method` set to
492/// [Method::Get](enum.Method.html).
493pub fn get<T: Into<URL>>(url: T) -> Request {
494    Request::new(Method::Get, url)
495}
496
497/// Alias for [Request::new](struct.Request.html#method.new) with `method` set to
498/// [Method::Head](enum.Method.html).
499pub fn head<T: Into<URL>>(url: T) -> Request {
500    Request::new(Method::Head, url)
501}
502
503/// Alias for [Request::new](struct.Request.html#method.new) with `method` set to
504/// [Method::Post](enum.Method.html).
505pub fn post<T: Into<URL>>(url: T) -> Request {
506    Request::new(Method::Post, url)
507}
508
509/// Alias for [Request::new](struct.Request.html#method.new) with `method` set to
510/// [Method::Put](enum.Method.html).
511pub fn put<T: Into<URL>>(url: T) -> Request {
512    Request::new(Method::Put, url)
513}
514
515/// Alias for [Request::new](struct.Request.html#method.new) with `method` set to
516/// [Method::Delete](enum.Method.html).
517pub fn delete<T: Into<URL>>(url: T) -> Request {
518    Request::new(Method::Delete, url)
519}
520
521/// Alias for [Request::new](struct.Request.html#method.new) with `method` set to
522/// [Method::Connect](enum.Method.html).
523pub fn connect<T: Into<URL>>(url: T) -> Request {
524    Request::new(Method::Connect, url)
525}
526
527/// Alias for [Request::new](struct.Request.html#method.new) with `method` set to
528/// [Method::Options](enum.Method.html).
529pub fn options<T: Into<URL>>(url: T) -> Request {
530    Request::new(Method::Options, url)
531}
532
533/// Alias for [Request::new](struct.Request.html#method.new) with `method` set to
534/// [Method::Trace](enum.Method.html).
535pub fn trace<T: Into<URL>>(url: T) -> Request {
536    Request::new(Method::Trace, url)
537}
538
539/// Alias for [Request::new](struct.Request.html#method.new) with `method` set to
540/// [Method::Patch](enum.Method.html).
541pub fn patch<T: Into<URL>>(url: T) -> Request {
542    Request::new(Method::Patch, url)
543}
544
545#[cfg(test)]
546mod parsing_tests {
547
548    use std::collections::HashMap;
549
550    use super::{get, ParsedRequest};
551
552    #[test]
553    fn test_headers() {
554        let mut headers = HashMap::new();
555        headers.insert("foo".to_string(), "bar".to_string());
556        headers.insert("foo".to_string(), "baz".to_string());
557
558        let req = get("http://www.example.org/test/res").with_headers(headers.clone());
559
560        assert_eq!(req.headers, headers);
561    }
562
563    #[test]
564    fn test_multiple_params() {
565        let req = get("http://www.example.org/test/res")
566            .with_param("foo", "bar")
567            .with_param("asd", "qwe");
568        let req = ParsedRequest::new(req).unwrap();
569        assert_eq!(&req.url.path_and_query, "/test/res?foo=bar&asd=qwe");
570    }
571
572    #[test]
573    fn test_domain() {
574        let req = get("http://www.example.org/test/res").with_param("foo", "bar");
575        let req = ParsedRequest::new(req).unwrap();
576        assert_eq!(&req.url.host, "www.example.org");
577    }
578
579    #[test]
580    fn test_protocol() {
581        let req =
582            ParsedRequest::new(get("http://www.example.org/").with_param("foo", "bar")).unwrap();
583        assert!(!req.url.https);
584        let req =
585            ParsedRequest::new(get("https://www.example.org/").with_param("foo", "bar")).unwrap();
586        assert!(req.url.https);
587    }
588}
589
590#[cfg(all(test, feature = "urlencoding"))]
591mod encoding_tests {
592    use super::{get, ParsedRequest};
593
594    #[test]
595    fn test_with_param() {
596        let req = get("http://www.example.org").with_param("foo", "bar");
597        let req = ParsedRequest::new(req).unwrap();
598        assert_eq!(&req.url.path_and_query, "/?foo=bar");
599
600        let req = get("http://www.example.org").with_param("ówò", "what's this? 👀");
601        let req = ParsedRequest::new(req).unwrap();
602        assert_eq!(
603            &req.url.path_and_query,
604            "/?%C3%B3w%C3%B2=what%27s%20this%3F%20%F0%9F%91%80"
605        );
606    }
607
608    #[test]
609    fn test_on_creation() {
610        let req = ParsedRequest::new(get("http://www.example.org/?foo=bar#baz")).unwrap();
611        assert_eq!(&req.url.path_and_query, "/?foo=bar");
612
613        let req = ParsedRequest::new(get("http://www.example.org/?ówò=what's this? 👀")).unwrap();
614        assert_eq!(
615            &req.url.path_and_query,
616            "/?%C3%B3w%C3%B2=what%27s%20this?%20%F0%9F%91%80"
617        );
618    }
619}