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}