mirror of https://github.com/fafhrd91/actix-web
Merge branch 'master' into feat/awc_response_timeout
This commit is contained in:
commit
daa2c57773
|
@ -38,13 +38,19 @@ jobs:
|
||||||
with:
|
with:
|
||||||
command: generate-lockfile
|
command: generate-lockfile
|
||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.0.1
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
|
- name: Install cargo-hack
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: install
|
||||||
|
args: cargo-hack
|
||||||
|
|
||||||
- name: check minimal
|
- name: check minimal
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: check
|
command: hack
|
||||||
args: --workspace --no-default-features --tests
|
args: --clean-per-run check --workspace --no-default-features --tests
|
||||||
|
|
||||||
- name: check full
|
- name: check full
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
|
@ -81,7 +87,7 @@ jobs:
|
||||||
&& github.ref == 'refs/heads/master'
|
&& github.ref == 'refs/heads/master'
|
||||||
run: |
|
run: |
|
||||||
cargo install cargo-tarpaulin --vers "^0.13"
|
cargo install cargo-tarpaulin --vers "^0.13"
|
||||||
cargo tarpaulin --out Xml
|
cargo tarpaulin --out Xml --verbose
|
||||||
- name: Upload to Codecov
|
- name: Upload to Codecov
|
||||||
if: >
|
if: >
|
||||||
matrix.target.os == 'ubuntu-latest'
|
matrix.target.os == 'ubuntu-latest'
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
### Changed
|
||||||
|
* Feature `cookies` is now optional and enabled by default. [#1981]
|
||||||
|
|
||||||
|
[#1981]: https://github.com/actix/actix-web/pull/1981
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0-beta.3 - 2021-02-10
|
## 4.0.0-beta.3 - 2021-02-10
|
||||||
|
|
27
Cargo.toml
27
Cargo.toml
|
@ -15,6 +15,7 @@ license = "MIT OR Apache-2.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
|
# features that docs.rs will build with
|
||||||
features = ["openssl", "rustls", "compress", "secure-cookies"]
|
features = ["openssl", "rustls", "compress", "secure-cookies"]
|
||||||
|
|
||||||
[badges]
|
[badges]
|
||||||
|
@ -38,12 +39,15 @@ members = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["compress"]
|
default = ["compress", "cookies"]
|
||||||
|
|
||||||
# content-encoding support
|
# content-encoding support
|
||||||
compress = ["actix-http/compress", "awc/compress"]
|
compress = ["actix-http/compress", "awc/compress"]
|
||||||
|
|
||||||
# sessions feature
|
# support for cookies
|
||||||
|
cookies = ["actix-http/cookies", "awc/cookies"]
|
||||||
|
|
||||||
|
# secure cookies feature
|
||||||
secure-cookies = ["actix-http/secure-cookies"]
|
secure-cookies = ["actix-http/secure-cookies"]
|
||||||
|
|
||||||
# openssl
|
# openssl
|
||||||
|
@ -95,17 +99,17 @@ futures-core = { version = "0.3.7", default-features = false }
|
||||||
futures-util = { version = "0.3.7", default-features = false }
|
futures-util = { version = "0.3.7", default-features = false }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
socket2 = "0.3.16"
|
|
||||||
pin-project = "1.0.0"
|
pin-project = "1.0.0"
|
||||||
regex = "1.4"
|
regex = "1.4"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
|
smallvec = "1.6"
|
||||||
|
socket2 = "0.3.16"
|
||||||
time = { version = "0.2.23", default-features = false, features = ["std"] }
|
time = { version = "0.2.23", default-features = false, features = ["std"] }
|
||||||
url = "2.1"
|
|
||||||
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
|
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
|
||||||
tls-rustls = { package = "rustls", version = "0.19.0", optional = true }
|
tls-rustls = { package = "rustls", version = "0.19.0", optional = true }
|
||||||
smallvec = "1.6"
|
url = "2.1"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies.tls-openssl]
|
[target.'cfg(windows)'.dependencies.tls-openssl]
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
|
@ -114,16 +118,13 @@ features = ["vendored"]
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix = { version = "0.11.0-beta.2", default-features = false }
|
|
||||||
rand = "0.8"
|
|
||||||
env_logger = "0.8"
|
|
||||||
serde_derive = "1.0"
|
|
||||||
brotli2 = "0.3.2"
|
brotli2 = "0.3.2"
|
||||||
flate2 = "1.0.13"
|
|
||||||
criterion = "0.3"
|
criterion = "0.3"
|
||||||
|
env_logger = "0.8"
|
||||||
[profile.dev]
|
flate2 = "1.0.13"
|
||||||
debug = false
|
rand = "0.8"
|
||||||
|
rcgen = "0.8"
|
||||||
|
serde_derive = "1.0"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|
|
@ -25,6 +25,7 @@ bitflags = "1"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
futures-core = { version = "0.3.7", default-features = false }
|
futures-core = { version = "0.3.7", default-features = false }
|
||||||
futures-util = { version = "0.3.7", default-features = false }
|
futures-util = { version = "0.3.7", default-features = false }
|
||||||
|
http-range = "0.1.4"
|
||||||
derive_more = "0.99.5"
|
derive_more = "0.99.5"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
|
|
|
@ -10,9 +10,6 @@ pub struct HttpRange {
|
||||||
pub length: u64,
|
pub length: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
const PREFIX: &str = "bytes=";
|
|
||||||
const PREFIX_LEN: usize = 6;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Display, Error)]
|
#[derive(Debug, Clone, Display, Error)]
|
||||||
#[display(fmt = "Parse HTTP Range failed")]
|
#[display(fmt = "Parse HTTP Range failed")]
|
||||||
pub struct ParseRangeErr(#[error(not(source))] ());
|
pub struct ParseRangeErr(#[error(not(source))] ());
|
||||||
|
@ -23,82 +20,16 @@ impl HttpRange {
|
||||||
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
|
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
|
||||||
/// `size` is full size of response (file).
|
/// `size` is full size of response (file).
|
||||||
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> {
|
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> {
|
||||||
if header.is_empty() {
|
match http_range::HttpRange::parse(header, size) {
|
||||||
return Ok(Vec::new());
|
Ok(ranges) => Ok(ranges
|
||||||
|
.iter()
|
||||||
|
.map(|range| HttpRange {
|
||||||
|
start: range.start,
|
||||||
|
length: range.length,
|
||||||
|
})
|
||||||
|
.collect()),
|
||||||
|
Err(_) => Err(ParseRangeErr(())),
|
||||||
}
|
}
|
||||||
if !header.starts_with(PREFIX) {
|
|
||||||
return Err(ParseRangeErr(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let size_sig = size as i64;
|
|
||||||
let mut no_overlap = false;
|
|
||||||
|
|
||||||
let all_ranges: Vec<Option<HttpRange>> = header[PREFIX_LEN..]
|
|
||||||
.split(',')
|
|
||||||
.map(|x| x.trim())
|
|
||||||
.filter(|x| !x.is_empty())
|
|
||||||
.map(|ra| {
|
|
||||||
let mut start_end_iter = ra.split('-');
|
|
||||||
|
|
||||||
let start_str = start_end_iter.next().ok_or(ParseRangeErr(()))?.trim();
|
|
||||||
let end_str = start_end_iter.next().ok_or(ParseRangeErr(()))?.trim();
|
|
||||||
|
|
||||||
if start_str.is_empty() {
|
|
||||||
// If no start is specified, end specifies the
|
|
||||||
// range start relative to the end of the file.
|
|
||||||
let mut length: i64 = end_str.parse().map_err(|_| ParseRangeErr(()))?;
|
|
||||||
|
|
||||||
if length > size_sig {
|
|
||||||
length = size_sig;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(HttpRange {
|
|
||||||
start: (size_sig - length) as u64,
|
|
||||||
length: length as u64,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
let start: i64 = start_str.parse().map_err(|_| ParseRangeErr(()))?;
|
|
||||||
|
|
||||||
if start < 0 {
|
|
||||||
return Err(ParseRangeErr(()));
|
|
||||||
}
|
|
||||||
if start >= size_sig {
|
|
||||||
no_overlap = true;
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let length = if end_str.is_empty() {
|
|
||||||
// If no end is specified, range extends to end of the file.
|
|
||||||
size_sig - start
|
|
||||||
} else {
|
|
||||||
let mut end: i64 = end_str.parse().map_err(|_| ParseRangeErr(()))?;
|
|
||||||
|
|
||||||
if start > end {
|
|
||||||
return Err(ParseRangeErr(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if end >= size_sig {
|
|
||||||
end = size_sig - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
end - start + 1
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(HttpRange {
|
|
||||||
start: start as u64,
|
|
||||||
length: length as u64,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Result<_, _>>()?;
|
|
||||||
|
|
||||||
let ranges: Vec<HttpRange> = all_ranges.into_iter().filter_map(|x| x).collect();
|
|
||||||
|
|
||||||
if no_overlap && ranges.is_empty() {
|
|
||||||
return Err(ParseRangeErr(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ranges)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
### Changed
|
||||||
|
* Feature `cookies` is now optional and disabled by default. [#1981]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994]
|
||||||
|
* `ResponseError` impl for `futures_channel::oneshot::Canceled` is removed. [#1994]
|
||||||
|
|
||||||
|
[#1981]: https://github.com/actix/actix-web/pull/1981
|
||||||
|
[#1994]: https://github.com/actix/actix-web/pull/1994
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.3 - 2021-02-10
|
## 3.0.0-beta.3 - 2021-02-10
|
||||||
|
|
|
@ -15,7 +15,8 @@ license = "MIT OR Apache-2.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
features = ["openssl", "rustls", "compress", "secure-cookies"]
|
# features that docs.rs will build with
|
||||||
|
features = ["openssl", "rustls", "compress", "cookies", "secure-cookies"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "actix_http"
|
name = "actix_http"
|
||||||
|
@ -30,11 +31,14 @@ openssl = ["actix-tls/openssl"]
|
||||||
# rustls support
|
# rustls support
|
||||||
rustls = ["actix-tls/rustls"]
|
rustls = ["actix-tls/rustls"]
|
||||||
|
|
||||||
# enable compressison support
|
# enable compression support
|
||||||
compress = ["flate2", "brotli2"]
|
compress = ["flate2", "brotli2"]
|
||||||
|
|
||||||
|
# support for cookies
|
||||||
|
cookies = ["cookie"]
|
||||||
|
|
||||||
# support for secure cookies
|
# support for secure cookies
|
||||||
secure-cookies = ["cookie/secure"]
|
secure-cookies = ["cookies", "cookie/secure"]
|
||||||
|
|
||||||
# trust-dns as client dns resolver
|
# trust-dns as client dns resolver
|
||||||
trust-dns = ["trust-dns-resolver"]
|
trust-dns = ["trust-dns-resolver"]
|
||||||
|
@ -46,24 +50,23 @@ actix-utils = "3.0.0-beta.2"
|
||||||
actix-rt = "2"
|
actix-rt = "2"
|
||||||
actix-tls = "3.0.0-beta.2"
|
actix-tls = "3.0.0-beta.2"
|
||||||
|
|
||||||
|
ahash = "0.7"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
bitflags = "1.2"
|
bitflags = "1.2"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
bytestring = "1"
|
bytestring = "1"
|
||||||
cookie = { version = "0.14.1", features = ["percent-encode"] }
|
cfg-if = "1"
|
||||||
|
cookie = { version = "0.14.1", features = ["percent-encode"], optional = true }
|
||||||
derive_more = "0.99.5"
|
derive_more = "0.99.5"
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
futures-channel = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
|
||||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
|
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
|
||||||
ahash = "0.7"
|
|
||||||
h2 = "0.3.0"
|
h2 = "0.3.0"
|
||||||
http = "0.2.2"
|
http = "0.2.2"
|
||||||
httparse = "1.3"
|
httparse = "1.3"
|
||||||
indexmap = "1.3"
|
|
||||||
itoa = "0.4"
|
itoa = "0.4"
|
||||||
lazy_static = "1.4"
|
|
||||||
language-tags = "0.2"
|
language-tags = "0.2"
|
||||||
|
lazy_static = "1.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
|
@ -72,11 +75,11 @@ rand = "0.8"
|
||||||
regex = "1.3"
|
regex = "1.3"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
serde_urlencoded = "0.7"
|
||||||
sha-1 = "0.9"
|
sha-1 = "0.9"
|
||||||
smallvec = "1.6"
|
smallvec = "1.6"
|
||||||
slab = "0.4"
|
|
||||||
serde_urlencoded = "0.7"
|
|
||||||
time = { version = "0.2.23", default-features = false, features = ["std"] }
|
time = { version = "0.2.23", default-features = false, features = ["std"] }
|
||||||
|
tokio = { version = "1.2", features = ["sync"] }
|
||||||
|
|
||||||
# compression
|
# compression
|
||||||
brotli2 = { version="0.3.2", optional = true }
|
brotli2 = { version="0.3.2", optional = true }
|
||||||
|
@ -90,6 +93,7 @@ actix-http-test = { version = "3.0.0-beta.2", features = ["openssl"] }
|
||||||
actix-tls = { version = "3.0.0-beta.2", features = ["openssl"] }
|
actix-tls = { version = "3.0.0-beta.2", features = ["openssl"] }
|
||||||
criterion = "0.3"
|
criterion = "0.3"
|
||||||
env_logger = "0.8"
|
env_logger = "0.8"
|
||||||
|
rcgen = "0.8"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
tls-openssl = { version = "0.10", package = "openssl" }
|
tls-openssl = { version = "0.10", package = "openssl" }
|
||||||
tls-rustls = { version = "0.19", package = "rustls" }
|
tls-rustls = { version = "0.19", package = "rustls" }
|
||||||
|
|
|
@ -1,714 +0,0 @@
|
||||||
//! Traits and structures to aid consuming and writing HTTP payloads.
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
fmt, mem,
|
|
||||||
pin::Pin,
|
|
||||||
task::{Context, Poll},
|
|
||||||
};
|
|
||||||
|
|
||||||
use bytes::{Bytes, BytesMut};
|
|
||||||
use futures_core::{ready, Stream};
|
|
||||||
use pin_project::pin_project;
|
|
||||||
|
|
||||||
use crate::error::Error;
|
|
||||||
|
|
||||||
/// Body size hint.
|
|
||||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
|
||||||
pub enum BodySize {
|
|
||||||
None,
|
|
||||||
Empty,
|
|
||||||
Sized(u64),
|
|
||||||
Stream,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BodySize {
|
|
||||||
pub fn is_eof(&self) -> bool {
|
|
||||||
matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type that implement this trait can be streamed to a peer.
|
|
||||||
pub trait MessageBody {
|
|
||||||
fn size(&self) -> BodySize;
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>>;
|
|
||||||
|
|
||||||
downcast_get_type_id!();
|
|
||||||
}
|
|
||||||
|
|
||||||
downcast!(MessageBody);
|
|
||||||
|
|
||||||
impl MessageBody for () {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Empty
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
Poll::Ready(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: MessageBody + Unpin> MessageBody for Box<T> {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
self.as_ref().size()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
Pin::new(self.get_mut().as_mut()).poll_next(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pin_project(project = ResponseBodyProj)]
|
|
||||||
pub enum ResponseBody<B> {
|
|
||||||
Body(#[pin] B),
|
|
||||||
Other(Body),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResponseBody<Body> {
|
|
||||||
pub fn into_body<B>(self) -> ResponseBody<B> {
|
|
||||||
match self {
|
|
||||||
ResponseBody::Body(b) => ResponseBody::Other(b),
|
|
||||||
ResponseBody::Other(b) => ResponseBody::Other(b),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B> ResponseBody<B> {
|
|
||||||
pub fn take_body(&mut self) -> ResponseBody<B> {
|
|
||||||
mem::replace(self, ResponseBody::Other(Body::None))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B: MessageBody> ResponseBody<B> {
|
|
||||||
pub fn as_ref(&self) -> Option<&B> {
|
|
||||||
if let ResponseBody::Body(ref b) = self {
|
|
||||||
Some(b)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B: MessageBody> MessageBody for ResponseBody<B> {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
match self {
|
|
||||||
ResponseBody::Body(ref body) => body.size(),
|
|
||||||
ResponseBody::Other(ref body) => body.size(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
match self.project() {
|
|
||||||
ResponseBodyProj::Body(body) => body.poll_next(cx),
|
|
||||||
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B: MessageBody> Stream for ResponseBody<B> {
|
|
||||||
type Item = Result<Bytes, Error>;
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Self::Item>> {
|
|
||||||
match self.project() {
|
|
||||||
ResponseBodyProj::Body(body) => body.poll_next(cx),
|
|
||||||
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents various types of HTTP message body.
|
|
||||||
pub enum Body {
|
|
||||||
/// Empty response. `Content-Length` header is not set.
|
|
||||||
None,
|
|
||||||
/// Zero sized response body. `Content-Length` header is set to `0`.
|
|
||||||
Empty,
|
|
||||||
/// Specific response body.
|
|
||||||
Bytes(Bytes),
|
|
||||||
/// Generic message body.
|
|
||||||
Message(Box<dyn MessageBody + Unpin>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Body {
|
|
||||||
/// Create body from slice (copy)
|
|
||||||
pub fn from_slice(s: &[u8]) -> Body {
|
|
||||||
Body::Bytes(Bytes::copy_from_slice(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create body from generic message body.
|
|
||||||
pub fn from_message<B: MessageBody + Unpin + 'static>(body: B) -> Body {
|
|
||||||
Body::Message(Box::new(body))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for Body {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
match self {
|
|
||||||
Body::None => BodySize::None,
|
|
||||||
Body::Empty => BodySize::Empty,
|
|
||||||
Body::Bytes(ref bin) => BodySize::Sized(bin.len() as u64),
|
|
||||||
Body::Message(ref body) => body.size(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
match self.get_mut() {
|
|
||||||
Body::None => Poll::Ready(None),
|
|
||||||
Body::Empty => Poll::Ready(None),
|
|
||||||
Body::Bytes(ref mut bin) => {
|
|
||||||
let len = bin.len();
|
|
||||||
if len == 0 {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
Poll::Ready(Some(Ok(mem::take(bin))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Body::Message(body) => Pin::new(&mut **body).poll_next(cx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for Body {
|
|
||||||
fn eq(&self, other: &Body) -> bool {
|
|
||||||
match *self {
|
|
||||||
Body::None => matches!(*other, Body::None),
|
|
||||||
Body::Empty => matches!(*other, Body::Empty),
|
|
||||||
Body::Bytes(ref b) => match *other {
|
|
||||||
Body::Bytes(ref b2) => b == b2,
|
|
||||||
_ => false,
|
|
||||||
},
|
|
||||||
Body::Message(_) => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for Body {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
Body::None => write!(f, "Body::None"),
|
|
||||||
Body::Empty => write!(f, "Body::Empty"),
|
|
||||||
Body::Bytes(ref b) => write!(f, "Body::Bytes({:?})", b),
|
|
||||||
Body::Message(_) => write!(f, "Body::Message(_)"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&'static str> for Body {
|
|
||||||
fn from(s: &'static str) -> Body {
|
|
||||||
Body::Bytes(Bytes::from_static(s.as_ref()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&'static [u8]> for Body {
|
|
||||||
fn from(s: &'static [u8]) -> Body {
|
|
||||||
Body::Bytes(Bytes::from_static(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Vec<u8>> for Body {
|
|
||||||
fn from(vec: Vec<u8>) -> Body {
|
|
||||||
Body::Bytes(Bytes::from(vec))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for Body {
|
|
||||||
fn from(s: String) -> Body {
|
|
||||||
s.into_bytes().into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<&'a String> for Body {
|
|
||||||
fn from(s: &'a String) -> Body {
|
|
||||||
Body::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Bytes> for Body {
|
|
||||||
fn from(s: Bytes) -> Body {
|
|
||||||
Body::Bytes(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<BytesMut> for Body {
|
|
||||||
fn from(s: BytesMut) -> Body {
|
|
||||||
Body::Bytes(s.freeze())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<serde_json::Value> for Body {
|
|
||||||
fn from(v: serde_json::Value) -> Body {
|
|
||||||
Body::Bytes(v.to_string().into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> From<SizedStream<S>> for Body
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, Error>> + Unpin + 'static,
|
|
||||||
{
|
|
||||||
fn from(s: SizedStream<S>) -> Body {
|
|
||||||
Body::from_message(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, E> From<BodyStream<S>> for Body
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
|
|
||||||
E: Into<Error> + 'static,
|
|
||||||
{
|
|
||||||
fn from(s: BodyStream<S>) -> Body {
|
|
||||||
Body::from_message(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for Bytes {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Sized(self.len() as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
if self.is_empty() {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for BytesMut {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Sized(self.len() as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
if self.is_empty() {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for &'static str {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Sized(self.len() as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
if self.is_empty() {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
Poll::Ready(Some(Ok(Bytes::from_static(
|
|
||||||
mem::take(self.get_mut()).as_ref(),
|
|
||||||
))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for Vec<u8> {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Sized(self.len() as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
if self.is_empty() {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
Poll::Ready(Some(Ok(Bytes::from(mem::take(self.get_mut())))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for String {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Sized(self.len() as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
if self.is_empty() {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
Poll::Ready(Some(Ok(Bytes::from(
|
|
||||||
mem::take(self.get_mut()).into_bytes(),
|
|
||||||
))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type represent streaming body.
|
|
||||||
/// Response does not contain `content-length` header and appropriate transfer encoding is used.
|
|
||||||
pub struct BodyStream<S: Unpin> {
|
|
||||||
stream: S,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, E> BodyStream<S>
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, E>> + Unpin,
|
|
||||||
E: Into<Error>,
|
|
||||||
{
|
|
||||||
pub fn new(stream: S) -> Self {
|
|
||||||
BodyStream { stream }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, E> MessageBody for BodyStream<S>
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, E>> + Unpin,
|
|
||||||
E: Into<Error>,
|
|
||||||
{
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Stream
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to pull out the next value of the underlying [`Stream`].
|
|
||||||
///
|
|
||||||
/// Empty values are skipped to prevent [`BodyStream`]'s transmission being
|
|
||||||
/// ended on a zero-length chunk, but rather proceed until the underlying
|
|
||||||
/// [`Stream`] ends.
|
|
||||||
fn poll_next(
|
|
||||||
mut self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
loop {
|
|
||||||
let stream = &mut self.as_mut().stream;
|
|
||||||
return Poll::Ready(match ready!(Pin::new(stream).poll_next(cx)) {
|
|
||||||
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
|
|
||||||
opt => opt.map(|res| res.map_err(Into::into)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type represent streaming body. This body implementation should be used
|
|
||||||
/// if total size of stream is known. Data get sent as is without using transfer encoding.
|
|
||||||
pub struct SizedStream<S: Unpin> {
|
|
||||||
size: u64,
|
|
||||||
stream: S,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> SizedStream<S>
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, Error>> + Unpin,
|
|
||||||
{
|
|
||||||
pub fn new(size: u64, stream: S) -> Self {
|
|
||||||
SizedStream { size, stream }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> MessageBody for SizedStream<S>
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, Error>> + Unpin,
|
|
||||||
{
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Sized(self.size as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to pull out the next value of the underlying [`Stream`].
|
|
||||||
///
|
|
||||||
/// Empty values are skipped to prevent [`SizedStream`]'s transmission being
|
|
||||||
/// ended on a zero-length chunk, but rather proceed until the underlying
|
|
||||||
/// [`Stream`] ends.
|
|
||||||
fn poll_next(
|
|
||||||
mut self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
loop {
|
|
||||||
let stream = &mut self.as_mut().stream;
|
|
||||||
return Poll::Ready(match ready!(Pin::new(stream).poll_next(cx)) {
|
|
||||||
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
|
|
||||||
val => val,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use futures_util::future::poll_fn;
|
|
||||||
use futures_util::pin_mut;
|
|
||||||
use futures_util::stream;
|
|
||||||
|
|
||||||
impl Body {
|
|
||||||
pub(crate) fn get_ref(&self) -> &[u8] {
|
|
||||||
match *self {
|
|
||||||
Body::Bytes(ref bin) => &bin,
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResponseBody<Body> {
|
|
||||||
pub(crate) fn get_ref(&self) -> &[u8] {
|
|
||||||
match *self {
|
|
||||||
ResponseBody::Body(ref b) => b.get_ref(),
|
|
||||||
ResponseBody::Other(ref b) => b.get_ref(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_static_str() {
|
|
||||||
assert_eq!(Body::from("").size(), BodySize::Sized(0));
|
|
||||||
assert_eq!(Body::from("test").size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from("test").get_ref(), b"test");
|
|
||||||
|
|
||||||
assert_eq!("test".size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| Pin::new(&mut "test").poll_next(cx))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_static_bytes() {
|
|
||||||
assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test");
|
|
||||||
assert_eq!(
|
|
||||||
Body::from_slice(b"test".as_ref()).size(),
|
|
||||||
BodySize::Sized(4)
|
|
||||||
);
|
|
||||||
assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test");
|
|
||||||
let sb = Bytes::from(&b"test"[..]);
|
|
||||||
pin_mut!(sb);
|
|
||||||
|
|
||||||
assert_eq!(sb.size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| sb.as_mut().poll_next(cx)).await.unwrap().ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_vec() {
|
|
||||||
assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test");
|
|
||||||
let test_vec = Vec::from("test");
|
|
||||||
pin_mut!(test_vec);
|
|
||||||
|
|
||||||
assert_eq!(test_vec.size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| test_vec.as_mut().poll_next(cx))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_bytes() {
|
|
||||||
let b = Bytes::from("test");
|
|
||||||
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
|
||||||
pin_mut!(b);
|
|
||||||
|
|
||||||
assert_eq!(b.size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_bytes_mut() {
|
|
||||||
let b = BytesMut::from("test");
|
|
||||||
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
|
||||||
pin_mut!(b);
|
|
||||||
|
|
||||||
assert_eq!(b.size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_string() {
|
|
||||||
let b = "test".to_owned();
|
|
||||||
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
|
||||||
assert_eq!(Body::from(&b).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(&b).get_ref(), b"test");
|
|
||||||
pin_mut!(b);
|
|
||||||
|
|
||||||
assert_eq!(b.size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_unit() {
|
|
||||||
assert_eq!(().size(), BodySize::Empty);
|
|
||||||
assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx))
|
|
||||||
.await
|
|
||||||
.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_box() {
|
|
||||||
let val = Box::new(());
|
|
||||||
pin_mut!(val);
|
|
||||||
assert_eq!(val.size(), BodySize::Empty);
|
|
||||||
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_body_eq() {
|
|
||||||
assert!(
|
|
||||||
Body::Bytes(Bytes::from_static(b"1"))
|
|
||||||
== Body::Bytes(Bytes::from_static(b"1"))
|
|
||||||
);
|
|
||||||
assert!(Body::Bytes(Bytes::from_static(b"1")) != Body::None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_body_debug() {
|
|
||||||
assert!(format!("{:?}", Body::None).contains("Body::None"));
|
|
||||||
assert!(format!("{:?}", Body::Empty).contains("Body::Empty"));
|
|
||||||
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1'));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_serde_json() {
|
|
||||||
use serde_json::json;
|
|
||||||
assert_eq!(
|
|
||||||
Body::from(serde_json::Value::String("test".into())).size(),
|
|
||||||
BodySize::Sized(6)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Body::from(json!({"test-key":"test-value"})).size(),
|
|
||||||
BodySize::Sized(25)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
mod body_stream {
|
|
||||||
use super::*;
|
|
||||||
//use futures::task::noop_waker;
|
|
||||||
//use futures::stream::once;
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn skips_empty_chunks() {
|
|
||||||
let body = BodyStream::new(stream::iter(
|
|
||||||
["1", "", "2"]
|
|
||||||
.iter()
|
|
||||||
.map(|&v| Ok(Bytes::from(v)) as Result<Bytes, ()>),
|
|
||||||
));
|
|
||||||
pin_mut!(body);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| body.as_mut().poll_next(cx))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.ok(),
|
|
||||||
Some(Bytes::from("1")),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| body.as_mut().poll_next(cx))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.ok(),
|
|
||||||
Some(Bytes::from("2")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Now it does not compile as it should
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn move_pinned_pointer() {
|
|
||||||
let (sender, receiver) = futures::channel::oneshot::channel();
|
|
||||||
let mut body_stream = Ok(BodyStream::new(once(async {
|
|
||||||
let x = Box::new(0i32);
|
|
||||||
let y = &x;
|
|
||||||
receiver.await.unwrap();
|
|
||||||
let _z = **y;
|
|
||||||
Ok::<_, ()>(Bytes::new())
|
|
||||||
})));
|
|
||||||
|
|
||||||
let waker = noop_waker();
|
|
||||||
let mut context = Context::from_waker(&waker);
|
|
||||||
pin_mut!(body_stream);
|
|
||||||
|
|
||||||
let _ = body_stream.as_mut().unwrap().poll_next(&mut context);
|
|
||||||
sender.send(()).unwrap();
|
|
||||||
let _ = std::mem::replace(&mut body_stream, Err([0; 32])).unwrap().poll_next(&mut context);
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
||||||
mod sized_stream {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn skips_empty_chunks() {
|
|
||||||
let body = SizedStream::new(
|
|
||||||
2,
|
|
||||||
stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))),
|
|
||||||
);
|
|
||||||
pin_mut!(body);
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| body.as_mut().poll_next(cx))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.ok(),
|
|
||||||
Some(Bytes::from("1")),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| body.as_mut().poll_next(cx))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.ok(),
|
|
||||||
Some(Bytes::from("2")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_body_casting() {
|
|
||||||
let mut body = String::from("hello cast");
|
|
||||||
let resp_body: &mut dyn MessageBody = &mut body;
|
|
||||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
|
||||||
assert_eq!(body, "hello cast");
|
|
||||||
let body = &mut resp_body.downcast_mut::<String>().unwrap();
|
|
||||||
body.push('!');
|
|
||||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
|
||||||
assert_eq!(body, "hello cast!");
|
|
||||||
let not_body = resp_body.downcast_ref::<()>();
|
|
||||||
assert!(not_body.is_none());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
use std::{
|
||||||
|
fmt, mem,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
|
use futures_core::Stream;
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
use super::{BodySize, BodyStream, MessageBody, SizedStream};
|
||||||
|
|
||||||
|
/// Represents various types of HTTP message body.
|
||||||
|
pub enum Body {
|
||||||
|
/// Empty response. `Content-Length` header is not set.
|
||||||
|
None,
|
||||||
|
/// Zero sized response body. `Content-Length` header is set to `0`.
|
||||||
|
Empty,
|
||||||
|
/// Specific response body.
|
||||||
|
Bytes(Bytes),
|
||||||
|
/// Generic message body.
|
||||||
|
Message(Box<dyn MessageBody + Unpin>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Body {
|
||||||
|
/// Create body from slice (copy)
|
||||||
|
pub fn from_slice(s: &[u8]) -> Body {
|
||||||
|
Body::Bytes(Bytes::copy_from_slice(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create body from generic message body.
|
||||||
|
pub fn from_message<B: MessageBody + Unpin + 'static>(body: B) -> Body {
|
||||||
|
Body::Message(Box::new(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for Body {
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
match self {
|
||||||
|
Body::None => BodySize::None,
|
||||||
|
Body::Empty => BodySize::Empty,
|
||||||
|
Body::Bytes(ref bin) => BodySize::Sized(bin.len() as u64),
|
||||||
|
Body::Message(ref body) => body.size(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||||
|
match self.get_mut() {
|
||||||
|
Body::None => Poll::Ready(None),
|
||||||
|
Body::Empty => Poll::Ready(None),
|
||||||
|
Body::Bytes(ref mut bin) => {
|
||||||
|
let len = bin.len();
|
||||||
|
if len == 0 {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
Poll::Ready(Some(Ok(mem::take(bin))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Body::Message(body) => Pin::new(&mut **body).poll_next(cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Body {
|
||||||
|
fn eq(&self, other: &Body) -> bool {
|
||||||
|
match *self {
|
||||||
|
Body::None => matches!(*other, Body::None),
|
||||||
|
Body::Empty => matches!(*other, Body::Empty),
|
||||||
|
Body::Bytes(ref b) => match *other {
|
||||||
|
Body::Bytes(ref b2) => b == b2,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
Body::Message(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Body {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Body::None => write!(f, "Body::None"),
|
||||||
|
Body::Empty => write!(f, "Body::Empty"),
|
||||||
|
Body::Bytes(ref b) => write!(f, "Body::Bytes({:?})", b),
|
||||||
|
Body::Message(_) => write!(f, "Body::Message(_)"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&'static str> for Body {
|
||||||
|
fn from(s: &'static str) -> Body {
|
||||||
|
Body::Bytes(Bytes::from_static(s.as_ref()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&'static [u8]> for Body {
|
||||||
|
fn from(s: &'static [u8]) -> Body {
|
||||||
|
Body::Bytes(Bytes::from_static(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<u8>> for Body {
|
||||||
|
fn from(vec: Vec<u8>) -> Body {
|
||||||
|
Body::Bytes(Bytes::from(vec))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Body {
|
||||||
|
fn from(s: String) -> Body {
|
||||||
|
s.into_bytes().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a String> for Body {
|
||||||
|
fn from(s: &'a String) -> Body {
|
||||||
|
Body::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Bytes> for Body {
|
||||||
|
fn from(s: Bytes) -> Body {
|
||||||
|
Body::Bytes(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BytesMut> for Body {
|
||||||
|
fn from(s: BytesMut) -> Body {
|
||||||
|
Body::Bytes(s.freeze())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Value> for Body {
|
||||||
|
fn from(v: serde_json::Value) -> Body {
|
||||||
|
Body::Bytes(v.to_string().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> From<SizedStream<S>> for Body
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, Error>> + Unpin + 'static,
|
||||||
|
{
|
||||||
|
fn from(s: SizedStream<S>) -> Body {
|
||||||
|
Body::from_message(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, E> From<BodyStream<S>> for Body
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
|
||||||
|
E: Into<Error> + 'static,
|
||||||
|
{
|
||||||
|
fn from(s: BodyStream<S>) -> Body {
|
||||||
|
Body::from_message(s)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
use std::{
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures_core::{ready, Stream};
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
use super::{BodySize, MessageBody};
|
||||||
|
|
||||||
|
/// Streaming response wrapper.
|
||||||
|
///
|
||||||
|
/// Response does not contain `Content-Length` header and appropriate transfer encoding is used.
|
||||||
|
pub struct BodyStream<S: Unpin> {
|
||||||
|
stream: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, E> BodyStream<S>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, E>> + Unpin,
|
||||||
|
E: Into<Error>,
|
||||||
|
{
|
||||||
|
pub fn new(stream: S) -> Self {
|
||||||
|
BodyStream { stream }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, E> MessageBody for BodyStream<S>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, E>> + Unpin,
|
||||||
|
E: Into<Error>,
|
||||||
|
{
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Stream
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to pull out the next value of the underlying [`Stream`].
|
||||||
|
///
|
||||||
|
/// Empty values are skipped to prevent [`BodyStream`]'s transmission being
|
||||||
|
/// ended on a zero-length chunk, but rather proceed until the underlying
|
||||||
|
/// [`Stream`] ends.
|
||||||
|
fn poll_next(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||||
|
loop {
|
||||||
|
let stream = &mut self.as_mut().stream;
|
||||||
|
|
||||||
|
let chunk = match ready!(Pin::new(stream).poll_next(cx)) {
|
||||||
|
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
|
||||||
|
opt => opt.map(|res| res.map_err(Into::into)),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Poll::Ready(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
//! [`MessageBody`] trait and foreign implementations.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
mem,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
use super::BodySize;
|
||||||
|
|
||||||
|
/// Type that implement this trait can be streamed to a peer.
|
||||||
|
pub trait MessageBody {
|
||||||
|
fn size(&self) -> BodySize;
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Error>>>;
|
||||||
|
|
||||||
|
downcast_get_type_id!();
|
||||||
|
}
|
||||||
|
|
||||||
|
downcast!(MessageBody);
|
||||||
|
|
||||||
|
impl MessageBody for () {
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||||
|
Poll::Ready(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: MessageBody + Unpin> MessageBody for Box<T> {
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
self.as_ref().size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||||
|
Pin::new(self.get_mut().as_mut()).poll_next(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for Bytes {
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||||
|
if self.is_empty() {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for BytesMut {
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||||
|
if self.is_empty() {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for &'static str {
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||||
|
if self.is_empty() {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
Poll::Ready(Some(Ok(Bytes::from_static(
|
||||||
|
mem::take(self.get_mut()).as_ref(),
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for Vec<u8> {
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||||
|
if self.is_empty() {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
Poll::Ready(Some(Ok(Bytes::from(mem::take(self.get_mut())))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for String {
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||||
|
if self.is_empty() {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
Poll::Ready(Some(Ok(Bytes::from(
|
||||||
|
mem::take(self.get_mut()).into_bytes(),
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,252 @@
|
||||||
|
//! Traits and structures to aid consuming and writing HTTP payloads.
|
||||||
|
|
||||||
|
#[allow(clippy::module_inception)]
|
||||||
|
mod body;
|
||||||
|
mod body_stream;
|
||||||
|
mod message_body;
|
||||||
|
mod response_body;
|
||||||
|
mod size;
|
||||||
|
mod sized_stream;
|
||||||
|
|
||||||
|
pub use self::body::Body;
|
||||||
|
pub use self::body_stream::BodyStream;
|
||||||
|
pub use self::message_body::MessageBody;
|
||||||
|
pub use self::response_body::ResponseBody;
|
||||||
|
pub use self::size::BodySize;
|
||||||
|
pub use self::sized_stream::SizedStream;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
|
use actix_rt::pin;
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
|
use futures_util::{future::poll_fn, stream};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Body {
|
||||||
|
pub(crate) fn get_ref(&self) -> &[u8] {
|
||||||
|
match *self {
|
||||||
|
Body::Bytes(ref bin) => &bin,
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseBody<Body> {
|
||||||
|
pub(crate) fn get_ref(&self) -> &[u8] {
|
||||||
|
match *self {
|
||||||
|
ResponseBody::Body(ref b) => b.get_ref(),
|
||||||
|
ResponseBody::Other(ref b) => b.get_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_static_str() {
|
||||||
|
assert_eq!(Body::from("").size(), BodySize::Sized(0));
|
||||||
|
assert_eq!(Body::from("test").size(), BodySize::Sized(4));
|
||||||
|
assert_eq!(Body::from("test").get_ref(), b"test");
|
||||||
|
|
||||||
|
assert_eq!("test".size(), BodySize::Sized(4));
|
||||||
|
assert_eq!(
|
||||||
|
poll_fn(|cx| Pin::new(&mut "test").poll_next(cx))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.ok(),
|
||||||
|
Some(Bytes::from("test"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_static_bytes() {
|
||||||
|
assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4));
|
||||||
|
assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test");
|
||||||
|
assert_eq!(
|
||||||
|
Body::from_slice(b"test".as_ref()).size(),
|
||||||
|
BodySize::Sized(4)
|
||||||
|
);
|
||||||
|
assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test");
|
||||||
|
let sb = Bytes::from(&b"test"[..]);
|
||||||
|
pin!(sb);
|
||||||
|
|
||||||
|
assert_eq!(sb.size(), BodySize::Sized(4));
|
||||||
|
assert_eq!(
|
||||||
|
poll_fn(|cx| sb.as_mut().poll_next(cx)).await.unwrap().ok(),
|
||||||
|
Some(Bytes::from("test"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_vec() {
|
||||||
|
assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4));
|
||||||
|
assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test");
|
||||||
|
let test_vec = Vec::from("test");
|
||||||
|
pin!(test_vec);
|
||||||
|
|
||||||
|
assert_eq!(test_vec.size(), BodySize::Sized(4));
|
||||||
|
assert_eq!(
|
||||||
|
poll_fn(|cx| test_vec.as_mut().poll_next(cx))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.ok(),
|
||||||
|
Some(Bytes::from("test"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_bytes() {
|
||||||
|
let b = Bytes::from("test");
|
||||||
|
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
||||||
|
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
||||||
|
pin!(b);
|
||||||
|
|
||||||
|
assert_eq!(b.size(), BodySize::Sized(4));
|
||||||
|
assert_eq!(
|
||||||
|
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
|
||||||
|
Some(Bytes::from("test"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_bytes_mut() {
|
||||||
|
let b = BytesMut::from("test");
|
||||||
|
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
||||||
|
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
||||||
|
pin!(b);
|
||||||
|
|
||||||
|
assert_eq!(b.size(), BodySize::Sized(4));
|
||||||
|
assert_eq!(
|
||||||
|
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
|
||||||
|
Some(Bytes::from("test"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_string() {
|
||||||
|
let b = "test".to_owned();
|
||||||
|
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
||||||
|
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
||||||
|
assert_eq!(Body::from(&b).size(), BodySize::Sized(4));
|
||||||
|
assert_eq!(Body::from(&b).get_ref(), b"test");
|
||||||
|
pin!(b);
|
||||||
|
|
||||||
|
assert_eq!(b.size(), BodySize::Sized(4));
|
||||||
|
assert_eq!(
|
||||||
|
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
|
||||||
|
Some(Bytes::from("test"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_unit() {
|
||||||
|
assert_eq!(().size(), BodySize::Empty);
|
||||||
|
assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx))
|
||||||
|
.await
|
||||||
|
.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_box() {
|
||||||
|
let val = Box::new(());
|
||||||
|
pin!(val);
|
||||||
|
assert_eq!(val.size(), BodySize::Empty);
|
||||||
|
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_body_eq() {
|
||||||
|
assert!(
|
||||||
|
Body::Bytes(Bytes::from_static(b"1"))
|
||||||
|
== Body::Bytes(Bytes::from_static(b"1"))
|
||||||
|
);
|
||||||
|
assert!(Body::Bytes(Bytes::from_static(b"1")) != Body::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_body_debug() {
|
||||||
|
assert!(format!("{:?}", Body::None).contains("Body::None"));
|
||||||
|
assert!(format!("{:?}", Body::Empty).contains("Body::Empty"));
|
||||||
|
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_serde_json() {
|
||||||
|
use serde_json::json;
|
||||||
|
assert_eq!(
|
||||||
|
Body::from(serde_json::Value::String("test".into())).size(),
|
||||||
|
BodySize::Sized(6)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Body::from(json!({"test-key":"test-value"})).size(),
|
||||||
|
BodySize::Sized(25)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn body_stream_skips_empty_chunks() {
|
||||||
|
let body = BodyStream::new(stream::iter(
|
||||||
|
["1", "", "2"]
|
||||||
|
.iter()
|
||||||
|
.map(|&v| Ok(Bytes::from(v)) as Result<Bytes, ()>),
|
||||||
|
));
|
||||||
|
pin!(body);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
poll_fn(|cx| body.as_mut().poll_next(cx))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.ok(),
|
||||||
|
Some(Bytes::from("1")),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
poll_fn(|cx| body.as_mut().poll_next(cx))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.ok(),
|
||||||
|
Some(Bytes::from("2")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mod sized_stream {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn skips_empty_chunks() {
|
||||||
|
let body = SizedStream::new(
|
||||||
|
2,
|
||||||
|
stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))),
|
||||||
|
);
|
||||||
|
pin!(body);
|
||||||
|
assert_eq!(
|
||||||
|
poll_fn(|cx| body.as_mut().poll_next(cx))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.ok(),
|
||||||
|
Some(Bytes::from("1")),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
poll_fn(|cx| body.as_mut().poll_next(cx))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.ok(),
|
||||||
|
Some(Bytes::from("2")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_body_casting() {
|
||||||
|
let mut body = String::from("hello cast");
|
||||||
|
let resp_body: &mut dyn MessageBody = &mut body;
|
||||||
|
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||||
|
assert_eq!(body, "hello cast");
|
||||||
|
let body = &mut resp_body.downcast_mut::<String>().unwrap();
|
||||||
|
body.push('!');
|
||||||
|
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||||
|
assert_eq!(body, "hello cast!");
|
||||||
|
let not_body = resp_body.downcast_ref::<()>();
|
||||||
|
assert!(not_body.is_none());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
use std::{
|
||||||
|
mem,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures_core::Stream;
|
||||||
|
use pin_project::pin_project;
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
use super::{Body, BodySize, MessageBody};
|
||||||
|
|
||||||
|
#[pin_project(project = ResponseBodyProj)]
|
||||||
|
pub enum ResponseBody<B> {
|
||||||
|
Body(#[pin] B),
|
||||||
|
Other(Body),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseBody<Body> {
|
||||||
|
pub fn into_body<B>(self) -> ResponseBody<B> {
|
||||||
|
match self {
|
||||||
|
ResponseBody::Body(b) => ResponseBody::Other(b),
|
||||||
|
ResponseBody::Other(b) => ResponseBody::Other(b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B> ResponseBody<B> {
|
||||||
|
pub fn take_body(&mut self) -> ResponseBody<B> {
|
||||||
|
mem::replace(self, ResponseBody::Other(Body::None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: MessageBody> ResponseBody<B> {
|
||||||
|
pub fn as_ref(&self) -> Option<&B> {
|
||||||
|
if let ResponseBody::Body(ref b) = self {
|
||||||
|
Some(b)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: MessageBody> MessageBody for ResponseBody<B> {
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
match self {
|
||||||
|
ResponseBody::Body(ref body) => body.size(),
|
||||||
|
ResponseBody::Other(ref body) => body.size(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||||
|
match self.project() {
|
||||||
|
ResponseBodyProj::Body(body) => body.poll_next(cx),
|
||||||
|
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: MessageBody> Stream for ResponseBody<B> {
|
||||||
|
type Item = Result<Bytes, Error>;
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Self::Item>> {
|
||||||
|
match self.project() {
|
||||||
|
ResponseBodyProj::Body(body) => body.poll_next(cx),
|
||||||
|
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/// Body size hint.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum BodySize {
|
||||||
|
/// Absence of body can be assumed from method or status code.
|
||||||
|
///
|
||||||
|
/// Will skip writing Content-Length header.
|
||||||
|
None,
|
||||||
|
|
||||||
|
/// Zero size body.
|
||||||
|
///
|
||||||
|
/// Will write `Content-Length: 0` header.
|
||||||
|
Empty,
|
||||||
|
|
||||||
|
/// Known size body.
|
||||||
|
///
|
||||||
|
/// Will write `Content-Length: N` header. `Sized(0)` is treated the same as `Empty`.
|
||||||
|
Sized(u64),
|
||||||
|
|
||||||
|
/// Unknown size body.
|
||||||
|
///
|
||||||
|
/// Will not write Content-Length header. Can be used with chunked Transfer-Encoding.
|
||||||
|
Stream,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BodySize {
|
||||||
|
/// Returns true if size hint indicates no or empty body.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use actix_http::body::BodySize;
|
||||||
|
/// assert!(BodySize::None.is_eof());
|
||||||
|
/// assert!(BodySize::Empty.is_eof());
|
||||||
|
/// assert!(BodySize::Sized(0).is_eof());
|
||||||
|
///
|
||||||
|
/// assert!(!BodySize::Sized(64).is_eof());
|
||||||
|
/// assert!(!BodySize::Stream.is_eof());
|
||||||
|
/// ```
|
||||||
|
pub fn is_eof(&self) -> bool {
|
||||||
|
matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
use std::{
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures_core::{ready, Stream};
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
use super::{BodySize, MessageBody};
|
||||||
|
|
||||||
|
/// Known sized streaming response wrapper.
|
||||||
|
///
|
||||||
|
/// This body implementation should be used if total size of stream is known. Data get sent as is
|
||||||
|
/// without using transfer encoding.
|
||||||
|
pub struct SizedStream<S: Unpin> {
|
||||||
|
size: u64,
|
||||||
|
stream: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> SizedStream<S>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, Error>> + Unpin,
|
||||||
|
{
|
||||||
|
pub fn new(size: u64, stream: S) -> Self {
|
||||||
|
SizedStream { size, stream }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> MessageBody for SizedStream<S>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, Error>> + Unpin,
|
||||||
|
{
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.size as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to pull out the next value of the underlying [`Stream`].
|
||||||
|
///
|
||||||
|
/// Empty values are skipped to prevent [`SizedStream`]'s transmission being
|
||||||
|
/// ended on a zero-length chunk, but rather proceed until the underlying
|
||||||
|
/// [`Stream`] ends.
|
||||||
|
fn poll_next(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||||
|
loop {
|
||||||
|
let stream = &mut self.as_mut().stream;
|
||||||
|
|
||||||
|
let chunk = match ready!(Pin::new(stream).poll_next(cx)) {
|
||||||
|
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
|
||||||
|
val => val,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Poll::Ready(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
use std::future::Future;
|
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
|
@ -8,7 +7,6 @@ use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf};
|
||||||
use actix_rt::task::JoinHandle;
|
use actix_rt::task::JoinHandle;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
use futures_util::future::{err, Either, FutureExt, Ready};
|
|
||||||
use h2::client::SendRequest;
|
use h2::client::SendRequest;
|
||||||
use pin_project::pin_project;
|
use pin_project::pin_project;
|
||||||
|
|
||||||
|
@ -26,9 +24,10 @@ pub(crate) enum ConnectionType<Io> {
|
||||||
H2(H2Connection),
|
H2(H2Connection),
|
||||||
}
|
}
|
||||||
|
|
||||||
// h2 connection has two parts: SendRequest and Connection.
|
/// `H2Connection` has two parts: `SendRequest` and `Connection`.
|
||||||
// Connection is spawned as async task on runtime and H2Connection would hold a handle for
|
///
|
||||||
// this task. So it can wake up and quit the task when SendRequest is dropped.
|
/// `Connection` is spawned as an async task on runtime and `H2Connection` holds a handle for
|
||||||
|
/// this task. Therefore, it can wake up and quit the task when SendRequest is dropped.
|
||||||
pub(crate) struct H2Connection {
|
pub(crate) struct H2Connection {
|
||||||
handle: JoinHandle<()>,
|
handle: JoinHandle<()>,
|
||||||
sender: SendRequest<Bytes>,
|
sender: SendRequest<Bytes>,
|
||||||
|
@ -74,7 +73,6 @@ impl DerefMut for H2Connection {
|
||||||
|
|
||||||
pub trait Connection {
|
pub trait Connection {
|
||||||
type Io: AsyncRead + AsyncWrite + Unpin;
|
type Io: AsyncRead + AsyncWrite + Unpin;
|
||||||
type Future: Future<Output = Result<(ResponseHead, Payload), SendRequestError>>;
|
|
||||||
|
|
||||||
fn protocol(&self) -> Protocol;
|
fn protocol(&self) -> Protocol;
|
||||||
|
|
||||||
|
@ -83,14 +81,16 @@ pub trait Connection {
|
||||||
self,
|
self,
|
||||||
head: H,
|
head: H,
|
||||||
body: B,
|
body: B,
|
||||||
) -> Self::Future;
|
) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>;
|
||||||
|
|
||||||
type TunnelFuture: Future<
|
|
||||||
Output = Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
|
|
||||||
>;
|
|
||||||
|
|
||||||
/// Send request, returns Response and Framed
|
/// Send request, returns Response and Framed
|
||||||
fn open_tunnel<H: Into<RequestHeadType>>(self, head: H) -> Self::TunnelFuture;
|
fn open_tunnel<H: Into<RequestHeadType> + 'static>(
|
||||||
|
self,
|
||||||
|
head: H,
|
||||||
|
) -> LocalBoxFuture<
|
||||||
|
'static,
|
||||||
|
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static {
|
pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static {
|
||||||
|
@ -103,7 +103,10 @@ pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static {
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
/// HTTP client connection
|
/// HTTP client connection
|
||||||
pub struct IoConnection<T> {
|
pub struct IoConnection<T>
|
||||||
|
where
|
||||||
|
T: AsyncWrite + Unpin + 'static,
|
||||||
|
{
|
||||||
io: Option<ConnectionType<T>>,
|
io: Option<ConnectionType<T>>,
|
||||||
created: time::Instant,
|
created: time::Instant,
|
||||||
pool: Option<Acquired<T>>,
|
pool: Option<Acquired<T>>,
|
||||||
|
@ -111,7 +114,7 @@ pub struct IoConnection<T> {
|
||||||
|
|
||||||
impl<T> fmt::Debug for IoConnection<T>
|
impl<T> fmt::Debug for IoConnection<T>
|
||||||
where
|
where
|
||||||
T: fmt::Debug,
|
T: AsyncWrite + Unpin + fmt::Debug + 'static,
|
||||||
{
|
{
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self.io {
|
match self.io {
|
||||||
|
@ -138,6 +141,11 @@ impl<T: AsyncRead + AsyncWrite + Unpin> IoConnection<T> {
|
||||||
pub(crate) fn into_inner(self) -> (ConnectionType<T>, time::Instant) {
|
pub(crate) fn into_inner(self) -> (ConnectionType<T>, time::Instant) {
|
||||||
(self.io.unwrap(), self.created)
|
(self.io.unwrap(), self.created)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn into_parts(self) -> (ConnectionType<T>, time::Instant, Acquired<T>) {
|
||||||
|
(self.io.unwrap(), self.created, self.pool.unwrap())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Connection for IoConnection<T>
|
impl<T> Connection for IoConnection<T>
|
||||||
|
@ -145,8 +153,6 @@ where
|
||||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||||
{
|
{
|
||||||
type Io = T;
|
type Io = T;
|
||||||
type Future =
|
|
||||||
LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>;
|
|
||||||
|
|
||||||
fn protocol(&self) -> Protocol {
|
fn protocol(&self) -> Protocol {
|
||||||
match self.io {
|
match self.io {
|
||||||
|
@ -160,33 +166,35 @@ where
|
||||||
mut self,
|
mut self,
|
||||||
head: H,
|
head: H,
|
||||||
body: B,
|
body: B,
|
||||||
) -> Self::Future {
|
) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>> {
|
||||||
match self.io.take().unwrap() {
|
match self.io.take().unwrap() {
|
||||||
ConnectionType::H1(io) => {
|
ConnectionType::H1(io) => Box::pin(h1proto::send_request(
|
||||||
h1proto::send_request(io, head.into(), body, self.created, self.pool)
|
io,
|
||||||
.boxed_local()
|
head.into(),
|
||||||
}
|
body,
|
||||||
ConnectionType::H2(io) => {
|
self.created,
|
||||||
h2proto::send_request(io, head.into(), body, self.created, self.pool)
|
self.pool,
|
||||||
.boxed_local()
|
)),
|
||||||
}
|
ConnectionType::H2(io) => Box::pin(h2proto::send_request(
|
||||||
|
io,
|
||||||
|
head.into(),
|
||||||
|
body,
|
||||||
|
self.created,
|
||||||
|
self.pool,
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type TunnelFuture = Either<
|
|
||||||
LocalBoxFuture<
|
|
||||||
'static,
|
|
||||||
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
|
|
||||||
>,
|
|
||||||
Ready<Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>>,
|
|
||||||
>;
|
|
||||||
|
|
||||||
/// Send request, returns Response and Framed
|
/// Send request, returns Response and Framed
|
||||||
fn open_tunnel<H: Into<RequestHeadType>>(mut self, head: H) -> Self::TunnelFuture {
|
fn open_tunnel<H: Into<RequestHeadType>>(
|
||||||
|
mut self,
|
||||||
|
head: H,
|
||||||
|
) -> LocalBoxFuture<
|
||||||
|
'static,
|
||||||
|
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
|
||||||
|
> {
|
||||||
match self.io.take().unwrap() {
|
match self.io.take().unwrap() {
|
||||||
ConnectionType::H1(io) => {
|
ConnectionType::H1(io) => Box::pin(h1proto::open_tunnel(io, head.into())),
|
||||||
Either::Left(h1proto::open_tunnel(io, head.into()).boxed_local())
|
|
||||||
}
|
|
||||||
ConnectionType::H2(io) => {
|
ConnectionType::H2(io) => {
|
||||||
if let Some(mut pool) = self.pool.take() {
|
if let Some(mut pool) = self.pool.take() {
|
||||||
pool.release(IoConnection::new(
|
pool.release(IoConnection::new(
|
||||||
|
@ -195,14 +203,18 @@ where
|
||||||
None,
|
None,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Either::Right(err(SendRequestError::TunnelNotSupported))
|
Box::pin(async { Err(SendRequestError::TunnelNotSupported) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) enum EitherConnection<A, B> {
|
pub(crate) enum EitherConnection<A, B>
|
||||||
|
where
|
||||||
|
A: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||||
|
B: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||||
|
{
|
||||||
A(IoConnection<A>),
|
A(IoConnection<A>),
|
||||||
B(IoConnection<B>),
|
B(IoConnection<B>),
|
||||||
}
|
}
|
||||||
|
@ -213,8 +225,6 @@ where
|
||||||
B: AsyncRead + AsyncWrite + Unpin + 'static,
|
B: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||||
{
|
{
|
||||||
type Io = EitherIo<A, B>;
|
type Io = EitherIo<A, B>;
|
||||||
type Future =
|
|
||||||
LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>;
|
|
||||||
|
|
||||||
fn protocol(&self) -> Protocol {
|
fn protocol(&self) -> Protocol {
|
||||||
match self {
|
match self {
|
||||||
|
@ -227,33 +237,30 @@ where
|
||||||
self,
|
self,
|
||||||
head: H,
|
head: H,
|
||||||
body: RB,
|
body: RB,
|
||||||
) -> Self::Future {
|
) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>> {
|
||||||
match self {
|
match self {
|
||||||
EitherConnection::A(con) => con.send_request(head, body),
|
EitherConnection::A(con) => con.send_request(head, body),
|
||||||
EitherConnection::B(con) => con.send_request(head, body),
|
EitherConnection::B(con) => con.send_request(head, body),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type TunnelFuture = LocalBoxFuture<
|
/// Send request, returns Response and Framed
|
||||||
|
fn open_tunnel<H: Into<RequestHeadType> + 'static>(
|
||||||
|
self,
|
||||||
|
head: H,
|
||||||
|
) -> LocalBoxFuture<
|
||||||
'static,
|
'static,
|
||||||
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
|
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
|
||||||
>;
|
> {
|
||||||
|
|
||||||
/// Send request, returns Response and Framed
|
|
||||||
fn open_tunnel<H: Into<RequestHeadType>>(self, head: H) -> Self::TunnelFuture {
|
|
||||||
match self {
|
match self {
|
||||||
EitherConnection::A(con) => con
|
EitherConnection::A(con) => Box::pin(async {
|
||||||
.open_tunnel(head)
|
let (head, framed) = con.open_tunnel(head).await?;
|
||||||
.map(|res| {
|
Ok((head, framed.into_map_io(EitherIo::A)))
|
||||||
res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::A)))
|
}),
|
||||||
})
|
EitherConnection::B(con) => Box::pin(async {
|
||||||
.boxed_local(),
|
let (head, framed) = con.open_tunnel(head).await?;
|
||||||
EitherConnection::B(con) => con
|
Ok((head, framed.into_map_io(EitherIo::B)))
|
||||||
.open_tunnel(head)
|
}),
|
||||||
.map(|res| {
|
|
||||||
res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::B)))
|
|
||||||
})
|
|
||||||
.boxed_local(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ pub enum ConnectError {
|
||||||
Resolver(Box<dyn std::error::Error>),
|
Resolver(Box<dyn std::error::Error>),
|
||||||
|
|
||||||
/// No dns records
|
/// No dns records
|
||||||
#[display(fmt = "No dns records found for the input")]
|
#[display(fmt = "No DNS records found for the input")]
|
||||||
NoRecords,
|
NoRecords,
|
||||||
|
|
||||||
/// Http2 error
|
/// Http2 error
|
||||||
|
|
|
@ -8,7 +8,7 @@ use bytes::buf::BufMut;
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use futures_core::Stream;
|
use futures_core::Stream;
|
||||||
use futures_util::future::poll_fn;
|
use futures_util::future::poll_fn;
|
||||||
use futures_util::{pin_mut, SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
|
||||||
use crate::error::PayloadError;
|
use crate::error::PayloadError;
|
||||||
use crate::h1;
|
use crate::h1;
|
||||||
|
@ -127,7 +127,7 @@ where
|
||||||
T: ConnectionLifetime + Unpin,
|
T: ConnectionLifetime + Unpin,
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
{
|
{
|
||||||
pin_mut!(body);
|
actix_rt::pin!(body);
|
||||||
|
|
||||||
let mut eof = false;
|
let mut eof = false;
|
||||||
while !eof {
|
while !eof {
|
||||||
|
@ -165,7 +165,10 @@ where
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
/// HTTP client connection
|
/// HTTP client connection
|
||||||
pub struct H1Connection<T> {
|
pub struct H1Connection<T>
|
||||||
|
where
|
||||||
|
T: AsyncWrite + Unpin + 'static,
|
||||||
|
{
|
||||||
/// T should be `Unpin`
|
/// T should be `Unpin`
|
||||||
io: Option<T>,
|
io: Option<T>,
|
||||||
created: time::Instant,
|
created: time::Instant,
|
||||||
|
|
|
@ -5,7 +5,6 @@ use std::time;
|
||||||
use actix_codec::{AsyncRead, AsyncWrite};
|
use actix_codec::{AsyncRead, AsyncWrite};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_util::future::poll_fn;
|
use futures_util::future::poll_fn;
|
||||||
use futures_util::pin_mut;
|
|
||||||
use h2::{
|
use h2::{
|
||||||
client::{Builder, Connection, SendRequest},
|
client::{Builder, Connection, SendRequest},
|
||||||
SendStream,
|
SendStream,
|
||||||
|
@ -36,6 +35,7 @@ where
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
{
|
{
|
||||||
trace!("Sending client request: {:?} {:?}", head, body.size());
|
trace!("Sending client request: {:?} {:?}", head, body.size());
|
||||||
|
|
||||||
let head_req = head.as_ref().method == Method::HEAD;
|
let head_req = head.as_ref().method == Method::HEAD;
|
||||||
let length = body.size();
|
let length = body.size();
|
||||||
let eof = matches!(
|
let eof = matches!(
|
||||||
|
@ -130,7 +130,7 @@ async fn send_body<B: MessageBody>(
|
||||||
mut send: SendStream<Bytes>,
|
mut send: SendStream<Bytes>,
|
||||||
) -> Result<(), SendRequestError> {
|
) -> Result<(), SendRequestError> {
|
||||||
let mut buf = None;
|
let mut buf = None;
|
||||||
pin_mut!(body);
|
actix_rt::pin!(body);
|
||||||
loop {
|
loop {
|
||||||
if buf.is_none() {
|
if buf.is_none() {
|
||||||
match poll_fn(|cx| body.as_mut().poll_next(cx)).await {
|
match poll_fn(|cx| body.as_mut().poll_next(cx)).await {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -78,6 +78,7 @@ impl<B: MessageBody> Encoder<B> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseBody::Body(Encoder {
|
ResponseBody::Body(Encoder {
|
||||||
body,
|
body,
|
||||||
eof: false,
|
eof: false,
|
||||||
|
|
|
@ -11,7 +11,6 @@ use actix_utils::dispatcher::DispatcherError as FramedDispatcherError;
|
||||||
use actix_utils::timeout::TimeoutError;
|
use actix_utils::timeout::TimeoutError;
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use derive_more::{Display, From};
|
use derive_more::{Display, From};
|
||||||
pub use futures_channel::oneshot::Canceled;
|
|
||||||
use http::uri::InvalidUri;
|
use http::uri::InvalidUri;
|
||||||
use http::{header, Error as HttpError, StatusCode};
|
use http::{header, Error as HttpError, StatusCode};
|
||||||
use serde::de::value::Error as DeError;
|
use serde::de::value::Error as DeError;
|
||||||
|
@ -19,10 +18,12 @@ use serde_json::error::Error as JsonError;
|
||||||
use serde_urlencoded::ser::Error as FormError;
|
use serde_urlencoded::ser::Error as FormError;
|
||||||
|
|
||||||
use crate::body::Body;
|
use crate::body::Body;
|
||||||
pub use crate::cookie::ParseError as CookieParseError;
|
|
||||||
use crate::helpers::Writer;
|
use crate::helpers::Writer;
|
||||||
use crate::response::{Response, ResponseBuilder};
|
use crate::response::{Response, ResponseBuilder};
|
||||||
|
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
|
pub use crate::cookie::ParseError as CookieParseError;
|
||||||
|
|
||||||
/// A specialized [`std::result::Result`]
|
/// A specialized [`std::result::Result`]
|
||||||
/// for actix web operations
|
/// for actix web operations
|
||||||
///
|
///
|
||||||
|
@ -184,9 +185,6 @@ impl ResponseError for DeError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`Canceled`].
|
|
||||||
impl ResponseError for Canceled {}
|
|
||||||
|
|
||||||
/// Returns [`StatusCode::BAD_REQUEST`] for [`Utf8Error`].
|
/// Returns [`StatusCode::BAD_REQUEST`] for [`Utf8Error`].
|
||||||
impl ResponseError for Utf8Error {
|
impl ResponseError for Utf8Error {
|
||||||
fn status_code(&self) -> StatusCode {
|
fn status_code(&self) -> StatusCode {
|
||||||
|
@ -397,6 +395,7 @@ impl ResponseError for PayloadError {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `BadRequest` for `cookie::ParseError`
|
/// Return `BadRequest` for `cookie::ParseError`
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
impl ResponseError for crate::cookie::ParseError {
|
impl ResponseError for crate::cookie::ParseError {
|
||||||
fn status_code(&self) -> StatusCode {
|
fn status_code(&self) -> StatusCode {
|
||||||
StatusCode::BAD_REQUEST
|
StatusCode::BAD_REQUEST
|
||||||
|
|
|
@ -40,7 +40,6 @@ bitflags! {
|
||||||
const SHUTDOWN = 0b0000_0100;
|
const SHUTDOWN = 0b0000_0100;
|
||||||
const READ_DISCONNECT = 0b0000_1000;
|
const READ_DISCONNECT = 0b0000_1000;
|
||||||
const WRITE_DISCONNECT = 0b0001_0000;
|
const WRITE_DISCONNECT = 0b0001_0000;
|
||||||
const UPGRADE = 0b0010_0000;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,10 +214,7 @@ where
|
||||||
U::Error: fmt::Display,
|
U::Error: fmt::Display,
|
||||||
{
|
{
|
||||||
fn can_read(&self, cx: &mut Context<'_>) -> bool {
|
fn can_read(&self, cx: &mut Context<'_>) -> bool {
|
||||||
if self
|
if self.flags.contains(Flags::READ_DISCONNECT) {
|
||||||
.flags
|
|
||||||
.intersects(Flags::READ_DISCONNECT | Flags::UPGRADE)
|
|
||||||
{
|
|
||||||
false
|
false
|
||||||
} else if let Some(ref info) = self.payload {
|
} else if let Some(ref info) = self.payload {
|
||||||
info.need_read(cx) == PayloadStatus::Read
|
info.need_read(cx) == PayloadStatus::Read
|
||||||
|
@ -501,7 +497,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process one incoming request.
|
/// Process one incoming request.
|
||||||
pub(self) fn poll_request(
|
fn poll_request(
|
||||||
mut self: Pin<&mut Self>,
|
mut self: Pin<&mut Self>,
|
||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
) -> Result<bool, DispatchError> {
|
) -> Result<bool, DispatchError> {
|
||||||
|
@ -728,6 +724,53 @@ where
|
||||||
let mut read_some = false;
|
let mut read_some = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
// Return early when read buf exceed decoder's max buffer size.
|
||||||
|
if this.read_buf.len() >= super::decoder::MAX_BUFFER_SIZE {
|
||||||
|
/*
|
||||||
|
At this point it's not known IO stream is still scheduled
|
||||||
|
to be waked up. so force wake up dispatcher just in case.
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
AsyncRead mostly would only have guarantee wake up
|
||||||
|
when the poll_read return Poll::Pending.
|
||||||
|
|
||||||
|
Case:
|
||||||
|
When read_buf is beyond max buffer size the early return
|
||||||
|
could be successfully be parsed as a new Request.
|
||||||
|
This case would not generate ParseError::TooLarge
|
||||||
|
and at this point IO stream is not fully read to Pending
|
||||||
|
and would result in dispatcher stuck until timeout (KA)
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This is a perf choice to reduce branch on
|
||||||
|
<Request as MessageType>::decode.
|
||||||
|
|
||||||
|
A Request head too large to parse is only checked on
|
||||||
|
httparse::Status::Partial condition.
|
||||||
|
*/
|
||||||
|
if this.payload.is_none() {
|
||||||
|
/*
|
||||||
|
When dispatcher has a payload the responsibility of
|
||||||
|
wake up it would be shift to h1::payload::Payload.
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
Self wake up when there is payload would waste poll
|
||||||
|
and/or result in over read.
|
||||||
|
|
||||||
|
Case:
|
||||||
|
When payload is (partial) dropped by user there is
|
||||||
|
no need to do read anymore.
|
||||||
|
At this case read_buf could always remain beyond
|
||||||
|
MAX_BUFFER_SIZE and self wake up would be busy poll
|
||||||
|
dispatcher and waste resource.
|
||||||
|
|
||||||
|
*/
|
||||||
|
cx.waker().wake_by_ref();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
// grow buffer if necessary.
|
// grow buffer if necessary.
|
||||||
let remaining = this.read_buf.capacity() - this.read_buf.len();
|
let remaining = this.read_buf.capacity() - this.read_buf.len();
|
||||||
if remaining < LW_BUFFER_SIZE {
|
if remaining < LW_BUFFER_SIZE {
|
||||||
|
@ -735,35 +778,18 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
match actix_codec::poll_read_buf(io.as_mut(), cx, this.read_buf) {
|
match actix_codec::poll_read_buf(io.as_mut(), cx, this.read_buf) {
|
||||||
Poll::Pending => return Ok(false),
|
|
||||||
Poll::Ready(Ok(n)) => {
|
Poll::Ready(Ok(n)) => {
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
} else {
|
|
||||||
// Return early when read buf exceed decoder's max buffer size.
|
|
||||||
if this.read_buf.len() >= super::decoder::MAX_BUFFER_SIZE {
|
|
||||||
// at this point it's not known io is still scheduled to
|
|
||||||
// be waked up. so force wake up dispatcher just in case.
|
|
||||||
// TODO: figure out the overhead.
|
|
||||||
if this.payload.is_none() {
|
|
||||||
// When dispatcher has a payload. The responsibility of
|
|
||||||
// wake up stream would be shift to PayloadSender.
|
|
||||||
// Therefore no self wake up is needed.
|
|
||||||
cx.waker().wake_by_ref();
|
|
||||||
}
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
read_some = true;
|
|
||||||
}
|
}
|
||||||
|
read_some = true;
|
||||||
}
|
}
|
||||||
|
Poll::Pending => return Ok(false),
|
||||||
Poll::Ready(Err(err)) => {
|
Poll::Ready(Err(err)) => {
|
||||||
return if err.kind() == io::ErrorKind::WouldBlock {
|
return match err.kind() {
|
||||||
Ok(false)
|
io::ErrorKind::WouldBlock => Ok(false),
|
||||||
} else if err.kind() == io::ErrorKind::ConnectionReset && read_some {
|
io::ErrorKind::ConnectionReset if read_some => Ok(true),
|
||||||
Ok(true)
|
_ => Err(DispatchError::Io(err)),
|
||||||
} else {
|
|
||||||
Err(DispatchError::Io(err))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -985,7 +1011,7 @@ mod tests {
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
futures_util::pin_mut!(h1);
|
actix_rt::pin!(h1);
|
||||||
|
|
||||||
match h1.as_mut().poll(cx) {
|
match h1.as_mut().poll(cx) {
|
||||||
Poll::Pending => panic!(),
|
Poll::Pending => panic!(),
|
||||||
|
@ -1025,7 +1051,7 @@ mod tests {
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
futures_util::pin_mut!(h1);
|
actix_rt::pin!(h1);
|
||||||
|
|
||||||
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
|
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
|
||||||
|
|
||||||
|
@ -1079,7 +1105,7 @@ mod tests {
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
futures_util::pin_mut!(h1);
|
actix_rt::pin!(h1);
|
||||||
|
|
||||||
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
|
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
|
||||||
|
|
||||||
|
@ -1138,7 +1164,7 @@ mod tests {
|
||||||
",
|
",
|
||||||
);
|
);
|
||||||
|
|
||||||
futures_util::pin_mut!(h1);
|
actix_rt::pin!(h1);
|
||||||
|
|
||||||
assert!(h1.as_mut().poll(cx).is_pending());
|
assert!(h1.as_mut().poll(cx).is_pending());
|
||||||
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
|
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
|
||||||
|
@ -1210,7 +1236,7 @@ mod tests {
|
||||||
",
|
",
|
||||||
);
|
);
|
||||||
|
|
||||||
futures_util::pin_mut!(h1);
|
actix_rt::pin!(h1);
|
||||||
|
|
||||||
assert!(h1.as_mut().poll(cx).is_ready());
|
assert!(h1.as_mut().poll(cx).is_ready());
|
||||||
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
|
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
|
||||||
|
@ -1271,7 +1297,7 @@ mod tests {
|
||||||
",
|
",
|
||||||
);
|
);
|
||||||
|
|
||||||
futures_util::pin_mut!(h1);
|
actix_rt::pin!(h1);
|
||||||
|
|
||||||
assert!(h1.as_mut().poll(cx).is_ready());
|
assert!(h1.as_mut().poll(cx).is_ready());
|
||||||
assert!(matches!(&h1.inner, DispatcherState::Upgrade(_)));
|
assert!(matches!(&h1.inner, DispatcherState::Upgrade(_)));
|
||||||
|
|
|
@ -549,7 +549,6 @@ mod tests {
|
||||||
);
|
);
|
||||||
let data =
|
let data =
|
||||||
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
||||||
eprintln!("{}", &data);
|
|
||||||
|
|
||||||
assert!(data.contains("Content-Length: 0\r\n"));
|
assert!(data.contains("Content-Length: 0\r\n"));
|
||||||
assert!(data.contains("Connection: close\r\n"));
|
assert!(data.contains("Connection: close\r\n"));
|
||||||
|
|
|
@ -5,12 +5,14 @@ use encoding_rs::{Encoding, UTF_8};
|
||||||
use http::header;
|
use http::header;
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
|
|
||||||
use crate::cookie::Cookie;
|
use crate::error::{ContentTypeError, ParseError};
|
||||||
use crate::error::{ContentTypeError, CookieParseError, ParseError};
|
|
||||||
use crate::extensions::Extensions;
|
use crate::extensions::Extensions;
|
||||||
use crate::header::{Header, HeaderMap};
|
use crate::header::{Header, HeaderMap};
|
||||||
use crate::payload::Payload;
|
use crate::payload::Payload;
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
|
use crate::{cookie::Cookie, error::CookieParseError};
|
||||||
|
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
struct Cookies(Vec<Cookie<'static>>);
|
struct Cookies(Vec<Cookie<'static>>);
|
||||||
|
|
||||||
/// Trait that implements general purpose operations on HTTP messages.
|
/// Trait that implements general purpose operations on HTTP messages.
|
||||||
|
@ -104,7 +106,7 @@ pub trait HttpMessage: Sized {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load request cookies.
|
/// Load request cookies.
|
||||||
#[inline]
|
#[cfg(feature = "cookies")]
|
||||||
fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
|
fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
|
||||||
if self.extensions().get::<Cookies>().is_none() {
|
if self.extensions().get::<Cookies>().is_none() {
|
||||||
let mut cookies = Vec::new();
|
let mut cookies = Vec::new();
|
||||||
|
@ -119,12 +121,14 @@ pub trait HttpMessage: Sized {
|
||||||
}
|
}
|
||||||
self.extensions_mut().insert(Cookies(cookies));
|
self.extensions_mut().insert(Cookies(cookies));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Ref::map(self.extensions(), |ext| {
|
Ok(Ref::map(self.extensions(), |ext| {
|
||||||
&ext.get::<Cookies>().unwrap().0
|
&ext.get::<Cookies>().unwrap().0
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return request cookie.
|
/// Return request cookie.
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
fn cookie(&self, name: &str) -> Option<Cookie<'static>> {
|
fn cookie(&self, name: &str) -> Option<Cookie<'static>> {
|
||||||
if let Ok(cookies) = self.cookies() {
|
if let Ok(cookies) = self.cookies() {
|
||||||
for cookie in cookies.iter() {
|
for cookie in cookies.iter() {
|
||||||
|
|
|
@ -1,4 +1,19 @@
|
||||||
//! HTTP primitives for the Actix ecosystem.
|
//! HTTP primitives for the Actix ecosystem.
|
||||||
|
//!
|
||||||
|
//! ## Crate Features
|
||||||
|
//! | Feature | Functionality |
|
||||||
|
//! | ---------------- | ----------------------------------------------------- |
|
||||||
|
//! | `openssl` | TLS support via [OpenSSL]. |
|
||||||
|
//! | `rustls` | TLS support via [rustls]. |
|
||||||
|
//! | `compress` | Payload compression support. (Deflate, Gzip & Brotli) |
|
||||||
|
//! | `cookies` | Support for cookies backed by the [cookie] crate. |
|
||||||
|
//! | `secure-cookies` | Adds for secure cookies. Enables `cookies` feature. |
|
||||||
|
//! | `trust-dns` | Use [trust-dns] as the client DNS resolver. |
|
||||||
|
//!
|
||||||
|
//! [OpenSSL]: https://crates.io/crates/openssl
|
||||||
|
//! [rustls]: https://crates.io/crates/rustls
|
||||||
|
//! [cookie]: https://crates.io/crates/cookie
|
||||||
|
//! [trust-dns]: https://crates.io/crates/trust-dns
|
||||||
|
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
#![allow(
|
#![allow(
|
||||||
|
@ -34,13 +49,15 @@ mod response;
|
||||||
mod service;
|
mod service;
|
||||||
mod time_parser;
|
mod time_parser;
|
||||||
|
|
||||||
pub use cookie;
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod h1;
|
pub mod h1;
|
||||||
pub mod h2;
|
pub mod h2;
|
||||||
pub mod test;
|
pub mod test;
|
||||||
pub mod ws;
|
pub mod ws;
|
||||||
|
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
|
pub use cookie;
|
||||||
|
|
||||||
pub use self::builder::HttpServiceBuilder;
|
pub use self::builder::HttpServiceBuilder;
|
||||||
pub use self::config::{KeepAlive, ServiceConfig};
|
pub use self::config::{KeepAlive, ServiceConfig};
|
||||||
pub use self::error::{Error, ResponseError, Result};
|
pub use self::error::{Error, ResponseError, Result};
|
||||||
|
@ -61,6 +78,7 @@ pub mod http {
|
||||||
pub use http::{uri, Error, Uri};
|
pub use http::{uri, Error, Uri};
|
||||||
pub use http::{Method, StatusCode, Version};
|
pub use http::{Method, StatusCode, Version};
|
||||||
|
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
pub use crate::cookie::{Cookie, CookieBuilder};
|
pub use crate::cookie::{Cookie, CookieBuilder};
|
||||||
pub use crate::header::HeaderMap;
|
pub use crate::header::HeaderMap;
|
||||||
|
|
||||||
|
|
|
@ -16,13 +16,17 @@ use futures_core::Stream;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::body::{Body, BodyStream, MessageBody, ResponseBody};
|
use crate::body::{Body, BodyStream, MessageBody, ResponseBody};
|
||||||
use crate::cookie::{Cookie, CookieJar};
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::extensions::Extensions;
|
use crate::extensions::Extensions;
|
||||||
use crate::header::{IntoHeaderPair, IntoHeaderValue};
|
use crate::header::{IntoHeaderPair, IntoHeaderValue};
|
||||||
use crate::http::header::{self, HeaderName, HeaderValue};
|
use crate::http::header::{self, HeaderName};
|
||||||
use crate::http::{Error as HttpError, HeaderMap, StatusCode};
|
use crate::http::{Error as HttpError, HeaderMap, StatusCode};
|
||||||
use crate::message::{BoxedResponseHead, ConnectionType, ResponseHead};
|
use crate::message::{BoxedResponseHead, ConnectionType, ResponseHead};
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
|
use crate::{
|
||||||
|
cookie::{Cookie, CookieJar},
|
||||||
|
http::header::HeaderValue,
|
||||||
|
};
|
||||||
|
|
||||||
/// An HTTP Response
|
/// An HTTP Response
|
||||||
pub struct Response<B = Body> {
|
pub struct Response<B = Body> {
|
||||||
|
@ -133,6 +137,7 @@ impl<B> Response<B> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an iterator for the cookies set by this response
|
/// Get an iterator for the cookies set by this response
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn cookies(&self) -> CookieIter<'_> {
|
pub fn cookies(&self) -> CookieIter<'_> {
|
||||||
CookieIter {
|
CookieIter {
|
||||||
|
@ -141,6 +146,7 @@ impl<B> Response<B> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a cookie to this response
|
/// Add a cookie to this response
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn add_cookie(&mut self, cookie: &Cookie<'_>) -> Result<(), HttpError> {
|
pub fn add_cookie(&mut self, cookie: &Cookie<'_>) -> Result<(), HttpError> {
|
||||||
let h = &mut self.head.headers;
|
let h = &mut self.head.headers;
|
||||||
|
@ -153,6 +159,7 @@ impl<B> Response<B> {
|
||||||
|
|
||||||
/// Remove all cookies with the given name from this response. Returns
|
/// Remove all cookies with the given name from this response. Returns
|
||||||
/// the number of cookies removed.
|
/// the number of cookies removed.
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn del_cookie(&mut self, name: &str) -> usize {
|
pub fn del_cookie(&mut self, name: &str) -> usize {
|
||||||
let h = &mut self.head.headers;
|
let h = &mut self.head.headers;
|
||||||
|
@ -298,10 +305,12 @@ impl Future for Response {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
pub struct CookieIter<'a> {
|
pub struct CookieIter<'a> {
|
||||||
iter: header::GetAll<'a>,
|
iter: header::GetAll<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
impl<'a> Iterator for CookieIter<'a> {
|
impl<'a> Iterator for CookieIter<'a> {
|
||||||
type Item = Cookie<'a>;
|
type Item = Cookie<'a>;
|
||||||
|
|
||||||
|
@ -316,13 +325,13 @@ impl<'a> Iterator for CookieIter<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An HTTP response builder
|
/// An HTTP response builder.
|
||||||
///
|
///
|
||||||
/// This type can be used to construct an instance of `Response` through a
|
/// This type can be used to construct an instance of `Response` through a builder-like pattern.
|
||||||
/// builder-like pattern.
|
|
||||||
pub struct ResponseBuilder {
|
pub struct ResponseBuilder {
|
||||||
head: Option<BoxedResponseHead>,
|
head: Option<BoxedResponseHead>,
|
||||||
err: Option<HttpError>,
|
err: Option<HttpError>,
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
cookies: Option<CookieJar>,
|
cookies: Option<CookieJar>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -333,6 +342,7 @@ impl ResponseBuilder {
|
||||||
ResponseBuilder {
|
ResponseBuilder {
|
||||||
head: Some(BoxedResponseHead::new(status)),
|
head: Some(BoxedResponseHead::new(status)),
|
||||||
err: None,
|
err: None,
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
cookies: None,
|
cookies: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -531,6 +541,7 @@ impl ResponseBuilder {
|
||||||
/// .finish()
|
/// .finish()
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self {
|
pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self {
|
||||||
if self.cookies.is_none() {
|
if self.cookies.is_none() {
|
||||||
let mut jar = CookieJar::new();
|
let mut jar = CookieJar::new();
|
||||||
|
@ -557,6 +568,7 @@ impl ResponseBuilder {
|
||||||
/// builder.finish()
|
/// builder.finish()
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
pub fn del_cookie<'a>(&mut self, cookie: &Cookie<'a>) -> &mut Self {
|
pub fn del_cookie<'a>(&mut self, cookie: &Cookie<'a>) -> &mut Self {
|
||||||
if self.cookies.is_none() {
|
if self.cookies.is_none() {
|
||||||
self.cookies = Some(CookieJar::new())
|
self.cookies = Some(CookieJar::new())
|
||||||
|
@ -624,8 +636,11 @@ impl ResponseBuilder {
|
||||||
return Response::from(Error::from(e)).into_body();
|
return Response::from(Error::from(e)).into_body();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allow unused mut when cookies feature is disabled
|
||||||
|
#[allow(unused_mut)]
|
||||||
let mut response = self.head.take().expect("cannot reuse response builder");
|
let mut response = self.head.take().expect("cannot reuse response builder");
|
||||||
|
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
if let Some(ref jar) = self.cookies {
|
if let Some(ref jar) = self.cookies {
|
||||||
for cookie in jar.delta() {
|
for cookie in jar.delta() {
|
||||||
match HeaderValue::from_str(&cookie.to_string()) {
|
match HeaderValue::from_str(&cookie.to_string()) {
|
||||||
|
@ -693,6 +708,7 @@ impl ResponseBuilder {
|
||||||
ResponseBuilder {
|
ResponseBuilder {
|
||||||
head: self.head.take(),
|
head: self.head.take(),
|
||||||
err: self.err.take(),
|
err: self.err.take(),
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
cookies: self.cookies.take(),
|
cookies: self.cookies.take(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -712,21 +728,28 @@ fn parts<'a>(
|
||||||
/// Convert `Response` to a `ResponseBuilder`. Body get dropped.
|
/// Convert `Response` to a `ResponseBuilder`. Body get dropped.
|
||||||
impl<B> From<Response<B>> for ResponseBuilder {
|
impl<B> From<Response<B>> for ResponseBuilder {
|
||||||
fn from(res: Response<B>) -> ResponseBuilder {
|
fn from(res: Response<B>) -> ResponseBuilder {
|
||||||
// If this response has cookies, load them into a jar
|
#[cfg(feature = "cookies")]
|
||||||
let mut jar: Option<CookieJar> = None;
|
let jar = {
|
||||||
for c in res.cookies() {
|
// If this response has cookies, load them into a jar
|
||||||
if let Some(ref mut j) = jar {
|
let mut jar: Option<CookieJar> = None;
|
||||||
j.add_original(c.into_owned());
|
|
||||||
} else {
|
for c in res.cookies() {
|
||||||
let mut j = CookieJar::new();
|
if let Some(ref mut j) = jar {
|
||||||
j.add_original(c.into_owned());
|
j.add_original(c.into_owned());
|
||||||
jar = Some(j);
|
} else {
|
||||||
|
let mut j = CookieJar::new();
|
||||||
|
j.add_original(c.into_owned());
|
||||||
|
jar = Some(j);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
jar
|
||||||
|
};
|
||||||
|
|
||||||
ResponseBuilder {
|
ResponseBuilder {
|
||||||
head: Some(res.head),
|
head: Some(res.head),
|
||||||
err: None,
|
err: None,
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
cookies: jar,
|
cookies: jar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -735,22 +758,6 @@ impl<B> From<Response<B>> for ResponseBuilder {
|
||||||
/// Convert `ResponseHead` to a `ResponseBuilder`
|
/// Convert `ResponseHead` to a `ResponseBuilder`
|
||||||
impl<'a> From<&'a ResponseHead> for ResponseBuilder {
|
impl<'a> From<&'a ResponseHead> for ResponseBuilder {
|
||||||
fn from(head: &'a ResponseHead) -> ResponseBuilder {
|
fn from(head: &'a ResponseHead) -> ResponseBuilder {
|
||||||
// If this response has cookies, load them into a jar
|
|
||||||
let mut jar: Option<CookieJar> = None;
|
|
||||||
|
|
||||||
let cookies = CookieIter {
|
|
||||||
iter: head.headers.get_all(header::SET_COOKIE),
|
|
||||||
};
|
|
||||||
for c in cookies {
|
|
||||||
if let Some(ref mut j) = jar {
|
|
||||||
j.add_original(c.into_owned());
|
|
||||||
} else {
|
|
||||||
let mut j = CookieJar::new();
|
|
||||||
j.add_original(c.into_owned());
|
|
||||||
jar = Some(j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut msg = BoxedResponseHead::new(head.status);
|
let mut msg = BoxedResponseHead::new(head.status);
|
||||||
msg.version = head.version;
|
msg.version = head.version;
|
||||||
msg.reason = head.reason;
|
msg.reason = head.reason;
|
||||||
|
@ -761,9 +768,32 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder {
|
||||||
|
|
||||||
msg.no_chunking(!head.chunked());
|
msg.no_chunking(!head.chunked());
|
||||||
|
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
|
let jar = {
|
||||||
|
// If this response has cookies, load them into a jar
|
||||||
|
let mut jar: Option<CookieJar> = None;
|
||||||
|
|
||||||
|
let cookies = CookieIter {
|
||||||
|
iter: head.headers.get_all(header::SET_COOKIE),
|
||||||
|
};
|
||||||
|
|
||||||
|
for c in cookies {
|
||||||
|
if let Some(ref mut j) = jar {
|
||||||
|
j.add_original(c.into_owned());
|
||||||
|
} else {
|
||||||
|
let mut j = CookieJar::new();
|
||||||
|
j.add_original(c.into_owned());
|
||||||
|
jar = Some(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jar
|
||||||
|
};
|
||||||
|
|
||||||
ResponseBuilder {
|
ResponseBuilder {
|
||||||
head: Some(msg),
|
head: Some(msg),
|
||||||
err: None,
|
err: None,
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
cookies: jar,
|
cookies: jar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,13 +11,14 @@ use std::{
|
||||||
|
|
||||||
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
|
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use http::{
|
use http::{Method, Uri, Version};
|
||||||
header::{self, HeaderValue},
|
|
||||||
Method, Uri, Version,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
use crate::{
|
use crate::{
|
||||||
cookie::{Cookie, CookieJar},
|
cookie::{Cookie, CookieJar},
|
||||||
|
header::{self, HeaderValue},
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
header::{HeaderMap, IntoHeaderPair},
|
header::{HeaderMap, IntoHeaderPair},
|
||||||
payload::Payload,
|
payload::Payload,
|
||||||
Request,
|
Request,
|
||||||
|
@ -53,6 +54,7 @@ struct Inner {
|
||||||
method: Method,
|
method: Method,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
cookies: CookieJar,
|
cookies: CookieJar,
|
||||||
payload: Option<Payload>,
|
payload: Option<Payload>,
|
||||||
}
|
}
|
||||||
|
@ -64,6 +66,7 @@ impl Default for TestRequest {
|
||||||
uri: Uri::from_str("/").unwrap(),
|
uri: Uri::from_str("/").unwrap(),
|
||||||
version: Version::HTTP_11,
|
version: Version::HTTP_11,
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
cookies: CookieJar::new(),
|
cookies: CookieJar::new(),
|
||||||
payload: None,
|
payload: None,
|
||||||
}))
|
}))
|
||||||
|
@ -132,6 +135,7 @@ impl TestRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set cookie for this request.
|
/// Set cookie for this request.
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
pub fn cookie<'a>(&mut self, cookie: Cookie<'a>) -> &mut Self {
|
pub fn cookie<'a>(&mut self, cookie: Cookie<'a>) -> &mut Self {
|
||||||
parts(&mut self.0).cookies.add(cookie.into_owned());
|
parts(&mut self.0).cookies.add(cookie.into_owned());
|
||||||
self
|
self
|
||||||
|
@ -165,17 +169,20 @@ impl TestRequest {
|
||||||
head.version = inner.version;
|
head.version = inner.version;
|
||||||
head.headers = inner.headers;
|
head.headers = inner.headers;
|
||||||
|
|
||||||
let cookie: String = inner
|
#[cfg(feature = "cookies")]
|
||||||
.cookies
|
{
|
||||||
.delta()
|
let cookie: String = inner
|
||||||
// ensure only name=value is written to cookie header
|
.cookies
|
||||||
.map(|c| Cookie::new(c.name(), c.value()).encoded().to_string())
|
.delta()
|
||||||
.collect::<Vec<_>>()
|
// ensure only name=value is written to cookie header
|
||||||
.join("; ");
|
.map(|c| Cookie::new(c.name(), c.value()).encoded().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("; ");
|
||||||
|
|
||||||
if !cookie.is_empty() {
|
if !cookie.is_empty() {
|
||||||
head.headers
|
head.headers
|
||||||
.insert(header::COOKIE, HeaderValue::from_str(&cookie).unwrap());
|
.insert(header::COOKIE, HeaderValue::from_str(&cookie).unwrap());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req
|
req
|
||||||
|
|
|
@ -14,7 +14,11 @@ use actix_service::{fn_service, ServiceFactoryExt};
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use futures_util::future::{err, ok, ready};
|
use futures_util::future::{err, ok, ready};
|
||||||
use futures_util::stream::{once, Stream, StreamExt};
|
use futures_util::stream::{once, Stream, StreamExt};
|
||||||
use openssl::ssl::{AlpnError, SslAcceptor, SslFiletype, SslMethod};
|
use openssl::{
|
||||||
|
pkey::PKey,
|
||||||
|
ssl::{SslAcceptor, SslMethod},
|
||||||
|
x509::X509,
|
||||||
|
};
|
||||||
|
|
||||||
async fn load_body<S>(stream: S) -> Result<BytesMut, PayloadError>
|
async fn load_body<S>(stream: S) -> Result<BytesMut, PayloadError>
|
||||||
where
|
where
|
||||||
|
@ -34,29 +38,26 @@ where
|
||||||
Ok(body)
|
Ok(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ssl_acceptor() -> SslAcceptor {
|
fn tls_config() -> SslAcceptor {
|
||||||
// load ssl keys
|
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
|
||||||
|
let cert_file = cert.serialize_pem().unwrap();
|
||||||
|
let key_file = cert.serialize_private_key_pem();
|
||||||
|
let cert = X509::from_pem(cert_file.as_bytes()).unwrap();
|
||||||
|
let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap();
|
||||||
|
|
||||||
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
||||||
builder
|
builder.set_certificate(&cert).unwrap();
|
||||||
.set_private_key_file("../tests/key.pem", SslFiletype::PEM)
|
builder.set_private_key(&key).unwrap();
|
||||||
.unwrap();
|
|
||||||
builder
|
|
||||||
.set_certificate_chain_file("../tests/cert.pem")
|
|
||||||
.unwrap();
|
|
||||||
builder.set_alpn_select_callback(|_, protos| {
|
builder.set_alpn_select_callback(|_, protos| {
|
||||||
const H2: &[u8] = b"\x02h2";
|
const H2: &[u8] = b"\x02h2";
|
||||||
const H11: &[u8] = b"\x08http/1.1";
|
|
||||||
if protos.windows(3).any(|window| window == H2) {
|
if protos.windows(3).any(|window| window == H2) {
|
||||||
Ok(b"h2")
|
Ok(b"h2")
|
||||||
} else if protos.windows(9).any(|window| window == H11) {
|
|
||||||
Ok(b"http/1.1")
|
|
||||||
} else {
|
} else {
|
||||||
Err(AlpnError::NOACK)
|
Err(openssl::ssl::AlpnError::NOACK)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
builder
|
builder.set_alpn_protos(b"\x02h2").unwrap();
|
||||||
.set_alpn_protos(b"\x08http/1.1\x02h2")
|
|
||||||
.expect("Can not contrust SslAcceptor");
|
|
||||||
|
|
||||||
builder.build()
|
builder.build()
|
||||||
}
|
}
|
||||||
|
@ -66,7 +67,7 @@ async fn test_h2() -> io::Result<()> {
|
||||||
let srv = test_server(move || {
|
let srv = test_server(move || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.h2(|_| ok::<_, Error>(Response::Ok().finish()))
|
.h2(|_| ok::<_, Error>(Response::Ok().finish()))
|
||||||
.openssl(ssl_acceptor())
|
.openssl(tls_config())
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -85,7 +86,7 @@ async fn test_h2_1() -> io::Result<()> {
|
||||||
assert_eq!(req.version(), Version::HTTP_2);
|
assert_eq!(req.version(), Version::HTTP_2);
|
||||||
ok::<_, Error>(Response::Ok().finish())
|
ok::<_, Error>(Response::Ok().finish())
|
||||||
})
|
})
|
||||||
.openssl(ssl_acceptor())
|
.openssl(tls_config())
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -104,7 +105,7 @@ async fn test_h2_body() -> io::Result<()> {
|
||||||
let body = load_body(req.take_payload()).await?;
|
let body = load_body(req.take_payload()).await?;
|
||||||
Ok::<_, Error>(Response::Ok().body(body))
|
Ok::<_, Error>(Response::Ok().body(body))
|
||||||
})
|
})
|
||||||
.openssl(ssl_acceptor())
|
.openssl(tls_config())
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -133,7 +134,7 @@ async fn test_h2_content_length() {
|
||||||
];
|
];
|
||||||
ok::<_, ()>(Response::new(statuses[indx]))
|
ok::<_, ()>(Response::new(statuses[indx]))
|
||||||
})
|
})
|
||||||
.openssl(ssl_acceptor())
|
.openssl(tls_config())
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -195,7 +196,7 @@ async fn test_h2_headers() {
|
||||||
}
|
}
|
||||||
ok::<_, ()>(builder.body(data.clone()))
|
ok::<_, ()>(builder.body(data.clone()))
|
||||||
})
|
})
|
||||||
.openssl(ssl_acceptor())
|
.openssl(tls_config())
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
}).await;
|
}).await;
|
||||||
|
|
||||||
|
@ -234,7 +235,7 @@ async fn test_h2_body2() {
|
||||||
let mut srv = test_server(move || {
|
let mut srv = test_server(move || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.h2(|_| ok::<_, ()>(Response::Ok().body(STR)))
|
.h2(|_| ok::<_, ()>(Response::Ok().body(STR)))
|
||||||
.openssl(ssl_acceptor())
|
.openssl(tls_config())
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -252,7 +253,7 @@ async fn test_h2_head_empty() {
|
||||||
let mut srv = test_server(move || {
|
let mut srv = test_server(move || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.finish(|_| ok::<_, ()>(Response::Ok().body(STR)))
|
.finish(|_| ok::<_, ()>(Response::Ok().body(STR)))
|
||||||
.openssl(ssl_acceptor())
|
.openssl(tls_config())
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -276,7 +277,7 @@ async fn test_h2_head_binary() {
|
||||||
let mut srv = test_server(move || {
|
let mut srv = test_server(move || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.h2(|_| ok::<_, ()>(Response::Ok().body(STR)))
|
.h2(|_| ok::<_, ()>(Response::Ok().body(STR)))
|
||||||
.openssl(ssl_acceptor())
|
.openssl(tls_config())
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -299,7 +300,7 @@ async fn test_h2_head_binary2() {
|
||||||
let srv = test_server(move || {
|
let srv = test_server(move || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.h2(|_| ok::<_, ()>(Response::Ok().body(STR)))
|
.h2(|_| ok::<_, ()>(Response::Ok().body(STR)))
|
||||||
.openssl(ssl_acceptor())
|
.openssl(tls_config())
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -323,7 +324,7 @@ async fn test_h2_body_length() {
|
||||||
Response::Ok().body(body::SizedStream::new(STR.len() as u64, body)),
|
Response::Ok().body(body::SizedStream::new(STR.len() as u64, body)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.openssl(ssl_acceptor())
|
.openssl(tls_config())
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -348,7 +349,7 @@ async fn test_h2_body_chunked_explicit() {
|
||||||
.streaming(body),
|
.streaming(body),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.openssl(ssl_acceptor())
|
.openssl(tls_config())
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -376,7 +377,7 @@ async fn test_h2_response_http_error_handling() {
|
||||||
.body(STR),
|
.body(STR),
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
.openssl(ssl_acceptor())
|
.openssl(tls_config())
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -394,7 +395,7 @@ async fn test_h2_service_error() {
|
||||||
let mut srv = test_server(move || {
|
let mut srv = test_server(move || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.h2(|_| err::<Response, Error>(ErrorBadRequest("error")))
|
.h2(|_| err::<Response, Error>(ErrorBadRequest("error")))
|
||||||
.openssl(ssl_acceptor())
|
.openssl(tls_config())
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -418,7 +419,7 @@ async fn test_h2_on_connect() {
|
||||||
assert!(req.extensions().contains::<isize>());
|
assert!(req.extensions().contains::<isize>());
|
||||||
ok::<_, ()>(Response::Ok().finish())
|
ok::<_, ()>(Response::Ok().finish())
|
||||||
})
|
})
|
||||||
.openssl(ssl_acceptor())
|
.openssl(tls_config())
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -31,14 +31,19 @@ where
|
||||||
Ok(body)
|
Ok(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ssl_acceptor() -> RustlsServerConfig {
|
fn tls_config() -> RustlsServerConfig {
|
||||||
// load ssl keys
|
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
|
||||||
|
let cert_file = cert.serialize_pem().unwrap();
|
||||||
|
let key_file = cert.serialize_private_key_pem();
|
||||||
|
|
||||||
let mut config = RustlsServerConfig::new(NoClientAuth::new());
|
let mut config = RustlsServerConfig::new(NoClientAuth::new());
|
||||||
let cert_file = &mut BufReader::new(File::open("../tests/cert.pem").unwrap());
|
let cert_file = &mut BufReader::new(cert_file.as_bytes());
|
||||||
let key_file = &mut BufReader::new(File::open("../tests/key.pem").unwrap());
|
let key_file = &mut BufReader::new(key_file.as_bytes());
|
||||||
|
|
||||||
let cert_chain = certs(cert_file).unwrap();
|
let cert_chain = certs(cert_file).unwrap();
|
||||||
let mut keys = pkcs8_private_keys(key_file).unwrap();
|
let mut keys = pkcs8_private_keys(key_file).unwrap();
|
||||||
config.set_single_cert(cert_chain, keys.remove(0)).unwrap();
|
config.set_single_cert(cert_chain, keys.remove(0)).unwrap();
|
||||||
|
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +52,7 @@ async fn test_h1() -> io::Result<()> {
|
||||||
let srv = test_server(move || {
|
let srv = test_server(move || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.h1(|_| future::ok::<_, Error>(Response::Ok().finish()))
|
.h1(|_| future::ok::<_, Error>(Response::Ok().finish()))
|
||||||
.rustls(ssl_acceptor())
|
.rustls(tls_config())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -61,7 +66,7 @@ async fn test_h2() -> io::Result<()> {
|
||||||
let srv = test_server(move || {
|
let srv = test_server(move || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.h2(|_| future::ok::<_, Error>(Response::Ok().finish()))
|
.h2(|_| future::ok::<_, Error>(Response::Ok().finish()))
|
||||||
.rustls(ssl_acceptor())
|
.rustls(tls_config())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -79,7 +84,7 @@ async fn test_h1_1() -> io::Result<()> {
|
||||||
assert_eq!(req.version(), Version::HTTP_11);
|
assert_eq!(req.version(), Version::HTTP_11);
|
||||||
future::ok::<_, Error>(Response::Ok().finish())
|
future::ok::<_, Error>(Response::Ok().finish())
|
||||||
})
|
})
|
||||||
.rustls(ssl_acceptor())
|
.rustls(tls_config())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -97,7 +102,7 @@ async fn test_h2_1() -> io::Result<()> {
|
||||||
assert_eq!(req.version(), Version::HTTP_2);
|
assert_eq!(req.version(), Version::HTTP_2);
|
||||||
future::ok::<_, Error>(Response::Ok().finish())
|
future::ok::<_, Error>(Response::Ok().finish())
|
||||||
})
|
})
|
||||||
.rustls(ssl_acceptor())
|
.rustls(tls_config())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -115,7 +120,7 @@ async fn test_h2_body1() -> io::Result<()> {
|
||||||
let body = load_body(req.take_payload()).await?;
|
let body = load_body(req.take_payload()).await?;
|
||||||
Ok::<_, Error>(Response::Ok().body(body))
|
Ok::<_, Error>(Response::Ok().body(body))
|
||||||
})
|
})
|
||||||
.rustls(ssl_acceptor())
|
.rustls(tls_config())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -143,7 +148,7 @@ async fn test_h2_content_length() {
|
||||||
];
|
];
|
||||||
future::ok::<_, ()>(Response::new(statuses[indx]))
|
future::ok::<_, ()>(Response::new(statuses[indx]))
|
||||||
})
|
})
|
||||||
.rustls(ssl_acceptor())
|
.rustls(tls_config())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -203,7 +208,7 @@ async fn test_h2_headers() {
|
||||||
}
|
}
|
||||||
future::ok::<_, ()>(config.body(data.clone()))
|
future::ok::<_, ()>(config.body(data.clone()))
|
||||||
})
|
})
|
||||||
.rustls(ssl_acceptor())
|
.rustls(tls_config())
|
||||||
}).await;
|
}).await;
|
||||||
|
|
||||||
let response = srv.sget("/").send().await.unwrap();
|
let response = srv.sget("/").send().await.unwrap();
|
||||||
|
@ -241,7 +246,7 @@ async fn test_h2_body2() {
|
||||||
let mut srv = test_server(move || {
|
let mut srv = test_server(move || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.h2(|_| future::ok::<_, ()>(Response::Ok().body(STR)))
|
.h2(|_| future::ok::<_, ()>(Response::Ok().body(STR)))
|
||||||
.rustls(ssl_acceptor())
|
.rustls(tls_config())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -258,7 +263,7 @@ async fn test_h2_head_empty() {
|
||||||
let mut srv = test_server(move || {
|
let mut srv = test_server(move || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.finish(|_| ok::<_, ()>(Response::Ok().body(STR)))
|
.finish(|_| ok::<_, ()>(Response::Ok().body(STR)))
|
||||||
.rustls(ssl_acceptor())
|
.rustls(tls_config())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -284,7 +289,7 @@ async fn test_h2_head_binary() {
|
||||||
let mut srv = test_server(move || {
|
let mut srv = test_server(move || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.h2(|_| ok::<_, ()>(Response::Ok().body(STR)))
|
.h2(|_| ok::<_, ()>(Response::Ok().body(STR)))
|
||||||
.rustls(ssl_acceptor())
|
.rustls(tls_config())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -309,7 +314,7 @@ async fn test_h2_head_binary2() {
|
||||||
let srv = test_server(move || {
|
let srv = test_server(move || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.h2(|_| ok::<_, ()>(Response::Ok().body(STR)))
|
.h2(|_| ok::<_, ()>(Response::Ok().body(STR)))
|
||||||
.rustls(ssl_acceptor())
|
.rustls(tls_config())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -335,7 +340,7 @@ async fn test_h2_body_length() {
|
||||||
Response::Ok().body(body::SizedStream::new(STR.len() as u64, body)),
|
Response::Ok().body(body::SizedStream::new(STR.len() as u64, body)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.rustls(ssl_acceptor())
|
.rustls(tls_config())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -359,7 +364,7 @@ async fn test_h2_body_chunked_explicit() {
|
||||||
.streaming(body),
|
.streaming(body),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.rustls(ssl_acceptor())
|
.rustls(tls_config())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -388,7 +393,7 @@ async fn test_h2_response_http_error_handling() {
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
.rustls(ssl_acceptor())
|
.rustls(tls_config())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -405,7 +410,7 @@ async fn test_h2_service_error() {
|
||||||
let mut srv = test_server(move || {
|
let mut srv = test_server(move || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.h2(|_| err::<Response, Error>(error::ErrorBadRequest("error")))
|
.h2(|_| err::<Response, Error>(error::ErrorBadRequest("error")))
|
||||||
.rustls(ssl_acceptor())
|
.rustls(tls_config())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -422,7 +427,7 @@ async fn test_h1_service_error() {
|
||||||
let mut srv = test_server(move || {
|
let mut srv = test_server(move || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.h1(|_| err::<Response, Error>(error::ErrorBadRequest("error")))
|
.h1(|_| err::<Response, Error>(error::ErrorBadRequest("error")))
|
||||||
.rustls(ssl_acceptor())
|
.rustls(tls_config())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
### Added
|
||||||
|
* `ClientResponse::timeout` for set the timeout of collecting response body. [#1931]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Feature `cookies` is now optional and enabled by default. [#1981]
|
||||||
|
|
||||||
|
[#1931]: https://github.com/actix/actix-web/pull/1931
|
||||||
|
[#1981]: https://github.com/actix/actix-web/pull/1981
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.2 - 2021-02-10
|
## 3.0.0-beta.2 - 2021-02-10
|
||||||
|
|
|
@ -22,10 +22,11 @@ name = "awc"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
features = ["openssl", "rustls", "compress"]
|
# features that docs.rs will build with
|
||||||
|
features = ["openssl", "rustls", "compress", "cookies"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["compress"]
|
default = ["compress", "cookies"]
|
||||||
|
|
||||||
# openssl
|
# openssl
|
||||||
openssl = ["tls-openssl", "actix-http/openssl"]
|
openssl = ["tls-openssl", "actix-http/openssl"]
|
||||||
|
@ -36,6 +37,9 @@ rustls = ["tls-rustls", "actix-http/rustls"]
|
||||||
# content-encoding support
|
# content-encoding support
|
||||||
compress = ["actix-http/compress"]
|
compress = ["actix-http/compress"]
|
||||||
|
|
||||||
|
# cookie parsing and cookie jar
|
||||||
|
cookies = ["actix-http/cookies"]
|
||||||
|
|
||||||
# trust-dns as dns resolver
|
# trust-dns as dns resolver
|
||||||
trust-dns = ["actix-http/trust-dns"]
|
trust-dns = ["actix-http/trust-dns"]
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,6 @@ impl ClientBuilder {
|
||||||
where
|
where
|
||||||
T: Service<HttpConnect, Error = ConnectError> + 'static,
|
T: Service<HttpConnect, Error = ConnectError> + 'static,
|
||||||
T::Response: Connection,
|
T::Response: Connection,
|
||||||
<T::Response as Connection>::Future: 'static,
|
|
||||||
T::Future: 'static,
|
T::Future: 'static,
|
||||||
{
|
{
|
||||||
self.connector = Some(Box::new(ConnectorWrapper(connector)));
|
self.connector = Some(Box::new(ConnectorWrapper(connector)));
|
||||||
|
|
|
@ -41,8 +41,6 @@ where
|
||||||
T: Service<ClientConnect, Error = ConnectError>,
|
T: Service<ClientConnect, Error = ConnectError>,
|
||||||
T::Response: Connection,
|
T::Response: Connection,
|
||||||
<T::Response as Connection>::Io: 'static,
|
<T::Response as Connection>::Io: 'static,
|
||||||
<T::Response as Connection>::Future: 'static,
|
|
||||||
<T::Response as Connection>::TunnelFuture: 'static,
|
|
||||||
T::Future: 'static,
|
T::Future: 'static,
|
||||||
{
|
{
|
||||||
fn send_request(
|
fn send_request(
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
//! Http client errors
|
//! HTTP client errors
|
||||||
|
|
||||||
pub use actix_http::client::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};
|
pub use actix_http::client::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};
|
||||||
pub use actix_http::error::PayloadError;
|
pub use actix_http::error::PayloadError;
|
||||||
pub use actix_http::http::Error as HttpError;
|
pub use actix_http::http::Error as HttpError;
|
||||||
|
|
|
@ -97,7 +97,9 @@ use std::convert::TryFrom;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
pub use actix_http::{client::Connector, cookie, http};
|
#[cfg(feature = "cookies")]
|
||||||
|
pub use actix_http::cookie;
|
||||||
|
pub use actix_http::{client::Connector, http};
|
||||||
|
|
||||||
use actix_http::http::{Error as HttpError, HeaderMap, Method, Uri};
|
use actix_http::http::{Error as HttpError, HeaderMap, Method, Uri};
|
||||||
use actix_http::RequestHead;
|
use actix_http::RequestHead;
|
||||||
|
|
|
@ -8,6 +8,7 @@ use futures_core::Stream;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use actix_http::body::Body;
|
use actix_http::body::Body;
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
use actix_http::cookie::{Cookie, CookieJar};
|
use actix_http::cookie::{Cookie, CookieJar};
|
||||||
use actix_http::http::header::{self, IntoHeaderPair};
|
use actix_http::http::header::{self, IntoHeaderPair};
|
||||||
use actix_http::http::{
|
use actix_http::http::{
|
||||||
|
@ -54,10 +55,12 @@ pub struct ClientRequest {
|
||||||
pub(crate) head: RequestHead,
|
pub(crate) head: RequestHead,
|
||||||
err: Option<HttpError>,
|
err: Option<HttpError>,
|
||||||
addr: Option<net::SocketAddr>,
|
addr: Option<net::SocketAddr>,
|
||||||
cookies: Option<CookieJar>,
|
|
||||||
response_decompress: bool,
|
response_decompress: bool,
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
config: Rc<ClientConfig>,
|
config: Rc<ClientConfig>,
|
||||||
|
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
|
cookies: Option<CookieJar>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientRequest {
|
impl ClientRequest {
|
||||||
|
@ -72,6 +75,7 @@ impl ClientRequest {
|
||||||
head: RequestHead::default(),
|
head: RequestHead::default(),
|
||||||
err: None,
|
err: None,
|
||||||
addr: None,
|
addr: None,
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
cookies: None,
|
cookies: None,
|
||||||
timeout: None,
|
timeout: None,
|
||||||
response_decompress: true,
|
response_decompress: true,
|
||||||
|
@ -290,6 +294,7 @@ impl ClientRequest {
|
||||||
/// println!("Response: {:?}", resp);
|
/// println!("Response: {:?}", resp);
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
pub fn cookie(mut self, cookie: Cookie<'_>) -> Self {
|
pub fn cookie(mut self, cookie: Cookie<'_>) -> Self {
|
||||||
if self.cookies.is_none() {
|
if self.cookies.is_none() {
|
||||||
let mut jar = CookieJar::new();
|
let mut jar = CookieJar::new();
|
||||||
|
@ -472,7 +477,8 @@ impl ClientRequest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prep_for_sending(mut self) -> Result<Self, PrepForSendingError> {
|
// allow unused mut when cookies feature is disabled
|
||||||
|
fn prep_for_sending(#[allow(unused_mut)] mut self) -> Result<Self, PrepForSendingError> {
|
||||||
if let Some(e) = self.err {
|
if let Some(e) = self.err {
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
|
@ -493,6 +499,7 @@ impl ClientRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
// set cookies
|
// set cookies
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
if let Some(ref mut jar) = self.cookies {
|
if let Some(ref mut jar) = self.cookies {
|
||||||
let cookie: String = jar
|
let cookie: String = jar
|
||||||
.delta()
|
.delta()
|
||||||
|
|
|
@ -1,23 +1,27 @@
|
||||||
use std::cell::{Ref, RefMut};
|
use std::{
|
||||||
use std::fmt;
|
cell::{Ref, RefMut},
|
||||||
use std::future::Future;
|
fmt,
|
||||||
use std::io;
|
future::Future,
|
||||||
use std::marker::PhantomData;
|
io,
|
||||||
use std::pin::Pin;
|
marker::PhantomData,
|
||||||
use std::task::{Context, Poll};
|
pin::Pin,
|
||||||
use std::time::{Duration, Instant};
|
task::{Context, Poll},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_http::{
|
||||||
|
error::PayloadError,
|
||||||
|
http::{header, HeaderMap, StatusCode, Version},
|
||||||
|
Extensions, HttpMessage, Payload, PayloadStream, ResponseHead,
|
||||||
|
};
|
||||||
|
use actix_rt::time::{sleep, Sleep};
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use futures_core::{ready, Stream};
|
use futures_core::{ready, Stream};
|
||||||
|
|
||||||
use actix_http::cookie::Cookie;
|
|
||||||
use actix_http::error::{CookieParseError, PayloadError};
|
|
||||||
use actix_http::http::header::{CONTENT_LENGTH, SET_COOKIE};
|
|
||||||
use actix_http::http::{HeaderMap, StatusCode, Version};
|
|
||||||
use actix_http::{Extensions, HttpMessage, Payload, PayloadStream, ResponseHead};
|
|
||||||
use actix_rt::time::{sleep, Sleep};
|
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
|
use actix_http::{cookie::Cookie, error::CookieParseError};
|
||||||
|
|
||||||
use crate::error::JsonPayloadError;
|
use crate::error::JsonPayloadError;
|
||||||
|
|
||||||
/// Client Response
|
/// Client Response
|
||||||
|
@ -78,13 +82,13 @@ impl<S> HttpMessage for ClientResponse<S> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load request cookies.
|
/// Load request cookies.
|
||||||
#[inline]
|
#[cfg(feature = "cookies")]
|
||||||
fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
|
fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
|
||||||
struct Cookies(Vec<Cookie<'static>>);
|
struct Cookies(Vec<Cookie<'static>>);
|
||||||
|
|
||||||
if self.extensions().get::<Cookies>().is_none() {
|
if self.extensions().get::<Cookies>().is_none() {
|
||||||
let mut cookies = Vec::new();
|
let mut cookies = Vec::new();
|
||||||
for hdr in self.headers().get_all(&SET_COOKIE) {
|
for hdr in self.headers().get_all(&header::SET_COOKIE) {
|
||||||
let s = std::str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?;
|
let s = std::str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?;
|
||||||
cookies.push(Cookie::parse_encoded(s)?.into_owned());
|
cookies.push(Cookie::parse_encoded(s)?.into_owned());
|
||||||
}
|
}
|
||||||
|
@ -152,15 +156,13 @@ impl<S> ClientResponse<S> {
|
||||||
pub fn timeout(self, dur: Duration) -> Self {
|
pub fn timeout(self, dur: Duration) -> Self {
|
||||||
let timeout = match self.timeout {
|
let timeout = match self.timeout {
|
||||||
ResponseTimeout::Disabled(Some(mut timeout))
|
ResponseTimeout::Disabled(Some(mut timeout))
|
||||||
| ResponseTimeout::Enabled(mut timeout) => {
|
| ResponseTimeout::Enabled(mut timeout) => match Instant::now().checked_add(dur) {
|
||||||
match Instant::now().checked_add(dur) {
|
Some(deadline) => {
|
||||||
Some(deadline) => {
|
timeout.as_mut().reset(deadline.into());
|
||||||
timeout.as_mut().reset(deadline.into());
|
ResponseTimeout::Enabled(timeout)
|
||||||
ResponseTimeout::Enabled(timeout)
|
|
||||||
}
|
|
||||||
None => ResponseTimeout::Enabled(Box::pin(sleep(dur))),
|
|
||||||
}
|
}
|
||||||
}
|
None => ResponseTimeout::Enabled(Box::pin(sleep(dur))),
|
||||||
|
},
|
||||||
_ => ResponseTimeout::Enabled(Box::pin(sleep(dur))),
|
_ => ResponseTimeout::Enabled(Box::pin(sleep(dur))),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -207,10 +209,7 @@ where
|
||||||
{
|
{
|
||||||
type Item = Result<Bytes, PayloadError>;
|
type Item = Result<Bytes, PayloadError>;
|
||||||
|
|
||||||
fn poll_next(
|
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Self::Item>> {
|
|
||||||
let this = self.get_mut();
|
let this = self.get_mut();
|
||||||
this.timeout.poll_timeout(cx)?;
|
this.timeout.poll_timeout(cx)?;
|
||||||
|
|
||||||
|
@ -244,7 +243,7 @@ where
|
||||||
/// Create `MessageBody` for request.
|
/// Create `MessageBody` for request.
|
||||||
pub fn new(res: &mut ClientResponse<S>) -> MessageBody<S> {
|
pub fn new(res: &mut ClientResponse<S>) -> MessageBody<S> {
|
||||||
let mut len = None;
|
let mut len = None;
|
||||||
if let Some(l) = res.headers().get(&CONTENT_LENGTH) {
|
if let Some(l) = res.headers().get(&header::CONTENT_LENGTH) {
|
||||||
if let Ok(s) = l.to_str() {
|
if let Ok(s) = l.to_str() {
|
||||||
if let Ok(l) = s.parse::<usize>() {
|
if let Ok(l) = s.parse::<usize>() {
|
||||||
len = Some(l)
|
len = Some(l)
|
||||||
|
@ -345,7 +344,8 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut len = None;
|
let mut len = None;
|
||||||
if let Some(l) = res.headers().get(&CONTENT_LENGTH) {
|
|
||||||
|
if let Some(l) = res.headers().get(&header::CONTENT_LENGTH) {
|
||||||
if let Ok(s) = l.to_str() {
|
if let Ok(s) = l.to_str() {
|
||||||
if let Ok(l) = s.parse::<usize>() {
|
if let Ok(l) = s.parse::<usize>() {
|
||||||
len = Some(l)
|
len = Some(l)
|
||||||
|
|
|
@ -22,9 +22,7 @@ use futures_core::{ready, Stream};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[cfg(feature = "compress")]
|
#[cfg(feature = "compress")]
|
||||||
use actix_http::{
|
use actix_http::{encoding::Decoder, http::header::ContentEncoding, Payload, PayloadStream};
|
||||||
encoding::Decoder, http::header::ContentEncoding, Payload, PayloadStream,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::error::{FreezeRequestError, InvalidUrl, SendRequestError};
|
use crate::error::{FreezeRequestError, InvalidUrl, SendRequestError};
|
||||||
use crate::response::ClientResponse;
|
use crate::response::ClientResponse;
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
//! Test helpers for actix http client to use during testing.
|
//! Test helpers for actix http client to use during testing.
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
use actix_http::cookie::{Cookie, CookieJar};
|
use actix_http::http::header::{Header, IntoHeaderValue};
|
||||||
use actix_http::http::header::{self, Header, HeaderValue, IntoHeaderValue};
|
|
||||||
use actix_http::http::{Error as HttpError, HeaderName, StatusCode, Version};
|
use actix_http::http::{Error as HttpError, HeaderName, StatusCode, Version};
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
|
use actix_http::{
|
||||||
|
cookie::{Cookie, CookieJar},
|
||||||
|
http::header::{self, HeaderValue},
|
||||||
|
};
|
||||||
use actix_http::{h1, Payload, ResponseHead};
|
use actix_http::{h1, Payload, ResponseHead};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
|
||||||
|
@ -12,6 +16,7 @@ use crate::ClientResponse;
|
||||||
/// Test `ClientResponse` builder
|
/// Test `ClientResponse` builder
|
||||||
pub struct TestResponse {
|
pub struct TestResponse {
|
||||||
head: ResponseHead,
|
head: ResponseHead,
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
cookies: CookieJar,
|
cookies: CookieJar,
|
||||||
payload: Option<Payload>,
|
payload: Option<Payload>,
|
||||||
}
|
}
|
||||||
|
@ -20,6 +25,7 @@ impl Default for TestResponse {
|
||||||
fn default() -> TestResponse {
|
fn default() -> TestResponse {
|
||||||
TestResponse {
|
TestResponse {
|
||||||
head: ResponseHead::new(StatusCode::OK),
|
head: ResponseHead::new(StatusCode::OK),
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
cookies: CookieJar::new(),
|
cookies: CookieJar::new(),
|
||||||
payload: None,
|
payload: None,
|
||||||
}
|
}
|
||||||
|
@ -69,6 +75,7 @@ impl TestResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set cookie for this response
|
/// Set cookie for this response
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
pub fn cookie(mut self, cookie: Cookie<'_>) -> Self {
|
pub fn cookie(mut self, cookie: Cookie<'_>) -> Self {
|
||||||
self.cookies.add(cookie.into_owned());
|
self.cookies.add(cookie.into_owned());
|
||||||
self
|
self
|
||||||
|
@ -84,8 +91,11 @@ impl TestResponse {
|
||||||
|
|
||||||
/// Complete response creation and generate `ClientResponse` instance
|
/// Complete response creation and generate `ClientResponse` instance
|
||||||
pub fn finish(self) -> ClientResponse {
|
pub fn finish(self) -> ClientResponse {
|
||||||
|
// allow unused mut when cookies feature is disabled
|
||||||
|
#[allow(unused_mut)]
|
||||||
let mut head = self.head;
|
let mut head = self.head;
|
||||||
|
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
for cookie in self.cookies.delta() {
|
for cookie in self.cookies.delta() {
|
||||||
head.headers.insert(
|
head.headers.insert(
|
||||||
header::SET_COOKIE,
|
header::SET_COOKIE,
|
||||||
|
|
|
@ -32,6 +32,7 @@ use std::rc::Rc;
|
||||||
use std::{fmt, str};
|
use std::{fmt, str};
|
||||||
|
|
||||||
use actix_codec::Framed;
|
use actix_codec::Framed;
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
use actix_http::cookie::{Cookie, CookieJar};
|
use actix_http::cookie::{Cookie, CookieJar};
|
||||||
use actix_http::{ws, Payload, RequestHead};
|
use actix_http::{ws, Payload, RequestHead};
|
||||||
use actix_rt::time::timeout;
|
use actix_rt::time::timeout;
|
||||||
|
@ -54,8 +55,10 @@ pub struct WebsocketsRequest {
|
||||||
addr: Option<SocketAddr>,
|
addr: Option<SocketAddr>,
|
||||||
max_size: usize,
|
max_size: usize,
|
||||||
server_mode: bool,
|
server_mode: bool,
|
||||||
cookies: Option<CookieJar>,
|
|
||||||
config: Rc<ClientConfig>,
|
config: Rc<ClientConfig>,
|
||||||
|
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
|
cookies: Option<CookieJar>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebsocketsRequest {
|
impl WebsocketsRequest {
|
||||||
|
@ -89,6 +92,7 @@ impl WebsocketsRequest {
|
||||||
protocols: None,
|
protocols: None,
|
||||||
max_size: 65_536,
|
max_size: 65_536,
|
||||||
server_mode: false,
|
server_mode: false,
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
cookies: None,
|
cookies: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,6 +121,7 @@ impl WebsocketsRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a cookie
|
/// Set a cookie
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
pub fn cookie(mut self, cookie: Cookie<'_>) -> Self {
|
pub fn cookie(mut self, cookie: Cookie<'_>) -> Self {
|
||||||
if self.cookies.is_none() {
|
if self.cookies.is_none() {
|
||||||
let mut jar = CookieJar::new();
|
let mut jar = CookieJar::new();
|
||||||
|
@ -270,6 +275,7 @@ impl WebsocketsRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
// set cookies
|
// set cookies
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
if let Some(ref mut jar) = self.cookies {
|
if let Some(ref mut jar) = self.cookies {
|
||||||
let cookie: String = jar
|
let cookie: String = jar
|
||||||
.delta()
|
.delta()
|
||||||
|
|
|
@ -7,17 +7,23 @@ use actix_http_test::test_server;
|
||||||
use actix_service::{map_config, ServiceFactoryExt};
|
use actix_service::{map_config, ServiceFactoryExt};
|
||||||
use actix_web::http::Version;
|
use actix_web::http::Version;
|
||||||
use actix_web::{dev::AppConfig, web, App, HttpResponse};
|
use actix_web::{dev::AppConfig, web, App, HttpResponse};
|
||||||
use openssl::ssl::{SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode};
|
use openssl::{
|
||||||
|
pkey::PKey,
|
||||||
|
ssl::{SslAcceptor, SslConnector, SslMethod, SslVerifyMode},
|
||||||
|
x509::X509,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn tls_config() -> SslAcceptor {
|
||||||
|
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
|
||||||
|
let cert_file = cert.serialize_pem().unwrap();
|
||||||
|
let key_file = cert.serialize_private_key_pem();
|
||||||
|
let cert = X509::from_pem(cert_file.as_bytes()).unwrap();
|
||||||
|
let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap();
|
||||||
|
|
||||||
fn ssl_acceptor() -> SslAcceptor {
|
|
||||||
// load ssl keys
|
|
||||||
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
||||||
builder
|
builder.set_certificate(&cert).unwrap();
|
||||||
.set_private_key_file("../tests/key.pem", SslFiletype::PEM)
|
builder.set_private_key(&key).unwrap();
|
||||||
.unwrap();
|
|
||||||
builder
|
|
||||||
.set_certificate_chain_file("../tests/cert.pem")
|
|
||||||
.unwrap();
|
|
||||||
builder.set_alpn_select_callback(|_, protos| {
|
builder.set_alpn_select_callback(|_, protos| {
|
||||||
const H2: &[u8] = b"\x02h2";
|
const H2: &[u8] = b"\x02h2";
|
||||||
if protos.windows(3).any(|window| window == H2) {
|
if protos.windows(3).any(|window| window == H2) {
|
||||||
|
@ -27,6 +33,7 @@ fn ssl_acceptor() -> SslAcceptor {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
builder.set_alpn_protos(b"\x02h2").unwrap();
|
builder.set_alpn_protos(b"\x02h2").unwrap();
|
||||||
|
|
||||||
builder.build()
|
builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +45,7 @@ async fn test_connection_window_size() {
|
||||||
App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))),
|
App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))),
|
||||||
|_| AppConfig::default(),
|
|_| AppConfig::default(),
|
||||||
))
|
))
|
||||||
.openssl(ssl_acceptor())
|
.openssl(tls_config())
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -11,17 +11,23 @@ use actix_service::{map_config, pipeline_factory, ServiceFactoryExt};
|
||||||
use actix_web::http::Version;
|
use actix_web::http::Version;
|
||||||
use actix_web::{dev::AppConfig, web, App, HttpResponse};
|
use actix_web::{dev::AppConfig, web, App, HttpResponse};
|
||||||
use futures_util::future::ok;
|
use futures_util::future::ok;
|
||||||
use openssl::ssl::{SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode};
|
use openssl::{
|
||||||
|
pkey::PKey,
|
||||||
|
ssl::{SslAcceptor, SslConnector, SslMethod, SslVerifyMode},
|
||||||
|
x509::X509,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn tls_config() -> SslAcceptor {
|
||||||
|
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
|
||||||
|
let cert_file = cert.serialize_pem().unwrap();
|
||||||
|
let key_file = cert.serialize_private_key_pem();
|
||||||
|
let cert = X509::from_pem(cert_file.as_bytes()).unwrap();
|
||||||
|
let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap();
|
||||||
|
|
||||||
fn ssl_acceptor() -> SslAcceptor {
|
|
||||||
// load ssl keys
|
|
||||||
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
||||||
builder
|
builder.set_certificate(&cert).unwrap();
|
||||||
.set_private_key_file("../tests/key.pem", SslFiletype::PEM)
|
builder.set_private_key(&key).unwrap();
|
||||||
.unwrap();
|
|
||||||
builder
|
|
||||||
.set_certificate_chain_file("../tests/cert.pem")
|
|
||||||
.unwrap();
|
|
||||||
builder.set_alpn_select_callback(|_, protos| {
|
builder.set_alpn_select_callback(|_, protos| {
|
||||||
const H2: &[u8] = b"\x02h2";
|
const H2: &[u8] = b"\x02h2";
|
||||||
if protos.windows(3).any(|window| window == H2) {
|
if protos.windows(3).any(|window| window == H2) {
|
||||||
|
@ -31,6 +37,7 @@ fn ssl_acceptor() -> SslAcceptor {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
builder.set_alpn_protos(b"\x02h2").unwrap();
|
builder.set_alpn_protos(b"\x02h2").unwrap();
|
||||||
|
|
||||||
builder.build()
|
builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +58,7 @@ async fn test_connection_reuse_h2() {
|
||||||
App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))),
|
App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))),
|
||||||
|_| AppConfig::default(),
|
|_| AppConfig::default(),
|
||||||
))
|
))
|
||||||
.openssl(ssl_acceptor())
|
.openssl(tls_config())
|
||||||
.map_err(|_| ()),
|
.map_err(|_| ()),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,9 +13,11 @@ pub enum UrlGenerationError {
|
||||||
/// Resource not found
|
/// Resource not found
|
||||||
#[display(fmt = "Resource not found")]
|
#[display(fmt = "Resource not found")]
|
||||||
ResourceNotFound,
|
ResourceNotFound,
|
||||||
|
|
||||||
/// Not all path pattern covered
|
/// Not all path pattern covered
|
||||||
#[display(fmt = "Not all path pattern covered")]
|
#[display(fmt = "Not all path pattern covered")]
|
||||||
NotEnoughElements,
|
NotEnoughElements,
|
||||||
|
|
||||||
/// URL parse error
|
/// URL parse error
|
||||||
#[display(fmt = "{}", _0)]
|
#[display(fmt = "{}", _0)]
|
||||||
ParseError(UrlParseError),
|
ParseError(UrlParseError),
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
//! ## Crate Features
|
//! ## Crate Features
|
||||||
//!
|
//!
|
||||||
//! * `compress` - content encoding compression support (enabled by default)
|
//! * `compress` - content encoding compression support (enabled by default)
|
||||||
|
//! * `cookies` - cookies support (enabled by default)
|
||||||
//! * `openssl` - HTTPS support via `openssl` crate, supports `HTTP/2`
|
//! * `openssl` - HTTPS support via `openssl` crate, supports `HTTP/2`
|
||||||
//! * `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2`
|
//! * `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2`
|
||||||
//! * `secure-cookies` - secure cookies support
|
//! * `secure-cookies` - secure cookies support
|
||||||
|
@ -98,8 +99,10 @@ pub mod test;
|
||||||
pub(crate) mod types;
|
pub(crate) mod types;
|
||||||
pub mod web;
|
pub mod web;
|
||||||
|
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
|
pub use actix_http::cookie;
|
||||||
pub use actix_http::Response as HttpResponse;
|
pub use actix_http::Response as HttpResponse;
|
||||||
pub use actix_http::{body, cookie, http, Error, HttpMessage, ResponseError, Result};
|
pub use actix_http::{body, http, Error, HttpMessage, ResponseError, Result};
|
||||||
pub use actix_rt as rt;
|
pub use actix_rt as rt;
|
||||||
pub use actix_web_codegen::*;
|
pub use actix_web_codegen::*;
|
||||||
|
|
||||||
|
|
|
@ -123,18 +123,24 @@ impl<B: MessageBody + Unpin + 'static> MapServiceResponseBody for ServiceRespons
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
// easier to code when cookies feature is disabled
|
||||||
|
#![allow(unused_imports)]
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use actix_service::IntoService;
|
use actix_service::IntoService;
|
||||||
|
|
||||||
use crate::dev::ServiceRequest;
|
use crate::dev::ServiceRequest;
|
||||||
use crate::http::StatusCode;
|
use crate::http::StatusCode;
|
||||||
use crate::middleware::{Compress, Condition, Logger};
|
use crate::middleware::{self, Condition, Logger};
|
||||||
use crate::test::{call_service, init_service, TestRequest};
|
use crate::test::{call_service, init_service, TestRequest};
|
||||||
use crate::{web, App, HttpResponse};
|
use crate::{web, App, HttpResponse};
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
async fn test_scope_middleware() {
|
async fn test_scope_middleware() {
|
||||||
|
use crate::middleware::Compress;
|
||||||
|
|
||||||
let logger = Logger::default();
|
let logger = Logger::default();
|
||||||
let compress = Compress::default();
|
let compress = Compress::default();
|
||||||
|
|
||||||
|
@ -154,7 +160,10 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
async fn test_resource_scope_middleware() {
|
async fn test_resource_scope_middleware() {
|
||||||
|
use crate::middleware::Compress;
|
||||||
|
|
||||||
let logger = Logger::default();
|
let logger = Logger::default();
|
||||||
let compress = Compress::default();
|
let compress = Compress::default();
|
||||||
|
|
||||||
|
|
|
@ -429,12 +429,14 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
fn test_no_request_cookies() {
|
fn test_no_request_cookies() {
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
assert!(req.cookies().unwrap().is_empty());
|
assert!(req.cookies().unwrap().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
fn test_request_cookies() {
|
fn test_request_cookies() {
|
||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.append_header((header::COOKIE, "cookie1=value1"))
|
.append_header((header::COOKIE, "cookie1=value1"))
|
||||||
|
|
|
@ -6,10 +6,12 @@ use std::sync::mpsc;
|
||||||
use std::{fmt, net, thread, time};
|
use std::{fmt, net, thread, time};
|
||||||
|
|
||||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
|
use actix_http::cookie::Cookie;
|
||||||
use actix_http::http::header::{ContentType, IntoHeaderPair};
|
use actix_http::http::header::{ContentType, IntoHeaderPair};
|
||||||
use actix_http::http::{Method, StatusCode, Uri, Version};
|
use actix_http::http::{Method, StatusCode, Uri, Version};
|
||||||
use actix_http::test::TestRequest as HttpTestRequest;
|
use actix_http::test::TestRequest as HttpTestRequest;
|
||||||
use actix_http::{cookie::Cookie, ws, Extensions, HttpService, Request};
|
use actix_http::{ws, Extensions, HttpService, Request};
|
||||||
use actix_router::{Path, ResourceDef, Url};
|
use actix_router::{Path, ResourceDef, Url};
|
||||||
use actix_rt::{time::sleep, System};
|
use actix_rt::{time::sleep, System};
|
||||||
use actix_service::{map_config, IntoService, IntoServiceFactory, Service, ServiceFactory};
|
use actix_service::{map_config, IntoService, IntoServiceFactory, Service, ServiceFactory};
|
||||||
|
@ -438,6 +440,7 @@ impl TestRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set cookie for this request.
|
/// Set cookie for this request.
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
pub fn cookie(mut self, cookie: Cookie<'_>) -> Self {
|
pub fn cookie(mut self, cookie: Cookie<'_>) -> Self {
|
||||||
self.req.cookie(cookie);
|
self.req.cookie(cookie);
|
||||||
self
|
self
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDEDCCAfgCCQCQdmIZc/Ib/jANBgkqhkiG9w0BAQsFADBKMQswCQYDVQQGEwJ1
|
|
||||||
czELMAkGA1UECAwCY2ExCzAJBgNVBAcMAnNmMSEwHwYJKoZIhvcNAQkBFhJmYWZo
|
|
||||||
cmQ5MUBnbWFpbC5jb20wHhcNMTkxMTE5MTEwNjU1WhcNMjkxMTE2MTEwNjU1WjBK
|
|
||||||
MQswCQYDVQQGEwJ1czELMAkGA1UECAwCY2ExCzAJBgNVBAcMAnNmMSEwHwYJKoZI
|
|
||||||
hvcNAQkBFhJmYWZocmQ5MUBnbWFpbC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
|
||||||
DwAwggEKAoIBAQDcnaz12CKzUL7248V7Axhms/O9UQXfAdw0yolEfC3P5jADa/1C
|
|
||||||
+kLWKjAc2coqDSbGsrsR6KiH2g06Kunx+tSGqUO+Sct7HEehmxndiSwx/hfMWezy
|
|
||||||
XRe/olcHFTeCk/Tllz4xGEplhPua6GLhJygLOhAMiV8cwCYrgyPqsDduExLDFCqc
|
|
||||||
K2xntIPreumXpiE3QY4+MWyteiJko4IWDFf/UwwsdCY5MlFfw1F/Uv9vz7FfOfvu
|
|
||||||
GccHd/ex8cOwotUqd6emZb+0bVE24Sv8U+yLnHIVx/tOkxgMAnJEpAnf2G3Wp3zU
|
|
||||||
b2GJosbmfGaf+xTfnGGhTLLL7kCtva+NvZr5AgMBAAEwDQYJKoZIhvcNAQELBQAD
|
|
||||||
ggEBANftoL8zDGrjCwWvct8kOOqset2ukK8vjIGwfm88CKsy0IfSochNz2qeIu9R
|
|
||||||
ZuO7c0pfjmRkir9ZQdq9vXgG3ccL9UstFsferPH9W3YJ83kgXg3fa0EmCiN/0hwz
|
|
||||||
6Ij1ZBiN1j3+d6+PJPgyYFNu2nGwox5mJ9+aRAGe0/9c63PEOY8P2TI4HsiPmYSl
|
|
||||||
fFR8k/03vr6e+rTKW85BgctjvYKe/TnFxeCQ7dZ+na7vlEtch4tNmy6O/vEk2kCt
|
|
||||||
5jW0DUxhmRsv2wGmfFRI0+LotHjoXQQZi6nN5aGL3odaGF3gYwIVlZNd3AdkwDQz
|
|
||||||
BzG0ZwXuDDV9bSs3MfWEWcy4xuU=
|
|
||||||
-----END CERTIFICATE-----
|
|
|
@ -1,28 +0,0 @@
|
||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDcnaz12CKzUL72
|
|
||||||
48V7Axhms/O9UQXfAdw0yolEfC3P5jADa/1C+kLWKjAc2coqDSbGsrsR6KiH2g06
|
|
||||||
Kunx+tSGqUO+Sct7HEehmxndiSwx/hfMWezyXRe/olcHFTeCk/Tllz4xGEplhPua
|
|
||||||
6GLhJygLOhAMiV8cwCYrgyPqsDduExLDFCqcK2xntIPreumXpiE3QY4+MWyteiJk
|
|
||||||
o4IWDFf/UwwsdCY5MlFfw1F/Uv9vz7FfOfvuGccHd/ex8cOwotUqd6emZb+0bVE2
|
|
||||||
4Sv8U+yLnHIVx/tOkxgMAnJEpAnf2G3Wp3zUb2GJosbmfGaf+xTfnGGhTLLL7kCt
|
|
||||||
va+NvZr5AgMBAAECggEBAKoU0UwzVgVCQgca8Jt2dnBvWYDhnxIfYAI/BvaKedMm
|
|
||||||
1ms87OKfB7oOiksjyI0E2JklH72dzZf2jm4CuZt5UjGC+xwPzlTaJ4s6hQVbBHyC
|
|
||||||
NRyxU1BCXtW5tThbrhD4OjxqjmLRJEIB9OunLtwAEQoeuFLB8Va7+HFhR+Zd9k3f
|
|
||||||
7aVA93pC5A50NRbZlke4miJ3Q8n7ZF0+UmxkBfm3fbqLk7aMWkoEKwLLTadjRlu1
|
|
||||||
bBp0YDStX66I/p1kujqBOdh6VpPvxFOa1sV9pq0jeiGc9YfSkzRSKzIn8GoyviFB
|
|
||||||
fHeszQdNlcnrSDSNnMABAw+ZpxUO7SCaftjwejEmKZUCgYEA+TY43VpmV95eY7eo
|
|
||||||
WKwGepiHE0fwQLuKGELmZdZI80tFi73oZMuiB5WzwmkaKGcJmm7KGE9KEvHQCo9j
|
|
||||||
xvmktBR0VEZH8pmVfun+4h6+0H7m/NKMBBeOyv/IK8jBgHjkkB6e6nmeR7CqTxCw
|
|
||||||
tf9tbajl1QN8gNzXZSjBDT/lanMCgYEA4qANOKOSiEARtgwyXQeeSJcM2uPv6zF3
|
|
||||||
ffM7vjSedtuEOHUSVeyBP/W8KDt7zyPppO/WNbURHS+HV0maS9yyj6zpVS2HGmbs
|
|
||||||
3fetswsQ+zYVdokW89x4oc2z4XOGHd1LcSlyhRwPt0u2g1E9L0irwTQLWU0npFmG
|
|
||||||
PRf7sN9+LeMCgYAGkDUDL2ROoB6gRa/7Vdx90hKMoXJkYgwLA4gJ2pDlR3A3c/Lw
|
|
||||||
5KQJyxmG3zm/IqeQF6be6QesZA30mT4peV2rGHbP2WH/s6fKReNelSy1VQJEWk8x
|
|
||||||
tGUgV4gwDwN5nLV4TjYlOrq+bJqvpmLhCC8bmj0jVQosYqSRl3cuICasnQKBgGlV
|
|
||||||
VO/Xb1su1EyWPK5qxRIeSxZOTYw2sMB01nbgxCqge0M2fvA6/hQ5ZlwY0cIEgits
|
|
||||||
YlcSMsMq/TAAANxz1vbaupUhlSMbZcsBvNV0Nk9c4vr2Wxm7hsJF9u66IEMvQUp2
|
|
||||||
pkjiMxfR9CHzF4orr9EcHI5EQ0Grbq5kwFKEfoRbAoGAcWoFPILeJOlp2yW/Ds3E
|
|
||||||
g2fQdI9BAamtEZEaslJmZMmsDTg5ACPcDkOSFEQIaJ7wLPXeZy74FVk/NrY5F8Gz
|
|
||||||
bjX9OD/xzwp852yW5L9r62vYJakAlXef5jI6CFdYKDDCcarU0S7W5k6kq9n+wrBR
|
|
||||||
i1NklYmUAMr2q59uJA5zsic=
|
|
||||||
-----END PRIVATE KEY-----
|
|
|
@ -73,15 +73,22 @@ async fn test_start() {
|
||||||
|
|
||||||
#[cfg(feature = "openssl")]
|
#[cfg(feature = "openssl")]
|
||||||
fn ssl_acceptor() -> std::io::Result<SslAcceptorBuilder> {
|
fn ssl_acceptor() -> std::io::Result<SslAcceptorBuilder> {
|
||||||
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
|
use openssl::{
|
||||||
// load ssl keys
|
pkey::PKey,
|
||||||
|
ssl::{SslAcceptor, SslMethod},
|
||||||
|
x509::X509,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
|
||||||
|
let cert_file = cert.serialize_pem().unwrap();
|
||||||
|
let key_file = cert.serialize_private_key_pem();
|
||||||
|
let cert = X509::from_pem(cert_file.as_bytes()).unwrap();
|
||||||
|
let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap();
|
||||||
|
|
||||||
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
||||||
builder
|
builder.set_certificate(&cert).unwrap();
|
||||||
.set_private_key_file("tests/key.pem", SslFiletype::PEM)
|
builder.set_private_key(&key).unwrap();
|
||||||
.unwrap();
|
|
||||||
builder
|
|
||||||
.set_certificate_chain_file("tests/cert.pem")
|
|
||||||
.unwrap();
|
|
||||||
Ok(builder)
|
Ok(builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,11 @@ use flate2::{
|
||||||
Compression,
|
Compression,
|
||||||
};
|
};
|
||||||
use futures_util::ready;
|
use futures_util::ready;
|
||||||
|
use openssl::{
|
||||||
|
pkey::PKey,
|
||||||
|
ssl::{SslAcceptor, SslMethod},
|
||||||
|
x509::X509,
|
||||||
|
};
|
||||||
use rand::{distributions::Alphanumeric, Rng};
|
use rand::{distributions::Alphanumeric, Rng};
|
||||||
|
|
||||||
use actix_web::dev::BodyEncoding;
|
use actix_web::dev::BodyEncoding;
|
||||||
|
@ -49,6 +54,30 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
|
||||||
Hello World Hello World Hello World Hello World Hello World \
|
Hello World Hello World Hello World Hello World Hello World \
|
||||||
Hello World Hello World Hello World Hello World Hello World";
|
Hello World Hello World Hello World Hello World Hello World";
|
||||||
|
|
||||||
|
fn openssl_config() -> SslAcceptor {
|
||||||
|
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
|
||||||
|
let cert_file = cert.serialize_pem().unwrap();
|
||||||
|
let key_file = cert.serialize_private_key_pem();
|
||||||
|
let cert = X509::from_pem(cert_file.as_bytes()).unwrap();
|
||||||
|
let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
||||||
|
builder.set_certificate(&cert).unwrap();
|
||||||
|
builder.set_private_key(&key).unwrap();
|
||||||
|
|
||||||
|
builder.set_alpn_select_callback(|_, protos| {
|
||||||
|
const H2: &[u8] = b"\x02h2";
|
||||||
|
if protos.windows(3).any(|window| window == H2) {
|
||||||
|
Ok(b"h2")
|
||||||
|
} else {
|
||||||
|
Err(openssl::ssl::AlpnError::NOACK)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.set_alpn_protos(b"\x02h2").unwrap();
|
||||||
|
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
struct TestBody {
|
struct TestBody {
|
||||||
data: Bytes,
|
data: Bytes,
|
||||||
chunk_size: usize,
|
chunk_size: usize,
|
||||||
|
@ -700,18 +729,8 @@ async fn test_brotli_encoding_large() {
|
||||||
#[cfg(feature = "openssl")]
|
#[cfg(feature = "openssl")]
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_brotli_encoding_large_openssl() {
|
async fn test_brotli_encoding_large_openssl() {
|
||||||
// load ssl keys
|
|
||||||
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
|
|
||||||
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
|
||||||
builder
|
|
||||||
.set_private_key_file("tests/key.pem", SslFiletype::PEM)
|
|
||||||
.unwrap();
|
|
||||||
builder
|
|
||||||
.set_certificate_chain_file("tests/cert.pem")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let data = STR.repeat(10);
|
let data = STR.repeat(10);
|
||||||
let srv = test::start_with(test::config().openssl(builder.build()), move || {
|
let srv = test::start_with(test::config().openssl(openssl_config()), move || {
|
||||||
App::new().service(web::resource("/").route(web::to(|bytes: Bytes| {
|
App::new().service(web::resource("/").route(web::to(|bytes: Bytes| {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.encoding(actix_web::http::ContentEncoding::Identity)
|
.encoding(actix_web::http::ContentEncoding::Identity)
|
||||||
|
@ -739,53 +758,72 @@ async fn test_brotli_encoding_large_openssl() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "rustls", feature = "openssl"))]
|
#[cfg(all(feature = "rustls", feature = "openssl"))]
|
||||||
#[actix_rt::test]
|
mod plus_rustls {
|
||||||
async fn test_reading_deflate_encoding_large_random_rustls() {
|
|
||||||
use rustls::internal::pemfile::{certs, pkcs8_private_keys};
|
|
||||||
use rustls::{NoClientAuth, ServerConfig};
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
|
|
||||||
let data = rand::thread_rng()
|
use rustls::{
|
||||||
.sample_iter(&Alphanumeric)
|
internal::pemfile::{certs, pkcs8_private_keys},
|
||||||
.take(160_000)
|
NoClientAuth, ServerConfig as RustlsServerConfig,
|
||||||
.map(char::from)
|
};
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
// load ssl keys
|
use super::*;
|
||||||
let mut config = ServerConfig::new(NoClientAuth::new());
|
|
||||||
let cert_file = &mut BufReader::new(File::open("tests/cert.pem").unwrap());
|
|
||||||
let key_file = &mut BufReader::new(File::open("tests/key.pem").unwrap());
|
|
||||||
let cert_chain = certs(cert_file).unwrap();
|
|
||||||
let mut keys = pkcs8_private_keys(key_file).unwrap();
|
|
||||||
config.set_single_cert(cert_chain, keys.remove(0)).unwrap();
|
|
||||||
|
|
||||||
let srv = test::start_with(test::config().rustls(config), || {
|
fn rustls_config() -> RustlsServerConfig {
|
||||||
App::new().service(web::resource("/").route(web::to(|bytes: Bytes| {
|
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
|
||||||
HttpResponse::Ok()
|
let cert_file = cert.serialize_pem().unwrap();
|
||||||
.encoding(actix_web::http::ContentEncoding::Identity)
|
let key_file = cert.serialize_private_key_pem();
|
||||||
.body(bytes)
|
|
||||||
})))
|
|
||||||
});
|
|
||||||
|
|
||||||
// encode data
|
let mut config = RustlsServerConfig::new(NoClientAuth::new());
|
||||||
let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
|
let cert_file = &mut BufReader::new(cert_file.as_bytes());
|
||||||
e.write_all(data.as_ref()).unwrap();
|
let key_file = &mut BufReader::new(key_file.as_bytes());
|
||||||
let enc = e.finish().unwrap();
|
|
||||||
|
|
||||||
// client request
|
let cert_chain = certs(cert_file).unwrap();
|
||||||
let req = srv
|
let mut keys = pkcs8_private_keys(key_file).unwrap();
|
||||||
.post("/")
|
config.set_single_cert(cert_chain, keys.remove(0)).unwrap();
|
||||||
.insert_header((actix_web::http::header::CONTENT_ENCODING, "deflate"))
|
|
||||||
.send_stream(TestBody::new(Bytes::from(enc), 1024));
|
|
||||||
|
|
||||||
let mut response = req.await.unwrap();
|
config
|
||||||
assert!(response.status().is_success());
|
}
|
||||||
|
|
||||||
// read response
|
#[actix_rt::test]
|
||||||
let bytes = response.body().await.unwrap();
|
async fn test_reading_deflate_encoding_large_random_rustls() {
|
||||||
assert_eq!(bytes.len(), data.len());
|
use rustls::internal::pemfile::{certs, pkcs8_private_keys};
|
||||||
assert_eq!(bytes, Bytes::from(data));
|
use rustls::{NoClientAuth, ServerConfig};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
|
||||||
|
let data = rand::thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(160_000)
|
||||||
|
.map(char::from)
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let srv = test::start_with(test::config().rustls(rustls_config()), || {
|
||||||
|
App::new().service(web::resource("/").route(web::to(|bytes: Bytes| {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.encoding(actix_web::http::ContentEncoding::Identity)
|
||||||
|
.body(bytes)
|
||||||
|
})))
|
||||||
|
});
|
||||||
|
|
||||||
|
// encode data
|
||||||
|
let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
|
||||||
|
e.write_all(data.as_ref()).unwrap();
|
||||||
|
let enc = e.finish().unwrap();
|
||||||
|
|
||||||
|
// client request
|
||||||
|
let req = srv
|
||||||
|
.post("/")
|
||||||
|
.insert_header((actix_web::http::header::CONTENT_ENCODING, "deflate"))
|
||||||
|
.send_stream(TestBody::new(Bytes::from(enc), 1024));
|
||||||
|
|
||||||
|
let mut response = req.await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
// read response
|
||||||
|
let bytes = response.body().await.unwrap();
|
||||||
|
assert_eq!(bytes.len(), data.len());
|
||||||
|
assert_eq!(bytes, Bytes::from(data));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
|
@ -806,15 +844,15 @@ async fn test_server_cookies() {
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let req = srv.get("/");
|
||||||
|
let res = req.send().await.unwrap();
|
||||||
|
assert!(res.status().is_success());
|
||||||
|
|
||||||
let first_cookie = http::CookieBuilder::new("first", "first_value")
|
let first_cookie = http::CookieBuilder::new("first", "first_value")
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.finish();
|
.finish();
|
||||||
let second_cookie = http::Cookie::new("second", "second_value");
|
let second_cookie = http::Cookie::new("second", "second_value");
|
||||||
|
|
||||||
let req = srv.get("/");
|
|
||||||
let res = req.send().await.unwrap();
|
|
||||||
assert!(res.status().is_success());
|
|
||||||
|
|
||||||
let cookies = res.cookies().expect("To have cookies");
|
let cookies = res.cookies().expect("To have cookies");
|
||||||
assert_eq!(cookies.len(), 2);
|
assert_eq!(cookies.len(), 2);
|
||||||
if cookies[0] == first_cookie {
|
if cookies[0] == first_cookie {
|
||||||
|
|
Loading…
Reference in New Issue