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 pub fn new(attempts: u8) -> Self {
74 Redirect { attempts }
75 }
76}
77
78#[async_trait::async_trait]
79impl Middleware for Redirect {
80 async fn handle(
81 &self,
82 mut req: Request,
83 client: Client,
84 next: Next<'_>,
85 ) -> Result<ResponseAsync> {
86 let mut redirect_count: u8 = 0;
87
88 // Note(Jeremiah): This is not ideal.
89 //
90 // HttpClient is currently too limiting for efficient redirects.
91 // We do not want to make unnecessary full requests, but it is
92 // presently required due to how Body streams work.
93 //
94 // Ideally we'd have methods to send a partial request stream,
95 // without sending the body, that would potnetially be able to
96 // get a server status before we attempt to send the body.
97 //
98 // As a work around we clone the request first (without the body),
99 // and try sending it until we get some status back that is not a
100 // redirect.
101
102 let mut base_url = req.url().clone();
103
104 while redirect_count < self.attempts {
105 redirect_count += 1;
106 let r: Request = req.clone();
107 let res: ResponseAsync = client.send(r).await?;
108 if REDIRECT_CODES.contains(&res.status()) {
109 if let Some(location) = res.header(headers::LOCATION) {
110 let http_req: &mut http_types::Request = req.as_mut();
111 *http_req.url_mut() = match Url::parse(location.last().as_str()) {
112 Ok(valid_url) => {
113 base_url = valid_url;
114 base_url.clone()
115 }
116 Err(e) => match e {
117 http_types::url::ParseError::RelativeUrlWithoutBase => {
118 base_url.join(location.last().as_str())?
119 }
120 e => return Err(e.into()),
121 },
122 };
123 }
124 } else {
125 break;
126 }
127 }
128
129 Ok(next.run(req, client).await?)
130 }
131}
132
133impl Default for Redirect {
134 /// Create a new instance of the Redirect middleware, which attempts to follow up to
135 /// 3 redirects (not including the actual request).
136 fn default() -> Self {
137 Self { attempts: 3 }
138 }
139}