diff --git a/axum-core/CHANGELOG.md b/axum-core/CHANGELOG.md index d2b090ab..ebeed0b9 100644 --- a/axum-core/CHANGELOG.md +++ b/axum-core/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# Unreleased + +- **added:** `ResponseParts::status` and `ResponseParts::status_mut` accessors, + allowing `IntoResponseParts` implementations to set the response status ([#3721]) + +[#3721]: https://github.com/tokio-rs/axum/pull/3721 + +# 0.5.6 + +Improve error messages with `#[diagnostic::do_not_recommend]`. + # 0.5.5 Released without changes to fix docs.rs build. diff --git a/axum-core/src/response/into_response_parts.rs b/axum-core/src/response/into_response_parts.rs index 55b705fc..3ce5c980 100644 --- a/axum-core/src/response/into_response_parts.rs +++ b/axum-core/src/response/into_response_parts.rs @@ -107,6 +107,18 @@ pub struct ResponseParts { } impl ResponseParts { + /// Gets the response status code. + #[must_use] + pub fn status(&self) -> StatusCode { + self.res.status() + } + + /// Gets a mutable reference to the response status code. + #[must_use] + pub fn status_mut(&mut self) -> &mut StatusCode { + self.res.status_mut() + } + /// Gets a reference to the response headers. #[must_use] pub fn headers(&self) -> &HeaderMap { diff --git a/axum/CHANGELOG.md b/axum/CHANGELOG.md index f0c6945e..04dd560d 100644 --- a/axum/CHANGELOG.md +++ b/axum/CHANGELOG.md @@ -7,9 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Unreleased +- **added:** `IntoResponseParts` impl for `Redirect`, allowing it to be combined + with a body in a response tuple ([#3721]) - **changed:** Updated `matchit` allowing for routes with captures and static prefixes and suffixes ([#3702]) [#3702]: https://github.com/tokio-rs/axum/pull/3702 +[#3721]: https://github.com/tokio-rs/axum/pull/3721 # 0.8.9 diff --git a/axum/src/response/redirect.rs b/axum/src/response/redirect.rs index e33928cd..b408e8cd 100644 --- a/axum/src/response/redirect.rs +++ b/axum/src/response/redirect.rs @@ -1,4 +1,4 @@ -use axum_core::response::{IntoResponse, Response}; +use axum_core::response::{IntoResponse, IntoResponseParts, Response, ResponseParts}; use http::{header::LOCATION, HeaderValue, StatusCode}; /// Response that redirects the request to another location. @@ -93,11 +93,65 @@ impl IntoResponse for Redirect { } } +impl IntoResponseParts for Redirect { + type Error = (StatusCode, String); + + /// Sets the redirect status code and `Location` header on the response. + /// + /// This allows `Redirect` to be used as part of a response tuple, for example + /// to include a body alongside a redirect as recommended by + /// [RFC 9110 ยง15.4.4](https://datatracker.ietf.org/doc/html/rfc9110#name-303-see-other). + /// + /// # Examples + /// + /// ```rust + /// use axum::response::{Html, Redirect}; + /// + /// let url = "https://example.com"; + /// + /// // Return a redirect with a body + /// let response = ( + /// Redirect::to(url), + /// Html(format!( + /// r#"

Redirecting to {url}

"#, + /// )), + /// ); + /// ``` + /// + /// Note that when used alongside an explicit [`StatusCode`] in a tuple, the + /// `StatusCode` takes precedence: + /// + /// ```rust + /// use axum::response::Redirect; + /// use axum::http::StatusCode; + /// + /// // The status will be 307, not 303 + /// let response = ( + /// StatusCode::TEMPORARY_REDIRECT, + /// Redirect::to("/new"), + /// "redirecting...", + /// ); + /// ``` + fn into_response_parts(self, mut res: ResponseParts) -> Result { + let location = HeaderValue::try_from(self.location).map_err(|err| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("invalid redirect location: {err}"), + ) + })?; + + *res.status_mut() = self.status_code; + res.headers_mut().insert(LOCATION, location); + + Ok(res) + } +} + #[cfg(test)] mod tests { use super::Redirect; use axum_core::response::IntoResponse; - use http::StatusCode; + use http::{header::LOCATION, StatusCode}; const EXAMPLE_URL: &str = "https://example.com"; @@ -135,4 +189,49 @@ mod tests { assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); } + + #[test] + fn into_response_parts_sets_status_and_location() { + let response = (Redirect::to(EXAMPLE_URL), "body").into_response(); + + assert_eq!(response.status(), StatusCode::SEE_OTHER); + assert_eq!(response.headers().get(LOCATION).unwrap(), EXAMPLE_URL); + } + + #[test] + fn into_response_parts_with_permanent_redirect() { + let response = (Redirect::permanent(EXAMPLE_URL), "body").into_response(); + + assert_eq!(response.status(), StatusCode::PERMANENT_REDIRECT); + assert_eq!(response.headers().get(LOCATION).unwrap(), EXAMPLE_URL); + } + + #[test] + fn into_response_parts_with_temporary_redirect() { + let response = (Redirect::temporary(EXAMPLE_URL), "body").into_response(); + + assert_eq!(response.status(), StatusCode::TEMPORARY_REDIRECT); + assert_eq!(response.headers().get(LOCATION).unwrap(), EXAMPLE_URL); + } + + #[test] + fn into_response_parts_explicit_status_overrides() { + // Explicit StatusCode in a tuple takes precedence over the Redirect status + let response = ( + StatusCode::TEMPORARY_REDIRECT, + Redirect::to(EXAMPLE_URL), + "body", + ) + .into_response(); + + assert_eq!(response.status(), StatusCode::TEMPORARY_REDIRECT); + assert_eq!(response.headers().get(LOCATION).unwrap(), EXAMPLE_URL); + } + + #[test] + fn into_response_parts_invalid_location() { + let response = (Redirect::permanent("invalid\nlocation"), "body").into_response(); + + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + } }