mirror of
https://github.com/fafhrd91/actix-web
synced 2025-08-01 04:51:51 +02:00
Compare commits
1 Commits
web-v4.0.0
...
remove-eit
Author | SHA1 | Date | |
---|---|---|---|
|
2ee953a118 |
12
CHANGES.md
12
CHANGES.md
@@ -1,31 +1,19 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 4.0.0-beta.15 - 2021-12-17
|
||||
### Added
|
||||
* Method on `Responder` trait (`customize`) for customizing responders and `CustomizeResponder` struct. [#2510]
|
||||
* Implement `Debug` for `DefaultHeaders`. [#2510]
|
||||
|
||||
### Changed
|
||||
* Align `DefaultHeader` method terminology, deprecating previous methods. [#2510]
|
||||
* Response service types in `ErrorHandlers` middleware now use `ServiceResponse<EitherBody<B>>` to allow changing the body type. [#2515]
|
||||
* Both variants in `ErrorHandlerResponse` now use `ServiceResponse<EitherBody<B>>`. [#2515]
|
||||
* Rename `test::{default_service => simple_service}`. Old name is deprecated. [#2518]
|
||||
* Rename `test::{read_response_json => call_and_read_body_json}`. Old name is deprecated. [#2518]
|
||||
* Rename `test::{read_response => call_and_read_body}`. Old name is deprecated. [#2518]
|
||||
* Relax body type and error bounds on test utilities.
|
||||
|
||||
### Removed
|
||||
* Top-level `EitherExtractError` export. [#2510]
|
||||
* Conversion implementations for `either` crate. [#2516]
|
||||
* `test::load_stream` and `test::load_body`; replace usage with `body::to_bytes`. [#2518]
|
||||
|
||||
[#2510]: https://github.com/actix/actix-web/pull/2510
|
||||
[#2515]: https://github.com/actix/actix-web/pull/2515
|
||||
[#2516]: https://github.com/actix/actix-web/pull/2516
|
||||
[#2518]: https://github.com/actix/actix-web/pull/2518
|
||||
|
||||
|
||||
## 4.0.0-beta.14 - 2021-12-11
|
||||
|
10
Cargo.toml
10
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-web"
|
||||
version = "4.0.0-beta.15"
|
||||
version = "4.0.0-beta.14"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
||||
keywords = ["actix", "http", "web", "framework", "async"]
|
||||
@@ -77,8 +77,8 @@ actix-service = "2.0.0"
|
||||
actix-utils = "3.0.0"
|
||||
actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true }
|
||||
|
||||
actix-http = "3.0.0-beta.16"
|
||||
actix-router = "0.5.0-beta.3"
|
||||
actix-http = "3.0.0-beta.15"
|
||||
actix-router = "0.5.0-beta.2"
|
||||
actix-web-codegen = "0.5.0-beta.6"
|
||||
|
||||
ahash = "0.7"
|
||||
@@ -106,8 +106,8 @@ time = { version = "0.3", default-features = false, features = ["formatting"] }
|
||||
url = "2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-test = { version = "0.1.0-beta.9", features = ["openssl", "rustls"] }
|
||||
awc = { version = "3.0.0-beta.14", features = ["openssl"] }
|
||||
actix-test = { version = "0.1.0-beta.8", features = ["openssl", "rustls"] }
|
||||
awc = { version = "3.0.0-beta.13", features = ["openssl"] }
|
||||
|
||||
brotli2 = "0.3.2"
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
|
@@ -6,10 +6,10 @@
|
||||
<p>
|
||||
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web/4.0.0-beta.15)
|
||||
[](https://docs.rs/actix-web/4.0.0-beta.14)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||

|
||||
[](https://deps.rs/crate/actix-web/4.0.0-beta.15)
|
||||
[](https://deps.rs/crate/actix-web/4.0.0-beta.14)
|
||||
<br />
|
||||
[](https://github.com/actix/actix-web/actions)
|
||||
[](https://codecov.io/gh/actix/actix-web)
|
||||
|
@@ -22,10 +22,10 @@ path = "src/lib.rs"
|
||||
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
|
||||
|
||||
[dependencies]
|
||||
actix-http = "3.0.0-beta.16"
|
||||
actix-http = "3.0.0-beta.15"
|
||||
actix-service = "2"
|
||||
actix-utils = "3"
|
||||
actix-web = { version = "4.0.0-beta.15", default-features = false }
|
||||
actix-web = { version = "4.0.0-beta.14", default-features = false }
|
||||
|
||||
askama_escape = "0.10"
|
||||
bitflags = "1"
|
||||
@@ -43,5 +43,5 @@ tokio-uring = { version = "0.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-test = "0.1.0-beta.9"
|
||||
actix-web = "4.0.0-beta.15"
|
||||
actix-test = "0.1.0-beta.8"
|
||||
actix-web = "4.0.0-beta.14"
|
||||
|
@@ -35,7 +35,7 @@ actix-tls = "3.0.0-rc.1"
|
||||
actix-utils = "3.0.0"
|
||||
actix-rt = "2.2"
|
||||
actix-server = "2.0.0-rc.1"
|
||||
awc = { version = "3.0.0-beta.14", default-features = false }
|
||||
awc = { version = "3.0.0-beta.13", default-features = false }
|
||||
|
||||
base64 = "0.13"
|
||||
bytes = "1"
|
||||
@@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||
tokio = { version = "1.2", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = { version = "4.0.0-beta.15", default-features = false, features = ["cookies"] }
|
||||
actix-http = "3.0.0-beta.16"
|
||||
actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["cookies"] }
|
||||
actix-http = "3.0.0-beta.15"
|
||||
|
@@ -1,22 +1,12 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-beta.16 - 2021-12-17
|
||||
### Added
|
||||
* New method on `MessageBody` trait, `try_into_bytes`, with default implementation, for optimizations on body types that complete in exactly one poll. Replaces `is_complete_body` and `take_complete_body`. [#2522]
|
||||
|
||||
### Changed
|
||||
* Rename trait `IntoHeaderPair => TryIntoHeaderPair`. [#2510]
|
||||
* Rename `TryIntoHeaderPair::{try_into_header_pair => try_into_pair}`. [#2510]
|
||||
* Rename trait `IntoHeaderValue => TryIntoHeaderValue`. [#2510]
|
||||
|
||||
### Removed
|
||||
* `MessageBody::{is_complete_body,take_complete_body}`. [#2522]
|
||||
|
||||
[#2510]: https://github.com/actix/actix-web/pull/2510
|
||||
[#2522]: https://github.com/actix/actix-web/pull/2522
|
||||
|
||||
|
||||
## 3.0.0-beta.15 - 2021-12-11
|
||||
@@ -37,8 +27,7 @@
|
||||
* `Request::take_conn_data()`. [#2491]
|
||||
* `Request::take_req_data()`. [#2487]
|
||||
* `impl Clone` for `RequestHead`. [#2487]
|
||||
* New methods on `MessageBody` trait, `is_complete_body` and `take_complete_body`, both with default implementations, for optimizations on body types that are done in exactly one poll/chunk. [#2497]
|
||||
* New `boxed` method on `MessageBody` trait for wrapping body type. [#2520]
|
||||
* New methods on `MessageBody` trait, `is_complete_body` and `take_complete_body`, both with default implementations, for optimisations on body types that are done in exactly one poll/chunk. [#2497]
|
||||
|
||||
### Changed
|
||||
* Rename `body::BoxBody::{from_body => new}`. [#2468]
|
||||
@@ -67,7 +56,6 @@
|
||||
[#2488]: https://github.com/actix/actix-web/pull/2488
|
||||
[#2491]: https://github.com/actix/actix-web/pull/2491
|
||||
[#2497]: https://github.com/actix/actix-web/pull/2497
|
||||
[#2520]: https://github.com/actix/actix-web/pull/2520
|
||||
|
||||
|
||||
## 3.0.0-beta.14 - 2021-11-30
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http"
|
||||
version = "3.0.0-beta.16"
|
||||
version = "3.0.0-beta.15"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "HTTP primitives for the Actix ecosystem"
|
||||
keywords = ["actix", "http", "framework", "async", "futures"]
|
||||
@@ -45,7 +45,7 @@ __compress = []
|
||||
actix-service = "2.0.0"
|
||||
actix-codec = "0.4.1"
|
||||
actix-utils = "3.0.0"
|
||||
actix-rt = { version = "2.2", default-features = false }
|
||||
actix-rt = "2.2"
|
||||
|
||||
ahash = "0.7"
|
||||
base64 = "0.13"
|
||||
@@ -55,7 +55,7 @@ bytestring = "1"
|
||||
derive_more = "0.99.5"
|
||||
encoding_rs = "0.8"
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
futures-task = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
|
||||
h2 = "0.3.9"
|
||||
http = "0.2.5"
|
||||
httparse = "1.5.1"
|
||||
@@ -66,6 +66,7 @@ local-channel = "0.1"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
percent-encoding = "2.1"
|
||||
pin-project = "1.0.0"
|
||||
pin-project-lite = "0.2"
|
||||
rand = "0.8"
|
||||
sha-1 = "0.9"
|
||||
@@ -83,12 +84,11 @@ zstd = { version = "0.9", optional = true }
|
||||
actix-http-test = { version = "3.0.0-beta.9", features = ["openssl"] }
|
||||
actix-server = "2.0.0-rc.1"
|
||||
actix-tls = { version = "3.0.0-rc.1", features = ["openssl"] }
|
||||
actix-web = "4.0.0-beta.15"
|
||||
actix-web = "4.0.0-beta.14"
|
||||
|
||||
async-stream = "0.3"
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
env_logger = "0.9"
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
rcgen = "0.8"
|
||||
regex = "1.3"
|
||||
rustls-pemfile = "0.2"
|
||||
|
@@ -3,11 +3,11 @@
|
||||
> HTTP primitives for the Actix ecosystem.
|
||||
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/3.0.0-beta.16)
|
||||
[](https://docs.rs/actix-http/3.0.0-beta.15)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.0.0-beta.16)
|
||||
[](https://deps.rs/crate/actix-http/3.0.0-beta.15)
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
@@ -8,97 +8,76 @@ use std::{
|
||||
use bytes::Bytes;
|
||||
|
||||
use super::{BodySize, MessageBody, MessageBodyMapErr};
|
||||
use crate::body;
|
||||
use crate::Error;
|
||||
|
||||
/// A boxed message body with boxed errors.
|
||||
#[derive(Debug)]
|
||||
pub struct BoxBody(BoxBodyInner);
|
||||
|
||||
enum BoxBodyInner {
|
||||
None(body::None),
|
||||
Bytes(Bytes),
|
||||
Stream(Pin<Box<dyn MessageBody<Error = Box<dyn StdError>>>>),
|
||||
}
|
||||
|
||||
impl fmt::Debug for BoxBodyInner {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::None(arg0) => f.debug_tuple("None").field(arg0).finish(),
|
||||
Self::Bytes(arg0) => f.debug_tuple("Bytes").field(arg0).finish(),
|
||||
Self::Stream(_) => f.debug_tuple("Stream").field(&"dyn MessageBody").finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct BoxBody(Pin<Box<dyn MessageBody<Error = Box<dyn StdError>>>>);
|
||||
|
||||
impl BoxBody {
|
||||
/// Same as `MessageBody::boxed`.
|
||||
///
|
||||
/// If the body type to wrap is unknown or generic it is better to use [`MessageBody::boxed`] to
|
||||
/// avoid double boxing.
|
||||
#[inline]
|
||||
/// Boxes a `MessageBody` and any errors it generates.
|
||||
pub fn new<B>(body: B) -> Self
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
match body.size() {
|
||||
BodySize::None => Self(BoxBodyInner::None(body::None)),
|
||||
_ => match body.try_into_bytes() {
|
||||
Ok(bytes) => Self(BoxBodyInner::Bytes(bytes)),
|
||||
Err(body) => {
|
||||
let body = MessageBodyMapErr::new(body, Into::into);
|
||||
Self(BoxBodyInner::Stream(Box::pin(body)))
|
||||
}
|
||||
},
|
||||
}
|
||||
let body = MessageBodyMapErr::new(body, Into::into);
|
||||
Self(Box::pin(body))
|
||||
}
|
||||
|
||||
/// Returns a mutable pinned reference to the inner message body type.
|
||||
#[inline]
|
||||
pub fn as_pin_mut(&mut self) -> Pin<&mut Self> {
|
||||
Pin::new(self)
|
||||
pub fn as_pin_mut(&mut self) -> Pin<&mut (dyn MessageBody<Error = Box<dyn StdError>>)> {
|
||||
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 = Box<dyn StdError>;
|
||||
type Error = Error;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
match &self.0 {
|
||||
BoxBodyInner::None(none) => none.size(),
|
||||
BoxBodyInner::Bytes(bytes) => bytes.size(),
|
||||
BoxBodyInner::Stream(stream) => stream.size(),
|
||||
}
|
||||
self.0.size()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
match &mut self.0 {
|
||||
BoxBodyInner::None(body) => {
|
||||
Pin::new(body).poll_next(cx).map_err(|err| match err {})
|
||||
}
|
||||
BoxBodyInner::Bytes(body) => {
|
||||
Pin::new(body).poll_next(cx).map_err(|err| match err {})
|
||||
}
|
||||
BoxBodyInner::Stream(body) => Pin::new(body).poll_next(cx),
|
||||
}
|
||||
self.0
|
||||
.as_mut()
|
||||
.poll_next(cx)
|
||||
.map_err(|err| Error::new_body().with_cause(err))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
match self.0 {
|
||||
BoxBodyInner::None(body) => Ok(body.try_into_bytes().unwrap()),
|
||||
BoxBodyInner::Bytes(body) => Ok(body.try_into_bytes().unwrap()),
|
||||
_ => Err(self),
|
||||
}
|
||||
fn is_complete_body(&self) -> bool {
|
||||
self.0.is_complete_body()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn boxed(self) -> BoxBody {
|
||||
self
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
debug_assert!(
|
||||
self.is_complete_body(),
|
||||
"boxed type does not allow taking complete body; caller should make sure to \
|
||||
call `is_complete_body` first",
|
||||
);
|
||||
|
||||
// we do not have DerefMut access to call take_complete_body directly but since
|
||||
// is_complete_body is true we should expect the entire bytes chunk in one poll_next
|
||||
|
||||
let waker = futures_util::task::noop_waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
|
||||
match self.as_pin_mut().poll_next(&mut cx) {
|
||||
Poll::Ready(Some(Ok(data))) => data,
|
||||
_ => {
|
||||
panic!(
|
||||
"boxed type indicated it allows taking complete body but failed to \
|
||||
return Bytes when polled",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -74,22 +74,18 @@ where
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
fn is_complete_body(&self) -> bool {
|
||||
match self {
|
||||
EitherBody::Left { body } => body
|
||||
.try_into_bytes()
|
||||
.map_err(|body| EitherBody::Left { body }),
|
||||
EitherBody::Right { body } => body
|
||||
.try_into_bytes()
|
||||
.map_err(|body| EitherBody::Right { body }),
|
||||
EitherBody::Left { body } => body.is_complete_body(),
|
||||
EitherBody::Right { body } => body.is_complete_body(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn boxed(self) -> BoxBody {
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
match self {
|
||||
EitherBody::Left { body } => body.boxed(),
|
||||
EitherBody::Right { body } => body.boxed(),
|
||||
EitherBody::Left { body } => body.take_complete_body(),
|
||||
EitherBody::Right { body } => body.take_complete_body(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -12,20 +12,16 @@ use bytes::{Bytes, BytesMut};
|
||||
use futures_core::ready;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use super::{BodySize, BoxBody};
|
||||
use super::BodySize;
|
||||
|
||||
/// An interface types that can converted to bytes and used as response bodies.
|
||||
// TODO: examples
|
||||
pub trait MessageBody {
|
||||
/// The type of error that will be returned if streaming body fails.
|
||||
///
|
||||
/// Since it is not appropriate to generate a response mid-stream, it only requires `Error` for
|
||||
/// internal use and logging.
|
||||
// TODO: consider this bound to only fmt::Display since the error type is not really used
|
||||
// and there is an impl for Into<Box<StdError>> on String
|
||||
type Error: Into<Box<dyn StdError>>;
|
||||
|
||||
/// Body size hint.
|
||||
///
|
||||
/// If [`BodySize::None`] is returned, optimizations that skip reading the body are allowed.
|
||||
fn size(&self) -> BodySize;
|
||||
|
||||
/// Attempt to pull out the next chunk of body bytes.
|
||||
@@ -35,32 +31,51 @@ pub trait MessageBody {
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>>;
|
||||
|
||||
/// Try to convert into the complete chunk of body bytes.
|
||||
/// Returns true if entire body bytes chunk is obtainable in one call to `poll_next`.
|
||||
///
|
||||
/// Implement this method if the entire body can be trivially extracted. This is useful for
|
||||
/// optimizations where `poll_next` calls can be avoided.
|
||||
/// This method's implementation should agree with [`take_complete_body`] and should always be
|
||||
/// checked before taking the body.
|
||||
///
|
||||
/// Body types with [`BodySize::None`] are allowed to return empty `Bytes`. Although, if calling
|
||||
/// this method, it is recommended to check `size` first and return early.
|
||||
/// The default implementation returns `false.
|
||||
///
|
||||
/// # Errors
|
||||
/// The default implementation will error and return the original type back to the caller for
|
||||
/// further use.
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Err(self)
|
||||
/// [`take_complete_body`]: MessageBody::take_complete_body
|
||||
fn is_complete_body(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Converts this body into `BoxBody`.
|
||||
#[inline]
|
||||
fn boxed(self) -> BoxBody
|
||||
where
|
||||
Self: Sized + 'static,
|
||||
{
|
||||
BoxBody::new(self)
|
||||
/// Returns the complete chunk of body bytes.
|
||||
///
|
||||
/// Implementors of this method should note the following:
|
||||
/// - It is acceptable to skip the omit checks of [`is_complete_body`]. The responsibility of
|
||||
/// performing this check is delegated to the caller.
|
||||
/// - If the result of [`is_complete_body`] is conditional, that condition should be given
|
||||
/// equivalent attention here.
|
||||
/// - A second call call to [`take_complete_body`] should return an empty `Bytes` or panic.
|
||||
/// - A call to [`poll_next`] after calling [`take_complete_body`] should return `None` unless
|
||||
/// the chunk is guaranteed to be empty.
|
||||
///
|
||||
/// The default implementation panics unconditionally, indicating a control flow bug in the
|
||||
/// calling code.
|
||||
///
|
||||
/// # Panics
|
||||
/// With a correct implementation, panics if called without first checking [`is_complete_body`].
|
||||
///
|
||||
/// [`is_complete_body`]: MessageBody::is_complete_body
|
||||
/// [`take_complete_body`]: MessageBody::take_complete_body
|
||||
/// [`poll_next`]: MessageBody::poll_next
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
assert!(
|
||||
self.is_complete_body(),
|
||||
"type ({}) allows taking complete body but did not provide an implementation \
|
||||
of `take_complete_body`",
|
||||
std::any::type_name::<Self>()
|
||||
);
|
||||
|
||||
unimplemented!(
|
||||
"type ({}) does not allow taking complete body; caller should make sure to \
|
||||
check `is_complete_body` first",
|
||||
std::any::type_name::<Self>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +95,14 @@ mod foreign_impls {
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
match *self {}
|
||||
}
|
||||
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
match *self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for () {
|
||||
@@ -99,14 +122,19 @@ mod foreign_impls {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::new())
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
Bytes::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> MessageBody for Box<B>
|
||||
where
|
||||
B: MessageBody + Unpin + ?Sized,
|
||||
B: MessageBody + Unpin,
|
||||
{
|
||||
type Error = B::Error;
|
||||
|
||||
@@ -122,11 +150,21 @@ mod foreign_impls {
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
Pin::new(self.get_mut().as_mut()).poll_next(cx)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_complete_body(&self) -> bool {
|
||||
self.as_ref().is_complete_body()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
self.as_mut().take_complete_body()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> MessageBody for Pin<Box<B>>
|
||||
where
|
||||
B: MessageBody + ?Sized,
|
||||
B: MessageBody,
|
||||
{
|
||||
type Error = B::Error;
|
||||
|
||||
@@ -137,10 +175,42 @@ mod foreign_impls {
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
self.get_mut().as_mut().poll_next(cx)
|
||||
self.as_mut().poll_next(cx)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_complete_body(&self) -> bool {
|
||||
self.as_ref().is_complete_body()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
debug_assert!(
|
||||
self.is_complete_body(),
|
||||
"inner type \"{}\" does not allow taking complete body; caller should make sure to \
|
||||
call `is_complete_body` first",
|
||||
std::any::type_name::<B>(),
|
||||
);
|
||||
|
||||
// we do not have DerefMut access to call take_complete_body directly but since
|
||||
// is_complete_body is true we should expect the entire bytes chunk in one poll_next
|
||||
|
||||
let waker = futures_util::task::noop_waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
|
||||
match self.as_mut().poll_next(&mut cx) {
|
||||
Poll::Ready(Some(Ok(data))) => data,
|
||||
_ => {
|
||||
panic!(
|
||||
"inner type \"{}\" indicated it allows taking complete body but failed to \
|
||||
return Bytes when polled",
|
||||
std::any::type_name::<B>()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,21 +222,25 @@ mod foreign_impls {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
mut self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(Bytes::from_static(mem::take(self.get_mut())))))
|
||||
Poll::Ready(Some(Ok(self.take_complete_body())))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::from_static(self))
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
Bytes::from_static(mem::take(self))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,21 +252,25 @@ mod foreign_impls {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
mut self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
|
||||
Poll::Ready(Some(Ok(self.take_complete_body())))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(self)
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
mem::take(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,21 +282,25 @@ mod foreign_impls {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
mut self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
|
||||
Poll::Ready(Some(Ok(self.take_complete_body())))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(self.freeze())
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
mem::take(self).freeze()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,21 +312,25 @@ mod foreign_impls {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
mut self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()).into())))
|
||||
Poll::Ready(Some(Ok(self.take_complete_body())))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::from(self))
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
Bytes::from(mem::take(self))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +342,6 @@ mod foreign_impls {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
@@ -271,8 +356,13 @@ mod foreign_impls {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::from_static(self.as_bytes()))
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
Bytes::from_static(mem::take(self).as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +374,6 @@ mod foreign_impls {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
@@ -298,8 +387,13 @@ mod foreign_impls {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::from(self))
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
Bytes::from(mem::take(self))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +405,6 @@ mod foreign_impls {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
@@ -321,8 +414,13 @@ mod foreign_impls {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(self.into_bytes())
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
mem::take(self).into_bytes()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,12 +475,6 @@ where
|
||||
None => Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
let Self { body, mapper } = self;
|
||||
body.try_into_bytes().map_err(|body| Self { body, mapper })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -392,7 +484,6 @@ mod tests {
|
||||
use bytes::{Bytes, BytesMut};
|
||||
|
||||
use super::*;
|
||||
use crate::body::{self, EitherBody};
|
||||
|
||||
macro_rules! assert_poll_next {
|
||||
($pin:expr, $exp:expr) => {
|
||||
@@ -494,45 +585,49 @@ mod tests {
|
||||
assert_poll_next!(pl, Bytes::from("test"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn complete_body_combinators() {
|
||||
let body = Bytes::from_static(b"test");
|
||||
let body = BoxBody::new(body);
|
||||
let body = EitherBody::<_, ()>::left(body);
|
||||
let body = EitherBody::<(), _>::right(body);
|
||||
// Do not support try_into_bytes:
|
||||
// let body = Box::new(body);
|
||||
// let body = Box::pin(body);
|
||||
#[test]
|
||||
fn take_string() {
|
||||
let mut data = "test".repeat(2);
|
||||
let data_bytes = Bytes::from(data.clone());
|
||||
assert!(data.is_complete_body());
|
||||
assert_eq!(data.take_complete_body(), data_bytes);
|
||||
|
||||
assert_eq!(body.try_into_bytes().unwrap(), Bytes::from("test"));
|
||||
let mut big_data = "test".repeat(64 * 1024);
|
||||
let data_bytes = Bytes::from(big_data.clone());
|
||||
assert!(big_data.is_complete_body());
|
||||
assert_eq!(big_data.take_complete_body(), data_bytes);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn complete_body_combinators_poll() {
|
||||
let body = Bytes::from_static(b"test");
|
||||
let body = BoxBody::new(body);
|
||||
let body = EitherBody::<_, ()>::left(body);
|
||||
let body = EitherBody::<(), _>::right(body);
|
||||
let mut body = body;
|
||||
#[test]
|
||||
fn take_boxed_equivalence() {
|
||||
let mut data = Bytes::from_static(b"test");
|
||||
assert!(data.is_complete_body());
|
||||
assert_eq!(data.take_complete_body(), b"test".as_ref());
|
||||
|
||||
assert_eq!(body.size(), BodySize::Sized(4));
|
||||
assert_poll_next!(Pin::new(&mut body), Bytes::from("test"));
|
||||
assert_poll_next_none!(Pin::new(&mut body));
|
||||
let mut data = Box::new(Bytes::from_static(b"test"));
|
||||
assert!(data.is_complete_body());
|
||||
assert_eq!(data.take_complete_body(), b"test".as_ref());
|
||||
|
||||
let mut data = Box::pin(Bytes::from_static(b"test"));
|
||||
assert!(data.is_complete_body());
|
||||
assert_eq!(data.take_complete_body(), b"test".as_ref());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn none_body_combinators() {
|
||||
fn none_body() -> BoxBody {
|
||||
let body = body::None;
|
||||
let body = BoxBody::new(body);
|
||||
let body = EitherBody::<_, ()>::left(body);
|
||||
let body = EitherBody::<(), _>::right(body);
|
||||
body.boxed()
|
||||
}
|
||||
#[test]
|
||||
fn take_policy() {
|
||||
let mut data = Bytes::from_static(b"test");
|
||||
// first call returns chunk
|
||||
assert_eq!(data.take_complete_body(), b"test".as_ref());
|
||||
// second call returns empty
|
||||
assert_eq!(data.take_complete_body(), b"".as_ref());
|
||||
|
||||
assert_eq!(none_body().size(), BodySize::None);
|
||||
assert_eq!(none_body().try_into_bytes().unwrap(), Bytes::new());
|
||||
assert_poll_next_none!(Pin::new(&mut none_body()));
|
||||
let waker = futures_util::task::noop_waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
let mut data = Bytes::from_static(b"test");
|
||||
// take returns whole chunk
|
||||
assert_eq!(data.take_complete_body(), b"test".as_ref());
|
||||
// subsequent poll_next returns None
|
||||
assert_eq!(Pin::new(&mut data).poll_next(&mut cx), Poll::Ready(None));
|
||||
}
|
||||
|
||||
// down-casting used to be done with a method on MessageBody trait
|
||||
|
@@ -42,7 +42,12 @@ impl MessageBody for None {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::new())
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
Bytes::new()
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ use zstd::stream::write::Encoder as ZstdEncoder;
|
||||
|
||||
use super::Writer;
|
||||
use crate::{
|
||||
body::{self, BodySize, MessageBody},
|
||||
body::{BodySize, MessageBody},
|
||||
error::BlockingError,
|
||||
header::{self, ContentEncoding, HeaderValue, CONTENT_ENCODING},
|
||||
ResponseHead, StatusCode,
|
||||
@@ -46,16 +46,14 @@ pin_project! {
|
||||
impl<B: MessageBody> Encoder<B> {
|
||||
fn none() -> Self {
|
||||
Encoder {
|
||||
body: EncoderBody::None {
|
||||
body: body::None::new(),
|
||||
},
|
||||
body: EncoderBody::None,
|
||||
encoder: None,
|
||||
fut: None,
|
||||
eof: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self {
|
||||
pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, mut body: B) -> Self {
|
||||
let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|
||||
|| head.status == StatusCode::SWITCHING_PROTOCOLS
|
||||
|| head.status == StatusCode::NO_CONTENT
|
||||
@@ -67,9 +65,11 @@ impl<B: MessageBody> Encoder<B> {
|
||||
return Self::none();
|
||||
}
|
||||
|
||||
let body = match body.try_into_bytes() {
|
||||
Ok(body) => EncoderBody::Full { body },
|
||||
Err(body) => EncoderBody::Stream { body },
|
||||
let body = if body.is_complete_body() {
|
||||
let body = body.take_complete_body();
|
||||
EncoderBody::Full { body }
|
||||
} else {
|
||||
EncoderBody::Stream { body }
|
||||
};
|
||||
|
||||
if can_encode {
|
||||
@@ -98,7 +98,7 @@ impl<B: MessageBody> Encoder<B> {
|
||||
pin_project! {
|
||||
#[project = EncoderBodyProj]
|
||||
enum EncoderBody<B> {
|
||||
None { body: body::None },
|
||||
None,
|
||||
Full { body: Bytes },
|
||||
Stream { #[pin] body: B },
|
||||
}
|
||||
@@ -110,10 +110,9 @@ where
|
||||
{
|
||||
type Error = EncoderError;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
match self {
|
||||
EncoderBody::None { body } => body.size(),
|
||||
EncoderBody::None => BodySize::None,
|
||||
EncoderBody::Full { body } => body.size(),
|
||||
EncoderBody::Stream { body } => body.size(),
|
||||
}
|
||||
@@ -124,9 +123,7 @@ where
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
match self.project() {
|
||||
EncoderBodyProj::None { body } => {
|
||||
Pin::new(body).poll_next(cx).map_err(|err| match err {})
|
||||
}
|
||||
EncoderBodyProj::None => Poll::Ready(None),
|
||||
EncoderBodyProj::Full { body } => {
|
||||
Pin::new(body).poll_next(cx).map_err(|err| match err {})
|
||||
}
|
||||
@@ -136,15 +133,21 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn is_complete_body(&self) -> bool {
|
||||
match self {
|
||||
EncoderBody::None { body } => Ok(body.try_into_bytes().unwrap()),
|
||||
EncoderBody::Full { body } => Ok(body.try_into_bytes().unwrap()),
|
||||
_ => Err(self),
|
||||
EncoderBody::None => true,
|
||||
EncoderBody::Full { .. } => true,
|
||||
EncoderBody::Stream { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
match self {
|
||||
EncoderBody::None => Bytes::new(),
|
||||
EncoderBody::Full { body } => body.take_complete_body(),
|
||||
EncoderBody::Stream { .. } => {
|
||||
panic!("EncoderBody::Stream variant cannot be taken")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +158,6 @@ where
|
||||
{
|
||||
type Error = EncoderError;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
if self.encoder.is_some() {
|
||||
BodySize::Stream
|
||||
@@ -232,21 +234,19 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(mut self) -> Result<Bytes, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn is_complete_body(&self) -> bool {
|
||||
if self.encoder.is_some() {
|
||||
Err(self)
|
||||
false
|
||||
} else {
|
||||
match self.body.try_into_bytes() {
|
||||
Ok(body) => Ok(body),
|
||||
Err(body) => {
|
||||
self.body = body;
|
||||
Err(self)
|
||||
}
|
||||
}
|
||||
self.body.is_complete_body()
|
||||
}
|
||||
}
|
||||
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
if self.encoder.is_some() {
|
||||
panic!("compressed body stream cannot be taken")
|
||||
} else {
|
||||
self.body.take_complete_body()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -332,28 +332,31 @@ impl From<PayloadError> for Error {
|
||||
}
|
||||
|
||||
/// A set of errors that can occur during dispatching HTTP requests.
|
||||
#[derive(Debug, Display, From)]
|
||||
#[derive(Debug, Display, Error, From)]
|
||||
#[non_exhaustive]
|
||||
pub enum DispatchError {
|
||||
/// Service error.
|
||||
/// Service error
|
||||
// FIXME: display and error type
|
||||
#[display(fmt = "Service Error")]
|
||||
Service(Response<BoxBody>),
|
||||
Service(#[error(not(source))] Response<BoxBody>),
|
||||
|
||||
/// Body streaming error.
|
||||
#[display(fmt = "Body error: {}", _0)]
|
||||
Body(Box<dyn StdError>),
|
||||
/// Body error
|
||||
// FIXME: display and error type
|
||||
#[display(fmt = "Body Error")]
|
||||
Body(#[error(not(source))] Box<dyn StdError>),
|
||||
|
||||
/// Upgrade service error.
|
||||
/// Upgrade service error
|
||||
Upgrade,
|
||||
|
||||
/// An `io::Error` that occurred while trying to read or write to a network stream.
|
||||
#[display(fmt = "IO error: {}", _0)]
|
||||
Io(io::Error),
|
||||
|
||||
/// Request parse error.
|
||||
#[display(fmt = "Request parse error: {}", _0)]
|
||||
/// Http request parse error.
|
||||
#[display(fmt = "Parse error: {}", _0)]
|
||||
Parse(ParseError),
|
||||
|
||||
/// HTTP/2 error.
|
||||
/// Http/2 error
|
||||
#[display(fmt = "{}", _0)]
|
||||
H2(h2::Error),
|
||||
|
||||
@@ -365,23 +368,21 @@ pub enum DispatchError {
|
||||
#[display(fmt = "Connection shutdown timeout")]
|
||||
DisconnectTimeout,
|
||||
|
||||
/// Internal error.
|
||||
/// Payload is not consumed
|
||||
#[display(fmt = "Task is completed but request's payload is not consumed")]
|
||||
PayloadIsNotConsumed,
|
||||
|
||||
/// Malformed request
|
||||
#[display(fmt = "Malformed request")]
|
||||
MalformedRequest,
|
||||
|
||||
/// Internal error
|
||||
#[display(fmt = "Internal error")]
|
||||
InternalError,
|
||||
}
|
||||
|
||||
impl StdError for DispatchError {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match self {
|
||||
// TODO: error source extraction?
|
||||
DispatchError::Service(_res) => None,
|
||||
DispatchError::Body(err) => Some(&**err),
|
||||
DispatchError::Io(err) => Some(err),
|
||||
DispatchError::Parse(err) => Some(err),
|
||||
DispatchError::H2(err) => Some(err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// Unknown error
|
||||
#[display(fmt = "Unknown error")]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// A set of error that can occur during parsing content type.
|
||||
|
@@ -15,14 +15,14 @@ use bitflags::bitflags;
|
||||
use bytes::{Buf, BytesMut};
|
||||
use futures_core::ready;
|
||||
use log::{error, trace};
|
||||
use pin_project_lite::pin_project;
|
||||
use pin_project::pin_project;
|
||||
|
||||
use crate::{
|
||||
body::{BodySize, BoxBody, MessageBody},
|
||||
config::ServiceConfig,
|
||||
error::{DispatchError, ParseError, PayloadError},
|
||||
service::HttpFlow,
|
||||
Error, Extensions, OnConnectData, Request, Response, StatusCode,
|
||||
Extensions, OnConnectData, Request, Response, StatusCode,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -46,111 +46,79 @@ bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
// there's 2 versions of Dispatcher state because of:
|
||||
// https://github.com/taiki-e/pin-project-lite/issues/3
|
||||
//
|
||||
// tl;dr: pin-project-lite doesn't play well with other attribute macros
|
||||
#[pin_project]
|
||||
/// Dispatcher for HTTP/1.1 protocol
|
||||
pub struct Dispatcher<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
|
||||
#[cfg(not(test))]
|
||||
pin_project! {
|
||||
/// Dispatcher for HTTP/1.1 protocol
|
||||
pub struct Dispatcher<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
B: MessageBody,
|
||||
|
||||
B: MessageBody,
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
#[pin]
|
||||
inner: DispatcherState<T, S, B, X, U>,
|
||||
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
#[pin]
|
||||
inner: DispatcherState<T, S, B, X, U>,
|
||||
}
|
||||
#[cfg(test)]
|
||||
poll_count: u64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pin_project! {
|
||||
/// Dispatcher for HTTP/1.1 protocol
|
||||
pub struct Dispatcher<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
#[pin_project(project = DispatcherStateProj)]
|
||||
enum DispatcherState<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
|
||||
B: MessageBody,
|
||||
B: MessageBody,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
#[pin]
|
||||
inner: DispatcherState<T, S, B, X, U>,
|
||||
|
||||
// used in tests
|
||||
poll_count: u64,
|
||||
}
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
Normal(#[pin] InnerDispatcher<T, S, B, X, U>),
|
||||
Upgrade(#[pin] U::Future),
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
#[project = DispatcherStateProj]
|
||||
enum DispatcherState<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
#[pin_project(project = InnerDispatcherProj)]
|
||||
struct InnerDispatcher<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
|
||||
B: MessageBody,
|
||||
B: MessageBody,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
Normal { #[pin] inner: InnerDispatcher<T, S, B, X, U> },
|
||||
Upgrade { #[pin] fut: U::Future },
|
||||
}
|
||||
}
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
flow: Rc<HttpFlow<S, X, U>>,
|
||||
flags: Flags,
|
||||
peer_addr: Option<net::SocketAddr>,
|
||||
conn_data: Option<Rc<Extensions>>,
|
||||
error: Option<DispatchError>,
|
||||
|
||||
pin_project! {
|
||||
#[project = InnerDispatcherProj]
|
||||
struct InnerDispatcher<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
#[pin]
|
||||
state: State<S, B, X>,
|
||||
payload: Option<PayloadSender>,
|
||||
messages: VecDeque<DispatcherMessage>,
|
||||
|
||||
B: MessageBody,
|
||||
ka_expire: Instant,
|
||||
#[pin]
|
||||
ka_timer: Option<Sleep>,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
flow: Rc<HttpFlow<S, X, U>>,
|
||||
flags: Flags,
|
||||
peer_addr: Option<net::SocketAddr>,
|
||||
conn_data: Option<Rc<Extensions>>,
|
||||
error: Option<DispatchError>,
|
||||
|
||||
#[pin]
|
||||
state: State<S, B, X>,
|
||||
payload: Option<PayloadSender>,
|
||||
messages: VecDeque<DispatcherMessage>,
|
||||
|
||||
ka_expire: Instant,
|
||||
#[pin]
|
||||
ka_timer: Option<Sleep>,
|
||||
|
||||
io: Option<T>,
|
||||
read_buf: BytesMut,
|
||||
write_buf: BytesMut,
|
||||
codec: Codec,
|
||||
}
|
||||
io: Option<T>,
|
||||
read_buf: BytesMut,
|
||||
write_buf: BytesMut,
|
||||
codec: Codec,
|
||||
}
|
||||
|
||||
enum DispatcherMessage {
|
||||
@@ -159,21 +127,19 @@ enum DispatcherMessage {
|
||||
Error(Response<()>),
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
#[project = StateProj]
|
||||
enum State<S, B, X>
|
||||
where
|
||||
S: Service<Request>,
|
||||
X: Service<Request, Response = Request>,
|
||||
#[pin_project(project = StateProj)]
|
||||
enum State<S, B, X>
|
||||
where
|
||||
S: Service<Request>,
|
||||
X: Service<Request, Response = Request>,
|
||||
|
||||
B: MessageBody,
|
||||
{
|
||||
None,
|
||||
ExpectCall { #[pin] fut: X::Future },
|
||||
ServiceCall { #[pin] fut: S::Future },
|
||||
SendPayload { #[pin] body: B },
|
||||
SendErrorPayload { #[pin] body: BoxBody },
|
||||
}
|
||||
B: MessageBody,
|
||||
{
|
||||
None,
|
||||
ExpectCall(#[pin] X::Future),
|
||||
ServiceCall(#[pin] S::Future),
|
||||
SendPayload(#[pin] B),
|
||||
SendErrorPayload(#[pin] BoxBody),
|
||||
}
|
||||
|
||||
impl<S, B, X> State<S, B, X>
|
||||
@@ -232,27 +198,25 @@ where
|
||||
};
|
||||
|
||||
Dispatcher {
|
||||
inner: DispatcherState::Normal {
|
||||
inner: InnerDispatcher {
|
||||
flow,
|
||||
flags,
|
||||
peer_addr,
|
||||
conn_data: conn_data.0.map(Rc::new),
|
||||
error: None,
|
||||
inner: DispatcherState::Normal(InnerDispatcher {
|
||||
flow,
|
||||
flags,
|
||||
peer_addr,
|
||||
conn_data: conn_data.0.map(Rc::new),
|
||||
error: None,
|
||||
|
||||
state: State::None,
|
||||
payload: None,
|
||||
messages: VecDeque::new(),
|
||||
state: State::None,
|
||||
payload: None,
|
||||
messages: VecDeque::new(),
|
||||
|
||||
ka_expire,
|
||||
ka_timer,
|
||||
ka_expire,
|
||||
ka_timer,
|
||||
|
||||
io: Some(io),
|
||||
read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
|
||||
write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
|
||||
codec: Codec::new(config),
|
||||
},
|
||||
},
|
||||
io: Some(io),
|
||||
read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
|
||||
write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
|
||||
codec: Codec::new(config),
|
||||
}),
|
||||
|
||||
#[cfg(test)]
|
||||
poll_count: 0,
|
||||
@@ -352,7 +316,7 @@ where
|
||||
let size = self.as_mut().send_response_inner(message, &body)?;
|
||||
let state = match size {
|
||||
BodySize::None | BodySize::Sized(0) => State::None,
|
||||
_ => State::SendPayload { body },
|
||||
_ => State::SendPayload(body),
|
||||
};
|
||||
self.project().state.set(state);
|
||||
Ok(())
|
||||
@@ -366,7 +330,7 @@ where
|
||||
let size = self.as_mut().send_response_inner(message, &body)?;
|
||||
let state = match size {
|
||||
BodySize::None | BodySize::Sized(0) => State::None,
|
||||
_ => State::SendErrorPayload { body },
|
||||
_ => State::SendErrorPayload(body),
|
||||
};
|
||||
self.project().state.set(state);
|
||||
Ok(())
|
||||
@@ -392,12 +356,12 @@ where
|
||||
// Handle `EXPECT: 100-Continue` header
|
||||
if req.head().expect() {
|
||||
// set InnerDispatcher state and continue loop to poll it.
|
||||
let fut = this.flow.expect.call(req);
|
||||
this.state.set(State::ExpectCall { fut });
|
||||
let task = this.flow.expect.call(req);
|
||||
this.state.set(State::ExpectCall(task));
|
||||
} else {
|
||||
// the same as expect call.
|
||||
let fut = this.flow.service.call(req);
|
||||
this.state.set(State::ServiceCall { fut });
|
||||
let task = this.flow.service.call(req);
|
||||
this.state.set(State::ServiceCall(task));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -417,7 +381,7 @@ where
|
||||
// all messages are dealt with.
|
||||
None => return Ok(PollResponse::DoNothing),
|
||||
},
|
||||
StateProj::ServiceCall { fut } => match fut.poll(cx) {
|
||||
StateProj::ServiceCall(fut) => match fut.poll(cx) {
|
||||
// service call resolved. send response.
|
||||
Poll::Ready(Ok(res)) => {
|
||||
let (res, body) = res.into().replace_body(());
|
||||
@@ -443,11 +407,11 @@ where
|
||||
}
|
||||
},
|
||||
|
||||
StateProj::SendPayload { mut body } => {
|
||||
StateProj::SendPayload(mut stream) => {
|
||||
// keep populate writer buffer until buffer size limit hit,
|
||||
// get blocked or finished.
|
||||
while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE {
|
||||
match body.as_mut().poll_next(cx) {
|
||||
match stream.as_mut().poll_next(cx) {
|
||||
Poll::Ready(Some(Ok(item))) => {
|
||||
this.codec
|
||||
.encode(Message::Chunk(Some(item)), this.write_buf)?;
|
||||
@@ -473,13 +437,13 @@ where
|
||||
return Ok(PollResponse::DrainWriteBuf);
|
||||
}
|
||||
|
||||
StateProj::SendErrorPayload { mut body } => {
|
||||
StateProj::SendErrorPayload(mut stream) => {
|
||||
// TODO: de-dupe impl with SendPayload
|
||||
|
||||
// keep populate writer buffer until buffer size limit hit,
|
||||
// get blocked or finished.
|
||||
while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE {
|
||||
match body.as_mut().poll_next(cx) {
|
||||
match stream.as_mut().poll_next(cx) {
|
||||
Poll::Ready(Some(Ok(item))) => {
|
||||
this.codec
|
||||
.encode(Message::Chunk(Some(item)), this.write_buf)?;
|
||||
@@ -494,9 +458,7 @@ where
|
||||
}
|
||||
|
||||
Poll::Ready(Some(Err(err))) => {
|
||||
return Err(DispatchError::Body(
|
||||
Error::new_body().with_cause(err).into(),
|
||||
))
|
||||
return Err(DispatchError::Service(err.into()))
|
||||
}
|
||||
|
||||
Poll::Pending => return Ok(PollResponse::DoNothing),
|
||||
@@ -507,14 +469,14 @@ where
|
||||
return Ok(PollResponse::DrainWriteBuf);
|
||||
}
|
||||
|
||||
StateProj::ExpectCall { fut } => match fut.poll(cx) {
|
||||
StateProj::ExpectCall(fut) => match fut.poll(cx) {
|
||||
// expect resolved. write continue to buffer and set InnerDispatcher state
|
||||
// to service call.
|
||||
Poll::Ready(Ok(req)) => {
|
||||
this.write_buf
|
||||
.extend_from_slice(b"HTTP/1.1 100 Continue\r\n\r\n");
|
||||
let fut = this.flow.service.call(req);
|
||||
this.state.set(State::ServiceCall { fut });
|
||||
this.state.set(State::ServiceCall(fut));
|
||||
}
|
||||
|
||||
// send expect error as response
|
||||
@@ -540,25 +502,25 @@ where
|
||||
let mut this = self.as_mut().project();
|
||||
if req.head().expect() {
|
||||
// set dispatcher state so the future is pinned.
|
||||
let fut = this.flow.expect.call(req);
|
||||
this.state.set(State::ExpectCall { fut });
|
||||
let task = this.flow.expect.call(req);
|
||||
this.state.set(State::ExpectCall(task));
|
||||
} else {
|
||||
// the same as above.
|
||||
let fut = this.flow.service.call(req);
|
||||
this.state.set(State::ServiceCall { fut });
|
||||
let task = this.flow.service.call(req);
|
||||
this.state.set(State::ServiceCall(task));
|
||||
};
|
||||
|
||||
// eagerly poll the future for once(or twice if expect is resolved immediately).
|
||||
loop {
|
||||
match self.as_mut().project().state.project() {
|
||||
StateProj::ExpectCall { fut } => {
|
||||
StateProj::ExpectCall(fut) => {
|
||||
match fut.poll(cx) {
|
||||
// expect is resolved. continue loop and poll the service call branch.
|
||||
Poll::Ready(Ok(req)) => {
|
||||
self.as_mut().send_continue();
|
||||
let mut this = self.as_mut().project();
|
||||
let fut = this.flow.service.call(req);
|
||||
this.state.set(State::ServiceCall { fut });
|
||||
let task = this.flow.service.call(req);
|
||||
this.state.set(State::ServiceCall(task));
|
||||
continue;
|
||||
}
|
||||
// future is pending. return Ok(()) to notify that a new state is
|
||||
@@ -574,7 +536,7 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
StateProj::ServiceCall { fut } => {
|
||||
StateProj::ServiceCall(fut) => {
|
||||
// return no matter the service call future's result.
|
||||
return match fut.poll(cx) {
|
||||
// future is resolved. send response and return a result. On success
|
||||
@@ -939,7 +901,7 @@ where
|
||||
}
|
||||
|
||||
match this.inner.project() {
|
||||
DispatcherStateProj::Normal { mut inner } => {
|
||||
DispatcherStateProj::Normal(mut inner) => {
|
||||
inner.as_mut().poll_keepalive(cx)?;
|
||||
|
||||
if inner.flags.contains(Flags::SHUTDOWN) {
|
||||
@@ -979,7 +941,7 @@ where
|
||||
self.as_mut()
|
||||
.project()
|
||||
.inner
|
||||
.set(DispatcherState::Upgrade { fut: upgrade });
|
||||
.set(DispatcherState::Upgrade(upgrade));
|
||||
return self.poll(cx);
|
||||
}
|
||||
};
|
||||
@@ -1031,8 +993,8 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
DispatcherStateProj::Upgrade { fut: upgrade } => upgrade.poll(cx).map_err(|err| {
|
||||
error!("Upgrade handler error: {}", err);
|
||||
DispatcherStateProj::Upgrade(fut) => fut.poll(cx).map_err(|e| {
|
||||
error!("Upgrade handler error: {}", e);
|
||||
DispatchError::Upgrade
|
||||
}),
|
||||
}
|
||||
@@ -1126,7 +1088,7 @@ mod tests {
|
||||
Poll::Ready(res) => assert!(res.is_err()),
|
||||
}
|
||||
|
||||
if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() {
|
||||
if let DispatcherStateProj::Normal(inner) = h1.project().inner.project() {
|
||||
assert!(inner.flags.contains(Flags::READ_DISCONNECT));
|
||||
assert_eq!(
|
||||
&inner.project().io.take().unwrap().write_buf[..26],
|
||||
@@ -1161,7 +1123,7 @@ mod tests {
|
||||
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
|
||||
|
||||
match h1.as_mut().poll(cx) {
|
||||
Poll::Pending => panic!("first poll should not be pending"),
|
||||
@@ -1171,7 +1133,7 @@ mod tests {
|
||||
// polls: initial => shutdown
|
||||
assert_eq!(h1.poll_count, 2);
|
||||
|
||||
if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() {
|
||||
if let DispatcherStateProj::Normal(inner) = h1.project().inner.project() {
|
||||
let res = &mut inner.project().io.take().unwrap().write_buf[..];
|
||||
stabilize_date_header(res);
|
||||
|
||||
@@ -1215,7 +1177,7 @@ mod tests {
|
||||
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
|
||||
|
||||
match h1.as_mut().poll(cx) {
|
||||
Poll::Pending => panic!("first poll should not be pending"),
|
||||
@@ -1225,7 +1187,7 @@ mod tests {
|
||||
// polls: initial => shutdown
|
||||
assert_eq!(h1.poll_count, 1);
|
||||
|
||||
if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() {
|
||||
if let DispatcherStateProj::Normal(inner) = h1.project().inner.project() {
|
||||
let res = &mut inner.project().io.take().unwrap().write_buf[..];
|
||||
stabilize_date_header(res);
|
||||
|
||||
@@ -1275,13 +1237,13 @@ mod tests {
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
assert!(h1.as_mut().poll(cx).is_pending());
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
|
||||
|
||||
// polls: manual
|
||||
assert_eq!(h1.poll_count, 1);
|
||||
eprintln!("poll count: {}", h1.poll_count);
|
||||
|
||||
if let DispatcherState::Normal { ref inner } = h1.inner {
|
||||
if let DispatcherState::Normal(ref inner) = h1.inner {
|
||||
let io = inner.io.as_ref().unwrap();
|
||||
let res = &io.write_buf()[..];
|
||||
assert_eq!(
|
||||
@@ -1296,7 +1258,7 @@ mod tests {
|
||||
// polls: manual manual shutdown
|
||||
assert_eq!(h1.poll_count, 3);
|
||||
|
||||
if let DispatcherState::Normal { ref inner } = h1.inner {
|
||||
if let DispatcherState::Normal(ref inner) = h1.inner {
|
||||
let io = inner.io.as_ref().unwrap();
|
||||
let mut res = (&io.write_buf()[..]).to_owned();
|
||||
stabilize_date_header(&mut res);
|
||||
@@ -1347,12 +1309,12 @@ mod tests {
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
assert!(h1.as_mut().poll(cx).is_ready());
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
|
||||
|
||||
// polls: manual shutdown
|
||||
assert_eq!(h1.poll_count, 2);
|
||||
|
||||
if let DispatcherState::Normal { ref inner } = h1.inner {
|
||||
if let DispatcherState::Normal(ref inner) = h1.inner {
|
||||
let io = inner.io.as_ref().unwrap();
|
||||
let mut res = (&io.write_buf()[..]).to_owned();
|
||||
stabilize_date_header(&mut res);
|
||||
@@ -1424,7 +1386,7 @@ mod tests {
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
assert!(h1.as_mut().poll(cx).is_ready());
|
||||
assert!(matches!(&h1.inner, DispatcherState::Upgrade { .. }));
|
||||
assert!(matches!(&h1.inner, DispatcherState::Upgrade(_)));
|
||||
|
||||
// polls: manual shutdown
|
||||
assert_eq!(h1.poll_count, 2);
|
||||
|
@@ -356,9 +356,9 @@ where
|
||||
type Future = Dispatcher<T, S, B, X, U>;
|
||||
|
||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self._poll_ready(cx).map_err(|err| {
|
||||
log::error!("HTTP/1 service readiness error: {:?}", err);
|
||||
DispatchError::Service(err)
|
||||
self._poll_ready(cx).map_err(|e| {
|
||||
log::error!("HTTP/1 service readiness error: {:?}", e);
|
||||
DispatchError::Service(e)
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -170,7 +170,7 @@ impl<B> Response<B> {
|
||||
/// Returns split head and body.
|
||||
///
|
||||
/// # Implementation Notes
|
||||
/// Due to internal performance optimizations, the first element of the returned tuple is a
|
||||
/// Due to internal performance optimisations, the first element of the returned tuple is a
|
||||
/// `Response` as well but only contains the head of the response this was called on.
|
||||
pub fn into_parts(self) -> (Response<()>, B) {
|
||||
self.replace_body(())
|
||||
@@ -194,7 +194,7 @@ impl<B> Response<B> {
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
self.map_body(|_, body| body.boxed())
|
||||
self.map_body(|_, body| BoxBody::new(body))
|
||||
}
|
||||
|
||||
/// Returns body, consuming this response.
|
||||
|
@@ -493,9 +493,9 @@ where
|
||||
type Future = HttpServiceHandlerResponse<T, S, B, X, U>;
|
||||
|
||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self._poll_ready(cx).map_err(|err| {
|
||||
log::error!("HTTP service readiness error: {:?}", err);
|
||||
DispatchError::Service(err)
|
||||
self._poll_ready(cx).map_err(|e| {
|
||||
log::error!("HTTP service readiness error: {:?}", e);
|
||||
DispatchError::Service(e)
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -264,7 +264,7 @@ impl TestSeqBuffer {
|
||||
|
||||
/// Create new empty `TestBuffer` instance.
|
||||
pub fn empty() -> Self {
|
||||
Self::new(BytesMut::new())
|
||||
Self::new("")
|
||||
}
|
||||
|
||||
pub fn read_buf(&self) -> Ref<'_, BytesMut> {
|
||||
|
@@ -15,7 +15,7 @@ path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = { version = "4.0.0-beta.15", default-features = false }
|
||||
actix-web = { version = "4.0.0-beta.14", default-features = false }
|
||||
|
||||
bytes = "1"
|
||||
derive_more = "0.99.5"
|
||||
@@ -28,7 +28,7 @@ twoway = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-http = "3.0.0-beta.16"
|
||||
actix-http = "3.0.0-beta.15"
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
tokio = { version = "1", features = ["sync"] }
|
||||
tokio-stream = "0.1"
|
||||
|
@@ -1,9 +1,6 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.5.0-beta.3 - 2021-12-17
|
||||
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-router"
|
||||
version = "0.5.0-beta.3"
|
||||
version = "0.5.0-beta.2"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",
|
||||
|
@@ -2,28 +2,22 @@ use crate::ResourcePath;
|
||||
|
||||
#[allow(dead_code)]
|
||||
const GEN_DELIMS: &[u8] = b":/?#[]@";
|
||||
|
||||
#[allow(dead_code)]
|
||||
const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,";
|
||||
|
||||
#[allow(dead_code)]
|
||||
const SUB_DELIMS: &[u8] = b"!$'()*,+?=;";
|
||||
|
||||
#[allow(dead_code)]
|
||||
const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;";
|
||||
|
||||
#[allow(dead_code)]
|
||||
const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
1234567890
|
||||
-._~";
|
||||
|
||||
const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
1234567890
|
||||
-._~
|
||||
!$'()*,";
|
||||
|
||||
const QS: &[u8] = b"+&=;b";
|
||||
|
||||
#[inline]
|
||||
@@ -40,20 +34,19 @@ thread_local! {
|
||||
static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+");
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct Url {
|
||||
uri: http::Uri,
|
||||
path: Option<String>,
|
||||
}
|
||||
|
||||
impl Url {
|
||||
#[inline]
|
||||
pub fn new(uri: http::Uri) -> Url {
|
||||
let path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes()));
|
||||
|
||||
Url { uri, path }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn with_quoter(uri: http::Uri, quoter: &Quoter) -> Url {
|
||||
Url {
|
||||
path: quoter.requote(uri.path().as_bytes()),
|
||||
@@ -61,16 +54,15 @@ impl Url {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn uri(&self) -> &http::Uri {
|
||||
&self.uri
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn path(&self) -> &str {
|
||||
match self.path {
|
||||
Some(ref path) => path,
|
||||
_ => self.uri.path(),
|
||||
if let Some(ref s) = self.path {
|
||||
s
|
||||
} else {
|
||||
self.uri.path()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +86,6 @@ impl ResourcePath for Url {
|
||||
}
|
||||
}
|
||||
|
||||
/// A quoter
|
||||
pub struct Quoter {
|
||||
safe_table: [u8; 16],
|
||||
protected_table: [u8; 16],
|
||||
@@ -102,7 +93,7 @@ pub struct Quoter {
|
||||
|
||||
impl Quoter {
|
||||
pub fn new(safe: &[u8], protected: &[u8]) -> Quoter {
|
||||
let mut quoter = Quoter {
|
||||
let mut q = Quoter {
|
||||
safe_table: [0; 16],
|
||||
protected_table: [0; 16],
|
||||
};
|
||||
@@ -110,24 +101,24 @@ impl Quoter {
|
||||
// prepare safe table
|
||||
for i in 0..128 {
|
||||
if ALLOWED.contains(&i) {
|
||||
set_bit(&mut quoter.safe_table, i);
|
||||
set_bit(&mut q.safe_table, i);
|
||||
}
|
||||
if QS.contains(&i) {
|
||||
set_bit(&mut quoter.safe_table, i);
|
||||
set_bit(&mut q.safe_table, i);
|
||||
}
|
||||
}
|
||||
|
||||
for ch in safe {
|
||||
set_bit(&mut quoter.safe_table, *ch)
|
||||
set_bit(&mut q.safe_table, *ch)
|
||||
}
|
||||
|
||||
// prepare protected table
|
||||
for ch in protected {
|
||||
set_bit(&mut quoter.safe_table, *ch);
|
||||
set_bit(&mut quoter.protected_table, *ch);
|
||||
set_bit(&mut q.safe_table, *ch);
|
||||
set_bit(&mut q.protected_table, *ch);
|
||||
}
|
||||
|
||||
quoter
|
||||
q
|
||||
}
|
||||
|
||||
pub fn requote(&self, val: &[u8]) -> Option<String> {
|
||||
@@ -224,7 +215,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_url() {
|
||||
fn test_parse_url() {
|
||||
let re = "/user/{id}/test";
|
||||
|
||||
let path = match_url(re, "/user/2345/test");
|
||||
@@ -240,24 +231,24 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protected_chars() {
|
||||
fn test_protected_chars() {
|
||||
let encoded = percent_encode(PROTECTED);
|
||||
let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded));
|
||||
assert_eq!(path.get("id").unwrap(), &encoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_protected_ascii() {
|
||||
let non_protected_ascii = ('\u{0}'..='\u{7F}')
|
||||
fn test_non_protecteed_ascii() {
|
||||
let nonprotected_ascii = ('\u{0}'..='\u{7F}')
|
||||
.filter(|&c| c.is_ascii() && !PROTECTED.contains(&(c as u8)))
|
||||
.collect::<String>();
|
||||
let encoded = percent_encode(non_protected_ascii.as_bytes());
|
||||
let encoded = percent_encode(nonprotected_ascii.as_bytes());
|
||||
let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded));
|
||||
assert_eq!(path.get("id").unwrap(), &non_protected_ascii);
|
||||
assert_eq!(path.get("id").unwrap(), &nonprotected_ascii);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_utf8_multibyte() {
|
||||
fn test_valid_utf8_multibyte() {
|
||||
let test = ('\u{FF00}'..='\u{FFFF}').collect::<String>();
|
||||
let encoded = percent_encode(test.as_bytes());
|
||||
let path = match_url("/a/{id}/b", format!("/a/{}/b", &encoded));
|
||||
@@ -265,7 +256,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_utf8() {
|
||||
fn test_invalid_utf8() {
|
||||
let invalid_utf8 = percent_encode((0x80..=0xff).collect::<Vec<_>>().as_slice());
|
||||
let uri = Uri::try_from(format!("/{}", invalid_utf8)).unwrap();
|
||||
let path = Path::new(Url::new(uri));
|
||||
@@ -275,7 +266,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_encoding() {
|
||||
fn test_from_hex() {
|
||||
let hex = b"0123456789abcdefABCDEF";
|
||||
|
||||
for i in 0..256 {
|
||||
|
@@ -3,13 +3,6 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.1.0-beta.9 - 2021-12-17
|
||||
* Re-export `actix_http::body::to_bytes`. [#2518]
|
||||
* Update `actix_web::test` re-exports. [#2518]
|
||||
|
||||
[#2518]: https://github.com/actix/actix-web/pull/2518
|
||||
|
||||
|
||||
## 0.1.0-beta.8 - 2021-12-11
|
||||
* No significant changes since `0.1.0-beta.7`.
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-test"
|
||||
version = "0.1.0-beta.9"
|
||||
version = "0.1.0-beta.8"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
@@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.4.1"
|
||||
actix-http = "3.0.0-beta.16"
|
||||
actix-http = "3.0.0-beta.15"
|
||||
actix-http-test = "3.0.0-beta.9"
|
||||
actix-rt = "2.1"
|
||||
actix-service = "2.0.0"
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = { version = "4.0.0-beta.15", default-features = false, features = ["cookies"] }
|
||||
awc = { version = "3.0.0-beta.14", default-features = false, features = ["cookies"] }
|
||||
actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["cookies"] }
|
||||
awc = { version = "3.0.0-beta.13", default-features = false, features = ["cookies"] }
|
||||
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = [] }
|
||||
|
@@ -37,14 +37,9 @@ extern crate tls_rustls as rustls;
|
||||
use std::{fmt, net, thread, time::Duration};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
pub use actix_http::{body::to_bytes, test::TestBuffer};
|
||||
pub use actix_http::test::TestBuffer;
|
||||
use actix_http::{header::HeaderMap, ws, HttpService, Method, Request, Response};
|
||||
pub use actix_http_test::unused_addr;
|
||||
use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _};
|
||||
pub use actix_web::test::{
|
||||
call_and_read_body, call_and_read_body_json, call_service, init_service, ok_service,
|
||||
read_body, read_body_json, simple_service, TestRequest,
|
||||
};
|
||||
use actix_web::{
|
||||
body::MessageBody,
|
||||
dev::{AppConfig, Server, ServerHandle, Service},
|
||||
@@ -53,6 +48,12 @@ use actix_web::{
|
||||
};
|
||||
use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector};
|
||||
use futures_core::Stream;
|
||||
|
||||
pub use actix_http_test::unused_addr;
|
||||
pub use actix_web::test::{
|
||||
call_service, default_service, init_service, load_stream, ok_service, read_body,
|
||||
read_body_json, read_response, read_response_json, TestRequest,
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Start default [`TestServer`].
|
||||
|
@@ -16,8 +16,8 @@ path = "src/lib.rs"
|
||||
[dependencies]
|
||||
actix = { version = "0.12.0", default-features = false }
|
||||
actix-codec = "0.4.1"
|
||||
actix-http = "3.0.0-beta.16"
|
||||
actix-web = { version = "4.0.0-beta.15", default-features = false }
|
||||
actix-http = "3.0.0-beta.15"
|
||||
actix-web = { version = "4.0.0-beta.14", default-features = false }
|
||||
|
||||
bytes = "1"
|
||||
bytestring = "1"
|
||||
@@ -27,8 +27,8 @@ tokio = { version = "1", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-test = "0.1.0-beta.9"
|
||||
awc = { version = "3.0.0-beta.14", default-features = false }
|
||||
actix-test = "0.1.0-beta.8"
|
||||
awc = { version = "3.0.0-beta.13", default-features = false }
|
||||
|
||||
env_logger = "0.9"
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
|
@@ -18,14 +18,14 @@ proc-macro = true
|
||||
quote = "1"
|
||||
syn = { version = "1", features = ["full", "parsing"] }
|
||||
proc-macro2 = "1"
|
||||
actix-router = "0.5.0-beta.3"
|
||||
actix-router = "0.5.0-beta.2"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-macros = "0.2.3"
|
||||
actix-rt = "2.2"
|
||||
actix-test = "0.1.0-beta.9"
|
||||
actix-test = "0.1.0-beta.8"
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = "4.0.0-beta.15"
|
||||
actix-web = "4.0.0-beta.14"
|
||||
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
trybuild = "1"
|
||||
|
@@ -1,9 +1,6 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-beta.14 - 2021-12-17
|
||||
* Add `ClientBuilder::add_default_header` and deprecate `ClientBuilder::header`. [#2510]
|
||||
|
||||
[#2510]: https://github.com/actix/actix-web/pull/2510
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "awc"
|
||||
version = "3.0.0-beta.14"
|
||||
version = "3.0.0-beta.13"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"fakeshadow <24548779@qq.com>",
|
||||
@@ -60,7 +60,7 @@ dangerous-h2c = []
|
||||
[dependencies]
|
||||
actix-codec = "0.4.1"
|
||||
actix-service = "2.0.0"
|
||||
actix-http = "3.0.0-beta.16"
|
||||
actix-http = "3.0.0-beta.15"
|
||||
actix-rt = { version = "2.1", default-features = false }
|
||||
actix-tls = { version = "3.0.0-rc.1", features = ["connect", "uri"] }
|
||||
actix-utils = "3.0.0"
|
||||
@@ -70,8 +70,8 @@ base64 = "0.13"
|
||||
bytes = "1"
|
||||
cfg-if = "1"
|
||||
derive_more = "0.99.5"
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
h2 = "0.3.9"
|
||||
http = "0.2.5"
|
||||
itoa = "0.4"
|
||||
@@ -93,13 +93,13 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features
|
||||
trust-dns-resolver = { version = "0.20.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-http = { version = "3.0.0-beta.16", features = ["openssl"] }
|
||||
actix-http = { version = "3.0.0-beta.15", features = ["openssl"] }
|
||||
actix-http-test = { version = "3.0.0-beta.9", features = ["openssl"] }
|
||||
actix-server = "2.0.0-rc.1"
|
||||
actix-test = { version = "0.1.0-beta.9", features = ["openssl", "rustls"] }
|
||||
actix-test = { version = "0.1.0-beta.8", features = ["openssl", "rustls"] }
|
||||
actix-tls = { version = "3.0.0-rc.1", features = ["openssl", "rustls"] }
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = { version = "4.0.0-beta.15", features = ["openssl"] }
|
||||
actix-web = { version = "4.0.0-beta.14", features = ["openssl"] }
|
||||
|
||||
brotli2 = "0.3.2"
|
||||
env_logger = "0.9"
|
||||
|
@@ -3,9 +3,9 @@
|
||||
> Async HTTP and WebSocket client library.
|
||||
|
||||
[](https://crates.io/crates/awc)
|
||||
[](https://docs.rs/awc/3.0.0-beta.14)
|
||||
[](https://docs.rs/awc/3.0.0-beta.13)
|
||||

|
||||
[](https://deps.rs/crate/awc/3.0.0-beta.14)
|
||||
[](https://deps.rs/crate/awc/3.0.0-beta.13)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
@@ -45,7 +45,9 @@ impl AnyBody {
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
Self::Body { body: body.boxed() }
|
||||
Self::Body {
|
||||
body: BoxBody::new(body),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs new `AnyBody` instance from a slice of bytes by copying it.
|
||||
|
@@ -4,25 +4,15 @@
|
||||
|
||||
set -x
|
||||
|
||||
EXIT=0
|
||||
cargo test --lib --tests -p=actix-router --all-features
|
||||
cargo test --lib --tests -p=actix-http --all-features
|
||||
cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
|
||||
cargo test --lib --tests -p=actix-web-codegen --all-features
|
||||
cargo test --lib --tests -p=awc --all-features
|
||||
cargo test --lib --tests -p=actix-http-test --all-features
|
||||
cargo test --lib --tests -p=actix-test --all-features
|
||||
cargo test --lib --tests -p=actix-files
|
||||
cargo test --lib --tests -p=actix-multipart --all-features
|
||||
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||
|
||||
save_exit_code() {
|
||||
eval $@
|
||||
local CMD_EXIT=$?
|
||||
[ "$CMD_EXIT" = "0" ] || EXIT=$CMD_EXIT
|
||||
}
|
||||
|
||||
save_exit_code cargo test --lib --tests -p=actix-router --all-features
|
||||
save_exit_code cargo test --lib --tests -p=actix-http --all-features
|
||||
save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
|
||||
save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features
|
||||
save_exit_code cargo test --lib --tests -p=awc --all-features
|
||||
save_exit_code cargo test --lib --tests -p=actix-http-test --all-features
|
||||
save_exit_code cargo test --lib --tests -p=actix-test --all-features
|
||||
save_exit_code cargo test --lib --tests -p=actix-files
|
||||
save_exit_code cargo test --lib --tests -p=actix-multipart --all-features
|
||||
save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features
|
||||
|
||||
save_exit_code cargo test --workspace --doc
|
||||
|
||||
exit $EXIT
|
||||
cargo test --workspace --doc
|
||||
|
@@ -1,41 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
bold="\033[1m"
|
||||
reset="\033[0m"
|
||||
|
||||
unreleased_for() {
|
||||
DIR=$1
|
||||
|
||||
CARGO_MANIFEST=$DIR/Cargo.toml
|
||||
CHANGELOG_FILE=$DIR/CHANGES.md
|
||||
|
||||
# get current version
|
||||
PACKAGE_NAME="$(sed -nE 's/^name ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST" | head -n 1)"
|
||||
CURRENT_VERSION="$(sed -nE 's/^version ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST")"
|
||||
|
||||
CHANGE_CHUNK_FILE="$(mktemp)"
|
||||
|
||||
# get changelog chunk and save to temp file
|
||||
cat "$CHANGELOG_FILE" |
|
||||
# skip up to unreleased heading
|
||||
sed '1,/Unreleased/ d' |
|
||||
# take up to previous version heading
|
||||
sed "/$CURRENT_VERSION/ q" |
|
||||
# drop last line
|
||||
sed '$d' \
|
||||
>"$CHANGE_CHUNK_FILE"
|
||||
|
||||
# if word count of changelog chunk is 0 then exit
|
||||
if [ "$(wc -w "$CHANGE_CHUNK_FILE" | awk '{ print $1 }')" = "0" ]; then
|
||||
return 0;
|
||||
fi
|
||||
|
||||
echo "${bold}# ${PACKAGE_NAME}${reset} since ${bold}v$CURRENT_VERSION${reset}"
|
||||
cat "$CHANGE_CHUNK_FILE"
|
||||
}
|
||||
|
||||
for f in $(fd --absolute-path CHANGES.md); do
|
||||
unreleased_for $(dirname $f)
|
||||
done
|
@@ -139,12 +139,4 @@ impl crate::body::MessageBody for AnyBody {
|
||||
AnyBody::Boxed { body } => body.as_pin_mut().poll_next(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_into_bytes(self) -> Result<crate::web::Bytes, Self> {
|
||||
match self {
|
||||
AnyBody::None => Ok(crate::web::Bytes::new()),
|
||||
AnyBody::Full { body } => Ok(body),
|
||||
_ => Err(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,15 +6,12 @@ use std::{
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_http::body::MessageBody;
|
||||
use actix_service::{Service, Transform};
|
||||
use futures_core::{future::LocalBoxFuture, ready};
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use crate::{
|
||||
body::{BoxBody, MessageBody},
|
||||
dev::{Service, Transform},
|
||||
error::Error,
|
||||
service::ServiceResponse,
|
||||
};
|
||||
use crate::{error::Error, service::ServiceResponse};
|
||||
|
||||
/// Middleware for enabling any middleware to be used in [`Resource::wrap`](crate::Resource::wrap),
|
||||
/// [`Scope::wrap`](crate::Scope::wrap) and [`Condition`](super::Condition).
|
||||
@@ -55,7 +52,7 @@ where
|
||||
T::Response: MapServiceResponseBody,
|
||||
T::Error: Into<Error>,
|
||||
{
|
||||
type Response = ServiceResponse<BoxBody>;
|
||||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Transform = CompatMiddleware<T::Transform>;
|
||||
type InitError = T::InitError;
|
||||
@@ -80,7 +77,7 @@ where
|
||||
S::Response: MapServiceResponseBody,
|
||||
S::Error: Into<Error>,
|
||||
{
|
||||
type Response = ServiceResponse<BoxBody>;
|
||||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Future = CompatMiddlewareFuture<S::Future>;
|
||||
|
||||
@@ -105,7 +102,7 @@ where
|
||||
T: MapServiceResponseBody,
|
||||
E: Into<Error>,
|
||||
{
|
||||
type Output = Result<ServiceResponse<BoxBody>, Error>;
|
||||
type Output = Result<ServiceResponse, Error>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let res = match ready!(self.project().fut.poll(cx)) {
|
||||
@@ -119,15 +116,14 @@ where
|
||||
|
||||
/// Convert `ServiceResponse`'s `ResponseBody<B>` generic type to `ResponseBody<Body>`.
|
||||
pub trait MapServiceResponseBody {
|
||||
fn map_body(self) -> ServiceResponse<BoxBody>;
|
||||
fn map_body(self) -> ServiceResponse;
|
||||
}
|
||||
|
||||
impl<B> MapServiceResponseBody for ServiceResponse<B>
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
B: MessageBody + Unpin + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn map_body(self) -> ServiceResponse<BoxBody> {
|
||||
fn map_body(self) -> ServiceResponse {
|
||||
self.map_into_boxed_body()
|
||||
}
|
||||
}
|
||||
|
@@ -106,7 +106,7 @@ mod tests {
|
||||
header::{HeaderValue, CONTENT_TYPE},
|
||||
StatusCode,
|
||||
},
|
||||
middleware::{err_handlers::*, Compat},
|
||||
middleware::err_handlers::*,
|
||||
test::{self, TestRequest},
|
||||
HttpResponse,
|
||||
};
|
||||
@@ -116,8 +116,7 @@ mod tests {
|
||||
res.response_mut()
|
||||
.headers_mut()
|
||||
.insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
|
||||
|
||||
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
Ok(ErrorHandlerResponse::Response(res))
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@@ -126,9 +125,7 @@ mod tests {
|
||||
ok(req.into_response(HttpResponse::InternalServerError().finish()))
|
||||
};
|
||||
|
||||
let mw = Compat::new(
|
||||
ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500),
|
||||
);
|
||||
let mw = ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500);
|
||||
|
||||
let mw = Condition::new(true, mw)
|
||||
.new_transform(srv.into_service())
|
||||
@@ -144,9 +141,7 @@ mod tests {
|
||||
ok(req.into_response(HttpResponse::InternalServerError().finish()))
|
||||
};
|
||||
|
||||
let mw = Compat::new(
|
||||
ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500),
|
||||
);
|
||||
let mw = ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500);
|
||||
|
||||
let mw = Condition::new(false, mw)
|
||||
.new_transform(srv.into_service())
|
||||
|
@@ -194,7 +194,7 @@ mod tests {
|
||||
use crate::{
|
||||
dev::ServiceRequest,
|
||||
http::header::CONTENT_TYPE,
|
||||
test::{self, TestRequest},
|
||||
test::{ok_service, TestRequest},
|
||||
HttpResponse,
|
||||
};
|
||||
|
||||
@@ -203,7 +203,7 @@ mod tests {
|
||||
let mw = DefaultHeaders::new()
|
||||
.add(("X-TEST", "0001"))
|
||||
.add(("X-TEST-TWO", HeaderValue::from_static("123")))
|
||||
.new_transform(test::ok_service())
|
||||
.new_transform(ok_service())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -234,9 +234,10 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn adding_content_type() {
|
||||
let srv = |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish()));
|
||||
let mw = DefaultHeaders::new()
|
||||
.add_content_type()
|
||||
.new_transform(test::ok_service())
|
||||
.new_transform(srv.into_service())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@@ -13,7 +13,6 @@ use futures_core::{future::LocalBoxFuture, ready};
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use crate::{
|
||||
body::EitherBody,
|
||||
dev::{ServiceRequest, ServiceResponse},
|
||||
http::StatusCode,
|
||||
Error, Result,
|
||||
@@ -22,10 +21,10 @@ use crate::{
|
||||
/// Return type for [`ErrorHandlers`] custom handlers.
|
||||
pub enum ErrorHandlerResponse<B> {
|
||||
/// Immediate HTTP response.
|
||||
Response(ServiceResponse<EitherBody<B>>),
|
||||
Response(ServiceResponse<B>),
|
||||
|
||||
/// A future that resolves to an HTTP response.
|
||||
Future(LocalBoxFuture<'static, Result<ServiceResponse<EitherBody<B>>, Error>>),
|
||||
Future(LocalBoxFuture<'static, Result<ServiceResponse<B>, Error>>),
|
||||
}
|
||||
|
||||
type ErrorHandler<B> = dyn Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>>;
|
||||
@@ -45,8 +44,7 @@ type ErrorHandler<B> = dyn Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse
|
||||
/// res.response_mut()
|
||||
/// .headers_mut()
|
||||
/// .insert(header::CONTENT_TYPE, header::HeaderValue::from_static("Error"));
|
||||
///
|
||||
/// Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
/// Ok(ErrorHandlerResponse::Response(res))
|
||||
/// }
|
||||
///
|
||||
/// let app = App::new()
|
||||
@@ -68,7 +66,7 @@ type Handlers<B> = Rc<AHashMap<StatusCode, Box<ErrorHandler<B>>>>;
|
||||
impl<B> Default for ErrorHandlers<B> {
|
||||
fn default() -> Self {
|
||||
ErrorHandlers {
|
||||
handlers: Default::default(),
|
||||
handlers: Rc::new(AHashMap::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,7 +95,7 @@ where
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<EitherBody<B>>;
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type Transform = ErrorHandlersMiddleware<S, B>;
|
||||
type InitError = ();
|
||||
@@ -121,7 +119,7 @@ where
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<EitherBody<B>>;
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type Future = ErrorHandlersFuture<S::Future, B>;
|
||||
|
||||
@@ -145,8 +143,8 @@ pin_project! {
|
||||
fut: Fut,
|
||||
handlers: Handlers<B>,
|
||||
},
|
||||
ErrorHandlerFuture {
|
||||
fut: LocalBoxFuture<'static, Result<ServiceResponse<EitherBody<B>>, Error>>,
|
||||
HandlerFuture {
|
||||
fut: LocalBoxFuture<'static, Fut::Output>,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -155,29 +153,25 @@ impl<Fut, B> Future for ErrorHandlersFuture<Fut, B>
|
||||
where
|
||||
Fut: Future<Output = Result<ServiceResponse<B>, Error>>,
|
||||
{
|
||||
type Output = Result<ServiceResponse<EitherBody<B>>, Error>;
|
||||
type Output = Fut::Output;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.as_mut().project() {
|
||||
ErrorHandlersProj::ServiceFuture { fut, handlers } => {
|
||||
let res = ready!(fut.poll(cx))?;
|
||||
|
||||
match handlers.get(&res.status()) {
|
||||
Some(handler) => match handler(res)? {
|
||||
ErrorHandlerResponse::Response(res) => Poll::Ready(Ok(res)),
|
||||
ErrorHandlerResponse::Future(fut) => {
|
||||
self.as_mut()
|
||||
.set(ErrorHandlersFuture::ErrorHandlerFuture { fut });
|
||||
|
||||
.set(ErrorHandlersFuture::HandlerFuture { fut });
|
||||
self.poll(cx)
|
||||
}
|
||||
},
|
||||
|
||||
None => Poll::Ready(Ok(res.map_into_left_body())),
|
||||
None => Poll::Ready(Ok(res)),
|
||||
}
|
||||
}
|
||||
|
||||
ErrorHandlersProj::ErrorHandlerFuture { fut } => fut.as_mut().poll(cx),
|
||||
ErrorHandlersProj::HandlerFuture { fut } => fut.as_mut().poll(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,33 +180,32 @@ where
|
||||
mod tests {
|
||||
use actix_service::IntoService;
|
||||
use actix_utils::future::ok;
|
||||
use bytes::Bytes;
|
||||
use futures_util::future::FutureExt as _;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
http::{
|
||||
header::{HeaderValue, CONTENT_TYPE},
|
||||
StatusCode,
|
||||
},
|
||||
test::{self, TestRequest},
|
||||
use crate::http::{
|
||||
header::{HeaderValue, CONTENT_TYPE},
|
||||
StatusCode,
|
||||
};
|
||||
use crate::test::{self, TestRequest};
|
||||
use crate::HttpResponse;
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn render_500<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
res.response_mut()
|
||||
.headers_mut()
|
||||
.insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
|
||||
Ok(ErrorHandlerResponse::Response(res))
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn add_header_error_handler() {
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn error_handler<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
res.response_mut()
|
||||
.headers_mut()
|
||||
.insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
|
||||
|
||||
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
}
|
||||
|
||||
let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
async fn test_handler() {
|
||||
let srv = |req: ServiceRequest| {
|
||||
ok(req.into_response(HttpResponse::InternalServerError().finish()))
|
||||
};
|
||||
|
||||
let mw = ErrorHandlers::new()
|
||||
.handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler)
|
||||
.handler(StatusCode::INTERNAL_SERVER_ERROR, render_500)
|
||||
.new_transform(srv.into_service())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -221,25 +214,24 @@ mod tests {
|
||||
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn render_500_async<B: 'static>(
|
||||
mut res: ServiceResponse<B>,
|
||||
) -> Result<ErrorHandlerResponse<B>> {
|
||||
res.response_mut()
|
||||
.headers_mut()
|
||||
.insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
|
||||
Ok(ErrorHandlerResponse::Future(ok(res).boxed_local()))
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn add_header_error_handler_async() {
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn error_handler<B: 'static>(
|
||||
mut res: ServiceResponse<B>,
|
||||
) -> Result<ErrorHandlerResponse<B>> {
|
||||
res.response_mut()
|
||||
.headers_mut()
|
||||
.insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
|
||||
|
||||
Ok(ErrorHandlerResponse::Future(
|
||||
ok(res.map_into_left_body()).boxed_local(),
|
||||
))
|
||||
}
|
||||
|
||||
let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
async fn test_handler_async() {
|
||||
let srv = |req: ServiceRequest| {
|
||||
ok(req.into_response(HttpResponse::InternalServerError().finish()))
|
||||
};
|
||||
|
||||
let mw = ErrorHandlers::new()
|
||||
.handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler)
|
||||
.handler(StatusCode::INTERNAL_SERVER_ERROR, render_500_async)
|
||||
.new_transform(srv.into_service())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -247,34 +239,4 @@ mod tests {
|
||||
let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await;
|
||||
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn changes_body_type() {
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn error_handler<B: 'static>(
|
||||
res: ServiceResponse<B>,
|
||||
) -> Result<ErrorHandlerResponse<B>> {
|
||||
let (req, res) = res.into_parts();
|
||||
let res = res.set_body(Bytes::from("sorry, that's no bueno"));
|
||||
|
||||
let res = ServiceResponse::new(req, res)
|
||||
.map_into_boxed_body()
|
||||
.map_into_right_body();
|
||||
|
||||
Ok(ErrorHandlerResponse::Response(res))
|
||||
}
|
||||
|
||||
let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
|
||||
let mw = ErrorHandlers::new()
|
||||
.handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler)
|
||||
.new_transform(srv.into_service())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let res = test::call_service(&mw, TestRequest::default().to_srv_request()).await;
|
||||
assert_eq!(test::read_body(res).await, "sorry, that's no bueno");
|
||||
}
|
||||
|
||||
// TODO: test where error is thrown
|
||||
}
|
||||
|
@@ -322,10 +322,13 @@ pin_project! {
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: MessageBody> MessageBody for StreamLog<B> {
|
||||
type Error = B::Error;
|
||||
impl<B> MessageBody for StreamLog<B>
|
||||
where
|
||||
B: MessageBody,
|
||||
B::Error: Into<Error>,
|
||||
{
|
||||
type Error = Error;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
self.body.size()
|
||||
}
|
||||
@@ -341,7 +344,7 @@ impl<B: MessageBody> MessageBody for StreamLog<B> {
|
||||
*this.size += chunk.len();
|
||||
Poll::Ready(Some(Ok(chunk)))
|
||||
}
|
||||
Some(Err(err)) => Poll::Ready(Some(Err(err))),
|
||||
Some(Err(err)) => Poll::Ready(Some(Err(err.into()))),
|
||||
None => Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ mod tests {
|
||||
.wrap(Condition::new(true, DefaultHeaders::new()))
|
||||
.wrap(DefaultHeaders::new().add(("X-Test2", "X-Value2")))
|
||||
.wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| {
|
||||
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
Ok(ErrorHandlerResponse::Response(res))
|
||||
}))
|
||||
.wrap(Logger::default())
|
||||
.wrap(NormalizePath::new(TrailingSlash::Trim));
|
||||
@@ -44,7 +44,7 @@ mod tests {
|
||||
.wrap(NormalizePath::new(TrailingSlash::Trim))
|
||||
.wrap(Logger::default())
|
||||
.wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| {
|
||||
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
Ok(ErrorHandlerResponse::Response(res))
|
||||
}))
|
||||
.wrap(DefaultHeaders::new().add(("X-Test2", "X-Value2")))
|
||||
.wrap(Condition::new(true, DefaultHeaders::new()))
|
||||
|
@@ -244,7 +244,8 @@ impl<B> HttpResponse<B> {
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
self.map_body(|_, body| body.boxed())
|
||||
// TODO: avoid double boxing with down-casting, if it improves perf
|
||||
self.map_body(|_, body| BoxBody::new(body))
|
||||
}
|
||||
|
||||
/// Extract response body
|
||||
|
@@ -451,7 +451,7 @@ impl<B> ServiceResponse<B> {
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
self.map_body(|_, body| body.boxed())
|
||||
self.map_body(|_, body| BoxBody::new(body))
|
||||
}
|
||||
}
|
||||
|
||||
|
909
src/test.rs
Normal file
909
src/test.rs
Normal file
@@ -0,0 +1,909 @@
|
||||
//! Various helpers for Actix applications to use during testing.
|
||||
|
||||
use std::{borrow::Cow, net::SocketAddr, rc::Rc};
|
||||
|
||||
pub use actix_http::test::TestBuffer;
|
||||
use actix_http::{
|
||||
header::TryIntoHeaderPair, test::TestRequest as HttpTestRequest, Extensions, Method,
|
||||
Request, StatusCode, Uri, Version,
|
||||
};
|
||||
use actix_router::{Path, ResourceDef, Url};
|
||||
use actix_service::{IntoService, IntoServiceFactory, Service, ServiceFactory};
|
||||
use actix_utils::future::{ok, poll_fn};
|
||||
use futures_core::Stream;
|
||||
use futures_util::StreamExt as _;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
#[cfg(feature = "cookies")]
|
||||
use crate::cookie::{Cookie, CookieJar};
|
||||
use crate::{
|
||||
app_service::AppInitServiceState,
|
||||
body::{self, BoxBody, MessageBody},
|
||||
config::AppConfig,
|
||||
data::Data,
|
||||
dev::Payload,
|
||||
http::header::ContentType,
|
||||
rmap::ResourceMap,
|
||||
service::{ServiceRequest, ServiceResponse},
|
||||
web::{Bytes, BytesMut},
|
||||
Error, HttpRequest, HttpResponse, HttpResponseBuilder,
|
||||
};
|
||||
|
||||
/// Create service that always responds with `HttpResponse::Ok()` and no body.
|
||||
pub fn ok_service(
|
||||
) -> impl Service<ServiceRequest, Response = ServiceResponse<BoxBody>, 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<ServiceRequest, Response = ServiceResponse<BoxBody>, Error = Error> {
|
||||
(move |req: ServiceRequest| {
|
||||
ok(req.into_response(HttpResponseBuilder::new(status_code).finish()))
|
||||
})
|
||||
.into_service()
|
||||
}
|
||||
|
||||
/// Initialize service from application builder instance.
|
||||
///
|
||||
/// ```
|
||||
/// use actix_service::Service;
|
||||
/// use actix_web::{test, web, App, HttpResponse, http::StatusCode};
|
||||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_init_service() {
|
||||
/// let app = test::init_service(
|
||||
/// App::new()
|
||||
/// .service(web::resource("/test").to(|| async { "OK" }))
|
||||
/// ).await;
|
||||
///
|
||||
/// // Create request object
|
||||
/// let req = test::TestRequest::with_uri("/test").to_request();
|
||||
///
|
||||
/// // Execute application
|
||||
/// let resp = app.call(req).await.unwrap();
|
||||
/// assert_eq!(resp.status(), StatusCode::OK);
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn init_service<R, S, B, E>(
|
||||
app: R,
|
||||
) -> impl Service<Request, Response = ServiceResponse<B>, Error = E>
|
||||
where
|
||||
R: IntoServiceFactory<S, Request>,
|
||||
S: ServiceFactory<Request, Config = AppConfig, Response = ServiceResponse<B>, Error = E>,
|
||||
S::InitError: std::fmt::Debug,
|
||||
{
|
||||
try_init_service(app)
|
||||
.await
|
||||
.expect("service initialization failed")
|
||||
}
|
||||
|
||||
/// Fallible version of [`init_service`] that allows testing initialization errors.
|
||||
pub(crate) async fn try_init_service<R, S, B, E>(
|
||||
app: R,
|
||||
) -> Result<impl Service<Request, Response = ServiceResponse<B>, Error = E>, S::InitError>
|
||||
where
|
||||
R: IntoServiceFactory<S, Request>,
|
||||
S: ServiceFactory<Request, Config = AppConfig, Response = ServiceResponse<B>, Error = E>,
|
||||
S::InitError: std::fmt::Debug,
|
||||
{
|
||||
let srv = app.into_factory();
|
||||
srv.new_service(AppConfig::default()).await
|
||||
}
|
||||
|
||||
/// Calls service and waits for response future completion.
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::{test, web, App, HttpResponse, http::StatusCode};
|
||||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_response() {
|
||||
/// let app = test::init_service(
|
||||
/// App::new()
|
||||
/// .service(web::resource("/test").to(|| async {
|
||||
/// HttpResponse::Ok()
|
||||
/// }))
|
||||
/// ).await;
|
||||
///
|
||||
/// // Create request object
|
||||
/// let req = test::TestRequest::with_uri("/test").to_request();
|
||||
///
|
||||
/// // Call application
|
||||
/// let resp = test::call_service(&app, req).await;
|
||||
/// assert_eq!(resp.status(), StatusCode::OK);
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn call_service<S, R, B, E>(app: &S, req: R) -> S::Response
|
||||
where
|
||||
S: Service<R, Response = ServiceResponse<B>, Error = E>,
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
app.call(req).await.unwrap()
|
||||
}
|
||||
|
||||
/// Helper function that returns a response body of a TestRequest
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::{test, web, App, HttpResponse, http::header};
|
||||
/// use bytes::Bytes;
|
||||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_index() {
|
||||
/// let app = test::init_service(
|
||||
/// App::new().service(
|
||||
/// web::resource("/index.html")
|
||||
/// .route(web::post().to(|| async {
|
||||
/// HttpResponse::Ok().body("welcome!")
|
||||
/// })))
|
||||
/// ).await;
|
||||
///
|
||||
/// let req = test::TestRequest::post()
|
||||
/// .uri("/index.html")
|
||||
/// .header(header::CONTENT_TYPE, "application/json")
|
||||
/// .to_request();
|
||||
///
|
||||
/// let result = test::read_response(&app, req).await;
|
||||
/// assert_eq!(result, Bytes::from_static(b"welcome!"));
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn read_response<S, B>(app: &S, req: Request) -> Bytes
|
||||
where
|
||||
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
|
||||
B: MessageBody + Unpin,
|
||||
B::Error: Into<Error>,
|
||||
{
|
||||
let resp = app
|
||||
.call(req)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("read_response failed at application call: {}", e));
|
||||
|
||||
let body = resp.into_body();
|
||||
let mut bytes = BytesMut::new();
|
||||
|
||||
actix_rt::pin!(body);
|
||||
while let Some(item) = poll_fn(|cx| body.as_mut().poll_next(cx)).await {
|
||||
bytes.extend_from_slice(&item.map_err(Into::into).unwrap());
|
||||
}
|
||||
|
||||
bytes.freeze()
|
||||
}
|
||||
|
||||
/// Helper function that returns a response body of a ServiceResponse.
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::{test, web, App, HttpResponse, http::header};
|
||||
/// use bytes::Bytes;
|
||||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_index() {
|
||||
/// let app = test::init_service(
|
||||
/// App::new().service(
|
||||
/// web::resource("/index.html")
|
||||
/// .route(web::post().to(|| async {
|
||||
/// HttpResponse::Ok().body("welcome!")
|
||||
/// })))
|
||||
/// ).await;
|
||||
///
|
||||
/// let req = test::TestRequest::post()
|
||||
/// .uri("/index.html")
|
||||
/// .header(header::CONTENT_TYPE, "application/json")
|
||||
/// .to_request();
|
||||
///
|
||||
/// let resp = test::call_service(&app, req).await;
|
||||
/// let result = test::read_body(resp).await;
|
||||
/// assert_eq!(result, Bytes::from_static(b"welcome!"));
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn read_body<B>(res: ServiceResponse<B>) -> Bytes
|
||||
where
|
||||
B: MessageBody + Unpin,
|
||||
B::Error: Into<Error>,
|
||||
{
|
||||
let body = res.into_body();
|
||||
let mut bytes = BytesMut::new();
|
||||
|
||||
actix_rt::pin!(body);
|
||||
while let Some(item) = poll_fn(|cx| body.as_mut().poll_next(cx)).await {
|
||||
bytes.extend_from_slice(&item.map_err(Into::into).unwrap());
|
||||
}
|
||||
|
||||
bytes.freeze()
|
||||
}
|
||||
|
||||
/// Helper function that returns a deserialized response body of a ServiceResponse.
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::{App, test, web, HttpResponse, http::header};
|
||||
/// use serde::{Serialize, Deserialize};
|
||||
///
|
||||
/// #[derive(Serialize, Deserialize)]
|
||||
/// pub struct Person {
|
||||
/// id: String,
|
||||
/// name: String,
|
||||
/// }
|
||||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_post_person() {
|
||||
/// let app = test::init_service(
|
||||
/// App::new().service(
|
||||
/// web::resource("/people")
|
||||
/// .route(web::post().to(|person: web::Json<Person>| async {
|
||||
/// HttpResponse::Ok()
|
||||
/// .json(person)})
|
||||
/// ))
|
||||
/// ).await;
|
||||
///
|
||||
/// let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes();
|
||||
///
|
||||
/// let resp = test::TestRequest::post()
|
||||
/// .uri("/people")
|
||||
/// .header(header::CONTENT_TYPE, "application/json")
|
||||
/// .set_payload(payload)
|
||||
/// .send_request(&mut app)
|
||||
/// .await;
|
||||
///
|
||||
/// assert!(resp.status().is_success());
|
||||
///
|
||||
/// let result: Person = test::read_body_json(resp).await;
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn read_body_json<T, B>(res: ServiceResponse<B>) -> T
|
||||
where
|
||||
B: MessageBody + Unpin,
|
||||
B::Error: Into<Error>,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let body = read_body(res).await;
|
||||
|
||||
serde_json::from_slice(&body).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"read_response_json failed during deserialization of body: {:?}, {}",
|
||||
body, e
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn load_stream<S>(mut stream: S) -> Result<Bytes, Error>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, Error>> + Unpin,
|
||||
{
|
||||
let mut data = BytesMut::new();
|
||||
while let Some(item) = stream.next().await {
|
||||
data.extend_from_slice(&item?);
|
||||
}
|
||||
Ok(data.freeze())
|
||||
}
|
||||
|
||||
pub async fn load_body<B>(body: B) -> Result<Bytes, Error>
|
||||
where
|
||||
B: MessageBody + Unpin,
|
||||
B::Error: Into<Error>,
|
||||
{
|
||||
body::to_bytes(body).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Helper function that returns a deserialized response body of a TestRequest
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::{App, test, web, HttpResponse, http::header};
|
||||
/// use serde::{Serialize, Deserialize};
|
||||
///
|
||||
/// #[derive(Serialize, Deserialize)]
|
||||
/// pub struct Person {
|
||||
/// id: String,
|
||||
/// name: String
|
||||
/// }
|
||||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_add_person() {
|
||||
/// let app = test::init_service(
|
||||
/// App::new().service(
|
||||
/// web::resource("/people")
|
||||
/// .route(web::post().to(|person: web::Json<Person>| async {
|
||||
/// HttpResponse::Ok()
|
||||
/// .json(person)})
|
||||
/// ))
|
||||
/// ).await;
|
||||
///
|
||||
/// let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes();
|
||||
///
|
||||
/// let req = test::TestRequest::post()
|
||||
/// .uri("/people")
|
||||
/// .header(header::CONTENT_TYPE, "application/json")
|
||||
/// .set_payload(payload)
|
||||
/// .to_request();
|
||||
///
|
||||
/// let result: Person = test::read_response_json(&mut app, req).await;
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn read_response_json<S, B, T>(app: &S, req: Request) -> T
|
||||
where
|
||||
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
|
||||
B: MessageBody + Unpin,
|
||||
B::Error: Into<Error>,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let body = read_response(app, req).await;
|
||||
|
||||
serde_json::from_slice(&body).unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"read_response_json failed during deserialization of body: {:?}",
|
||||
body
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Test `Request` builder.
|
||||
///
|
||||
/// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern.
|
||||
/// You can generate various types of request via TestRequest's methods:
|
||||
/// * `TestRequest::to_request` creates `actix_http::Request` instance.
|
||||
/// * `TestRequest::to_srv_request` creates `ServiceRequest` instance, which is used for testing middlewares and chain adapters.
|
||||
/// * `TestRequest::to_srv_response` creates `ServiceResponse` instance.
|
||||
/// * `TestRequest::to_http_request` creates `HttpRequest` instance, which is used for testing handlers.
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::{test, HttpRequest, HttpResponse, HttpMessage};
|
||||
/// use actix_web::http::{header, StatusCode};
|
||||
///
|
||||
/// async fn index(req: HttpRequest) -> HttpResponse {
|
||||
/// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) {
|
||||
/// HttpResponse::Ok().into()
|
||||
/// } else {
|
||||
/// HttpResponse::BadRequest().into()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_index() {
|
||||
/// let req = test::TestRequest::default().insert_header("content-type", "text/plain")
|
||||
/// .to_http_request();
|
||||
///
|
||||
/// let resp = index(req).await.unwrap();
|
||||
/// assert_eq!(resp.status(), StatusCode::OK);
|
||||
///
|
||||
/// let req = test::TestRequest::default().to_http_request();
|
||||
/// let resp = index(req).await.unwrap();
|
||||
/// assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
/// }
|
||||
/// ```
|
||||
pub struct TestRequest {
|
||||
req: HttpTestRequest,
|
||||
rmap: ResourceMap,
|
||||
config: AppConfig,
|
||||
path: Path<Url>,
|
||||
peer_addr: Option<SocketAddr>,
|
||||
app_data: Extensions,
|
||||
#[cfg(feature = "cookies")]
|
||||
cookies: CookieJar,
|
||||
}
|
||||
|
||||
impl Default for TestRequest {
|
||||
fn default() -> TestRequest {
|
||||
TestRequest {
|
||||
req: HttpTestRequest::default(),
|
||||
rmap: ResourceMap::new(ResourceDef::new("")),
|
||||
config: AppConfig::default(),
|
||||
path: Path::new(Url::new(Uri::default())),
|
||||
peer_addr: None,
|
||||
app_data: Extensions::new(),
|
||||
#[cfg(feature = "cookies")]
|
||||
cookies: CookieJar::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
impl TestRequest {
|
||||
/// Create TestRequest and set request uri
|
||||
pub fn with_uri(path: &str) -> TestRequest {
|
||||
TestRequest::default().uri(path)
|
||||
}
|
||||
|
||||
/// Create TestRequest and set method to `Method::GET`
|
||||
pub fn get() -> TestRequest {
|
||||
TestRequest::default().method(Method::GET)
|
||||
}
|
||||
|
||||
/// Create TestRequest and set method to `Method::POST`
|
||||
pub fn post() -> TestRequest {
|
||||
TestRequest::default().method(Method::POST)
|
||||
}
|
||||
|
||||
/// Create TestRequest and set method to `Method::PUT`
|
||||
pub fn put() -> TestRequest {
|
||||
TestRequest::default().method(Method::PUT)
|
||||
}
|
||||
|
||||
/// Create TestRequest and set method to `Method::PATCH`
|
||||
pub fn patch() -> TestRequest {
|
||||
TestRequest::default().method(Method::PATCH)
|
||||
}
|
||||
|
||||
/// Create TestRequest and set method to `Method::DELETE`
|
||||
pub fn delete() -> TestRequest {
|
||||
TestRequest::default().method(Method::DELETE)
|
||||
}
|
||||
|
||||
/// Set HTTP version of this request
|
||||
pub fn version(mut self, ver: Version) -> Self {
|
||||
self.req.version(ver);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set HTTP method of this request
|
||||
pub fn method(mut self, meth: Method) -> Self {
|
||||
self.req.method(meth);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set HTTP Uri of this request
|
||||
pub fn uri(mut self, path: &str) -> Self {
|
||||
self.req.uri(path);
|
||||
self
|
||||
}
|
||||
|
||||
/// Insert a header, replacing any that were set with an equivalent field name.
|
||||
pub fn insert_header(mut self, header: impl TryIntoHeaderPair) -> Self {
|
||||
self.req.insert_header(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Append a header, keeping any that were set with an equivalent field name.
|
||||
pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self {
|
||||
self.req.append_header(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set cookie for this request.
|
||||
#[cfg(feature = "cookies")]
|
||||
pub fn cookie(mut self, cookie: Cookie<'_>) -> Self {
|
||||
self.cookies.add(cookie.into_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set request path pattern parameter.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::test::TestRequest;
|
||||
///
|
||||
/// let req = TestRequest::default().param("foo", "bar");
|
||||
/// let req = TestRequest::default().param("foo".to_owned(), "bar".to_owned());
|
||||
/// ```
|
||||
pub fn param(
|
||||
mut self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
value: impl Into<Cow<'static, str>>,
|
||||
) -> Self {
|
||||
self.path.add_static(name, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set peer addr.
|
||||
pub fn peer_addr(mut self, addr: SocketAddr) -> Self {
|
||||
self.peer_addr = Some(addr);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set request payload.
|
||||
pub fn set_payload<B: Into<Bytes>>(mut self, data: B) -> Self {
|
||||
self.req.set_payload(data);
|
||||
self
|
||||
}
|
||||
|
||||
/// Serialize `data` to a URL encoded form and set it as the request payload. The `Content-Type`
|
||||
/// header is set to `application/x-www-form-urlencoded`.
|
||||
pub fn set_form<T: Serialize>(mut self, data: &T) -> Self {
|
||||
let bytes = serde_urlencoded::to_string(data)
|
||||
.expect("Failed to serialize test data as a urlencoded form");
|
||||
self.req.set_payload(bytes);
|
||||
self.req.insert_header(ContentType::form_url_encoded());
|
||||
self
|
||||
}
|
||||
|
||||
/// Serialize `data` to JSON and set it as the request payload. The `Content-Type` header is
|
||||
/// set to `application/json`.
|
||||
pub fn set_json<T: Serialize>(mut self, data: &T) -> Self {
|
||||
let bytes = serde_json::to_string(data).expect("Failed to serialize test data to json");
|
||||
self.req.set_payload(bytes);
|
||||
self.req.insert_header(ContentType::json());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set application data. This is equivalent of `App::data()` method
|
||||
/// for testing purpose.
|
||||
pub fn data<T: 'static>(mut self, data: T) -> Self {
|
||||
self.app_data.insert(Data::new(data));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set application data. This is equivalent of `App::app_data()` method
|
||||
/// for testing purpose.
|
||||
pub fn app_data<T: 'static>(mut self, data: T) -> Self {
|
||||
self.app_data.insert(data);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Set request config
|
||||
pub(crate) fn rmap(mut self, rmap: ResourceMap) -> Self {
|
||||
self.rmap = rmap;
|
||||
self
|
||||
}
|
||||
|
||||
fn finish(&mut self) -> Request {
|
||||
// mut used when cookie feature is enabled
|
||||
#[allow(unused_mut)]
|
||||
let mut req = self.req.finish();
|
||||
|
||||
#[cfg(feature = "cookies")]
|
||||
{
|
||||
use actix_http::header::{HeaderValue, COOKIE};
|
||||
|
||||
let cookie: String = self
|
||||
.cookies
|
||||
.delta()
|
||||
// ensure only name=value is written to cookie header
|
||||
.map(|c| c.stripped().encoded().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ");
|
||||
|
||||
if !cookie.is_empty() {
|
||||
req.headers_mut()
|
||||
.insert(COOKIE, HeaderValue::from_str(&cookie).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
req
|
||||
}
|
||||
|
||||
/// Complete request creation and generate `Request` instance
|
||||
pub fn to_request(mut self) -> Request {
|
||||
let mut req = self.finish();
|
||||
req.head_mut().peer_addr = self.peer_addr;
|
||||
req
|
||||
}
|
||||
|
||||
/// Complete request creation and generate `ServiceRequest` instance
|
||||
pub fn to_srv_request(mut self) -> ServiceRequest {
|
||||
let (mut head, payload) = self.finish().into_parts();
|
||||
head.peer_addr = self.peer_addr;
|
||||
self.path.get_mut().update(&head.uri);
|
||||
|
||||
let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone());
|
||||
|
||||
ServiceRequest::new(
|
||||
HttpRequest::new(
|
||||
self.path,
|
||||
head,
|
||||
app_state,
|
||||
Rc::new(self.app_data),
|
||||
None,
|
||||
Default::default(),
|
||||
),
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
/// Complete request creation and generate `ServiceResponse` instance
|
||||
pub fn to_srv_response<B>(self, res: HttpResponse<B>) -> ServiceResponse<B> {
|
||||
self.to_srv_request().into_response(res)
|
||||
}
|
||||
|
||||
/// Complete request creation and generate `HttpRequest` instance
|
||||
pub fn to_http_request(mut self) -> HttpRequest {
|
||||
let (mut head, _) = self.finish().into_parts();
|
||||
head.peer_addr = self.peer_addr;
|
||||
self.path.get_mut().update(&head.uri);
|
||||
|
||||
let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone());
|
||||
|
||||
HttpRequest::new(
|
||||
self.path,
|
||||
head,
|
||||
app_state,
|
||||
Rc::new(self.app_data),
|
||||
None,
|
||||
Default::default(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Complete request creation and generate `HttpRequest` and `Payload` instances
|
||||
pub fn to_http_parts(mut self) -> (HttpRequest, Payload) {
|
||||
let (mut head, payload) = self.finish().into_parts();
|
||||
head.peer_addr = self.peer_addr;
|
||||
self.path.get_mut().update(&head.uri);
|
||||
|
||||
let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone());
|
||||
|
||||
let req = HttpRequest::new(
|
||||
self.path,
|
||||
head,
|
||||
app_state,
|
||||
Rc::new(self.app_data),
|
||||
None,
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
(req, payload)
|
||||
}
|
||||
|
||||
/// Complete request creation, calls service and waits for response future completion.
|
||||
pub async fn send_request<S, B, E>(self, app: &S) -> S::Response
|
||||
where
|
||||
S: Service<Request, Response = ServiceResponse<B>, Error = E>,
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
let req = self.to_request();
|
||||
call_service(app, req).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn set_server_hostname(&mut self, host: &str) {
|
||||
self.config.set_host(host)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
use actix_http::HttpMessage;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::*;
|
||||
use crate::{http::header, web, App, HttpResponse, Responder};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_basics() {
|
||||
let req = TestRequest::default()
|
||||
.version(Version::HTTP_2)
|
||||
.insert_header(header::ContentType::json())
|
||||
.insert_header(header::Date(SystemTime::now().into()))
|
||||
.param("test", "123")
|
||||
.data(10u32)
|
||||
.app_data(20u64)
|
||||
.peer_addr("127.0.0.1:8081".parse().unwrap())
|
||||
.to_http_request();
|
||||
assert!(req.headers().contains_key(header::CONTENT_TYPE));
|
||||
assert!(req.headers().contains_key(header::DATE));
|
||||
assert_eq!(
|
||||
req.head().peer_addr,
|
||||
Some("127.0.0.1:8081".parse().unwrap())
|
||||
);
|
||||
assert_eq!(&req.match_info()["test"], "123");
|
||||
assert_eq!(req.version(), Version::HTTP_2);
|
||||
let data = req.app_data::<Data<u32>>().unwrap();
|
||||
assert!(req.app_data::<Data<u64>>().is_none());
|
||||
assert_eq!(*data.get_ref(), 10);
|
||||
|
||||
assert!(req.app_data::<u32>().is_none());
|
||||
let data = req.app_data::<u64>().unwrap();
|
||||
assert_eq!(*data, 20);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_request_methods() {
|
||||
let app = init_service(
|
||||
App::new().service(
|
||||
web::resource("/index.html")
|
||||
.route(web::put().to(|| HttpResponse::Ok().body("put!")))
|
||||
.route(web::patch().to(|| HttpResponse::Ok().body("patch!")))
|
||||
.route(web::delete().to(|| HttpResponse::Ok().body("delete!"))),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let put_req = TestRequest::put()
|
||||
.uri("/index.html")
|
||||
.insert_header((header::CONTENT_TYPE, "application/json"))
|
||||
.to_request();
|
||||
|
||||
let result = read_response(&app, put_req).await;
|
||||
assert_eq!(result, Bytes::from_static(b"put!"));
|
||||
|
||||
let patch_req = TestRequest::patch()
|
||||
.uri("/index.html")
|
||||
.insert_header((header::CONTENT_TYPE, "application/json"))
|
||||
.to_request();
|
||||
|
||||
let result = read_response(&app, patch_req).await;
|
||||
assert_eq!(result, Bytes::from_static(b"patch!"));
|
||||
|
||||
let delete_req = TestRequest::delete().uri("/index.html").to_request();
|
||||
let result = read_response(&app, delete_req).await;
|
||||
assert_eq!(result, Bytes::from_static(b"delete!"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_response() {
|
||||
let app = init_service(
|
||||
App::new().service(
|
||||
web::resource("/index.html")
|
||||
.route(web::post().to(|| HttpResponse::Ok().body("welcome!"))),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = TestRequest::post()
|
||||
.uri("/index.html")
|
||||
.insert_header((header::CONTENT_TYPE, "application/json"))
|
||||
.to_request();
|
||||
|
||||
let result = read_response(&app, req).await;
|
||||
assert_eq!(result, Bytes::from_static(b"welcome!"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_send_request() {
|
||||
let app = init_service(
|
||||
App::new().service(
|
||||
web::resource("/index.html")
|
||||
.route(web::get().to(|| HttpResponse::Ok().body("welcome!"))),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let resp = TestRequest::get()
|
||||
.uri("/index.html")
|
||||
.send_request(&app)
|
||||
.await;
|
||||
|
||||
let result = read_body(resp).await;
|
||||
assert_eq!(result, Bytes::from_static(b"welcome!"));
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Person {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_response_json() {
|
||||
let app = init_service(App::new().service(web::resource("/people").route(
|
||||
web::post().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
|
||||
)))
|
||||
.await;
|
||||
|
||||
let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes();
|
||||
|
||||
let req = TestRequest::post()
|
||||
.uri("/people")
|
||||
.insert_header((header::CONTENT_TYPE, "application/json"))
|
||||
.set_payload(payload)
|
||||
.to_request();
|
||||
|
||||
let result: Person = read_response_json(&app, req).await;
|
||||
assert_eq!(&result.id, "12345");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_body_json() {
|
||||
let app = init_service(App::new().service(web::resource("/people").route(
|
||||
web::post().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
|
||||
)))
|
||||
.await;
|
||||
|
||||
let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes();
|
||||
|
||||
let resp = TestRequest::post()
|
||||
.uri("/people")
|
||||
.insert_header((header::CONTENT_TYPE, "application/json"))
|
||||
.set_payload(payload)
|
||||
.send_request(&app)
|
||||
.await;
|
||||
|
||||
let result: Person = read_body_json(resp).await;
|
||||
assert_eq!(&result.name, "User name");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_request_response_form() {
|
||||
let app = init_service(App::new().service(web::resource("/people").route(
|
||||
web::post().to(|person: web::Form<Person>| HttpResponse::Ok().json(person)),
|
||||
)))
|
||||
.await;
|
||||
|
||||
let payload = Person {
|
||||
id: "12345".to_string(),
|
||||
name: "User name".to_string(),
|
||||
};
|
||||
|
||||
let req = TestRequest::post()
|
||||
.uri("/people")
|
||||
.set_form(&payload)
|
||||
.to_request();
|
||||
|
||||
assert_eq!(req.content_type(), "application/x-www-form-urlencoded");
|
||||
|
||||
let result: Person = read_response_json(&app, req).await;
|
||||
assert_eq!(&result.id, "12345");
|
||||
assert_eq!(&result.name, "User name");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_request_response_json() {
|
||||
let app = init_service(App::new().service(web::resource("/people").route(
|
||||
web::post().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
|
||||
)))
|
||||
.await;
|
||||
|
||||
let payload = Person {
|
||||
id: "12345".to_string(),
|
||||
name: "User name".to_string(),
|
||||
};
|
||||
|
||||
let req = TestRequest::post()
|
||||
.uri("/people")
|
||||
.set_json(&payload)
|
||||
.to_request();
|
||||
|
||||
assert_eq!(req.content_type(), "application/json");
|
||||
|
||||
let result: Person = read_response_json(&app, req).await;
|
||||
assert_eq!(&result.id, "12345");
|
||||
assert_eq!(&result.name, "User name");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_async_with_block() {
|
||||
async fn async_with_block() -> Result<HttpResponse, Error> {
|
||||
let res = web::block(move || Some(4usize).ok_or("wrong")).await;
|
||||
|
||||
match res {
|
||||
Ok(value) => Ok(HttpResponse::Ok()
|
||||
.content_type("text/plain")
|
||||
.body(format!("Async with block value: {:?}", value))),
|
||||
Err(_) => panic!("Unexpected"),
|
||||
}
|
||||
}
|
||||
|
||||
let app =
|
||||
init_service(App::new().service(web::resource("/index.html").to(async_with_block)))
|
||||
.await;
|
||||
|
||||
let req = TestRequest::post().uri("/index.html").to_request();
|
||||
let res = app.call(req).await.unwrap();
|
||||
assert!(res.status().is_success());
|
||||
}
|
||||
|
||||
// allow deprecated App::data
|
||||
#[allow(deprecated)]
|
||||
#[actix_rt::test]
|
||||
async fn test_server_data() {
|
||||
async fn handler(data: web::Data<usize>) -> impl Responder {
|
||||
assert_eq!(**data, 10);
|
||||
HttpResponse::Ok()
|
||||
}
|
||||
|
||||
let app = init_service(
|
||||
App::new()
|
||||
.data(10usize)
|
||||
.service(web::resource("/index.html").to(handler)),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = TestRequest::post().uri("/index.html").to_request();
|
||||
let res = app.call(req).await.unwrap();
|
||||
assert!(res.status().is_success());
|
||||
}
|
||||
}
|
@@ -1,81 +0,0 @@
|
||||
//! Various helpers for Actix applications to use during testing.
|
||||
//!
|
||||
//! # Creating A Test Service
|
||||
//! - [`init_service`]
|
||||
//!
|
||||
//! # Off-The-Shelf Test Services
|
||||
//! - [`ok_service`]
|
||||
//! - [`simple_service`]
|
||||
//!
|
||||
//! # Calling Test Service
|
||||
//! - [`TestRequest`]
|
||||
//! - [`call_service`]
|
||||
//! - [`call_and_read_body`]
|
||||
//! - [`call_and_read_body_json`]
|
||||
//!
|
||||
//! # Reading Response Payloads
|
||||
//! - [`read_body`]
|
||||
//! - [`read_body_json`]
|
||||
|
||||
// TODO: more docs on generally how testing works with these parts
|
||||
|
||||
pub use actix_http::test::TestBuffer;
|
||||
|
||||
mod test_request;
|
||||
mod test_services;
|
||||
mod test_utils;
|
||||
|
||||
pub use self::test_request::TestRequest;
|
||||
#[allow(deprecated)]
|
||||
pub use self::test_services::{default_service, ok_service, simple_service};
|
||||
#[allow(deprecated)]
|
||||
pub use self::test_utils::{
|
||||
call_and_read_body, call_and_read_body_json, call_service, init_service, read_body,
|
||||
read_body_json, read_response, read_response_json,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::test_utils::try_init_service;
|
||||
|
||||
/// Reduces boilerplate code when testing expected response payloads.
|
||||
///
|
||||
/// Must be used inside an async test. Works for both `ServiceRequest` and `HttpRequest`.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{http::StatusCode, HttpResponse};
|
||||
///
|
||||
/// let res = HttpResponse::with_body(StatusCode::OK, "http response");
|
||||
/// assert_body_eq!(res, b"http response");
|
||||
/// ```
|
||||
#[cfg(test)]
|
||||
macro_rules! assert_body_eq {
|
||||
($res:ident, $expected:expr) => {
|
||||
assert_eq!(
|
||||
::actix_http::body::to_bytes($res.into_body())
|
||||
.await
|
||||
.expect("error reading test response body"),
|
||||
::bytes::Bytes::from_static($expected),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use assert_body_eq;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{http::StatusCode, service::ServiceResponse, HttpResponse};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn assert_body_works_for_service_and_regular_response() {
|
||||
let res = HttpResponse::with_body(StatusCode::OK, "http response");
|
||||
assert_body_eq!(res, b"http response");
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let res = HttpResponse::with_body(StatusCode::OK, "service response");
|
||||
let res = ServiceResponse::new(req, res);
|
||||
assert_body_eq!(res, b"service response");
|
||||
}
|
||||
}
|
@@ -1,431 +0,0 @@
|
||||
use std::{borrow::Cow, net::SocketAddr, rc::Rc};
|
||||
|
||||
use actix_http::{test::TestRequest as HttpTestRequest, Request};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
app_service::AppInitServiceState,
|
||||
config::AppConfig,
|
||||
data::Data,
|
||||
dev::{Extensions, Path, Payload, ResourceDef, Service, Url},
|
||||
http::header::ContentType,
|
||||
http::{header::TryIntoHeaderPair, Method, Uri, Version},
|
||||
rmap::ResourceMap,
|
||||
service::{ServiceRequest, ServiceResponse},
|
||||
test,
|
||||
web::Bytes,
|
||||
HttpRequest, HttpResponse,
|
||||
};
|
||||
|
||||
#[cfg(feature = "cookies")]
|
||||
use crate::cookie::{Cookie, CookieJar};
|
||||
|
||||
/// Test `Request` builder.
|
||||
///
|
||||
/// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern.
|
||||
/// You can generate various types of request via TestRequest's methods:
|
||||
/// * `TestRequest::to_request` creates `actix_http::Request` instance.
|
||||
/// * `TestRequest::to_srv_request` creates `ServiceRequest` instance, which is used for testing middlewares and chain adapters.
|
||||
/// * `TestRequest::to_srv_response` creates `ServiceResponse` instance.
|
||||
/// * `TestRequest::to_http_request` creates `HttpRequest` instance, which is used for testing handlers.
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::{test, HttpRequest, HttpResponse, HttpMessage};
|
||||
/// use actix_web::http::{header, StatusCode};
|
||||
///
|
||||
/// async fn index(req: HttpRequest) -> HttpResponse {
|
||||
/// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) {
|
||||
/// HttpResponse::Ok().into()
|
||||
/// } else {
|
||||
/// HttpResponse::BadRequest().into()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_index() {
|
||||
/// let req = test::TestRequest::default().insert_header("content-type", "text/plain")
|
||||
/// .to_http_request();
|
||||
///
|
||||
/// let resp = index(req).await.unwrap();
|
||||
/// assert_eq!(resp.status(), StatusCode::OK);
|
||||
///
|
||||
/// let req = test::TestRequest::default().to_http_request();
|
||||
/// let resp = index(req).await.unwrap();
|
||||
/// assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
/// }
|
||||
/// ```
|
||||
pub struct TestRequest {
|
||||
req: HttpTestRequest,
|
||||
rmap: ResourceMap,
|
||||
config: AppConfig,
|
||||
path: Path<Url>,
|
||||
peer_addr: Option<SocketAddr>,
|
||||
app_data: Extensions,
|
||||
#[cfg(feature = "cookies")]
|
||||
cookies: CookieJar,
|
||||
}
|
||||
|
||||
impl Default for TestRequest {
|
||||
fn default() -> TestRequest {
|
||||
TestRequest {
|
||||
req: HttpTestRequest::default(),
|
||||
rmap: ResourceMap::new(ResourceDef::new("")),
|
||||
config: AppConfig::default(),
|
||||
path: Path::new(Url::new(Uri::default())),
|
||||
peer_addr: None,
|
||||
app_data: Extensions::new(),
|
||||
#[cfg(feature = "cookies")]
|
||||
cookies: CookieJar::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
impl TestRequest {
|
||||
/// Create TestRequest and set request uri
|
||||
pub fn with_uri(path: &str) -> TestRequest {
|
||||
TestRequest::default().uri(path)
|
||||
}
|
||||
|
||||
/// Create TestRequest and set method to `Method::GET`
|
||||
pub fn get() -> TestRequest {
|
||||
TestRequest::default().method(Method::GET)
|
||||
}
|
||||
|
||||
/// Create TestRequest and set method to `Method::POST`
|
||||
pub fn post() -> TestRequest {
|
||||
TestRequest::default().method(Method::POST)
|
||||
}
|
||||
|
||||
/// Create TestRequest and set method to `Method::PUT`
|
||||
pub fn put() -> TestRequest {
|
||||
TestRequest::default().method(Method::PUT)
|
||||
}
|
||||
|
||||
/// Create TestRequest and set method to `Method::PATCH`
|
||||
pub fn patch() -> TestRequest {
|
||||
TestRequest::default().method(Method::PATCH)
|
||||
}
|
||||
|
||||
/// Create TestRequest and set method to `Method::DELETE`
|
||||
pub fn delete() -> TestRequest {
|
||||
TestRequest::default().method(Method::DELETE)
|
||||
}
|
||||
|
||||
/// Set HTTP version of this request
|
||||
pub fn version(mut self, ver: Version) -> Self {
|
||||
self.req.version(ver);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set HTTP method of this request
|
||||
pub fn method(mut self, meth: Method) -> Self {
|
||||
self.req.method(meth);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set HTTP Uri of this request
|
||||
pub fn uri(mut self, path: &str) -> Self {
|
||||
self.req.uri(path);
|
||||
self
|
||||
}
|
||||
|
||||
/// Insert a header, replacing any that were set with an equivalent field name.
|
||||
pub fn insert_header(mut self, header: impl TryIntoHeaderPair) -> Self {
|
||||
self.req.insert_header(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Append a header, keeping any that were set with an equivalent field name.
|
||||
pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self {
|
||||
self.req.append_header(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set cookie for this request.
|
||||
#[cfg(feature = "cookies")]
|
||||
pub fn cookie(mut self, cookie: Cookie<'_>) -> Self {
|
||||
self.cookies.add(cookie.into_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set request path pattern parameter.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::test::TestRequest;
|
||||
///
|
||||
/// let req = TestRequest::default().param("foo", "bar");
|
||||
/// let req = TestRequest::default().param("foo".to_owned(), "bar".to_owned());
|
||||
/// ```
|
||||
pub fn param(
|
||||
mut self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
value: impl Into<Cow<'static, str>>,
|
||||
) -> Self {
|
||||
self.path.add_static(name, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set peer addr.
|
||||
pub fn peer_addr(mut self, addr: SocketAddr) -> Self {
|
||||
self.peer_addr = Some(addr);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set request payload.
|
||||
pub fn set_payload<B: Into<Bytes>>(mut self, data: B) -> Self {
|
||||
self.req.set_payload(data);
|
||||
self
|
||||
}
|
||||
|
||||
/// Serialize `data` to a URL encoded form and set it as the request payload. The `Content-Type`
|
||||
/// header is set to `application/x-www-form-urlencoded`.
|
||||
pub fn set_form<T: Serialize>(mut self, data: &T) -> Self {
|
||||
let bytes = serde_urlencoded::to_string(data)
|
||||
.expect("Failed to serialize test data as a urlencoded form");
|
||||
self.req.set_payload(bytes);
|
||||
self.req.insert_header(ContentType::form_url_encoded());
|
||||
self
|
||||
}
|
||||
|
||||
/// Serialize `data` to JSON and set it as the request payload. The `Content-Type` header is
|
||||
/// set to `application/json`.
|
||||
pub fn set_json<T: Serialize>(mut self, data: &T) -> Self {
|
||||
let bytes = serde_json::to_string(data).expect("Failed to serialize test data to json");
|
||||
self.req.set_payload(bytes);
|
||||
self.req.insert_header(ContentType::json());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set application data. This is equivalent of `App::data()` method
|
||||
/// for testing purpose.
|
||||
pub fn data<T: 'static>(mut self, data: T) -> Self {
|
||||
self.app_data.insert(Data::new(data));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set application data. This is equivalent of `App::app_data()` method
|
||||
/// for testing purpose.
|
||||
pub fn app_data<T: 'static>(mut self, data: T) -> Self {
|
||||
self.app_data.insert(data);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Set request config
|
||||
pub(crate) fn rmap(mut self, rmap: ResourceMap) -> Self {
|
||||
self.rmap = rmap;
|
||||
self
|
||||
}
|
||||
|
||||
fn finish(&mut self) -> Request {
|
||||
// mut used when cookie feature is enabled
|
||||
#[allow(unused_mut)]
|
||||
let mut req = self.req.finish();
|
||||
|
||||
#[cfg(feature = "cookies")]
|
||||
{
|
||||
use actix_http::header::{HeaderValue, COOKIE};
|
||||
|
||||
let cookie: String = self
|
||||
.cookies
|
||||
.delta()
|
||||
// ensure only name=value is written to cookie header
|
||||
.map(|c| c.stripped().encoded().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ");
|
||||
|
||||
if !cookie.is_empty() {
|
||||
req.headers_mut()
|
||||
.insert(COOKIE, HeaderValue::from_str(&cookie).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
req
|
||||
}
|
||||
|
||||
/// Complete request creation and generate `Request` instance
|
||||
pub fn to_request(mut self) -> Request {
|
||||
let mut req = self.finish();
|
||||
req.head_mut().peer_addr = self.peer_addr;
|
||||
req
|
||||
}
|
||||
|
||||
/// Complete request creation and generate `ServiceRequest` instance
|
||||
pub fn to_srv_request(mut self) -> ServiceRequest {
|
||||
let (mut head, payload) = self.finish().into_parts();
|
||||
head.peer_addr = self.peer_addr;
|
||||
self.path.get_mut().update(&head.uri);
|
||||
|
||||
let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone());
|
||||
|
||||
ServiceRequest::new(
|
||||
HttpRequest::new(
|
||||
self.path,
|
||||
head,
|
||||
app_state,
|
||||
Rc::new(self.app_data),
|
||||
None,
|
||||
Default::default(),
|
||||
),
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
/// Complete request creation and generate `ServiceResponse` instance
|
||||
pub fn to_srv_response<B>(self, res: HttpResponse<B>) -> ServiceResponse<B> {
|
||||
self.to_srv_request().into_response(res)
|
||||
}
|
||||
|
||||
/// Complete request creation and generate `HttpRequest` instance
|
||||
pub fn to_http_request(mut self) -> HttpRequest {
|
||||
let (mut head, _) = self.finish().into_parts();
|
||||
head.peer_addr = self.peer_addr;
|
||||
self.path.get_mut().update(&head.uri);
|
||||
|
||||
let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone());
|
||||
|
||||
HttpRequest::new(
|
||||
self.path,
|
||||
head,
|
||||
app_state,
|
||||
Rc::new(self.app_data),
|
||||
None,
|
||||
Default::default(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Complete request creation and generate `HttpRequest` and `Payload` instances
|
||||
pub fn to_http_parts(mut self) -> (HttpRequest, Payload) {
|
||||
let (mut head, payload) = self.finish().into_parts();
|
||||
head.peer_addr = self.peer_addr;
|
||||
self.path.get_mut().update(&head.uri);
|
||||
|
||||
let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone());
|
||||
|
||||
let req = HttpRequest::new(
|
||||
self.path,
|
||||
head,
|
||||
app_state,
|
||||
Rc::new(self.app_data),
|
||||
None,
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
(req, payload)
|
||||
}
|
||||
|
||||
/// Complete request creation, calls service and waits for response future completion.
|
||||
pub async fn send_request<S, B, E>(self, app: &S) -> S::Response
|
||||
where
|
||||
S: Service<Request, Response = ServiceResponse<B>, Error = E>,
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
let req = self.to_request();
|
||||
test::call_service(app, req).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn set_server_hostname(&mut self, host: &str) {
|
||||
self.config.set_host(host)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::SystemTime;
|
||||
|
||||
use super::*;
|
||||
use crate::{http::header, test::init_service, web, App, Error, HttpResponse, Responder};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_basics() {
|
||||
let req = TestRequest::default()
|
||||
.version(Version::HTTP_2)
|
||||
.insert_header(header::ContentType::json())
|
||||
.insert_header(header::Date(SystemTime::now().into()))
|
||||
.param("test", "123")
|
||||
.data(10u32)
|
||||
.app_data(20u64)
|
||||
.peer_addr("127.0.0.1:8081".parse().unwrap())
|
||||
.to_http_request();
|
||||
assert!(req.headers().contains_key(header::CONTENT_TYPE));
|
||||
assert!(req.headers().contains_key(header::DATE));
|
||||
assert_eq!(
|
||||
req.head().peer_addr,
|
||||
Some("127.0.0.1:8081".parse().unwrap())
|
||||
);
|
||||
assert_eq!(&req.match_info()["test"], "123");
|
||||
assert_eq!(req.version(), Version::HTTP_2);
|
||||
let data = req.app_data::<Data<u32>>().unwrap();
|
||||
assert!(req.app_data::<Data<u64>>().is_none());
|
||||
assert_eq!(*data.get_ref(), 10);
|
||||
|
||||
assert!(req.app_data::<u32>().is_none());
|
||||
let data = req.app_data::<u64>().unwrap();
|
||||
assert_eq!(*data, 20);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_send_request() {
|
||||
let app = init_service(
|
||||
App::new().service(
|
||||
web::resource("/index.html")
|
||||
.route(web::get().to(|| HttpResponse::Ok().body("welcome!"))),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let resp = TestRequest::get()
|
||||
.uri("/index.html")
|
||||
.send_request(&app)
|
||||
.await;
|
||||
|
||||
let result = test::read_body(resp).await;
|
||||
assert_eq!(result, Bytes::from_static(b"welcome!"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_async_with_block() {
|
||||
async fn async_with_block() -> Result<HttpResponse, Error> {
|
||||
let res = web::block(move || Some(4usize).ok_or("wrong")).await;
|
||||
|
||||
match res {
|
||||
Ok(value) => Ok(HttpResponse::Ok()
|
||||
.content_type("text/plain")
|
||||
.body(format!("Async with block value: {:?}", value))),
|
||||
Err(_) => panic!("Unexpected"),
|
||||
}
|
||||
}
|
||||
|
||||
let app =
|
||||
init_service(App::new().service(web::resource("/index.html").to(async_with_block)))
|
||||
.await;
|
||||
|
||||
let req = TestRequest::post().uri("/index.html").to_request();
|
||||
let res = app.call(req).await.unwrap();
|
||||
assert!(res.status().is_success());
|
||||
}
|
||||
|
||||
// allow deprecated App::data
|
||||
#[allow(deprecated)]
|
||||
#[actix_rt::test]
|
||||
async fn test_server_data() {
|
||||
async fn handler(data: web::Data<usize>) -> impl Responder {
|
||||
assert_eq!(**data, 10);
|
||||
HttpResponse::Ok()
|
||||
}
|
||||
|
||||
let app = init_service(
|
||||
App::new()
|
||||
.data(10usize)
|
||||
.service(web::resource("/index.html").to(handler)),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = TestRequest::post().uri("/index.html").to_request();
|
||||
let res = app.call(req).await.unwrap();
|
||||
assert!(res.status().is_success());
|
||||
}
|
||||
}
|
@@ -1,31 +0,0 @@
|
||||
use actix_utils::future::ok;
|
||||
|
||||
use crate::{
|
||||
body::BoxBody,
|
||||
dev::{fn_service, Service, ServiceRequest, ServiceResponse},
|
||||
http::StatusCode,
|
||||
Error, HttpResponseBuilder,
|
||||
};
|
||||
|
||||
/// Creates service that always responds with `200 OK` and no body.
|
||||
pub fn ok_service(
|
||||
) -> impl Service<ServiceRequest, Response = ServiceResponse<BoxBody>, Error = Error> {
|
||||
simple_service(StatusCode::OK)
|
||||
}
|
||||
|
||||
/// Creates service that always responds with given status code and no body.
|
||||
pub fn simple_service(
|
||||
status_code: StatusCode,
|
||||
) -> impl Service<ServiceRequest, Response = ServiceResponse<BoxBody>, Error = Error> {
|
||||
fn_service(move |req: ServiceRequest| {
|
||||
ok(req.into_response(HttpResponseBuilder::new(status_code).finish()))
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated(since = "4.0.0", note = "Renamed to `simple_service`.")]
|
||||
pub fn default_service(
|
||||
status_code: StatusCode,
|
||||
) -> impl Service<ServiceRequest, Response = ServiceResponse<BoxBody>, Error = Error> {
|
||||
simple_service(status_code)
|
||||
}
|
@@ -1,474 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
use actix_http::Request;
|
||||
use actix_service::IntoServiceFactory;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::{
|
||||
body::{self, MessageBody},
|
||||
config::AppConfig,
|
||||
dev::{Service, ServiceFactory},
|
||||
service::ServiceResponse,
|
||||
web::Bytes,
|
||||
Error,
|
||||
};
|
||||
|
||||
/// Initialize service from application builder instance.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_service::Service;
|
||||
/// use actix_web::{test, web, App, HttpResponse, http::StatusCode};
|
||||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_init_service() {
|
||||
/// let app = test::init_service(
|
||||
/// App::new()
|
||||
/// .service(web::resource("/test").to(|| async { "OK" }))
|
||||
/// ).await;
|
||||
///
|
||||
/// // Create request object
|
||||
/// let req = test::TestRequest::with_uri("/test").to_request();
|
||||
///
|
||||
/// // Execute application
|
||||
/// let res = app.call(req).await.unwrap();
|
||||
/// assert_eq!(res.status(), StatusCode::OK);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if service initialization returns an error.
|
||||
pub async fn init_service<R, S, B, E>(
|
||||
app: R,
|
||||
) -> impl Service<Request, Response = ServiceResponse<B>, Error = E>
|
||||
where
|
||||
R: IntoServiceFactory<S, Request>,
|
||||
S: ServiceFactory<Request, Config = AppConfig, Response = ServiceResponse<B>, Error = E>,
|
||||
S::InitError: std::fmt::Debug,
|
||||
{
|
||||
try_init_service(app)
|
||||
.await
|
||||
.expect("service initialization failed")
|
||||
}
|
||||
|
||||
/// Fallible version of [`init_service`] that allows testing initialization errors.
|
||||
pub(crate) async fn try_init_service<R, S, B, E>(
|
||||
app: R,
|
||||
) -> Result<impl Service<Request, Response = ServiceResponse<B>, Error = E>, S::InitError>
|
||||
where
|
||||
R: IntoServiceFactory<S, Request>,
|
||||
S: ServiceFactory<Request, Config = AppConfig, Response = ServiceResponse<B>, Error = E>,
|
||||
S::InitError: std::fmt::Debug,
|
||||
{
|
||||
let srv = app.into_factory();
|
||||
srv.new_service(AppConfig::default()).await
|
||||
}
|
||||
|
||||
/// Calls service and waits for response future completion.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{test, web, App, HttpResponse, http::StatusCode};
|
||||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_response() {
|
||||
/// let app = test::init_service(
|
||||
/// App::new()
|
||||
/// .service(web::resource("/test").to(|| async {
|
||||
/// HttpResponse::Ok()
|
||||
/// }))
|
||||
/// ).await;
|
||||
///
|
||||
/// // Create request object
|
||||
/// let req = test::TestRequest::with_uri("/test").to_request();
|
||||
///
|
||||
/// // Call application
|
||||
/// let res = test::call_service(&app, req).await;
|
||||
/// assert_eq!(res.status(), StatusCode::OK);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if service call returns error.
|
||||
pub async fn call_service<S, R, B, E>(app: &S, req: R) -> S::Response
|
||||
where
|
||||
S: Service<R, Response = ServiceResponse<B>, Error = E>,
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
app.call(req)
|
||||
.await
|
||||
.expect("test service call returned error")
|
||||
}
|
||||
|
||||
/// Helper function that returns a response body of a TestRequest
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{test, web, App, HttpResponse, http::header};
|
||||
/// use bytes::Bytes;
|
||||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_index() {
|
||||
/// let app = test::init_service(
|
||||
/// App::new().service(
|
||||
/// web::resource("/index.html")
|
||||
/// .route(web::post().to(|| async {
|
||||
/// HttpResponse::Ok().body("welcome!")
|
||||
/// })))
|
||||
/// ).await;
|
||||
///
|
||||
/// let req = test::TestRequest::post()
|
||||
/// .uri("/index.html")
|
||||
/// .header(header::CONTENT_TYPE, "application/json")
|
||||
/// .to_request();
|
||||
///
|
||||
/// let result = test::call_and_read_body(&app, req).await;
|
||||
/// assert_eq!(result, Bytes::from_static(b"welcome!"));
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if:
|
||||
/// - service call returns error;
|
||||
/// - body yields an error while it is being read.
|
||||
pub async fn call_and_read_body<S, B>(app: &S, req: Request) -> Bytes
|
||||
where
|
||||
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
|
||||
B: MessageBody,
|
||||
B::Error: fmt::Debug,
|
||||
{
|
||||
let res = call_service(app, req).await;
|
||||
read_body(res).await
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated(since = "4.0.0", note = "Renamed to `call_and_read_body`.")]
|
||||
pub async fn read_response<S, B>(app: &S, req: Request) -> Bytes
|
||||
where
|
||||
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
|
||||
B: MessageBody,
|
||||
B::Error: fmt::Debug,
|
||||
{
|
||||
let res = call_service(app, req).await;
|
||||
read_body(res).await
|
||||
}
|
||||
|
||||
/// Helper function that returns a response body of a ServiceResponse.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{test, web, App, HttpResponse, http::header};
|
||||
/// use bytes::Bytes;
|
||||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_index() {
|
||||
/// let app = test::init_service(
|
||||
/// App::new().service(
|
||||
/// web::resource("/index.html")
|
||||
/// .route(web::post().to(|| async {
|
||||
/// HttpResponse::Ok().body("welcome!")
|
||||
/// })))
|
||||
/// ).await;
|
||||
///
|
||||
/// let req = test::TestRequest::post()
|
||||
/// .uri("/index.html")
|
||||
/// .header(header::CONTENT_TYPE, "application/json")
|
||||
/// .to_request();
|
||||
///
|
||||
/// let res = test::call_service(&app, req).await;
|
||||
/// let result = test::read_body(res).await;
|
||||
/// assert_eq!(result, Bytes::from_static(b"welcome!"));
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if body yields an error while it is being read.
|
||||
pub async fn read_body<B>(res: ServiceResponse<B>) -> Bytes
|
||||
where
|
||||
B: MessageBody,
|
||||
B::Error: fmt::Debug,
|
||||
{
|
||||
let body = res.into_body();
|
||||
body::to_bytes(body)
|
||||
.await
|
||||
.expect("error reading test response body")
|
||||
}
|
||||
|
||||
/// Helper function that returns a deserialized response body of a ServiceResponse.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{App, test, web, HttpResponse, http::header};
|
||||
/// use serde::{Serialize, Deserialize};
|
||||
///
|
||||
/// #[derive(Serialize, Deserialize)]
|
||||
/// pub struct Person {
|
||||
/// id: String,
|
||||
/// name: String,
|
||||
/// }
|
||||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_post_person() {
|
||||
/// let app = test::init_service(
|
||||
/// App::new().service(
|
||||
/// web::resource("/people")
|
||||
/// .route(web::post().to(|person: web::Json<Person>| async {
|
||||
/// HttpResponse::Ok()
|
||||
/// .json(person)})
|
||||
/// ))
|
||||
/// ).await;
|
||||
///
|
||||
/// let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes();
|
||||
///
|
||||
/// let res = test::TestRequest::post()
|
||||
/// .uri("/people")
|
||||
/// .header(header::CONTENT_TYPE, "application/json")
|
||||
/// .set_payload(payload)
|
||||
/// .send_request(&mut app)
|
||||
/// .await;
|
||||
///
|
||||
/// assert!(res.status().is_success());
|
||||
///
|
||||
/// let result: Person = test::read_body_json(res).await;
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if:
|
||||
/// - body yields an error while it is being read;
|
||||
/// - received body is not a valid JSON representation of `T`.
|
||||
pub async fn read_body_json<T, B>(res: ServiceResponse<B>) -> T
|
||||
where
|
||||
B: MessageBody,
|
||||
B::Error: fmt::Debug,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let body = read_body(res).await;
|
||||
|
||||
serde_json::from_slice(&body).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"could not deserialize body into a {}\nerr: {}\nbody: {:?}",
|
||||
std::any::type_name::<T>(),
|
||||
err,
|
||||
body,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper function that returns a deserialized response body of a TestRequest
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{App, test, web, HttpResponse, http::header};
|
||||
/// use serde::{Serialize, Deserialize};
|
||||
///
|
||||
/// #[derive(Serialize, Deserialize)]
|
||||
/// pub struct Person {
|
||||
/// id: String,
|
||||
/// name: String
|
||||
/// }
|
||||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_add_person() {
|
||||
/// let app = test::init_service(
|
||||
/// App::new().service(
|
||||
/// web::resource("/people")
|
||||
/// .route(web::post().to(|person: web::Json<Person>| async {
|
||||
/// HttpResponse::Ok()
|
||||
/// .json(person)})
|
||||
/// ))
|
||||
/// ).await;
|
||||
///
|
||||
/// let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes();
|
||||
///
|
||||
/// let req = test::TestRequest::post()
|
||||
/// .uri("/people")
|
||||
/// .header(header::CONTENT_TYPE, "application/json")
|
||||
/// .set_payload(payload)
|
||||
/// .to_request();
|
||||
///
|
||||
/// let result: Person = test::call_and_read_body_json(&mut app, req).await;
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if:
|
||||
/// - service call returns an error body yields an error while it is being read;
|
||||
/// - body yields an error while it is being read;
|
||||
/// - received body is not a valid JSON representation of `T`.
|
||||
pub async fn call_and_read_body_json<S, B, T>(app: &S, req: Request) -> T
|
||||
where
|
||||
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
|
||||
B: MessageBody,
|
||||
B::Error: fmt::Debug,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let res = call_service(app, req).await;
|
||||
read_body_json(res).await
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated(since = "4.0.0", note = "Renamed to `call_and_read_body_json`.")]
|
||||
pub async fn read_response_json<S, B, T>(app: &S, req: Request) -> T
|
||||
where
|
||||
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
|
||||
B: MessageBody,
|
||||
B::Error: fmt::Debug,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
call_and_read_body_json(app, req).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::*;
|
||||
use crate::{http::header, test::TestRequest, web, App, HttpMessage, HttpResponse};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_request_methods() {
|
||||
let app = init_service(
|
||||
App::new().service(
|
||||
web::resource("/index.html")
|
||||
.route(web::put().to(|| HttpResponse::Ok().body("put!")))
|
||||
.route(web::patch().to(|| HttpResponse::Ok().body("patch!")))
|
||||
.route(web::delete().to(|| HttpResponse::Ok().body("delete!"))),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let put_req = TestRequest::put()
|
||||
.uri("/index.html")
|
||||
.insert_header((header::CONTENT_TYPE, "application/json"))
|
||||
.to_request();
|
||||
|
||||
let result = call_and_read_body(&app, put_req).await;
|
||||
assert_eq!(result, Bytes::from_static(b"put!"));
|
||||
|
||||
let patch_req = TestRequest::patch()
|
||||
.uri("/index.html")
|
||||
.insert_header((header::CONTENT_TYPE, "application/json"))
|
||||
.to_request();
|
||||
|
||||
let result = call_and_read_body(&app, patch_req).await;
|
||||
assert_eq!(result, Bytes::from_static(b"patch!"));
|
||||
|
||||
let delete_req = TestRequest::delete().uri("/index.html").to_request();
|
||||
let result = call_and_read_body(&app, delete_req).await;
|
||||
assert_eq!(result, Bytes::from_static(b"delete!"));
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Person {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_response_json() {
|
||||
let app = init_service(App::new().service(web::resource("/people").route(
|
||||
web::post().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
|
||||
)))
|
||||
.await;
|
||||
|
||||
let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes();
|
||||
|
||||
let req = TestRequest::post()
|
||||
.uri("/people")
|
||||
.insert_header((header::CONTENT_TYPE, "application/json"))
|
||||
.set_payload(payload)
|
||||
.to_request();
|
||||
|
||||
let result: Person = call_and_read_body_json(&app, req).await;
|
||||
assert_eq!(&result.id, "12345");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_body_json() {
|
||||
let app = init_service(App::new().service(web::resource("/people").route(
|
||||
web::post().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
|
||||
)))
|
||||
.await;
|
||||
|
||||
let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes();
|
||||
|
||||
let res = TestRequest::post()
|
||||
.uri("/people")
|
||||
.insert_header((header::CONTENT_TYPE, "application/json"))
|
||||
.set_payload(payload)
|
||||
.send_request(&app)
|
||||
.await;
|
||||
|
||||
let result: Person = read_body_json(res).await;
|
||||
assert_eq!(&result.name, "User name");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_request_response_form() {
|
||||
let app = init_service(App::new().service(web::resource("/people").route(
|
||||
web::post().to(|person: web::Form<Person>| HttpResponse::Ok().json(person)),
|
||||
)))
|
||||
.await;
|
||||
|
||||
let payload = Person {
|
||||
id: "12345".to_string(),
|
||||
name: "User name".to_string(),
|
||||
};
|
||||
|
||||
let req = TestRequest::post()
|
||||
.uri("/people")
|
||||
.set_form(&payload)
|
||||
.to_request();
|
||||
|
||||
assert_eq!(req.content_type(), "application/x-www-form-urlencoded");
|
||||
|
||||
let result: Person = call_and_read_body_json(&app, req).await;
|
||||
assert_eq!(&result.id, "12345");
|
||||
assert_eq!(&result.name, "User name");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_response() {
|
||||
let app = init_service(
|
||||
App::new().service(
|
||||
web::resource("/index.html")
|
||||
.route(web::post().to(|| HttpResponse::Ok().body("welcome!"))),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = TestRequest::post()
|
||||
.uri("/index.html")
|
||||
.insert_header((header::CONTENT_TYPE, "application/json"))
|
||||
.to_request();
|
||||
|
||||
let result = call_and_read_body(&app, req).await;
|
||||
assert_eq!(result, Bytes::from_static(b"welcome!"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_request_response_json() {
|
||||
let app = init_service(App::new().service(web::resource("/people").route(
|
||||
web::post().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
|
||||
)))
|
||||
.await;
|
||||
|
||||
let payload = Person {
|
||||
id: "12345".to_string(),
|
||||
name: "User name".to_string(),
|
||||
};
|
||||
|
||||
let req = TestRequest::post()
|
||||
.uri("/people")
|
||||
.set_json(&payload)
|
||||
.to_request();
|
||||
|
||||
assert_eq!(req.content_type(), "application/json");
|
||||
|
||||
let result: Person = call_and_read_body_json(&app, req).await;
|
||||
assert_eq!(&result.id, "12345");
|
||||
assert_eq!(&result.name, "User name");
|
||||
}
|
||||
}
|
@@ -20,6 +20,8 @@ use crate::{
|
||||
|
||||
/// Combines two extractor or responder types into a single type.
|
||||
///
|
||||
/// Can be converted to and from an [`either::Either`].
|
||||
///
|
||||
/// # Extractor
|
||||
/// Provides a mechanism for trying two extractors, a primary and a fallback. Useful for
|
||||
/// "polymorphic payloads" where, for example, a form might be JSON or URL encoded.
|
||||
|
@@ -449,13 +449,12 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
body,
|
||||
error::InternalError,
|
||||
http::{
|
||||
header::{self, CONTENT_LENGTH, CONTENT_TYPE},
|
||||
StatusCode,
|
||||
},
|
||||
test::{assert_body_eq, TestRequest},
|
||||
test::{assert_body_eq, load_body, TestRequest},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
@@ -518,7 +517,7 @@ mod tests {
|
||||
let resp = HttpResponse::from_error(s.err().unwrap());
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
let body = body::to_bytes(resp.into_body()).await.unwrap();
|
||||
let body = load_body(resp.into_body()).await.unwrap();
|
||||
let msg: MyObject = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(msg.name, "invalid request");
|
||||
}
|
||||
|
Reference in New Issue
Block a user