feat: impl IntoResponseParts for Redirect (#3721)

This commit is contained in:
Dihan Nahdi
2026-05-02 17:46:22 +02:00
committed by GitHub
parent bff1764b70
commit b0123f7051
4 changed files with 123 additions and 2 deletions
+7
View File
@@ -5,6 +5,13 @@ 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]`.
@@ -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 {
+3
View File
@@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **added:** `MethodRouter::method_filter` ([#3586])
- **added:** `serve::Executor` trait and `Serve::with_executor` for customizing how connection
tasks are spawned, enabling use cases like tracing and telemetry instrumentation ([#3704])
- **added:** `IntoResponseParts` impl for `Redirect`, allowing it to be combined
with a body in a response tuple ([#3721])
- **changed:** `serve` has an additional generic argument and can now work with any response body
type, not just `axum::body::Body` ([#3205])
- **changed:** `Redirect` constructors now accept any `impl Into<String>` ([#3635])
@@ -32,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#3586]: https://github.com/tokio-rs/axum/pull/3586
[#3635]: https://github.com/tokio-rs/axum/pull/3635
[#3702]: https://github.com/tokio-rs/axum/pull/3702
[#3721]: https://github.com/tokio-rs/axum/pull/3721
# 0.8.9
+101 -2
View File
@@ -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#"<p>Redirecting to <a href="{url}">{url}</a></p>"#,
/// )),
/// );
/// ```
///
/// 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<ResponseParts, Self::Error> {
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);
}
}