From bc84f1a237aad132c562baf22d31b8e85caeae0c Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Mon, 16 Aug 2021 13:08:35 +0200 Subject: [PATCH 1/2] Add `Redirect` response --- CHANGELOG.md | 1 + examples/oauth.rs | 44 ++++++++--------------- src/response/mod.rs | 5 ++- src/response/redirect.rs | 77 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 31 deletions(-) create mode 100644 src/response/redirect.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index d28c8f42e4..b8e6981eac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `NestedUri` for extracting request URI in nested services ([#161](https://github.com/tokio-rs/axum/pull/161)) - Implement `FromRequest` for `http::Extensions` - Implement SSE as an `IntoResponse` instead of a service ([#98](https://github.com/tokio-rs/axum/pull/98)) +- Add `Redirect` response. ## Breaking changes diff --git a/examples/oauth.rs b/examples/oauth.rs index ed199dea91..1131ff135d 100644 --- a/examples/oauth.rs +++ b/examples/oauth.rs @@ -9,14 +9,13 @@ use async_session::{MemoryStore, Session, SessionStore}; use axum::{ async_trait, + body::{Bytes, Empty}, extract::{Extension, FromRequest, Query, RequestParts, TypedHeader}, prelude::*, - response::IntoResponse, + response::{IntoResponse, Redirect}, AddExtensionLayer, }; -use http::header::SET_COOKIE; -use http::StatusCode; -use hyper::Body; +use http::{header::SET_COOKIE, HeaderMap}; use oauth2::{ basic::BasicClient, reqwest::async_http_client, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, Scope, TokenResponse, TokenUrl, @@ -118,7 +117,7 @@ async fn discord_auth(Extension(client): Extension) -> impl IntoRes .url(); // Redirect to Discord's oauth service - Redirect(auth_url.into()) + Redirect::permanent(auth_url.to_string().parse().unwrap()) } // Valid user session required. If there is none, redirect to the auth page @@ -137,12 +136,12 @@ async fn logout( let session = match store.load_session(cookie.to_string()).await.unwrap() { Some(s) => s, // No session active, just redirect - None => return Redirect("/".to_string()), + None => return Redirect::permanent("/".parse().unwrap()), }; store.destroy_session(session).await.unwrap(); - Redirect("/".to_string()) + Redirect::permanent("/".parse().unwrap()) } #[derive(Debug, Deserialize)] @@ -187,35 +186,20 @@ async fn login_authorized( let cookie = format!("{}={}; SameSite=Lax; Path=/", COOKIE_NAME, cookie); // Set cookie - let r = http::Response::builder() - .header("Location", "/") - .header(SET_COOKIE, cookie) - .status(302); + let mut headers = HeaderMap::new(); + headers.insert(SET_COOKIE, cookie.parse().unwrap()); - r.body(Body::empty()).unwrap() -} - -// Utility to save some lines of code -struct Redirect(String); -impl IntoResponse for Redirect { - type Body = Body; - type BodyError = hyper::Error; - - fn into_response(self) -> http::Response { - let builder = http::Response::builder() - .header("Location", self.0) - .status(StatusCode::FOUND); - builder.body(Body::empty()).unwrap() - } + (headers, Redirect::permanent("/".parse().unwrap())) } struct AuthRedirect; + impl IntoResponse for AuthRedirect { - type Body = Body; - type BodyError = hyper::Error; + type Body = Empty; + type BodyError = ::Error; - fn into_response(self) -> http::Response { - Redirect("/auth/discord".to_string()).into_response() + fn into_response(self) -> http::Response { + Redirect::permanent("/auth/discord".parse().unwrap()).into_response() } } diff --git a/src/response/mod.rs b/src/response/mod.rs index ff1fba2730..39919e3038 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -16,8 +16,11 @@ use tower::{util::Either, BoxError}; #[doc(no_inline)] pub use crate::Json; -pub mod sse; +mod redirect; + +pub use self::redirect::Redirect; +pub mod sse; pub use sse::{sse, Sse}; /// Trait for generating responses. diff --git a/src/response/redirect.rs b/src/response/redirect.rs new file mode 100644 index 0000000000..b95cbcda71 --- /dev/null +++ b/src/response/redirect.rs @@ -0,0 +1,77 @@ +use super::IntoResponse; +use bytes::Bytes; +use http::{header::LOCATION, HeaderValue, Response, StatusCode, Uri}; +use http_body::{Body, Empty}; +use std::convert::TryFrom; + +/// Response that redirects the request to another location. +/// +/// # Example +/// +/// ```rust +/// use axum::{prelude::*, response::Redirect}; +/// +/// let app = route("/old", get(|| async { Redirect::permanent("/new".parse().unwrap()) })) +/// .route("/new", get(|| async { "Hello!" })); +/// # async { +/// # hyper::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); +/// # }; +/// ``` +#[derive(Debug, Clone)] +pub struct Redirect { + status_code: StatusCode, + location: HeaderValue, +} + +impl Redirect { + /// Create a new [`Redirect`] that uses a [`307 Temporary Redirect`][mdn] status code. + /// + /// # Panics + /// + /// - If `uri` isn't a valid [`HeaderValue`]. + /// + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307 + pub fn temporary(uri: Uri) -> Self { + Self::with_status_code(StatusCode::TEMPORARY_REDIRECT, uri) + } + + /// Create a new [`Redirect`] that uses a [`308 Permanent Redirect`][mdn] status code. + /// + /// # Panics + /// + /// - If `uri` isn't a valid [`HeaderValue`]. + /// + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308 + pub fn permanent(uri: Uri) -> Self { + Self::with_status_code(StatusCode::PERMANENT_REDIRECT, uri) + } + + // This is intentionally not public since other kinds of redirects might not + // use the `Location` header, namely `304 Not Modified`. + // + // We're open to adding more constructors upon request, if they make sense :) + fn with_status_code(status_code: StatusCode, uri: Uri) -> Self { + assert!( + status_code.is_redirection(), + "not a redirection status code" + ); + + Self { + status_code, + location: HeaderValue::try_from(uri.to_string()) + .expect("URI isn't a valid header value"), + } + } +} + +impl IntoResponse for Redirect { + type Body = Empty; + type BodyError = ::Error; + + fn into_response(self) -> Response { + let mut res = Response::new(Empty::new()); + *res.status_mut() = self.status_code; + res.headers_mut().insert(LOCATION, self.location); + res + } +} From 8054ffaa5f27a481dfd78f5962d72b65ad571681 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Mon, 16 Aug 2021 19:24:14 +0200 Subject: [PATCH 2/2] Add `Redirect::found` --- examples/oauth.rs | 10 +++++----- src/response/redirect.rs | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/examples/oauth.rs b/examples/oauth.rs index 1131ff135d..ce9a49263c 100644 --- a/examples/oauth.rs +++ b/examples/oauth.rs @@ -117,7 +117,7 @@ async fn discord_auth(Extension(client): Extension) -> impl IntoRes .url(); // Redirect to Discord's oauth service - Redirect::permanent(auth_url.to_string().parse().unwrap()) + Redirect::found(auth_url.to_string().parse().unwrap()) } // Valid user session required. If there is none, redirect to the auth page @@ -136,12 +136,12 @@ async fn logout( let session = match store.load_session(cookie.to_string()).await.unwrap() { Some(s) => s, // No session active, just redirect - None => return Redirect::permanent("/".parse().unwrap()), + None => return Redirect::found("/".parse().unwrap()), }; store.destroy_session(session).await.unwrap(); - Redirect::permanent("/".parse().unwrap()) + Redirect::found("/".parse().unwrap()) } #[derive(Debug, Deserialize)] @@ -189,7 +189,7 @@ async fn login_authorized( let mut headers = HeaderMap::new(); headers.insert(SET_COOKIE, cookie.parse().unwrap()); - (headers, Redirect::permanent("/".parse().unwrap())) + (headers, Redirect::found("/".parse().unwrap())) } struct AuthRedirect; @@ -199,7 +199,7 @@ impl IntoResponse for AuthRedirect { type BodyError = ::Error; fn into_response(self) -> http::Response { - Redirect::permanent("/auth/discord".parse().unwrap()).into_response() + Redirect::found("/auth/discord".parse().unwrap()).into_response() } } diff --git a/src/response/redirect.rs b/src/response/redirect.rs index b95cbcda71..f9d60158f2 100644 --- a/src/response/redirect.rs +++ b/src/response/redirect.rs @@ -28,7 +28,7 @@ impl Redirect { /// /// # Panics /// - /// - If `uri` isn't a valid [`HeaderValue`]. + /// If `uri` isn't a valid [`HeaderValue`]. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307 pub fn temporary(uri: Uri) -> Self { @@ -39,13 +39,24 @@ impl Redirect { /// /// # Panics /// - /// - If `uri` isn't a valid [`HeaderValue`]. + /// If `uri` isn't a valid [`HeaderValue`]. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308 pub fn permanent(uri: Uri) -> Self { Self::with_status_code(StatusCode::PERMANENT_REDIRECT, uri) } + /// Create a new [`Redirect`] that uses a [`302 Found`][mdn] status code. + /// + /// # Panics + /// + /// If `uri` isn't a valid [`HeaderValue`]. + /// + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302 + pub fn found(uri: Uri) -> Self { + Self::with_status_code(StatusCode::FOUND, uri) + } + // This is intentionally not public since other kinds of redirects might not // use the `Location` header, namely `304 Not Modified`. //