diff --git a/axum-core/CHANGELOG.md b/axum-core/CHANGELOG.md index 0d56e641..374eaf48 100644 --- a/axum-core/CHANGELOG.md +++ b/axum-core/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Unreleased -- **added:** Add `IntoResponseHeaders` trait ([#644]) +- **added:** Add `IntoResponseParts` trait ([#797]) - **added:** Implement `IntoResponse` for `bytes::BytesMut` and `bytes::Chain` ([#767]) - **breaking:** Using `HeaderMap` as an extractor will no longer remove the headers and thus they'll still be accessible to other extractors, such as `axum::extract::Json`. Instead @@ -37,10 +37,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `::Rejection` is now `Infallible`. - `ExtensionsAlreadyExtracted` has been removed. -[#644]: https://github.com/tokio-rs/axum/pull/644 [#698]: https://github.com/tokio-rs/axum/pull/698 [#699]: https://github.com/tokio-rs/axum/pull/699 [#767]: https://github.com/tokio-rs/axum/pull/767 +[#797]: https://github.com/tokio-rs/axum/pull/797 # 0.1.1 (06. December, 2021) diff --git a/axum-core/Cargo.toml b/axum-core/Cargo.toml index f99ce4d3..f4f172d6 100644 --- a/axum-core/Cargo.toml +++ b/axum-core/Cargo.toml @@ -22,3 +22,4 @@ mime = "0.3.16" futures-util = "0.3" axum = { path = "../axum", version = "0.4" } hyper = "0.14" +tokio = { version = "1.0", features = ["macros"] } diff --git a/axum-core/src/extract/rejection.rs b/axum-core/src/extract/rejection.rs index 10440e49..fc81eda7 100644 --- a/axum-core/src/extract/rejection.rs +++ b/axum-core/src/extract/rejection.rs @@ -1,8 +1,7 @@ //! Rejection response types. -use crate::body; -use http::{Response, StatusCode}; -use http_body::Full; +use crate::response::{IntoResponse, Response}; +use http::StatusCode; use std::fmt; /// Rejection type used if you try and extract the request body more than @@ -15,11 +14,9 @@ impl BodyAlreadyExtracted { const BODY: &'static str = "Cannot have two request body extractors for a single handler"; } -impl crate::response::IntoResponse for BodyAlreadyExtracted { - fn into_response(self) -> crate::response::Response { - let mut res = Response::new(body::boxed(Full::from(Self::BODY))); - *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; - res +impl IntoResponse for BodyAlreadyExtracted { + fn into_response(self) -> Response { + (StatusCode::INTERNAL_SERVER_ERROR, Self::BODY).into_response() } } diff --git a/axum-core/src/macros.rs b/axum-core/src/macros.rs index 723e319f..71d7d201 100644 --- a/axum-core/src/macros.rs +++ b/axum-core/src/macros.rs @@ -20,12 +20,10 @@ macro_rules! define_rejection { impl crate::response::IntoResponse for $name { fn into_response(self) -> $crate::response::Response { - let body = http_body::Full::from(format!(concat!($body, ": {}"), self.0)); - let body = $crate::body::boxed(body); - let mut res = - http::Response::new(body); - *res.status_mut() = http::StatusCode::$status; - res + ( + http::StatusCode::$status, + format!(concat!($body, ": {}"), self.0) + ).into_response() } } diff --git a/axum-core/src/response/into_response.rs b/axum-core/src/response/into_response.rs new file mode 100644 index 00000000..f11158d7 --- /dev/null +++ b/axum-core/src/response/into_response.rs @@ -0,0 +1,531 @@ +use super::{IntoResponseParts, Response, ResponseParts}; +use crate::{body, BoxError}; +use bytes::{buf::Chain, Buf, Bytes, BytesMut}; +use http::{ + header::{self, HeaderMap, HeaderName, HeaderValue}, + StatusCode, Version, +}; +use http_body::{ + combinators::{MapData, MapErr}, + Empty, Full, SizeHint, +}; +use std::{ + borrow::Cow, + convert::{Infallible, TryInto}, + fmt, + pin::Pin, + task::{Context, Poll}, +}; + +/// Trait for generating responses. +/// +/// Types that implement `IntoResponse` can be returned from handlers. +/// +/// # Implementing `IntoResponse` +/// +/// You generally shouldn't have to implement `IntoResponse` manually, as axum +/// provides implementations for many common types. +/// +/// However it might be necessary if you have a custom error type that you want +/// to return from handlers: +/// +/// ```rust +/// use axum::{ +/// Router, +/// body::{self, Bytes}, +/// routing::get, +/// http::StatusCode, +/// response::{IntoResponse, Response}, +/// }; +/// +/// enum MyError { +/// SomethingWentWrong, +/// SomethingElseWentWrong, +/// } +/// +/// impl IntoResponse for MyError { +/// fn into_response(self) -> Response { +/// let body = match self { +/// MyError::SomethingWentWrong => "something went wrong", +/// MyError::SomethingElseWentWrong => "something else went wrong", +/// }; +/// +/// // its often easiest to implement `IntoResponse` by calling other implementations +/// (StatusCode::INTERNAL_SERVER_ERROR, body).into_response() +/// } +/// } +/// +/// // `Result` can now be returned from handlers +/// let app = Router::new().route("/", get(handler)); +/// +/// async fn handler() -> Result<(), MyError> { +/// Err(MyError::SomethingWentWrong) +/// } +/// # async { +/// # hyper::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); +/// # }; +/// ``` +/// +/// Or if you have a custom body type you'll also need to implement +/// `IntoResponse` for it: +/// +/// ```rust +/// use axum::{ +/// body, +/// routing::get, +/// response::{IntoResponse, Response}, +/// Router, +/// }; +/// use http_body::Body; +/// use http::HeaderMap; +/// use bytes::Bytes; +/// use std::{ +/// convert::Infallible, +/// task::{Poll, Context}, +/// pin::Pin, +/// }; +/// +/// struct MyBody; +/// +/// // First implement `Body` for `MyBody`. This could for example use +/// // some custom streaming protocol. +/// impl Body for MyBody { +/// type Data = Bytes; +/// type Error = Infallible; +/// +/// fn poll_data( +/// self: Pin<&mut Self>, +/// cx: &mut Context<'_> +/// ) -> Poll>> { +/// # unimplemented!() +/// // ... +/// } +/// +/// fn poll_trailers( +/// self: Pin<&mut Self>, +/// cx: &mut Context<'_> +/// ) -> Poll, Self::Error>> { +/// # unimplemented!() +/// // ... +/// } +/// } +/// +/// // Now we can implement `IntoResponse` directly for `MyBody` +/// impl IntoResponse for MyBody { +/// fn into_response(self) -> Response { +/// Response::new(body::boxed(self)) +/// } +/// } +/// +/// // `MyBody` can now be returned from handlers. +/// let app = Router::new().route("/", get(|| async { MyBody })); +/// # async { +/// # hyper::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); +/// # }; +/// ``` +pub trait IntoResponse { + /// Create a response. + fn into_response(self) -> Response; +} + +impl IntoResponse for StatusCode { + fn into_response(self) -> Response { + let mut res = ().into_response(); + *res.status_mut() = self; + res + } +} + +impl IntoResponse for Version { + fn into_response(self) -> Response { + let mut res = ().into_response(); + *res.version_mut() = self; + res + } +} + +impl IntoResponse for () { + fn into_response(self) -> Response { + Empty::new().into_response() + } +} + +impl IntoResponse for Infallible { + fn into_response(self) -> Response { + match self {} + } +} + +impl IntoResponse for Result +where + T: IntoResponse, + E: IntoResponse, +{ + fn into_response(self) -> Response { + match self { + Ok(value) => value.into_response(), + Err(err) => err.into_response(), + } + } +} + +impl IntoResponse for Response +where + B: http_body::Body + Send + 'static, + B::Error: Into, +{ + fn into_response(self) -> Response { + self.map(body::boxed) + } +} + +impl IntoResponse for http::response::Parts { + fn into_response(self) -> Response { + Response::from_parts(self, body::boxed(Empty::new())) + } +} + +impl IntoResponse for Full { + fn into_response(self) -> Response { + Response::new(body::boxed(self)) + } +} + +impl IntoResponse for Empty { + fn into_response(self) -> Response { + Response::new(body::boxed(self)) + } +} + +impl IntoResponse for http_body::combinators::BoxBody +where + E: Into + 'static, +{ + fn into_response(self) -> Response { + Response::new(body::boxed(self)) + } +} + +impl IntoResponse for http_body::combinators::UnsyncBoxBody +where + E: Into + 'static, +{ + fn into_response(self) -> Response { + Response::new(body::boxed(self)) + } +} + +impl IntoResponse for MapData +where + B: http_body::Body + Send + 'static, + F: FnMut(B::Data) -> Bytes + Send + 'static, + B::Error: Into, +{ + fn into_response(self) -> Response { + Response::new(body::boxed(self)) + } +} + +impl IntoResponse for MapErr +where + B: http_body::Body + Send + 'static, + F: FnMut(B::Error) -> E + Send + 'static, + E: Into, +{ + fn into_response(self) -> Response { + Response::new(body::boxed(self)) + } +} + +impl IntoResponse for &'static str { + fn into_response(self) -> Response { + Cow::Borrowed(self).into_response() + } +} + +impl IntoResponse for String { + fn into_response(self) -> Response { + Cow::<'static, str>::Owned(self).into_response() + } +} + +impl IntoResponse for Cow<'static, str> { + fn into_response(self) -> Response { + let mut res = Full::from(self).into_response(); + res.headers_mut().insert( + header::CONTENT_TYPE, + HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()), + ); + res + } +} + +impl IntoResponse for Bytes { + fn into_response(self) -> Response { + let mut res = Full::from(self).into_response(); + res.headers_mut().insert( + header::CONTENT_TYPE, + HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM.as_ref()), + ); + res + } +} + +impl IntoResponse for BytesMut { + fn into_response(self) -> Response { + self.freeze().into_response() + } +} + +impl IntoResponse for Chain +where + T: Buf + Unpin + Send + 'static, + U: Buf + Unpin + Send + 'static, +{ + fn into_response(self) -> Response { + let (first, second) = self.into_inner(); + let mut res = Response::new(body::boxed(BytesChainBody { + first: Some(first), + second: Some(second), + })); + res.headers_mut().insert( + header::CONTENT_TYPE, + HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM.as_ref()), + ); + res + } +} + +struct BytesChainBody { + first: Option, + second: Option, +} + +impl http_body::Body for BytesChainBody +where + T: Buf + Unpin, + U: Buf + Unpin, +{ + type Data = Bytes; + type Error = Infallible; + + fn poll_data( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if let Some(mut buf) = self.first.take() { + let bytes = buf.copy_to_bytes(buf.remaining()); + return Poll::Ready(Some(Ok(bytes))); + } + + if let Some(mut buf) = self.second.take() { + let bytes = buf.copy_to_bytes(buf.remaining()); + return Poll::Ready(Some(Ok(bytes))); + } + + Poll::Ready(None) + } + + fn poll_trailers( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + Poll::Ready(Ok(None)) + } + + fn is_end_stream(&self) -> bool { + self.first.is_none() && self.second.is_none() + } + + fn size_hint(&self) -> SizeHint { + match (self.first.as_ref(), self.second.as_ref()) { + (Some(first), Some(second)) => { + let total_size = first.remaining() + second.remaining(); + SizeHint::with_exact(total_size as u64) + } + (Some(buf), None) => SizeHint::with_exact(buf.remaining() as u64), + (None, Some(buf)) => SizeHint::with_exact(buf.remaining() as u64), + (None, None) => SizeHint::with_exact(0), + } + } +} + +impl IntoResponse for &'static [u8] { + fn into_response(self) -> Response { + Cow::Borrowed(self).into_response() + } +} + +impl IntoResponse for Vec { + fn into_response(self) -> Response { + Cow::<'static, [u8]>::Owned(self).into_response() + } +} + +impl IntoResponse for Cow<'static, [u8]> { + fn into_response(self) -> Response { + let mut res = Full::from(self).into_response(); + res.headers_mut().insert( + header::CONTENT_TYPE, + HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM.as_ref()), + ); + res + } +} + +impl IntoResponse for (StatusCode, R) +where + R: IntoResponse, +{ + fn into_response(self) -> Response { + let mut res = self.1.into_response(); + *res.status_mut() = self.0; + res + } +} + +impl IntoResponse for (Version, R) +where + R: IntoResponse, +{ + fn into_response(self) -> Response { + let mut res = self.1.into_response(); + *res.version_mut() = self.0; + res + } +} + +impl IntoResponse for (Version, StatusCode, R) +where + R: IntoResponse, +{ + fn into_response(self) -> Response { + let (version, status, res) = self; + let mut res = res.into_response(); + *res.version_mut() = version; + *res.status_mut() = status; + res + } +} + +impl IntoResponse for HeaderMap { + fn into_response(self) -> Response { + let mut res = ().into_response(); + *res.headers_mut() = self; + res + } +} + +impl IntoResponse for [(K, V); N] +where + K: TryInto, + K::Error: fmt::Display, + V: TryInto, + V::Error: fmt::Display, +{ + fn into_response(self) -> Response { + let mut res = ().into_response(); + + for (key, value) in self { + let key = match key.try_into() { + Ok(key) => key, + Err(err) => { + return (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response() + } + }; + + let value = match value.try_into() { + Ok(value) => value, + Err(err) => { + return (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response() + } + }; + + res.headers_mut().insert(key, value); + } + + res + } +} + +macro_rules! impl_into_response { + ( $($ty:ident),* $(,)? ) => { + #[allow(non_snake_case)] + impl IntoResponse for ($($ty),*, R) + where + $( $ty: IntoResponseParts, )* + R: IntoResponse, + { + fn into_response(self) -> Response { + let ($($ty),*, res) = self; + + let res = res.into_response(); + let mut parts = ResponseParts { res: Ok(res) }; + + $( + $ty.into_response_parts(&mut parts); + )* + + match parts.res { + Ok(res) => res, + Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err).into_response(), + } + } + } + + #[allow(non_snake_case)] + impl IntoResponse for (StatusCode, $($ty),*, R) + where + $( $ty: IntoResponseParts, )* + R: IntoResponse, + { + fn into_response(self) -> Response { + let (status, $($ty),*, res) = self; + + let res = res.into_response(); + let mut parts = ResponseParts { res: Ok(res) }; + + $( + $ty.into_response_parts(&mut parts); + )* + + match parts.res { + Ok(mut res) => { + *res.status_mut() = status; + res + } + Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err).into_response(), + } + } + } + + #[allow(non_snake_case)] + impl IntoResponse for (Version, StatusCode, $($ty),*, R) + where + $( $ty: IntoResponseParts, )* + R: IntoResponse, + { + fn into_response(self) -> Response { + let (version, status, $($ty),*, res) = self; + + let res = res.into_response(); + let mut parts = ResponseParts { res: Ok(res) }; + + $( + $ty.into_response_parts(&mut parts); + )* + + match parts.res { + Ok(mut res) => { + *res.version_mut() = version; + *res.status_mut() = status; + res + } + Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err).into_response(), + } + } + } + } +} + +all_the_tuples!(impl_into_response); diff --git a/axum-core/src/response/into_response_parts.rs b/axum-core/src/response/into_response_parts.rs new file mode 100644 index 00000000..4a403e6d --- /dev/null +++ b/axum-core/src/response/into_response_parts.rs @@ -0,0 +1,124 @@ +use super::Response; +use http::header::{HeaderMap, HeaderName, HeaderValue}; +use std::{convert::TryInto, fmt}; + +/// Trait for adding headers and extensions to a response. +/// +/// You generally don't need to implement this trait manually. It's recommended instead to rely +/// on the implementations in axum. +pub trait IntoResponseParts { + /// Set parts of the response + fn into_response_parts(self, res: &mut ResponseParts); +} + +/// Parts of a response. +/// +/// Used with [`IntoResponseParts`]. +#[derive(Debug)] +pub struct ResponseParts { + pub(crate) res: Result, +} + +impl ResponseParts { + /// Insert a header into the response. + /// + /// If the header already exists, it will be overwritten. + pub fn insert_header(&mut self, key: K, value: V) + where + K: TryInto, + K::Error: fmt::Display, + V: TryInto, + V::Error: fmt::Display, + { + self.update_headers(key, value, |headers, key, value| { + headers.insert(key, value); + }); + } + + /// Append a header to the response. + /// + /// If the header already exists it will be appended to. + pub fn append_header(&mut self, key: K, value: V) + where + K: TryInto, + K::Error: fmt::Display, + V: TryInto, + V::Error: fmt::Display, + { + self.update_headers(key, value, |headers, key, value| { + headers.append(key, value); + }); + } + + fn update_headers(&mut self, key: K, value: V, f: F) + where + K: TryInto, + K::Error: fmt::Display, + V: TryInto, + V::Error: fmt::Display, + F: FnOnce(&mut HeaderMap, HeaderName, HeaderValue), + { + if let Ok(response) = &mut self.res { + let key = match key.try_into() { + Ok(key) => key, + Err(err) => { + self.res = Err(err.to_string()); + return; + } + }; + + let value = match value.try_into() { + Ok(value) => value, + Err(err) => { + self.res = Err(err.to_string()); + return; + } + }; + + f(response.headers_mut(), key, value); + } + } + + /// Insert an extension into the response. + /// + /// If the extension already exists it will be overwritten. + pub fn insert_extension(&mut self, extension: T) + where + T: Send + Sync + 'static, + { + if let Ok(res) = &mut self.res { + res.extensions_mut().insert(extension); + } + } +} + +impl Extend<(Option, HeaderValue)> for ResponseParts { + fn extend(&mut self, iter: T) + where + T: IntoIterator, HeaderValue)>, + { + if let Ok(res) = &mut self.res { + res.headers_mut().extend(iter); + } + } +} + +impl IntoResponseParts for HeaderMap { + fn into_response_parts(self, res: &mut ResponseParts) { + res.extend(self); + } +} + +impl IntoResponseParts for [(K, V); N] +where + K: TryInto, + K::Error: fmt::Display, + V: TryInto, + V::Error: fmt::Display, +{ + fn into_response_parts(self, res: &mut ResponseParts) { + for (key, value) in self { + res.insert_header(key, value); + } + } +} diff --git a/axum-core/src/response/mod.rs b/axum-core/src/response/mod.rs index f72b4708..d5bbf9a8 100644 --- a/axum-core/src/response/mod.rs +++ b/axum-core/src/response/mod.rs @@ -4,532 +4,16 @@ //! //! [`axum::response`]: https://docs.rs/axum/latest/axum/response/index.html -use crate::{ - body::{boxed, BoxBody}, - BoxError, -}; -use bytes::{buf::Chain, Buf, Bytes, BytesMut}; -use http::{ - header::{self, HeaderMap, HeaderName, HeaderValue}, - StatusCode, -}; -use http_body::{ - combinators::{MapData, MapErr}, - Empty, Full, SizeHint, -}; -use std::{ - borrow::Cow, - convert::Infallible, - iter, - pin::Pin, - task::{Context, Poll}, +use crate::body::BoxBody; + +mod into_response; +mod into_response_parts; + +pub use self::{ + into_response::IntoResponse, + into_response_parts::{IntoResponseParts, ResponseParts}, }; /// Type alias for [`http::Response`] whose body type defaults to [`BoxBody`], the most common body /// type used with axum. pub type Response = http::Response; - -/// Trait for generating responses. -/// -/// Types that implement `IntoResponse` can be returned from handlers. -/// -/// # Implementing `IntoResponse` -/// -/// You generally shouldn't have to implement `IntoResponse` manually, as axum -/// provides implementations for many common types. -/// -/// However it might be necessary if you have a custom error type that you want -/// to return from handlers: -/// -/// ```rust -/// use axum::{ -/// Router, -/// body::{self, Bytes}, -/// routing::get, -/// http::StatusCode, -/// response::{IntoResponse, Response}, -/// }; -/// -/// enum MyError { -/// SomethingWentWrong, -/// SomethingElseWentWrong, -/// } -/// -/// impl IntoResponse for MyError { -/// fn into_response(self) -> Response { -/// let body = match self { -/// MyError::SomethingWentWrong => { -/// body::boxed(body::Full::from("something went wrong")) -/// }, -/// MyError::SomethingElseWentWrong => { -/// body::boxed(body::Full::from("something else went wrong")) -/// }, -/// }; -/// -/// Response::builder() -/// .status(StatusCode::INTERNAL_SERVER_ERROR) -/// .body(body) -/// .unwrap() -/// } -/// } -/// -/// // `Result` can now be returned from handlers -/// let app = Router::new().route("/", get(handler)); -/// -/// async fn handler() -> Result<(), MyError> { -/// Err(MyError::SomethingWentWrong) -/// } -/// # async { -/// # hyper::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); -/// # }; -/// ``` -/// -/// Or if you have a custom body type you'll also need to implement -/// `IntoResponse` for it: -/// -/// ```rust -/// use axum::{ -/// body, -/// routing::get, -/// response::{IntoResponse, Response}, -/// Router, -/// }; -/// use http_body::Body; -/// use http::HeaderMap; -/// use bytes::Bytes; -/// use std::{ -/// convert::Infallible, -/// task::{Poll, Context}, -/// pin::Pin, -/// }; -/// -/// struct MyBody; -/// -/// // First implement `Body` for `MyBody`. This could for example use -/// // some custom streaming protocol. -/// impl Body for MyBody { -/// type Data = Bytes; -/// type Error = Infallible; -/// -/// fn poll_data( -/// self: Pin<&mut Self>, -/// cx: &mut Context<'_> -/// ) -> Poll>> { -/// # unimplemented!() -/// // ... -/// } -/// -/// fn poll_trailers( -/// self: Pin<&mut Self>, -/// cx: &mut Context<'_> -/// ) -> Poll, Self::Error>> { -/// # unimplemented!() -/// // ... -/// } -/// } -/// -/// // Now we can implement `IntoResponse` directly for `MyBody` -/// impl IntoResponse for MyBody { -/// fn into_response(self) -> Response { -/// Response::new(body::boxed(self)) -/// } -/// } -/// -/// // We don't need to implement `IntoResponse for Response` as that is -/// // covered by a blanket implementation in axum. -/// -/// // `MyBody` can now be returned from handlers. -/// let app = Router::new().route("/", get(|| async { MyBody })); -/// # async { -/// # hyper::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); -/// # }; -/// ``` -pub trait IntoResponse { - /// Create a response. - fn into_response(self) -> Response; -} - -/// Trait for generating response headers. -pub trait IntoResponseHeaders { - /// The return type of `into_headers`. - /// - /// The iterator item is a [`Result`] to allow the implementation to return a server error - /// instead. - /// - /// The header name is optional because [`HeaderMap`]s iterator doesn't yield it multiple times - /// for headers that have multiple values, to avoid unnecessary copies. - #[doc(hidden)] - type IntoIter: IntoIterator, HeaderValue), Response>>; - - /// Attempt to turn `self` into a list of headers. - /// - /// In practice, only the implementation for `axum::response::Headers` ever returns `Err(_)`. - #[doc(hidden)] - fn into_headers(self) -> Self::IntoIter; -} - -impl IntoResponse for () { - fn into_response(self) -> Response { - Response::new(boxed(Empty::new())) - } -} - -impl IntoResponse for Infallible { - fn into_response(self) -> Response { - match self {} - } -} - -impl IntoResponse for Result -where - T: IntoResponse, - E: IntoResponse, -{ - fn into_response(self) -> Response { - match self { - Ok(value) => value.into_response(), - Err(err) => err.into_response(), - } - } -} - -impl IntoResponse for Response -where - B: http_body::Body + Send + 'static, - B::Error: Into, -{ - fn into_response(self) -> Response { - self.map(boxed) - } -} - -macro_rules! impl_into_response_for_body { - ($body:ty) => { - impl IntoResponse for $body { - fn into_response(self) -> Response { - Response::new(boxed(self)) - } - } - }; -} - -impl_into_response_for_body!(Full); -impl_into_response_for_body!(Empty); - -impl IntoResponse for http::response::Parts { - fn into_response(self) -> Response { - Response::from_parts(self, boxed(Empty::new())) - } -} - -impl IntoResponse for http_body::combinators::BoxBody -where - E: Into + 'static, -{ - fn into_response(self) -> Response { - Response::new(boxed(self)) - } -} - -impl IntoResponse for http_body::combinators::UnsyncBoxBody -where - E: Into + 'static, -{ - fn into_response(self) -> Response { - Response::new(boxed(self)) - } -} - -impl IntoResponse for MapData -where - B: http_body::Body + Send + 'static, - F: FnMut(B::Data) -> Bytes + Send + 'static, - B::Error: Into, -{ - fn into_response(self) -> Response { - Response::new(boxed(self)) - } -} - -impl IntoResponse for MapErr -where - B: http_body::Body + Send + 'static, - F: FnMut(B::Error) -> E + Send + 'static, - E: Into, -{ - fn into_response(self) -> Response { - Response::new(boxed(self)) - } -} - -impl IntoResponse for &'static str { - #[inline] - fn into_response(self) -> Response { - Cow::Borrowed(self).into_response() - } -} - -impl IntoResponse for String { - #[inline] - fn into_response(self) -> Response { - Cow::<'static, str>::Owned(self).into_response() - } -} - -impl IntoResponse for Cow<'static, str> { - fn into_response(self) -> Response { - let mut res = Response::new(boxed(Full::from(self))); - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()), - ); - res - } -} - -impl IntoResponse for Bytes { - fn into_response(self) -> Response { - let mut res = Response::new(boxed(Full::from(self))); - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM.as_ref()), - ); - res - } -} - -impl IntoResponse for BytesMut { - fn into_response(self) -> Response { - self.freeze().into_response() - } -} - -impl IntoResponse for Chain -where - T: Buf + Unpin + Send + 'static, - U: Buf + Unpin + Send + 'static, -{ - fn into_response(self) -> Response { - let (first, second) = self.into_inner(); - let mut res = Response::new(boxed(BytesChainBody { - first: Some(first), - second: Some(second), - })); - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM.as_ref()), - ); - res - } -} - -struct BytesChainBody { - first: Option, - second: Option, -} - -impl http_body::Body for BytesChainBody -where - T: Buf + Unpin, - U: Buf + Unpin, -{ - type Data = Bytes; - type Error = Infallible; - - fn poll_data( - mut self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll>> { - if let Some(mut buf) = self.first.take() { - let bytes = buf.copy_to_bytes(buf.remaining()); - return Poll::Ready(Some(Ok(bytes))); - } - - if let Some(mut buf) = self.second.take() { - let bytes = buf.copy_to_bytes(buf.remaining()); - return Poll::Ready(Some(Ok(bytes))); - } - - Poll::Ready(None) - } - - fn poll_trailers( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll, Self::Error>> { - Poll::Ready(Ok(None)) - } - - fn is_end_stream(&self) -> bool { - self.first.is_none() && self.second.is_none() - } - - fn size_hint(&self) -> SizeHint { - match (self.first.as_ref(), self.second.as_ref()) { - (Some(first), Some(second)) => { - let total_size = first.remaining() + second.remaining(); - SizeHint::with_exact(total_size as u64) - } - (Some(buf), None) => SizeHint::with_exact(buf.remaining() as u64), - (None, Some(buf)) => SizeHint::with_exact(buf.remaining() as u64), - (None, None) => SizeHint::with_exact(0), - } - } -} - -impl IntoResponse for &'static [u8] { - fn into_response(self) -> Response { - let mut res = Response::new(boxed(Full::from(self))); - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM.as_ref()), - ); - res - } -} - -impl IntoResponse for Vec { - fn into_response(self) -> Response { - let mut res = Response::new(boxed(Full::from(self))); - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM.as_ref()), - ); - res - } -} - -impl IntoResponse for Cow<'static, [u8]> { - fn into_response(self) -> Response { - let mut res = Response::new(boxed(Full::from(self))); - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static(mime::APPLICATION_OCTET_STREAM.as_ref()), - ); - res - } -} - -impl IntoResponse for StatusCode { - fn into_response(self) -> Response { - Response::builder() - .status(self) - .body(boxed(Empty::new())) - .unwrap() - } -} - -impl IntoResponse for HeaderMap { - fn into_response(self) -> Response { - let mut res = Response::new(boxed(Empty::new())); - *res.headers_mut() = self; - res - } -} - -impl IntoResponse for (StatusCode, T) -where - T: IntoResponse, -{ - fn into_response(self) -> Response { - let mut res = self.1.into_response(); - *res.status_mut() = self.0; - res - } -} - -impl IntoResponse for (H, T) -where - H: IntoResponseHeaders, - T: IntoResponse, -{ - fn into_response(self) -> Response { - let mut res = self.1.into_response(); - - if let Err(e) = try_extend_headers(res.headers_mut(), self.0.into_headers()) { - return e; - } - - res - } -} - -impl IntoResponse for (StatusCode, H, T) -where - H: IntoResponseHeaders, - T: IntoResponse, -{ - fn into_response(self) -> Response { - let mut res = self.2.into_response(); - *res.status_mut() = self.0; - - if let Err(e) = try_extend_headers(res.headers_mut(), self.1.into_headers()) { - return e; - } - - res - } -} - -impl IntoResponseHeaders for HeaderMap { - // FIXME: Use type_alias_impl_trait when available - type IntoIter = iter::Map< - http::header::IntoIter, - fn( - (Option, HeaderValue), - ) -> Result<(Option, HeaderValue), Response>, - >; - - fn into_headers(self) -> Self::IntoIter { - self.into_iter().map(Ok) - } -} - -// Slightly adjusted version of `impl Extend<(Option, T)> for HeaderMap`. -// Accepts an iterator that returns Results and short-circuits on an `Err`. -fn try_extend_headers( - headers: &mut HeaderMap, - iter: impl IntoIterator, HeaderValue), Response>>, -) -> Result<(), Response> { - use http::header::Entry; - - let mut iter = iter.into_iter(); - - // The structure of this is a bit weird, but it is mostly to make the - // borrow checker happy. - let (mut key, mut val) = match iter.next().transpose()? { - Some((Some(key), val)) => (key, val), - Some((None, _)) => panic!("expected a header name, but got None"), - None => return Ok(()), - }; - - 'outer: loop { - let mut entry = match headers.entry(key) { - Entry::Occupied(mut e) => { - // Replace all previous values while maintaining a handle to - // the entry. - e.insert(val); - e - } - Entry::Vacant(e) => e.insert_entry(val), - }; - - // As long as `HeaderName` is none, keep inserting the value into - // the current entry - loop { - match iter.next().transpose()? { - Some((Some(k), v)) => { - key = k; - val = v; - continue 'outer; - } - Some((None, v)) => { - entry.append(v); - } - None => { - return Ok(()); - } - } - } - } -} diff --git a/axum-extra/src/response/erased_json.rs b/axum-extra/src/response/erased_json.rs index 4a83cf30..98c791a7 100644 --- a/axum-extra/src/response/erased_json.rs +++ b/axum-extra/src/response/erased_json.rs @@ -1,5 +1,4 @@ use axum::{ - body::{self, Full}, http::{header, HeaderValue, StatusCode}, response::{IntoResponse, Response}, }; @@ -49,22 +48,16 @@ impl ErasedJson { impl IntoResponse for ErasedJson { fn into_response(self) -> Response { - let bytes = match self.0 { - Ok(res) => res, - Err(err) => { - return Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .header(header::CONTENT_TYPE, mime::TEXT_PLAIN_UTF_8.as_ref()) - .body(body::boxed(Full::from(err.to_string()))) - .unwrap(); - } - }; - - let mut res = Response::new(body::boxed(Full::new(bytes))); - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()), - ); - res + match self.0 { + Ok(bytes) => ( + [( + header::CONTENT_TYPE, + HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()), + )], + bytes, + ) + .into_response(), + Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(), + } } } diff --git a/axum/CHANGELOG.md b/axum/CHANGELOG.md index ad9078eb..07d4ae76 100644 --- a/axum/CHANGELOG.md +++ b/axum/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **added:** `middleware::from_fn` for creating middleware from async functions. This previously lived in axum-extra but has been moved to axum ([#719]) - **added:** Document sharing state between handler and middleware (#783]) +- **added:** `Extension<_>` can now be used in tuples for building responses, and will set an + extension on the response ([#797]) - **breaking:** `sse::Event` now accepts types implementing `AsRef` instead of `Into` as field values. - **breaking:** `sse::Event` now panics if a setter method is called twice instead of silently @@ -69,6 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#755]: https://github.com/tokio-rs/axum/pull/755 [#783]: https://github.com/tokio-rs/axum/pull/783 [#791]: https://github.com/tokio-rs/axum/pull/791 +[#797]: https://github.com/tokio-rs/axum/pull/797 # 0.4.4 (13. January, 2022) diff --git a/axum/src/docs/response.md b/axum/src/docs/response.md index 4bb0959f..d0379b07 100644 --- a/axum/src/docs/response.md +++ b/axum/src/docs/response.md @@ -2,27 +2,21 @@ Types and traits for generating responses. # Building responses -Anything that implements [`IntoResponse`] can be returned from a handler: +Anything that implements [`IntoResponse`] can be returned from a handler. axum +provides implementations for common types: ```rust,no_run use axum::{ - body::Body, - routing::get, - handler::Handler, - http::{Request, header::{HeaderMap, HeaderName, HeaderValue}}, - response::{IntoResponse, Html, Json, Headers}, - Router, + Json, + response::{Html, IntoResponse}, + http::{StatusCode, Uri, header::{self, HeaderMap, HeaderName}}, }; -use http::{StatusCode, Response, Uri}; -use serde_json::{Value, json}; -// We've already seen returning &'static str -async fn plain_text() -> &'static str { - "foo" -} +// `()` gives an empty response +async fn empty() {} -// String works too and will get a `text/plain; charset=utf-8` content-type -async fn plain_text_string(uri: Uri) -> String { +// String will get a `text/plain; charset=utf-8` content-type +async fn plain_text(uri: Uri) -> String { format!("Hi from {}", uri.path()) } @@ -31,84 +25,158 @@ async fn bytes() -> Vec { vec![1, 2, 3, 4] } -// `()` gives an empty response -async fn empty() {} +// `Json` will get a `application/json` content-type and work with anything that +// implements `serde::Serialize` +async fn json() -> Json> { + Json(vec!["foo".to_owned(), "bar".to_owned()]) +} + +// `Html` will get a `text/html` content-type +async fn html() -> Html<&'static str> { + Html("

Hello, World!

") +} // `StatusCode` gives an empty response with that status code -async fn empty_with_status() -> StatusCode { +async fn status() -> StatusCode { StatusCode::NOT_FOUND } -// A tuple of `StatusCode` and something that implements `IntoResponse` can -// be used to override the status code -async fn with_status() -> (StatusCode, &'static str) { - (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong") +// `HeaderMap` gives an empty response with some headers +async fn headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert(header::SERVER, "axum".parse().unwrap()); + headers } -// A tuple of `HeaderMap` and something that implements `IntoResponse` can -// be used to override the headers -async fn with_headers() -> (HeaderMap, &'static str) { +// An array of tuples also gives headers +async fn array_headers() -> [(HeaderName, &'static str); 2] { + [ + (header::SERVER, "axum"), + (header::CONTENT_TYPE, "text/plain") + ] +} + +// Use `impl IntoResponse` to avoid writing the whole type +async fn impl_trait() -> impl IntoResponse { + [ + (header::SERVER, "axum"), + (header::CONTENT_TYPE, "text/plain") + ] +} +``` + +Additionally you can return tuples to build more complex responses from +individual parts. + +```rust,no_run +use axum::{ + Json, + response::IntoResponse, + http::{StatusCode, HeaderMap, Uri, header}, + extract::Extension, +}; + +// `(StatusCode, impl IntoResponse)` will override the status code of the response +async fn with_status(uri: Uri) -> (StatusCode, String) { + (StatusCode::NOT_FOUND, format!("Not Found: {}", uri.path())) +} + +// Use `impl IntoResponse` to avoid having to type the whole type +async fn impl_trait(uri: Uri) -> impl IntoResponse { + (StatusCode::NOT_FOUND, format!("Not Found: {}", uri.path())) +} + +// `(HeaderMap, impl IntoResponse)` to add additional headers +async fn with_headers() -> impl IntoResponse { let mut headers = HeaderMap::new(); - headers.insert( - HeaderName::from_static("x-foo"), - HeaderValue::from_static("foo"), - ); + headers.insert(header::CONTENT_TYPE, "text/plain".parse().unwrap()); (headers, "foo") } -// You can also override both status and headers at the same time -async fn with_headers_and_status() -> (StatusCode, HeaderMap, &'static str) { - let mut headers = HeaderMap::new(); - headers.insert( - HeaderName::from_static("x-foo"), - HeaderValue::from_static("foo"), - ); - (StatusCode::INTERNAL_SERVER_ERROR, headers, "foo") +// Or an array of tuples to more easily build the headers +async fn with_array_headers() -> impl IntoResponse { + ([(header::CONTENT_TYPE, "text/plain")], "foo") } -// `Headers` makes building the header map easier and `impl Trait` is easier -// so you don't have to write the whole type -async fn with_easy_headers() -> impl IntoResponse { - Headers(vec![("x-foo", "foo")]) +// Use string keys for custom headers +async fn with_array_headers_custom() -> impl IntoResponse { + ([("x-custom", "custom")], "foo") } -// `Html` gives a content-type of `text/html` -async fn html() -> Html<&'static str> { - Html("

Hello, World!

") +// `(StatusCode, headers, impl IntoResponse)` to set status and add headers +// `headers` can be either a `HeaderMap` or an array of tuples +async fn with_status_and_array_headers() -> impl IntoResponse { + ( + StatusCode::NOT_FOUND, + [(header::CONTENT_TYPE, "text/plain")], + "foo", + ) } -// `Json` gives a content-type of `application/json` and works with any type -// that implements `serde::Serialize` -async fn json() -> Json { - Json(json!({ "data": 42 })) +// `(Extension<_>, impl IntoResponse)` to set response extensions +async fn with_status_extensions() -> impl IntoResponse { + ( + Extension(Foo("foo")), + "foo", + ) } -// `Result` where `T` and `E` implement `IntoResponse` is useful for -// returning errors -async fn result() -> Result<&'static str, StatusCode> { - Ok("all good") -} +struct Foo(&'static str); -// `Response` gives full control -async fn response() -> Response { - Response::builder().body(Body::empty()).unwrap() -} +// Or mix and match all the things +async fn all_the_things(uri: Uri) -> impl IntoResponse { + let mut header_map = HeaderMap::new(); + if uri.path() == "/" { + header_map.insert(header::SERVER, "axum".parse().unwrap()); + } -let app = Router::new() - .route("/plain_text", get(plain_text)) - .route("/plain_text_string", get(plain_text_string)) - .route("/bytes", get(bytes)) - .route("/empty", get(empty)) - .route("/empty_with_status", get(empty_with_status)) - .route("/with_status", get(with_status)) - .route("/with_headers", get(with_headers)) - .route("/with_headers_and_status", get(with_headers_and_status)) - .route("/with_easy_headers", get(with_easy_headers)) - .route("/html", get(html)) - .route("/json", get(json)) - .route("/result", get(result)) - .route("/response", get(response)); -# async { -# axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); -# }; + ( + // set status code + StatusCode::NOT_FOUND, + // headers ith an array + [("x-custom", "custom")], + // some extensions + Extension(Foo("foo")), + Extension(Foo("bar")), + // more headers, built dynamically + header_map, + // and finally the body + "foo", + ) +} ``` + +In general you can return tuples like: + +- `(StatusCode, impl IntoResponse)` +- `(Version, impl IntoResponse)` +- `(StatusCode, Version, impl IntoResponse)` +- `(T1, .., Tn, impl IntoResponse)` where `T1` to `Tn` all implement [`IntoResponseParts`]. +- `(StatusCode, T1, .., Tn, impl IntoResponse)` where `T1` to `Tn` all implement [`IntoResponseParts`]. +- `(StatusCode, Version, T1, .., Tn, impl IntoResponse)` where `T1` to `Tn` all implement [`IntoResponseParts`]. + +This means you cannot accidentally override the status, version, or body, as [`IntoResponseParts`] only allows +setting headers and extensions. + +Use [`Response`](crate::response::Response) for more low level control: + +```rust,no_run +use axum::{ + Json, + response::{IntoResponse, Response}, + body::Full, + http::StatusCode, +}; + +async fn response() -> impl IntoResponse { + Response::builder() + .status(StatusCode::NOT_FOUND) + .header("x-foo", "custom header") + .body(Full::from("not found")) + .unwrap() +} +``` + +[`IntoResponse`]: crate::response::IntoResponse +[`IntoResponseParts`]: crate::response::IntoResponseParts +[`StatusCode`]: http::StatusCode diff --git a/axum/src/extract/content_length_limit.rs b/axum/src/extract/content_length_limit.rs index 7a41d254..afdb7247 100644 --- a/axum/src/extract/content_length_limit.rs +++ b/axum/src/extract/content_length_limit.rs @@ -1,7 +1,6 @@ -use crate::response::IntoResponse; - use super::{rejection::*, FromRequest, RequestParts}; use async_trait::async_trait; +use axum_core::response::IntoResponse; use std::ops::Deref; /// Extractor that will reject requests with a body larger than some size. diff --git a/axum/src/extract/extension.rs b/axum/src/extract/extension.rs index cb03e024..34fc297f 100644 --- a/axum/src/extract/extension.rs +++ b/axum/src/extract/extension.rs @@ -1,5 +1,7 @@ use super::{rejection::*, FromRequest, RequestParts}; +use crate::response::IntoResponseParts; use async_trait::async_trait; +use axum_core::response::{IntoResponse, Response, ResponseParts}; use std::ops::Deref; /// Extractor that gets a value from request extensions. @@ -73,3 +75,23 @@ impl Deref for Extension { &self.0 } } + +impl IntoResponseParts for Extension +where + T: Send + Sync + 'static, +{ + fn into_response_parts(self, res: &mut ResponseParts) { + res.insert_extension(self.0); + } +} + +impl IntoResponse for Extension +where + T: Send + Sync + 'static, +{ + fn into_response(self) -> Response { + let mut res = ().into_response(); + res.extensions_mut().insert(self.0); + res + } +} diff --git a/axum/src/extract/path/mod.rs b/axum/src/extract/path/mod.rs index ba2c2509..177da224 100644 --- a/axum/src/extract/path/mod.rs +++ b/axum/src/extract/path/mod.rs @@ -4,12 +4,11 @@ mod de; use crate::{ - body::{boxed, Full}, extract::{rejection::*, FromRequest, RequestParts}, - response::{IntoResponse, Response}, routing::{InvalidUtf8InPathParam, UrlParams}, }; use async_trait::async_trait; +use axum_core::response::{IntoResponse, Response}; use http::StatusCode; use serde::de::DeserializeOwned; use std::{ @@ -382,9 +381,7 @@ impl IntoResponse for FailedToDeserializePathParams { (StatusCode::INTERNAL_SERVER_ERROR, self.0.kind.to_string()) } }; - let mut res = Response::new(boxed(Full::from(body))); - *res.status_mut() = status; - res + (status, body).into_response() } } diff --git a/axum/src/extract/rejection.rs b/axum/src/extract/rejection.rs index 02e3b31a..516260a0 100644 --- a/axum/src/extract/rejection.rs +++ b/axum/src/extract/rejection.rs @@ -1,10 +1,7 @@ //! Rejection response types. -use crate::{ - body::{boxed, Full}, - response::{IntoResponse, Response}, - BoxError, Error, -}; +use crate::{BoxError, Error}; +use axum_core::response::{IntoResponse, Response}; pub use crate::extract::path::FailedToDeserializePathParams; pub use axum_core::extract::rejection::*; @@ -90,9 +87,7 @@ impl FailedToDeserializeQueryString { impl IntoResponse for FailedToDeserializeQueryString { fn into_response(self) -> Response { - let mut res = Response::new(boxed(Full::from(self.to_string()))); - *res.status_mut() = http::StatusCode::UNPROCESSABLE_ENTITY; - res + (http::StatusCode::UNPROCESSABLE_ENTITY, self.to_string()).into_response() } } diff --git a/axum/src/extract/typed_header.rs b/axum/src/extract/typed_header.rs index 85c9d557..d710b184 100644 --- a/axum/src/extract/typed_header.rs +++ b/axum/src/extract/typed_header.rs @@ -1,7 +1,8 @@ use super::{FromRequest, RequestParts}; -use crate::response::{IntoResponse, Response}; use async_trait::async_trait; +use axum_core::response::{IntoResponse, IntoResponseParts, Response, ResponseParts}; use headers::HeaderMapExt; +use http::header::{HeaderName, HeaderValue}; use std::ops::Deref; /// Extractor that extracts a typed header value from [`headers`]. @@ -65,6 +66,47 @@ impl Deref for TypedHeader { } } +impl IntoResponseParts for TypedHeader +where + T: headers::Header, +{ + fn into_response_parts(self, res: &mut ResponseParts) { + struct ExtendHeaders<'a> { + res: &'a mut ResponseParts, + key: &'static HeaderName, + } + + impl<'a> Extend for ExtendHeaders<'a> { + fn extend(&mut self, iter: T) + where + T: IntoIterator, + { + for value in iter { + self.res.append_header(self.key, value); + } + } + } + + let mut extend = ExtendHeaders { + res, + key: T::name(), + }; + + self.0.encode(&mut extend); + } +} + +impl IntoResponse for TypedHeader +where + T: headers::Header, +{ + fn into_response(self) -> Response { + let mut res = ().into_response(); + res.headers_mut().typed_insert(self.0); + res + } +} + /// Rejection used for [`TypedHeader`](super::TypedHeader). #[cfg(feature = "headers")] #[derive(Debug)] @@ -97,9 +139,7 @@ pub enum TypedHeaderRejectionReason { impl IntoResponse for TypedHeaderRejection { fn into_response(self) -> Response { - let mut res = self.to_string().into_response(); - *res.status_mut() = http::StatusCode::BAD_REQUEST; - res + (http::StatusCode::BAD_REQUEST, self.to_string()).into_response() } } diff --git a/axum/src/json.rs b/axum/src/json.rs index 2460e194..c1e6c8f9 100644 --- a/axum/src/json.rs +++ b/axum/src/json.rs @@ -1,10 +1,10 @@ use crate::{ - body::{self, Bytes, Full, HttpBody}, + body::{Bytes, HttpBody}, extract::{rejection::*, FromRequest, RequestParts}, - response::{IntoResponse, Response}, BoxError, }; use async_trait::async_trait; +use axum_core::response::{IntoResponse, Response}; use http::{ header::{self, HeaderValue}, StatusCode, @@ -158,26 +158,25 @@ where T: Serialize, { fn into_response(self) -> Response { - let bytes = match serde_json::to_vec(&self.0) { - Ok(res) => res, - Err(err) => { - return Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .header( - header::CONTENT_TYPE, - HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()), - ) - .body(body::boxed(Full::from(err.to_string()))) - .unwrap(); - } - }; - - let mut res = Response::new(body::boxed(Full::from(bytes))); - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()), - ); - res + match serde_json::to_vec(&self.0) { + Ok(bytes) => ( + [( + header::CONTENT_TYPE, + HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()), + )], + bytes, + ) + .into_response(), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [( + header::CONTENT_TYPE, + HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()), + )], + err.to_string(), + ) + .into_response(), + } } } diff --git a/axum/src/macros.rs b/axum/src/macros.rs index 9e8fd3b2..cc5592d5 100644 --- a/axum/src/macros.rs +++ b/axum/src/macros.rs @@ -60,9 +60,7 @@ macro_rules! define_rejection { #[allow(deprecated)] impl $crate::response::IntoResponse for $name { fn into_response(self) -> $crate::response::Response { - let mut res = http::Response::new($crate::body::boxed(crate::body::Full::from($body))); - *res.status_mut() = http::StatusCode::$status; - res + (http::StatusCode::$status, $body).into_response() } } @@ -100,12 +98,12 @@ macro_rules! define_rejection { } } - impl IntoResponse for $name { + impl crate::response::IntoResponse for $name { fn into_response(self) -> $crate::response::Response { - let mut res = - http::Response::new($crate::body::boxed(crate::body::Full::from(format!(concat!($body, ": {}"), self.0)))); - *res.status_mut() = http::StatusCode::$status; - res + ( + http::StatusCode::$status, + format!(concat!($body, ": {}"), self.0), + ).into_response() } } diff --git a/axum/src/response/headers.rs b/axum/src/response/headers.rs deleted file mode 100644 index c12c532e..00000000 --- a/axum/src/response/headers.rs +++ /dev/null @@ -1,181 +0,0 @@ -use super::{IntoResponse, IntoResponseHeaders, Response}; -use http::{ - header::{HeaderName, HeaderValue}, - StatusCode, -}; -use std::{convert::TryInto, fmt}; - -/// A response with headers. -/// -/// # Example -/// -/// ```rust -/// use axum::{ -/// Router, -/// response::{IntoResponse, Headers}, -/// routing::get, -/// }; -/// use http::header::{HeaderName, HeaderValue}; -/// -/// // It works with any `IntoIterator` where `Key` can be -/// // turned into a `HeaderName` and `Value` can be turned into a `HeaderValue` -/// // -/// // Such as `Vec<(HeaderName, HeaderValue)>` -/// async fn just_headers() -> impl IntoResponse { -/// Headers(vec![ -/// (HeaderName::from_static("X-Foo"), HeaderValue::from_static("foo")), -/// ]) -/// } -/// -/// // Or `Vec<(&str, &str)>` -/// async fn from_strings() -> impl IntoResponse { -/// Headers(vec![("X-Foo", "foo")]) -/// } -/// -/// // Or `[(&str, &str)]` if you're on Rust 1.53+ -/// -/// let app = Router::new() -/// .route("/just-headers", get(just_headers)) -/// .route("/from-strings", get(from_strings)); -/// # async { -/// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); -/// # }; -/// ``` -/// -/// If a conversion to `HeaderName` or `HeaderValue` fails a `500 Internal -/// Server Error` response will be returned. -/// -/// You can also return `(Headers, impl IntoResponse)` to customize the headers -/// of a response, or `(StatusCode, Headers, impl IntoResponse)` to customize -/// the status code and headers. -#[derive(Clone, Copy, Debug)] -pub struct Headers(pub H); - -impl IntoResponseHeaders for Headers -where - H: IntoIterator, - K: TryInto, - K::Error: fmt::Display, - V: TryInto, - V::Error: fmt::Display, -{ - type IntoIter = IntoIter; - - fn into_headers(self) -> Self::IntoIter { - IntoIter { - inner: self.0.into_iter(), - } - } -} - -impl IntoResponse for Headers -where - H: IntoIterator, - K: TryInto, - K::Error: fmt::Display, - V: TryInto, - V::Error: fmt::Display, -{ - fn into_response(self) -> Response { - (self, ()).into_response() - } -} - -#[doc(hidden)] -#[derive(Debug)] -pub struct IntoIter { - inner: H, -} - -impl Iterator for IntoIter -where - H: Iterator, - K: TryInto, - K::Error: fmt::Display, - V: TryInto, - V::Error: fmt::Display, -{ - type Item = Result<(Option, HeaderValue), Response>; - - fn next(&mut self) -> Option { - self.inner.next().map(|(key, value)| { - let key = key - .try_into() - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response())?; - let value = value - .try_into() - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response())?; - - Ok((Some(key), value)) - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use futures_util::FutureExt; - use http::header::USER_AGENT; - - #[test] - fn vec_of_header_name_and_value() { - let res = Headers(vec![(USER_AGENT, HeaderValue::from_static("axum"))]).into_response(); - - assert_eq!(res.headers()["user-agent"], "axum"); - assert_eq!(res.status(), StatusCode::OK); - } - - #[test] - fn vec_of_strings() { - let res = Headers(vec![("user-agent", "axum")]).into_response(); - - assert_eq!(res.headers()["user-agent"], "axum"); - } - - #[test] - fn with_body() { - let res = (Headers(vec![("user-agent", "axum")]), "foo").into_response(); - - assert_eq!(res.headers()["user-agent"], "axum"); - let body = hyper::body::to_bytes(res.into_body()) - .now_or_never() - .unwrap() - .unwrap(); - assert_eq!(&body[..], b"foo"); - } - - #[test] - fn with_status_and_body() { - let res = ( - StatusCode::NOT_FOUND, - Headers(vec![("user-agent", "axum")]), - "foo", - ) - .into_response(); - - assert_eq!(res.headers()["user-agent"], "axum"); - assert_eq!(res.status(), StatusCode::NOT_FOUND); - let body = hyper::body::to_bytes(res.into_body()) - .now_or_never() - .unwrap() - .unwrap(); - assert_eq!(&body[..], b"foo"); - } - - #[test] - fn invalid_header_name() { - let bytes: &[u8] = &[0, 159, 146, 150]; // invalid utf-8 - let res = Headers(vec![(bytes, "axum")]).into_response(); - - assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[test] - fn invalid_header_value() { - let bytes: &[u8] = &[0, 159, 146, 150]; // invalid utf-8 - let res = Headers(vec![("user-agent", bytes)]).into_response(); - - assert!(res.headers().get("user-agent").is_none()); - assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); - } -} diff --git a/axum/src/response/mod.rs b/axum/src/response/mod.rs index 7d6540b5..db5a20ea 100644 --- a/axum/src/response/mod.rs +++ b/axum/src/response/mod.rs @@ -1,10 +1,8 @@ #![doc = include_str!("../docs/response.md")] use crate::body::{Bytes, Full}; -use axum_core::body::boxed; use http::{header, HeaderValue}; -mod headers; mod redirect; pub mod sse; @@ -14,10 +12,10 @@ pub mod sse; pub use crate::Json; #[doc(inline)] -pub use axum_core::response::{IntoResponse, IntoResponseHeaders, Response}; +pub use axum_core::response::{IntoResponse, IntoResponseParts, Response, ResponseParts}; #[doc(inline)] -pub use self::{headers::Headers, redirect::Redirect, sse::Sse}; +pub use self::{redirect::Redirect, sse::Sse}; /// An HTML response. /// @@ -30,12 +28,14 @@ where T: Into>, { fn into_response(self) -> Response { - let mut res = Response::new(boxed(self.0.into())); - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static(mime::TEXT_HTML_UTF_8.as_ref()), - ); - res + ( + [( + header::CONTENT_TYPE, + HeaderValue::from_static(mime::TEXT_HTML_UTF_8.as_ref()), + )], + self.0.into(), + ) + .into_response() } } @@ -47,43 +47,166 @@ impl From for Html { #[cfg(test)] mod tests { - use super::*; - use crate::body::Empty; - use http::{ - header::{HeaderMap, HeaderName}, - StatusCode, - }; + use crate::extract::Extension; + use crate::{body::Body, routing::get, Router}; + use axum_core::response::IntoResponse; + use http::HeaderMap; + use http::{StatusCode, Uri}; - #[test] - fn test_merge_headers() { - struct MyResponse; + // just needs to compile + #[allow(dead_code)] + fn impl_trait_result_works() { + async fn impl_trait_ok() -> Result { + Ok(()) + } - impl IntoResponse for MyResponse { - fn into_response(self) -> Response { - let mut resp = Response::new(boxed(Empty::new())); - resp.headers_mut() - .insert(HeaderName::from_static("a"), HeaderValue::from_static("1")); - resp + async fn impl_trait_err() -> Result<(), impl IntoResponse> { + Err(()) + } + + async fn impl_trait_both(uri: Uri) -> Result { + if uri.path() == "/" { + Ok(()) + } else { + Err(()) } } - fn check(resp: impl IntoResponse) { - let resp = resp.into_response(); - assert_eq!( - resp.headers().get(HeaderName::from_static("a")).unwrap(), - &HeaderValue::from_static("1") - ); - assert_eq!( - resp.headers().get(HeaderName::from_static("b")).unwrap(), - &HeaderValue::from_static("2") - ); + async fn impl_trait(uri: Uri) -> impl IntoResponse { + if uri.path() == "/" { + Ok(()) + } else { + Err(()) + } } - let headers: HeaderMap = - std::iter::once((HeaderName::from_static("b"), HeaderValue::from_static("2"))) - .collect(); + Router::::new() + .route("/", get(impl_trait_ok)) + .route("/", get(impl_trait_err)) + .route("/", get(impl_trait_both)) + .route("/", get(impl_trait)); + } - check((headers.clone(), MyResponse)); - check((StatusCode::OK, headers, MyResponse)); + // just needs to compile + #[allow(dead_code)] + fn tuple_responses() { + async fn status() -> impl IntoResponse { + StatusCode::OK + } + + async fn status_headermap() -> impl IntoResponse { + (StatusCode::OK, HeaderMap::new()) + } + + async fn status_header_array() -> impl IntoResponse { + (StatusCode::OK, [("content-type", "text/plain")]) + } + + async fn status_headermap_body() -> impl IntoResponse { + (StatusCode::OK, HeaderMap::new(), String::new()) + } + + async fn status_header_array_body() -> impl IntoResponse { + ( + StatusCode::OK, + [("content-type", "text/plain")], + String::new(), + ) + } + + async fn status_headermap_impl_into_response() -> impl IntoResponse { + (StatusCode::OK, HeaderMap::new(), impl_into_response()) + } + + async fn status_header_array_impl_into_response() -> impl IntoResponse { + ( + StatusCode::OK, + [("content-type", "text/plain")], + impl_into_response(), + ) + } + + fn impl_into_response() -> impl IntoResponse {} + + async fn status_header_array_extension_body() -> impl IntoResponse { + ( + StatusCode::OK, + [("content-type", "text/plain")], + Extension(1), + String::new(), + ) + } + + async fn status_header_array_extension_mixed_body() -> impl IntoResponse { + ( + StatusCode::OK, + [("content-type", "text/plain")], + Extension(1), + HeaderMap::new(), + String::new(), + ) + } + + // + + async fn headermap() -> impl IntoResponse { + HeaderMap::new() + } + + async fn header_array() -> impl IntoResponse { + [("content-type", "text/plain")] + } + + async fn headermap_body() -> impl IntoResponse { + (HeaderMap::new(), String::new()) + } + + async fn header_array_body() -> impl IntoResponse { + ([("content-type", "text/plain")], String::new()) + } + + async fn headermap_impl_into_response() -> impl IntoResponse { + (HeaderMap::new(), impl_into_response()) + } + + async fn header_array_impl_into_response() -> impl IntoResponse { + ([("content-type", "text/plain")], impl_into_response()) + } + + async fn header_array_extension_body() -> impl IntoResponse { + ( + [("content-type", "text/plain")], + Extension(1), + String::new(), + ) + } + + async fn header_array_extension_mixed_body() -> impl IntoResponse { + ( + [("content-type", "text/plain")], + Extension(1), + HeaderMap::new(), + String::new(), + ) + } + + Router::::new() + .route("/", get(status)) + .route("/", get(status_headermap)) + .route("/", get(status_header_array)) + .route("/", get(status_headermap_body)) + .route("/", get(status_header_array_body)) + .route("/", get(status_headermap_impl_into_response)) + .route("/", get(status_header_array_impl_into_response)) + .route("/", get(status_header_array_extension_body)) + .route("/", get(status_header_array_extension_mixed_body)) + .route("/", get(headermap)) + .route("/", get(header_array)) + .route("/", get(headermap_body)) + .route("/", get(header_array_body)) + .route("/", get(headermap_impl_into_response)) + .route("/", get(header_array_impl_into_response)) + .route("/", get(header_array_extension_body)) + .route("/", get(header_array_extension_mixed_body)); } } diff --git a/axum/src/response/redirect.rs b/axum/src/response/redirect.rs index c79bf4c3..a61cf674 100644 --- a/axum/src/response/redirect.rs +++ b/axum/src/response/redirect.rs @@ -1,5 +1,4 @@ -use super::{IntoResponse, Response}; -use crate::body::{boxed, Empty}; +use axum_core::response::{IntoResponse, Response}; use http::{header::LOCATION, HeaderValue, StatusCode, Uri}; use std::convert::TryFrom; @@ -111,9 +110,6 @@ impl Redirect { impl IntoResponse for Redirect { fn into_response(self) -> Response { - let mut res = Response::new(boxed(Empty::new())); - *res.status_mut() = self.status_code; - res.headers_mut().insert(LOCATION, self.location); - res + (self.status_code, [(LOCATION, self.location)]).into_response() } } diff --git a/axum/src/response/sse.rs b/axum/src/response/sse.rs index 692d30f1..9013d4f7 100644 --- a/axum/src/response/sse.rs +++ b/axum/src/response/sse.rs @@ -28,10 +28,13 @@ //! ``` use crate::{ - body::{self, Bytes, HttpBody}, - response::{IntoResponse, Response}, + body::{Bytes, HttpBody}, BoxError, }; +use axum_core::{ + body, + response::{IntoResponse, Response}, +}; use bytes::{BufMut, BytesMut}; use futures_util::{ ready, @@ -95,16 +98,17 @@ where E: Into, { fn into_response(self) -> Response { - let body = body::boxed(Body { - event_stream: SyncWrapper::new(self.stream), - keep_alive: self.keep_alive.map(KeepAliveStream::new), - }); - - Response::builder() - .header(http::header::CONTENT_TYPE, mime::TEXT_EVENT_STREAM.as_ref()) - .header(http::header::CACHE_CONTROL, "no-cache") - .body(body) - .unwrap() + ( + [ + (http::header::CONTENT_TYPE, mime::TEXT_EVENT_STREAM.as_ref()), + (http::header::CACHE_CONTROL, "no-cache"), + ], + body::boxed(Body { + event_stream: SyncWrapper::new(self.stream), + keep_alive: self.keep_alive.map(KeepAliveStream::new), + }), + ) + .into_response() } } diff --git a/axum/src/routing/method_routing.rs b/axum/src/routing/method_routing.rs index 5ddc838c..06476dd0 100644 --- a/axum/src/routing/method_routing.rs +++ b/axum/src/routing/method_routing.rs @@ -1202,10 +1202,9 @@ mod tests { if method == Method::POST { "OK".into_response() } else { - let headers = crate::response::Headers([(ALLOW, "GET,POST")]); ( StatusCode::METHOD_NOT_ALLOWED, - headers, + [(ALLOW, "GET,POST")], "Method not allowed", ) .into_response() diff --git a/examples/templates/src/main.rs b/examples/templates/src/main.rs index a3395fb8..505e660b 100644 --- a/examples/templates/src/main.rs +++ b/examples/templates/src/main.rs @@ -6,7 +6,6 @@ use askama::Template; use axum::{ - body::{self, Full}, extract, http::StatusCode, response::{Html, IntoResponse, Response}, @@ -55,13 +54,11 @@ where fn into_response(self) -> Response { match self.0.render() { Ok(html) => Html(html).into_response(), - Err(err) => Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(body::boxed(Full::from(format!( - "Failed to render template. Error: {}", - err - )))) - .unwrap(), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to render template. Error: {}", err), + ) + .into_response(), } } }