mirror of
https://github.com/actix/actix-web.git
synced 2024-12-01 14:01:37 +00:00
add redirect service (#1961)
This commit is contained in:
parent
e7c34f2e45
commit
3c69d078b2
5 changed files with 269 additions and 4 deletions
2
.github/workflows/clippy-fmt.yml
vendored
2
.github/workflows/clippy-fmt.yml
vendored
|
@ -10,6 +10,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: dtolnay/rust-toolchain@nightly
|
- uses: dtolnay/rust-toolchain@nightly
|
||||||
|
with: { components: rustfmt }
|
||||||
- run: cargo fmt --all -- --check
|
- run: cargo fmt --all -- --check
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
|
@ -21,7 +22,6 @@ jobs:
|
||||||
with: { components: clippy }
|
with: { components: clippy }
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
- name: Generate Cargo.lock
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
run: cargo generate-lockfile
|
run: cargo generate-lockfile
|
||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.2.0
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
|
@ -5,8 +5,10 @@
|
||||||
- Add `ContentDisposition::attachment` constructor. [#2867]
|
- Add `ContentDisposition::attachment` constructor. [#2867]
|
||||||
- Add `ErrorHandlers::default_handler()` (as well as `default_handler_{server, client}()`) to make registering handlers for groups of response statuses easier. [#2784]
|
- Add `ErrorHandlers::default_handler()` (as well as `default_handler_{server, client}()`) to make registering handlers for groups of response statuses easier. [#2784]
|
||||||
- Add `Logger::custom_response_replace()`. [#2631]
|
- Add `Logger::custom_response_replace()`. [#2631]
|
||||||
|
- Add rudimentary redirection service at `web::redirect()` / `web::Redirect`. [#1961]
|
||||||
- Add `guard::Acceptable` for matching against `Accept` header mime types. [#2265]
|
- Add `guard::Acceptable` for matching against `Accept` header mime types. [#2265]
|
||||||
|
|
||||||
|
[#1961]: https://github.com/actix/actix-web/pull/1961
|
||||||
[#2265]: https://github.com/actix/actix-web/pull/2265
|
[#2265]: https://github.com/actix/actix-web/pull/2265
|
||||||
[#2631]: https://github.com/actix/actix-web/pull/2631
|
[#2631]: https://github.com/actix/actix-web/pull/2631
|
||||||
[#2784]: https://github.com/actix/actix-web/pull/2784
|
[#2784]: https://github.com/actix/actix-web/pull/2784
|
||||||
|
|
|
@ -86,6 +86,7 @@ mod helpers;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
mod info;
|
mod info;
|
||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
|
mod redirect;
|
||||||
mod request;
|
mod request;
|
||||||
mod request_data;
|
mod request_data;
|
||||||
mod resource;
|
mod resource;
|
||||||
|
|
238
actix-web/src/redirect.rs
Normal file
238
actix-web/src/redirect.rs
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
//! See [`Redirect`] for service/responder documentation.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use actix_utils::future::ready;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
dev::{fn_service, AppService, HttpServiceFactory, ResourceDef, ServiceRequest},
|
||||||
|
http::{header::LOCATION, StatusCode},
|
||||||
|
HttpRequest, HttpResponse, Responder,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// An HTTP service for redirecting one path to another path or URL.
|
||||||
|
///
|
||||||
|
/// By default, the "307 Temporary Redirect" status is used when responding. See [this MDN
|
||||||
|
/// article][mdn-redirects] on why 307 is preferred over 302.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// As service:
|
||||||
|
/// ```
|
||||||
|
/// use actix_web::{web, App};
|
||||||
|
///
|
||||||
|
/// App::new()
|
||||||
|
/// // redirect "/duck" to DuckDuckGo
|
||||||
|
/// .service(web::redirect("/duck", "https://duck.com"))
|
||||||
|
/// .service(
|
||||||
|
/// // redirect "/api/old" to "/api/new"
|
||||||
|
/// web::scope("/api").service(web::redirect("/old", "/new"))
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// As responder:
|
||||||
|
/// ```
|
||||||
|
/// use actix_web::web::Redirect;
|
||||||
|
///
|
||||||
|
/// async fn handler() -> impl Responder {
|
||||||
|
/// // sends a permanent (308) redirect to duck.com
|
||||||
|
/// Redirect::to("https://duck.com").permanent()
|
||||||
|
/// }
|
||||||
|
/// # actix_web::web::to(handler);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Redirect {
|
||||||
|
from: Cow<'static, str>,
|
||||||
|
to: Cow<'static, str>,
|
||||||
|
status_code: StatusCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Redirect {
|
||||||
|
/// Construct a new `Redirect` service that matches a path.
|
||||||
|
///
|
||||||
|
/// This service will match exact paths equal to `from` within the current scope. I.e., when
|
||||||
|
/// registered on the root `App`, it will match exact, whole paths. But when registered on a
|
||||||
|
/// `Scope`, it will match paths under that scope, ignoring the defined scope prefix, just like
|
||||||
|
/// a normal `Resource` or `Route`.
|
||||||
|
///
|
||||||
|
/// The `to` argument can be path or URL; whatever is provided shall be used verbatim when
|
||||||
|
/// setting the redirect location. This means that relative paths can be used to navigate
|
||||||
|
/// relatively to matched paths.
|
||||||
|
///
|
||||||
|
/// Prefer [`Redirect::to()`](Self::to) when using `Redirect` as a responder since `from` has
|
||||||
|
/// no meaning in that context.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// # use actix_web::{web::Redirect, App};
|
||||||
|
/// App::new()
|
||||||
|
/// // redirects "/oh/hi/mark" to "/oh/bye/johnny"
|
||||||
|
/// .service(Redirect::new("/oh/hi/mark", "../../bye/johnny"));
|
||||||
|
/// ```
|
||||||
|
pub fn new(from: impl Into<Cow<'static, str>>, to: impl Into<Cow<'static, str>>) -> Self {
|
||||||
|
Self {
|
||||||
|
from: from.into(),
|
||||||
|
to: to.into(),
|
||||||
|
status_code: StatusCode::TEMPORARY_REDIRECT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a new `Redirect` to use as a responder.
|
||||||
|
///
|
||||||
|
/// Only receives the `to` argument since responders do not need to do route matching.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use actix_web::web::Redirect;
|
||||||
|
///
|
||||||
|
/// async fn admin_page() -> impl Responder {
|
||||||
|
/// // sends a temporary 307 redirect to the login path
|
||||||
|
/// Redirect::to("/login")
|
||||||
|
/// }
|
||||||
|
/// # actix_web::web::to(handler);
|
||||||
|
/// ```
|
||||||
|
pub fn to(to: impl Into<Cow<'static, str>>) -> Self {
|
||||||
|
Self {
|
||||||
|
from: "/".into(),
|
||||||
|
to: to.into(),
|
||||||
|
status_code: StatusCode::TEMPORARY_REDIRECT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use the "308 Permanent Redirect" status when responding.
|
||||||
|
///
|
||||||
|
/// See [this MDN article][mdn-redirects] on why 308 is preferred over 301.
|
||||||
|
///
|
||||||
|
/// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#permanent_redirections
|
||||||
|
pub fn permanent(self) -> Self {
|
||||||
|
self.using_status_code(StatusCode::PERMANENT_REDIRECT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use the "307 Temporary Redirect" status when responding.
|
||||||
|
///
|
||||||
|
/// See [this MDN article][mdn-redirects] on why 307 is preferred over 302.
|
||||||
|
///
|
||||||
|
/// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections
|
||||||
|
pub fn temporary(self) -> Self {
|
||||||
|
self.using_status_code(StatusCode::TEMPORARY_REDIRECT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use the "303 See Other" status when responding.
|
||||||
|
///
|
||||||
|
/// This status code is semantically correct as the response to a successful login, for example.
|
||||||
|
pub fn see_other(self) -> Self {
|
||||||
|
self.using_status_code(StatusCode::SEE_OTHER)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allows the use of custom status codes for less common redirect types.
|
||||||
|
///
|
||||||
|
/// In most cases, the default status ("308 Permanent Redirect") or using the `temporary`
|
||||||
|
/// method, which uses the "307 Temporary Redirect" status have more consistent behavior than
|
||||||
|
/// 301 and 302 codes, respectively.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use actix_web::{http::StatusCode, web::Redirect};
|
||||||
|
/// // redirects would use "301 Moved Permanently" status code
|
||||||
|
/// Redirect::new("/old", "/new")
|
||||||
|
/// .using_status_code(StatusCode::MOVED_PERMANENTLY);
|
||||||
|
///
|
||||||
|
/// // redirects would use "302 Found" status code
|
||||||
|
/// Redirect::new("/old", "/new")
|
||||||
|
/// .using_status_code(StatusCode::FOUND);
|
||||||
|
/// ```
|
||||||
|
pub fn using_status_code(mut self, status: StatusCode) -> Self {
|
||||||
|
self.status_code = status;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpServiceFactory for Redirect {
|
||||||
|
fn register(self, config: &mut AppService) {
|
||||||
|
let redirect = self.clone();
|
||||||
|
let rdef = ResourceDef::new(self.from.into_owned());
|
||||||
|
let redirect_factory = fn_service(move |mut req: ServiceRequest| {
|
||||||
|
let res = redirect.clone().respond_to(req.parts_mut().0);
|
||||||
|
ready(Ok(req.into_response(res.map_into_boxed_body())))
|
||||||
|
});
|
||||||
|
|
||||||
|
config.register_service(rdef, None, redirect_factory, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Responder for Redirect {
|
||||||
|
type Body = ();
|
||||||
|
|
||||||
|
fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||||
|
let mut res = HttpResponse::with_body(self.status_code, ());
|
||||||
|
|
||||||
|
if let Ok(hdr_val) = self.to.parse() {
|
||||||
|
res.headers_mut().insert(LOCATION, hdr_val);
|
||||||
|
} else {
|
||||||
|
log::error!(
|
||||||
|
"redirect target location can not be converted to header value: {:?}",
|
||||||
|
self.to
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{dev::Service, http::StatusCode, test, App};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn absolute_redirects() {
|
||||||
|
let redirector = Redirect::new("/one", "/two").permanent();
|
||||||
|
|
||||||
|
let svc = test::init_service(App::new().service(redirector)).await;
|
||||||
|
|
||||||
|
let req = test::TestRequest::default().uri("/one").to_request();
|
||||||
|
let res = svc.call(req).await.unwrap();
|
||||||
|
assert_eq!(res.status(), StatusCode::from_u16(308).unwrap());
|
||||||
|
let hdr = res.headers().get(&LOCATION).unwrap();
|
||||||
|
assert_eq!(hdr.to_str().unwrap(), "/two");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn relative_redirects() {
|
||||||
|
let redirector = Redirect::new("/one", "two").permanent();
|
||||||
|
|
||||||
|
let svc = test::init_service(App::new().service(redirector)).await;
|
||||||
|
|
||||||
|
let req = test::TestRequest::default().uri("/one").to_request();
|
||||||
|
let res = svc.call(req).await.unwrap();
|
||||||
|
assert_eq!(res.status(), StatusCode::from_u16(308).unwrap());
|
||||||
|
let hdr = res.headers().get(&LOCATION).unwrap();
|
||||||
|
assert_eq!(hdr.to_str().unwrap(), "two");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn temporary_redirects() {
|
||||||
|
let external_service = Redirect::new("/external", "https://duck.com");
|
||||||
|
|
||||||
|
let svc = test::init_service(App::new().service(external_service)).await;
|
||||||
|
|
||||||
|
let req = test::TestRequest::default().uri("/external").to_request();
|
||||||
|
let res = svc.call(req).await.unwrap();
|
||||||
|
assert_eq!(res.status(), StatusCode::from_u16(307).unwrap());
|
||||||
|
let hdr = res.headers().get(&LOCATION).unwrap();
|
||||||
|
assert_eq!(hdr.to_str().unwrap(), "https://duck.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn as_responder() {
|
||||||
|
let responder = Redirect::to("https://duck.com");
|
||||||
|
|
||||||
|
let req = test::TestRequest::default().to_http_request();
|
||||||
|
let res = responder.respond_to(&req);
|
||||||
|
|
||||||
|
assert_eq!(res.status(), StatusCode::from_u16(307).unwrap());
|
||||||
|
let hdr = res.headers().get(&LOCATION).unwrap();
|
||||||
|
assert_eq!(hdr.to_str().unwrap(), "https://duck.com");
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,10 +11,12 @@
|
||||||
//! - [`Bytes`]: Raw payload
|
//! - [`Bytes`]: Raw payload
|
||||||
//!
|
//!
|
||||||
//! # Responders
|
//! # Responders
|
||||||
//! - [`Json`]: JSON request payload
|
//! - [`Json`]: JSON response
|
||||||
//! - [`Bytes`]: Raw request payload
|
//! - [`Form`]: URL-encoded response
|
||||||
|
//! - [`Bytes`]: Raw bytes response
|
||||||
|
//! - [`Redirect`](Redirect::to): Convenient redirect responses
|
||||||
|
|
||||||
use std::future::Future;
|
use std::{borrow::Cow, future::Future};
|
||||||
|
|
||||||
use actix_router::IntoPatterns;
|
use actix_router::IntoPatterns;
|
||||||
pub use bytes::{Buf, BufMut, Bytes, BytesMut};
|
pub use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||||
|
@ -26,6 +28,7 @@ use crate::{
|
||||||
|
|
||||||
pub use crate::config::ServiceConfig;
|
pub use crate::config::ServiceConfig;
|
||||||
pub use crate::data::Data;
|
pub use crate::data::Data;
|
||||||
|
pub use crate::redirect::Redirect;
|
||||||
pub use crate::request_data::ReqData;
|
pub use crate::request_data::ReqData;
|
||||||
pub use crate::types::*;
|
pub use crate::types::*;
|
||||||
|
|
||||||
|
@ -45,6 +48,7 @@ pub use crate::types::*;
|
||||||
/// For instance, to route `GET`-requests on any route matching `/users/{userid}/{friend}` and store
|
/// For instance, to route `GET`-requests on any route matching `/users/{userid}/{friend}` and store
|
||||||
/// `userid` and `friend` in the exposed `Path` object:
|
/// `userid` and `friend` in the exposed `Path` object:
|
||||||
///
|
///
|
||||||
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::{web, App, HttpResponse};
|
/// use actix_web::{web, App, HttpResponse};
|
||||||
///
|
///
|
||||||
|
@ -74,6 +78,7 @@ pub fn resource<T: IntoPatterns>(path: T) -> Resource {
|
||||||
/// - `/{project_id}/path2`
|
/// - `/{project_id}/path2`
|
||||||
/// - `/{project_id}/path3`
|
/// - `/{project_id}/path3`
|
||||||
///
|
///
|
||||||
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::{web, App, HttpResponse};
|
/// use actix_web::{web, App, HttpResponse};
|
||||||
///
|
///
|
||||||
|
@ -183,6 +188,25 @@ pub fn service<T: IntoPatterns>(path: T) -> WebService {
|
||||||
WebService::new(path)
|
WebService::new(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a relative or absolute redirect.
|
||||||
|
///
|
||||||
|
/// See [`Redirect`] docs for usage details.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use actix_web::{web, App};
|
||||||
|
///
|
||||||
|
/// let app = App::new()
|
||||||
|
/// // the client will resolve this redirect to /api/to-path
|
||||||
|
/// .service(web::redirect("/api/from-path", "to-path"));
|
||||||
|
/// ```
|
||||||
|
pub fn redirect(
|
||||||
|
from: impl Into<Cow<'static, str>>,
|
||||||
|
to: impl Into<Cow<'static, str>>,
|
||||||
|
) -> Redirect {
|
||||||
|
Redirect::new(from, to)
|
||||||
|
}
|
||||||
|
|
||||||
/// Executes blocking function on a thread pool, returns future that resolves to result of the
|
/// Executes blocking function on a thread pool, returns future that resolves to result of the
|
||||||
/// function execution.
|
/// function execution.
|
||||||
pub fn block<F, R>(f: F) -> impl Future<Output = Result<R, BlockingError>>
|
pub fn block<F, R>(f: F) -> impl Future<Output = Result<R, BlockingError>>
|
||||||
|
|
Loading…
Reference in a new issue