actix_web_lab/
redirect.rs

1//! See [`Redirect`] for service/responder documentation.
2
3#![allow(deprecated)]
4
5use std::{borrow::Cow, future::ready};
6
7use actix_web::{
8    dev::{fn_service, AppService, HttpServiceFactory, ResourceDef, ServiceRequest},
9    http::{header::LOCATION, StatusCode},
10    HttpRequest, HttpResponse, Responder,
11};
12use tracing::debug;
13
14/// An HTTP service for redirecting one path to another path or URL.
15///
16/// _This feature has [graduated to Actix Web][graduated]. Further development will occur there._
17///
18/// Redirects are either [relative](Redirect::to) or [absolute](Redirect::to).
19///
20/// By default, the "307 Temporary Redirect" status is used when responding. See [this MDN
21/// article](mdn-redirects) on why 307 is preferred over 302.
22///
23/// # Examples
24/// ```
25/// use actix_web::{web, App};
26/// use actix_web_lab::web as web_lab;
27///
28/// App::new()
29///     // redirect "/duck" to DuckDuckGo
30///     .service(web_lab::Redirect::new("/duck", "https://duckduckgo.com/"))
31///     .service(
32///         // redirect "/api/old" to "/api/new" using `web::redirect` helper
33///         web::scope("/api").service(web_lab::redirect("/old", "/new")),
34///     );
35/// ```
36///
37/// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#permanent_redirections
38/// [graduated]: https://docs.rs/actix-web/4/actix_web/web/struct.Redirect.html
39#[deprecated(since = "0.19.0", note = "Type has graduated to Actix Web.")]
40#[derive(Debug, Clone)]
41pub struct Redirect {
42    from: Cow<'static, str>,
43    to: Cow<'static, str>,
44    status_code: StatusCode,
45}
46
47impl Redirect {
48    /// Create a new `Redirect` service, first providing the path that should be redirected.
49    ///
50    /// The default "to" location is the root path (`/`). It is expected that you should call either
51    /// [`to`](Redirect::to) or [`to`](Redirect::to) afterwards.
52    ///
53    /// Note this function has no effect when used as a responder.
54    ///
55    /// Redirect to an address or path.
56    ///
57    /// Whatever argument is provided shall be used as-is when setting the redirect location.
58    /// You can also use relative paths to navigate relative to the matched path.
59    ///
60    /// # Examples
61    /// ```
62    /// # use actix_web_lab::web::Redirect;
63    /// // redirects "/oh/hi/mark" to "/oh/bye/mark"
64    /// Redirect::new("/oh/hi/mark", "../../bye/mark");
65    /// ```
66    pub fn new(from: impl Into<Cow<'static, str>>, to: impl Into<Cow<'static, str>>) -> Self {
67        Self {
68            from: from.into(),
69            to: to.into(),
70            status_code: StatusCode::TEMPORARY_REDIRECT,
71        }
72    }
73
74    /// Shortcut for creating a redirect to use as a `Responder`.
75    ///
76    /// Only receives a `to` argument since responders do not need to do route matching.
77    pub fn to(to: impl Into<Cow<'static, str>>) -> Self {
78        Self {
79            from: "/".into(),
80            to: to.into(),
81            status_code: StatusCode::TEMPORARY_REDIRECT,
82        }
83    }
84
85    /// Use the "308 Permanent Redirect" status when responding.
86    ///
87    /// See [this MDN article](mdn-redirects) on why 308 is preferred over 301.
88    ///
89    /// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#permanent_redirections
90    pub fn permanent(self) -> Self {
91        self.using_status_code(StatusCode::PERMANENT_REDIRECT)
92    }
93
94    /// Use the "307 Temporary Redirect" status when responding.
95    ///
96    /// See [this MDN article](mdn-redirects) on why 307 is preferred over 302.
97    ///
98    /// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections
99    pub fn temporary(self) -> Self {
100        self.using_status_code(StatusCode::TEMPORARY_REDIRECT)
101    }
102
103    /// Allows the use of custom status codes for less common redirect types.
104    ///
105    /// In most cases, the default status ("308 Permanent Redirect") or using the `temporary`
106    /// method, which uses the "307 Temporary Redirect" status have more consistent behavior than
107    /// 301 and 302 codes, respectively.
108    ///
109    /// ```
110    /// # use actix_web::http::StatusCode;
111    /// # use actix_web_lab::web::Redirect;
112    /// // redirects would use "301 Moved Permanently" status code
113    /// Redirect::new("/old", "/new").using_status_code(StatusCode::MOVED_PERMANENTLY);
114    ///
115    /// // redirects would use "302 Found" status code
116    /// Redirect::new("/old", "/new").using_status_code(StatusCode::FOUND);
117    /// ```
118    pub fn using_status_code(mut self, status: StatusCode) -> Self {
119        self.status_code = status;
120        self
121    }
122}
123
124impl HttpServiceFactory for Redirect {
125    fn register(self, config: &mut AppService) {
126        let redirect = self.clone();
127        let rdef = ResourceDef::new(self.from.into_owned());
128        let redirect_factory = fn_service(move |mut req: ServiceRequest| {
129            let res = redirect.clone().respond_to(req.parts_mut().0);
130            ready(Ok(req.into_response(res.map_into_boxed_body())))
131        });
132
133        config.register_service(rdef, None, redirect_factory, None)
134    }
135}
136
137impl Responder for Redirect {
138    type Body = ();
139
140    fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
141        let mut res = HttpResponse::with_body(self.status_code, ());
142
143        if let Ok(hdr_val) = self.to.parse() {
144            res.headers_mut().insert(LOCATION, hdr_val);
145        } else {
146            debug!(
147                "redirect target location can not be converted to header value: {:?}",
148                self.to
149            );
150        }
151
152        res
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use actix_web::{dev::Service, http::StatusCode, test, App};
159
160    use super::*;
161
162    #[actix_web::test]
163    async fn absolute_redirects() {
164        let redirector = Redirect::new("/one", "/two").permanent();
165
166        let svc = test::init_service(App::new().service(redirector)).await;
167
168        let req = test::TestRequest::default().uri("/one").to_request();
169        let res = svc.call(req).await.unwrap();
170        assert_eq!(res.status(), StatusCode::from_u16(308).unwrap());
171        let hdr = res.headers().get(&LOCATION).unwrap();
172        assert_eq!(hdr.to_str().unwrap(), "/two");
173    }
174
175    #[actix_web::test]
176    async fn relative_redirects() {
177        let redirector = Redirect::new("/one", "two").permanent();
178
179        let svc = test::init_service(App::new().service(redirector)).await;
180
181        let req = test::TestRequest::default().uri("/one").to_request();
182        let res = svc.call(req).await.unwrap();
183        assert_eq!(res.status(), StatusCode::from_u16(308).unwrap());
184        let hdr = res.headers().get(&LOCATION).unwrap();
185        assert_eq!(hdr.to_str().unwrap(), "two");
186    }
187
188    #[actix_web::test]
189    async fn temporary_redirects() {
190        let external_service = Redirect::new("/external", "https://duck.com");
191
192        let svc = test::init_service(App::new().service(external_service)).await;
193
194        let req = test::TestRequest::default().uri("/external").to_request();
195        let res = svc.call(req).await.unwrap();
196        assert_eq!(res.status(), StatusCode::from_u16(307).unwrap());
197        let hdr = res.headers().get(&LOCATION).unwrap();
198        assert_eq!(hdr.to_str().unwrap(), "https://duck.com");
199    }
200
201    #[actix_web::test]
202    async fn as_responder() {
203        let responder = Redirect::to("https://duck.com");
204
205        let req = test::TestRequest::default().to_http_request();
206        let res = responder.respond_to(&req);
207
208        assert_eq!(res.status(), StatusCode::from_u16(307).unwrap());
209        let hdr = res.headers().get(&LOCATION).unwrap();
210        assert_eq!(hdr.to_str().unwrap(), "https://duck.com");
211    }
212}