From c7c02ef99d6ef2c600355b1c889eb4f11ffcb88a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sat, 4 Dec 2021 19:40:47 +0000 Subject: [PATCH] body ergonomics v3 (#2468) --- CHANGES.md | 4 + actix-files/src/chunked.rs | 3 +- actix-files/src/named.rs | 21 +- actix-http/CHANGES.md | 24 +- actix-http/examples/echo2.rs | 8 +- actix-http/src/body/body.rs | 333 --------------------- actix-http/src/body/body_stream.rs | 23 +- actix-http/src/body/boxed.rs | 80 +++++ actix-http/src/body/either.rs | 83 ++++++ actix-http/src/body/message_body.rs | 443 ++++++++++++++++++++-------- actix-http/src/body/mod.rs | 268 +---------------- actix-http/src/body/none.rs | 43 +++ actix-http/src/body/size.rs | 2 +- actix-http/src/body/sized_stream.rs | 2 + actix-http/src/body/utils.rs | 78 +++++ actix-http/src/builder.rs | 20 +- actix-http/src/encoding/encoder.rs | 144 +++++---- actix-http/src/encoding/mod.rs | 3 + actix-http/src/error.rs | 29 +- actix-http/src/h1/dispatcher.rs | 59 ++-- actix-http/src/h1/service.rs | 44 ++- actix-http/src/h1/utils.rs | 38 ++- actix-http/src/h2/dispatcher.rs | 14 +- actix-http/src/h2/service.rs | 28 +- actix-http/src/message.rs | 9 +- actix-http/src/response.rs | 175 ++++++----- actix-http/src/response_builder.rs | 67 ++--- actix-http/src/service.rs | 204 ++++++------- actix-http/src/ws/dispatcher.rs | 26 +- actix-http/src/ws/mod.rs | 33 ++- actix-http/tests/test_client.rs | 4 +- actix-http/tests/test_openssl.rs | 12 +- actix-http/tests/test_rustls.rs | 12 +- actix-http/tests/test_server.rs | 43 +-- actix-http/tests/test_ws.rs | 6 +- actix-test/src/lib.rs | 7 +- actix-web-actors/Cargo.toml | 2 +- actix-web-actors/src/ws.rs | 16 +- actix-web-actors/tests/test_ws.rs | 2 +- awc/Cargo.toml | 1 + awc/src/any_body.rs | 266 +++++++++++++++++ awc/src/client/connection.rs | 8 +- awc/src/client/error.rs | 20 +- awc/src/client/h1proto.rs | 10 +- awc/src/client/h2proto.rs | 16 +- awc/src/connect.rs | 13 +- awc/src/error.rs | 2 + awc/src/frozen.rs | 10 +- awc/src/lib.rs | 3 + awc/src/middleware/redirect.rs | 17 +- awc/src/request.rs | 13 +- awc/src/sender.rs | 24 +- benches/responder.rs | 17 +- src/app.rs | 56 ++-- src/app_service.rs | 28 +- src/data.rs | 2 +- src/dev.rs | 53 +++- src/error/error.rs | 6 +- src/error/internal.rs | 19 +- src/error/macros.rs | 2 +- src/error/response_error.rs | 30 +- src/handler.rs | 43 ++- src/http/header/accept.rs | 2 +- src/lib.rs | 2 + src/middleware/compat.rs | 6 +- src/middleware/compress.rs | 34 +-- src/middleware/logger.rs | 2 +- src/resource.rs | 29 +- src/responder.rs | 299 +++++++++++-------- src/response/builder.rs | 53 ++-- src/response/response.rs | 34 ++- src/route.rs | 23 +- src/scope.rs | 80 ++--- src/server.rs | 10 +- src/service.rs | 42 ++- src/test.rs | 24 +- src/types/either.rs | 10 +- src/types/form.rs | 43 ++- src/types/json.rs | 30 +- src/types/path.rs | 2 +- src/types/payload.rs | 6 +- src/types/query.rs | 2 +- src/web.rs | 8 +- tests/test_server.rs | 7 +- 84 files changed, 2134 insertions(+), 1685 deletions(-) delete mode 100644 actix-http/src/body/body.rs create mode 100644 actix-http/src/body/boxed.rs create mode 100644 actix-http/src/body/either.rs create mode 100644 actix-http/src/body/none.rs create mode 100644 actix-http/src/body/utils.rs create mode 100644 awc/src/any_body.rs diff --git a/CHANGES.md b/CHANGES.md index c754d4dd6..2dc45c3ed 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ ### Added * Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480] * `AcceptEncoding` typed header. [#2482] +* `HttpResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468] +* `ServiceResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468] ### Changed * Rename `Accept::{mime_precedence => ranked}`. [#2480] @@ -11,8 +13,10 @@ ### Fixed * Accept wildcard `*` items in `AcceptLanguage`. [#2480] +* Re-exports `dev::{BodySize, MessageBody, SizedStream}`. They are exposed through the `body` module. [#2468] * Typed headers containing lists that require one or more items now enforce this minimum. [#2482] +[#2468]: https://github.com/actix/actix-web/pull/2468 [#2480]: https://github.com/actix/actix-web/pull/2480 [#2482]: https://github.com/actix/actix-web/pull/2482 diff --git a/actix-files/src/chunked.rs b/actix-files/src/chunked.rs index fbb46e417..68221ccc3 100644 --- a/actix-files/src/chunked.rs +++ b/actix-files/src/chunked.rs @@ -6,8 +6,7 @@ use std::{ task::{Context, Poll}, }; -use actix_web::error::Error; -use bytes::Bytes; +use actix_web::{error::Error, web::Bytes}; use futures_core::{ready, Stream}; use pin_project_lite::pin_project; diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 547048bbd..89775c6b3 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -10,12 +10,12 @@ use std::{ #[cfg(unix)] use std::os::unix::fs::MetadataExt; -use actix_http::body::AnyBody; use actix_service::{Service, ServiceFactory}; use actix_web::{ + body::{self, BoxBody, SizedStream}, dev::{ AppService, BodyEncoding, HttpServiceFactory, ResourceDef, ServiceRequest, - ServiceResponse, SizedStream, + ServiceResponse, }, http::{ header::{ @@ -113,6 +113,8 @@ pub(crate) use std::fs::File; #[cfg(feature = "experimental-io-uring")] pub(crate) use tokio_uring::fs::File; +use super::chunked; + impl NamedFile { /// Creates an instance from a previously opened file. /// @@ -394,7 +396,7 @@ impl NamedFile { } /// Creates an `HttpResponse` with file as a streaming body. - pub fn into_response(self, req: &HttpRequest) -> HttpResponse { + pub fn into_response(self, req: &HttpRequest) -> HttpResponse { if self.status_code != StatusCode::OK { let mut res = HttpResponse::build(self.status_code); @@ -416,7 +418,7 @@ impl NamedFile { res.encoding(current_encoding); } - let reader = super::chunked::new_chunked_read(self.md.len(), 0, self.file); + let reader = chunked::new_chunked_read(self.md.len(), 0, self.file); return res.streaming(reader); } @@ -527,10 +529,13 @@ impl NamedFile { if precondition_failed { return resp.status(StatusCode::PRECONDITION_FAILED).finish(); } else if not_modified { - return resp.status(StatusCode::NOT_MODIFIED).body(AnyBody::None); + return resp + .status(StatusCode::NOT_MODIFIED) + .body(body::None::new()) + .map_into_boxed_body(); } - let reader = super::chunked::new_chunked_read(length, offset, self.file); + let reader = chunked::new_chunked_read(length, offset, self.file); if offset != 0 || length != self.md.len() { resp.status(StatusCode::PARTIAL_CONTENT); @@ -595,7 +600,9 @@ impl DerefMut for NamedFile { } impl Responder for NamedFile { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { + type Body = BoxBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { self.into_response(req) } } diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 1eaccfb2e..23c15296a 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -3,9 +3,31 @@ ## Unreleased - 2021-xx-xx ### Added * Add timeout for canceling HTTP/2 server side connection handshake. Default to 5 seconds. [#2483] -* HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483] +* HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483] +* `Response::map_into_boxed_body`. [#2468] +* `body::EitherBody` enum. [#2468] +* `body::None` struct. [#2468] +* Impl `MessageBody` for `bytestring::ByteString`. [#2468] +* `impl Clone for ws::HandshakeError`. [#2468] + +### Changed +* Rename `body::BoxBody::{from_body => new}`. [#2468] +* Body type for `Responses` returned from `Response::{new, ok, etc...}` is now `BoxBody`. [#2468] +* The `Error` associated type on `MessageBody` type now requires `impl Error` (or similar). [#2468] +* Error types using in service builders now require `Into>`. [#2468] +* `From` implementations on error types now return a `Response`. [#2468] +* `ResponseBuilder::body(B)` now returns `Response>`. [#2468] +* `ResponseBuilder::finish()` now returns `Response>`. [#2468] + +### Removed +* `ResponseBuilder::streaming`. [#2468] +* `impl Future` for `ResponseBuilder`. [#2468] +* Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468] +* Move `body::AnyBody` to `awc`. Replaced with `EitherBody` and `BoxBody`. [#2468] [#2483]: https://github.com/actix/actix-web/pull/2483 +[#2468]: https://github.com/actix/actix-web/pull/2468 + ## 3.0.0-beta.14 - 2021-11-30 ### Changed diff --git a/actix-http/examples/echo2.rs b/actix-http/examples/echo2.rs index 6e5ddec7c..6092c01ce 100644 --- a/actix-http/examples/echo2.rs +++ b/actix-http/examples/echo2.rs @@ -1,12 +1,14 @@ use std::io; -use actix_http::{body::AnyBody, http::HeaderValue, http::StatusCode}; -use actix_http::{Error, HttpService, Request, Response}; +use actix_http::{ + body::MessageBody, http::HeaderValue, http::StatusCode, Error, HttpService, Request, + Response, +}; use actix_server::Server; use bytes::BytesMut; use futures_util::StreamExt as _; -async fn handle_request(mut req: Request) -> Result, Error> { +async fn handle_request(mut req: Request) -> Result, Error> { let mut body = BytesMut::new(); while let Some(item) = req.payload().next().await { body.extend_from_slice(&item?) diff --git a/actix-http/src/body/body.rs b/actix-http/src/body/body.rs deleted file mode 100644 index e8861024b..000000000 --- a/actix-http/src/body/body.rs +++ /dev/null @@ -1,333 +0,0 @@ -use std::{ - borrow::Cow, - error::Error as StdError, - fmt, mem, - pin::Pin, - task::{Context, Poll}, -}; - -use bytes::{Bytes, BytesMut}; -use futures_core::Stream; -use pin_project::pin_project; - -use crate::error::Error; - -use super::{BodySize, BodyStream, MessageBody, MessageBodyMapErr, SizedStream}; - -#[deprecated(since = "4.0.0", note = "Renamed to `AnyBody`.")] -pub type Body = AnyBody; - -/// Represents various types of HTTP message body. -#[pin_project(project = AnyBodyProj)] -#[derive(Clone)] -pub enum AnyBody { - /// Empty response. `Content-Length` header is not set. - None, - - /// Complete, in-memory response body. - Bytes(Bytes), - - /// Generic / Other message body. - Body(#[pin] B), -} - -impl AnyBody { - /// Constructs a "body" representing an empty response. - pub fn none() -> Self { - Self::None - } - - /// Constructs a new, 0-length body. - pub fn empty() -> Self { - Self::Bytes(Bytes::new()) - } - - /// Create boxed body from generic message body. - pub fn new_boxed(body: B) -> Self - where - B: MessageBody + 'static, - B::Error: Into>, - { - Self::Body(BoxBody::from_body(body)) - } - - /// Constructs new `AnyBody` instance from a slice of bytes by copying it. - /// - /// If your bytes container is owned, it may be cheaper to use a `From` impl. - pub fn copy_from_slice(s: &[u8]) -> Self { - Self::Bytes(Bytes::copy_from_slice(s)) - } - - #[doc(hidden)] - #[deprecated(since = "4.0.0", note = "Renamed to `copy_from_slice`.")] - pub fn from_slice(s: &[u8]) -> Self { - Self::Bytes(Bytes::copy_from_slice(s)) - } -} - -impl AnyBody -where - B: MessageBody + 'static, - B::Error: Into>, -{ - /// Create body from generic message body. - pub fn new(body: B) -> Self { - Self::Body(body) - } - - pub fn into_boxed(self) -> AnyBody { - match self { - Self::None => AnyBody::None, - Self::Bytes(bytes) => AnyBody::Bytes(bytes), - Self::Body(body) => AnyBody::new_boxed(body), - } - } -} - -impl MessageBody for AnyBody -where - B: MessageBody, - B::Error: Into> + 'static, -{ - type Error = Error; - - fn size(&self) -> BodySize { - match self { - AnyBody::None => BodySize::None, - AnyBody::Bytes(ref bin) => BodySize::Sized(bin.len() as u64), - AnyBody::Body(ref body) => body.size(), - } - } - - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - match self.project() { - AnyBodyProj::None => Poll::Ready(None), - AnyBodyProj::Bytes(bin) => { - let len = bin.len(); - if len == 0 { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(mem::take(bin)))) - } - } - - AnyBodyProj::Body(body) => body - .poll_next(cx) - .map_err(|err| Error::new_body().with_cause(err)), - } - } -} - -impl PartialEq for AnyBody { - fn eq(&self, other: &AnyBody) -> bool { - match *self { - AnyBody::None => matches!(*other, AnyBody::None), - AnyBody::Bytes(ref b) => match *other { - AnyBody::Bytes(ref b2) => b == b2, - _ => false, - }, - AnyBody::Body(_) => false, - } - } -} - -impl fmt::Debug for AnyBody { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - AnyBody::None => write!(f, "AnyBody::None"), - AnyBody::Bytes(ref bytes) => write!(f, "AnyBody::Bytes({:?})", bytes), - AnyBody::Body(ref stream) => write!(f, "AnyBody::Message({:?})", stream), - } - } -} - -impl From<&'static str> for AnyBody { - fn from(string: &'static str) -> Self { - Self::Bytes(Bytes::from_static(string.as_ref())) - } -} - -impl From<&'static [u8]> for AnyBody { - fn from(bytes: &'static [u8]) -> Self { - Self::Bytes(Bytes::from_static(bytes)) - } -} - -impl From> for AnyBody { - fn from(vec: Vec) -> Self { - Self::Bytes(Bytes::from(vec)) - } -} - -impl From for AnyBody { - fn from(string: String) -> Self { - Self::Bytes(Bytes::from(string)) - } -} - -impl From<&'_ String> for AnyBody { - fn from(string: &String) -> Self { - Self::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string))) - } -} - -impl From> for AnyBody { - fn from(string: Cow<'_, str>) -> Self { - match string { - Cow::Owned(s) => Self::from(s), - Cow::Borrowed(s) => { - Self::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s))) - } - } - } -} - -impl From for AnyBody { - fn from(bytes: Bytes) -> Self { - Self::Bytes(bytes) - } -} - -impl From for AnyBody { - fn from(bytes: BytesMut) -> Self { - Self::Bytes(bytes.freeze()) - } -} - -impl From> for AnyBody> -where - S: Stream> + 'static, - E: Into> + 'static, -{ - fn from(stream: SizedStream) -> Self { - AnyBody::new(stream) - } -} - -impl From> for AnyBody -where - S: Stream> + 'static, - E: Into> + 'static, -{ - fn from(stream: SizedStream) -> Self { - AnyBody::new_boxed(stream) - } -} - -impl From> for AnyBody> -where - S: Stream> + 'static, - E: Into> + 'static, -{ - fn from(stream: BodyStream) -> Self { - AnyBody::new(stream) - } -} - -impl From> for AnyBody -where - S: Stream> + 'static, - E: Into> + 'static, -{ - fn from(stream: BodyStream) -> Self { - AnyBody::new_boxed(stream) - } -} - -/// A boxed message body with boxed errors. -pub struct BoxBody(Pin>>>); - -impl BoxBody { - /// Boxes a `MessageBody` and any errors it generates. - pub fn from_body(body: B) -> Self - where - B: MessageBody + 'static, - B::Error: Into>, - { - let body = MessageBodyMapErr::new(body, Into::into); - Self(Box::pin(body)) - } - - /// Returns a mutable pinned reference to the inner message body type. - pub fn as_pin_mut( - &mut self, - ) -> Pin<&mut (dyn MessageBody>)> { - self.0.as_mut() - } -} - -impl fmt::Debug for BoxBody { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("BoxAnyBody(dyn MessageBody)") - } -} - -impl MessageBody for BoxBody { - type Error = Error; - - fn size(&self) -> BodySize { - self.0.size() - } - - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - self.0 - .as_mut() - .poll_next(cx) - .map_err(|err| Error::new_body().with_cause(err)) - } -} - -#[cfg(test)] -mod tests { - use std::marker::PhantomPinned; - - use static_assertions::{assert_impl_all, assert_not_impl_all}; - - use super::*; - use crate::body::to_bytes; - - struct PinType(PhantomPinned); - - impl MessageBody for PinType { - type Error = crate::Error; - - fn size(&self) -> BodySize { - unimplemented!() - } - - fn poll_next( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll>> { - unimplemented!() - } - } - - assert_impl_all!(AnyBody<()>: MessageBody, fmt::Debug, Send, Sync, Unpin); - assert_impl_all!(AnyBody>: MessageBody, fmt::Debug, Send, Sync, Unpin); - assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Send, Sync, Unpin); - assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Unpin); - assert_impl_all!(BoxBody: MessageBody, fmt::Debug, Unpin); - assert_impl_all!(AnyBody: MessageBody); - - assert_not_impl_all!(AnyBody: Send, Sync, Unpin); - assert_not_impl_all!(BoxBody: Send, Sync, Unpin); - assert_not_impl_all!(AnyBody: Send, Sync, Unpin); - - #[actix_rt::test] - async fn nested_boxed_body() { - let body = AnyBody::copy_from_slice(&[1, 2, 3]); - let boxed_body = BoxBody::from_body(BoxBody::from_body(body)); - - assert_eq!( - to_bytes(boxed_body).await.unwrap(), - Bytes::from(vec![1, 2, 3]), - ); - } -} diff --git a/actix-http/src/body/body_stream.rs b/actix-http/src/body/body_stream.rs index 31de9b48f..1da7a848a 100644 --- a/actix-http/src/body/body_stream.rs +++ b/actix-http/src/body/body_stream.rs @@ -20,6 +20,8 @@ pin_project! { } } +// TODO: from_infallible method + impl BodyStream where S: Stream>, @@ -75,6 +77,7 @@ mod tests { use derive_more::{Display, Error}; use futures_core::ready; use futures_util::{stream, FutureExt as _}; + use pin_project_lite::pin_project; use static_assertions::{assert_impl_all, assert_not_impl_all}; use super::*; @@ -166,12 +169,14 @@ mod tests { BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)])); assert!(matches!(to_bytes(body).await, Err(StreamErr))); - #[pin_project::pin_project(project = TimeDelayStreamProj)] - #[derive(Debug)] - enum TimeDelayStream { - Start, - Sleep(Pin>), - Done, + pin_project! { + #[derive(Debug)] + #[project = TimeDelayStreamProj] + enum TimeDelayStream { + Start, + Sleep { delay: Pin> }, + Done, + } } impl Stream for TimeDelayStream { @@ -184,12 +189,14 @@ mod tests { match self.as_mut().get_mut() { TimeDelayStream::Start => { let sleep = sleep(Duration::from_millis(1)); - self.as_mut().set(TimeDelayStream::Sleep(Box::pin(sleep))); + self.as_mut().set(TimeDelayStream::Sleep { + delay: Box::pin(sleep), + }); cx.waker().wake_by_ref(); Poll::Pending } - TimeDelayStream::Sleep(ref mut delay) => { + TimeDelayStream::Sleep { ref mut delay } => { ready!(delay.poll_unpin(cx)); self.set(TimeDelayStream::Done); cx.waker().wake_by_ref(); diff --git a/actix-http/src/body/boxed.rs b/actix-http/src/body/boxed.rs new file mode 100644 index 000000000..9442bd1df --- /dev/null +++ b/actix-http/src/body/boxed.rs @@ -0,0 +1,80 @@ +use std::{ + error::Error as StdError, + fmt, + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::Bytes; + +use super::{BodySize, MessageBody, MessageBodyMapErr}; +use crate::Error; + +/// A boxed message body with boxed errors. +pub struct BoxBody(Pin>>>); + +impl BoxBody { + /// Boxes a `MessageBody` and any errors it generates. + pub fn new(body: B) -> Self + where + B: MessageBody + 'static, + { + let body = MessageBodyMapErr::new(body, Into::into); + Self(Box::pin(body)) + } + + /// Returns a mutable pinned reference to the inner message body type. + pub fn as_pin_mut( + &mut self, + ) -> Pin<&mut (dyn MessageBody>)> { + self.0.as_mut() + } +} + +impl fmt::Debug for BoxBody { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("BoxBody(dyn MessageBody)") + } +} + +impl MessageBody for BoxBody { + type Error = Error; + + fn size(&self) -> BodySize { + self.0.size() + } + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + self.0 + .as_mut() + .poll_next(cx) + .map_err(|err| Error::new_body().with_cause(err)) + } +} + +#[cfg(test)] +mod tests { + + use static_assertions::{assert_impl_all, assert_not_impl_all}; + + use super::*; + use crate::body::to_bytes; + + assert_impl_all!(BoxBody: MessageBody, fmt::Debug, Unpin); + + assert_not_impl_all!(BoxBody: Send, Sync, Unpin); + + #[actix_rt::test] + async fn nested_boxed_body() { + let body = Bytes::from_static(&[1, 2, 3]); + let boxed_body = BoxBody::new(BoxBody::new(body)); + + assert_eq!( + to_bytes(boxed_body).await.unwrap(), + Bytes::from(vec![1, 2, 3]), + ); + } +} diff --git a/actix-http/src/body/either.rs b/actix-http/src/body/either.rs new file mode 100644 index 000000000..6169ee627 --- /dev/null +++ b/actix-http/src/body/either.rs @@ -0,0 +1,83 @@ +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::Bytes; +use pin_project_lite::pin_project; + +use super::{BodySize, BoxBody, MessageBody}; +use crate::Error; + +pin_project! { + #[project = EitherBodyProj] + #[derive(Debug, Clone)] + pub enum EitherBody { + /// A body of type `L`. + Left { #[pin] body: L }, + + /// A body of type `R`. + Right { #[pin] body: R }, + } +} + +impl EitherBody { + /// Creates new `EitherBody` using left variant and boxed right variant. + pub fn new(body: L) -> Self { + Self::Left { body } + } +} + +impl EitherBody { + /// Creates new `EitherBody` using left variant. + pub fn left(body: L) -> Self { + Self::Left { body } + } + + /// Creates new `EitherBody` using right variant. + pub fn right(body: R) -> Self { + Self::Right { body } + } +} + +impl MessageBody for EitherBody +where + L: MessageBody + 'static, + R: MessageBody + 'static, +{ + type Error = Error; + + fn size(&self) -> BodySize { + match self { + EitherBody::Left { body } => body.size(), + EitherBody::Right { body } => body.size(), + } + } + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + match self.project() { + EitherBodyProj::Left { body } => body + .poll_next(cx) + .map_err(|err| Error::new_body().with_cause(err)), + EitherBodyProj::Right { body } => body + .poll_next(cx) + .map_err(|err| Error::new_body().with_cause(err)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn type_parameter_inference() { + let _body: EitherBody<(), _> = EitherBody::new(()); + + let _body: EitherBody<_, ()> = EitherBody::left(()); + let _body: EitherBody<(), _> = EitherBody::right(()); + } +} diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs index 62a7e9b1c..053b6f286 100644 --- a/actix-http/src/body/message_body.rs +++ b/actix-http/src/body/message_body.rs @@ -2,6 +2,7 @@ use std::{ convert::Infallible, + error::Error as StdError, mem, pin::Pin, task::{Context, Poll}, @@ -13,9 +14,12 @@ use pin_project_lite::pin_project; use super::BodySize; -/// An interface for response bodies. +/// An interface types that can converted to bytes and used as response bodies. +// TODO: examples pub trait MessageBody { - type Error; + // TODO: consider this bound to only fmt::Display since the error type is not really used + // and there is an impl for Into> on String + type Error: Into>; /// Body size hint. fn size(&self) -> BodySize; @@ -27,152 +31,218 @@ pub trait MessageBody { ) -> Poll>>; } -impl MessageBody for () { - type Error = Infallible; +mod foreign_impls { + use super::*; - fn size(&self) -> BodySize { - BodySize::Sized(0) - } + impl MessageBody for Infallible { + type Error = Infallible; - fn poll_next( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll>> { - Poll::Ready(None) - } -} + #[inline] + fn size(&self) -> BodySize { + match *self {} + } -impl MessageBody for Box -where - B: MessageBody + Unpin, -{ - type Error = B::Error; - - fn size(&self) -> BodySize { - self.as_ref().size() - } - - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - Pin::new(self.get_mut().as_mut()).poll_next(cx) - } -} - -impl MessageBody for Pin> -where - B: MessageBody, -{ - type Error = B::Error; - - fn size(&self) -> BodySize { - self.as_ref().size() - } - - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - self.as_mut().poll_next(cx) - } -} - -impl MessageBody for Bytes { - type Error = Infallible; - - fn size(&self) -> BodySize { - BodySize::Sized(self.len() as u64) - } - - fn poll_next( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll>> { - if self.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(mem::take(self.get_mut())))) + #[inline] + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + match *self {} } } -} -impl MessageBody for BytesMut { - type Error = Infallible; + impl MessageBody for () { + type Error = Infallible; - fn size(&self) -> BodySize { - BodySize::Sized(self.len() as u64) - } + #[inline] + fn size(&self) -> BodySize { + BodySize::Sized(0) + } - fn poll_next( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll>> { - if self.is_empty() { + #[inline] + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze()))) } } -} -impl MessageBody for &'static str { - type Error = Infallible; + impl MessageBody for Box + where + B: MessageBody + Unpin, + { + type Error = B::Error; - fn size(&self) -> BodySize { - BodySize::Sized(self.len() as u64) - } + #[inline] + fn size(&self) -> BodySize { + self.as_ref().size() + } - fn poll_next( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll>> { - if self.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(Bytes::from_static( - mem::take(self.get_mut()).as_ref(), - )))) + #[inline] + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + Pin::new(self.get_mut().as_mut()).poll_next(cx) } } -} -impl MessageBody for Vec { - type Error = Infallible; + impl MessageBody for Pin> + where + B: MessageBody, + { + type Error = B::Error; - fn size(&self) -> BodySize { - BodySize::Sized(self.len() as u64) - } + #[inline] + fn size(&self) -> BodySize { + self.as_ref().size() + } - fn poll_next( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll>> { - if self.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(Bytes::from(mem::take(self.get_mut()))))) + #[inline] + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + self.as_mut().poll_next(cx) } } -} -impl MessageBody for String { - type Error = Infallible; + impl MessageBody for &'static [u8] { + type Error = Infallible; - fn size(&self) -> BodySize { - BodySize::Sized(self.len() as u64) + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let bytes = mem::take(self.get_mut()); + let bytes = Bytes::from_static(bytes); + Poll::Ready(Some(Ok(bytes))) + } + } } - fn poll_next( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll>> { - if self.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(Bytes::from( - mem::take(self.get_mut()).into_bytes(), - )))) + impl MessageBody for Bytes { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let bytes = mem::take(self.get_mut()); + Poll::Ready(Some(Ok(bytes))) + } + } + } + + impl MessageBody for BytesMut { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let bytes = mem::take(self.get_mut()).freeze(); + Poll::Ready(Some(Ok(bytes))) + } + } + } + + impl MessageBody for Vec { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let bytes = mem::take(self.get_mut()); + Poll::Ready(Some(Ok(Bytes::from(bytes)))) + } + } + } + + impl MessageBody for &'static str { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let string = mem::take(self.get_mut()); + let bytes = Bytes::from_static(string.as_bytes()); + Poll::Ready(Some(Ok(bytes))) + } + } + } + + impl MessageBody for String { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let string = mem::take(self.get_mut()); + Poll::Ready(Some(Ok(Bytes::from(string)))) + } + } + } + + impl MessageBody for bytestring::ByteString { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + let string = mem::take(self.get_mut()); + Poll::Ready(Some(Ok(string.into_bytes()))) } } } @@ -202,6 +272,7 @@ impl MessageBody for MessageBodyMapErr where B: MessageBody, F: FnOnce(B::Error) -> E, + E: Into>, { type Error = E; @@ -226,3 +297,129 @@ where } } } + +#[cfg(test)] +mod tests { + use actix_rt::pin; + use actix_utils::future::poll_fn; + use bytes::{Bytes, BytesMut}; + + use super::*; + + macro_rules! assert_poll_next { + ($pin:expr, $exp:expr) => { + assert_eq!( + poll_fn(|cx| $pin.as_mut().poll_next(cx)) + .await + .unwrap() // unwrap option + .unwrap(), // unwrap result + $exp + ); + }; + } + + macro_rules! assert_poll_next_none { + ($pin:expr) => { + assert!(poll_fn(|cx| $pin.as_mut().poll_next(cx)).await.is_none()); + }; + } + + #[actix_rt::test] + async fn boxing_equivalence() { + assert_eq!(().size(), BodySize::Sized(0)); + assert_eq!(().size(), Box::new(()).size()); + assert_eq!(().size(), Box::pin(()).size()); + + let pl = Box::new(()); + pin!(pl); + assert_poll_next_none!(pl); + + let mut pl = Box::pin(()); + assert_poll_next_none!(pl); + } + + #[actix_rt::test] + async fn test_unit() { + let pl = (); + assert_eq!(pl.size(), BodySize::Sized(0)); + pin!(pl); + assert_poll_next_none!(pl); + } + + #[actix_rt::test] + async fn test_static_str() { + assert_eq!("".size(), BodySize::Sized(0)); + assert_eq!("test".size(), BodySize::Sized(4)); + + let pl = "test"; + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + #[actix_rt::test] + async fn test_static_bytes() { + assert_eq!(b"".as_ref().size(), BodySize::Sized(0)); + assert_eq!(b"test".as_ref().size(), BodySize::Sized(4)); + + let pl = b"test".as_ref(); + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + #[actix_rt::test] + async fn test_vec() { + assert_eq!(vec![0; 0].size(), BodySize::Sized(0)); + assert_eq!(Vec::from("test").size(), BodySize::Sized(4)); + + let pl = Vec::from("test"); + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + #[actix_rt::test] + async fn test_bytes() { + assert_eq!(Bytes::new().size(), BodySize::Sized(0)); + assert_eq!(Bytes::from_static(b"test").size(), BodySize::Sized(4)); + + let pl = Bytes::from_static(b"test"); + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + #[actix_rt::test] + async fn test_bytes_mut() { + assert_eq!(BytesMut::new().size(), BodySize::Sized(0)); + assert_eq!(BytesMut::from(b"test".as_ref()).size(), BodySize::Sized(4)); + + let pl = BytesMut::from("test"); + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + #[actix_rt::test] + async fn test_string() { + assert_eq!(String::new().size(), BodySize::Sized(0)); + assert_eq!("test".to_owned().size(), BodySize::Sized(4)); + + let pl = "test".to_owned(); + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + // down-casting used to be done with a method on MessageBody trait + // test is kept to demonstrate equivalence of Any trait + #[actix_rt::test] + async fn test_body_casting() { + let mut body = String::from("hello cast"); + // let mut resp_body: &mut dyn MessageBody = &mut body; + let resp_body: &mut dyn std::any::Any = &mut body; + let body = resp_body.downcast_ref::().unwrap(); + assert_eq!(body, "hello cast"); + let body = &mut resp_body.downcast_mut::().unwrap(); + body.push('!'); + let body = resp_body.downcast_ref::().unwrap(); + assert_eq!(body, "hello cast!"); + let not_body = resp_body.downcast_ref::<()>(); + assert!(not_body.is_none()); + } +} diff --git a/actix-http/src/body/mod.rs b/actix-http/src/body/mod.rs index df6c6b08a..af7c4626f 100644 --- a/actix-http/src/body/mod.rs +++ b/actix-http/src/body/mod.rs @@ -1,272 +1,20 @@ //! Traits and structures to aid consuming and writing HTTP payloads. -use std::task::Poll; - -use actix_rt::pin; -use actix_utils::future::poll_fn; -use bytes::{Bytes, BytesMut}; -use futures_core::ready; - -#[allow(clippy::module_inception)] -mod body; mod body_stream; +mod boxed; +mod either; mod message_body; +mod none; mod size; mod sized_stream; +mod utils; -#[allow(deprecated)] -pub use self::body::{AnyBody, Body, BoxBody}; pub use self::body_stream::BodyStream; +pub use self::boxed::BoxBody; +pub use self::either::EitherBody; pub use self::message_body::MessageBody; pub(crate) use self::message_body::MessageBodyMapErr; +pub use self::none::None; pub use self::size::BodySize; pub use self::sized_stream::SizedStream; - -/// Collects the body produced by a `MessageBody` implementation into `Bytes`. -/// -/// Any errors produced by the body stream are returned immediately. -/// -/// # Examples -/// ``` -/// use actix_http::body::{AnyBody, to_bytes}; -/// use bytes::Bytes; -/// -/// # async fn test_to_bytes() { -/// let body = AnyBody::none(); -/// let bytes = to_bytes(body).await.unwrap(); -/// assert!(bytes.is_empty()); -/// -/// let body = AnyBody::copy_from_slice(b"123"); -/// let bytes = to_bytes(body).await.unwrap(); -/// assert_eq!(bytes, b"123"[..]); -/// # } -/// ``` -pub async fn to_bytes(body: B) -> Result { - let cap = match body.size() { - BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()), - BodySize::Sized(size) => size as usize, - // good enough first guess for chunk size - BodySize::Stream => 32_768, - }; - - let mut buf = BytesMut::with_capacity(cap); - - pin!(body); - - poll_fn(|cx| loop { - let body = body.as_mut(); - - match ready!(body.poll_next(cx)) { - Some(Ok(bytes)) => buf.extend_from_slice(&*bytes), - None => return Poll::Ready(Ok(())), - Some(Err(err)) => return Poll::Ready(Err(err)), - } - }) - .await?; - - Ok(buf.freeze()) -} - -#[cfg(test)] -mod tests { - use std::pin::Pin; - - use actix_rt::pin; - use actix_utils::future::poll_fn; - use bytes::{Bytes, BytesMut}; - - use super::{to_bytes, AnyBody as TestAnyBody, BodySize, MessageBody as _}; - - impl AnyBody { - pub(crate) fn get_ref(&self) -> &[u8] { - match *self { - AnyBody::Bytes(ref bin) => bin, - _ => panic!(), - } - } - } - - /// AnyBody alias because rustc does not (can not?) infer the default type parameter. - type AnyBody = TestAnyBody; - - #[actix_rt::test] - async fn test_static_str() { - assert_eq!(AnyBody::from("").size(), BodySize::Sized(0)); - assert_eq!(AnyBody::from("test").size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from("test").get_ref(), b"test"); - - assert_eq!("test".size(), BodySize::Sized(4)); - assert_eq!( - poll_fn(|cx| Pin::new(&mut "test").poll_next(cx)) - .await - .unwrap() - .ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_static_bytes() { - assert_eq!(AnyBody::from(b"test".as_ref()).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(b"test".as_ref()).get_ref(), b"test"); - assert_eq!( - AnyBody::copy_from_slice(b"test".as_ref()).size(), - BodySize::Sized(4) - ); - assert_eq!( - AnyBody::copy_from_slice(b"test".as_ref()).get_ref(), - b"test" - ); - let sb = Bytes::from(&b"test"[..]); - pin!(sb); - - assert_eq!(sb.size(), BodySize::Sized(4)); - assert_eq!( - poll_fn(|cx| sb.as_mut().poll_next(cx)).await.unwrap().ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_vec() { - assert_eq!(AnyBody::from(Vec::from("test")).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(Vec::from("test")).get_ref(), b"test"); - let test_vec = Vec::from("test"); - pin!(test_vec); - - assert_eq!(test_vec.size(), BodySize::Sized(4)); - assert_eq!( - poll_fn(|cx| test_vec.as_mut().poll_next(cx)) - .await - .unwrap() - .ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_bytes() { - let b = Bytes::from("test"); - assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); - pin!(b); - - assert_eq!(b.size(), BodySize::Sized(4)); - assert_eq!( - poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_bytes_mut() { - let b = BytesMut::from("test"); - assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); - pin!(b); - - assert_eq!(b.size(), BodySize::Sized(4)); - assert_eq!( - poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_string() { - let b = "test".to_owned(); - assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); - assert_eq!(AnyBody::from(&b).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(&b).get_ref(), b"test"); - pin!(b); - - assert_eq!(b.size(), BodySize::Sized(4)); - assert_eq!( - poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_unit() { - assert_eq!(().size(), BodySize::Sized(0)); - assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx)) - .await - .is_none()); - } - - #[actix_rt::test] - async fn test_box_and_pin() { - let val = Box::new(()); - pin!(val); - assert_eq!(val.size(), BodySize::Sized(0)); - assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none()); - - let mut val = Box::pin(()); - assert_eq!(val.size(), BodySize::Sized(0)); - assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none()); - } - - #[actix_rt::test] - async fn test_body_eq() { - assert!( - AnyBody::Bytes(Bytes::from_static(b"1")) - == AnyBody::Bytes(Bytes::from_static(b"1")) - ); - assert!(AnyBody::Bytes(Bytes::from_static(b"1")) != AnyBody::None); - } - - #[actix_rt::test] - async fn test_body_debug() { - assert!(format!("{:?}", AnyBody::None).contains("Body::None")); - assert!(format!("{:?}", AnyBody::from(Bytes::from_static(b"1"))).contains('1')); - } - - #[actix_rt::test] - async fn test_serde_json() { - use serde_json::{json, Value}; - assert_eq!( - AnyBody::from( - serde_json::to_vec(&Value::String("test".to_owned())).unwrap() - ) - .size(), - BodySize::Sized(6) - ); - assert_eq!( - AnyBody::from( - serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap() - ) - .size(), - BodySize::Sized(25) - ); - } - - // down-casting used to be done with a method on MessageBody trait - // test is kept to demonstrate equivalence of Any trait - #[actix_rt::test] - async fn test_body_casting() { - let mut body = String::from("hello cast"); - // let mut resp_body: &mut dyn MessageBody = &mut body; - let resp_body: &mut dyn std::any::Any = &mut body; - let body = resp_body.downcast_ref::().unwrap(); - assert_eq!(body, "hello cast"); - let body = &mut resp_body.downcast_mut::().unwrap(); - body.push('!'); - let body = resp_body.downcast_ref::().unwrap(); - assert_eq!(body, "hello cast!"); - let not_body = resp_body.downcast_ref::<()>(); - assert!(not_body.is_none()); - } - - #[actix_rt::test] - async fn test_to_bytes() { - let body = AnyBody::empty(); - let bytes = to_bytes(body).await.unwrap(); - assert!(bytes.is_empty()); - - let body = AnyBody::copy_from_slice(b"123"); - let bytes = to_bytes(body).await.unwrap(); - assert_eq!(bytes, b"123"[..]); - } -} +pub use self::utils::to_bytes; diff --git a/actix-http/src/body/none.rs b/actix-http/src/body/none.rs new file mode 100644 index 000000000..0fc7c8c9f --- /dev/null +++ b/actix-http/src/body/none.rs @@ -0,0 +1,43 @@ +use std::{ + convert::Infallible, + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::Bytes; + +use super::{BodySize, MessageBody}; + +/// Body type for responses that forbid payloads. +/// +/// Distinct from an empty response which would contain a Content-Length header. +/// +/// For an "empty" body, use `()` or `Bytes::new()`. +#[derive(Debug, Clone, Copy, Default)] +#[non_exhaustive] +pub struct None; + +impl None { + /// Constructs new "none" body. + #[inline] + pub fn new() -> Self { + None + } +} + +impl MessageBody for None { + type Error = Infallible; + + #[inline] + fn size(&self) -> BodySize { + BodySize::None + } + + #[inline] + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + Poll::Ready(Option::None) + } +} diff --git a/actix-http/src/body/size.rs b/actix-http/src/body/size.rs index e238eadac..d64af9d44 100644 --- a/actix-http/src/body/size.rs +++ b/actix-http/src/body/size.rs @@ -18,7 +18,7 @@ pub enum BodySize { } impl BodySize { - /// Returns true if size hint indicates no or empty body. + /// Returns true if size hint indicates omitted or empty body. /// /// Streams will return false because it cannot be known without reading the stream. /// diff --git a/actix-http/src/body/sized_stream.rs b/actix-http/src/body/sized_stream.rs index b92de44cc..c8606897d 100644 --- a/actix-http/src/body/sized_stream.rs +++ b/actix-http/src/body/sized_stream.rs @@ -32,6 +32,8 @@ where } } +// TODO: from_infallible method + impl MessageBody for SizedStream where S: Stream>, diff --git a/actix-http/src/body/utils.rs b/actix-http/src/body/utils.rs new file mode 100644 index 000000000..a421ffd76 --- /dev/null +++ b/actix-http/src/body/utils.rs @@ -0,0 +1,78 @@ +use std::task::Poll; + +use actix_rt::pin; +use actix_utils::future::poll_fn; +use bytes::{Bytes, BytesMut}; +use futures_core::ready; + +use super::{BodySize, MessageBody}; + +/// Collects the body produced by a `MessageBody` implementation into `Bytes`. +/// +/// Any errors produced by the body stream are returned immediately. +/// +/// # Examples +/// ``` +/// use actix_http::body::{self, to_bytes}; +/// use bytes::Bytes; +/// +/// # async fn test_to_bytes() { +/// let body = body::None::new(); +/// let bytes = to_bytes(body).await.unwrap(); +/// assert!(bytes.is_empty()); +/// +/// let body = Bytes::from_static(b"123"); +/// let bytes = to_bytes(body).await.unwrap(); +/// assert_eq!(bytes, b"123"[..]); +/// # } +/// ``` +pub async fn to_bytes(body: B) -> Result { + let cap = match body.size() { + BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()), + BodySize::Sized(size) => size as usize, + // good enough first guess for chunk size + BodySize::Stream => 32_768, + }; + + let mut buf = BytesMut::with_capacity(cap); + + pin!(body); + + poll_fn(|cx| loop { + let body = body.as_mut(); + + match ready!(body.poll_next(cx)) { + Some(Ok(bytes)) => buf.extend_from_slice(&*bytes), + None => return Poll::Ready(Ok(())), + Some(Err(err)) => return Poll::Ready(Err(err)), + } + }) + .await?; + + Ok(buf.freeze()) +} + +#[cfg(test)] +mod test { + use futures_util::{stream, StreamExt as _}; + + use super::*; + use crate::{body::BodyStream, Error}; + + #[actix_rt::test] + async fn test_to_bytes() { + let bytes = to_bytes(()).await.unwrap(); + assert!(bytes.is_empty()); + + let body = Bytes::from_static(b"123"); + let bytes = to_bytes(body).await.unwrap(); + assert_eq!(bytes, b"123"[..]); + + let stream = + stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")]) + .map(Ok::<_, Error>); + let body = BodyStream::new(stream); + let bytes = to_bytes(body).await.unwrap(); + assert_eq!(bytes, b"123abc"[..]); + } +} diff --git a/actix-http/src/builder.rs b/actix-http/src/builder.rs index 4e68dc920..ca821f1d9 100644 --- a/actix-http/src/builder.rs +++ b/actix-http/src/builder.rs @@ -1,10 +1,10 @@ -use std::{error::Error as StdError, fmt, marker::PhantomData, net, rc::Rc}; +use std::{fmt, marker::PhantomData, net, rc::Rc}; use actix_codec::Framed; use actix_service::{IntoServiceFactory, Service, ServiceFactory}; use crate::{ - body::{AnyBody, MessageBody}, + body::{BoxBody, MessageBody}, config::{KeepAlive, ServiceConfig}, h1::{self, ExpectHandler, H1Service, UpgradeHandler}, h2::H2Service, @@ -31,7 +31,7 @@ pub struct HttpServiceBuilder { impl HttpServiceBuilder where S: ServiceFactory, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, >::Future: 'static, { @@ -54,11 +54,11 @@ where impl HttpServiceBuilder where S: ServiceFactory, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, >::Future: 'static, X: ServiceFactory, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U::Error: fmt::Display, @@ -120,7 +120,7 @@ where where F: IntoServiceFactory, X1: ServiceFactory, - X1::Error: Into>, + X1::Error: Into>, X1::InitError: fmt::Debug, { HttpServiceBuilder { @@ -178,7 +178,7 @@ where where B: MessageBody, F: IntoServiceFactory, - S::Error: Into>, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, { @@ -200,12 +200,11 @@ where pub fn h2(self, service: F) -> H2Service where F: IntoServiceFactory, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, B: MessageBody + 'static, - B::Error: Into>, { let cfg = ServiceConfig::new( self.keep_alive, @@ -223,12 +222,11 @@ where pub fn finish(self, service: F) -> HttpService where F: IntoServiceFactory, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, B: MessageBody + 'static, - B::Error: Into>, { let cfg = ServiceConfig::new( self.keep_alive, diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 62100ff1d..49e5663dc 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -12,7 +12,7 @@ use actix_rt::task::{spawn_blocking, JoinHandle}; use bytes::Bytes; use derive_more::Display; use futures_core::ready; -use pin_project::pin_project; +use pin_project_lite::pin_project; #[cfg(feature = "compress-brotli")] use brotli2::write::BrotliEncoder; @@ -23,8 +23,10 @@ use flate2::write::{GzEncoder, ZlibEncoder}; #[cfg(feature = "compress-zstd")] use zstd::stream::write::Encoder as ZstdEncoder; +use super::Writer; use crate::{ - body::{AnyBody, BodySize, MessageBody}, + body::{BodySize, MessageBody}, + error::BlockingError, http::{ header::{ContentEncoding, CONTENT_ENCODING}, HeaderValue, StatusCode, @@ -32,84 +34,92 @@ use crate::{ ResponseHead, }; -use super::Writer; -use crate::error::BlockingError; - const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024; -#[pin_project] -pub struct Encoder { - eof: bool, - #[pin] - body: EncoderBody, - encoder: Option, - fut: Option>>, +pin_project! { + pub struct Encoder { + #[pin] + body: EncoderBody, + encoder: Option, + fut: Option>>, + eof: bool, + } } impl Encoder { + fn none() -> Self { + Encoder { + body: EncoderBody::None, + encoder: None, + fut: None, + eof: true, + } + } + pub fn response( encoding: ContentEncoding, head: &mut ResponseHead, - body: AnyBody, - ) -> AnyBody> { + body: B, + ) -> Self { let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING) || head.status == StatusCode::SWITCHING_PROTOCOLS || head.status == StatusCode::NO_CONTENT || encoding == ContentEncoding::Identity || encoding == ContentEncoding::Auto); - let body = match body { - AnyBody::None => return AnyBody::None, - AnyBody::Bytes(buf) => { - if can_encode { - EncoderBody::Bytes(buf) - } else { - return AnyBody::Bytes(buf); - } - } - AnyBody::Body(body) => EncoderBody::Stream(body), - }; + match body.size() { + // no need to compress an empty body + BodySize::None => return Self::none(), + + // we cannot assume that Sized is not a stream + BodySize::Sized(_) | BodySize::Stream => {} + } + + // TODO potentially some optimisation for single-chunk responses here by trying to read the + // payload eagerly, stopping after 2 polls if the first is a chunk and the second is None if can_encode { - // Modify response body only if encoder is not None + // Modify response body only if encoder is set if let Some(enc) = ContentEncoder::encoder(encoding) { update_head(encoding, head); head.no_chunking(false); - return AnyBody::Body(Encoder { - body, - eof: false, - fut: None, + return Encoder { + body: EncoderBody::Stream { body }, encoder: Some(enc), - }); + fut: None, + eof: false, + }; } } - AnyBody::Body(Encoder { - body, - eof: false, - fut: None, + Encoder { + body: EncoderBody::Stream { body }, encoder: None, - }) + fut: None, + eof: false, + } } } -#[pin_project(project = EncoderBodyProj)] -enum EncoderBody { - Bytes(Bytes), - Stream(#[pin] B), +pin_project! { + #[project = EncoderBodyProj] + enum EncoderBody { + None, + Stream { #[pin] body: B }, + } } impl MessageBody for EncoderBody where B: MessageBody, { - type Error = EncoderError; + type Error = EncoderError; fn size(&self) -> BodySize { match self { - EncoderBody::Bytes(ref b) => b.size(), - EncoderBody::Stream(ref b) => b.size(), + EncoderBody::None => BodySize::None, + EncoderBody::Stream { body } => body.size(), } } @@ -118,14 +128,11 @@ where cx: &mut Context<'_>, ) -> Poll>> { match self.project() { - EncoderBodyProj::Bytes(b) => { - if b.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(std::mem::take(b)))) - } - } - EncoderBodyProj::Stream(b) => b.poll_next(cx).map_err(EncoderError::Body), + EncoderBodyProj::None => Poll::Ready(None), + + EncoderBodyProj::Stream { body } => body + .poll_next(cx) + .map_err(|err| EncoderError::Body(err.into())), } } } @@ -134,7 +141,7 @@ impl MessageBody for Encoder where B: MessageBody, { - type Error = EncoderError; + type Error = EncoderError; fn size(&self) -> BodySize { if self.encoder.is_none() { @@ -197,6 +204,7 @@ where None => { if let Some(encoder) = this.encoder.take() { let chunk = encoder.finish().map_err(EncoderError::Io)?; + if chunk.is_empty() { return Poll::Ready(None); } else { @@ -222,12 +230,15 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { enum ContentEncoder { #[cfg(feature = "compress-gzip")] Deflate(ZlibEncoder), + #[cfg(feature = "compress-gzip")] Gzip(GzEncoder), + #[cfg(feature = "compress-brotli")] Br(BrotliEncoder), - // We need explicit 'static lifetime here because ZstdEncoder need lifetime - // argument, and we use `spawn_blocking` in `Encoder::poll_next` that require `FnOnce() -> R + Send + 'static` + + // Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we + // use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`. #[cfg(feature = "compress-zstd")] Zstd(ZstdEncoder<'static, Writer>), } @@ -240,20 +251,24 @@ impl ContentEncoder { Writer::new(), flate2::Compression::fast(), ))), + #[cfg(feature = "compress-gzip")] ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new( Writer::new(), flate2::Compression::fast(), ))), + #[cfg(feature = "compress-brotli")] ContentEncoding::Br => { Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) } + #[cfg(feature = "compress-zstd")] ContentEncoding::Zstd => { let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?; Some(ContentEncoder::Zstd(encoder)) } + _ => None, } } @@ -263,10 +278,13 @@ impl ContentEncoder { match *self { #[cfg(feature = "compress-brotli")] ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), + #[cfg(feature = "compress-gzip")] ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), + #[cfg(feature = "compress-gzip")] ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), + #[cfg(feature = "compress-zstd")] ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(), } @@ -279,16 +297,19 @@ impl ContentEncoder { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, + #[cfg(feature = "compress-gzip")] ContentEncoder::Gzip(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, + #[cfg(feature = "compress-gzip")] ContentEncoder::Deflate(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, + #[cfg(feature = "compress-zstd")] ContentEncoder::Zstd(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), @@ -307,6 +328,7 @@ impl ContentEncoder { Err(err) } }, + #[cfg(feature = "compress-gzip")] ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), @@ -315,6 +337,7 @@ impl ContentEncoder { Err(err) } }, + #[cfg(feature = "compress-gzip")] ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), @@ -323,6 +346,7 @@ impl ContentEncoder { Err(err) } }, + #[cfg(feature = "compress-zstd")] ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), @@ -337,9 +361,9 @@ impl ContentEncoder { #[derive(Debug, Display)] #[non_exhaustive] -pub enum EncoderError { +pub enum EncoderError { #[display(fmt = "body")] - Body(E), + Body(Box), #[display(fmt = "blocking")] Blocking(BlockingError), @@ -348,18 +372,18 @@ pub enum EncoderError { Io(io::Error), } -impl StdError for EncoderError { +impl StdError for EncoderError { fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { - EncoderError::Body(err) => Some(err), + EncoderError::Body(err) => Some(&**err), EncoderError::Blocking(err) => Some(err), EncoderError::Io(err) => Some(err), } } } -impl From> for crate::Error { - fn from(err: EncoderError) -> Self { +impl From for crate::Error { + fn from(err: EncoderError) -> Self { crate::Error::new_encoder().with_cause(err) } } diff --git a/actix-http/src/encoding/mod.rs b/actix-http/src/encoding/mod.rs index cb271c638..d51dd66c0 100644 --- a/actix-http/src/encoding/mod.rs +++ b/actix-http/src/encoding/mod.rs @@ -10,6 +10,9 @@ mod encoder; pub use self::decoder::Decoder; pub use self::encoder::Encoder; +/// Special-purpose writer for streaming (de-)compression. +/// +/// Pre-allocates 8KiB of capacity. pub(self) struct Writer { buf: BytesMut, } diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 970c0c564..231e90e57 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -5,7 +5,7 @@ use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Err use derive_more::{Display, Error, From}; use http::{uri::InvalidUri, StatusCode}; -use crate::{body::AnyBody, ws, Response}; +use crate::{body::BoxBody, ws, Response}; pub use http::Error as HttpError; @@ -66,14 +66,15 @@ impl Error { } } -impl From for Response> { +impl From for Response { fn from(err: Error) -> Self { + // TODO: more appropriate error status codes, usage assessment needed let status_code = match err.inner.kind { Kind::Parse => StatusCode::BAD_REQUEST, _ => StatusCode::INTERNAL_SERVER_ERROR, }; - Response::new(status_code).set_body(AnyBody::from(err.to_string())) + Response::new(status_code).set_body(BoxBody::new(err.to_string())) } } @@ -132,12 +133,6 @@ impl From for Error { } } -impl From for Error { - fn from(err: ws::ProtocolError) -> Self { - Self::new_ws().with_cause(err) - } -} - impl From for Error { fn from(err: HttpError) -> Self { Self::new_http().with_cause(err) @@ -150,6 +145,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: ws::ProtocolError) -> Self { + Self::new_ws().with_cause(err) + } +} + /// A set of errors that can occur during parsing HTTP streams. #[derive(Debug, Display, Error)] #[non_exhaustive] @@ -240,7 +241,7 @@ impl From for Error { } } -impl From for Response { +impl From for Response { fn from(err: ParseError) -> Self { Error::from(err).into() } @@ -337,7 +338,7 @@ pub enum DispatchError { /// Service error // FIXME: display and error type #[display(fmt = "Service Error")] - Service(#[error(not(source))] Response), + Service(#[error(not(source))] Response), /// Body error // FIXME: display and error type @@ -421,11 +422,11 @@ mod tests { #[test] fn test_into_response() { - let resp: Response = ParseError::Incomplete.into(); + let resp: Response = ParseError::Incomplete.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into(); - let resp: Response = Error::new_http().with_cause(err).into(); + let resp: Response = Error::new_http().with_cause(err).into(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } @@ -450,7 +451,7 @@ mod tests { fn test_error_http_response() { let orig = io::Error::new(io::ErrorKind::Other, "other"); let err = Error::new_io().with_cause(orig); - let resp: Response = err.into(); + let resp: Response = err.into(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 163d84f5b..6695d1bf3 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -1,6 +1,5 @@ use std::{ collections::VecDeque, - error::Error as StdError, fmt, future::Future, io, mem, net, @@ -19,7 +18,7 @@ use log::{error, trace}; use pin_project::pin_project; use crate::{ - body::{AnyBody, BodySize, MessageBody}, + body::{BodySize, BoxBody, MessageBody}, config::ServiceConfig, error::{DispatchError, ParseError, PayloadError}, service::HttpFlow, @@ -51,13 +50,12 @@ bitflags! { pub struct Dispatcher where S: Service, - S::Error: Into>, + S::Error: Into>, B: MessageBody, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -73,13 +71,12 @@ where enum DispatcherState where S: Service, - S::Error: Into>, + S::Error: Into>, B: MessageBody, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -92,13 +89,12 @@ where struct InnerDispatcher where S: Service, - S::Error: Into>, + S::Error: Into>, B: MessageBody, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -137,13 +133,12 @@ where X: Service, B: MessageBody, - B::Error: Into>, { None, ExpectCall(#[pin] X::Future), ServiceCall(#[pin] S::Future), SendPayload(#[pin] B), - SendErrorPayload(#[pin] AnyBody), + SendErrorPayload(#[pin] BoxBody), } impl State @@ -153,7 +148,6 @@ where X: Service, B: MessageBody, - B::Error: Into>, { fn is_empty(&self) -> bool { matches!(self, State::None) @@ -171,14 +165,13 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into>, + S::Error: Into>, S::Response: Into>, B: MessageBody, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -232,14 +225,13 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into>, + S::Error: Into>, S::Response: Into>, B: MessageBody, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -335,7 +327,7 @@ where fn send_error_response( mut self: Pin<&mut Self>, message: Response<()>, - body: AnyBody, + body: BoxBody, ) -> Result<(), DispatchError> { let size = self.as_mut().send_response_inner(message, &body)?; let state = match size { @@ -380,7 +372,7 @@ where // send_response would update InnerDispatcher state to SendPayload or // None(If response body is empty). // continue loop to poll it. - self.as_mut().send_error_response(res, AnyBody::empty())?; + self.as_mut().send_error_response(res, BoxBody::new(()))?; } // return with upgrade request and poll it exclusively. @@ -400,7 +392,7 @@ where // send service call error as response Poll::Ready(Err(err)) => { - let res: Response = err.into(); + let res: Response = err.into(); let (res, body) = res.replace_body(()); self.as_mut().send_error_response(res, body)?; } @@ -497,7 +489,7 @@ where // send expect error as response Poll::Ready(Err(err)) => { - let res: Response = err.into(); + let res: Response = err.into(); let (res, body) = res.replace_body(()); self.as_mut().send_error_response(res, body)?; } @@ -546,7 +538,7 @@ where // to notify the dispatcher a new state is set and the outer loop // should be continue. Poll::Ready(Err(err)) => { - let res: Response = err.into(); + let res: Response = err.into(); let (res, body) = res.replace_body(()); return self.send_error_response(res, body); } @@ -566,7 +558,7 @@ where Poll::Pending => Ok(()), // see the comment on ExpectCall state branch's Ready(Err(err)). Poll::Ready(Err(err)) => { - let res: Response = err.into(); + let res: Response = err.into(); let (res, body) = res.replace_body(()); self.send_error_response(res, body) } @@ -772,7 +764,7 @@ where trace!("Slow request timeout"); let _ = self.as_mut().send_error_response( Response::with_body(StatusCode::REQUEST_TIMEOUT, ()), - AnyBody::empty(), + BoxBody::new(()), ); this = self.project(); this.flags.insert(Flags::STARTED | Flags::SHUTDOWN); @@ -909,14 +901,13 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into>, + S::Error: Into>, S::Response: Into>, B: MessageBody, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -1067,17 +1058,19 @@ mod tests { } } - fn ok_service() -> impl Service, Error = Error> + fn ok_service( + ) -> impl Service, Error = Error> { fn_service(|_req: Request| ready(Ok::<_, Error>(Response::ok()))) } fn echo_path_service( - ) -> impl Service, Error = Error> { + ) -> impl Service, Error = Error> + { fn_service(|req: Request| { let path = req.path().as_bytes(); ready(Ok::<_, Error>( - Response::ok().set_body(AnyBody::copy_from_slice(path)), + Response::ok().set_body(Bytes::copy_from_slice(path)), )) }) } diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index 8a50417d2..70e83901c 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -1,5 +1,4 @@ use std::{ - error::Error as StdError, fmt, marker::PhantomData, net, @@ -16,7 +15,7 @@ use actix_utils::future::ready; use futures_core::future::LocalBoxFuture; use crate::{ - body::{AnyBody, MessageBody}, + body::{BoxBody, MessageBody}, config::ServiceConfig, error::DispatchError, service::HttpServiceHandler, @@ -38,7 +37,7 @@ pub struct H1Service { impl H1Service where S: ServiceFactory, - S::Error: Into>, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, B: MessageBody, @@ -63,21 +62,20 @@ impl H1Service where S: ServiceFactory, S::Future: 'static, - S::Error: Into>, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, B: MessageBody, - B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U::Future: 'static, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create simple tcp stream service @@ -114,16 +112,15 @@ mod openssl { where S: ServiceFactory, S::Future: 'static, - S::Error: Into>, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, B: MessageBody, - B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory< @@ -132,7 +129,7 @@ mod openssl { Response = (), >, U::Future: 'static, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create OpenSSL based service. @@ -177,16 +174,15 @@ mod rustls { where S: ServiceFactory, S::Future: 'static, - S::Error: Into>, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, B: MessageBody, - B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory< @@ -195,7 +191,7 @@ mod rustls { Response = (), >, U::Future: 'static, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create Rustls based service. @@ -226,7 +222,7 @@ mod rustls { impl H1Service where S: ServiceFactory, - S::Error: Into>, + S::Error: Into>, S::Response: Into>, S::InitError: fmt::Debug, B: MessageBody, @@ -234,7 +230,7 @@ where pub fn expect(self, expect: X1) -> H1Service where X1: ServiceFactory, - X1::Error: Into>, + X1::Error: Into>, X1::InitError: fmt::Debug, { H1Service { @@ -277,21 +273,20 @@ where S: ServiceFactory, S::Future: 'static, - S::Error: Into>, + S::Error: Into>, S::Response: Into>, S::InitError: fmt::Debug, B: MessageBody, - B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U::Future: 'static, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { type Response = (); @@ -347,17 +342,16 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into>, + S::Error: Into>, S::Response: Into>, B: MessageBody, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, { type Response = (); type Error = DispatchError; diff --git a/actix-http/src/h1/utils.rs b/actix-http/src/h1/utils.rs index 2547f4494..905585a32 100644 --- a/actix-http/src/h1/utils.rs +++ b/actix-http/src/h1/utils.rs @@ -1,22 +1,30 @@ -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use pin_project_lite::pin_project; -use crate::body::{BodySize, MessageBody}; -use crate::error::Error; -use crate::h1::{Codec, Message}; -use crate::response::Response; +use crate::{ + body::{BodySize, MessageBody}, + error::Error, + h1::{Codec, Message}, + response::Response, +}; -/// Send HTTP/1 response -#[pin_project::pin_project] -pub struct SendResponse { - res: Option, BodySize)>>, - #[pin] - body: Option, - #[pin] - framed: Option>, +pin_project! { + /// Send HTTP/1 response + pub struct SendResponse { + res: Option, BodySize)>>, + + #[pin] + body: Option, + + #[pin] + framed: Option>, + } } impl SendResponse diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 607997eb7..6d2f4579a 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -24,7 +24,7 @@ use log::{error, trace}; use pin_project_lite::pin_project; use crate::{ - body::{AnyBody, BodySize, MessageBody}, + body::{BodySize, BoxBody, MessageBody}, config::ServiceConfig, service::HttpFlow, OnConnectData, Payload, Request, Response, ResponseHead, @@ -51,7 +51,7 @@ where { pub(crate) fn new( flow: Rc>, - mut connection: Connection, + mut conn: Connection, on_connect_data: OnConnectData, config: ServiceConfig, peer_addr: Option, @@ -66,14 +66,14 @@ where }) .unwrap_or_else(|| Box::pin(sleep(dur))), on_flight: false, - ping_pong: connection.ping_pong().unwrap(), + ping_pong: conn.ping_pong().unwrap(), }); Self { flow, config, peer_addr, - connection, + connection: conn, on_connect_data, ping_pong, _phantom: PhantomData, @@ -92,12 +92,11 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into>, + S::Error: Into>, S::Future: 'static, S::Response: Into>, B: MessageBody, - B::Error: Into>, { type Output = Result<(), crate::error::DispatchError>; @@ -132,7 +131,7 @@ where let res = match fut.await { Ok(res) => handle_response(res.into(), tx, config).await, Err(err) => { - let res: Response = err.into(); + let res: Response = err.into(); handle_response(res, tx, config).await } }; @@ -207,7 +206,6 @@ async fn handle_response( ) -> Result<(), DispatchError> where B: MessageBody, - B::Error: Into>, { let (res, body) = res.replace_body(()); diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 0ad17ec0a..8a9061b94 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -1,5 +1,4 @@ use std::{ - error::Error as StdError, future::Future, marker::PhantomData, net, @@ -19,7 +18,7 @@ use futures_core::{future::LocalBoxFuture, ready}; use log::error; use crate::{ - body::{AnyBody, MessageBody}, + body::{BoxBody, MessageBody}, config::ServiceConfig, error::DispatchError, service::HttpFlow, @@ -39,12 +38,11 @@ pub struct H2Service { impl H2Service where S: ServiceFactory, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, { /// Create new `H2Service` instance with config. pub(crate) fn with_config>( @@ -70,12 +68,11 @@ impl H2Service where S: ServiceFactory, S::Future: 'static, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, { /// Create plain TCP based service pub fn tcp( @@ -114,12 +111,11 @@ mod openssl { where S: ServiceFactory, S::Future: 'static, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, { /// Create OpenSSL based service. pub fn openssl( @@ -162,12 +158,11 @@ mod rustls { where S: ServiceFactory, S::Future: 'static, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, { /// Create Rustls based service. pub fn rustls( @@ -204,12 +199,11 @@ where S: ServiceFactory, S::Future: 'static, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, { type Response = (); type Error = DispatchError; @@ -244,7 +238,7 @@ where impl H2ServiceHandler where S: Service, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, @@ -267,11 +261,10 @@ impl Service<(T, Option)> for H2ServiceHandler, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, - B::Error: Into>, { type Response = (); type Error = DispatchError; @@ -320,7 +313,7 @@ pub struct H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, @@ -332,11 +325,10 @@ impl Future for H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody, - B::Error: Into>, { type Output = Result<(), DispatchError>; diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index e0bed0631..c8e1ce6db 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -46,8 +46,8 @@ pub trait Head: Default + 'static { #[derive(Debug)] pub struct RequestHead { - pub uri: Uri, pub method: Method, + pub uri: Uri, pub version: Version, pub headers: HeaderMap, pub extensions: RefCell, @@ -58,13 +58,13 @@ pub struct RequestHead { impl Default for RequestHead { fn default() -> RequestHead { RequestHead { - uri: Uri::default(), method: Method::default(), + uri: Uri::default(), version: Version::HTTP_11, headers: HeaderMap::with_capacity(16), - flags: Flags::empty(), - peer_addr: None, extensions: RefCell::new(Extensions::new()), + peer_addr: None, + flags: Flags::empty(), } } } @@ -192,6 +192,7 @@ impl RequestHead { } #[derive(Debug)] +#[allow(clippy::large_enum_variant)] pub enum RequestHeadType { Owned(RequestHead), Rc(Rc, Option), diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index 47f1c37e2..ad41094ae 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -6,14 +6,15 @@ use std::{ }; use bytes::{Bytes, BytesMut}; +use bytestring::ByteString; use crate::{ - body::{AnyBody, MessageBody}, - error::Error, + body::{BoxBody, MessageBody}, extensions::Extensions, + header::{self, IntoHeaderValue}, http::{HeaderMap, StatusCode}, message::{BoxedResponseHead, ResponseHead}, - ResponseBuilder, + Error, ResponseBuilder, }; /// An HTTP response. @@ -22,13 +23,13 @@ pub struct Response { pub(crate) body: B, } -impl Response { +impl Response { /// Constructs a new response with default body. #[inline] pub fn new(status: StatusCode) -> Self { Response { head: BoxedResponseHead::new(status), - body: AnyBody::empty(), + body: BoxBody::new(()), } } @@ -189,6 +190,14 @@ impl Response { } } + #[inline] + pub fn map_into_boxed_body(self) -> Response + where + B: MessageBody + 'static, + { + self.map_body(|_, body| BoxBody::new(body)) + } + /// Returns body, consuming this response. pub fn into_body(self) -> B { self.body @@ -223,81 +232,99 @@ impl Default for Response { } } -impl>, E: Into> From> - for Response +impl>, E: Into> From> + for Response { fn from(res: Result) -> Self { match res { Ok(val) => val.into(), - Err(err) => err.into().into(), + Err(err) => Response::from(err.into()), } } } -impl From for Response { +impl From for Response { fn from(mut builder: ResponseBuilder) -> Self { - builder.finish() + builder.finish().map_into_boxed_body() } } -impl From for Response { +impl From for Response { fn from(val: std::convert::Infallible) -> Self { match val {} } } -impl From<&'static str> for Response { +impl From<&'static str> for Response<&'static str> { fn from(val: &'static str) -> Self { - Response::build(StatusCode::OK) - .content_type(mime::TEXT_PLAIN_UTF_8) - .body(val) + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } -impl From<&'static [u8]> for Response { +impl From<&'static [u8]> for Response<&'static [u8]> { fn from(val: &'static [u8]) -> Self { - Response::build(StatusCode::OK) - .content_type(mime::APPLICATION_OCTET_STREAM) - .body(val) + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } -impl From for Response { +impl From for Response { fn from(val: String) -> Self { - Response::build(StatusCode::OK) - .content_type(mime::TEXT_PLAIN_UTF_8) - .body(val) + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } -impl<'a> From<&'a String> for Response { - fn from(val: &'a String) -> Self { - Response::build(StatusCode::OK) - .content_type(mime::TEXT_PLAIN_UTF_8) - .body(val) +impl From<&String> for Response { + fn from(val: &String) -> Self { + let mut res = Response::with_body(StatusCode::OK, val.clone()); + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } -impl From for Response { +impl From for Response { fn from(val: Bytes) -> Self { - Response::build(StatusCode::OK) - .content_type(mime::APPLICATION_OCTET_STREAM) - .body(val) + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } -impl From for Response { +impl From for Response { fn from(val: BytesMut) -> Self { - Response::build(StatusCode::OK) - .content_type(mime::APPLICATION_OCTET_STREAM) - .body(val) + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res + } +} + +impl From for Response { + fn from(val: ByteString) -> Self { + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } #[cfg(test)] mod tests { use super::*; - use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; + use crate::{ + body::to_bytes, + http::header::{HeaderValue, CONTENT_TYPE, COOKIE}, + }; #[test] fn test_debug() { @@ -309,73 +336,73 @@ mod tests { assert!(dbg.contains("Response")); } - #[test] - fn test_into_response() { - let resp: Response = "test".into(); - assert_eq!(resp.status(), StatusCode::OK); + #[actix_rt::test] + async fn test_into_response() { + let res = Response::from("test"); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); - let resp: Response = b"test".as_ref().into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from(b"test".as_ref()); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); - let resp: Response = "test".to_owned().into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from("test".to_owned()); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); - let resp: Response = (&"test".to_owned()).into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from("test".to_owned()); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); let b = Bytes::from_static(b"test"); - let resp: Response = b.into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from(b); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); let b = Bytes::from_static(b"test"); - let resp: Response = b.into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from(b); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); let b = BytesMut::from("test"); - let resp: Response = b.into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from(b); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); } } diff --git a/actix-http/src/response_builder.rs b/actix-http/src/response_builder.rs index c5fcb625c..0537112d5 100644 --- a/actix-http/src/response_builder.rs +++ b/actix-http/src/response_builder.rs @@ -2,19 +2,11 @@ use std::{ cell::{Ref, RefMut}, - error::Error as StdError, - fmt, - future::Future, - pin::Pin, - str, - task::{Context, Poll}, + fmt, str, }; -use bytes::Bytes; -use futures_core::Stream; - use crate::{ - body::{AnyBody, BodyStream}, + body::{EitherBody, MessageBody}, error::{Error, HttpError}, header::{self, IntoHeaderPair, IntoHeaderValue}, message::{BoxedResponseHead, ConnectionType, ResponseHead}, @@ -235,10 +227,14 @@ impl ResponseBuilder { /// Generate response with a wrapped body. /// /// This `ResponseBuilder` will be left in a useless state. - #[inline] - pub fn body>(&mut self, body: B) -> Response { - self.message_body(body.into()) - .unwrap_or_else(Response::from) + pub fn body(&mut self, body: B) -> Response> + where + B: MessageBody + 'static, + { + match self.message_body(body) { + Ok(res) => res.map_body(|_, body| EitherBody::left(body)), + Err(err) => Response::from(err).map_body(|_, body| EitherBody::right(body)), + } } /// Generate response with a body. @@ -253,24 +249,12 @@ impl ResponseBuilder { Ok(Response { head, body }) } - /// Generate response with a streaming body. - /// - /// This `ResponseBuilder` will be left in a useless state. - #[inline] - pub fn streaming(&mut self, stream: S) -> Response - where - S: Stream> + 'static, - E: Into> + 'static, - { - self.body(AnyBody::new_boxed(BodyStream::new(stream))) - } - /// Generate response with an empty body. /// /// This `ResponseBuilder` will be left in a useless state. #[inline] - pub fn finish(&mut self) -> Response { - self.body(AnyBody::empty()) + pub fn finish(&mut self) -> Response> { + self.body(()) } /// Create an owned `ResponseBuilder`, leaving the original in a useless state. @@ -327,14 +311,6 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder { } } -impl Future for ResponseBuilder { - type Output = Result, Error>; - - fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll { - Poll::Ready(Ok(self.finish())) - } -} - impl fmt::Debug for ResponseBuilder { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let head = self.head.as_ref().unwrap(); @@ -356,8 +332,9 @@ impl fmt::Debug for ResponseBuilder { #[cfg(test)] mod tests { + use bytes::Bytes; + use super::*; - use crate::body::AnyBody; use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; #[test] @@ -383,20 +360,28 @@ mod tests { #[test] fn test_force_close() { let resp = Response::build(StatusCode::OK).force_close().finish(); - assert!(!resp.keep_alive()) + assert!(!resp.keep_alive()); } #[test] fn test_content_type() { let resp = Response::build(StatusCode::OK) .content_type("text/plain") - .body(AnyBody::empty()); - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") + .body(Bytes::new()); + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain"); + + let resp = Response::build(StatusCode::OK) + .content_type(mime::APPLICATION_JAVASCRIPT_UTF_8) + .body(Bytes::new()); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + "application/javascript; charset=utf-8" + ); } #[test] fn test_into_builder() { - let mut resp: Response = "test".into(); + let mut resp: Response<_> = "test".into(); assert_eq!(resp.status(), StatusCode::OK); resp.headers_mut().insert( diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index fb0cccb38..7af34ba05 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -1,5 +1,4 @@ use std::{ - error::Error as StdError, fmt, future::Future, marker::PhantomData, @@ -15,10 +14,10 @@ use actix_service::{ fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _, }; use futures_core::{future::LocalBoxFuture, ready}; -use pin_project::pin_project; +use pin_project_lite::pin_project; use crate::{ - body::{AnyBody, MessageBody}, + body::{BoxBody, MessageBody}, builder::HttpServiceBuilder, config::{KeepAlive, ServiceConfig}, error::DispatchError, @@ -38,7 +37,7 @@ pub struct HttpService { impl HttpService where S: ServiceFactory, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, @@ -53,12 +52,11 @@ where impl HttpService where S: ServiceFactory, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, { /// Create new `HttpService` instance. pub fn new>(service: F) -> Self { @@ -93,7 +91,7 @@ where impl HttpService where S: ServiceFactory, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, @@ -107,7 +105,7 @@ where pub fn expect(self, expect: X1) -> HttpService where X1: ServiceFactory, - X1::Error: Into>, + X1::Error: Into>, X1::InitError: fmt::Debug, { HttpService { @@ -151,17 +149,16 @@ impl HttpService where S: ServiceFactory, S::Future: 'static, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory< @@ -170,7 +167,7 @@ where Response = (), >, U::Future: 'static, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create simple tcp stream service @@ -208,17 +205,16 @@ mod openssl { where S: ServiceFactory, S::Future: 'static, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory< @@ -227,7 +223,7 @@ mod openssl { Response = (), >, U::Future: 'static, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create OpenSSL based service. @@ -281,17 +277,16 @@ mod rustls { where S: ServiceFactory, S::Future: 'static, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory< @@ -300,7 +295,7 @@ mod rustls { Response = (), >, U::Future: 'static, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create Rustls based service. @@ -348,22 +343,21 @@ where S: ServiceFactory, S::Future: 'static, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U::Future: 'static, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { type Response = (); @@ -426,11 +420,11 @@ where impl HttpServiceHandler where S: Service, - S::Error: Into>, + S::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed)>, - U::Error: Into>, + U::Error: Into>, { pub(super) fn new( cfg: ServiceConfig, @@ -450,7 +444,7 @@ where pub(super) fn _poll_ready( &self, cx: &mut Context<'_>, - ) -> Poll>> { + ) -> Poll>> { ready!(self.flow.expect.poll_ready(cx).map_err(Into::into))?; ready!(self.flow.service.poll_ready(cx).map_err(Into::into))?; @@ -486,18 +480,17 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, { type Response = (); type Error = DispatchError; @@ -519,23 +512,27 @@ where match proto { Protocol::Http2 => HttpServiceHandlerResponse { - state: State::H2Handshake(Some(( - h2::handshake_with_timeout(io, &self.cfg), - self.cfg.clone(), - self.flow.clone(), - on_connect_data, - peer_addr, - ))), + state: State::H2Handshake { + handshake: Some(( + h2::handshake_with_timeout(io, &self.cfg), + self.cfg.clone(), + self.flow.clone(), + on_connect_data, + peer_addr, + )), + }, }, Protocol::Http1 => HttpServiceHandlerResponse { - state: State::H1(h1::Dispatcher::new( - io, - self.cfg.clone(), - self.flow.clone(), - on_connect_data, - peer_addr, - )), + state: State::H1 { + dispatcher: h1::Dispatcher::new( + io, + self.cfg.clone(), + self.flow.clone(), + on_connect_data, + peer_addr, + ), + }, }, proto => unimplemented!("Unsupported HTTP version: {:?}.", proto), @@ -543,58 +540,65 @@ where } } -#[pin_project(project = StateProj)] -enum State -where - T: AsyncRead + AsyncWrite + Unpin, +pin_project! { + #[project = StateProj] + enum State + where + T: AsyncRead, + T: AsyncWrite, + T: Unpin, - S: Service, - S::Future: 'static, - S::Error: Into>, + S: Service, + S::Future: 'static, + S::Error: Into>, - B: MessageBody, - B::Error: Into>, + B: MessageBody, - X: Service, - X::Error: Into>, + X: Service, + X::Error: Into>, - U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display, -{ - H1(#[pin] h1::Dispatcher), - H2(#[pin] h2::Dispatcher), - H2Handshake( - Option<( - h2::HandshakeWithTimeout, - ServiceConfig, - Rc>, - OnConnectData, - Option, - )>, - ), + U: Service<(Request, Framed), Response = ()>, + U::Error: fmt::Display, + { + H1 { #[pin] dispatcher: h1::Dispatcher }, + H2 { #[pin] dispatcher: h2::Dispatcher }, + H2Handshake { + handshake: Option<( + h2::HandshakeWithTimeout, + ServiceConfig, + Rc>, + OnConnectData, + Option, + )>, + }, + } } -#[pin_project] -pub struct HttpServiceHandlerResponse -where - T: AsyncRead + AsyncWrite + Unpin, +pin_project! { + pub struct HttpServiceHandlerResponse + where + T: AsyncRead, + T: AsyncWrite, + T: Unpin, - S: Service, - S::Error: Into> + 'static, - S::Future: 'static, - S::Response: Into> + 'static, + S: Service, + S::Error: Into>, + S::Error: 'static, + S::Future: 'static, + S::Response: Into>, + S::Response: 'static, - B: MessageBody, - B::Error: Into>, + B: MessageBody, - X: Service, - X::Error: Into>, + X: Service, + X::Error: Into>, - U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display, -{ - #[pin] - state: State, + U: Service<(Request, Framed), Response = ()>, + U::Error: fmt::Display, + { + #[pin] + state: State, + } } impl Future for HttpServiceHandlerResponse @@ -602,15 +606,14 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -619,23 +622,24 @@ where fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match self.as_mut().project().state.project() { - StateProj::H1(disp) => disp.poll(cx), - StateProj::H2(disp) => disp.poll(cx), - StateProj::H2Handshake(data) => { + StateProj::H1 { dispatcher } => dispatcher.poll(cx), + StateProj::H2 { dispatcher } => dispatcher.poll(cx), + StateProj::H2Handshake { handshake: data } => { match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) { Ok((conn, timer)) => { - let (_, cfg, srv, on_connect_data, peer_addr) = + let (_, config, flow, on_connect_data, peer_addr) = data.take().unwrap(); - self.as_mut().project().state.set(State::H2( - h2::Dispatcher::new( - srv, + + self.as_mut().project().state.set(State::H2 { + dispatcher: h2::Dispatcher::new( + flow, conn, on_connect_data, - cfg, + config, peer_addr, timer, ), - )); + }); self.poll(cx) } Err(err) => { diff --git a/actix-http/src/ws/dispatcher.rs b/actix-http/src/ws/dispatcher.rs index f49cbe5d4..a3f766e9c 100644 --- a/actix-http/src/ws/dispatcher.rs +++ b/actix-http/src/ws/dispatcher.rs @@ -4,17 +4,21 @@ use std::task::{Context, Poll}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_service::{IntoService, Service}; +use pin_project_lite::pin_project; use super::{Codec, Frame, Message}; -#[pin_project::pin_project] -pub struct Dispatcher -where - S: Service + 'static, - T: AsyncRead + AsyncWrite, -{ - #[pin] - inner: inner::Dispatcher, +pin_project! { + pub struct Dispatcher + where + S: Service, + S: 'static, + T: AsyncRead, + T: AsyncWrite, + { + #[pin] + inner: inner::Dispatcher, + } } impl Dispatcher @@ -72,7 +76,7 @@ mod inner { use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed}; - use crate::{body::AnyBody, Response}; + use crate::{body::BoxBody, Response}; /// Framed transport errors pub enum DispatcherError @@ -136,7 +140,7 @@ mod inner { } } - impl From> for Response + impl From> for Response where E: fmt::Debug + fmt::Display, U: Encoder + Decoder, @@ -144,7 +148,7 @@ mod inner { ::Error: fmt::Debug, { fn from(err: DispatcherError) -> Self { - Response::internal_server_error().set_body(AnyBody::from(err.to_string())) + Response::internal_server_error().set_body(BoxBody::new(err.to_string())) } } diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index 70e0e62a2..cb1aa6730 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -8,9 +8,9 @@ use std::io; use derive_more::{Display, Error, From}; use http::{header, Method, StatusCode}; +use crate::body::BoxBody; use crate::{ - body::AnyBody, header::HeaderValue, message::RequestHead, response::Response, - ResponseBuilder, + header::HeaderValue, message::RequestHead, response::Response, ResponseBuilder, }; mod codec; @@ -69,7 +69,7 @@ pub enum ProtocolError { } /// WebSocket handshake errors -#[derive(Debug, PartialEq, Display, Error)] +#[derive(Debug, Clone, Copy, PartialEq, Display, Error)] pub enum HandshakeError { /// Only get method is allowed. #[display(fmt = "Method not allowed.")] @@ -96,8 +96,8 @@ pub enum HandshakeError { BadWebsocketKey, } -impl From<&HandshakeError> for Response { - fn from(err: &HandshakeError) -> Self { +impl From for Response { + fn from(err: HandshakeError) -> Self { match err { HandshakeError::GetMethodRequired => { let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED); @@ -139,9 +139,9 @@ impl From<&HandshakeError> for Response { } } -impl From for Response { - fn from(err: HandshakeError) -> Self { - (&err).into() +impl From<&HandshakeError> for Response { + fn from(err: &HandshakeError) -> Self { + (*err).into() } } @@ -220,9 +220,10 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder { #[cfg(test)] mod tests { + use crate::{header, Method}; + use super::*; - use crate::{body::AnyBody, test::TestRequest}; - use http::{header, Method}; + use crate::test::TestRequest; #[test] fn test_handshake() { @@ -336,17 +337,17 @@ mod tests { #[test] fn test_ws_error_http_response() { - let resp: Response = HandshakeError::GetMethodRequired.into(); + let resp: Response = HandshakeError::GetMethodRequired.into(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - let resp: Response = HandshakeError::NoWebsocketUpgrade.into(); + let resp: Response = HandshakeError::NoWebsocketUpgrade.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: Response = HandshakeError::NoConnectionUpgrade.into(); + let resp: Response = HandshakeError::NoConnectionUpgrade.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: Response = HandshakeError::NoVersionHeader.into(); + let resp: Response = HandshakeError::NoVersionHeader.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: Response = HandshakeError::UnsupportedVersion.into(); + let resp: Response = HandshakeError::UnsupportedVersion.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: Response = HandshakeError::BadWebsocketKey.into(); + let resp: Response = HandshakeError::BadWebsocketKey.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } } diff --git a/actix-http/tests/test_client.rs b/actix-http/tests/test_client.rs index 414266d81..4c923873f 100644 --- a/actix-http/tests/test_client.rs +++ b/actix-http/tests/test_client.rs @@ -1,7 +1,7 @@ use std::convert::Infallible; use actix_http::{ - body::AnyBody, http, http::StatusCode, HttpMessage, HttpService, Request, Response, + body::BoxBody, http, http::StatusCode, HttpMessage, HttpService, Request, Response, }; use actix_http_test::test_server; use actix_service::ServiceFactoryExt; @@ -99,7 +99,7 @@ async fn test_with_query_parameter() { #[display(fmt = "expect failed")] struct ExpectFailed; -impl From for Response { +impl From for Response { fn from(_: ExpectFailed) -> Self { Response::new(StatusCode::EXPECTATION_FAILED) } diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index e7dd78171..86ee17c74 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -5,7 +5,7 @@ extern crate tls_openssl as openssl; use std::{convert::Infallible, io}; use actix_http::{ - body::{AnyBody, SizedStream}, + body::{BodyStream, BoxBody, SizedStream}, error::PayloadError, http::{ header::{self, HeaderValue}, @@ -348,7 +348,7 @@ async fn test_h2_body_chunked_explicit() { ok::<_, Infallible>( Response::build(StatusCode::OK) .insert_header((header::TRANSFER_ENCODING, "chunked")) - .streaming(body), + .body(BodyStream::new(body)), ) }) .openssl(tls_config()) @@ -399,9 +399,11 @@ async fn test_h2_response_http_error_handling() { #[display(fmt = "error")] struct BadRequest; -impl From for Response { +impl From for Response { fn from(err: BadRequest) -> Self { - Response::build(StatusCode::BAD_REQUEST).body(err.to_string()) + Response::build(StatusCode::BAD_REQUEST) + .body(err.to_string()) + .map_into_boxed_body() } } @@ -409,7 +411,7 @@ impl From for Response { async fn test_h2_service_error() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| err::, _>(BadRequest)) + .h2(|_| err::, _>(BadRequest)) .openssl(tls_config()) .map_err(|_| ()) }) diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index b5289bf7c..873752779 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -10,7 +10,7 @@ use std::{ }; use actix_http::{ - body::{AnyBody, SizedStream}, + body::{BodyStream, BoxBody, SizedStream}, error::PayloadError, http::{ header::{self, HeaderName, HeaderValue}, @@ -416,7 +416,7 @@ async fn test_h2_body_chunked_explicit() { ok::<_, Infallible>( Response::build(StatusCode::OK) .insert_header((header::TRANSFER_ENCODING, "chunked")) - .streaming(body), + .body(BodyStream::new(body)), ) }) .rustls(tls_config()) @@ -467,9 +467,9 @@ async fn test_h2_response_http_error_handling() { #[display(fmt = "error")] struct BadRequest; -impl From for Response { +impl From for Response { fn from(_: BadRequest) -> Self { - Response::bad_request().set_body(AnyBody::from("error")) + Response::bad_request().set_body(BoxBody::new("error")) } } @@ -477,7 +477,7 @@ impl From for Response { async fn test_h2_service_error() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| err::, _>(BadRequest)) + .h2(|_| err::, _>(BadRequest)) .rustls(tls_config()) }) .await; @@ -494,7 +494,7 @@ async fn test_h2_service_error() { async fn test_h1_service_error() { let mut srv = test_server(move || { HttpService::build() - .h1(|_| err::, _>(BadRequest)) + .h1(|_| err::, _>(BadRequest)) .rustls(tls_config()) }) .await; diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index 11bc8e939..adf2a28ca 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -6,7 +6,7 @@ use std::{ }; use actix_http::{ - body::{AnyBody, SizedStream}, + body::{self, BodyStream, BoxBody, SizedStream}, header, http, Error, HttpMessage, HttpService, KeepAlive, Request, Response, StatusCode, }; @@ -69,7 +69,7 @@ async fn test_h1_2() { #[display(fmt = "expect failed")] struct ExpectFailed; -impl From for Response { +impl From for Response { fn from(_: ExpectFailed) -> Self { Response::new(StatusCode::EXPECTATION_FAILED) } @@ -622,7 +622,7 @@ async fn test_h1_body_chunked_explicit() { ok::<_, Infallible>( Response::build(StatusCode::OK) .insert_header((header::TRANSFER_ENCODING, "chunked")) - .streaming(body), + .body(BodyStream::new(body)), ) }) .tcp() @@ -656,7 +656,9 @@ async fn test_h1_body_chunked_implicit() { HttpService::build() .h1(|_| { let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); - ok::<_, Infallible>(Response::build(StatusCode::OK).streaming(body)) + ok::<_, Infallible>( + Response::build(StatusCode::OK).body(BodyStream::new(body)), + ) }) .tcp() }) @@ -714,9 +716,9 @@ async fn test_h1_response_http_error_handling() { #[display(fmt = "error")] struct BadRequest; -impl From for Response { +impl From for Response { fn from(_: BadRequest) -> Self { - Response::bad_request().set_body(AnyBody::from("error")) + Response::bad_request().set_body(BoxBody::new("error")) } } @@ -724,7 +726,7 @@ impl From for Response { async fn test_h1_service_error() { let mut srv = test_server(|| { HttpService::build() - .h1(|_| err::, _>(BadRequest)) + .h1(|_| err::, _>(BadRequest)) .tcp() }) .await; @@ -773,36 +775,35 @@ async fn test_not_modified_spec_h1() { let mut srv = test_server(|| { HttpService::build() .h1(|req: Request| { - let res: Response = match req.path() { + let res: Response = match req.path() { // with no content-length "/none" => { - Response::with_body(StatusCode::NOT_MODIFIED, AnyBody::None) + Response::with_body(StatusCode::NOT_MODIFIED, body::None::new()) + .map_into_boxed_body() } // with no content-length - "/body" => Response::with_body( - StatusCode::NOT_MODIFIED, - AnyBody::from("1234"), - ), + "/body" => Response::with_body(StatusCode::NOT_MODIFIED, "1234") + .map_into_boxed_body(), // with manual content-length header and specific None body "/cl-none" => { - let mut res = - Response::with_body(StatusCode::NOT_MODIFIED, AnyBody::None); + let mut res = Response::with_body( + StatusCode::NOT_MODIFIED, + body::None::new(), + ); res.headers_mut() .insert(CL.clone(), header::HeaderValue::from_static("24")); - res + res.map_into_boxed_body() } // with manual content-length header and ignore-able body "/cl-body" => { - let mut res = Response::with_body( - StatusCode::NOT_MODIFIED, - AnyBody::from("1234"), - ); + let mut res = + Response::with_body(StatusCode::NOT_MODIFIED, "1234"); res.headers_mut() .insert(CL.clone(), header::HeaderValue::from_static("4")); - res + res.map_into_boxed_body() } _ => panic!("unknown route"), diff --git a/actix-http/tests/test_ws.rs b/actix-http/tests/test_ws.rs index 6d0de2316..c91382013 100644 --- a/actix-http/tests/test_ws.rs +++ b/actix-http/tests/test_ws.rs @@ -6,7 +6,7 @@ use std::{ use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_http::{ - body::{AnyBody, BodySize}, + body::{BodySize, BoxBody}, h1, ws::{self, CloseCode, Frame, Item, Message}, Error, HttpService, Request, Response, @@ -50,14 +50,14 @@ enum WsServiceError { Dispatcher, } -impl From for Response { +impl From for Response { fn from(err: WsServiceError) -> Self { match err { WsServiceError::Http(err) => err.into(), WsServiceError::Ws(err) => err.into(), WsServiceError::Io(_err) => unreachable!(), WsServiceError::Dispatcher => Response::internal_server_error() - .set_body(AnyBody::from(format!("{}", err))), + .set_body(BoxBody::new(format!("{}", err))), } } } diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs index b80918ec0..1decd6e98 100644 --- a/actix-test/src/lib.rs +++ b/actix-test/src/lib.rs @@ -31,7 +31,7 @@ extern crate tls_openssl as openssl; #[cfg(feature = "rustls")] extern crate tls_rustls as rustls; -use std::{error::Error as StdError, fmt, net, thread, time::Duration}; +use std::{fmt, net, thread, time::Duration}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; pub use actix_http::test::TestBuffer; @@ -41,7 +41,8 @@ use actix_http::{ }; use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _}; use actix_web::{ - dev::{AppConfig, MessageBody, Server, ServerHandle, Service}, + body::MessageBody, + dev::{AppConfig, Server, ServerHandle, Service}, rt::{self, System}, web, Error, }; @@ -88,7 +89,6 @@ where S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, { start_with(TestServerConfig::default(), factory) } @@ -128,7 +128,6 @@ where S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, { // for sending handles and server info back from the spawned thread let (started_tx, started_rx) = std::sync::mpsc::channel(); diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index e7b8cd0f0..28b5b29ea 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -22,7 +22,7 @@ actix-web = { version = "4.0.0-beta.11", default-features = false } bytes = "1" bytestring = "1" futures-core = { version = "0.3.7", default-features = false } -pin-project = "1.0.0" +pin-project-lite = "0.2" tokio = { version = "1", features = ["sync"] } [dev-dependencies] diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index f0a53d4e0..b6323c2ed 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -30,6 +30,7 @@ use actix_web::{ use bytes::{Bytes, BytesMut}; use bytestring::ByteString; use futures_core::Stream; +use pin_project_lite::pin_project; use tokio::sync::oneshot::Sender; /// Perform WebSocket handshake and start actor. @@ -462,13 +463,14 @@ where } } -#[pin_project::pin_project] -struct WsStream { - #[pin] - stream: S, - decoder: Codec, - buf: BytesMut, - closed: bool, +pin_project! { + struct WsStream { + #[pin] + stream: S, + decoder: Codec, + buf: BytesMut, + closed: bool, + } } impl WsStream diff --git a/actix-web-actors/tests/test_ws.rs b/actix-web-actors/tests/test_ws.rs index 0a8e50b3e..e481b2839 100644 --- a/actix-web-actors/tests/test_ws.rs +++ b/actix-web-actors/tests/test_ws.rs @@ -78,7 +78,7 @@ async fn test_with_credentials() { match srv.ws().await { Ok(_) => panic!("WebSocket client without credentials should panic"), Err(awc::error::WsClientError::InvalidResponseStatus(status)) => { - assert_eq!(status, StatusCode::UNAUTHORIZED) + assert_eq!(status, StatusCode::UNAUTHORIZED); } Err(e) => panic!("Invalid error from WebSocket client: {}", e), } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 851e5cd43..fc60f5edb 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -105,6 +105,7 @@ brotli2 = "0.3.2" env_logger = "0.9" flate2 = "1.0.13" futures-util = { version = "0.3.7", default-features = false } +static_assertions = "1.1" rcgen = "0.8" rustls-pemfile = "0.2" diff --git a/awc/src/any_body.rs b/awc/src/any_body.rs new file mode 100644 index 000000000..cb9038ff3 --- /dev/null +++ b/awc/src/any_body.rs @@ -0,0 +1,266 @@ +use std::{ + borrow::Cow, + fmt, mem, + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::{Bytes, BytesMut}; +use futures_core::Stream; +use pin_project_lite::pin_project; + +use actix_http::body::{BodySize, BodyStream, BoxBody, MessageBody, SizedStream}; + +use crate::BoxError; + +pin_project! { + /// Represents various types of HTTP message body. + #[derive(Clone)] + #[project = AnyBodyProj] + pub enum AnyBody { + /// Empty response. `Content-Length` header is not set. + None, + + /// Complete, in-memory response body. + Bytes { body: Bytes }, + + /// Generic / Other message body. + Body { #[pin] body: B }, + } +} + +impl AnyBody { + /// Constructs a "body" representing an empty response. + pub fn none() -> Self { + Self::None + } + + /// Constructs a new, 0-length body. + pub fn empty() -> Self { + Self::Bytes { body: Bytes::new() } + } + + /// Create boxed body from generic message body. + pub fn new_boxed(body: B) -> Self + where + B: MessageBody + 'static, + { + Self::Body { + body: BoxBody::new(body), + } + } + + /// Constructs new `AnyBody` instance from a slice of bytes by copying it. + /// + /// If your bytes container is owned, it may be cheaper to use a `From` impl. + pub fn copy_from_slice(s: &[u8]) -> Self { + Self::Bytes { + body: Bytes::copy_from_slice(s), + } + } + + #[doc(hidden)] + #[deprecated(since = "4.0.0", note = "Renamed to `copy_from_slice`.")] + pub fn from_slice(s: &[u8]) -> Self { + Self::Bytes { + body: Bytes::copy_from_slice(s), + } + } +} + +impl AnyBody { + /// Create body from generic message body. + pub fn new(body: B) -> Self { + Self::Body { body } + } +} + +impl AnyBody +where + B: MessageBody + 'static, +{ + pub fn into_boxed(self) -> AnyBody { + match self { + Self::None => AnyBody::None, + Self::Bytes { body: bytes } => AnyBody::Bytes { body: bytes }, + Self::Body { body } => AnyBody::new_boxed(body), + } + } +} + +impl MessageBody for AnyBody +where + B: MessageBody, +{ + type Error = crate::BoxError; + + fn size(&self) -> BodySize { + match self { + AnyBody::None => BodySize::None, + AnyBody::Bytes { ref body } => BodySize::Sized(body.len() as u64), + AnyBody::Body { ref body } => body.size(), + } + } + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + match self.project() { + AnyBodyProj::None => Poll::Ready(None), + AnyBodyProj::Bytes { body } => { + let len = body.len(); + if len == 0 { + Poll::Ready(None) + } else { + Poll::Ready(Some(Ok(mem::take(body)))) + } + } + + AnyBodyProj::Body { body } => body.poll_next(cx).map_err(|err| err.into()), + } + } +} + +impl PartialEq for AnyBody { + fn eq(&self, other: &AnyBody) -> bool { + match self { + AnyBody::None => matches!(*other, AnyBody::None), + AnyBody::Bytes { body } => match other { + AnyBody::Bytes { body: b2 } => body == b2, + _ => false, + }, + AnyBody::Body { .. } => false, + } + } +} + +impl fmt::Debug for AnyBody { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + AnyBody::None => write!(f, "AnyBody::None"), + AnyBody::Bytes { ref body } => write!(f, "AnyBody::Bytes({:?})", body), + AnyBody::Body { ref body } => write!(f, "AnyBody::Message({:?})", body), + } + } +} + +impl From<&'static str> for AnyBody { + fn from(string: &'static str) -> Self { + Self::Bytes { + body: Bytes::from_static(string.as_ref()), + } + } +} + +impl From<&'static [u8]> for AnyBody { + fn from(bytes: &'static [u8]) -> Self { + Self::Bytes { + body: Bytes::from_static(bytes), + } + } +} + +impl From> for AnyBody { + fn from(vec: Vec) -> Self { + Self::Bytes { + body: Bytes::from(vec), + } + } +} + +impl From for AnyBody { + fn from(string: String) -> Self { + Self::Bytes { + body: Bytes::from(string), + } + } +} + +impl From<&'_ String> for AnyBody { + fn from(string: &String) -> Self { + Self::Bytes { + body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string)), + } + } +} + +impl From> for AnyBody { + fn from(string: Cow<'_, str>) -> Self { + match string { + Cow::Owned(s) => Self::from(s), + Cow::Borrowed(s) => Self::Bytes { + body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)), + }, + } + } +} + +impl From for AnyBody { + fn from(bytes: Bytes) -> Self { + Self::Bytes { body: bytes } + } +} + +impl From for AnyBody { + fn from(bytes: BytesMut) -> Self { + Self::Bytes { + body: bytes.freeze(), + } + } +} + +impl From> for AnyBody +where + S: Stream> + 'static, + E: Into + 'static, +{ + fn from(stream: SizedStream) -> Self { + AnyBody::new_boxed(stream) + } +} + +impl From> for AnyBody +where + S: Stream> + 'static, + E: Into + 'static, +{ + fn from(stream: BodyStream) -> Self { + AnyBody::new_boxed(stream) + } +} + +#[cfg(test)] +mod tests { + use std::marker::PhantomPinned; + + use static_assertions::{assert_impl_all, assert_not_impl_all}; + + use super::*; + + struct PinType(PhantomPinned); + + impl MessageBody for PinType { + type Error = crate::BoxError; + + fn size(&self) -> BodySize { + unimplemented!() + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + unimplemented!() + } + } + + assert_impl_all!(AnyBody<()>: MessageBody, fmt::Debug, Send, Sync, Unpin); + assert_impl_all!(AnyBody>: MessageBody, fmt::Debug, Send, Sync, Unpin); + assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Send, Sync, Unpin); + assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Unpin); + assert_impl_all!(AnyBody: MessageBody); + + assert_not_impl_all!(AnyBody: Send, Sync, Unpin); + assert_not_impl_all!(AnyBody: Send, Sync, Unpin); +} diff --git a/awc/src/client/connection.rs b/awc/src/client/connection.rs index 6bbc9ad07..0e1f0bfec 100644 --- a/awc/src/client/connection.rs +++ b/awc/src/client/connection.rs @@ -12,9 +12,9 @@ use bytes::Bytes; use futures_core::future::LocalBoxFuture; use h2::client::SendRequest; -use actix_http::{ - body::MessageBody, h1::ClientCodec, Error, Payload, RequestHeadType, ResponseHead, -}; +use actix_http::{body::MessageBody, h1::ClientCodec, Payload, RequestHeadType, ResponseHead}; + +use crate::BoxError; use super::error::SendRequestError; use super::pool::Acquired; @@ -254,7 +254,7 @@ where where H: Into + 'static, RB: MessageBody + 'static, - RB::Error: Into, + RB::Error: Into, { Box::pin(async move { match self { diff --git a/awc/src/client/error.rs b/awc/src/client/error.rs index 68ffb6fbf..d351b5d5e 100644 --- a/awc/src/client/error.rs +++ b/awc/src/client/error.rs @@ -1,13 +1,13 @@ -use std::{error::Error as StdError, fmt, io}; +use std::{fmt, io}; use derive_more::{Display, From}; -use actix_http::{ - error::{Error, ParseError}, - http::Error as HttpError, -}; +use actix_http::{error::ParseError, http::Error as HttpError}; + #[cfg(feature = "openssl")] -use actix_tls::accept::openssl::reexports::Error as OpenSslError; +use actix_tls::accept::openssl::reexports::Error as OpensslError; + +use crate::BoxError; /// A set of errors that can occur while connecting to an HTTP host #[derive(Debug, Display, From)] @@ -20,7 +20,7 @@ pub enum ConnectError { /// SSL error #[cfg(feature = "openssl")] #[display(fmt = "{}", _0)] - SslError(OpenSslError), + SslError(OpensslError), /// Failed to resolve the hostname #[display(fmt = "Failed resolving hostname: {}", _0)] @@ -118,11 +118,11 @@ pub enum SendRequestError { TunnelNotSupported, /// Error sending request body - Body(Error), + Body(BoxError), /// Other errors that can occur after submitting a request. #[display(fmt = "{:?}: {}", _1, _0)] - Custom(Box, Box), + Custom(BoxError, Box), } impl std::error::Error for SendRequestError {} @@ -141,7 +141,7 @@ pub enum FreezeRequestError { /// Other errors that can occur after submitting a request. #[display(fmt = "{:?}: {}", _1, _0)] - Custom(Box, Box), + Custom(BoxError, Box), } impl std::error::Error for FreezeRequestError {} diff --git a/awc/src/client/h1proto.rs b/awc/src/client/h1proto.rs index c442cd4cf..b26a97eeb 100644 --- a/awc/src/client/h1proto.rs +++ b/awc/src/client/h1proto.rs @@ -13,7 +13,7 @@ use actix_http::{ header::{HeaderMap, IntoHeaderValue, EXPECT, HOST}, StatusCode, }, - Error, Payload, RequestHeadType, ResponseHead, + Payload, RequestHeadType, ResponseHead, }; use actix_utils::future::poll_fn; use bytes::buf::BufMut; @@ -22,6 +22,8 @@ use futures_core::{ready, Stream}; use futures_util::SinkExt as _; use pin_project_lite::pin_project; +use crate::BoxError; + use super::connection::{ConnectionIo, H1Connection}; use super::error::{ConnectError, SendRequestError}; @@ -33,7 +35,7 @@ pub(crate) async fn send_request( where Io: ConnectionIo, B: MessageBody, - B::Error: Into, + B::Error: Into, { // set request host header if !head.as_ref().headers.contains_key(HOST) @@ -155,7 +157,7 @@ pub(crate) async fn send_body( where Io: ConnectionIo, B: MessageBody, - B::Error: Into, + B::Error: Into, { actix_rt::pin!(body); @@ -166,7 +168,7 @@ where Some(Ok(chunk)) => { framed.as_mut().write(h1::Message::Chunk(Some(chunk)))?; } - Some(Err(err)) => return Err(err.into().into()), + Some(Err(err)) => return Err(SendRequestError::Body(err.into())), None => { eof = true; framed.as_mut().write(h1::Message::Chunk(None))?; diff --git a/awc/src/client/h2proto.rs b/awc/src/client/h2proto.rs index 66fb790a3..9ced5776b 100644 --- a/awc/src/client/h2proto.rs +++ b/awc/src/client/h2proto.rs @@ -13,9 +13,11 @@ use log::trace; use actix_http::{ body::{BodySize, MessageBody}, header::HeaderMap, - Error, Payload, RequestHeadType, ResponseHead, + Payload, RequestHeadType, ResponseHead, }; +use crate::BoxError; + use super::{ config::ConnectorConfig, connection::{ConnectionIo, H2Connection}, @@ -30,7 +32,7 @@ pub(crate) async fn send_request( where Io: ConnectionIo, B: MessageBody, - B::Error: Into, + B::Error: Into, { trace!("Sending client request: {:?} {:?}", head, body.size()); @@ -133,10 +135,12 @@ where async fn send_body(body: B, mut send: SendStream) -> Result<(), SendRequestError> where B: MessageBody, - B::Error: Into, + B::Error: Into, { let mut buf = None; + actix_rt::pin!(body); + loop { if buf.is_none() { match poll_fn(|cx| body.as_mut().poll_next(cx)).await { @@ -144,10 +148,10 @@ where send.reserve_capacity(b.len()); buf = Some(b); } - Some(Err(e)) => return Err(e.into().into()), + Some(Err(err)) => return Err(SendRequestError::Body(err.into())), None => { - if let Err(e) = send.send_data(Bytes::new(), true) { - return Err(e.into()); + if let Err(err) = send.send_data(Bytes::new(), true) { + return Err(err.into()); } send.reserve_capacity(0); return Ok(()); diff --git a/awc/src/connect.rs b/awc/src/connect.rs index 05f2a6495..19870b069 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -7,16 +7,17 @@ use std::{ }; use actix_codec::Framed; -use actix_http::{ - body::AnyBody, h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead, -}; +use actix_http::{h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead}; use actix_service::Service; use futures_core::{future::LocalBoxFuture, ready}; -use crate::client::{ - Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError, +use crate::{ + any_body::AnyBody, + client::{ + Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError, + }, + response::ClientResponse, }; -use crate::response::ClientResponse; pub type BoxConnectorService = Rc< dyn Service< diff --git a/awc/src/error.rs b/awc/src/error.rs index d415efe95..726e1a506 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -11,6 +11,8 @@ use serde_json::error::Error as JsonError; pub use crate::client::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; +// TODO: address display, error, and from impls + /// Websocket client error #[derive(Debug, Display, From)] pub enum WsClientError { diff --git a/awc/src/frozen.rs b/awc/src/frozen.rs index 46a00b000..472397359 100644 --- a/awc/src/frozen.rs +++ b/awc/src/frozen.rs @@ -1,18 +1,18 @@ -use std::{convert::TryFrom, error::Error as StdError, net, rc::Rc, time::Duration}; +use std::{convert::TryFrom, net, rc::Rc, time::Duration}; use bytes::Bytes; use futures_core::Stream; use serde::Serialize; use actix_http::{ - body::AnyBody, http::{header::IntoHeaderValue, Error as HttpError, HeaderMap, HeaderName, Method, Uri}, RequestHead, }; use crate::{ + any_body::AnyBody, sender::{RequestSender, SendClientRequest}, - ClientConfig, + BoxError, ClientConfig, }; /// `FrozenClientRequest` struct represents cloneable client request. @@ -82,7 +82,7 @@ impl FrozenClientRequest { pub fn send_stream(&self, stream: S) -> SendClientRequest where S: Stream> + Unpin + 'static, - E: Into> + 'static, + E: Into + 'static, { RequestSender::Rc(self.head.clone(), None).send_stream( self.addr, @@ -207,7 +207,7 @@ impl FrozenSendBuilder { pub fn send_stream(self, stream: S) -> SendClientRequest where S: Stream> + Unpin + 'static, - E: Into> + 'static, + E: Into + 'static, { if let Some(e) = self.err { return e.into(); diff --git a/awc/src/lib.rs b/awc/src/lib.rs index fc99419eb..2f4183120 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -104,6 +104,7 @@ #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] +mod any_body; mod builder; mod client; mod connect; @@ -139,6 +140,8 @@ use actix_service::Service; use self::client::{ConnectInfo, TcpConnectError, TcpConnection}; +pub(crate) type BoxError = Box; + /// An asynchronous HTTP and WebSocket client. /// /// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index 12a71f7cb..89cff22cd 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -8,7 +8,6 @@ use std::{ }; use actix_http::{ - body::AnyBody, http::{header, Method, StatusCode, Uri}, RequestHead, RequestHeadType, }; @@ -17,10 +16,12 @@ use bytes::Bytes; use futures_core::ready; use super::Transform; - -use crate::client::{InvalidUrl, SendRequestError}; -use crate::connect::{ConnectRequest, ConnectResponse}; -use crate::ClientResponse; +use crate::{ + any_body::AnyBody, + client::{InvalidUrl, SendRequestError}, + connect::{ConnectRequest, ConnectResponse}, + ClientResponse, +}; pub struct Redirect { max_redirect_times: u8, @@ -95,7 +96,7 @@ where }; let body_opt = match body { - AnyBody::Bytes(ref b) => Some(b.clone()), + AnyBody::Bytes { ref body } => Some(body.clone()), _ => None, }; @@ -192,7 +193,9 @@ where let body_new = if is_redirect { // try to reuse body match body { - Some(ref bytes) => AnyBody::Bytes(bytes.clone()), + Some(ref bytes) => AnyBody::Bytes { + body: bytes.clone(), + }, // TODO: should this be AnyBody::Empty or AnyBody::None. _ => AnyBody::empty(), } diff --git a/awc/src/request.rs b/awc/src/request.rs index f364b43c7..d26b703f6 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -1,11 +1,10 @@ -use std::{convert::TryFrom, error::Error as StdError, fmt, net, rc::Rc, time::Duration}; +use std::{convert::TryFrom, fmt, net, rc::Rc, time::Duration}; use bytes::Bytes; use futures_core::Stream; use serde::Serialize; use actix_http::{ - body::AnyBody, http::{ header::{self, IntoHeaderPair}, ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, Version, @@ -13,15 +12,17 @@ use actix_http::{ RequestHead, }; -#[cfg(feature = "cookies")] -use crate::cookie::{Cookie, CookieJar}; use crate::{ + any_body::AnyBody, error::{FreezeRequestError, InvalidUrl}, frozen::FrozenClientRequest, sender::{PrepForSendingError, RequestSender, SendClientRequest}, - ClientConfig, + BoxError, ClientConfig, }; +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, CookieJar}; + /// An HTTP Client request builder /// /// This type can be used to construct an instance of `ClientRequest` through a @@ -404,7 +405,7 @@ impl ClientRequest { pub fn send_stream(self, stream: S) -> SendClientRequest where S: Stream> + Unpin + 'static, - E: Into> + 'static, + E: Into + 'static, { let slf = match self.prep_for_sending() { Ok(slf) => slf, diff --git a/awc/src/sender.rs b/awc/src/sender.rs index 7e1bcd646..51fce1913 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -1,5 +1,4 @@ use std::{ - error::Error as StdError, future::Future, net, pin::Pin, @@ -9,12 +8,12 @@ use std::{ }; use actix_http::{ - body::{AnyBody, BodyStream}, + body::BodyStream, http::{ header::{self, HeaderMap, HeaderName, IntoHeaderValue}, Error as HttpError, }, - Error, RequestHead, RequestHeadType, + RequestHead, RequestHeadType, }; use actix_rt::time::{sleep, Sleep}; use bytes::Bytes; @@ -26,8 +25,9 @@ use serde::Serialize; use actix_http::{encoding::Decoder, http::header::ContentEncoding, Payload, PayloadStream}; use crate::{ + any_body::AnyBody, error::{FreezeRequestError, InvalidUrl, SendRequestError}, - ClientConfig, ClientResponse, ConnectRequest, ConnectResponse, + BoxError, ClientConfig, ClientResponse, ConnectRequest, ConnectResponse, }; #[derive(Debug, From)] @@ -162,12 +162,6 @@ impl From for SendClientRequest { } } -impl From for SendClientRequest { - fn from(e: Error) -> Self { - SendClientRequest::Err(Some(e.into())) - } -} - impl From for SendClientRequest { fn from(e: HttpError) -> Self { SendClientRequest::Err(Some(e.into())) @@ -236,7 +230,9 @@ impl RequestSender { response_decompress, timeout, config, - AnyBody::Bytes(Bytes::from(body)), + AnyBody::Bytes { + body: Bytes::from(body), + }, ) } @@ -265,7 +261,9 @@ impl RequestSender { response_decompress, timeout, config, - AnyBody::Bytes(Bytes::from(body)), + AnyBody::Bytes { + body: Bytes::from(body), + }, ) } @@ -279,7 +277,7 @@ impl RequestSender { ) -> SendClientRequest where S: Stream> + Unpin + 'static, - E: Into> + 'static, + E: Into + 'static, { self.send_body( addr, diff --git a/benches/responder.rs b/benches/responder.rs index 5d0b98d5f..20aae3351 100644 --- a/benches/responder.rs +++ b/benches/responder.rs @@ -1,9 +1,10 @@ use std::{future::Future, time::Instant}; +use actix_http::body::BoxBody; use actix_utils::future::{ready, Ready}; -use actix_web::http::StatusCode; -use actix_web::test::TestRequest; -use actix_web::{error, Error, HttpRequest, HttpResponse, Responder}; +use actix_web::{ + error, http::StatusCode, test::TestRequest, Error, HttpRequest, HttpResponse, Responder, +}; use criterion::{criterion_group, criterion_main, Criterion}; use futures_util::future::{join_all, Either}; @@ -50,7 +51,9 @@ where } impl Responder for StringResponder { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { + type Body = BoxBody; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { HttpResponse::build(StatusCode::OK) .content_type("text/plain; charset=utf-8") .body(self.0) @@ -58,9 +61,11 @@ impl Responder for StringResponder { } impl Responder for OptionResponder { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { + type Body = BoxBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self.0 { - Some(t) => t.respond_to(req), + Some(t) => t.respond_to(req).map_into_boxed_body(), None => HttpResponse::from_error(error::ErrorInternalServerError("err")), } } diff --git a/src/app.rs b/src/app.rs index a291a959e..36063ec79 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,37 +1,35 @@ -use std::cell::RefCell; -use std::fmt; -use std::future::Future; -use std::marker::PhantomData; -use std::rc::Rc; +use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, rc::Rc}; -use actix_http::body::{AnyBody, MessageBody}; -use actix_http::{Extensions, Request}; -use actix_service::boxed::{self, BoxServiceFactory}; +use actix_http::{ + body::{BoxBody, MessageBody}, + Extensions, Request, +}; use actix_service::{ - apply, apply_fn_factory, IntoServiceFactory, ServiceFactory, ServiceFactoryExt, Transform, + apply, apply_fn_factory, boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt, + Transform, }; use futures_util::future::FutureExt as _; -use crate::app_service::{AppEntry, AppInit, AppRoutingFactory}; -use crate::config::ServiceConfig; -use crate::data::{Data, DataFactory, FnDataFactory}; -use crate::dev::ResourceDef; -use crate::error::Error; -use crate::resource::Resource; -use crate::route::Route; -use crate::service::{ - AppServiceFactory, HttpServiceFactory, ServiceFactoryWrapper, ServiceRequest, - ServiceResponse, +use crate::{ + app_service::{AppEntry, AppInit, AppRoutingFactory}, + config::ServiceConfig, + data::{Data, DataFactory, FnDataFactory}, + dev::ResourceDef, + error::Error, + resource::Resource, + route::Route, + service::{ + AppServiceFactory, BoxedHttpServiceFactory, HttpServiceFactory, ServiceFactoryWrapper, + ServiceRequest, ServiceResponse, + }, }; -type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; - /// Application builder - structure that follows the builder pattern /// for building application instances. pub struct App { endpoint: T, services: Vec>, - default: Option>, + default: Option>, factory_ref: Rc>>, data_factories: Vec, external: Vec, @@ -39,7 +37,7 @@ pub struct App { _phantom: PhantomData, } -impl App { +impl App { /// Create application builder. Application can be configured with a builder-like pattern. #[allow(clippy::new_without_default)] pub fn new() -> Self { @@ -287,7 +285,7 @@ where /// ); /// } /// ``` - pub fn default_service(mut self, f: F) -> Self + pub fn default_service(mut self, svc: F) -> Self where F: IntoServiceFactory, U: ServiceFactory< @@ -298,10 +296,12 @@ where > + 'static, U::InitError: fmt::Debug, { - // create and configure default resource - self.default = Some(Rc::new(boxed::factory(f.into_factory().map_init_err( - |e| log::error!("Can not construct default service: {:?}", e), - )))); + let svc = svc + .into_factory() + .map(|res| res.map_into_boxed_body()) + .map_init_err(|e| log::error!("Can not construct default service: {:?}", e)); + + self.default = Some(Rc::new(boxed::factory(svc))); self } diff --git a/src/app_service.rs b/src/app_service.rs index cf34b302e..bca8f2629 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -2,10 +2,7 @@ use std::{cell::RefCell, mem, rc::Rc}; use actix_http::{Extensions, Request}; use actix_router::{Path, ResourceDef, Router, Url}; -use actix_service::{ - boxed::{self, BoxService, BoxServiceFactory}, - fn_service, Service, ServiceFactory, -}; +use actix_service::{boxed, fn_service, Service, ServiceFactory}; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; @@ -15,13 +12,14 @@ use crate::{ guard::Guard, request::{HttpRequest, HttpRequestPool}, rmap::ResourceMap, - service::{AppServiceFactory, ServiceRequest, ServiceResponse}, + service::{ + AppServiceFactory, BoxedHttpService, BoxedHttpServiceFactory, ServiceRequest, + ServiceResponse, + }, Error, HttpResponse, }; type Guards = Vec>; -type HttpService = BoxService; -type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; /// Service factory to convert `Request` to a `ServiceRequest`. /// It also executes data factories. @@ -39,7 +37,7 @@ where pub(crate) extensions: RefCell>, pub(crate) async_data_factories: Rc<[FnDataFactory]>, pub(crate) services: Rc>>>, - pub(crate) default: Option>, + pub(crate) default: Option>, pub(crate) factory_ref: Rc>>, pub(crate) external: RefCell>, } @@ -230,8 +228,14 @@ where } pub struct AppRoutingFactory { - services: Rc<[(ResourceDef, HttpNewService, RefCell>)]>, - default: Rc, + services: Rc< + [( + ResourceDef, + BoxedHttpServiceFactory, + RefCell>, + )], + >, + default: Rc, } impl ServiceFactory for AppRoutingFactory { @@ -279,8 +283,8 @@ impl ServiceFactory for AppRoutingFactory { /// The Actix Web router default entry point. pub struct AppRouting { - router: Router, - default: HttpService, + router: Router, + default: BoxedHttpService, } impl Service for AppRouting { diff --git a/src/data.rs b/src/data.rs index d27ad196b..b29e4ecf4 100644 --- a/src/data.rs +++ b/src/data.rs @@ -284,7 +284,7 @@ mod tests { async fn test_data_from_arc() { let data_new = Data::new(String::from("test-123")); let data_from_arc = Data::from(Arc::new(String::from("test-123"))); - assert_eq!(data_new.0, data_from_arc.0) + assert_eq!(data_new.0, data_from_arc.0); } #[actix_rt::test] diff --git a/src/dev.rs b/src/dev.rs index 59805b822..d4a64985c 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -1,7 +1,7 @@ //! Lower-level types and re-exports. //! //! Most users will not have to interact with the types in this module, but it is useful for those -//! writing extractors, middleware and libraries, or interacting with the service API directly. +//! writing extractors, middleware, libraries, or interacting with the service API directly. pub use crate::config::{AppConfig, AppService}; #[doc(hidden)] @@ -14,11 +14,6 @@ pub use crate::types::form::UrlEncoded; pub use crate::types::json::JsonBody; pub use crate::types::readlines::Readlines; -#[allow(deprecated)] -pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, SizedStream}; - -#[cfg(feature = "__compress")] -pub use actix_http::encoding::Decoder as Decompress; pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, Response, ResponseHead}; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_server::{Server, ServerHandle}; @@ -26,8 +21,10 @@ pub use actix_service::{ always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform, }; +#[cfg(feature = "__compress")] +pub use actix_http::encoding::Decoder as Decompress; + use crate::http::header::ContentEncoding; -use actix_http::ResponseBuilder; use actix_router::Patterns; @@ -62,7 +59,7 @@ pub trait BodyEncoding { fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self; } -impl BodyEncoding for ResponseBuilder { +impl BodyEncoding for actix_http::ResponseBuilder { fn get_encoding(&self) -> Option { self.extensions().get::().map(|enc| enc.0) } @@ -73,7 +70,7 @@ impl BodyEncoding for ResponseBuilder { } } -impl BodyEncoding for Response { +impl BodyEncoding for actix_http::Response { fn get_encoding(&self) -> Option { self.extensions().get::().map(|enc| enc.0) } @@ -105,3 +102,41 @@ impl BodyEncoding for crate::HttpResponse { self } } + +// TODO: remove this if it doesn't appear to be needed + +#[allow(dead_code)] +#[derive(Debug)] +pub(crate) enum AnyBody { + None, + Full { body: crate::web::Bytes }, + Boxed { body: actix_http::body::BoxBody }, +} + +impl crate::body::MessageBody for AnyBody { + type Error = crate::BoxError; + + /// Body size hint. + fn size(&self) -> crate::body::BodySize { + match self { + AnyBody::None => crate::body::BodySize::None, + AnyBody::Full { body } => body.size(), + AnyBody::Boxed { body } => body.size(), + } + } + + /// Attempt to pull out the next chunk of body bytes. + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll>> { + match self.get_mut() { + AnyBody::None => std::task::Poll::Ready(None), + AnyBody::Full { body } => { + let bytes = std::mem::take(body); + std::task::Poll::Ready(Some(Ok(bytes))) + } + AnyBody::Boxed { body } => body.as_pin_mut().poll_next(cx), + } + } +} diff --git a/src/error/error.rs b/src/error/error.rs index add290867..be17c1962 100644 --- a/src/error/error.rs +++ b/src/error/error.rs @@ -1,6 +1,6 @@ use std::{error::Error as StdError, fmt}; -use actix_http::{body::AnyBody, Response}; +use actix_http::{body::BoxBody, Response}; use crate::{HttpResponse, ResponseError}; @@ -69,8 +69,8 @@ impl From for Error { } } -impl From for Response { - fn from(err: Error) -> Response { +impl From for Response { + fn from(err: Error) -> Response { err.error_response().into() } } diff --git a/src/error/internal.rs b/src/error/internal.rs index 3d99012dc..c766ba83e 100644 --- a/src/error/internal.rs +++ b/src/error/internal.rs @@ -1,6 +1,10 @@ use std::{cell::RefCell, fmt, io::Write as _}; -use actix_http::{body::AnyBody, header, StatusCode}; +use actix_http::{ + body::BoxBody, + header::{self, IntoHeaderValue as _}, + StatusCode, +}; use bytes::{BufMut as _, BytesMut}; use crate::{Error, HttpRequest, HttpResponse, Responder, ResponseError}; @@ -84,11 +88,10 @@ where let mut buf = BytesMut::new().writer(); let _ = write!(buf, "{}", self); - res.headers_mut().insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("text/plain; charset=utf-8"), - ); - res.set_body(AnyBody::from(buf.into_inner())) + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + + res.set_body(BoxBody::new(buf.into_inner())) } InternalErrorType::Response(ref resp) => { @@ -106,7 +109,9 @@ impl Responder for InternalError where T: fmt::Debug + fmt::Display + 'static, { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { + type Body = BoxBody; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { HttpResponse::from_error(self) } } diff --git a/src/error/macros.rs b/src/error/macros.rs index 38650c5e8..78b1ed9f6 100644 --- a/src/error/macros.rs +++ b/src/error/macros.rs @@ -97,7 +97,7 @@ mod tests { let resp_body: &mut dyn MB = &mut body; let body = resp_body.downcast_ref::().unwrap(); assert_eq!(body, "hello cast"); - let body = &mut resp_body.downcast_mut::().unwrap(); + let body = resp_body.downcast_mut::().unwrap(); body.push('!'); let body = resp_body.downcast_ref::().unwrap(); assert_eq!(body, "hello cast!"); diff --git a/src/error/response_error.rs b/src/error/response_error.rs index 2c34be3cb..7260efa1a 100644 --- a/src/error/response_error.rs +++ b/src/error/response_error.rs @@ -6,11 +6,17 @@ use std::{ io::{self, Write as _}, }; -use actix_http::{body::AnyBody, header, Response, StatusCode}; +use actix_http::{ + body::BoxBody, + header::{self, IntoHeaderValue}, + Response, StatusCode, +}; use bytes::BytesMut; -use crate::error::{downcast_dyn, downcast_get_type_id}; -use crate::{helpers, HttpResponse}; +use crate::{ + error::{downcast_dyn, downcast_get_type_id}, + helpers, HttpResponse, +}; /// Errors that can generate responses. // TODO: add std::error::Error bound when replacement for Box is found @@ -27,18 +33,16 @@ pub trait ResponseError: fmt::Debug + fmt::Display { /// /// By default, the generated response uses a 500 Internal Server Error status code, a /// `Content-Type` of `text/plain`, and the body is set to `Self`'s `Display` impl. - fn error_response(&self) -> HttpResponse { + fn error_response(&self) -> HttpResponse { let mut res = HttpResponse::new(self.status_code()); let mut buf = BytesMut::new(); let _ = write!(helpers::MutWriter(&mut buf), "{}", self); - res.headers_mut().insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("text/plain; charset=utf-8"), - ); + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); - res.set_body(AnyBody::from(buf)) + res.set_body(BoxBody::new(buf)) } downcast_get_type_id!(); @@ -86,8 +90,8 @@ impl ResponseError for actix_http::Error { StatusCode::INTERNAL_SERVER_ERROR } - fn error_response(&self) -> HttpResponse { - HttpResponse::new(self.status_code()).set_body(self.to_string().into()) + fn error_response(&self) -> HttpResponse { + HttpResponse::with_body(self.status_code(), self.to_string()).map_into_boxed_body() } } @@ -123,8 +127,8 @@ impl ResponseError for actix_http::error::ContentTypeError { } impl ResponseError for actix_http::ws::HandshakeError { - fn error_response(&self) -> HttpResponse { - Response::from(self).into() + fn error_response(&self) -> HttpResponse { + Response::from(self).map_into_boxed_body().into() } } diff --git a/src/handler.rs b/src/handler.rs index ddefe8d53..e543ecc7f 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,21 +1,21 @@ use std::future::Future; -use actix_service::{ - boxed::{self, BoxServiceFactory}, - fn_service, -}; +use actix_service::{boxed, fn_service}; use crate::{ - service::{ServiceRequest, ServiceResponse}, - Error, FromRequest, HttpResponse, Responder, + body::MessageBody, + service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse}, + BoxError, FromRequest, HttpResponse, Responder, }; /// A request handler is an async function that accepts zero or more parameters that can be -/// extracted from a request (i.e., [`impl FromRequest`](crate::FromRequest)) and returns a type -/// that can be converted into an [`HttpResponse`] (that is, it impls the [`Responder`] trait). +/// extracted from a request (i.e., [`impl FromRequest`]) and returns a type that can be converted +/// into an [`HttpResponse`] (that is, it impls the [`Responder`] trait). /// /// If you got the error `the trait Handler<_, _, _> is not implemented`, then your function is not -/// a valid handler. See [Request Handlers](https://actix.rs/docs/handlers/) for more information. +/// a valid handler. See for more information. +/// +/// [`impl FromRequest`]: crate::FromRequest pub trait Handler: Clone + 'static where R: Future, @@ -24,29 +24,44 @@ where fn call(&self, param: T) -> R; } -pub fn handler_service( - handler: F, -) -> BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()> +pub(crate) fn handler_service(handler: F) -> BoxedHttpServiceFactory where F: Handler, T: FromRequest, R: Future, R::Output: Responder, + ::Body: MessageBody, + <::Body as MessageBody>::Error: Into, { boxed::factory(fn_service(move |req: ServiceRequest| { let handler = handler.clone(); + async move { let (req, mut payload) = req.into_parts(); + let res = match T::from_request(&req, &mut payload).await { Err(err) => HttpResponse::from_error(err), - Ok(data) => handler.call(data).await.respond_to(&req), + + Ok(data) => handler + .call(data) + .await + .respond_to(&req) + .map_into_boxed_body(), }; + Ok(ServiceResponse::new(req, res)) } })) } -/// FromRequest trait impl for tuples +/// Generates a [`Handler`] trait impl for N-ary functions where N is specified with a sequence of +/// space separated type parameters. +/// +/// # Examples +/// ```ignore +/// factory_tuple! {} // implements Handler for types: fn() -> Res +/// factory_tuple! { A B C } // implements Handler for types: fn(A, B, C) -> Res +/// ``` macro_rules! factory_tuple ({ $($param:ident)* } => { impl Handler<($($param,)*), Res> for Func where Func: Fn($($param),*) -> Res + Clone + 'static, diff --git a/src/http/header/accept.rs b/src/http/header/accept.rs index fe291c011..70e4118cf 100644 --- a/src/http/header/accept.rs +++ b/src/http/header/accept.rs @@ -208,7 +208,7 @@ impl Accept { /// If no q-factors are provided, the first mime type is chosen. Note that items without /// q-factors are given the maximum preference value. /// - /// As per the spec, will return [`Mime::STAR_STAR`] (indicating no preference) if the contained + /// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained /// list is empty. /// /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 diff --git a/src/lib.rs b/src/lib.rs index 3ad77ff5f..f6ec4082a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,3 +115,5 @@ pub use crate::scope::Scope; pub use crate::server::HttpServer; // TODO: is exposing the error directly really needed pub use crate::types::{Either, EitherExtractError}; + +pub(crate) type BoxError = Box; diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs index a75335981..e6ef1806f 100644 --- a/src/middleware/compat.rs +++ b/src/middleware/compat.rs @@ -1,13 +1,12 @@ //! For middleware documentation, see [`Compat`]. use std::{ - error::Error as StdError, future::Future, pin::Pin, task::{Context, Poll}, }; -use actix_http::body::{AnyBody, MessageBody}; +use actix_http::body::MessageBody; use actix_service::{Service, Transform}; use futures_core::{future::LocalBoxFuture, ready}; use pin_project_lite::pin_project; @@ -123,10 +122,9 @@ pub trait MapServiceResponseBody { impl MapServiceResponseBody for ServiceResponse where B: MessageBody + Unpin + 'static, - B::Error: Into>, { fn map_body(self) -> ServiceResponse { - self.map_body(|_, body| AnyBody::new_boxed(body)) + self.map_into_boxed_body() } } diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 3b99fd6b3..d017e9a5a 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -10,14 +10,13 @@ use std::{ }; use actix_http::{ - body::{AnyBody, MessageBody}, + body::{EitherBody, MessageBody}, encoding::Encoder, http::header::{ContentEncoding, ACCEPT_ENCODING}, StatusCode, }; use actix_service::{Service, Transform}; use actix_utils::future::{ok, Either, Ready}; -use bytes::Bytes; use futures_core::ready; use once_cell::sync::Lazy; use pin_project_lite::pin_project; @@ -62,7 +61,7 @@ where B: MessageBody, S: Service, Error = Error>, { - type Response = ServiceResponse>>; + type Response = ServiceResponse>>; type Error = Error; type Transform = CompressMiddleware; type InitError = (); @@ -112,7 +111,7 @@ where S: Service, Error = Error>, B: MessageBody, { - type Response = ServiceResponse>>; + type Response = ServiceResponse>>; type Error = Error; type Future = Either, Ready>>; @@ -144,19 +143,15 @@ where // There is an HTTP header but we cannot match what client as asked for Some(Err(_)) => { - let res = HttpResponse::new(StatusCode::NOT_ACCEPTABLE); + let res = HttpResponse::with_body( + StatusCode::NOT_ACCEPTABLE, + SUPPORTED_ALGORITHM_NAMES.clone(), + ); - let res: HttpResponse>> = res.map_body(move |head, _| { - let body_bytes = Bytes::from(SUPPORTED_ALGORITHM_NAMES.as_bytes()); - - Encoder::response( - ContentEncoding::Identity, - head, - AnyBody::Bytes(body_bytes), - ) - }); - - Either::right(ok(req.into_response(res))) + Either::right(ok(req + .into_response(res) + .map_into_boxed_body() + .map_into_right_body())) } } } @@ -179,7 +174,7 @@ where B: MessageBody, S: Service, Error = Error>, { - type Output = Result>>, Error>; + type Output = Result>>, Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); @@ -193,10 +188,11 @@ where }; Poll::Ready(Ok(resp.map_body(move |head, body| { - Encoder::response(enc, head, AnyBody::Body(body)) + EitherBody::left(Encoder::response(enc, head, body)) }))) } - Err(e) => Poll::Ready(Err(e)), + + Err(err) => Poll::Ready(Err(err)), } } } diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 6ab16a4eb..f89b13a1c 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -22,7 +22,7 @@ use regex::{Regex, RegexSet}; use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use crate::{ - dev::{BodySize, MessageBody}, + body::{BodySize, MessageBody}, http::HeaderName, service::{ServiceRequest, ServiceResponse}, Error, HttpResponse, Result, diff --git a/src/resource.rs b/src/resource.rs index 851ce0fc9..fc417bac2 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,32 +1,29 @@ -use std::cell::RefCell; -use std::fmt; -use std::future::Future; -use std::rc::Rc; +use std::{cell::RefCell, fmt, future::Future, rc::Rc}; use actix_http::Extensions; use actix_router::{IntoPatterns, Patterns}; -use actix_service::boxed::{self, BoxService, BoxServiceFactory}; use actix_service::{ - apply, apply_fn_factory, fn_service, IntoServiceFactory, Service, ServiceFactory, + apply, apply_fn_factory, boxed, fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt, Transform, }; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; use crate::{ + body::MessageBody, data::Data, - dev::{ensure_leading_slash, AppService, HttpServiceFactory, ResourceDef}, + dev::{ensure_leading_slash, AppService, ResourceDef}, guard::Guard, handler::Handler, responder::Responder, route::{Route, RouteService}, - service::{ServiceRequest, ServiceResponse}, - Error, FromRequest, HttpResponse, + service::{ + BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest, + ServiceResponse, + }, + BoxError, Error, FromRequest, HttpResponse, }; -type HttpService = BoxService; -type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; - /// *Resource* is an entry in resources table which corresponds to requested URL. /// /// Resource in turn has at least one route. @@ -56,7 +53,7 @@ pub struct Resource { routes: Vec, app_data: Option, guards: Vec>, - default: HttpNewService, + default: BoxedHttpServiceFactory, factory_ref: Rc>>, } @@ -242,6 +239,8 @@ where I: FromRequest + 'static, R: Future + 'static, R::Output: Responder + 'static, + ::Body: MessageBody, + <::Body as MessageBody>::Error: Into, { self.routes.push(Route::new().to(handler)); self @@ -422,7 +421,7 @@ where pub struct ResourceFactory { routes: Vec, - default: HttpNewService, + default: BoxedHttpServiceFactory, } impl ServiceFactory for ResourceFactory { @@ -454,7 +453,7 @@ impl ServiceFactory for ResourceFactory { pub struct ResourceService { routes: Vec, - default: HttpService, + default: BoxedHttpService, } impl Service for ResourceService { diff --git a/src/responder.rs b/src/responder.rs index 8a84be598..9d8a0e8ed 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -1,19 +1,21 @@ use std::borrow::Cow; use actix_http::{ - body::AnyBody, + body::{BoxBody, EitherBody, MessageBody}, http::{header::IntoHeaderPair, Error as HttpError, HeaderMap, StatusCode}, }; use bytes::{Bytes, BytesMut}; -use crate::{Error, HttpRequest, HttpResponse, HttpResponseBuilder}; +use crate::{BoxError, Error, HttpRequest, HttpResponse, HttpResponseBuilder}; /// Trait implemented by types that can be converted to an HTTP response. /// /// Any types that implement this trait can be used in the return type of a handler. pub trait Responder { + type Body: MessageBody + 'static; + /// Convert self to `HttpResponse`. - fn respond_to(self, req: &HttpRequest) -> HttpResponse; + fn respond_to(self, req: &HttpRequest) -> HttpResponse; /// Override a status code for a Responder. /// @@ -59,38 +61,52 @@ pub trait Responder { } impl Responder for HttpResponse { + type Body = BoxBody; + #[inline] - fn respond_to(self, _: &HttpRequest) -> HttpResponse { + fn respond_to(self, _: &HttpRequest) -> HttpResponse { self } } -impl Responder for actix_http::Response { +impl Responder for actix_http::Response { + type Body = BoxBody; + #[inline] - fn respond_to(self, _: &HttpRequest) -> HttpResponse { + fn respond_to(self, _: &HttpRequest) -> HttpResponse { HttpResponse::from(self) } } impl Responder for HttpResponseBuilder { + type Body = BoxBody; + #[inline] - fn respond_to(mut self, _: &HttpRequest) -> HttpResponse { + fn respond_to(mut self, _: &HttpRequest) -> HttpResponse { self.finish() } } impl Responder for actix_http::ResponseBuilder { + type Body = BoxBody; + #[inline] - fn respond_to(mut self, _: &HttpRequest) -> HttpResponse { - HttpResponse::from(self.finish()) + fn respond_to(mut self, req: &HttpRequest) -> HttpResponse { + self.finish().map_into_boxed_body().respond_to(req) } } -impl Responder for Option { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { +impl Responder for Option +where + T: Responder, + ::Error: Into, +{ + type Body = EitherBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self { - Some(val) => val.respond_to(req), - None => HttpResponse::new(StatusCode::NOT_FOUND), + Some(val) => val.respond_to(req).map_into_left_body(), + None => HttpResponse::new(StatusCode::NOT_FOUND).map_into_right_body(), } } } @@ -98,47 +114,69 @@ impl Responder for Option { impl Responder for Result where T: Responder, + ::Error: Into, E: Into, { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { + type Body = EitherBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self { - Ok(val) => val.respond_to(req), - Err(e) => HttpResponse::from_error(e.into()), + Ok(val) => val.respond_to(req).map_into_left_body(), + Err(err) => HttpResponse::from_error(err.into()).map_into_right_body(), } } } impl Responder for (T, StatusCode) { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { + type Body = T::Body; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { let mut res = self.0.respond_to(req); *res.status_mut() = self.1; res } } -macro_rules! impl_responder { - ($res: ty, $ct: path) => { +macro_rules! impl_responder_by_forward_into_base_response { + ($res:ty, $body:ty) => { impl Responder for $res { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { - HttpResponse::Ok().content_type($ct).body(self) + type Body = $body; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + let res: actix_http::Response<_> = self.into(); + res.into() + } + } + }; + + ($res:ty) => { + impl_responder_by_forward_into_base_response!($res, $res); + }; +} + +impl_responder_by_forward_into_base_response!(&'static [u8]); +impl_responder_by_forward_into_base_response!(Bytes); +impl_responder_by_forward_into_base_response!(BytesMut); + +impl_responder_by_forward_into_base_response!(&'static str); +impl_responder_by_forward_into_base_response!(String); + +macro_rules! impl_into_string_responder { + ($res:ty) => { + impl Responder for $res { + type Body = String; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + let string: String = self.into(); + let res: actix_http::Response<_> = string.into(); + res.into() } } }; } -impl_responder!(&'static str, mime::TEXT_PLAIN_UTF_8); - -impl_responder!(String, mime::TEXT_PLAIN_UTF_8); - -impl_responder!(&'_ String, mime::TEXT_PLAIN_UTF_8); - -impl_responder!(Cow<'_, str>, mime::TEXT_PLAIN_UTF_8); - -impl_responder!(&'static [u8], mime::APPLICATION_OCTET_STREAM); - -impl_responder!(Bytes, mime::APPLICATION_OCTET_STREAM); - -impl_responder!(BytesMut, mime::APPLICATION_OCTET_STREAM); +impl_into_string_responder!(&'_ String); +impl_into_string_responder!(Cow<'_, str>); /// Allows overriding status code and headers for a responder. pub struct CustomResponder { @@ -204,11 +242,17 @@ impl CustomResponder { } } -impl Responder for CustomResponder { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { +impl Responder for CustomResponder +where + T: Responder, + ::Error: Into, +{ + type Body = EitherBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { let headers = match self.headers { Ok(headers) => headers, - Err(err) => return HttpResponse::from_error(Error::from(err)), + Err(err) => return HttpResponse::from_error(err).map_into_right_body(), }; let mut res = self.responder.respond_to(req); @@ -222,7 +266,7 @@ impl Responder for CustomResponder { res.headers_mut().insert(k, v); } - res + res.map_into_left_body() } } @@ -231,11 +275,15 @@ pub(crate) mod tests { use actix_service::Service; use bytes::{Bytes, BytesMut}; + use actix_http::body::to_bytes; + use super::*; - use crate::dev::AnyBody; - use crate::http::{header::CONTENT_TYPE, HeaderValue, StatusCode}; - use crate::test::{init_service, TestRequest}; - use crate::{error, web, App}; + use crate::{ + error, + http::{header::CONTENT_TYPE, HeaderValue, StatusCode}, + test::{assert_body_eq, init_service, TestRequest}, + web, App, + }; #[actix_rt::test] async fn test_option_responder() { @@ -253,112 +301,116 @@ pub(crate) mod tests { let req = TestRequest::with_uri("/some").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); - match resp.response().body() { - AnyBody::Bytes(ref b) => { - let bytes = b.clone(); - assert_eq!(bytes, Bytes::from_static(b"some")); - } - _ => panic!(), - } - } - - pub(crate) trait BodyTest { - fn bin_ref(&self) -> &[u8]; - fn body(&self) -> &AnyBody; - } - - impl BodyTest for AnyBody { - fn bin_ref(&self) -> &[u8] { - match self { - AnyBody::Bytes(ref bin) => bin, - _ => unreachable!("bug in test impl"), - } - } - fn body(&self) -> &AnyBody { - self - } + assert_body_eq!(resp, b"some"); } #[actix_rt::test] async fn test_responder() { let req = TestRequest::default().to_http_request(); - let resp = "test".respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); + let res = "test".respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - - let resp = b"test".respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = b"test".respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); - - let resp = "test".to_string().respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), ); - let resp = (&"test".to_string()).respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); + let res = "test".to_string().respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = (&"test".to_string()).respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("text/plain; charset=utf-8") + ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); let s = String::from("test"); - let resp = Cow::Borrowed(s.as_str()).respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); + let res = Cow::Borrowed(s.as_str()).respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - - let resp = Cow::<'_, str>::Owned(s).respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), ); - let resp = Cow::Borrowed("test").respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); + let res = Cow::<'_, str>::Owned(s).respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - - let resp = Bytes::from_static(b"test").respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = Cow::Borrowed("test").respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("text/plain; charset=utf-8") + ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = Bytes::from_static(b"test").respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); - - let resp = BytesMut::from(b"test".as_ref()).respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = BytesMut::from(b"test".as_ref()).respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); // InternalError - let resp = error::InternalError::new("err", StatusCode::BAD_REQUEST).respond_to(&req); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + let res = error::InternalError::new("err", StatusCode::BAD_REQUEST).respond_to(&req); + assert_eq!(res.status(), StatusCode::BAD_REQUEST); } #[actix_rt::test] @@ -368,11 +420,14 @@ pub(crate) mod tests { // Result let resp = Ok::<_, Error>("test".to_string()).respond_to(&req); assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); + assert_eq!( + to_bytes(resp.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); let res = Err::(error::InternalError::new("err", StatusCode::BAD_REQUEST)) .respond_to(&req); @@ -389,7 +444,10 @@ pub(crate) mod tests { .respond_to(&req); assert_eq!(res.status(), StatusCode::BAD_REQUEST); - assert_eq!(res.body().bin_ref(), b"test"); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); let res = "test" .to_string() @@ -397,11 +455,14 @@ pub(crate) mod tests { .respond_to(&req); assert_eq!(res.status(), StatusCode::OK); - assert_eq!(res.body().bin_ref(), b"test"); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("json") ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); } #[actix_rt::test] @@ -409,17 +470,23 @@ pub(crate) mod tests { let req = TestRequest::default().to_http_request(); let res = ("test".to_string(), StatusCode::BAD_REQUEST).respond_to(&req); assert_eq!(res.status(), StatusCode::BAD_REQUEST); - assert_eq!(res.body().bin_ref(), b"test"); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); let req = TestRequest::default().to_http_request(); let res = ("test".to_string(), StatusCode::OK) .with_header((CONTENT_TYPE, mime::APPLICATION_JSON)) .respond_to(&req); assert_eq!(res.status(), StatusCode::OK); - assert_eq!(res.body().bin_ref(), b"test"); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/json") ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); } } diff --git a/src/response/builder.rs b/src/response/builder.rs index e61f7e16f..b5bef2e99 100644 --- a/src/response/builder.rs +++ b/src/response/builder.rs @@ -1,14 +1,13 @@ use std::{ cell::{Ref, RefMut}, convert::TryInto, - error::Error as StdError, future::Future, pin::Pin, task::{Context, Poll}, }; use actix_http::{ - body::{AnyBody, BodyStream}, + body::{BodyStream, BoxBody, MessageBody}, http::{ header::{self, HeaderName, IntoHeaderPair, IntoHeaderValue}, ConnectionType, Error as HttpError, StatusCode, @@ -26,14 +25,14 @@ use cookie::{Cookie, CookieJar}; use crate::{ error::{Error, JsonPayloadError}, - HttpResponse, + BoxError, HttpResponse, }; /// An HTTP response builder. /// /// This type can be used to construct an instance of `Response` through a builder-like pattern. pub struct HttpResponseBuilder { - res: Option>, + res: Option>, err: Option, #[cfg(feature = "cookies")] cookies: Option, @@ -44,7 +43,7 @@ impl HttpResponseBuilder { /// Create response builder pub fn new(status: StatusCode) -> Self { Self { - res: Some(Response::new(status)), + res: Some(Response::with_body(status, BoxBody::new(()))), err: None, #[cfg(feature = "cookies")] cookies: None, @@ -299,7 +298,6 @@ impl HttpResponseBuilder { } /// Mutable reference to a the response's extensions - #[inline] pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> { self.res .as_mut() @@ -307,18 +305,20 @@ impl HttpResponseBuilder { .extensions_mut() } - /// Set a body and generate `Response`. + /// Set a body and build the `HttpResponse`. /// /// `HttpResponseBuilder` can not be used after this call. - #[inline] - pub fn body>(&mut self, body: B) -> HttpResponse { - match self.message_body(body.into()) { - Ok(res) => res, + pub fn body(&mut self, body: B) -> HttpResponse + where + B: MessageBody + 'static, + { + match self.message_body(body) { + Ok(res) => res.map_into_boxed_body(), Err(err) => HttpResponse::from_error(err), } } - /// Set a body and generate `Response`. + /// Set a body and build the `HttpResponse`. /// /// `HttpResponseBuilder` can not be used after this call. pub fn message_body(&mut self, body: B) -> Result, Error> { @@ -332,7 +332,7 @@ impl HttpResponseBuilder { .expect("cannot reuse response builder") .set_body(body); - #[allow(unused_mut)] + #[allow(unused_mut)] // mut is only unused when cookies are disabled let mut res = HttpResponse::from(res); #[cfg(feature = "cookies")] @@ -348,19 +348,19 @@ impl HttpResponseBuilder { Ok(res) } - /// Set a streaming body and generate `Response`. + /// Set a streaming body and build the `HttpResponse`. /// /// `HttpResponseBuilder` can not be used after this call. #[inline] pub fn streaming(&mut self, stream: S) -> HttpResponse where S: Stream> + 'static, - E: Into> + 'static, + E: Into + 'static, { - self.body(AnyBody::new_boxed(BodyStream::new(stream))) + self.body(BodyStream::new(stream)) } - /// Set a json body and generate `Response` + /// Set a JSON body and build the `HttpResponse`. /// /// `HttpResponseBuilder` can not be used after this call. pub fn json(&mut self, value: impl Serialize) -> HttpResponse { @@ -376,18 +376,18 @@ impl HttpResponseBuilder { self.insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)); } - self.body(AnyBody::from(body)) + self.body(body) } Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)), } } - /// Set an empty body and generate `Response` + /// Set an empty body and build the `HttpResponse`. /// /// `HttpResponseBuilder` can not be used after this call. #[inline] pub fn finish(&mut self) -> HttpResponse { - self.body(AnyBody::empty()) + self.body(()) } /// This method construct new `HttpResponseBuilder` @@ -416,7 +416,7 @@ impl From for HttpResponse { } } -impl From for Response { +impl From for Response { fn from(mut builder: HttpResponseBuilder) -> Self { builder.finish().into() } @@ -435,12 +435,9 @@ mod tests { use actix_http::body; use super::*; - use crate::{ - dev::AnyBody, - http::{ - header::{self, HeaderValue, CONTENT_TYPE}, - StatusCode, - }, + use crate::http::{ + header::{self, HeaderValue, CONTENT_TYPE}, + StatusCode, }; #[test] @@ -475,7 +472,7 @@ mod tests { fn test_content_type() { let resp = HttpResponseBuilder::new(StatusCode::OK) .content_type("text/plain") - .body(AnyBody::empty()); + .body(Bytes::new()); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") } diff --git a/src/response/response.rs b/src/response/response.rs index 23562ab0e..97de21e42 100644 --- a/src/response/response.rs +++ b/src/response/response.rs @@ -8,7 +8,7 @@ use std::{ }; use actix_http::{ - body::{AnyBody, MessageBody}, + body::{BoxBody, EitherBody, MessageBody}, http::{header::HeaderMap, StatusCode}, Extensions, Response, ResponseHead, }; @@ -25,12 +25,12 @@ use { use crate::{error::Error, HttpResponseBuilder}; /// An outgoing response. -pub struct HttpResponse { +pub struct HttpResponse { res: Response, pub(crate) error: Option, } -impl HttpResponse { +impl HttpResponse { /// Constructs a response. #[inline] pub fn new(status: StatusCode) -> Self { @@ -227,8 +227,26 @@ impl HttpResponse { } } - // TODO: into_body equivalent - // TODO: into_boxed_body + // TODO: docs for the body map methods below + + #[inline] + pub fn map_into_left_body(self) -> HttpResponse> { + self.map_body(|_, body| EitherBody::left(body)) + } + + #[inline] + pub fn map_into_right_body(self) -> HttpResponse> { + self.map_body(|_, body| EitherBody::right(body)) + } + + #[inline] + pub fn map_into_boxed_body(self) -> HttpResponse + where + B: MessageBody + 'static, + { + // TODO: avoid double boxing with down-casting, if it improves perf + self.map_body(|_, body| BoxBody::new(body)) + } /// Extract response body pub fn into_body(self) -> B { @@ -273,14 +291,14 @@ impl From> for Response { } } -// Future is only implemented for AnyBody payload type because it's the most useful for making +// Future is only implemented for BoxBody payload type because it's the most useful for making // simple handlers without async blocks. Making it generic over all MessageBody types requires a // future impl on Response which would cause it's body field to be, undesirably, Option. // // This impl is not particularly efficient due to the Response construction and should probably // not be invoked if performance is important. Prefer an async fn/block in such cases. -impl Future for HttpResponse { - type Output = Result, Error>; +impl Future for HttpResponse { + type Output = Result, Error>; fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll { if let Some(err) = self.error.take() { diff --git a/src/route.rs b/src/route.rs index 0c0699430..1eb323068 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,19 +1,18 @@ -#![allow(clippy::rc_buffer)] // inner value is mutated before being shared (`Rc::get_mut`) - -use std::{future::Future, rc::Rc}; +use std::{future::Future, mem, rc::Rc}; use actix_http::http::Method; use actix_service::{ - boxed::{self, BoxService, BoxServiceFactory}, - Service, ServiceFactory, ServiceFactoryExt, + boxed::{self, BoxService}, + fn_service, Service, ServiceFactory, ServiceFactoryExt, }; use futures_core::future::LocalBoxFuture; use crate::{ + body::MessageBody, guard::{self, Guard}, handler::{handler_service, Handler}, - service::{ServiceRequest, ServiceResponse}, - Error, FromRequest, HttpResponse, Responder, + service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse}, + BoxError, Error, FromRequest, HttpResponse, Responder, }; /// Resource route definition @@ -21,7 +20,7 @@ use crate::{ /// Route uses builder-like pattern for configuration. /// If handler is not explicitly set, default *404 Not Found* handler is used. pub struct Route { - service: BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>, + service: BoxedHttpServiceFactory, guards: Rc>>, } @@ -30,13 +29,15 @@ impl Route { #[allow(clippy::new_without_default)] pub fn new() -> Route { Route { - service: handler_service(HttpResponse::NotFound), + service: boxed::factory(fn_service(|req: ServiceRequest| async { + Ok(req.into_response(HttpResponse::NotFound())) + })), guards: Rc::new(Vec::new()), } } pub(crate) fn take_guards(&mut self) -> Vec> { - std::mem::take(Rc::get_mut(&mut self.guards).unwrap()) + mem::take(Rc::get_mut(&mut self.guards).unwrap()) } } @@ -181,6 +182,8 @@ impl Route { T: FromRequest + 'static, R: Future + 'static, R::Output: Responder + 'static, + ::Body: MessageBody, + <::Body as MessageBody>::Error: Into, { self.service = handler_service(handler); self diff --git a/src/scope.rs b/src/scope.rs index c20b5d7c8..ff013671b 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -3,9 +3,8 @@ use std::{cell::RefCell, fmt, future::Future, mem, rc::Rc}; use actix_http::Extensions; use actix_router::{ResourceDef, Router}; use actix_service::{ - apply, apply_fn_factory, - boxed::{self, BoxService, BoxServiceFactory}, - IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt, Transform, + apply, apply_fn_factory, boxed, IntoServiceFactory, Service, ServiceFactory, + ServiceFactoryExt, Transform, }; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; @@ -13,16 +12,17 @@ use futures_util::future::join_all; use crate::{ config::ServiceConfig, data::Data, - dev::{AppService, HttpServiceFactory}, + dev::AppService, guard::Guard, rmap::ResourceMap, - service::{AppServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse}, + service::{ + AppServiceFactory, BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, + ServiceFactoryWrapper, ServiceRequest, ServiceResponse, + }, Error, Resource, Route, }; type Guards = Vec>; -type HttpService = BoxService; -type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; /// Resources scope. /// @@ -58,7 +58,7 @@ pub struct Scope { app_data: Option, services: Vec>, guards: Vec>, - default: Option>, + default: Option>, external: Vec, factory_ref: Rc>>, } @@ -470,8 +470,14 @@ where } pub struct ScopeFactory { - services: Rc<[(ResourceDef, HttpNewService, RefCell>)]>, - default: Rc, + services: Rc< + [( + ResourceDef, + BoxedHttpServiceFactory, + RefCell>, + )], + >, + default: Rc, } impl ServiceFactory for ScopeFactory { @@ -518,8 +524,8 @@ impl ServiceFactory for ScopeFactory { } pub struct ScopeService { - router: Router>>, - default: HttpService, + router: Router>>, + default: BoxedHttpService, } impl Service for ScopeService { @@ -580,12 +586,11 @@ mod tests { use bytes::Bytes; use crate::{ - dev::AnyBody, guard, http::{header, HeaderValue, Method, StatusCode}, middleware::DefaultHeaders, service::{ServiceRequest, ServiceResponse}, - test::{call_service, init_service, read_body, TestRequest}, + test::{assert_body_eq, call_service, init_service, read_body, TestRequest}, web, App, HttpMessage, HttpRequest, HttpResponse, }; @@ -748,20 +753,13 @@ mod tests { .await; let req = TestRequest::with_uri("/ab-project1/path1").to_request(); - let resp = srv.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - - match resp.response().body() { - AnyBody::Bytes(ref b) => { - let bytes = b.clone(); - assert_eq!(bytes, Bytes::from_static(b"project: project1")); - } - _ => panic!(), - } + let res = srv.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_body_eq!(res, b"project: project1"); let req = TestRequest::with_uri("/aa-project1/path1").to_request(); - let resp = srv.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); + let res = srv.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] @@ -849,16 +847,9 @@ mod tests { .await; let req = TestRequest::with_uri("/app/project_1/path1").to_request(); - let resp = srv.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::CREATED); - - match resp.response().body() { - AnyBody::Bytes(ref b) => { - let bytes = b.clone(); - assert_eq!(bytes, Bytes::from_static(b"project: project_1")); - } - _ => panic!(), - } + let res = srv.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::CREATED); + assert_body_eq!(res, b"project: project_1"); } #[actix_rt::test] @@ -877,20 +868,13 @@ mod tests { .await; let req = TestRequest::with_uri("/app/test/1/path1").to_request(); - let resp = srv.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::CREATED); - - match resp.response().body() { - AnyBody::Bytes(ref b) => { - let bytes = b.clone(); - assert_eq!(bytes, Bytes::from_static(b"project: test - 1")); - } - _ => panic!(), - } + let res = srv.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::CREATED); + assert_body_eq!(res, b"project: test - 1"); let req = TestRequest::with_uri("/app/test/1/path2").to_request(); - let resp = srv.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); + let res = srv.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] diff --git a/src/server.rs b/src/server.rs index 1bf56655b..3db849410 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,8 +1,6 @@ use std::{ any::Any, - cmp, - error::Error as StdError, - fmt, io, + cmp, fmt, io, marker::PhantomData, net, sync::{Arc, Mutex}, @@ -75,15 +73,13 @@ where I: IntoServiceFactory, S: ServiceFactory + 'static, - // S::Future: 'static, S::Error: Into + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, S::Service: 'static, - // S::Service: 'static, + B: MessageBody + 'static, - B::Error: Into>, { /// Create new HTTP server with application factory pub fn new(factory: F) -> Self { @@ -656,8 +652,8 @@ fn create_tcp_listener(addr: net::SocketAddr, backlog: u32) -> io::Result io::Result { builder.set_alpn_select_callback(|_, protocols| { const H2: &[u8] = b"\x02h2"; diff --git a/src/service.rs b/src/service.rs index 8ba38df43..df9e809e4 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,14 +1,19 @@ -use std::cell::{Ref, RefMut}; -use std::rc::Rc; -use std::{fmt, net}; +use std::{ + cell::{Ref, RefMut}, + fmt, net, + rc::Rc, +}; use actix_http::{ - body::{AnyBody, MessageBody}, + body::{BoxBody, EitherBody, MessageBody}, http::{HeaderMap, Method, StatusCode, Uri, Version}, Extensions, HttpMessage, Payload, PayloadStream, RequestHead, Response, ResponseHead, }; use actix_router::{IntoPatterns, Path, Patterns, Resource, ResourceDef, Url}; -use actix_service::{IntoServiceFactory, ServiceFactory}; +use actix_service::{ + boxed::{BoxService, BoxServiceFactory}, + IntoServiceFactory, ServiceFactory, +}; #[cfg(feature = "cookies")] use cookie::{Cookie, ParseError as CookieParseError}; @@ -21,6 +26,10 @@ use crate::{ Error, HttpRequest, HttpResponse, }; +pub(crate) type BoxedHttpService = BoxService, Error>; +pub(crate) type BoxedHttpServiceFactory = + BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; + pub trait HttpServiceFactory { fn register(self, config: &mut AppService); } @@ -326,12 +335,12 @@ impl fmt::Debug for ServiceRequest { } /// A service level response wrapper. -pub struct ServiceResponse { +pub struct ServiceResponse { request: HttpRequest, response: HttpResponse, } -impl ServiceResponse { +impl ServiceResponse { /// Create service response from the error pub fn from_err>(err: E, request: HttpRequest) -> Self { let response = HttpResponse::from_error(err); @@ -401,6 +410,7 @@ impl ServiceResponse { impl ServiceResponse { /// Set a new body + #[inline] pub fn map_body(self, f: F) -> ServiceResponse where F: FnOnce(&mut ResponseHead, B) -> B2, @@ -412,6 +422,24 @@ impl ServiceResponse { request: self.request, } } + + #[inline] + pub fn map_into_left_body(self) -> ServiceResponse> { + self.map_body(|_, body| EitherBody::left(body)) + } + + #[inline] + pub fn map_into_right_body(self) -> ServiceResponse> { + self.map_body(|_, body| EitherBody::right(body)) + } + + #[inline] + pub fn map_into_boxed_body(self) -> ServiceResponse + where + B: MessageBody + 'static, + { + self.map_body(|_, body| BoxBody::new(body)) + } } impl From> for HttpResponse { diff --git a/src/test.rs b/src/test.rs index 77765e267..2cd01039d 100644 --- a/src/test.rs +++ b/src/test.rs @@ -4,7 +4,6 @@ use std::{borrow::Cow, net::SocketAddr, rc::Rc}; pub use actix_http::test::TestBuffer; use actix_http::{ - body, http::{header::IntoHeaderPair, Method, StatusCode, Uri, Version}, test::TestRequest as HttpTestRequest, Extensions, Request, @@ -20,9 +19,10 @@ use serde::{de::DeserializeOwned, Serialize}; use crate::cookie::{Cookie, CookieJar}; use crate::{ app_service::AppInitServiceState, + body::{self, BoxBody, MessageBody}, config::AppConfig, data::Data, - dev::{AnyBody, MessageBody, Payload}, + dev::Payload, http::header::ContentType, rmap::ResourceMap, service::{ServiceRequest, ServiceResponse}, @@ -32,14 +32,14 @@ use crate::{ /// Create service that always responds with `HttpResponse::Ok()` and no body. pub fn ok_service( -) -> impl Service, Error = Error> { +) -> impl Service, Error = Error> { default_service(StatusCode::OK) } /// Create service that always responds with given status code and no body. pub fn default_service( status_code: StatusCode, -) -> impl Service, Error = Error> { +) -> impl Service, Error = Error> { (move |req: ServiceRequest| { ok(req.into_response(HttpResponseBuilder::new(status_code).finish())) }) @@ -632,6 +632,22 @@ impl TestRequest { } } +/// Reduces boilerplate code when testing expected response payloads. +#[cfg(test)] +macro_rules! assert_body_eq { + ($res:ident, $expected:expr) => { + assert_eq!( + ::actix_http::body::to_bytes($res.into_body()) + .await + .expect("body read should have succeeded"), + Bytes::from_static($expected), + ) + }; +} + +#[cfg(test)] +pub(crate) use assert_body_eq; + #[cfg(test)] mod tests { use std::time::SystemTime; diff --git a/src/types/either.rs b/src/types/either.rs index 0a8a90133..3c759736e 100644 --- a/src/types/either.rs +++ b/src/types/either.rs @@ -12,7 +12,7 @@ use futures_core::ready; use pin_project_lite::pin_project; use crate::{ - dev, + body, dev, web::{Form, Json}, Error, FromRequest, HttpRequest, HttpResponse, Responder, }; @@ -146,10 +146,12 @@ where L: Responder, R: Responder, { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { + type Body = body::EitherBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self { - Either::Left(a) => a.respond_to(req), - Either::Right(b) => b.respond_to(req), + Either::Left(a) => a.respond_to(req).map_into_left_body(), + Either::Right(b) => b.respond_to(req).map_into_right_body(), } } } diff --git a/src/types/form.rs b/src/types/form.rs index 098a864de..9c09c6b73 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -20,8 +20,9 @@ use serde::{de::DeserializeOwned, Serialize}; #[cfg(feature = "__compress")] use crate::dev::Decompress; use crate::{ - error::UrlencodedError, extract::FromRequest, http::header::CONTENT_LENGTH, web, Error, - HttpMessage, HttpRequest, HttpResponse, Responder, + body::EitherBody, error::UrlencodedError, extract::FromRequest, + http::header::CONTENT_LENGTH, web, Error, HttpMessage, HttpRequest, HttpResponse, + Responder, }; /// URL encoded payload extractor and responder. @@ -180,12 +181,21 @@ impl fmt::Display for Form { /// See [here](#responder) for example of usage as a handler return type. impl Responder for Form { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { + type Body = EitherBody; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { match serde_urlencoded::to_string(&self.0) { - Ok(body) => HttpResponse::Ok() + Ok(body) => match HttpResponse::Ok() .content_type(mime::APPLICATION_WWW_FORM_URLENCODED) - .body(body), - Err(err) => HttpResponse::from_error(UrlencodedError::Serialize(err)), + .message_body(body) + { + Ok(res) => res.map_into_left_body(), + Err(err) => HttpResponse::from_error(err).map_into_right_body(), + }, + + Err(err) => { + HttpResponse::from_error(UrlencodedError::Serialize(err)).map_into_right_body() + } } } } @@ -408,11 +418,14 @@ mod tests { use serde::{Deserialize, Serialize}; use super::*; - use crate::http::{ - header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE}, - StatusCode, - }; use crate::test::TestRequest; + use crate::{ + http::{ + header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE}, + StatusCode, + }, + test::assert_body_eq, + }; #[derive(Deserialize, Serialize, Debug, PartialEq)] struct Info { @@ -520,15 +533,13 @@ mod tests { hello: "world".to_string(), counter: 123, }); - let resp = form.respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); + let res = form.respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/x-www-form-urlencoded") ); - - use crate::responder::tests::BodyTest; - assert_eq!(resp.body().bin_ref(), b"hello=world&counter=123"); + assert_body_eq!(res, b"hello=world&counter=123"); } #[actix_rt::test] diff --git a/src/types/json.rs b/src/types/json.rs index 6d07fe45a..2b4d220e2 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -19,6 +19,7 @@ use actix_http::Payload; #[cfg(feature = "__compress")] use crate::dev::Decompress; use crate::{ + body::EitherBody, error::{Error, JsonPayloadError}, extract::FromRequest, http::header::CONTENT_LENGTH, @@ -116,12 +117,21 @@ impl Serialize for Json { /// /// If serialization failed impl Responder for Json { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { + type Body = EitherBody; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { match serde_json::to_string(&self.0) { - Ok(body) => HttpResponse::Ok() + Ok(body) => match HttpResponse::Ok() .content_type(mime::APPLICATION_JSON) - .body(body), - Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)), + .message_body(body) + { + Ok(res) => res.map_into_left_body(), + Err(err) => HttpResponse::from_error(err).map_into_right_body(), + }, + + Err(err) => { + HttpResponse::from_error(JsonPayloadError::Serialize(err)).map_into_right_body() + } } } } @@ -444,7 +454,7 @@ mod tests { header::{self, CONTENT_LENGTH, CONTENT_TYPE}, StatusCode, }, - test::{load_body, TestRequest}, + test::{assert_body_eq, load_body, TestRequest}, }; #[derive(Serialize, Deserialize, PartialEq, Debug)] @@ -472,15 +482,13 @@ mod tests { let j = Json(MyObject { name: "test".to_string(), }); - let resp = j.respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); + let res = j.respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), + res.headers().get(header::CONTENT_TYPE).unwrap(), header::HeaderValue::from_static("application/json") ); - - use crate::responder::tests::BodyTest; - assert_eq!(resp.body().bin_ref(), b"{\"name\":\"test\"}"); + assert_body_eq!(res, b"{\"name\":\"test\"}"); } #[actix_rt::test] diff --git a/src/types/path.rs b/src/types/path.rs index cd24deb81..4b60d27c0 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -90,7 +90,7 @@ impl fmt::Display for Path { } } -/// See [here](#usage) for example of usage as an extractor. +/// See [here](#Examples) for example of usage as an extractor. impl FromRequest for Path where T: de::DeserializeOwned, diff --git a/src/types/payload.rs b/src/types/payload.rs index 00047e8b1..73987def5 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -43,12 +43,12 @@ use crate::{ /// Ok(format!("Request Body Bytes:\n{:?}", bytes)) /// } /// ``` -pub struct Payload(crate::dev::Payload); +pub struct Payload(dev::Payload); impl Payload { /// Unwrap to inner Payload type. #[inline] - pub fn into_inner(self) -> crate::dev::Payload { + pub fn into_inner(self) -> dev::Payload { self.0 } } @@ -62,7 +62,7 @@ impl Stream for Payload { } } -/// See [here](#usage) for example of usage as an extractor. +/// See [here](#Examples) for example of usage as an extractor. impl FromRequest for Payload { type Error = Error; type Future = Ready>; diff --git a/src/types/query.rs b/src/types/query.rs index ba2034bfc..9fac21173 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -105,7 +105,7 @@ impl fmt::Display for Query { } } -/// See [here](#usage) for example of usage as an extractor. +/// See [here](#Examples) for example of usage as an extractor. impl FromRequest for Query { type Error = Error; type Future = Ready>; diff --git a/src/web.rs b/src/web.rs index e9f5c8518..b58adc2f8 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,14 +1,14 @@ //! Essentials helper functions and types for application registration. -use std::future::Future; +use std::{error::Error as StdError, future::Future}; use actix_http::http::Method; use actix_router::IntoPatterns; pub use bytes::{Buf, BufMut, Bytes, BytesMut}; use crate::{ - error::BlockingError, extract::FromRequest, handler::Handler, resource::Resource, - responder::Responder, route::Route, scope::Scope, service::WebService, + body::MessageBody, error::BlockingError, extract::FromRequest, handler::Handler, + resource::Resource, responder::Responder, route::Route, scope::Scope, service::WebService, }; pub use crate::config::ServiceConfig; @@ -145,6 +145,8 @@ where I: FromRequest + 'static, R: Future + 'static, R::Output: Responder + 'static, + ::Body: MessageBody + 'static, + <::Body as MessageBody>::Error: Into>, { Route::new().to(handler) } diff --git a/tests/test_server.rs b/tests/test_server.rs index 3f0fbfccc..a850f228d 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -200,13 +200,10 @@ async fn test_body_encoding_override() { .body(STR) }))) .service(web::resource("/raw").route(web::to(|| { - let body = actix_web::dev::AnyBody::Bytes(STR.into()); let mut response = - HttpResponse::with_body(actix_web::http::StatusCode::OK, body); - + HttpResponse::with_body(actix_web::http::StatusCode::OK, STR); response.encoding(ContentEncoding::Deflate); - - response + response.map_into_boxed_body() }))) });