Skip to main content

actix_web/
redirect.rs

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