crux_http/middleware/
redirect.rs

1//! HTTP Redirect middleware.
2//!
3//! # Examples
4//!
5//! ```no_run
6//! # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
7//! # struct Capabilities { http: crux_http::Http<Event> }
8//! # fn update(caps: &Capabilities) {
9//!
10//! caps.http
11//!     .get("https://httpbin.org/redirect/2")
12//!     .middleware(crux_http::middleware::Redirect::default())
13//!     .send(Event::ReceiveResponse)
14//! # }
15//! ```
16
17use crate::middleware::{Middleware, Next, Request};
18use crate::{Client, ResponseAsync, Result};
19use http_types::{headers, StatusCode, Url};
20
21// List of acceptable 300-series redirect codes.
22const REDIRECT_CODES: &[StatusCode] = &[
23    StatusCode::MovedPermanently,
24    StatusCode::Found,
25    StatusCode::SeeOther,
26    StatusCode::TemporaryRedirect,
27    StatusCode::PermanentRedirect,
28];
29
30/// A middleware which attempts to follow HTTP redirects.
31#[derive(Debug)]
32pub struct Redirect {
33    attempts: u8,
34}
35
36impl Redirect {
37    /// Create a new instance of the Redirect middleware, which attempts to follow redirects
38    /// up to as many times as specified.
39    ///
40    /// Consider using `Redirect::default()` for the default number of redirect attempts.
41    ///
42    /// This middleware will follow redirects from the `Location` header if the server returns
43    /// any of the following http response codes:
44    /// - 301 Moved Permanently
45    /// - 302 Found
46    /// - 303 See other
47    /// - 307 Temporary Redirect
48    /// - 308 Permanent Redirect
49    ///
50    /// # Errors
51    ///
52    /// An error will be passed through the middleware stack if the value of the `Location`
53    /// header is not a validly parsing url.
54    ///
55    /// # Caveats
56    ///
57    /// This will presently make at least one additional HTTP request before the actual request to
58    /// determine if there is a redirect that should be followed, so as to preserve any request body.
59    ///
60    /// # Examples
61    ///
62    /// ```no_run
63    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
64    /// # struct Capabilities { http: crux_http::Http<Event> }
65    /// # fn update(caps: &Capabilities) {
66    ///
67    /// caps.http
68    ///     .get("https://httpbin.org/redirect/2")
69    ///     .middleware(crux_http::middleware::Redirect::default())
70    ///     .send(Event::ReceiveResponse)
71    /// # }
72    /// ```
73    #[must_use]
74    pub fn new(attempts: u8) -> Self {
75        Redirect { attempts }
76    }
77}
78
79#[async_trait::async_trait]
80impl Middleware for Redirect {
81    async fn handle(
82        &self,
83        mut request: Request,
84        client: Client,
85        next: Next<'_>,
86    ) -> Result<ResponseAsync> {
87        let mut redirect_count: u8 = 0;
88
89        // Note(Jeremiah): This is not ideal.
90        //
91        // HttpClient is currently too limiting for efficient redirects.
92        // We do not want to make unnecessary full requests, but it is
93        // presently required due to how Body streams work.
94        //
95        // Ideally we'd have methods to send a partial request stream,
96        // without sending the body, that would potnetially be able to
97        // get a server status before we attempt to send the body.
98        //
99        // As a work around we clone the request first (without the body),
100        // and try sending it until we get some status back that is not a
101        // redirect.
102
103        let mut base_url = request.url().clone();
104
105        while redirect_count < self.attempts {
106            redirect_count += 1;
107            let r: Request = request.clone();
108            let res: ResponseAsync = client.send(r).await?;
109            if REDIRECT_CODES.contains(&res.status()) {
110                if let Some(location) = res.header(headers::LOCATION) {
111                    let http_req: &mut http_types::Request = request.as_mut();
112                    *http_req.url_mut() = match Url::parse(location.last().as_str()) {
113                        Ok(valid_url) => {
114                            base_url = valid_url;
115                            base_url.clone()
116                        }
117                        Err(e) => match e {
118                            http_types::url::ParseError::RelativeUrlWithoutBase => {
119                                base_url.join(location.last().as_str())?
120                            }
121                            e => return Err(e.into()),
122                        },
123                    };
124                }
125            } else {
126                break;
127            }
128        }
129
130        Ok(next.run(request, client).await?)
131    }
132}
133
134impl Default for Redirect {
135    /// Create a new instance of the Redirect middleware, which attempts to follow up to
136    /// 3 redirects (not including the actual request).
137    fn default() -> Self {
138        Self { attempts: 3 }
139    }
140}