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}