actix_web/
redirect.rs

1//! See [`Redirect`] for service/responder documentation.
2
3use std::borrow::Cow;
4
5use actix_utils::future::ready;
6
7use crate::{
8    dev::{fn_service, AppService, HttpServiceFactory, ResourceDef, ServiceRequest},
9    http::{header::LOCATION, StatusCode},
10    HttpRequest, HttpResponse, Responder,
11};
12
13/// An HTTP service for redirecting one path to another path or URL.
14///
15/// By default, the "307 Temporary Redirect" status is used when responding. See [this MDN
16/// article][mdn-redirects] on why 307 is preferred over 302.
17///
18/// # Examples
19/// As service:
20/// ```
21/// use actix_web::{web, App};
22///
23/// App::new()
24///     // redirect "/duck" to DuckDuckGo
25///     .service(web::redirect("/duck", "https://duck.com"))
26///     .service(
27///         // redirect "/api/old" to "/api/new"
28///         web::scope("/api").service(web::redirect("/old", "/new"))
29///     );
30/// ```
31///
32/// As responder:
33/// ```
34/// use actix_web::{web::Redirect, Responder};
35///
36/// async fn handler() -> impl Responder {
37///     // sends a permanent (308) redirect to duck.com
38///     Redirect::to("https://duck.com").permanent()
39/// }
40/// # actix_web::web::to(handler);
41/// ```
42///
43/// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections
44#[derive(Debug, Clone)]
45pub struct Redirect {
46    from: Cow<'static, str>,
47    to: Cow<'static, str>,
48    status_code: StatusCode,
49}
50
51impl Redirect {
52    /// Construct a new `Redirect` service that matches a path.
53    ///
54    /// This service will match exact paths equal to `from` within the current scope. I.e., when
55    /// registered on the root `App`, it will match exact, whole paths. But when registered on a
56    /// `Scope`, it will match paths under that scope, ignoring the defined scope prefix, just like
57    /// a normal `Resource` or `Route`.
58    ///
59    /// The `to` argument can be path or URL; whatever is provided shall be used verbatim when
60    /// setting the redirect location. This means that relative paths can be used to navigate
61    /// relatively to matched paths.
62    ///
63    /// Prefer [`Redirect::to()`](Self::to) when using `Redirect` as a responder since `from` has
64    /// no meaning in that context.
65    ///
66    /// # Examples
67    /// ```
68    /// # use actix_web::{web::Redirect, App};
69    /// App::new()
70    ///     // redirects "/oh/hi/mark" to "/oh/bye/johnny"
71    ///     .service(Redirect::new("/oh/hi/mark", "../../bye/johnny"));
72    /// ```
73    pub fn new(from: impl Into<Cow<'static, str>>, to: impl Into<Cow<'static, str>>) -> Self {
74        Self {
75            from: from.into(),
76            to: to.into(),
77            status_code: StatusCode::TEMPORARY_REDIRECT,
78        }
79    }
80
81    /// Construct a new `Redirect` to use as a responder.
82    ///
83    /// Only receives the `to` argument since responders do not need to do route matching.
84    ///
85    /// # Examples
86    /// ```
87    /// use actix_web::{web::Redirect, Responder};
88    ///
89    /// async fn admin_page() -> impl Responder {
90    ///     // sends a temporary 307 redirect to the login path
91    ///     Redirect::to("/login")
92    /// }
93    /// # actix_web::web::to(admin_page);
94    /// ```
95    pub fn to(to: impl Into<Cow<'static, str>>) -> Self {
96        Self {
97            from: "/".into(),
98            to: to.into(),
99            status_code: StatusCode::TEMPORARY_REDIRECT,
100        }
101    }
102
103    /// Use the "308 Permanent Redirect" status when responding.
104    ///
105    /// See [this MDN article][mdn-redirects] on why 308 is preferred over 301.
106    ///
107    /// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#permanent_redirections
108    pub fn permanent(self) -> Self {
109        self.using_status_code(StatusCode::PERMANENT_REDIRECT)
110    }
111
112    /// Use the "307 Temporary Redirect" status when responding.
113    ///
114    /// See [this MDN article][mdn-redirects] on why 307 is preferred over 302.
115    ///
116    /// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections
117    pub fn temporary(self) -> Self {
118        self.using_status_code(StatusCode::TEMPORARY_REDIRECT)
119    }
120
121    /// Use the "303 See Other" status when responding.
122    ///
123    /// This status code is semantically correct as the response to a successful login, for example.
124    pub fn see_other(self) -> Self {
125        self.using_status_code(StatusCode::SEE_OTHER)
126    }
127
128    /// Allows the use of custom status codes for less common redirect types.
129    ///
130    /// In most cases, the default status ("308 Permanent Redirect") or using the `temporary`
131    /// method, which uses the "307 Temporary Redirect" status have more consistent behavior than
132    /// 301 and 302 codes, respectively.
133    ///
134    /// ```
135    /// # use actix_web::{http::StatusCode, web::Redirect};
136    /// // redirects would use "301 Moved Permanently" status code
137    /// Redirect::new("/old", "/new")
138    ///     .using_status_code(StatusCode::MOVED_PERMANENTLY);
139    ///
140    /// // redirects would use "302 Found" status code
141    /// Redirect::new("/old", "/new")
142    ///     .using_status_code(StatusCode::FOUND);
143    /// ```
144    pub fn using_status_code(mut self, status: StatusCode) -> Self {
145        self.status_code = status;
146        self
147    }
148}
149
150impl HttpServiceFactory for Redirect {
151    fn register(self, config: &mut AppService) {
152        let redirect = self.clone();
153        let rdef = ResourceDef::new(self.from.into_owned());
154        let redirect_factory = fn_service(move |mut req: ServiceRequest| {
155            let res = redirect.clone().respond_to(req.parts_mut().0);
156            ready(Ok(req.into_response(res.map_into_boxed_body())))
157        });
158
159        config.register_service(rdef, None, redirect_factory, None)
160    }
161}
162
163impl Responder for Redirect {
164    type Body = ();
165
166    fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
167        let mut res = HttpResponse::with_body(self.status_code, ());
168
169        if let Ok(hdr_val) = self.to.parse() {
170            res.headers_mut().insert(LOCATION, hdr_val);
171        } else {
172            log::error!(
173                "redirect target location can not be converted to header value: {:?}",
174                self.to,
175            );
176        }
177
178        res
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::{dev::Service, test, App};
186
187    #[actix_rt::test]
188    async fn absolute_redirects() {
189        let redirector = Redirect::new("/one", "/two").permanent();
190
191        let svc = test::init_service(App::new().service(redirector)).await;
192
193        let req = test::TestRequest::default().uri("/one").to_request();
194        let res = svc.call(req).await.unwrap();
195        assert_eq!(res.status(), StatusCode::from_u16(308).unwrap());
196        let hdr = res.headers().get(&LOCATION).unwrap();
197        assert_eq!(hdr.to_str().unwrap(), "/two");
198    }
199
200    #[actix_rt::test]
201    async fn relative_redirects() {
202        let redirector = Redirect::new("/one", "two").permanent();
203
204        let svc = test::init_service(App::new().service(redirector)).await;
205
206        let req = test::TestRequest::default().uri("/one").to_request();
207        let res = svc.call(req).await.unwrap();
208        assert_eq!(res.status(), StatusCode::from_u16(308).unwrap());
209        let hdr = res.headers().get(&LOCATION).unwrap();
210        assert_eq!(hdr.to_str().unwrap(), "two");
211    }
212
213    #[actix_rt::test]
214    async fn temporary_redirects() {
215        let external_service = Redirect::new("/external", "https://duck.com");
216
217        let svc = test::init_service(App::new().service(external_service)).await;
218
219        let req = test::TestRequest::default().uri("/external").to_request();
220        let res = svc.call(req).await.unwrap();
221        assert_eq!(res.status(), StatusCode::from_u16(307).unwrap());
222        let hdr = res.headers().get(&LOCATION).unwrap();
223        assert_eq!(hdr.to_str().unwrap(), "https://duck.com");
224    }
225
226    #[actix_rt::test]
227    async fn as_responder() {
228        let responder = Redirect::to("https://duck.com");
229
230        let req = test::TestRequest::default().to_http_request();
231        let res = responder.respond_to(&req);
232
233        assert_eq!(res.status(), StatusCode::from_u16(307).unwrap());
234        let hdr = res.headers().get(&LOCATION).unwrap();
235        assert_eq!(hdr.to_str().unwrap(), "https://duck.com");
236    }
237}