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}