diff --git a/CHANGELOG.md b/CHANGELOG.md index a18fca13..9b2bd993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Breaking changes +- Add associated `Body` and `BodyError` types to `IntoResponse`. This is + required for returning responses with bodies other than `hyper::Body` from + handlers. See the docs for advice on how to implement `IntoResponse` ([#86](https://github.com/tokio-rs/axum/pull/86)) - Change WebSocket API to use an extractor ([#121](https://github.com/tokio-rs/axum/pull/121)) - Add `RoutingDsl::or` for combining routes. ([#108](https://github.com/tokio-rs/axum/pull/108)) - Ensure a `HandleError` service created from `axum::ServiceExt::handle_error` diff --git a/examples/error_handling_and_dependency_injection.rs b/examples/error_handling_and_dependency_injection.rs index e354f402..8fbbb88e 100644 --- a/examples/error_handling_and_dependency_injection.rs +++ b/examples/error_handling_and_dependency_injection.rs @@ -16,10 +16,12 @@ use axum::{ response::IntoResponse, AddExtensionLayer, }; -use http::StatusCode; +use bytes::Bytes; +use http::{Response, StatusCode}; +use http_body::Full; use serde::{Deserialize, Serialize}; use serde_json::json; -use std::{net::SocketAddr, sync::Arc}; +use std::{convert::Infallible, net::SocketAddr, sync::Arc}; use uuid::Uuid; #[tokio::main] @@ -89,7 +91,10 @@ impl From for AppError { } impl IntoResponse for AppError { - fn into_response(self) -> http::Response { + type Body = Full; + type BodyError = Infallible; + + fn into_response(self) -> Response { let (status, error_json) = match self { AppError::UserRepo(UserRepoError::NotFound) => { (StatusCode::NOT_FOUND, json!("User not found")) diff --git a/examples/templates.rs b/examples/templates.rs index 4f3bda27..0b7f2b48 100644 --- a/examples/templates.rs +++ b/examples/templates.rs @@ -6,8 +6,10 @@ use askama::Template; use axum::{prelude::*, response::IntoResponse}; +use bytes::Bytes; use http::{Response, StatusCode}; -use std::net::SocketAddr; +use http_body::Full; +use std::{convert::Infallible, net::SocketAddr}; #[tokio::main] async fn main() { @@ -46,12 +48,15 @@ impl IntoResponse for HtmlTemplate where T: Template, { - fn into_response(self) -> http::Response { + type Body = Full; + type BodyError = Infallible; + + fn into_response(self) -> Response { match self.0.render() { Ok(html) => response::Html(html).into_response(), Err(err) => Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from(format!( + .body(Full::from(format!( "Failed to render template. Error: {}", err ))) diff --git a/examples/versioning.rs b/examples/versioning.rs index f87c3955..67239280 100644 --- a/examples/versioning.rs +++ b/examples/versioning.rs @@ -10,8 +10,10 @@ use axum::{ extract::{FromRequest, RequestParts}, prelude::*, }; +use bytes::Bytes; use http::Response; use http::StatusCode; +use http_body::Full; use std::collections::HashMap; use std::net::SocketAddr; @@ -51,7 +53,7 @@ impl FromRequest for Version where B: Send, { - type Rejection = Response; + type Rejection = Response>; async fn from_request(req: &mut RequestParts) -> Result { let params = extract::Path::>::from_request(req) diff --git a/src/extract/content_length_limit.rs b/src/extract/content_length_limit.rs index dc038496..833034aa 100644 --- a/src/extract/content_length_limit.rs +++ b/src/extract/content_length_limit.rs @@ -1,3 +1,5 @@ +use crate::response::IntoResponse; + use super::{rejection::*, FromRequest, RequestParts}; use async_trait::async_trait; use std::ops::Deref; @@ -27,6 +29,7 @@ pub struct ContentLengthLimit(pub T); impl FromRequest for ContentLengthLimit where T: FromRequest, + T::Rejection: IntoResponse, B: Send, { type Rejection = ContentLengthLimitRejection; diff --git a/src/extract/rejection.rs b/src/extract/rejection.rs index 6303fddd..c2488b4a 100644 --- a/src/extract/rejection.rs +++ b/src/extract/rejection.rs @@ -1,7 +1,10 @@ //! Rejection response types. use super::IntoResponse; -use crate::body::Body; +use crate::body::{box_body, BoxBody, BoxStdError}; +use bytes::Bytes; +use http_body::Full; +use std::convert::Infallible; use tower::BoxError; define_rejection! { @@ -141,8 +144,11 @@ impl InvalidUrlParam { } impl IntoResponse for InvalidUrlParam { - fn into_response(self) -> http::Response { - let mut res = http::Response::new(Body::from(format!( + type Body = Full; + type BodyError = Infallible; + + fn into_response(self) -> http::Response { + let mut res = http::Response::new(Full::from(format!( "Invalid URL param. Expected something of type `{}`", self.type_name ))); @@ -163,8 +169,11 @@ impl InvalidPathParam { } impl IntoResponse for InvalidPathParam { - fn into_response(self) -> http::Response { - let mut res = http::Response::new(Body::from(format!("Invalid URL param. {}", self.0))); + type Body = Full; + type BodyError = Infallible; + + fn into_response(self) -> http::Response { + let mut res = http::Response::new(Full::from(format!("Invalid URL param. {}", self.0))); *res.status_mut() = http::StatusCode::BAD_REQUEST; res } @@ -191,8 +200,11 @@ impl FailedToDeserializeQueryString { } impl IntoResponse for FailedToDeserializeQueryString { - fn into_response(self) -> http::Response { - let mut res = http::Response::new(Body::from(format!( + type Body = Full; + type BodyError = Infallible; + + fn into_response(self) -> http::Response { + let mut res = http::Response::new(Full::from(format!( "Failed to deserialize query string. Expected something of type `{}`. Error: {}", self.type_name, self.error, ))); @@ -317,12 +329,15 @@ impl IntoResponse for ContentLengthLimitRejection where T: IntoResponse, { - fn into_response(self) -> http::Response { + type Body = BoxBody; + type BodyError = BoxStdError; + + fn into_response(self) -> http::Response { match self { - Self::PayloadTooLarge(inner) => inner.into_response(), - Self::LengthRequired(inner) => inner.into_response(), - Self::HeadersAlreadyExtracted(inner) => inner.into_response(), - Self::Inner(inner) => inner.into_response(), + Self::PayloadTooLarge(inner) => inner.into_response().map(box_body), + Self::LengthRequired(inner) => inner.into_response().map(box_body), + Self::HeadersAlreadyExtracted(inner) => inner.into_response().map(box_body), + Self::Inner(inner) => inner.into_response().map(box_body), } } } @@ -339,7 +354,10 @@ pub struct TypedHeaderRejection { #[cfg(feature = "headers")] #[cfg_attr(docsrs, doc(cfg(feature = "headers")))] impl IntoResponse for TypedHeaderRejection { - fn into_response(self) -> http::Response { + type Body = Full; + type BodyError = Infallible; + + fn into_response(self) -> http::Response { let mut res = format!("{} ({})", self.err, self.name).into_response(); *res.status_mut() = http::StatusCode::BAD_REQUEST; res diff --git a/src/extract/tuple.rs b/src/extract/tuple.rs index 5d3b2bff..a921eb86 100644 --- a/src/extract/tuple.rs +++ b/src/extract/tuple.rs @@ -1,5 +1,8 @@ use super::{FromRequest, RequestParts}; -use crate::response::IntoResponse; +use crate::{ + body::{box_body, BoxBody}, + response::IntoResponse, +}; use async_trait::async_trait; use http::Response; use std::convert::Infallible; @@ -29,11 +32,11 @@ macro_rules! impl_from_request { $( $tail: FromRequest + Send, )* B: Send, { - type Rejection = Response; + type Rejection = Response; async fn from_request(req: &mut RequestParts) -> Result { - let $head = $head::from_request(req).await.map_err(IntoResponse::into_response)?; - $( let $tail = $tail::from_request(req).await.map_err(IntoResponse::into_response)?; )* + let $head = $head::from_request(req).await.map_err(|err| err.into_response().map(box_body))?; + $( let $tail = $tail::from_request(req).await.map_err(|err| err.into_response().map(box_body))?; )* Ok(($head, $($tail,)*)) } } diff --git a/src/extract/ws.rs b/src/extract/ws.rs index 604ff965..ed9e63fa 100644 --- a/src/extract/ws.rs +++ b/src/extract/ws.rs @@ -48,10 +48,8 @@ use http::{ header::{self, HeaderName, HeaderValue}, Method, Response, StatusCode, }; -use hyper::{ - upgrade::{OnUpgrade, Upgraded}, - Body, -}; +use http_body::Full; +use hyper::upgrade::{OnUpgrade, Upgraded}; use sha1::{Digest, Sha1}; use std::{ borrow::Cow, @@ -256,7 +254,10 @@ where F: FnOnce(WebSocket) -> Fut + Send + 'static, Fut: Future + Send + 'static, { - fn into_response(self) -> Response { + type Body = Full; + type BodyError = ::Error; + + fn into_response(self) -> Response { // check requested protocols let protocol = self .extractor @@ -315,7 +316,7 @@ where builder = builder.header(header::SEC_WEBSOCKET_PROTOCOL, protocol); } - builder.body(Body::empty()).unwrap() + builder.body(Full::default()).unwrap() } } diff --git a/src/handler/mod.rs b/src/handler/mod.rs index f723df6f..e0691b11 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -268,8 +268,9 @@ macro_rules! impl_handler { Fut: Future + Send, B: Send + 'static, Res: IntoResponse, + B: Send + 'static, $head: FromRequest + Send, - $( $tail: FromRequest + Send, )* + $( $tail: FromRequest + Send,)* { type Sealed = sealed::Hidden; @@ -278,13 +279,13 @@ macro_rules! impl_handler { let $head = match $head::from_request(&mut req).await { Ok(value) => value, - Err(rejection) => return rejection.into_response().map(crate::body::box_body), + Err(rejection) => return rejection.into_response().map(box_body), }; $( let $tail = match $tail::from_request(&mut req).await { Ok(value) => value, - Err(rejection) => return rejection.into_response().map(crate::body::box_body), + Err(rejection) => return rejection.into_response().map(box_body), }; )* diff --git a/src/json.rs b/src/json.rs index 1ad4a7be..b0df06be 100644 --- a/src/json.rs +++ b/src/json.rs @@ -1,16 +1,20 @@ use crate::{ extract::{has_content_type, rejection::*, take_body, FromRequest, RequestParts}, prelude::response::IntoResponse, - Body, }; use async_trait::async_trait; +use bytes::Bytes; use http::{ header::{self, HeaderValue}, StatusCode, }; +use http_body::Full; use hyper::Response; use serde::{de::DeserializeOwned, Serialize}; -use std::ops::{Deref, DerefMut}; +use std::{ + convert::Infallible, + ops::{Deref, DerefMut}, +}; /// JSON Extractor/Response /// @@ -132,19 +136,22 @@ impl IntoResponse for Json where T: Serialize, { - fn into_response(self) -> Response { + type Body = Full; + type BodyError = Infallible; + + 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, "text/plain") - .body(Body::from(err.to_string())) + .body(Full::from(err.to_string())) .unwrap(); } }; - let mut res = Response::new(Body::from(bytes)); + let mut res = Response::new(Full::from(bytes)); res.headers_mut().insert( header::CONTENT_TYPE, HeaderValue::from_static("application/json"), diff --git a/src/lib.rs b/src/lib.rs index 541fd738..8e0d2369 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -707,7 +707,6 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(test, allow(clippy::float_cmp))] -use self::body::Body; use http::Request; use routing::{EmptyRouter, Route}; use tower::Service; diff --git a/src/macros.rs b/src/macros.rs index e7dd96e5..cfb09eeb 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -49,8 +49,11 @@ macro_rules! define_rejection { #[allow(deprecated)] impl $crate::response::IntoResponse for $name { - fn into_response(self) -> http::Response<$crate::body::Body> { - let mut res = http::Response::new($crate::body::Body::from($body)); + type Body = http_body::Full; + type BodyError = std::convert::Infallible; + + fn into_response(self) -> http::Response { + let mut res = http::Response::new(http_body::Full::from($body)); *res.status_mut() = http::StatusCode::$status; res } @@ -77,9 +80,12 @@ macro_rules! define_rejection { } impl IntoResponse for $name { - fn into_response(self) -> http::Response { + type Body = http_body::Full; + type BodyError = std::convert::Infallible; + + fn into_response(self) -> http::Response { let mut res = - http::Response::new(Body::from(format!(concat!($body, ": {}"), self.0))); + http::Response::new(http_body::Full::from(format!(concat!($body, ": {}"), self.0))); *res.status_mut() = http::StatusCode::$status; res } @@ -106,7 +112,10 @@ macro_rules! composite_rejection { } impl $crate::response::IntoResponse for $name { - fn into_response(self) -> http::Response<$crate::body::Body> { + type Body = http_body::Full; + type BodyError = std::convert::Infallible; + + fn into_response(self) -> http::Response { match self { $( Self::$variant(inner) => inner.into_response(), diff --git a/src/response.rs b/src/response.rs index 48fbb500..e0c7ad97 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,10 +1,14 @@ //! Types and traits for generating responses. -use crate::Body; +use crate::body::{box_body, BoxBody, BoxStdError}; use bytes::Bytes; use http::{header, HeaderMap, HeaderValue, Response, StatusCode}; +use http_body::{ + combinators::{MapData, MapErr}, + Empty, Full, +}; use std::{borrow::Cow, convert::Infallible}; -use tower::util::Either; +use tower::{util::Either, BoxError}; #[doc(no_inline)] pub use crate::Json; @@ -12,19 +16,119 @@ pub use crate::Json; /// 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. +/// +/// A manual implementation should only be necessary if you have a custom +/// response body type: +/// +/// ```rust +/// use axum::{prelude::*, response::IntoResponse}; +/// use http_body::Body; +/// use http::{Response, 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 { +/// type Body = Self; +/// type BodyError = ::Error; +/// +/// fn into_response(self) -> Response { +/// Response::new(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 = route("/", get(|| async { MyBody })); +/// # async { +/// # hyper::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); +/// # }; +/// ``` pub trait IntoResponse { + /// The body type of the response. + /// + /// Unless you're implementing this trait for a custom body type, these are + /// some common types you can use: + /// + /// - [`hyper::Body`]: A good default that supports most use cases. + /// - [`http_body::Empty`]: When you know your response is always + /// empty. + /// - [`http_body::Full`]: When you know your response always + /// contains exactly one chunk. + /// - [`BoxBody`]: If you need to unify multiple body types into one, or + /// return a body type that cannot be named. Can be created with + /// [`box_body`]. + /// + /// [`http_body::Empty`]: http_body::Empty + /// [`http_body::Full`]: http_body::Full + type Body: http_body::Body + Send + Sync + 'static; + + /// The error type `Self::Body` might generate. + /// + /// Generally it should be possible to set this to: + /// + /// ```rust,ignore + /// type BodyError = ::Error; + /// ``` + /// + /// This associated type exists mainly to make returning `impl IntoResponse` + /// possible and to simplify trait bounds internally in axum. + type BodyError: Into; + /// Create a response. - fn into_response(self) -> Response; + fn into_response(self) -> Response; } impl IntoResponse for () { - fn into_response(self) -> Response { - Response::new(Body::empty()) + type Body = Empty; + type BodyError = Infallible; + + fn into_response(self) -> Response { + Response::new(Empty::new()) } } impl IntoResponse for Infallible { - fn into_response(self) -> Response { + type Body = Empty; + type BodyError = Infallible; + + fn into_response(self) -> Response { match self {} } } @@ -34,10 +138,13 @@ where T: IntoResponse, K: IntoResponse, { - fn into_response(self) -> Response { + type Body = BoxBody; + type BodyError = BoxStdError; + + fn into_response(self) -> Response { match self { - Either::A(inner) => inner.into_response(), - Either::B(inner) => inner.into_response(), + Either::A(inner) => inner.into_response().map(box_body), + Either::B(inner) => inner.into_response().map(box_body), } } } @@ -47,43 +154,113 @@ where T: IntoResponse, E: IntoResponse, { - fn into_response(self) -> Response { + type Body = BoxBody; + type BodyError = BoxStdError; + + fn into_response(self) -> Response { match self { - Ok(value) => value.into_response(), - Err(err) => err.into_response(), + Ok(value) => value.into_response().map(box_body), + Err(err) => err.into_response().map(box_body), } } } -impl IntoResponse for Response { +impl IntoResponse for Response +where + B: http_body::Body + Send + Sync + 'static, + B::Error: Into, +{ + type Body = B; + type BodyError = ::Error; + fn into_response(self) -> Self { self } } -impl IntoResponse for Body { - fn into_response(self) -> Response { +macro_rules! impl_into_response_for_body { + ($body:ty) => { + impl IntoResponse for $body { + type Body = $body; + type BodyError = <$body as http_body::Body>::Error; + + fn into_response(self) -> Response { + Response::new(self) + } + } + }; +} + +impl_into_response_for_body!(hyper::Body); +impl_into_response_for_body!(Full); +impl_into_response_for_body!(Empty); + +impl IntoResponse for http_body::combinators::BoxBody +where + E: Into + 'static, +{ + type Body = Self; + type BodyError = E; + + fn into_response(self) -> Response { + Response::new(self) + } +} + +impl IntoResponse for MapData +where + B: http_body::Body + Send + Sync + 'static, + F: FnMut(B::Data) -> Bytes + Send + Sync + 'static, + B::Error: Into, +{ + type Body = Self; + type BodyError = ::Error; + + fn into_response(self) -> Response { + Response::new(self) + } +} + +impl IntoResponse for MapErr +where + B: http_body::Body + Send + Sync + 'static, + F: FnMut(B::Error) -> E + Send + Sync + 'static, + E: Into, +{ + type Body = Self; + type BodyError = E; + + fn into_response(self) -> Response { Response::new(self) } } impl IntoResponse for &'static str { + type Body = Full; + type BodyError = Infallible; + #[inline] - fn into_response(self) -> Response { + fn into_response(self) -> Response { Cow::Borrowed(self).into_response() } } impl IntoResponse for String { + type Body = Full; + type BodyError = Infallible; + #[inline] - fn into_response(self) -> Response { + fn into_response(self) -> Response { Cow::<'static, str>::Owned(self).into_response() } } impl IntoResponse for std::borrow::Cow<'static, str> { - fn into_response(self) -> Response { - let mut res = Response::new(Body::from(self)); + type Body = Full; + type BodyError = Infallible; + + fn into_response(self) -> Response { + let mut res = Response::new(Full::from(self)); res.headers_mut() .insert(header::CONTENT_TYPE, HeaderValue::from_static("text/plain")); res @@ -91,8 +268,11 @@ impl IntoResponse for std::borrow::Cow<'static, str> { } impl IntoResponse for Bytes { - fn into_response(self) -> Response { - let mut res = Response::new(Body::from(self)); + type Body = Full; + type BodyError = Infallible; + + fn into_response(self) -> Response { + let mut res = Response::new(Full::from(self)); res.headers_mut().insert( header::CONTENT_TYPE, HeaderValue::from_static("application/octet-stream"), @@ -102,8 +282,11 @@ impl IntoResponse for Bytes { } impl IntoResponse for &'static [u8] { - fn into_response(self) -> Response { - let mut res = Response::new(Body::from(self)); + type Body = Full; + type BodyError = Infallible; + + fn into_response(self) -> Response { + let mut res = Response::new(Full::from(self)); res.headers_mut().insert( header::CONTENT_TYPE, HeaderValue::from_static("application/octet-stream"), @@ -113,8 +296,11 @@ impl IntoResponse for &'static [u8] { } impl IntoResponse for Vec { - fn into_response(self) -> Response { - let mut res = Response::new(Body::from(self)); + type Body = Full; + type BodyError = Infallible; + + fn into_response(self) -> Response { + let mut res = Response::new(Full::from(self)); res.headers_mut().insert( header::CONTENT_TYPE, HeaderValue::from_static("application/octet-stream"), @@ -124,8 +310,11 @@ impl IntoResponse for Vec { } impl IntoResponse for std::borrow::Cow<'static, [u8]> { - fn into_response(self) -> Response { - let mut res = Response::new(Body::from(self)); + type Body = Full; + type BodyError = Infallible; + + fn into_response(self) -> Response { + let mut res = Response::new(Full::from(self)); res.headers_mut().insert( header::CONTENT_TYPE, HeaderValue::from_static("application/octet-stream"), @@ -135,11 +324,11 @@ impl IntoResponse for std::borrow::Cow<'static, [u8]> { } impl IntoResponse for StatusCode { - fn into_response(self) -> Response { - Response::builder() - .status(self) - .body(Body::empty()) - .unwrap() + type Body = Empty; + type BodyError = Infallible; + + fn into_response(self) -> Response { + Response::builder().status(self).body(Empty::new()).unwrap() } } @@ -147,7 +336,10 @@ impl IntoResponse for (StatusCode, T) where T: IntoResponse, { - fn into_response(self) -> Response { + type Body = T::Body; + type BodyError = T::BodyError; + + fn into_response(self) -> Response { let mut res = self.1.into_response(); *res.status_mut() = self.0; res @@ -158,7 +350,10 @@ impl IntoResponse for (HeaderMap, T) where T: IntoResponse, { - fn into_response(self) -> Response { + type Body = T::Body; + type BodyError = T::BodyError; + + fn into_response(self) -> Response { let mut res = self.1.into_response(); res.headers_mut().extend(self.0); res @@ -169,7 +364,10 @@ impl IntoResponse for (StatusCode, HeaderMap, T) where T: IntoResponse, { - fn into_response(self) -> Response { + type Body = T::Body; + type BodyError = T::BodyError; + + fn into_response(self) -> Response { let mut res = self.2.into_response(); *res.status_mut() = self.0; res.headers_mut().extend(self.1); @@ -178,8 +376,11 @@ where } impl IntoResponse for HeaderMap { - fn into_response(self) -> Response { - let mut res = Response::new(Body::empty()); + type Body = Empty; + type BodyError = Infallible; + + fn into_response(self) -> Response { + let mut res = Response::new(Empty::new()); *res.headers_mut() = self; res } @@ -193,9 +394,12 @@ pub struct Html(pub T); impl IntoResponse for Html where - T: Into, + T: Into>, { - fn into_response(self) -> Response { + type Body = Full; + type BodyError = Infallible; + + fn into_response(self) -> Response { let mut res = Response::new(self.0.into()); res.headers_mut() .insert(header::CONTENT_TYPE, HeaderValue::from_static("text/html")); @@ -212,6 +416,7 @@ impl From for Html { #[cfg(test)] mod tests { use super::*; + use crate::body::Body; use http::header::{HeaderMap, HeaderName}; #[test] @@ -219,6 +424,9 @@ mod tests { struct MyResponse; impl IntoResponse for MyResponse { + type Body = Body; + type BodyError = ::Error; + fn into_response(self) -> Response { let mut resp = Response::new(String::new().into()); resp.headers_mut()