1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-12 21:43:41 +02:00

Compare commits

...

35 Commits

Author SHA1 Message Date
ab5eb7c1aa prepare actix-multipart release 0.4.0-beta.8 2021-11-22 18:48:14 +00:00
18b8ef0765 prepare actix-test release 0.1.0-beta.7 2021-11-22 18:47:43 +00:00
b806b4773c prepare actix-http-test release 3.0.0-beta.7 2021-11-22 18:46:58 +00:00
0062d99b6f prepare actix-files release 0.6.0-beta.9 2021-11-22 18:46:19 +00:00
99e6a9c26d prepare awc release 3.0.0-beta.11 2021-11-22 18:41:43 +00:00
5f5bd2184e prepare actix-web release 4.0.0-beta.12 2021-11-22 18:20:55 +00:00
88e074879d prepare actix-http release 3.0.0-beta.13 2021-11-22 18:19:09 +00:00
e7987e7429 awc: support http2 over plain tcp with feature flag (#2439)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-11-22 18:16:56 +00:00
a172f5968d prepare for actix-tls v3 beta 9 (#2456) 2021-11-22 15:37:23 +00:00
a2a42ec152 use anybody in doc test 2021-11-22 01:35:33 +00:00
dd347e0bd0 implement io-uring for actix-files (#2408)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-11-22 01:19:09 +00:00
194a691537 files: 304 Not Modified responses omit Content-Length header (#2453) 2021-11-19 14:04:12 +00:00
56ee97f722 add files path traversal tests 2021-11-18 18:14:34 +00:00
66620a1012 simplify handler.rs (#2450) 2021-11-17 20:11:35 +00:00
e33618ed6d ensure content disposition header in multipart (#2451)
Co-authored-by: Craig Pastro <craig.pastro@gmail.com>
2021-11-17 17:44:50 +00:00
1fe309bcc6 increase ci test timeout 2021-11-17 15:32:42 +00:00
168a7284d3 fix actix_http::Error conversion. (#2449) 2021-11-17 13:13:05 +00:00
68a3acb9c2 bump zstd dep 2021-11-16 23:22:29 +00:00
84c6d25fd3 bump env logger dep 2021-11-16 23:07:08 +00:00
0a135c7dc9 bump actix-codec to 0.4.1 2021-11-16 22:41:24 +00:00
668a33c793 remove internal usage of Body 2021-11-16 22:10:30 +00:00
d8cbb879dd make AnyBody generic on Body type (#2448) 2021-11-16 21:41:35 +00:00
13cf5a9e44 remove chunked encoding header for websockets 2021-11-16 16:55:45 +00:00
4df1cd78b7 simplify AnyBody and BodySize (#2446) 2021-11-16 09:21:10 +00:00
e8a0e16863 run tarpaulin on workspace 2021-11-15 18:11:51 +00:00
a2f59c02f7 bump actix-server to beta 9 (#2442) 2021-11-15 04:03:33 +00:00
2754608f3c fix codegen tests 2021-11-08 02:46:43 +00:00
c020cedb63 Log internal server errors (#2387) 2021-11-07 17:02:23 +00:00
5e554dca35 fix awc clippy warning (#2431) 2021-11-04 15:57:55 +00:00
6ec2d7b909 add keep alive to h2 through ping pong (#2433) 2021-11-04 15:15:23 +00:00
ec6d284a8e improve "data no configured" message (#2429) 2021-10-31 13:19:21 +00:00
be9530eb72 avoid building actix-tls with no-default-features (#2426) 2021-10-26 13:16:48 +01:00
855e260fdb Add html_utf8 content type. (#2423) 2021-10-26 09:24:38 +01:00
d13854505f move actix_http::client module to awc (#2425) 2021-10-26 00:37:40 +01:00
d40b6748bc remove dead dep (#2420) 2021-10-22 00:22:58 +01:00
105 changed files with 2748 additions and 1669 deletions

View File

@ -1,14 +1,12 @@
[alias] [alias]
chk = "check --workspace --all-features --tests --examples --bins" lint = "clippy --workspace --tests --examples --bins -- -Dclippy::todo"
lint = "clippy --workspace --all-features --tests --examples --bins" lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dclippy::todo"
ci-min = "hack check --workspace --no-default-features"
ci-min-test = "hack check --workspace --no-default-features --tests --examples"
ci-default = "check --workspace --bins --tests --examples"
ci-full = "check --workspace --all-features --bins --tests --examples"
ci-test = "test --workspace --all-features --lib --tests --no-fail-fast -- --nocapture"
ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture"
ci-feature-powerset-check-no-tls="hack --workspace --feature-powerset --skip=__compress,rustls,openssl check" # lib checking
ci-feature-powerset-check-rustls="hack --workspace --feature-powerset --features=rustls --skip=__compress,openssl check" ci-check-min = "hack --workspace check --no-default-features"
ci-feature-powerset-check-openssl="hack --workspace --feature-powerset --features=openssl --skip=__compress,rustls check" ci-check-default = "hack --workspace check"
ci-feature-powerset-check-all="hack --workspace --feature-powerset --skip=__compress check" ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,io-uring check"
ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check"
# testing
ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture"

View File

@ -62,26 +62,34 @@ jobs:
- name: check minimal - name: check minimal
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: ci-min } with: { command: ci-check-min }
- name: check minimal + tests
uses: actions-rs/cargo@v1
with: { command: ci-min-test }
- name: check default - name: check default
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: ci-default } with: { command: ci-check-default }
- name: check full
uses: actions-rs/cargo@v1
with: { command: ci-full }
- name: tests - name: tests
uses: actions-rs/cargo@v1 timeout-minutes: 60
timeout-minutes: 40 run: |
with: cargo test --lib --tests -p=actix-router --all-features
command: ci-test cargo test --lib --tests -p=actix-http --all-features
args: --skip=test_reading_deflate_encoding_large_random_rustls cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
cargo test --lib --tests -p=actix-web-codegen --all-features
cargo test --lib --tests -p=awc --all-features
cargo test --lib --tests -p=actix-http-test --all-features
cargo test --lib --tests -p=actix-test --all-features
cargo test --lib --tests -p=actix-files
cargo test --lib --tests -p=actix-multipart --all-features
cargo test --lib --tests -p=actix-web-actors --all-features
- name: tests (io-uring)
if: matrix.target.os == 'ubuntu-latest'
timeout-minutes: 60
run: >
sudo bash -c "ulimit -Sl 512
&& ulimit -Hl 512
&& PATH=$PATH:/usr/share/rust/.cargo/bin
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
- name: Clear the cargo caches - name: Clear the cargo caches
run: | run: |
@ -114,9 +122,12 @@ jobs:
args: cargo-hack args: cargo-hack
- name: check feature combinations - name: check feature combinations
# if: github.ref == 'refs/heads/master'
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: ci-feature-powerset-check-all } with: { command: ci-check-all-feature-powerset }
- name: check feature combinations
uses: actions-rs/cargo@v1
with: { command: ci-check-all-feature-powerset-linux }
coverage: coverage:
name: coverage name: coverage
@ -141,7 +152,7 @@ jobs:
if: github.ref == 'refs/heads/master' if: 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 --verbose cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose
- name: Upload to Codecov - name: Upload to Codecov
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master'
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v1
@ -166,13 +177,13 @@ jobs:
- name: Cache Dependencies - name: Cache Dependencies
uses: Swatinem/rust-cache@v1.3.0 uses: Swatinem/rust-cache@v1.3.0
- name: Install cargo-hack # - name: Install cargo-hack
uses: actions-rs/cargo@v1 # uses: actions-rs/cargo@v1
with: # with:
command: install # command: install
args: cargo-hack # args: cargo-hack
- name: doc tests - name: doc tests
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
timeout-minutes: 40 timeout-minutes: 60
with: { command: ci-doctest } with: { command: ci-doctest }

View File

@ -3,6 +3,32 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 4.0.0-beta.12 - 2021-11-22
### Changed
* Compress middleware's response type is now `AnyBody<Encoder<B>>`. [#2448]
### Fixed
* Relax `Unpin` bound on `S` (stream) parameter of `HttpResponseBuilder::streaming`. [#2448]
### Removed
* `dev::ResponseBody` re-export; is function is replaced by the new `dev::AnyBody` enum. [#2446]
[#2446]: https://github.com/actix/actix-web/pull/2446
[#2448]: https://github.com/actix/actix-web/pull/2448
## 4.0.0-beta.11 - 2021-11-15
### Added
* Re-export `dev::ServerHandle` from `actix-server`. [#2442]
### Changed
* `ContentType::html` now produces `text/html; charset=utf-8` instead of `text/html`. [#2423]
* Update `actix-server` to `2.0.0-beta.9`. [#2442]
[#2423]: https://github.com/actix/actix-web/pull/2423
[#2442]: https://github.com/actix/actix-web/pull/2442
## 4.0.0-beta.10 - 2021-10-20 ## 4.0.0-beta.10 - 2021-10-20
### Added ### Added
* Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362] * Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362]
@ -11,7 +37,7 @@
### Changed ### Changed
* Associated type `FromRequest::Config` was removed. [#2233] * Associated type `FromRequest::Config` was removed. [#2233]
* Inner field made private on `web::Payload`. [#2384] * Inner field made private on `web::Payload`. [#2384]
* `Data::into_inner` and `Data::get_ref` no longer require T: Sized. [#2403] * `Data::into_inner` and `Data::get_ref` no longer requires `T: Sized`. [#2403]
* Updated rustls to v0.20. [#2414] * Updated rustls to v0.20. [#2414]
* Minimum supported Rust version (MSRV) is now 1.52. * Minimum supported Rust version (MSRV) is now 1.52.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.0.0-beta.10" version = "4.0.0-beta.12"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
@ -65,16 +65,19 @@ rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"]
# Don't rely on these whatsoever. They may disappear at anytime. # Don't rely on these whatsoever. They may disappear at anytime.
__compress = [] __compress = []
# io-uring feature only avaiable for Linux OSes.
experimental-io-uring = ["actix-server/io-uring"]
[dependencies] [dependencies]
actix-codec = "0.4.0" actix-codec = "0.4.1"
actix-macros = "0.2.3" actix-macros = "0.2.3"
actix-rt = "2.2" actix-rt = "2.3"
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-beta.9"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-tls = { version = "3.0.0-beta.7", default-features = false, optional = true } actix-tls = { version = "3.0.0-beta.9", default-features = false, optional = true }
actix-http = "3.0.0-beta.11" actix-http = "3.0.0-beta.13"
actix-router = "0.5.0-beta.2" actix-router = "0.5.0-beta.2"
actix-web-codegen = "0.5.0-beta.5" actix-web-codegen = "0.5.0-beta.5"
@ -104,12 +107,12 @@ time = { version = "0.3", default-features = false, features = ["formatting"] }
url = "2.1" url = "2.1"
[dev-dependencies] [dev-dependencies]
actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.7", features = ["openssl", "rustls"] }
awc = { version = "3.0.0-beta.9", features = ["openssl"] } awc = { version = "3.0.0-beta.11", features = ["openssl"] }
brotli2 = "0.3.2" brotli2 = "0.3.2"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.8" env_logger = "0.9"
flate2 = "1.0.13" flate2 = "1.0.13"
futures-util = { version = "0.3.7", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
rand = "0.8" rand = "0.8"
@ -117,7 +120,7 @@ rcgen = "0.8"
rustls-pemfile = "0.2" rustls-pemfile = "0.2"
tls-openssl = { package = "openssl", version = "0.10.9" } tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.20.0" } tls-rustls = { package = "rustls", version = "0.20.0" }
zstd = "0.7" zstd = "0.9"
[profile.dev] [profile.dev]
# Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. # Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much.

View File

@ -6,10 +6,10 @@
<p> <p>
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.10)](https://docs.rs/actix-web/4.0.0-beta.10) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.12)](https://docs.rs/actix-web/4.0.0-beta.12)
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.10) [![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.12/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.12)
<br /> <br />
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)

View File

@ -3,6 +3,18 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.6.0-beta.9 - 2021-11-22
* Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408]
* Add `NamedFile::open_async`. [#2408]
* Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453]
* The `Responder` impl for `NamedFile` now has a boxed future associated type. [#2408]
* The `Service` impl for `NamedFileService` now has a boxed future associated type. [#2408]
* Add `impl Clone` for `FilesService`. [#2408]
[#2408]: https://github.com/actix/actix-web/pull/2408
[#2453]: https://github.com/actix/actix-web/pull/2453
## 0.6.0-beta.8 - 2021-10-20 ## 0.6.0-beta.8 - 2021-10-20
* Minimum supported Rust version (MSRV) is now 1.52. * Minimum supported Rust version (MSRV) is now 1.52.

View File

@ -1,7 +1,11 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.0-beta.8" version = "0.6.0-beta.9"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "Static file serving for Actix Web" description = "Static file serving for Actix Web"
keywords = ["actix", "http", "async", "futures"] keywords = ["actix", "http", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
@ -14,11 +18,13 @@ edition = "2018"
name = "actix_files" name = "actix_files"
path = "src/lib.rs" path = "src/lib.rs"
[features]
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
[dependencies] [dependencies]
actix-web = { version = "4.0.0-beta.10", default-features = false } actix-web = { version = "4.0.0-beta.11", default-features = false }
actix-http = "3.0.0-beta.11" actix-http = "3.0.0-beta.13"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-utils = "3.0.0"
askama_escape = "0.10" askama_escape = "0.10"
bitflags = "1" bitflags = "1"
@ -30,8 +36,11 @@ log = "0.4"
mime = "0.3" mime = "0.3"
mime_guess = "2.0.1" mime_guess = "2.0.1"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project-lite = "0.2.7"
tokio-uring = { version = "0.1", optional = true }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-web = "4.0.0-beta.10" actix-web = "4.0.0-beta.11"
actix-test = "0.1.0-beta.5" actix-test = "0.1.0-beta.7"

View File

@ -3,11 +3,11 @@
> Static file serving for Actix Web > Static file serving for Actix Web
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.8)](https://docs.rs/actix-files/0.6.0-beta.8) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.9)](https://docs.rs/actix-files/0.6.0-beta.9)
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
![License](https://img.shields.io/crates/l/actix-files.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.8/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.8) [![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.9/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.9)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -1,98 +1,278 @@
use std::{ use std::{
cmp, fmt, cmp, fmt,
fs::File,
future::Future, future::Future,
io::{self, Read, Seek}, io,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
use actix_web::{ use actix_web::error::Error;
error::{BlockingError, Error},
rt::task::{spawn_blocking, JoinHandle},
};
use bytes::Bytes; use bytes::Bytes;
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use pin_project_lite::pin_project;
#[doc(hidden)] use super::named::File;
/// A helper created from a `std::fs::File` which reads the file
/// chunk-by-chunk on a `ThreadPool`. pin_project! {
pub struct ChunkedReadFile { /// Adapter to read a `std::file::File` in chunks.
#[doc(hidden)]
pub struct ChunkedReadFile<F, Fut> {
size: u64, size: u64,
offset: u64, offset: u64,
state: ChunkedReadFileState, #[pin]
state: ChunkedReadFileState<Fut>,
counter: u64, counter: u64,
} callback: F,
enum ChunkedReadFileState {
File(Option<File>),
Future(JoinHandle<Result<(File, Bytes), io::Error>>),
}
impl ChunkedReadFile {
pub(crate) fn new(size: u64, offset: u64, file: File) -> Self {
Self {
size,
offset,
state: ChunkedReadFileState::File(Some(file)),
counter: 0,
}
} }
} }
impl fmt::Debug for ChunkedReadFile { #[cfg(not(feature = "experimental-io-uring"))]
pin_project! {
#[project = ChunkedReadFileStateProj]
#[project_replace = ChunkedReadFileStateProjReplace]
enum ChunkedReadFileState<Fut> {
File { file: Option<File>, },
Future { #[pin] fut: Fut },
}
}
#[cfg(feature = "experimental-io-uring")]
pin_project! {
#[project = ChunkedReadFileStateProj]
#[project_replace = ChunkedReadFileStateProjReplace]
enum ChunkedReadFileState<Fut> {
File { file: Option<(File, BytesMut)> },
Future { #[pin] fut: Fut },
}
}
impl<F, Fut> fmt::Debug for ChunkedReadFile<F, Fut> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("ChunkedReadFile") f.write_str("ChunkedReadFile")
} }
} }
impl Stream for ChunkedReadFile { pub(crate) fn new_chunked_read(
type Item = Result<Bytes, Error>; size: u64,
offset: u64,
file: File,
) -> impl Stream<Item = Result<Bytes, Error>> {
ChunkedReadFile {
size,
offset,
#[cfg(not(feature = "experimental-io-uring"))]
state: ChunkedReadFileState::File { file: Some(file) },
#[cfg(feature = "experimental-io-uring")]
state: ChunkedReadFileState::File {
file: Some((file, BytesMut::new())),
},
counter: 0,
callback: chunked_read_file_callback,
}
}
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { #[cfg(not(feature = "experimental-io-uring"))]
let this = self.as_mut().get_mut(); async fn chunked_read_file_callback(
match this.state { mut file: File,
ChunkedReadFileState::File(ref mut file) => { offset: u64,
let size = this.size; max_bytes: usize,
let offset = this.offset; ) -> Result<(File, Bytes), Error> {
let counter = this.counter; use io::{Read as _, Seek as _};
if size == counter {
Poll::Ready(None)
} else {
let mut file = file
.take()
.expect("ChunkedReadFile polled after completion");
let fut = spawn_blocking(move || {
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
let res = actix_web::rt::task::spawn_blocking(move || {
let mut buf = Vec::with_capacity(max_bytes); let mut buf = Vec::with_capacity(max_bytes);
file.seek(io::SeekFrom::Start(offset))?; file.seek(io::SeekFrom::Start(offset))?;
let n_bytes = let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
if n_bytes == 0 {
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
} else {
Ok((file, Bytes::from(buf)))
}
})
.await
.map_err(|_| actix_web::error::BlockingError)??;
Ok(res)
}
#[cfg(feature = "experimental-io-uring")]
async fn chunked_read_file_callback(
file: File,
offset: u64,
max_bytes: usize,
mut bytes_mut: BytesMut,
) -> io::Result<(File, Bytes, BytesMut)> {
bytes_mut.reserve(max_bytes);
let (res, mut bytes_mut) = file.read_at(bytes_mut, offset).await;
let n_bytes = res?;
if n_bytes == 0 { if n_bytes == 0 {
return Err(io::ErrorKind::UnexpectedEof.into()); return Err(io::ErrorKind::UnexpectedEof.into());
} }
Ok((file, Bytes::from(buf))) let bytes = bytes_mut.split_to(n_bytes).freeze();
});
this.state = ChunkedReadFileState::Future(fut); Ok((file, bytes, bytes_mut))
}
#[cfg(feature = "experimental-io-uring")]
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
where
F: Fn(File, u64, usize, BytesMut) -> Fut,
Fut: Future<Output = io::Result<(File, Bytes, BytesMut)>>,
{
type Item = Result<Bytes, Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let mut this = self.as_mut().project();
match this.state.as_mut().project() {
ChunkedReadFileStateProj::File { file } => {
let size = *this.size;
let offset = *this.offset;
let counter = *this.counter;
if size == counter {
Poll::Ready(None)
} else {
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
let (file, bytes_mut) = file
.take()
.expect("ChunkedReadFile polled after completion");
let fut = (this.callback)(file, offset, max_bytes, bytes_mut);
this.state
.project_replace(ChunkedReadFileState::Future { fut });
self.poll_next(cx) self.poll_next(cx)
} }
} }
ChunkedReadFileState::Future(ref mut fut) => { ChunkedReadFileStateProj::Future { fut } => {
let (file, bytes) = let (file, bytes, bytes_mut) = ready!(fut.poll(cx))?;
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
this.state = ChunkedReadFileState::File(Some(file));
this.offset += bytes.len() as u64; this.state.project_replace(ChunkedReadFileState::File {
this.counter += bytes.len() as u64; file: Some((file, bytes_mut)),
});
*this.offset += bytes.len() as u64;
*this.counter += bytes.len() as u64;
Poll::Ready(Some(Ok(bytes))) Poll::Ready(Some(Ok(bytes)))
} }
} }
} }
} }
#[cfg(not(feature = "experimental-io-uring"))]
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
where
F: Fn(File, u64, usize) -> Fut,
Fut: Future<Output = Result<(File, Bytes), Error>>,
{
type Item = Result<Bytes, Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let mut this = self.as_mut().project();
match this.state.as_mut().project() {
ChunkedReadFileStateProj::File { file } => {
let size = *this.size;
let offset = *this.offset;
let counter = *this.counter;
if size == counter {
Poll::Ready(None)
} else {
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
let file = file
.take()
.expect("ChunkedReadFile polled after completion");
let fut = (this.callback)(file, offset, max_bytes);
this.state
.project_replace(ChunkedReadFileState::Future { fut });
self.poll_next(cx)
}
}
ChunkedReadFileStateProj::Future { fut } => {
let (file, bytes) = ready!(fut.poll(cx))?;
this.state
.project_replace(ChunkedReadFileState::File { file: Some(file) });
*this.offset += bytes.len() as u64;
*this.counter += bytes.len() as u64;
Poll::Ready(Some(Ok(bytes)))
}
}
}
}
#[cfg(feature = "experimental-io-uring")]
use bytes_mut::BytesMut;
// TODO: remove new type and use bytes::BytesMut directly
#[doc(hidden)]
#[cfg(feature = "experimental-io-uring")]
mod bytes_mut {
use std::ops::{Deref, DerefMut};
use tokio_uring::buf::{IoBuf, IoBufMut};
#[derive(Debug)]
pub struct BytesMut(bytes::BytesMut);
impl BytesMut {
pub(super) fn new() -> Self {
Self(bytes::BytesMut::new())
}
}
impl Deref for BytesMut {
type Target = bytes::BytesMut;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for BytesMut {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
unsafe impl IoBuf for BytesMut {
fn stable_ptr(&self) -> *const u8 {
self.0.as_ptr()
}
fn bytes_init(&self) -> usize {
self.0.len()
}
fn bytes_total(&self) -> usize {
self.0.capacity()
}
}
unsafe impl IoBufMut for BytesMut {
fn stable_mut_ptr(&mut self) -> *mut u8 {
self.0.as_mut_ptr()
}
unsafe fn set_init(&mut self, init_len: usize) {
if self.len() < init_len {
self.0.set_len(init_len);
}
}
}
}

View File

@ -6,7 +6,6 @@ use std::{
}; };
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt}; use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
use actix_utils::future::ok;
use actix_web::{ use actix_web::{
dev::{ dev::{
AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest,
@ -20,8 +19,9 @@ use actix_web::{
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use crate::{ use crate::{
directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService, directory_listing, named,
MimeOverride, PathFilter, service::{FilesService, FilesServiceInner},
Directory, DirectoryRenderer, HttpNewService, MimeOverride, PathFilter,
}; };
/// Static files handling service. /// Static files handling service.
@ -283,11 +283,17 @@ impl Files {
/// Setting a fallback static file handler: /// Setting a fallback static file handler:
/// ``` /// ```
/// use actix_files::{Files, NamedFile}; /// use actix_files::{Files, NamedFile};
/// use actix_web::dev::{ServiceRequest, ServiceResponse, fn_service};
/// ///
/// # fn run() -> Result<(), actix_web::Error> { /// # fn run() -> Result<(), actix_web::Error> {
/// let files = Files::new("/", "./static") /// let files = Files::new("/", "./static")
/// .index_file("index.html") /// .index_file("index.html")
/// .default_handler(NamedFile::open("./static/404.html")?); /// .default_handler(fn_service(|req: ServiceRequest| async {
/// let (req, _) = req.into_parts();
/// let file = NamedFile::open_async("./static/404.html").await?;
/// let res = file.into_response(&req);
/// Ok(ServiceResponse::new(req, res))
/// }));
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
@ -353,7 +359,7 @@ impl ServiceFactory<ServiceRequest> for Files {
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>; type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
let mut srv = FilesService { let mut inner = FilesServiceInner {
directory: self.directory.clone(), directory: self.directory.clone(),
index: self.index.clone(), index: self.index.clone(),
show_index: self.show_index, show_index: self.show_index,
@ -372,14 +378,14 @@ impl ServiceFactory<ServiceRequest> for Files {
Box::pin(async { Box::pin(async {
match fut.await { match fut.await {
Ok(default) => { Ok(default) => {
srv.default = Some(default); inner.default = Some(default);
Ok(srv) Ok(FilesService(Rc::new(inner)))
} }
Err(_) => Err(()), Err(_) => Err(()),
} }
}) })
} else { } else {
Box::pin(ok(srv)) Box::pin(async move { Ok(FilesService(Rc::new(inner))) })
} }
} }
} }

View File

@ -33,12 +33,12 @@ mod path_buf;
mod range; mod range;
mod service; mod service;
pub use crate::chunked::ChunkedReadFile; pub use self::chunked::ChunkedReadFile;
pub use crate::directory::Directory; pub use self::directory::Directory;
pub use crate::files::Files; pub use self::files::Files;
pub use crate::named::NamedFile; pub use self::named::NamedFile;
pub use crate::range::HttpRange; pub use self::range::HttpRange;
pub use crate::service::FilesService; pub use self::service::FilesService;
use self::directory::{directory_listing, DirectoryRenderer}; use self::directory::{directory_listing, DirectoryRenderer};
use self::error::FilesError; use self::error::FilesError;
@ -62,13 +62,12 @@ type PathFilter = dyn Fn(&Path, &RequestHead) -> bool;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{ use std::{
fs::{self, File}, fs::{self},
ops::Add, ops::Add,
time::{Duration, SystemTime}, time::{Duration, SystemTime},
}; };
use actix_service::ServiceFactory; use actix_service::ServiceFactory;
use actix_utils::future::ok;
use actix_web::{ use actix_web::{
guard, guard,
http::{ http::{
@ -82,6 +81,7 @@ mod tests {
}; };
use super::*; use super::*;
use crate::named::File;
#[actix_web::test] #[actix_web::test]
async fn test_file_extension_to_mime() { async fn test_file_extension_to_mime() {
@ -100,7 +100,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_if_modified_since_without_if_none_match() { async fn test_if_modified_since_without_if_none_match() {
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open_async("Cargo.toml").await.unwrap();
let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default() let req = TestRequest::default()
@ -112,7 +112,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_if_modified_since_without_if_none_match_same() { async fn test_if_modified_since_without_if_none_match_same() {
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open_async("Cargo.toml").await.unwrap();
let since = file.last_modified().unwrap(); let since = file.last_modified().unwrap();
let req = TestRequest::default() let req = TestRequest::default()
@ -124,7 +124,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_if_modified_since_with_if_none_match() { async fn test_if_modified_since_with_if_none_match() {
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open_async("Cargo.toml").await.unwrap();
let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default() let req = TestRequest::default()
@ -137,7 +137,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_if_unmodified_since() { async fn test_if_unmodified_since() {
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open_async("Cargo.toml").await.unwrap();
let since = file.last_modified().unwrap(); let since = file.last_modified().unwrap();
let req = TestRequest::default() let req = TestRequest::default()
@ -149,7 +149,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_if_unmodified_since_failed() { async fn test_if_unmodified_since_failed() {
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open_async("Cargo.toml").await.unwrap();
let since = header::HttpDate::from(SystemTime::UNIX_EPOCH); let since = header::HttpDate::from(SystemTime::UNIX_EPOCH);
let req = TestRequest::default() let req = TestRequest::default()
@ -161,8 +161,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_text() { async fn test_named_file_text() {
assert!(NamedFile::open("test--").is_err()); assert!(NamedFile::open_async("test--").await.is_err());
let mut file = NamedFile::open("Cargo.toml").unwrap(); let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
{ {
file.file(); file.file();
let _f: &File = &file; let _f: &File = &file;
@ -185,8 +185,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_content_disposition() { async fn test_named_file_content_disposition() {
assert!(NamedFile::open("test--").is_err()); assert!(NamedFile::open_async("test--").await.is_err());
let mut file = NamedFile::open("Cargo.toml").unwrap(); let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
{ {
file.file(); file.file();
let _f: &File = &file; let _f: &File = &file;
@ -202,7 +202,8 @@ mod tests {
"inline; filename=\"Cargo.toml\"" "inline; filename=\"Cargo.toml\""
); );
let file = NamedFile::open("Cargo.toml") let file = NamedFile::open_async("Cargo.toml")
.await
.unwrap() .unwrap()
.disable_content_disposition(); .disable_content_disposition();
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();
@ -212,8 +213,19 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_non_ascii_file_name() { async fn test_named_file_non_ascii_file_name() {
let mut file = let file = {
NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml").unwrap(); #[cfg(feature = "experimental-io-uring")]
{
crate::named::File::open("Cargo.toml").await.unwrap()
}
#[cfg(not(feature = "experimental-io-uring"))]
{
crate::named::File::open("Cargo.toml").unwrap()
}
};
let mut file = NamedFile::from_file(file, "貨物.toml").unwrap();
{ {
file.file(); file.file();
let _f: &File = &file; let _f: &File = &file;
@ -236,7 +248,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_set_content_type() { async fn test_named_file_set_content_type() {
let mut file = NamedFile::open("Cargo.toml") let mut file = NamedFile::open_async("Cargo.toml")
.await
.unwrap() .unwrap()
.set_content_type(mime::TEXT_XML); .set_content_type(mime::TEXT_XML);
{ {
@ -261,7 +274,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_image() { async fn test_named_file_image() {
let mut file = NamedFile::open("tests/test.png").unwrap(); let mut file = NamedFile::open_async("tests/test.png").await.unwrap();
{ {
file.file(); file.file();
let _f: &File = &file; let _f: &File = &file;
@ -284,7 +297,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_javascript() { async fn test_named_file_javascript() {
let file = NamedFile::open("tests/test.js").unwrap(); let file = NamedFile::open_async("tests/test.js").await.unwrap();
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req).await.unwrap();
@ -304,7 +317,8 @@ mod tests {
disposition: DispositionType::Attachment, disposition: DispositionType::Attachment,
parameters: vec![DispositionParam::Filename(String::from("test.png"))], parameters: vec![DispositionParam::Filename(String::from("test.png"))],
}; };
let mut file = NamedFile::open("tests/test.png") let mut file = NamedFile::open_async("tests/test.png")
.await
.unwrap() .unwrap()
.set_content_disposition(cd); .set_content_disposition(cd);
{ {
@ -329,7 +343,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_binary() { async fn test_named_file_binary() {
let mut file = NamedFile::open("tests/test.binary").unwrap(); let mut file = NamedFile::open_async("tests/test.binary").await.unwrap();
{ {
file.file(); file.file();
let _f: &File = &file; let _f: &File = &file;
@ -352,7 +366,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_status_code_text() { async fn test_named_file_status_code_text() {
let mut file = NamedFile::open("Cargo.toml") let mut file = NamedFile::open_async("Cargo.toml")
.await
.unwrap() .unwrap()
.set_status_code(StatusCode::NOT_FOUND); .set_status_code(StatusCode::NOT_FOUND);
{ {
@ -568,7 +583,8 @@ mod tests {
async fn test_named_file_content_encoding() { async fn test_named_file_content_encoding() {
let srv = test::init_service(App::new().wrap(Compress::default()).service( let srv = test::init_service(App::new().wrap(Compress::default()).service(
web::resource("/").to(|| async { web::resource("/").to(|| async {
NamedFile::open("Cargo.toml") NamedFile::open_async("Cargo.toml")
.await
.unwrap() .unwrap()
.set_content_encoding(header::ContentEncoding::Identity) .set_content_encoding(header::ContentEncoding::Identity)
}), }),
@ -588,7 +604,8 @@ mod tests {
async fn test_named_file_content_encoding_gzip() { async fn test_named_file_content_encoding_gzip() {
let srv = test::init_service(App::new().wrap(Compress::default()).service( let srv = test::init_service(App::new().wrap(Compress::default()).service(
web::resource("/").to(|| async { web::resource("/").to(|| async {
NamedFile::open("Cargo.toml") NamedFile::open_async("Cargo.toml")
.await
.unwrap() .unwrap()
.set_content_encoding(header::ContentEncoding::Gzip) .set_content_encoding(header::ContentEncoding::Gzip)
}), }),
@ -614,7 +631,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_allowed_method() { async fn test_named_file_allowed_method() {
let req = TestRequest::default().method(Method::GET).to_http_request(); let req = TestRequest::default().method(Method::GET).to_http_request();
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open_async("Cargo.toml").await.unwrap();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
@ -705,8 +722,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_default_handler_file_missing() { async fn test_default_handler_file_missing() {
let st = Files::new("/", ".") let st = Files::new("/", ".")
.default_handler(|req: ServiceRequest| { .default_handler(|req: ServiceRequest| async {
ok(req.into_response(HttpResponse::Ok().body("default content"))) Ok(req.into_response(HttpResponse::Ok().body("default content")))
}) })
.new_service(()) .new_service(())
.await .await
@ -789,9 +806,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_serve_named_file() { async fn test_serve_named_file() {
let srv = let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
test::init_service(App::new().service(NamedFile::open("Cargo.toml").unwrap())) let srv = test::init_service(App::new().service(factory)).await;
.await;
let req = TestRequest::get().uri("/Cargo.toml").to_request(); let req = TestRequest::get().uri("/Cargo.toml").to_request();
let res = test::call_service(&srv, req).await; let res = test::call_service(&srv, req).await;
@ -808,11 +824,9 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_serve_named_file_prefix() { async fn test_serve_named_file_prefix() {
let srv = test::init_service( let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
App::new() let srv =
.service(web::scope("/test").service(NamedFile::open("Cargo.toml").unwrap())), test::init_service(App::new().service(web::scope("/test").service(factory))).await;
)
.await;
let req = TestRequest::get().uri("/test/Cargo.toml").to_request(); let req = TestRequest::get().uri("/test/Cargo.toml").to_request();
let res = test::call_service(&srv, req).await; let res = test::call_service(&srv, req).await;
@ -829,10 +843,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_default_service() { async fn test_named_file_default_service() {
let srv = test::init_service( let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
App::new().default_service(NamedFile::open("Cargo.toml").unwrap()), let srv = test::init_service(App::new().default_service(factory)).await;
)
.await;
for route in ["/foobar", "/baz", "/"].iter() { for route in ["/foobar", "/baz", "/"].iter() {
let req = TestRequest::get().uri(route).to_request(); let req = TestRequest::get().uri(route).to_request();
@ -847,8 +859,9 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_default_handler_named_file() { async fn test_default_handler_named_file() {
let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
let st = Files::new("/", ".") let st = Files::new("/", ".")
.default_handler(NamedFile::open("Cargo.toml").unwrap()) .default_handler(factory)
.new_service(()) .new_service(())
.await .await
.unwrap(); .unwrap();
@ -926,8 +939,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_default_handler_filter() { async fn test_default_handler_filter() {
let st = Files::new("/", ".") let st = Files::new("/", ".")
.default_handler(|req: ServiceRequest| { .default_handler(|req: ServiceRequest| async {
ok(req.into_response(HttpResponse::Ok().body("default content"))) Ok(req.into_response(HttpResponse::Ok().body("default content")))
}) })
.path_filter(|path, _| path.extension() == Some("png".as_ref())) .path_filter(|path, _| path.extension() == Some("png".as_ref()))
.new_service(()) .new_service(())

View File

@ -1,17 +1,22 @@
use actix_service::{Service, ServiceFactory}; use std::{
use actix_utils::future::{ok, ready, Ready}; fmt,
use actix_web::dev::{AppService, HttpServiceFactory, ResourceDef}; fs::Metadata,
use std::fs::{File, Metadata}; io,
use std::io; ops::{Deref, DerefMut},
use std::ops::{Deref, DerefMut}; path::{Path, PathBuf},
use std::path::{Path, PathBuf}; time::{SystemTime, UNIX_EPOCH},
use std::time::{SystemTime, UNIX_EPOCH}; };
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use actix_http::body::AnyBody;
use actix_service::{Service, ServiceFactory};
use actix_web::{ use actix_web::{
dev::{BodyEncoding, ServiceRequest, ServiceResponse, SizedStream}, dev::{
AppService, BodyEncoding, HttpServiceFactory, ResourceDef, ServiceRequest,
ServiceResponse, SizedStream,
},
http::{ http::{
header::{ header::{
self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue, self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
@ -21,9 +26,9 @@ use actix_web::{
Error, HttpMessage, HttpRequest, HttpResponse, Responder, Error, HttpMessage, HttpRequest, HttpResponse, Responder,
}; };
use bitflags::bitflags; use bitflags::bitflags;
use futures_core::future::LocalBoxFuture;
use mime_guess::from_path; use mime_guess::from_path;
use crate::ChunkedReadFile;
use crate::{encoding::equiv_utf8_text, range::HttpRange}; use crate::{encoding::equiv_utf8_text, range::HttpRange};
bitflags! { bitflags! {
@ -48,9 +53,9 @@ impl Default for Flags {
/// use actix_web::App; /// use actix_web::App;
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// ///
/// # fn run() -> Result<(), Box<dyn std::error::Error>> { /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
/// let app = App::new() /// let file = NamedFile::open_async("./static/index.html").await?;
/// .service(NamedFile::open("./static/index.html")?); /// let app = App::new().service(file);
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
@ -62,10 +67,9 @@ impl Default for Flags {
/// ///
/// #[get("/")] /// #[get("/")]
/// async fn index() -> impl Responder { /// async fn index() -> impl Responder {
/// NamedFile::open("./static/index.html") /// NamedFile::open_async("./static/index.html").await
/// } /// }
/// ``` /// ```
#[derive(Debug)]
pub struct NamedFile { pub struct NamedFile {
path: PathBuf, path: PathBuf,
file: File, file: File,
@ -78,6 +82,37 @@ pub struct NamedFile {
pub(crate) encoding: Option<ContentEncoding>, pub(crate) encoding: Option<ContentEncoding>,
} }
impl fmt::Debug for NamedFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NamedFile")
.field("path", &self.path)
.field(
"file",
#[cfg(feature = "experimental-io-uring")]
{
&"tokio_uring::File"
},
#[cfg(not(feature = "experimental-io-uring"))]
{
&self.file
},
)
.field("modified", &self.modified)
.field("md", &self.md)
.field("flags", &self.flags)
.field("status_code", &self.status_code)
.field("content_type", &self.content_type)
.field("content_disposition", &self.content_disposition)
.field("encoding", &self.encoding)
.finish()
}
}
#[cfg(not(feature = "experimental-io-uring"))]
pub(crate) use std::fs::File;
#[cfg(feature = "experimental-io-uring")]
pub(crate) use tokio_uring::fs::File;
impl NamedFile { impl NamedFile {
/// Creates an instance from a previously opened file. /// Creates an instance from a previously opened file.
/// ///
@ -85,8 +120,7 @@ impl NamedFile {
/// `ContentDisposition` headers. /// `ContentDisposition` headers.
/// ///
/// # Examples /// # Examples
/// /// ```ignore
/// ```
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// use std::io::{self, Write}; /// use std::io::{self, Write};
/// use std::env; /// use std::env;
@ -147,7 +181,30 @@ impl NamedFile {
(ct, cd) (ct, cd)
}; };
let md = file.metadata()?; let md = {
#[cfg(not(feature = "experimental-io-uring"))]
{
file.metadata()?
}
#[cfg(feature = "experimental-io-uring")]
{
use std::os::unix::prelude::{AsRawFd, FromRawFd};
let fd = file.as_raw_fd();
// SAFETY: fd is borrowed and lives longer than the unsafe block
unsafe {
let file = std::fs::File::from_raw_fd(fd);
let md = file.metadata();
// SAFETY: forget the fd before exiting block in success or error case but don't
// run destructor (that would close file handle)
std::mem::forget(file);
md?
}
}
};
let modified = md.modified().ok(); let modified = md.modified().ok();
let encoding = None; let encoding = None;
@ -164,17 +221,45 @@ impl NamedFile {
}) })
} }
#[cfg(not(feature = "experimental-io-uring"))]
/// Attempts to open a file in read-only mode. /// Attempts to open a file in read-only mode.
/// ///
/// # Examples /// # Examples
///
/// ``` /// ```
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
///
/// let file = NamedFile::open("foo.txt"); /// let file = NamedFile::open("foo.txt");
/// ``` /// ```
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> { pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
Self::from_file(File::open(&path)?, path) let file = File::open(&path)?;
Self::from_file(file, path)
}
/// Attempts to open a file asynchronously in read-only mode.
///
/// When the `experimental-io-uring` crate feature is enabled, this will be async.
/// Otherwise, it will be just like [`open`][Self::open].
///
/// # Examples
/// ```
/// use actix_files::NamedFile;
/// # async fn open() {
/// let file = NamedFile::open_async("foo.txt").await.unwrap();
/// # }
/// ```
pub async fn open_async<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
let file = {
#[cfg(not(feature = "experimental-io-uring"))]
{
File::open(&path)?
}
#[cfg(feature = "experimental-io-uring")]
{
File::open(&path).await?
}
};
Self::from_file(file, path)
} }
/// Returns reference to the underlying `File` object. /// Returns reference to the underlying `File` object.
@ -186,13 +271,12 @@ impl NamedFile {
/// Retrieve the path of this file. /// Retrieve the path of this file.
/// ///
/// # Examples /// # Examples
///
/// ``` /// ```
/// # use std::io; /// # use std::io;
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// ///
/// # fn path() -> io::Result<()> { /// # async fn path() -> io::Result<()> {
/// let file = NamedFile::open("test.txt")?; /// let file = NamedFile::open_async("test.txt").await?;
/// assert_eq!(file.path().as_os_str(), "foo.txt"); /// assert_eq!(file.path().as_os_str(), "foo.txt");
/// # Ok(()) /// # Ok(())
/// # } /// # }
@ -332,7 +416,7 @@ impl NamedFile {
res.encoding(current_encoding); res.encoding(current_encoding);
} }
let reader = ChunkedReadFile::new(self.md.len(), 0, self.file); let reader = super::chunked::new_chunked_read(self.md.len(), 0, self.file);
return res.streaming(reader); return res.streaming(reader);
} }
@ -443,10 +527,10 @@ impl NamedFile {
if precondition_failed { if precondition_failed {
return resp.status(StatusCode::PRECONDITION_FAILED).finish(); return resp.status(StatusCode::PRECONDITION_FAILED).finish();
} else if not_modified { } else if not_modified {
return resp.status(StatusCode::NOT_MODIFIED).finish(); return resp.status(StatusCode::NOT_MODIFIED).body(AnyBody::None);
} }
let reader = ChunkedReadFile::new(length, offset, self.file); let reader = super::chunked::new_chunked_read(length, offset, self.file);
if offset != 0 || length != self.md.len() { if offset != 0 || length != self.md.len() {
resp.status(StatusCode::PARTIAL_CONTENT); resp.status(StatusCode::PARTIAL_CONTENT);
@ -456,20 +540,6 @@ impl NamedFile {
} }
} }
impl Deref for NamedFile {
type Target = File;
fn deref(&self) -> &File {
&self.file
}
}
impl DerefMut for NamedFile {
fn deref_mut(&mut self) -> &mut File {
&mut self.file
}
}
/// Returns true if `req` has no `If-Match` header or one which matches `etag`. /// Returns true if `req` has no `If-Match` header or one which matches `etag`.
fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
match req.get_header::<header::IfMatch>() { match req.get_header::<header::IfMatch>() {
@ -510,6 +580,20 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
} }
} }
impl Deref for NamedFile {
type Target = File;
fn deref(&self) -> &Self::Target {
&self.file
}
}
impl DerefMut for NamedFile {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.file
}
}
impl Responder for NamedFile { impl Responder for NamedFile {
fn respond_to(self, req: &HttpRequest) -> HttpResponse { fn respond_to(self, req: &HttpRequest) -> HttpResponse {
self.into_response(req) self.into_response(req)
@ -520,14 +604,16 @@ impl ServiceFactory<ServiceRequest> for NamedFile {
type Response = ServiceResponse; type Response = ServiceResponse;
type Error = Error; type Error = Error;
type Config = (); type Config = ();
type InitError = ();
type Service = NamedFileService; type Service = NamedFileService;
type Future = Ready<Result<Self::Service, ()>>; type InitError = ();
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
ok(NamedFileService { let service = NamedFileService {
path: self.path.clone(), path: self.path.clone(),
}) };
Box::pin(async move { Ok(service) })
} }
} }
@ -540,18 +626,19 @@ pub struct NamedFileService {
impl Service<ServiceRequest> for NamedFileService { impl Service<ServiceRequest> for NamedFileService {
type Response = ServiceResponse; type Response = ServiceResponse;
type Error = Error; type Error = Error;
type Future = Ready<Result<Self::Response, Self::Error>>; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::always_ready!(); actix_service::always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future { fn call(&self, req: ServiceRequest) -> Self::Future {
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
ready(
NamedFile::open(&self.path) let path = self.path.clone();
.map_err(|e| e.into()) Box::pin(async move {
.map(|f| f.into_response(&req)) let file = NamedFile::open_async(path).await?;
.map(|res| ServiceResponse::new(req, res)), let res = file.into_response(&req);
) Ok(ServiceResponse::new(req, res))
})
} }
} }

View File

@ -1,14 +1,14 @@
use std::{ use std::{
future::{ready, Ready},
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
}; };
use actix_utils::future::{ready, Ready};
use actix_web::{dev::Payload, FromRequest, HttpRequest}; use actix_web::{dev::Payload, FromRequest, HttpRequest};
use crate::error::UriSegmentError; use crate::error::UriSegmentError;
#[derive(Debug)] #[derive(Debug, PartialEq, Eq)]
pub(crate) struct PathBufWrap(PathBuf); pub(crate) struct PathBufWrap(PathBuf);
impl FromStr for PathBufWrap { impl FromStr for PathBufWrap {
@ -21,6 +21,8 @@ impl FromStr for PathBufWrap {
impl PathBufWrap { impl PathBufWrap {
/// Parse a path, giving the choice of allowing hidden files to be considered valid segments. /// Parse a path, giving the choice of allowing hidden files to be considered valid segments.
///
/// Path traversal is guarded by this method.
pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> { pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
let mut buf = PathBuf::new(); let mut buf = PathBuf::new();
@ -115,4 +117,24 @@ mod tests {
PathBuf::from_iter(vec!["test", ".tt"]) PathBuf::from_iter(vec!["test", ".tt"])
); );
} }
#[test]
fn path_traversal() {
assert_eq!(
PathBufWrap::parse_path("/../README.md", false).unwrap().0,
PathBuf::from_iter(vec!["README.md"])
);
assert_eq!(
PathBufWrap::parse_path("/../README.md", true).unwrap().0,
PathBuf::from_iter(vec!["README.md"])
);
assert_eq!(
PathBufWrap::parse_path("/../../../../../../../../../../etc/passwd", false)
.unwrap()
.0,
PathBuf::from_iter(vec!["etc/passwd"])
);
}
} }

View File

@ -1,7 +1,6 @@
use std::{fmt, io, path::PathBuf, rc::Rc}; use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc};
use actix_service::Service; use actix_service::Service;
use actix_utils::future::ok;
use actix_web::{ use actix_web::{
dev::{ServiceRequest, ServiceResponse}, dev::{ServiceRequest, ServiceResponse},
error::Error, error::Error,
@ -17,7 +16,18 @@ use crate::{
}; };
/// Assembled file serving service. /// Assembled file serving service.
pub struct FilesService { #[derive(Clone)]
pub struct FilesService(pub(crate) Rc<FilesServiceInner>);
impl Deref for FilesService {
type Target = FilesServiceInner;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
pub struct FilesServiceInner {
pub(crate) directory: PathBuf, pub(crate) directory: PathBuf,
pub(crate) index: Option<String>, pub(crate) index: Option<String>,
pub(crate) show_index: bool, pub(crate) show_index: bool,
@ -31,20 +41,50 @@ pub struct FilesService {
pub(crate) hidden_files: bool, pub(crate) hidden_files: bool,
} }
impl fmt::Debug for FilesServiceInner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("FilesServiceInner")
}
}
impl FilesService { impl FilesService {
fn handle_err( async fn handle_err(
&self, &self,
err: io::Error, err: io::Error,
req: ServiceRequest, req: ServiceRequest,
) -> LocalBoxFuture<'static, Result<ServiceResponse, Error>> { ) -> Result<ServiceResponse, Error> {
log::debug!("error handling {}: {}", req.path(), err); log::debug!("error handling {}: {}", req.path(), err);
if let Some(ref default) = self.default { if let Some(ref default) = self.default {
Box::pin(default.call(req)) default.call(req).await
} else { } else {
Box::pin(ok(req.error_response(err))) Ok(req.error_response(err))
} }
} }
fn serve_named_file(
&self,
req: ServiceRequest,
mut named_file: NamedFile,
) -> ServiceResponse {
if let Some(ref mime_override) = self.mime_override {
let new_disposition = mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition;
}
named_file.flags = self.file_flags;
let (req, _) = req.into_parts();
let res = named_file.into_response(&req);
ServiceResponse::new(req, res)
}
fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse {
let dir = Directory::new(self.directory.clone(), path);
let (req, _) = req.into_parts();
(self.renderer)(&dir, &req).unwrap_or_else(|e| ServiceResponse::from_err(e, req))
}
} }
impl fmt::Debug for FilesService { impl fmt::Debug for FilesService {
@ -56,7 +96,7 @@ impl fmt::Debug for FilesService {
impl Service<ServiceRequest> for FilesService { impl Service<ServiceRequest> for FilesService {
type Response = ServiceResponse; type Response = ServiceResponse;
type Error = Error; type Error = Error;
type Future = LocalBoxFuture<'static, Result<ServiceResponse, Error>>; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::always_ready!(); actix_service::always_ready!();
@ -69,103 +109,87 @@ impl Service<ServiceRequest> for FilesService {
matches!(*req.method(), Method::HEAD | Method::GET) matches!(*req.method(), Method::HEAD | Method::GET)
}; };
let this = self.clone();
Box::pin(async move {
if !is_method_valid { if !is_method_valid {
return Box::pin(ok(req.into_response( return Ok(req.into_response(
actix_web::HttpResponse::MethodNotAllowed() actix_web::HttpResponse::MethodNotAllowed()
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8)) .insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
.body("Request did not meet this resource's requirements."), .body("Request did not meet this resource's requirements."),
))); ));
} }
let real_path = let real_path =
match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) { match PathBufWrap::parse_path(req.match_info().path(), this.hidden_files) {
Ok(item) => item, Ok(item) => item,
Err(e) => return Box::pin(ok(req.error_response(e))), Err(e) => return Ok(req.error_response(e)),
}; };
if let Some(filter) = &self.path_filter { if let Some(filter) = &this.path_filter {
if !filter(real_path.as_ref(), req.head()) { if !filter(real_path.as_ref(), req.head()) {
if let Some(ref default) = self.default { if let Some(ref default) = this.default {
return Box::pin(default.call(req)); return default.call(req).await;
} else { } else {
return Box::pin(ok( return Ok(
req.into_response(actix_web::HttpResponse::NotFound().finish()) req.into_response(actix_web::HttpResponse::NotFound().finish())
)); );
} }
} }
} }
// full file path // full file path
let path = self.directory.join(&real_path); let path = this.directory.join(&real_path);
if let Err(err) = path.canonicalize() { if let Err(err) = path.canonicalize() {
return Box::pin(self.handle_err(err, req)); return this.handle_err(err, req).await;
} }
if path.is_dir() { if path.is_dir() {
if self.redirect_to_slash if this.redirect_to_slash
&& !req.path().ends_with('/') && !req.path().ends_with('/')
&& (self.index.is_some() || self.show_index) && (this.index.is_some() || this.show_index)
{ {
let redirect_to = format!("{}/", req.path()); let redirect_to = format!("{}/", req.path());
return Box::pin(ok(req.into_response( return Ok(req.into_response(
HttpResponse::Found() HttpResponse::Found()
.insert_header((header::LOCATION, redirect_to)) .insert_header((header::LOCATION, redirect_to))
.finish(), .finish(),
))); ));
} }
let serve_named_file = |req: ServiceRequest, mut named_file: NamedFile| { match this.index {
if let Some(ref mime_override) = self.mime_override { Some(ref index) => {
let new_disposition = mime_override(&named_file.content_type.type_()); let named_path = path.join(index);
named_file.content_disposition.disposition = new_disposition; match NamedFile::open_async(named_path).await {
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
Err(_) if this.show_index => Ok(this.show_index(req, path)),
Err(err) => this.handle_err(err, req).await,
} }
named_file.flags = self.file_flags; }
None if this.show_index => Ok(this.show_index(req, path)),
let (req, _) = req.into_parts(); _ => Ok(ServiceResponse::from_err(
let res = named_file.into_response(&req);
Box::pin(ok(ServiceResponse::new(req, res)))
};
let show_index = |req: ServiceRequest| {
let dir = Directory::new(self.directory.clone(), path.clone());
let (req, _) = req.into_parts();
let x = (self.renderer)(&dir, &req);
Box::pin(match x {
Ok(resp) => ok(resp),
Err(err) => ok(ServiceResponse::from_err(err, req)),
})
};
match self.index {
Some(ref index) => match NamedFile::open(path.join(index)) {
Ok(named_file) => serve_named_file(req, named_file),
Err(_) if self.show_index => show_index(req),
Err(err) => self.handle_err(err, req),
},
None if self.show_index => show_index(req),
_ => Box::pin(ok(ServiceResponse::from_err(
FilesError::IsDirectory, FilesError::IsDirectory,
req.into_parts().0, req.into_parts().0,
))), )),
} }
} else { } else {
match NamedFile::open(path) { match NamedFile::open_async(&path).await {
Ok(mut named_file) => { Ok(mut named_file) => {
if let Some(ref mime_override) = self.mime_override { if let Some(ref mime_override) = this.mime_override {
let new_disposition = mime_override(&named_file.content_type.type_()); let new_disposition =
mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition; named_file.content_disposition.disposition = new_disposition;
} }
named_file.flags = self.file_flags; named_file.flags = this.file_flags;
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
let res = named_file.into_response(&req); let res = named_file.into_response(&req);
Box::pin(ok(ServiceResponse::new(req, res))) Ok(ServiceResponse::new(req, res))
} }
Err(err) => self.handle_err(err, req), Err(err) => this.handle_err(err, req).await,
} }
} }
})
} }
} }

View File

@ -0,0 +1,27 @@
use actix_files::Files;
use actix_web::{
http::StatusCode,
test::{self, TestRequest},
App,
};
#[actix_rt::test]
async fn test_directory_traversal_prevention() {
let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await;
let req =
TestRequest::with_uri("/../../../../../../../../../../../etc/passwd").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let req = TestRequest::with_uri(
"/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd",
)
.to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let req = TestRequest::with_uri("/%00/etc/passwd%00").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}

View File

@ -1,8 +1,21 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.7 - 2021-11-22
* Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
[#2408]: https://github.com/actix/actix-web/pull/2408
## 3.0.0-beta.6 - 2021-11-15
* `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
* Update `actix-server` to `2.0.0-beta.9`. [#2442]
* Minimum supported Rust version (MSRV) is now 1.52. * Minimum supported Rust version (MSRV) is now 1.52.
[#2442]: https://github.com/actix/actix-web/pull/2442
## 3.0.0-beta.5 - 2021-09-09 ## 3.0.0-beta.5 - 2021-09-09
* Minimum supported Rust version (MSRV) is now 1.51. * Minimum supported Rust version (MSRV) is now 1.51.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http-test" name = "actix-http-test"
version = "3.0.0-beta.5" version = "3.0.0-beta.7"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Various helpers for Actix applications to use during testing" description = "Various helpers for Actix applications to use during testing"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
@ -30,17 +30,17 @@ openssl = ["tls-openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-service = "2.0.0" actix-service = "2.0.0"
actix-codec = "0.4.0" actix-codec = "0.4.1"
actix-tls = "3.0.0-beta.7" actix-tls = "3.0.0-beta.9"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-rt = "2.2" actix-rt = "2.2"
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-beta.9"
awc = { version = "3.0.0-beta.9", default-features = false } awc = { version = "3.0.0-beta.11", default-features = false }
base64 = "0.13" base64 = "0.13"
bytes = "1" bytes = "1"
futures-core = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.7", default-features = false }
http = "0.2.2" http = "0.2.5"
log = "0.4" log = "0.4"
socket2 = "0.4" socket2 = "0.4"
serde = "1.0" serde = "1.0"
@ -48,7 +48,8 @@ serde_json = "1.0"
slab = "0.4" slab = "0.4"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
tokio = { version = "1.2", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0-beta.10", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-beta.11" actix-http = "3.0.0-beta.13"

View File

@ -3,11 +3,11 @@
> Various helpers for Actix applications to use during testing. > Various helpers for Actix applications to use during testing.
[![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test)
[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.5)](https://docs.rs/actix-http-test/3.0.0-beta.5) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.7)](https://docs.rs/actix-http-test/3.0.0-beta.7)
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
<br> <br>
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.5/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.5) [![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.7)
[![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -7,7 +7,7 @@
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
extern crate tls_openssl as openssl; extern crate tls_openssl as openssl;
use std::{net, sync::mpsc, thread, time::Duration}; use std::{net, thread, time::Duration};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::{net::TcpStream, System}; use actix_rt::{net::TcpStream, System};
@ -19,17 +19,17 @@ use bytes::Bytes;
use futures_core::stream::Stream; use futures_core::stream::Stream;
use http::Method; use http::Method;
use socket2::{Domain, Protocol, Socket, Type}; use socket2::{Domain, Protocol, Socket, Type};
use tokio::sync::mpsc;
/// Start test server /// Start test server.
/// ///
/// `TestServer` is very simple test server that simplify process of writing /// `TestServer` is very simple test server that simplify process of writing integration tests cases
/// integration tests cases for actix web applications. /// for HTTP applications.
/// ///
/// # Examples /// # Examples
/// /// ```no_run
/// ```
/// use actix_http::HttpService; /// use actix_http::HttpService;
/// use actix_http_test::TestServer; /// use actix_http_test::test_server;
/// use actix_web::{web, App, HttpResponse, Error}; /// use actix_web::{web, App, HttpResponse, Error};
/// ///
/// async fn my_handler() -> Result<HttpResponse, Error> { /// async fn my_handler() -> Result<HttpResponse, Error> {
@ -38,10 +38,9 @@ use socket2::{Domain, Protocol, Socket, Type};
/// ///
/// #[actix_web::test] /// #[actix_web::test]
/// async fn test_example() { /// async fn test_example() {
/// let mut srv = TestServer::start( /// let mut srv = TestServer::start(||
/// || HttpService::new( /// HttpService::new(
/// App::new().service( /// App::new().service(web::resource("/").to(my_handler))
/// web::resource("/").to(my_handler))
/// ) /// )
/// ); /// );
/// ///
@ -55,72 +54,86 @@ pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer
test_server_with_addr(tcp, factory).await test_server_with_addr(tcp, factory).await
} }
/// Start [`test server`](test_server()) on a concrete Address /// Start [`test server`](test_server()) on an existing address binding.
pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>( pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
tcp: net::TcpListener, tcp: net::TcpListener,
factory: F, factory: F,
) -> TestServer { ) -> TestServer {
let (tx, rx) = mpsc::channel(); let (started_tx, started_rx) = std::sync::mpsc::channel();
let (thread_stop_tx, thread_stop_rx) = mpsc::channel(1);
// run server in separate thread // run server in separate thread
thread::spawn(move || { thread::spawn(move || {
let sys = System::new(); System::new().block_on(async move {
let local_addr = tcp.local_addr().unwrap(); let local_addr = tcp.local_addr().unwrap();
let srv = Server::build() let srv = Server::build()
.listen("test", tcp, factory)?
.workers(1) .workers(1)
.disable_signals(); .disable_signals()
.system_exit()
.listen("test", tcp, factory)
.expect("test server could not be created");
sys.block_on(async { let srv = srv.run();
srv.run(); started_tx
tx.send((System::current(), local_addr)).unwrap(); .send((System::current(), srv.handle(), local_addr))
.unwrap();
// drive server loop
srv.await.unwrap();
}); });
sys.run() // notify TestServer that server and system have shut down
// all thread managed resources should be dropped at this point
let _ = thread_stop_tx.send(());
}); });
let (system, addr) = rx.recv().unwrap(); let (system, server, addr) = started_rx.recv().unwrap();
let client = { let client = {
let connector = {
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
{ let connector = {
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
builder.set_verify(SslVerifyMode::NONE); builder.set_verify(SslVerifyMode::NONE);
let _ = builder let _ = builder
.set_alpn_protos(b"\x02h2\x08http/1.1") .set_alpn_protos(b"\x02h2\x08http/1.1")
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
Connector::new() Connector::new()
.conn_lifetime(Duration::from_secs(0)) .conn_lifetime(Duration::from_secs(0))
.timeout(Duration::from_millis(30000)) .timeout(Duration::from_millis(30000))
.ssl(builder.build()) .ssl(builder.build())
} };
#[cfg(not(feature = "openssl"))] #[cfg(not(feature = "openssl"))]
{ let connector = {
Connector::new() Connector::new()
.conn_lifetime(Duration::from_secs(0)) .conn_lifetime(Duration::from_secs(0))
.timeout(Duration::from_millis(30000)) .timeout(Duration::from_millis(30000))
}
}; };
Client::builder().connector(connector).finish() Client::builder().connector(connector).finish()
}; };
TestServer { TestServer {
addr, server,
client, client,
system, system,
addr,
thread_stop_rx,
} }
} }
/// Test server controller /// Test server controller
pub struct TestServer { pub struct TestServer {
server: actix_server::ServerHandle,
client: awc::Client,
system: actix_rt::System,
addr: net::SocketAddr, addr: net::SocketAddr,
client: Client, thread_stop_rx: mpsc::Receiver<()>,
system: System,
} }
impl TestServer { impl TestServer {
@ -257,15 +270,32 @@ impl TestServer {
self.client.headers() self.client.headers()
} }
/// Stop HTTP server /// Stop HTTP server.
fn stop(&mut self) { ///
/// Waits for spawned `Server` and `System` to (force) shutdown.
pub async fn stop(&mut self) {
// signal server to stop
self.server.stop(false).await;
// also signal system to stop
// though this is handled by `ServerBuilder::exit_system` too
self.system.stop(); self.system.stop();
// wait for thread to be stopped but don't care about result
let _ = self.thread_stop_rx.recv().await;
} }
} }
impl Drop for TestServer { impl Drop for TestServer {
fn drop(&mut self) { fn drop(&mut self) {
self.stop() // calls in this Drop impl should be enough to shut down the server, system, and thread
// without needing to await anything
// signal server to stop
let _ = self.server.stop(true);
// signal system to stop
self.system.stop();
} }
} }

View File

@ -3,6 +3,44 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.13 - 2021-11-22
### Added
* `body::AnyBody::empty` for quickly creating an empty body. [#2446]
* `body::AnyBody::none` for quickly creating a "none" body. [#2456]
* `impl Clone` for `body::AnyBody<S> where S: Clone`. [#2448]
* `body::AnyBody::into_boxed` for quickly converting to a type-erased, boxed body type. [#2448]
### Changed
* Rename `body::AnyBody::{Message => Body}`. [#2446]
* Rename `body::AnyBody::{from_message => new_boxed}`. [#2448]
* Rename `body::AnyBody::{from_slice => copy_from_slice}`. [#2448]
* Rename `body::{BoxAnyBody => BoxBody}`. [#2448]
* Change representation of `AnyBody` to include a type parameter in `Body` variant. Defaults to `BoxBody`. [#2448]
* `Encoder::response` now returns `AnyBody<Encoder<B>>`. [#2448]
### Removed
* `body::AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446]
* `body::BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446]
* `EncoderError::Boxed`; it is no longer required. [#2446]
* `body::ResponseBody`; is function is replaced by the new `body::AnyBody` enum. [#2446]
[#2446]: https://github.com/actix/actix-web/pull/2446
[#2448]: https://github.com/actix/actix-web/pull/2448
[#2456]: https://github.com/actix/actix-web/pull/2456
## 3.0.0-beta.12 - 2021-11-15
### Changed
* Update `actix-server` to `2.0.0-beta.9`. [#2442]
### Removed
* `client` module. [#2425]
* `trust-dns` feature. [#2425]
[#2425]: https://github.com/actix/actix-web/pull/2425
[#2442]: https://github.com/actix/actix-web/pull/2442
## 3.0.0-beta.11 - 2021-10-20 ## 3.0.0-beta.11 - 2021-10-20
### Changed ### Changed
* Updated rustls to v0.20. [#2414] * Updated rustls to v0.20. [#2414]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.0.0-beta.11" version = "3.0.0-beta.13"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "HTTP primitives for the Actix ecosystem" description = "HTTP primitives for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"] keywords = ["actix", "http", "framework", "async", "futures"]
@ -27,29 +27,25 @@ path = "src/lib.rs"
default = [] default = []
# openssl # openssl
openssl = ["actix-tls/openssl"] openssl = ["actix-tls/accept", "actix-tls/openssl"]
# rustls support # rustls support
rustls = ["actix-tls/rustls"] rustls = ["actix-tls/accept", "actix-tls/rustls"]
# enable compression support # enable compression support
compress-brotli = ["brotli2", "__compress"] compress-brotli = ["brotli2", "__compress"]
compress-gzip = ["flate2", "__compress"] compress-gzip = ["flate2", "__compress"]
compress-zstd = ["zstd", "__compress"] compress-zstd = ["zstd", "__compress"]
# trust-dns as client dns resolver
trust-dns = ["trust-dns-resolver"]
# Internal (PRIVATE!) features used to aid testing and cheking feature status. # Internal (PRIVATE!) features used to aid testing and cheking feature status.
# Don't rely on these whatsoever. They may disappear at anytime. # Don't rely on these whatsoever. They may disappear at anytime.
__compress = [] __compress = []
[dependencies] [dependencies]
actix-service = "2.0.0" actix-service = "2.0.0"
actix-codec = "0.4.0" actix-codec = "0.4.1"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-rt = "2.2" actix-rt = "2.2"
actix-tls = { version = "3.0.0-beta.7", features = ["accept", "connect"] }
ahash = "0.7" ahash = "0.7"
base64 = "0.13" base64 = "0.13"
@ -61,13 +57,12 @@ encoding_rs = "0.8"
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"] }
h2 = "0.3.1" h2 = "0.3.1"
http = "0.2.2" http = "0.2.5"
httparse = "1.5.1" httparse = "1.5.1"
httpdate = "1.0.1" httpdate = "1.0.1"
itoa = "0.4" itoa = "0.4"
language-tags = "0.3" language-tags = "0.3"
local-channel = "0.1" local-channel = "0.1"
once_cell = "1.5"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
percent-encoding = "2.1" percent-encoding = "2.1"
@ -76,29 +71,31 @@ pin-project-lite = "0.2"
rand = "0.8" rand = "0.8"
sha-1 = "0.9" sha-1 = "0.9"
smallvec = "1.6.1" smallvec = "1.6.1"
tokio = { version = "1.2", features = ["sync"] }
# tls
actix-tls = { version = "3.0.0-beta.9", default-features = false, optional = true }
# compression # compression
brotli2 = { version="0.3.2", optional = true } brotli2 = { version="0.3.2", optional = true }
flate2 = { version = "1.0.13", optional = true } flate2 = { version = "1.0.13", optional = true }
zstd = { version = "0.7", optional = true } zstd = { version = "0.9", optional = true }
trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies] [dev-dependencies]
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-beta.9"
actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] }
actix-tls = { version = "3.0.0-beta.7", features = ["openssl"] } actix-tls = { version = "3.0.0-beta.9", features = ["openssl"] }
async-stream = "0.3" async-stream = "0.3"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.8" env_logger = "0.9"
rcgen = "0.8" rcgen = "0.8"
regex = "1.3" regex = "1.3"
rustls-pemfile = "0.2" rustls-pemfile = "0.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.9" } tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.20.0" } tls-rustls = { package = "rustls", version = "0.20.0" }
tokio = { version = "1.2", features = ["net", "rt"] }
[[example]] [[example]]
name = "ws" name = "ws"

View File

@ -3,11 +3,11 @@
> HTTP primitives for the Actix ecosystem. > HTTP primitives for the Actix ecosystem.
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.11)](https://docs.rs/actix-http/3.0.0-beta.11) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.13)](https://docs.rs/actix-http/3.0.0-beta.13)
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.11/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.11) [![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.13/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.13)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -1,12 +1,12 @@
use std::io; use std::io;
use actix_http::{body::Body, http::HeaderValue, http::StatusCode}; use actix_http::{body::AnyBody, http::HeaderValue, http::StatusCode};
use actix_http::{Error, HttpService, Request, Response}; use actix_http::{Error, HttpService, Request, Response};
use actix_server::Server; use actix_server::Server;
use bytes::BytesMut; use bytes::BytesMut;
use futures_util::StreamExt as _; use futures_util::StreamExt as _;
async fn handle_request(mut req: Request) -> Result<Response<Body>, Error> { async fn handle_request(mut req: Request) -> Result<Response<AnyBody>, Error> {
let mut body = BytesMut::new(); let mut body = BytesMut::new();
while let Some(item) = req.payload().next().await { while let Some(item) = req.payload().next().await {
body.extend_from_slice(&item?) body.extend_from_slice(&item?)

View File

@ -8,53 +8,94 @@ use std::{
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::Stream; use futures_core::Stream;
use pin_project::pin_project;
use crate::error::Error; use crate::error::Error;
use super::{BodySize, BodyStream, MessageBody, MessageBodyMapErr, SizedStream}; use super::{BodySize, BodyStream, MessageBody, MessageBodyMapErr, SizedStream};
#[deprecated(since = "4.0.0", note = "Renamed to `AnyBody`.")]
pub type Body = AnyBody; pub type Body = AnyBody;
/// Represents various types of HTTP message body. /// Represents various types of HTTP message body.
pub enum AnyBody { #[pin_project(project = AnyBodyProj)]
#[derive(Clone)]
pub enum AnyBody<B = BoxBody> {
/// Empty response. `Content-Length` header is not set. /// Empty response. `Content-Length` header is not set.
None, None,
/// Zero sized response body. `Content-Length` header is set to `0`. /// Complete, in-memory response body.
Empty,
/// Specific response body.
Bytes(Bytes), Bytes(Bytes),
/// Generic message body. /// Generic / Other message body.
Message(BoxAnyBody), Body(#[pin] B),
} }
impl AnyBody { impl AnyBody {
/// Create body from slice (copy) /// Constructs a "body" representing an empty response.
pub fn from_slice(s: &[u8]) -> Self { pub fn none() -> Self {
Self::Bytes(Bytes::copy_from_slice(s)) Self::None
} }
/// Create body from generic message body. /// Constructs a new, 0-length body.
pub fn from_message<B>(body: B) -> Self pub fn empty() -> Self {
Self::Bytes(Bytes::new())
}
/// Create boxed body from generic message body.
pub fn new_boxed<B>(body: B) -> Self
where where
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError + 'static>>, B::Error: Into<Box<dyn StdError + 'static>>,
{ {
Self::Message(BoxAnyBody::from_body(body)) Self::Body(BoxBody::from_body(body))
}
/// Constructs new `AnyBody` instance from a slice of bytes by copying it.
///
/// If your bytes container is owned, it may be cheaper to use a `From` impl.
pub fn copy_from_slice(s: &[u8]) -> Self {
Self::Bytes(Bytes::copy_from_slice(s))
}
#[doc(hidden)]
#[deprecated(since = "4.0.0", note = "Renamed to `copy_from_slice`.")]
pub fn from_slice(s: &[u8]) -> Self {
Self::Bytes(Bytes::copy_from_slice(s))
} }
} }
impl MessageBody for AnyBody { impl<B> AnyBody<B>
where
B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError + 'static>>,
{
/// Create body from generic message body.
pub fn new(body: B) -> Self {
Self::Body(body)
}
pub fn into_boxed(self) -> AnyBody {
match self {
Self::None => AnyBody::None,
Self::Bytes(bytes) => AnyBody::Bytes(bytes),
Self::Body(body) => AnyBody::new_boxed(body),
}
}
}
impl<B> MessageBody for AnyBody<B>
where
B: MessageBody,
B::Error: Into<Box<dyn StdError>> + 'static,
{
type Error = Error; type Error = Error;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
match self { match self {
AnyBody::None => BodySize::None, AnyBody::None => BodySize::None,
AnyBody::Empty => BodySize::Empty,
AnyBody::Bytes(ref bin) => BodySize::Sized(bin.len() as u64), AnyBody::Bytes(ref bin) => BodySize::Sized(bin.len() as u64),
AnyBody::Message(ref body) => body.size(), AnyBody::Body(ref body) => body.size(),
} }
} }
@ -62,10 +103,9 @@ impl MessageBody for AnyBody {
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
match self.get_mut() { match self.project() {
AnyBody::None => Poll::Ready(None), AnyBodyProj::None => Poll::Ready(None),
AnyBody::Empty => Poll::Ready(None), AnyBodyProj::Bytes(bin) => {
AnyBody::Bytes(ref mut bin) => {
let len = bin.len(); let len = bin.len();
if len == 0 { if len == 0 {
Poll::Ready(None) Poll::Ready(None)
@ -74,8 +114,7 @@ impl MessageBody for AnyBody {
} }
} }
AnyBody::Message(body) => body AnyBodyProj::Body(body) => body
.as_pin_mut()
.poll_next(cx) .poll_next(cx)
.map_err(|err| Error::new_body().with_cause(err)), .map_err(|err| Error::new_body().with_cause(err)),
} }
@ -83,80 +122,88 @@ impl MessageBody for AnyBody {
} }
impl PartialEq for AnyBody { impl PartialEq for AnyBody {
fn eq(&self, other: &Body) -> bool { fn eq(&self, other: &AnyBody) -> bool {
match *self { match *self {
AnyBody::None => matches!(*other, AnyBody::None), AnyBody::None => matches!(*other, AnyBody::None),
AnyBody::Empty => matches!(*other, AnyBody::Empty),
AnyBody::Bytes(ref b) => match *other { AnyBody::Bytes(ref b) => match *other {
AnyBody::Bytes(ref b2) => b == b2, AnyBody::Bytes(ref b2) => b == b2,
_ => false, _ => false,
}, },
AnyBody::Message(_) => false, AnyBody::Body(_) => false,
} }
} }
} }
impl fmt::Debug for AnyBody { impl<S: fmt::Debug> fmt::Debug for AnyBody<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self { match *self {
AnyBody::None => write!(f, "AnyBody::None"), AnyBody::None => write!(f, "AnyBody::None"),
AnyBody::Empty => write!(f, "AnyBody::Empty"), AnyBody::Bytes(ref bytes) => write!(f, "AnyBody::Bytes({:?})", bytes),
AnyBody::Bytes(ref b) => write!(f, "AnyBody::Bytes({:?})", b), AnyBody::Body(ref stream) => write!(f, "AnyBody::Message({:?})", stream),
AnyBody::Message(_) => write!(f, "AnyBody::Message(_)"),
} }
} }
} }
impl From<&'static str> for AnyBody { impl<B> From<&'static str> for AnyBody<B> {
fn from(s: &'static str) -> Body { fn from(string: &'static str) -> Self {
AnyBody::Bytes(Bytes::from_static(s.as_ref())) Self::Bytes(Bytes::from_static(string.as_ref()))
} }
} }
impl From<&'static [u8]> for AnyBody { impl<B> From<&'static [u8]> for AnyBody<B> {
fn from(s: &'static [u8]) -> Body { fn from(bytes: &'static [u8]) -> Self {
AnyBody::Bytes(Bytes::from_static(s)) Self::Bytes(Bytes::from_static(bytes))
} }
} }
impl From<Vec<u8>> for AnyBody { impl<B> From<Vec<u8>> for AnyBody<B> {
fn from(vec: Vec<u8>) -> Body { fn from(vec: Vec<u8>) -> Self {
AnyBody::Bytes(Bytes::from(vec)) Self::Bytes(Bytes::from(vec))
} }
} }
impl From<String> for AnyBody { impl<B> From<String> for AnyBody<B> {
fn from(s: String) -> Body { fn from(string: String) -> Self {
s.into_bytes().into() Self::Bytes(Bytes::from(string))
} }
} }
impl From<&'_ String> for AnyBody { impl<B> From<&'_ String> for AnyBody<B> {
fn from(s: &String) -> Body { fn from(string: &String) -> Self {
AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s))) Self::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string)))
} }
} }
impl From<Cow<'_, str>> for AnyBody { impl<B> From<Cow<'_, str>> for AnyBody<B> {
fn from(s: Cow<'_, str>) -> Body { fn from(string: Cow<'_, str>) -> Self {
match s { match string {
Cow::Owned(s) => AnyBody::from(s), Cow::Owned(s) => Self::from(s),
Cow::Borrowed(s) => { Cow::Borrowed(s) => {
AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s))) Self::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)))
} }
} }
} }
} }
impl From<Bytes> for AnyBody { impl<B> From<Bytes> for AnyBody<B> {
fn from(s: Bytes) -> Body { fn from(bytes: Bytes) -> Self {
AnyBody::Bytes(s) Self::Bytes(bytes)
} }
} }
impl From<BytesMut> for AnyBody { impl<B> From<BytesMut> for AnyBody<B> {
fn from(s: BytesMut) -> Body { fn from(bytes: BytesMut) -> Self {
AnyBody::Bytes(s.freeze()) Self::Bytes(bytes.freeze())
}
}
impl<S, E> From<SizedStream<S>> for AnyBody<SizedStream<S>>
where
S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Box<dyn StdError>> + 'static,
{
fn from(stream: SizedStream<S>) -> Self {
AnyBody::new(stream)
} }
} }
@ -165,8 +212,18 @@ where
S: Stream<Item = Result<Bytes, E>> + 'static, S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Box<dyn StdError>> + 'static, E: Into<Box<dyn StdError>> + 'static,
{ {
fn from(s: SizedStream<S>) -> Body { fn from(stream: SizedStream<S>) -> Self {
AnyBody::from_message(s) AnyBody::new_boxed(stream)
}
}
impl<S, E> From<BodyStream<S>> for AnyBody<BodyStream<S>>
where
S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Box<dyn StdError>> + 'static,
{
fn from(stream: BodyStream<S>) -> Self {
AnyBody::new(stream)
} }
} }
@ -175,15 +232,15 @@ where
S: Stream<Item = Result<Bytes, E>> + 'static, S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Box<dyn StdError>> + 'static, E: Into<Box<dyn StdError>> + 'static,
{ {
fn from(s: BodyStream<S>) -> Body { fn from(stream: BodyStream<S>) -> Self {
AnyBody::from_message(s) AnyBody::new_boxed(stream)
} }
} }
/// A boxed message body with boxed errors. /// A boxed message body with boxed errors.
pub struct BoxAnyBody(Pin<Box<dyn MessageBody<Error = Box<dyn StdError + 'static>>>>); pub struct BoxBody(Pin<Box<dyn MessageBody<Error = Box<dyn StdError>>>>);
impl BoxAnyBody { impl BoxBody {
/// Boxes a `MessageBody` and any errors it generates. /// Boxes a `MessageBody` and any errors it generates.
pub fn from_body<B>(body: B) -> Self pub fn from_body<B>(body: B) -> Self
where where
@ -197,18 +254,18 @@ impl BoxAnyBody {
/// Returns a mutable pinned reference to the inner message body type. /// Returns a mutable pinned reference to the inner message body type.
pub fn as_pin_mut( pub fn as_pin_mut(
&mut self, &mut self,
) -> Pin<&mut (dyn MessageBody<Error = Box<dyn StdError + 'static>>)> { ) -> Pin<&mut (dyn MessageBody<Error = Box<dyn StdError>>)> {
self.0.as_mut() self.0.as_mut()
} }
} }
impl fmt::Debug for BoxAnyBody { impl fmt::Debug for BoxBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("BoxAnyBody(dyn MessageBody)") f.write_str("BoxAnyBody(dyn MessageBody)")
} }
} }
impl MessageBody for BoxAnyBody { impl MessageBody for BoxBody {
type Error = Error; type Error = Error;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
@ -225,3 +282,52 @@ impl MessageBody for BoxAnyBody {
.map_err(|err| Error::new_body().with_cause(err)) .map_err(|err| Error::new_body().with_cause(err))
} }
} }
#[cfg(test)]
mod tests {
use std::marker::PhantomPinned;
use static_assertions::{assert_impl_all, assert_not_impl_all};
use super::*;
use crate::body::to_bytes;
struct PinType(PhantomPinned);
impl MessageBody for PinType {
type Error = crate::Error;
fn size(&self) -> BodySize {
unimplemented!()
}
fn poll_next(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
unimplemented!()
}
}
assert_impl_all!(AnyBody<()>: MessageBody, fmt::Debug, Send, Sync, Unpin);
assert_impl_all!(AnyBody<AnyBody<()>>: MessageBody, fmt::Debug, Send, Sync, Unpin);
assert_impl_all!(AnyBody<Bytes>: MessageBody, fmt::Debug, Send, Sync, Unpin);
assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Unpin);
assert_impl_all!(BoxBody: MessageBody, fmt::Debug, Unpin);
assert_impl_all!(AnyBody<PinType>: MessageBody);
assert_not_impl_all!(AnyBody: Send, Sync, Unpin);
assert_not_impl_all!(BoxBody: Send, Sync, Unpin);
assert_not_impl_all!(AnyBody<PinType>: Send, Sync, Unpin);
#[actix_rt::test]
async fn nested_boxed_body() {
let body = AnyBody::copy_from_slice(&[1, 2, 3]);
let boxed_body = BoxBody::from_body(BoxBody::from_body(body));
assert_eq!(
to_bytes(boxed_body).await.unwrap(),
Bytes::from(vec![1, 2, 3]),
);
}
}

View File

@ -75,10 +75,22 @@ mod tests {
use derive_more::{Display, Error}; use derive_more::{Display, Error};
use futures_core::ready; use futures_core::ready;
use futures_util::{stream, FutureExt as _}; use futures_util::{stream, FutureExt as _};
use static_assertions::{assert_impl_all, assert_not_impl_all};
use super::*; use super::*;
use crate::body::to_bytes; use crate::body::to_bytes;
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, crate::Error>>>: MessageBody);
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, &'static str>>>: MessageBody);
assert_impl_all!(BodyStream<stream::Repeat<Result<Bytes, &'static str>>>: MessageBody);
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, Infallible>>>: MessageBody);
assert_impl_all!(BodyStream<stream::Repeat<Result<Bytes, Infallible>>>: MessageBody);
assert_not_impl_all!(BodyStream<stream::Empty<Bytes>>: MessageBody);
assert_not_impl_all!(BodyStream<stream::Repeat<Bytes>>: MessageBody);
// crate::Error is not Clone
assert_not_impl_all!(BodyStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
#[actix_rt::test] #[actix_rt::test]
async fn skips_empty_chunks() { async fn skips_empty_chunks() {
let body = BodyStream::new(stream::iter( let body = BodyStream::new(stream::iter(
@ -124,6 +136,30 @@ mod tests {
assert!(matches!(to_bytes(body).await, Err(StreamErr))); assert!(matches!(to_bytes(body).await, Err(StreamErr)));
} }
#[actix_rt::test]
async fn stream_string_error() {
// `&'static str` does not impl `Error`
// but it does impl `Into<Box<dyn Error>>`
let body = BodyStream::new(stream::once(async { Err("stringy error") }));
assert!(matches!(to_bytes(body).await, Err("stringy error")));
}
#[actix_rt::test]
async fn stream_boxed_error() {
// `Box<dyn Error>` does not impl `Error`
// but it does impl `Into<Box<dyn Error>>`
let body = BodyStream::new(stream::once(async {
Err(Box::<dyn StdError>::from("stringy error"))
}));
assert_eq!(
to_bytes(body).await.unwrap_err().to_string(),
"stringy error"
);
}
#[actix_rt::test] #[actix_rt::test]
async fn stream_delayed_error() { async fn stream_delayed_error() {
let body = let body =

View File

@ -31,7 +31,7 @@ impl MessageBody for () {
type Error = Infallible; type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Empty BodySize::Sized(0)
} }
fn poll_next( fn poll_next(

View File

@ -11,15 +11,14 @@ use futures_core::ready;
mod body; mod body;
mod body_stream; mod body_stream;
mod message_body; mod message_body;
mod response_body;
mod size; mod size;
mod sized_stream; mod sized_stream;
pub use self::body::{AnyBody, Body, BoxAnyBody}; #[allow(deprecated)]
pub use self::body::{AnyBody, Body, BoxBody};
pub use self::body_stream::BodyStream; pub use self::body_stream::BodyStream;
pub use self::message_body::MessageBody; pub use self::message_body::MessageBody;
pub(crate) use self::message_body::MessageBodyMapErr; pub(crate) use self::message_body::MessageBodyMapErr;
pub use self::response_body::ResponseBody;
pub use self::size::BodySize; pub use self::size::BodySize;
pub use self::sized_stream::SizedStream; pub use self::sized_stream::SizedStream;
@ -29,23 +28,24 @@ pub use self::sized_stream::SizedStream;
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use actix_http::body::{Body, to_bytes}; /// use actix_http::body::{AnyBody, to_bytes};
/// use bytes::Bytes; /// use bytes::Bytes;
/// ///
/// # async fn test_to_bytes() { /// # async fn test_to_bytes() {
/// let body = Body::Empty; /// let body = AnyBody::none();
/// let bytes = to_bytes(body).await.unwrap(); /// let bytes = to_bytes(body).await.unwrap();
/// assert!(bytes.is_empty()); /// assert!(bytes.is_empty());
/// ///
/// let body = Body::Bytes(Bytes::from_static(b"123")); /// let body = AnyBody::copy_from_slice(b"123");
/// let bytes = to_bytes(body).await.unwrap(); /// let bytes = to_bytes(body).await.unwrap();
/// assert_eq!(bytes, b"123"[..]); /// assert_eq!(bytes, b"123"[..]);
/// # } /// # }
/// ``` /// ```
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> { pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
let cap = match body.size() { let cap = match body.size() {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => return Ok(Bytes::new()), BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()),
BodySize::Sized(size) => size as usize, BodySize::Sized(size) => size as usize,
// good enough first guess for chunk size
BodySize::Stream => 32_768, BodySize::Stream => 32_768,
}; };
@ -75,22 +75,25 @@ mod tests {
use actix_utils::future::poll_fn; use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use super::*; use super::{to_bytes, AnyBody as TestAnyBody, BodySize, MessageBody as _};
impl Body { impl AnyBody {
pub(crate) fn get_ref(&self) -> &[u8] { pub(crate) fn get_ref(&self) -> &[u8] {
match *self { match *self {
Body::Bytes(ref bin) => bin, AnyBody::Bytes(ref bin) => bin,
_ => panic!(), _ => panic!(),
} }
} }
} }
/// AnyBody alias because rustc does not (can not?) infer the default type parameter.
type AnyBody = TestAnyBody;
#[actix_rt::test] #[actix_rt::test]
async fn test_static_str() { async fn test_static_str() {
assert_eq!(Body::from("").size(), BodySize::Sized(0)); assert_eq!(AnyBody::from("").size(), BodySize::Sized(0));
assert_eq!(Body::from("test").size(), BodySize::Sized(4)); assert_eq!(AnyBody::from("test").size(), BodySize::Sized(4));
assert_eq!(Body::from("test").get_ref(), b"test"); assert_eq!(AnyBody::from("test").get_ref(), b"test");
assert_eq!("test".size(), BodySize::Sized(4)); assert_eq!("test".size(), BodySize::Sized(4));
assert_eq!( assert_eq!(
@ -104,13 +107,16 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_static_bytes() { async fn test_static_bytes() {
assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4)); assert_eq!(AnyBody::from(b"test".as_ref()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test"); assert_eq!(AnyBody::from(b"test".as_ref()).get_ref(), b"test");
assert_eq!( assert_eq!(
Body::from_slice(b"test".as_ref()).size(), AnyBody::copy_from_slice(b"test".as_ref()).size(),
BodySize::Sized(4) BodySize::Sized(4)
); );
assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test"); assert_eq!(
AnyBody::copy_from_slice(b"test".as_ref()).get_ref(),
b"test"
);
let sb = Bytes::from(&b"test"[..]); let sb = Bytes::from(&b"test"[..]);
pin!(sb); pin!(sb);
@ -123,8 +129,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_vec() { async fn test_vec() {
assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4)); assert_eq!(AnyBody::from(Vec::from("test")).size(), BodySize::Sized(4));
assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test"); assert_eq!(AnyBody::from(Vec::from("test")).get_ref(), b"test");
let test_vec = Vec::from("test"); let test_vec = Vec::from("test");
pin!(test_vec); pin!(test_vec);
@ -141,8 +147,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_bytes() { async fn test_bytes() {
let b = Bytes::from("test"); let b = Bytes::from("test");
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test"); assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test");
pin!(b); pin!(b);
assert_eq!(b.size(), BodySize::Sized(4)); assert_eq!(b.size(), BodySize::Sized(4));
@ -155,8 +161,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_bytes_mut() { async fn test_bytes_mut() {
let b = BytesMut::from("test"); let b = BytesMut::from("test");
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test"); assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test");
pin!(b); pin!(b);
assert_eq!(b.size(), BodySize::Sized(4)); assert_eq!(b.size(), BodySize::Sized(4));
@ -169,10 +175,10 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_string() { async fn test_string() {
let b = "test".to_owned(); let b = "test".to_owned();
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test"); assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test");
assert_eq!(Body::from(&b).size(), BodySize::Sized(4)); assert_eq!(AnyBody::from(&b).size(), BodySize::Sized(4));
assert_eq!(Body::from(&b).get_ref(), b"test"); assert_eq!(AnyBody::from(&b).get_ref(), b"test");
pin!(b); pin!(b);
assert_eq!(b.size(), BodySize::Sized(4)); assert_eq!(b.size(), BodySize::Sized(4));
@ -184,7 +190,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_unit() { async fn test_unit() {
assert_eq!(().size(), BodySize::Empty); assert_eq!(().size(), BodySize::Sized(0));
assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx)) assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx))
.await .await
.is_none()); .is_none());
@ -194,40 +200,43 @@ mod tests {
async fn test_box_and_pin() { async fn test_box_and_pin() {
let val = Box::new(()); let val = Box::new(());
pin!(val); pin!(val);
assert_eq!(val.size(), BodySize::Empty); assert_eq!(val.size(), BodySize::Sized(0));
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none()); assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
let mut val = Box::pin(()); let mut val = Box::pin(());
assert_eq!(val.size(), BodySize::Empty); assert_eq!(val.size(), BodySize::Sized(0));
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none()); assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_body_eq() { async fn test_body_eq() {
assert!( assert!(
Body::Bytes(Bytes::from_static(b"1")) AnyBody::Bytes(Bytes::from_static(b"1"))
== Body::Bytes(Bytes::from_static(b"1")) == AnyBody::Bytes(Bytes::from_static(b"1"))
); );
assert!(Body::Bytes(Bytes::from_static(b"1")) != Body::None); assert!(AnyBody::Bytes(Bytes::from_static(b"1")) != AnyBody::None);
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_body_debug() { async fn test_body_debug() {
assert!(format!("{:?}", Body::None).contains("Body::None")); assert!(format!("{:?}", AnyBody::None).contains("Body::None"));
assert!(format!("{:?}", Body::Empty).contains("Body::Empty")); assert!(format!("{:?}", AnyBody::from(Bytes::from_static(b"1"))).contains('1'));
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1'));
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_serde_json() { async fn test_serde_json() {
use serde_json::{json, Value}; use serde_json::{json, Value};
assert_eq!( assert_eq!(
Body::from(serde_json::to_vec(&Value::String("test".to_owned())).unwrap()) AnyBody::from(
serde_json::to_vec(&Value::String("test".to_owned())).unwrap()
)
.size(), .size(),
BodySize::Sized(6) BodySize::Sized(6)
); );
assert_eq!( assert_eq!(
Body::from(serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap()) AnyBody::from(
serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap()
)
.size(), .size(),
BodySize::Sized(25) BodySize::Sized(25)
); );
@ -252,11 +261,11 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_to_bytes() { async fn test_to_bytes() {
let body = Body::Empty; let body = AnyBody::empty();
let bytes = to_bytes(body).await.unwrap(); let bytes = to_bytes(body).await.unwrap();
assert!(bytes.is_empty()); assert!(bytes.is_empty());
let body = Body::Bytes(Bytes::from_static(b"123")); let body = AnyBody::copy_from_slice(b"123");
let bytes = to_bytes(body).await.unwrap(); let bytes = to_bytes(body).await.unwrap();
assert_eq!(bytes, b"123"[..]); assert_eq!(bytes, b"123"[..]);
} }

View File

@ -1,84 +0,0 @@
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 for ResponseBody<B>
where
B: MessageBody,
B::Error: Into<Error>,
{
type Error = Error;
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, Self::Error>>> {
Stream::poll_next(self, cx)
}
}
impl<B> Stream for ResponseBody<B>
where
B: MessageBody,
B::Error: Into<Error>,
{
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).map_err(Into::into),
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
}
}
}

View File

@ -6,14 +6,9 @@ pub enum BodySize {
/// Will skip writing Content-Length header. /// Will skip writing Content-Length header.
None, None,
/// Zero size body.
///
/// Will write `Content-Length: 0` header.
Empty,
/// Known size body. /// Known size body.
/// ///
/// Will write `Content-Length: N` header. `Sized(0)` is treated the same as `Empty`. /// Will write `Content-Length: N` header.
Sized(u64), Sized(u64),
/// Unknown size body. /// Unknown size body.
@ -25,16 +20,17 @@ pub enum BodySize {
impl BodySize { impl BodySize {
/// Returns true if size hint indicates no or empty body. /// Returns true if size hint indicates no or empty body.
/// ///
/// Streams will return false because it cannot be known without reading the stream.
///
/// ``` /// ```
/// # use actix_http::body::BodySize; /// # use actix_http::body::BodySize;
/// assert!(BodySize::None.is_eof()); /// assert!(BodySize::None.is_eof());
/// assert!(BodySize::Empty.is_eof());
/// assert!(BodySize::Sized(0).is_eof()); /// assert!(BodySize::Sized(0).is_eof());
/// ///
/// assert!(!BodySize::Sized(64).is_eof()); /// assert!(!BodySize::Sized(64).is_eof());
/// assert!(!BodySize::Stream.is_eof()); /// assert!(!BodySize::Stream.is_eof());
/// ``` /// ```
pub fn is_eof(&self) -> bool { pub fn is_eof(&self) -> bool {
matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0)) matches!(self, BodySize::None | BodySize::Sized(0))
} }
} }

View File

@ -72,10 +72,22 @@ mod tests {
use actix_rt::pin; use actix_rt::pin;
use actix_utils::future::poll_fn; use actix_utils::future::poll_fn;
use futures_util::stream; use futures_util::stream;
use static_assertions::{assert_impl_all, assert_not_impl_all};
use super::*; use super::*;
use crate::body::to_bytes; use crate::body::to_bytes;
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, crate::Error>>>: MessageBody);
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, &'static str>>>: MessageBody);
assert_impl_all!(SizedStream<stream::Repeat<Result<Bytes, &'static str>>>: MessageBody);
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, Infallible>>>: MessageBody);
assert_impl_all!(SizedStream<stream::Repeat<Result<Bytes, Infallible>>>: MessageBody);
assert_not_impl_all!(SizedStream<stream::Empty<Bytes>>: MessageBody);
assert_not_impl_all!(SizedStream<stream::Repeat<Bytes>>: MessageBody);
// crate::Error is not Clone
assert_not_impl_all!(SizedStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
#[actix_rt::test] #[actix_rt::test]
async fn skips_empty_chunks() { async fn skips_empty_chunks() {
let body = SizedStream::new( let body = SizedStream::new(
@ -119,4 +131,37 @@ mod tests {
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12"))); assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
} }
#[actix_rt::test]
async fn stream_string_error() {
// `&'static str` does not impl `Error`
// but it does impl `Into<Box<dyn Error>>`
let body = SizedStream::new(0, stream::once(async { Err("stringy error") }));
assert_eq!(to_bytes(body).await, Ok(Bytes::new()));
let body = SizedStream::new(1, stream::once(async { Err("stringy error") }));
assert!(matches!(to_bytes(body).await, Err("stringy error")));
}
#[actix_rt::test]
async fn stream_boxed_error() {
// `Box<dyn Error>` does not impl `Error`
// but it does impl `Into<Box<dyn Error>>`
let body = SizedStream::new(
0,
stream::once(async { Err(Box::<dyn StdError>::from("stringy error")) }),
);
assert_eq!(to_bytes(body).await.unwrap(), Bytes::new());
let body = SizedStream::new(
1,
stream::once(async { Err(Box::<dyn StdError>::from("stringy error")) }),
);
assert_eq!(
to_bytes(body).await.unwrap_err().to_string(),
"stringy error"
);
}
} }

View File

@ -20,8 +20,10 @@ pub(crate) const DATE_VALUE_LENGTH: usize = 29;
pub enum KeepAlive { pub enum KeepAlive {
/// Keep alive in seconds /// Keep alive in seconds
Timeout(usize), Timeout(usize),
/// Rely on OS to shutdown tcp connection /// Rely on OS to shutdown tcp connection
Os, Os,
/// Disabled /// Disabled
Disabled, Disabled,
} }

View File

@ -24,7 +24,7 @@ use flate2::write::{GzEncoder, ZlibEncoder};
use zstd::stream::write::Encoder as ZstdEncoder; use zstd::stream::write::Encoder as ZstdEncoder;
use crate::{ use crate::{
body::{Body, BodySize, BoxAnyBody, MessageBody, ResponseBody}, body::{AnyBody, BodySize, MessageBody},
http::{ http::{
header::{ContentEncoding, CONTENT_ENCODING}, header::{ContentEncoding, CONTENT_ENCODING},
HeaderValue, StatusCode, HeaderValue, StatusCode,
@ -50,8 +50,8 @@ impl<B: MessageBody> Encoder<B> {
pub fn response( pub fn response(
encoding: ContentEncoding, encoding: ContentEncoding,
head: &mut ResponseHead, head: &mut ResponseHead,
body: ResponseBody<B>, body: AnyBody<B>,
) -> ResponseBody<Encoder<B>> { ) -> AnyBody<Encoder<B>> {
let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING) let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|| head.status == StatusCode::SWITCHING_PROTOCOLS || head.status == StatusCode::SWITCHING_PROTOCOLS
|| head.status == StatusCode::NO_CONTENT || head.status == StatusCode::NO_CONTENT
@ -59,19 +59,15 @@ impl<B: MessageBody> Encoder<B> {
|| encoding == ContentEncoding::Auto); || encoding == ContentEncoding::Auto);
let body = match body { let body = match body {
ResponseBody::Other(b) => match b { AnyBody::None => return AnyBody::None,
Body::None => return ResponseBody::Other(Body::None), AnyBody::Bytes(buf) => {
Body::Empty => return ResponseBody::Other(Body::Empty),
Body::Bytes(buf) => {
if can_encode { if can_encode {
EncoderBody::Bytes(buf) EncoderBody::Bytes(buf)
} else { } else {
return ResponseBody::Other(Body::Bytes(buf)); return AnyBody::Bytes(buf);
} }
} }
Body::Message(stream) => EncoderBody::BoxedStream(stream), AnyBody::Body(body) => EncoderBody::Stream(body),
},
ResponseBody::Body(stream) => EncoderBody::Stream(stream),
}; };
if can_encode { if can_encode {
@ -79,7 +75,8 @@ impl<B: MessageBody> Encoder<B> {
if let Some(enc) = ContentEncoder::encoder(encoding) { if let Some(enc) = ContentEncoder::encoder(encoding) {
update_head(encoding, head); update_head(encoding, head);
head.no_chunking(false); head.no_chunking(false);
return ResponseBody::Body(Encoder {
return AnyBody::Body(Encoder {
body, body,
eof: false, eof: false,
fut: None, fut: None,
@ -88,7 +85,7 @@ impl<B: MessageBody> Encoder<B> {
} }
} }
ResponseBody::Body(Encoder { AnyBody::Body(Encoder {
body, body,
eof: false, eof: false,
fut: None, fut: None,
@ -101,7 +98,6 @@ impl<B: MessageBody> Encoder<B> {
enum EncoderBody<B> { enum EncoderBody<B> {
Bytes(Bytes), Bytes(Bytes),
Stream(#[pin] B), Stream(#[pin] B),
BoxedStream(BoxAnyBody),
} }
impl<B> MessageBody for EncoderBody<B> impl<B> MessageBody for EncoderBody<B>
@ -114,7 +110,6 @@ where
match self { match self {
EncoderBody::Bytes(ref b) => b.size(), EncoderBody::Bytes(ref b) => b.size(),
EncoderBody::Stream(ref b) => b.size(), EncoderBody::Stream(ref b) => b.size(),
EncoderBody::BoxedStream(ref b) => b.size(),
} }
} }
@ -131,9 +126,6 @@ where
} }
} }
EncoderBodyProj::Stream(b) => b.poll_next(cx).map_err(EncoderError::Body), EncoderBodyProj::Stream(b) => b.poll_next(cx).map_err(EncoderError::Body),
EncoderBodyProj::BoxedStream(ref mut b) => {
b.as_pin_mut().poll_next(cx).map_err(EncoderError::Boxed)
}
} }
} }
} }
@ -349,9 +341,6 @@ pub enum EncoderError<E> {
#[display(fmt = "body")] #[display(fmt = "body")]
Body(E), Body(E),
#[display(fmt = "boxed")]
Boxed(Box<dyn StdError>),
#[display(fmt = "blocking")] #[display(fmt = "blocking")]
Blocking(BlockingError), Blocking(BlockingError),
@ -363,7 +352,6 @@ impl<E: StdError + 'static> StdError for EncoderError<E> {
fn source(&self) -> Option<&(dyn StdError + 'static)> { fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self { match self {
EncoderError::Body(err) => Some(err), EncoderError::Body(err) => Some(err),
EncoderError::Boxed(err) => Some(&**err),
EncoderError::Blocking(err) => Some(err), EncoderError::Blocking(err) => Some(err),
EncoderError::Io(err) => Some(err), EncoderError::Io(err) => Some(err),
} }

View File

@ -5,10 +5,7 @@ use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Err
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
use http::{uri::InvalidUri, StatusCode}; use http::{uri::InvalidUri, StatusCode};
use crate::{ use crate::{body::AnyBody, ws, Response};
body::{AnyBody, Body},
ws, Response,
};
pub use http::Error as HttpError; pub use http::Error as HttpError;
@ -29,6 +26,11 @@ impl Error {
} }
} }
pub(crate) fn with_cause(mut self, cause: impl Into<Box<dyn StdError>>) -> Self {
self.inner.cause = Some(cause.into());
self
}
pub(crate) fn new_http() -> Self { pub(crate) fn new_http() -> Self {
Self::new(Kind::Http) Self::new(Kind::Http)
} }
@ -49,14 +51,12 @@ impl Error {
Self::new(Kind::SendResponse) Self::new(Kind::SendResponse)
} }
// TODO: remove allow #[allow(unused)] // reserved for future use (TODO: remove allow when being used)
#[allow(dead_code)]
pub(crate) fn new_io() -> Self { pub(crate) fn new_io() -> Self {
Self::new(Kind::Io) Self::new(Kind::Io)
} }
// used in encoder behind feature flag so ignore unused warning #[allow(unused)] // used in encoder behind feature flag so ignore unused warning
#[allow(unused)]
pub(crate) fn new_encoder() -> Self { pub(crate) fn new_encoder() -> Self {
Self::new(Kind::Encoder) Self::new(Kind::Encoder)
} }
@ -64,26 +64,21 @@ impl Error {
pub(crate) fn new_ws() -> Self { pub(crate) fn new_ws() -> Self {
Self::new(Kind::Ws) Self::new(Kind::Ws)
} }
pub(crate) fn with_cause(mut self, cause: impl Into<Box<dyn StdError>>) -> Self {
self.inner.cause = Some(cause.into());
self
}
} }
impl From<Error> for Response<AnyBody> { impl<B> From<Error> for Response<AnyBody<B>> {
fn from(err: Error) -> Self { fn from(err: Error) -> Self {
let status_code = match err.inner.kind { let status_code = match err.inner.kind {
Kind::Parse => StatusCode::BAD_REQUEST, Kind::Parse => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
}; };
Response::new(status_code).set_body(Body::from(err.to_string())) Response::new(status_code).set_body(AnyBody::from(err.to_string()))
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Display)]
pub enum Kind { pub(crate) enum Kind {
#[display(fmt = "error processing HTTP")] #[display(fmt = "error processing HTTP")]
Http, Http,

View File

@ -120,7 +120,7 @@ impl Decoder for ClientCodec {
debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set"); debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set");
if let Some((req, payload)) = self.inner.decoder.decode(src)? { if let Some((req, payload)) = self.inner.decoder.decode(src)? {
if let Some(ctype) = req.ctype() { if let Some(ctype) = req.conn_type() {
// do not use peer's keep-alive // do not use peer's keep-alive
self.inner.ctype = if ctype == ConnectionType::KeepAlive { self.inner.ctype = if ctype == ConnectionType::KeepAlive {
self.inner.ctype self.inner.ctype

View File

@ -29,7 +29,7 @@ pub struct Codec {
decoder: decoder::MessageDecoder<Request>, decoder: decoder::MessageDecoder<Request>,
payload: Option<PayloadDecoder>, payload: Option<PayloadDecoder>,
version: Version, version: Version,
ctype: ConnectionType, conn_type: ConnectionType,
// encoder part // encoder part
flags: Flags, flags: Flags,
@ -65,7 +65,7 @@ impl Codec {
decoder: decoder::MessageDecoder::default(), decoder: decoder::MessageDecoder::default(),
payload: None, payload: None,
version: Version::HTTP_11, version: Version::HTTP_11,
ctype: ConnectionType::Close, conn_type: ConnectionType::Close,
encoder: encoder::MessageEncoder::default(), encoder: encoder::MessageEncoder::default(),
} }
} }
@ -73,13 +73,13 @@ impl Codec {
/// Check if request is upgrade. /// Check if request is upgrade.
#[inline] #[inline]
pub fn upgrade(&self) -> bool { pub fn upgrade(&self) -> bool {
self.ctype == ConnectionType::Upgrade self.conn_type == ConnectionType::Upgrade
} }
/// Check if last response is keep-alive. /// Check if last response is keep-alive.
#[inline] #[inline]
pub fn keepalive(&self) -> bool { pub fn keepalive(&self) -> bool {
self.ctype == ConnectionType::KeepAlive self.conn_type == ConnectionType::KeepAlive
} }
/// Check if keep-alive enabled on server level. /// Check if keep-alive enabled on server level.
@ -124,11 +124,11 @@ impl Decoder for Codec {
let head = req.head(); let head = req.head();
self.flags.set(Flags::HEAD, head.method == Method::HEAD); self.flags.set(Flags::HEAD, head.method == Method::HEAD);
self.version = head.version; self.version = head.version;
self.ctype = head.connection_type(); self.conn_type = head.connection_type();
if self.ctype == ConnectionType::KeepAlive if self.conn_type == ConnectionType::KeepAlive
&& !self.flags.contains(Flags::KEEPALIVE_ENABLED) && !self.flags.contains(Flags::KEEPALIVE_ENABLED)
{ {
self.ctype = ConnectionType::Close self.conn_type = ConnectionType::Close
} }
match payload { match payload {
PayloadType::None => self.payload = None, PayloadType::None => self.payload = None,
@ -159,14 +159,14 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
res.head_mut().version = self.version; res.head_mut().version = self.version;
// connection status // connection status
self.ctype = if let Some(ct) = res.head().ctype() { self.conn_type = if let Some(ct) = res.head().conn_type() {
if ct == ConnectionType::KeepAlive { if ct == ConnectionType::KeepAlive {
self.ctype self.conn_type
} else { } else {
ct ct
} }
} else { } else {
self.ctype self.conn_type
}; };
// encode message // encode message
@ -177,10 +177,9 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
self.flags.contains(Flags::STREAM), self.flags.contains(Flags::STREAM),
self.version, self.version,
length, length,
self.ctype, self.conn_type,
&self.config, &self.config,
)?; )?;
// self.headers_size = (dst.len() - len) as u32;
} }
Message::Chunk(Some(bytes)) => { Message::Chunk(Some(bytes)) => {
self.encoder.encode_chunk(bytes.as_ref(), dst)?; self.encoder.encode_chunk(bytes.as_ref(), dst)?;
@ -189,6 +188,7 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
self.encoder.encode_eof(dst)?; self.encoder.encode_eof(dst)?;
} }
} }
Ok(()) Ok(())
} }
} }

View File

@ -325,7 +325,7 @@ where
) -> Result<(), DispatchError> { ) -> Result<(), DispatchError> {
let size = self.as_mut().send_response_inner(message, &body)?; let size = self.as_mut().send_response_inner(message, &body)?;
let state = match size { let state = match size {
BodySize::None | BodySize::Empty => State::None, BodySize::None | BodySize::Sized(0) => State::None,
_ => State::SendPayload(body), _ => State::SendPayload(body),
}; };
self.project().state.set(state); self.project().state.set(state);
@ -339,7 +339,7 @@ where
) -> Result<(), DispatchError> { ) -> Result<(), DispatchError> {
let size = self.as_mut().send_response_inner(message, &body)?; let size = self.as_mut().send_response_inner(message, &body)?;
let state = match size { let state = match size {
BodySize::None | BodySize::Empty => State::None, BodySize::None | BodySize::Sized(0) => State::None,
_ => State::SendErrorPayload(body), _ => State::SendErrorPayload(body),
}; };
self.project().state.set(state); self.project().state.set(state);
@ -380,7 +380,7 @@ where
// send_response would update InnerDispatcher state to SendPayload or // send_response would update InnerDispatcher state to SendPayload or
// None(If response body is empty). // None(If response body is empty).
// continue loop to poll it. // continue loop to poll it.
self.as_mut().send_error_response(res, AnyBody::Empty)?; self.as_mut().send_error_response(res, AnyBody::empty())?;
} }
// return with upgrade request and poll it exclusively. // return with upgrade request and poll it exclusively.
@ -772,7 +772,7 @@ where
trace!("Slow request timeout"); trace!("Slow request timeout");
let _ = self.as_mut().send_error_response( let _ = self.as_mut().send_error_response(
Response::with_body(StatusCode::REQUEST_TIMEOUT, ()), Response::with_body(StatusCode::REQUEST_TIMEOUT, ()),
AnyBody::Empty, AnyBody::empty(),
); );
this = self.project(); this = self.project();
this.flags.insert(Flags::STARTED | Flags::SHUTDOWN); this.flags.insert(Flags::STARTED | Flags::SHUTDOWN);
@ -1077,7 +1077,7 @@ mod tests {
fn_service(|req: Request| { fn_service(|req: Request| {
let path = req.path().as_bytes(); let path = req.path().as_bytes();
ready(Ok::<_, Error>( ready(Ok::<_, Error>(
Response::ok().set_body(AnyBody::from_slice(path)), Response::ok().set_body(AnyBody::copy_from_slice(path)),
)) ))
}) })
} }

View File

@ -56,7 +56,7 @@ pub(crate) trait MessageType: Sized {
dst: &mut BytesMut, dst: &mut BytesMut,
version: Version, version: Version,
mut length: BodySize, mut length: BodySize,
ctype: ConnectionType, conn_type: ConnectionType,
config: &ServiceConfig, config: &ServiceConfig,
) -> io::Result<()> { ) -> io::Result<()> {
let chunked = self.chunked(); let chunked = self.chunked();
@ -71,14 +71,23 @@ pub(crate) trait MessageType: Sized {
| StatusCode::PROCESSING | StatusCode::PROCESSING
| StatusCode::NO_CONTENT => { | StatusCode::NO_CONTENT => {
// skip content-length and transfer-encoding headers // skip content-length and transfer-encoding headers
// See https://tools.ietf.org/html/rfc7230#section-3.3.1 // see https://tools.ietf.org/html/rfc7230#section-3.3.1
// and https://tools.ietf.org/html/rfc7230#section-3.3.2 // and https://tools.ietf.org/html/rfc7230#section-3.3.2
skip_len = true; skip_len = true;
length = BodySize::None length = BodySize::None
} }
StatusCode::NOT_MODIFIED => {
// 304 responses should never have a body but should retain a manually set
// content-length header see https://tools.ietf.org/html/rfc7232#section-4.1
skip_len = false;
length = BodySize::None;
}
_ => {} _ => {}
} }
} }
match length { match length {
BodySize::Stream => { BodySize::Stream => {
if chunked { if chunked {
@ -93,19 +102,16 @@ pub(crate) trait MessageType: Sized {
dst.put_slice(b"\r\n"); dst.put_slice(b"\r\n");
} }
} }
BodySize::Empty => { BodySize::Sized(0) if camel_case => {
if camel_case { dst.put_slice(b"\r\nContent-Length: 0\r\n")
dst.put_slice(b"\r\nContent-Length: 0\r\n");
} else {
dst.put_slice(b"\r\ncontent-length: 0\r\n");
}
} }
BodySize::Sized(0) => dst.put_slice(b"\r\ncontent-length: 0\r\n"),
BodySize::Sized(len) => helpers::write_content_length(len, dst), BodySize::Sized(len) => helpers::write_content_length(len, dst),
BodySize::None => dst.put_slice(b"\r\n"), BodySize::None => dst.put_slice(b"\r\n"),
} }
// Connection // Connection
match ctype { match conn_type {
ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"), ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"),
ConnectionType::KeepAlive if version < Version::HTTP_11 => { ConnectionType::KeepAlive if version < Version::HTTP_11 => {
if camel_case { if camel_case {
@ -330,13 +336,13 @@ impl<T: MessageType> MessageEncoder<T> {
stream: bool, stream: bool,
version: Version, version: Version,
length: BodySize, length: BodySize,
ctype: ConnectionType, conn_type: ConnectionType,
config: &ServiceConfig, config: &ServiceConfig,
) -> io::Result<()> { ) -> io::Result<()> {
// transfer encoding // transfer encoding
if !head { if !head {
self.te = match length { self.te = match length {
BodySize::Empty => TransferEncoding::empty(), BodySize::Sized(0) => TransferEncoding::empty(),
BodySize::Sized(len) => TransferEncoding::length(len), BodySize::Sized(len) => TransferEncoding::length(len),
BodySize::Stream => { BodySize::Stream => {
if message.chunked() && !stream { if message.chunked() && !stream {
@ -352,7 +358,7 @@ impl<T: MessageType> MessageEncoder<T> {
} }
message.encode_status(dst)?; message.encode_status(dst)?;
message.encode_headers(dst, version, length, ctype, config) message.encode_headers(dst, version, length, conn_type, config)
} }
} }
@ -366,10 +372,12 @@ pub(crate) struct TransferEncoding {
enum TransferEncodingKind { enum TransferEncodingKind {
/// An Encoder for when Transfer-Encoding includes `chunked`. /// An Encoder for when Transfer-Encoding includes `chunked`.
Chunked(bool), Chunked(bool),
/// An Encoder for when Content-Length is set. /// An Encoder for when Content-Length is set.
/// ///
/// Enforces that the body is not longer than the Content-Length header. /// Enforces that the body is not longer than the Content-Length header.
Length(u64), Length(u64),
/// An Encoder for when Content-Length is not known. /// An Encoder for when Content-Length is not known.
/// ///
/// Application decides when to stop writing. /// Application decides when to stop writing.
@ -553,7 +561,7 @@ mod tests {
let _ = head.encode_headers( let _ = head.encode_headers(
&mut bytes, &mut bytes,
Version::HTTP_11, Version::HTTP_11,
BodySize::Empty, BodySize::Sized(0),
ConnectionType::Close, ConnectionType::Close,
&ServiceConfig::default(), &ServiceConfig::default(),
); );
@ -624,7 +632,7 @@ mod tests {
let _ = head.encode_headers( let _ = head.encode_headers(
&mut bytes, &mut bytes,
Version::HTTP_11, Version::HTTP_11,
BodySize::Empty, BodySize::Sized(0),
ConnectionType::Close, ConnectionType::Close,
&ServiceConfig::default(), &ServiceConfig::default(),
); );

View File

@ -102,7 +102,6 @@ where
mod openssl { mod openssl {
use super::*; use super::*;
use actix_service::ServiceFactoryExt;
use actix_tls::accept::{ use actix_tls::accept::{
openssl::{Acceptor, SslAcceptor, SslError, TlsStream}, openssl::{Acceptor, SslAcceptor, SslError, TlsStream},
TlsError, TlsError,
@ -133,7 +132,7 @@ mod openssl {
U::Error: fmt::Display + Into<Response<AnyBody>>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create openssl based service /// Create OpenSSL based service.
pub fn openssl( pub fn openssl(
self, self,
acceptor: SslAcceptor, acceptor: SslAcceptor,
@ -145,11 +144,13 @@ mod openssl {
InitError = (), InitError = (),
> { > {
Acceptor::new(acceptor) Acceptor::new(acceptor)
.map_err(TlsError::Tls) .map_init_err(|_| {
.map_init_err(|_| panic!()) unreachable!("TLS acceptor service factory does not error on init")
.and_then(|io: TlsStream<TcpStream>| { })
.map_err(TlsError::into_service_error)
.map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().peer_addr().ok(); let peer_addr = io.get_ref().peer_addr().ok();
ready(Ok((io, peer_addr))) (io, peer_addr)
}) })
.and_then(self.map_err(TlsError::Service)) .and_then(self.map_err(TlsError::Service))
} }
@ -158,16 +159,17 @@ mod openssl {
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
mod rustls { mod rustls {
use super::*;
use std::io; use std::io;
use actix_service::ServiceFactoryExt; use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{ use actix_tls::accept::{
rustls::{Acceptor, ServerConfig, TlsStream}, rustls::{Acceptor, ServerConfig, TlsStream},
TlsError, TlsError,
}; };
use super::*;
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U> impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
@ -193,7 +195,7 @@ mod rustls {
U::Error: fmt::Display + Into<Response<AnyBody>>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create rustls based service /// Create Rustls based service.
pub fn rustls( pub fn rustls(
self, self,
config: ServerConfig, config: ServerConfig,
@ -205,11 +207,13 @@ mod rustls {
InitError = (), InitError = (),
> { > {
Acceptor::new(config) Acceptor::new(config)
.map_err(TlsError::Tls) .map_init_err(|_| {
.map_init_err(|_| panic!()) unreachable!("TLS acceptor service factory does not error on init")
.and_then(|io: TlsStream<TcpStream>| { })
.map_err(TlsError::into_service_error)
.map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok(); let peer_addr = io.get_ref().0.peer_addr().ok();
ready(Ok((io, peer_addr))) (io, peer_addr)
}) })
.and_then(self.map_err(TlsError::Service)) .and_then(self.map_err(TlsError::Service))
} }

View File

@ -10,11 +10,15 @@ use std::{
}; };
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::time::Sleep;
use actix_service::Service; use actix_service::Service;
use actix_utils::future::poll_fn; use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::ready; use futures_core::ready;
use h2::server::{Connection, SendResponse}; use h2::{
server::{Connection, SendResponse},
Ping, PingPong,
};
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
use log::{error, trace}; use log::{error, trace};
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
@ -36,29 +40,46 @@ pin_project! {
on_connect_data: OnConnectData, on_connect_data: OnConnectData,
config: ServiceConfig, config: ServiceConfig,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
_phantom: PhantomData<B>, ping_pong: Option<H2PingPong>,
_phantom: PhantomData<B>
} }
} }
impl<T, S, B, X, U> Dispatcher<T, S, B, X, U> { impl<T, S, B, X, U> Dispatcher<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
{
pub(crate) fn new( pub(crate) fn new(
flow: Rc<HttpFlow<S, X, U>>, flow: Rc<HttpFlow<S, X, U>>,
connection: Connection<T, Bytes>, mut connection: Connection<T, Bytes>,
on_connect_data: OnConnectData, on_connect_data: OnConnectData,
config: ServiceConfig, config: ServiceConfig,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
) -> Self { ) -> Self {
let ping_pong = config.keep_alive_timer().map(|timer| H2PingPong {
timer: Box::pin(timer),
on_flight: false,
ping_pong: connection.ping_pong().unwrap(),
});
Self { Self {
flow, flow,
config, config,
peer_addr, peer_addr,
connection, connection,
on_connect_data, on_connect_data,
ping_pong,
_phantom: PhantomData, _phantom: PhantomData,
} }
} }
} }
struct H2PingPong {
timer: Pin<Box<Sleep>>,
on_flight: bool,
ping_pong: PingPong,
}
impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U> impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
@ -77,9 +98,9 @@ where
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut(); let this = self.get_mut();
while let Some((req, tx)) = loop {
ready!(Pin::new(&mut this.connection).poll_accept(cx)?) match Pin::new(&mut this.connection).poll_accept(cx)? {
{ Poll::Ready(Some((req, tx))) => {
let (parts, body) = req.into_parts(); let (parts, body) = req.into_parts();
let pl = crate::h2::Payload::new(body); let pl = crate::h2::Payload::new(body);
let pl = Payload::<crate::payload::PayloadStream>::H2(pl); let pl = Payload::<crate::payload::PayloadStream>::H2(pl);
@ -123,8 +144,46 @@ where
} }
}); });
} }
Poll::Ready(None) => return Poll::Ready(Ok(())),
Poll::Pending => match this.ping_pong.as_mut() {
Some(ping_pong) => loop {
if ping_pong.on_flight {
// When have on flight ping pong. poll pong and and keep alive timer.
// on success pong received update keep alive timer to determine the next timing of
// ping pong.
match ping_pong.ping_pong.poll_pong(cx)? {
Poll::Ready(_) => {
ping_pong.on_flight = false;
Poll::Ready(Ok(())) let dead_line =
this.config.keep_alive_expire().unwrap();
ping_pong.timer.as_mut().reset(dead_line);
}
Poll::Pending => {
return ping_pong
.timer
.as_mut()
.poll(cx)
.map(|_| Ok(()))
}
}
} else {
// When there is no on flight ping pong. keep alive timer is used to wait for next
// timing of ping pong. Therefore at this point it serves as an interval instead.
ready!(ping_pong.timer.as_mut().poll(cx));
ping_pong.ping_pong.send_ping(Ping::opaque())?;
let dead_line = this.config.keep_alive_expire().unwrap();
ping_pong.timer.as_mut().reset(dead_line);
ping_pong.on_flight = true;
}
},
None => return Poll::Pending,
},
}
}
} }
} }
@ -226,9 +285,11 @@ fn prepare_response(
let _ = match size { let _ = match size {
BodySize::None | BodySize::Stream => None, BodySize::None | BodySize::Stream => None,
BodySize::Empty => res
BodySize::Sized(0) => res
.headers_mut() .headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")), .insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
BodySize::Sized(len) => { BodySize::Sized(len) => {
let mut buf = itoa::Buffer::new(); let mut buf = itoa::Buffer::new();

View File

@ -101,9 +101,11 @@ where
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
mod openssl { mod openssl {
use actix_service::{fn_factory, fn_service, ServiceFactoryExt}; use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream}; use actix_tls::accept::{
use actix_tls::accept::TlsError; openssl::{Acceptor, SslAcceptor, SslError, TlsStream},
TlsError,
};
use super::*; use super::*;
@ -118,7 +120,7 @@ mod openssl {
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>, B::Error: Into<Box<dyn StdError>>,
{ {
/// Create OpenSSL based service /// Create OpenSSL based service.
pub fn openssl( pub fn openssl(
self, self,
acceptor: SslAcceptor, acceptor: SslAcceptor,
@ -130,16 +132,14 @@ mod openssl {
InitError = S::InitError, InitError = S::InitError,
> { > {
Acceptor::new(acceptor) Acceptor::new(acceptor)
.map_err(TlsError::Tls) .map_init_err(|_| {
.map_init_err(|_| panic!()) unreachable!("TLS acceptor service factory does not error on init")
.and_then(fn_factory(|| { })
ready(Ok::<_, S::InitError>(fn_service( .map_err(TlsError::into_service_error)
|io: TlsStream<TcpStream>| { .map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().peer_addr().ok(); let peer_addr = io.get_ref().peer_addr().ok();
ready(Ok((io, peer_addr))) (io, peer_addr)
}, })
)))
}))
.and_then(self.map_err(TlsError::Service)) .and_then(self.map_err(TlsError::Service))
} }
} }
@ -147,12 +147,16 @@ mod openssl {
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
mod rustls { mod rustls {
use super::*;
use actix_service::ServiceFactoryExt;
use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream};
use actix_tls::accept::TlsError;
use std::io; use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls::{Acceptor, ServerConfig, TlsStream},
TlsError,
};
use super::*;
impl<S, B> H2Service<TlsStream<TcpStream>, S, B> impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
@ -164,7 +168,7 @@ mod rustls {
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>, B::Error: Into<Box<dyn StdError>>,
{ {
/// Create Rustls based service /// Create Rustls based service.
pub fn rustls( pub fn rustls(
self, self,
mut config: ServerConfig, mut config: ServerConfig,
@ -180,16 +184,14 @@ mod rustls {
config.alpn_protocols = protos; config.alpn_protocols = protos;
Acceptor::new(config) Acceptor::new(config)
.map_err(TlsError::Tls) .map_init_err(|_| {
.map_init_err(|_| panic!()) unreachable!("TLS acceptor service factory does not error on init")
.and_then(fn_factory(|| { })
ready(Ok::<_, S::InitError>(fn_service( .map_err(TlsError::into_service_error)
|io: TlsStream<TcpStream>| { .map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok(); let peer_addr = io.get_ref().0.peer_addr().ok();
ready(Ok((io, peer_addr))) (io, peer_addr)
}, })
)))
}))
.and_then(self.map_err(TlsError::Service)) .and_then(self.map_err(TlsError::Service))
} }
} }

View File

@ -195,6 +195,7 @@ mod tests {
use super::*; use super::*;
// copy of encoding from actix-web headers // copy of encoding from actix-web headers
#[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug)]
pub enum Encoding { pub enum Encoding {
Chunked, Chunked,

View File

@ -29,7 +29,6 @@ extern crate log;
pub mod body; pub mod body;
mod builder; mod builder;
pub mod client;
mod config; mod config;
#[cfg(feature = "__compress")] #[cfg(feature = "__compress")]

View File

@ -317,7 +317,7 @@ impl ResponseHead {
} }
#[inline] #[inline]
pub(crate) fn ctype(&self) -> Option<ConnectionType> { pub(crate) fn conn_type(&self) -> Option<ConnectionType> {
if self.flags.contains(Flags::CLOSE) { if self.flags.contains(Flags::CLOSE) {
Some(ConnectionType::Close) Some(ConnectionType::Close)
} else if self.flags.contains(Flags::KEEP_ALIVE) { } else if self.flags.contains(Flags::KEEP_ALIVE) {

View File

@ -28,7 +28,7 @@ impl Response<AnyBody> {
pub fn new(status: StatusCode) -> Self { pub fn new(status: StatusCode) -> Self {
Response { Response {
head: BoxedResponseHead::new(status), head: BoxedResponseHead::new(status),
body: AnyBody::Empty, body: AnyBody::empty(),
} }
} }

View File

@ -262,7 +262,7 @@ impl ResponseBuilder {
S: Stream<Item = Result<Bytes, E>> + 'static, S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Box<dyn StdError>> + 'static, E: Into<Box<dyn StdError>> + 'static,
{ {
self.body(AnyBody::from_message(BodyStream::new(stream))) self.body(AnyBody::new_boxed(BodyStream::new(stream)))
} }
/// Generate response with an empty body. /// Generate response with an empty body.
@ -270,7 +270,7 @@ impl ResponseBuilder {
/// This `ResponseBuilder` will be left in a useless state. /// This `ResponseBuilder` will be left in a useless state.
#[inline] #[inline]
pub fn finish(&mut self) -> Response<AnyBody> { pub fn finish(&mut self) -> Response<AnyBody> {
self.body(AnyBody::Empty) self.body(AnyBody::empty())
} }
/// Create an owned `ResponseBuilder`, leaving the original in a useless state. /// Create an owned `ResponseBuilder`, leaving the original in a useless state.
@ -357,7 +357,7 @@ impl fmt::Debug for ResponseBuilder {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::body::Body; use crate::body::AnyBody;
use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE};
#[test] #[test]
@ -390,13 +390,13 @@ mod tests {
fn test_content_type() { fn test_content_type() {
let resp = Response::build(StatusCode::OK) let resp = Response::build(StatusCode::OK)
.content_type("text/plain") .content_type("text/plain")
.body(Body::Empty); .body(AnyBody::empty());
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain")
} }
#[test] #[test]
fn test_into_builder() { fn test_into_builder() {
let mut resp: Response<Body> = "test".into(); let mut resp: Response<AnyBody> = "test".into();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
resp.headers_mut().insert( resp.headers_mut().insert(

View File

@ -195,9 +195,11 @@ where
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
mod openssl { mod openssl {
use actix_service::ServiceFactoryExt; use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream}; use actix_tls::accept::{
use actix_tls::accept::TlsError; openssl::{Acceptor, SslAcceptor, SslError, TlsStream},
TlsError,
};
use super::*; use super::*;
@ -227,7 +229,7 @@ mod openssl {
U::Error: fmt::Display + Into<Response<AnyBody>>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create openssl based service /// Create OpenSSL based service.
pub fn openssl( pub fn openssl(
self, self,
acceptor: SslAcceptor, acceptor: SslAcceptor,
@ -239,9 +241,11 @@ mod openssl {
InitError = (), InitError = (),
> { > {
Acceptor::new(acceptor) Acceptor::new(acceptor)
.map_err(TlsError::Tls) .map_init_err(|_| {
.map_init_err(|_| panic!()) unreachable!("TLS acceptor service factory does not error on init")
.and_then(|io: TlsStream<TcpStream>| async { })
.map_err(TlsError::into_service_error)
.map(|io: TlsStream<TcpStream>| {
let proto = if let Some(protos) = io.ssl().selected_alpn_protocol() { let proto = if let Some(protos) = io.ssl().selected_alpn_protocol() {
if protos.windows(2).any(|window| window == b"h2") { if protos.windows(2).any(|window| window == b"h2") {
Protocol::Http2 Protocol::Http2
@ -251,8 +255,9 @@ mod openssl {
} else { } else {
Protocol::Http1 Protocol::Http1
}; };
let peer_addr = io.get_ref().peer_addr().ok(); let peer_addr = io.get_ref().peer_addr().ok();
Ok((io, proto, peer_addr)) (io, proto, peer_addr)
}) })
.and_then(self.map_err(TlsError::Service)) .and_then(self.map_err(TlsError::Service))
} }
@ -263,11 +268,13 @@ mod openssl {
mod rustls { mod rustls {
use std::io; use std::io;
use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream}; use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::TlsError; use actix_tls::accept::{
rustls::{Acceptor, ServerConfig, TlsStream},
TlsError,
};
use super::*; use super::*;
use actix_service::ServiceFactoryExt;
impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U> impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U>
where where
@ -295,7 +302,7 @@ mod rustls {
U::Error: fmt::Display + Into<Response<AnyBody>>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create rustls based service /// Create Rustls based service.
pub fn rustls( pub fn rustls(
self, self,
mut config: ServerConfig, mut config: ServerConfig,
@ -311,8 +318,10 @@ mod rustls {
config.alpn_protocols = protos; config.alpn_protocols = protos;
Acceptor::new(config) Acceptor::new(config)
.map_err(TlsError::Tls) .map_init_err(|_| {
.map_init_err(|_| panic!()) unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.and_then(|io: TlsStream<TcpStream>| async { .and_then(|io: TlsStream<TcpStream>| async {
let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() { let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() {
if protos.windows(2).any(|window| window == b"h2") { if protos.windows(2).any(|window| window == b"h2") {

View File

@ -210,7 +210,6 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder {
Response::build(StatusCode::SWITCHING_PROTOCOLS) Response::build(StatusCode::SWITCHING_PROTOCOLS)
.upgrade("websocket") .upgrade("websocket")
.insert_header((header::TRANSFER_ENCODING, "chunked"))
.insert_header(( .insert_header((
header::SEC_WEBSOCKET_ACCEPT, header::SEC_WEBSOCKET_ACCEPT,
// key is known to be header value safe ascii // key is known to be header value safe ascii

View File

@ -0,0 +1,77 @@
use std::io;
use actix_http::{error::Error, HttpService, Response};
use actix_server::Server;
#[actix_rt::test]
async fn h2_ping_pong() -> io::Result<()> {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
let lst = std::net::TcpListener::bind("127.0.0.1:0")?;
let addr = lst.local_addr().unwrap();
let join = std::thread::spawn(move || {
actix_rt::System::new().block_on(async move {
let srv = Server::build()
.disable_signals()
.workers(1)
.listen("h2_ping_pong", lst, || {
HttpService::build()
.keep_alive(3)
.h2(|_| async { Ok::<_, Error>(Response::ok()) })
.tcp()
})?
.run();
tx.send(srv.handle()).unwrap();
srv.await
})
});
let handle = rx.recv().unwrap();
let (sync_tx, rx) = std::sync::mpsc::sync_channel(1);
// use a separate thread for h2 client so it can be blocked.
std::thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async move {
let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
let (mut tx, conn) = h2::client::handshake(stream).await.unwrap();
tokio::spawn(async move { conn.await.unwrap() });
let (res, _) = tx.send_request(::http::Request::new(()), true).unwrap();
let res = res.await.unwrap();
assert_eq!(res.status().as_u16(), 200);
sync_tx.send(()).unwrap();
// intentionally block the client thread so it can not answer ping pong.
std::thread::sleep(std::time::Duration::from_secs(1000));
})
});
rx.recv().unwrap();
let now = std::time::Instant::now();
// stop server gracefully. this step would take up to 30 seconds.
handle.stop(true).await;
// join server thread. only when connection are all gone this step would finish.
join.join().unwrap()?;
// check the time used for join server thread so it's known that the server shutdown
// is from keep alive and not server graceful shutdown timeout.
assert!(now.elapsed() < std::time::Duration::from_secs(30));
Ok(())
}

View File

@ -5,10 +5,10 @@ extern crate tls_openssl as openssl;
use std::{convert::Infallible, io}; use std::{convert::Infallible, io};
use actix_http::{ use actix_http::{
body::{AnyBody, Body, SizedStream}, body::{AnyBody, SizedStream},
error::PayloadError, error::PayloadError,
http::{ http::{
header::{self, HeaderName, HeaderValue}, header::{self, HeaderValue},
Method, StatusCode, Version, Method, StatusCode, Version,
}, },
Error, HttpMessage, HttpService, Request, Response, Error, HttpMessage, HttpService, Request, Response,
@ -143,38 +143,25 @@ async fn test_h2_content_length() {
}) })
.await; .await;
let header = HeaderName::from_static("content-length"); static VALUE: HeaderValue = HeaderValue::from_static("0");
let value = HeaderValue::from_static("0");
{ {
for &i in &[0] { let req = srv.request(Method::HEAD, srv.surl("/0")).send();
let req = srv req.await.expect_err("should timeout on recv 1xx frame");
.request(Method::HEAD, srv.surl(&format!("/{}", i)))
.send();
let _response = req.await.expect_err("should timeout on recv 1xx frame");
// assert_eq!(response.headers().get(&header), None);
let req = srv let req = srv.request(Method::GET, srv.surl("/0")).send();
.request(Method::GET, srv.surl(&format!("/{}", i))) req.await.expect_err("should timeout on recv 1xx frame");
.send();
let _response = req.await.expect_err("should timeout on recv 1xx frame");
// assert_eq!(response.headers().get(&header), None);
}
for &i in &[1] { let req = srv.request(Method::GET, srv.surl("/1")).send();
let req = srv
.request(Method::GET, srv.surl(&format!("/{}", i)))
.send();
let response = req.await.unwrap(); let response = req.await.unwrap();
assert_eq!(response.headers().get(&header), None); assert!(response.headers().get("content-length").is_none());
}
for &i in &[2, 3] { for &i in &[2, 3] {
let req = srv let req = srv
.request(Method::GET, srv.surl(&format!("/{}", i))) .request(Method::GET, srv.surl(&format!("/{}", i)))
.send(); .send();
let response = req.await.unwrap(); let response = req.await.unwrap();
assert_eq!(response.headers().get(&header), Some(&value)); assert_eq!(response.headers().get("content-length"), Some(&VALUE));
} }
} }
} }
@ -422,7 +409,7 @@ impl From<BadRequest> for Response<AnyBody> {
async fn test_h2_service_error() { 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<Body>, _>(BadRequest)) .h2(|_| err::<Response<AnyBody>, _>(BadRequest))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })

View File

@ -10,7 +10,7 @@ use std::{
}; };
use actix_http::{ use actix_http::{
body::{AnyBody, Body, SizedStream}, body::{AnyBody, SizedStream},
error::PayloadError, error::PayloadError,
http::{ http::{
header::{self, HeaderName, HeaderValue}, header::{self, HeaderName, HeaderValue},
@ -26,10 +26,7 @@ use bytes::{Bytes, BytesMut};
use derive_more::{Display, Error}; use derive_more::{Display, Error};
use futures_core::Stream; use futures_core::Stream;
use futures_util::stream::{once, StreamExt as _}; use futures_util::stream::{once, StreamExt as _};
use rustls::{ use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig, ServerName};
Certificate, OwnedTrustAnchor, PrivateKey, RootCertStore,
ServerConfig as RustlsServerConfig, ServerName,
};
use rustls_pemfile::{certs, pkcs8_private_keys}; use rustls_pemfile::{certs, pkcs8_private_keys};
async fn load_body<S>(mut stream: S) -> Result<BytesMut, PayloadError> async fn load_body<S>(mut stream: S) -> Result<BytesMut, PayloadError>
@ -480,7 +477,7 @@ impl From<BadRequest> for Response<AnyBody> {
async fn test_h2_service_error() { 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<Body>, _>(BadRequest)) .h2(|_| err::<Response<AnyBody>, _>(BadRequest))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -497,7 +494,7 @@ async fn test_h2_service_error() {
async fn test_h1_service_error() { 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<Body>, _>(BadRequest)) .h1(|_| err::<Response<AnyBody>, _>(BadRequest))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;

View File

@ -6,7 +6,7 @@ use std::{
}; };
use actix_http::{ use actix_http::{
body::{AnyBody, Body, SizedStream}, body::{AnyBody, SizedStream},
header, http, Error, HttpMessage, HttpService, KeepAlive, Request, Response, header, http, Error, HttpMessage, HttpService, KeepAlive, Request, Response,
StatusCode, StatusCode,
}; };
@ -24,7 +24,7 @@ use regex::Regex;
#[actix_rt::test] #[actix_rt::test]
async fn test_h1() { async fn test_h1() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(KeepAlive::Disabled) .keep_alive(KeepAlive::Disabled)
.client_timeout(1000) .client_timeout(1000)
@ -39,11 +39,13 @@ async fn test_h1() {
let response = srv.get("/").send().await.unwrap(); let response = srv.get("/").send().await.unwrap();
assert!(response.status().is_success()); assert!(response.status().is_success());
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_2() { async fn test_h1_2() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(KeepAlive::Disabled) .keep_alive(KeepAlive::Disabled)
.client_timeout(1000) .client_timeout(1000)
@ -59,6 +61,8 @@ async fn test_h1_2() {
let response = srv.get("/").send().await.unwrap(); let response = srv.get("/").send().await.unwrap();
assert!(response.status().is_success()); assert!(response.status().is_success());
srv.stop().await;
} }
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
@ -73,7 +77,7 @@ impl From<ExpectFailed> for Response<AnyBody> {
#[actix_rt::test] #[actix_rt::test]
async fn test_expect_continue() { async fn test_expect_continue() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.expect(fn_service(|req: Request| { .expect(fn_service(|req: Request| {
if req.head().uri.query() == Some("yes=") { if req.head().uri.query() == Some("yes=") {
@ -98,11 +102,13 @@ async fn test_expect_continue() {
let mut data = String::new(); let mut data = String::new();
let _ = stream.read_to_string(&mut data); let _ = stream.read_to_string(&mut data);
assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n")); assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n"));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_expect_continue_h1() { async fn test_expect_continue_h1() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.expect(fn_service(|req: Request| { .expect(fn_service(|req: Request| {
sleep(Duration::from_millis(20)).then(move |_| { sleep(Duration::from_millis(20)).then(move |_| {
@ -129,6 +135,8 @@ async fn test_expect_continue_h1() {
let mut data = String::new(); let mut data = String::new();
let _ = stream.read_to_string(&mut data); let _ = stream.read_to_string(&mut data);
assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n")); assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n"));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@ -136,7 +144,7 @@ async fn test_chunked_payload() {
let chunk_sizes = vec![32768, 32, 32768]; let chunk_sizes = vec![32768, 32, 32768];
let total_size: usize = chunk_sizes.iter().sum(); let total_size: usize = chunk_sizes.iter().sum();
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(fn_service(|mut request: Request| { .h1(fn_service(|mut request: Request| {
request request
@ -188,11 +196,13 @@ async fn test_chunked_payload() {
}; };
assert_eq!(returned_size, total_size); assert_eq!(returned_size, total_size);
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_slow_request() { async fn test_slow_request() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.client_timeout(100) .client_timeout(100)
.finish(|_| ok::<_, Infallible>(Response::ok())) .finish(|_| ok::<_, Infallible>(Response::ok()))
@ -205,11 +215,13 @@ async fn test_slow_request() {
let mut data = String::new(); let mut data = String::new();
let _ = stream.read_to_string(&mut data); let _ = stream.read_to_string(&mut data);
assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); assert!(data.starts_with("HTTP/1.1 408 Request Timeout"));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http1_malformed_request() { async fn test_http1_malformed_request() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
@ -221,11 +233,13 @@ async fn test_http1_malformed_request() {
let mut data = String::new(); let mut data = String::new();
let _ = stream.read_to_string(&mut data); let _ = stream.read_to_string(&mut data);
assert!(data.starts_with("HTTP/1.1 400 Bad Request")); assert!(data.starts_with("HTTP/1.1 400 Bad Request"));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http1_keepalive() { async fn test_http1_keepalive() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
@ -242,11 +256,13 @@ async fn test_http1_keepalive() {
let mut data = vec![0; 1024]; let mut data = vec![0; 1024];
let _ = stream.read(&mut data); let _ = stream.read(&mut data);
assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n");
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http1_keepalive_timeout() { async fn test_http1_keepalive_timeout() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(1) .keep_alive(1)
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
@ -264,11 +280,13 @@ async fn test_http1_keepalive_timeout() {
let mut data = vec![0; 1024]; let mut data = vec![0; 1024];
let res = stream.read(&mut data).unwrap(); let res = stream.read(&mut data).unwrap();
assert_eq!(res, 0); assert_eq!(res, 0);
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http1_keepalive_close() { async fn test_http1_keepalive_close() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
@ -285,11 +303,13 @@ async fn test_http1_keepalive_close() {
let mut data = vec![0; 1024]; let mut data = vec![0; 1024];
let res = stream.read(&mut data).unwrap(); let res = stream.read(&mut data).unwrap();
assert_eq!(res, 0); assert_eq!(res, 0);
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http10_keepalive_default_close() { async fn test_http10_keepalive_default_close() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
@ -305,11 +325,13 @@ async fn test_http10_keepalive_default_close() {
let mut data = vec![0; 1024]; let mut data = vec![0; 1024];
let res = stream.read(&mut data).unwrap(); let res = stream.read(&mut data).unwrap();
assert_eq!(res, 0); assert_eq!(res, 0);
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http10_keepalive() { async fn test_http10_keepalive() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
@ -332,11 +354,13 @@ async fn test_http10_keepalive() {
let mut data = vec![0; 1024]; let mut data = vec![0; 1024];
let res = stream.read(&mut data).unwrap(); let res = stream.read(&mut data).unwrap();
assert_eq!(res, 0); assert_eq!(res, 0);
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http1_keepalive_disabled() { async fn test_http1_keepalive_disabled() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(KeepAlive::Disabled) .keep_alive(KeepAlive::Disabled)
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
@ -353,6 +377,8 @@ async fn test_http1_keepalive_disabled() {
let mut data = vec![0; 1024]; let mut data = vec![0; 1024];
let res = stream.read(&mut data).unwrap(); let res = stream.read(&mut data).unwrap();
assert_eq!(res, 0); assert_eq!(res, 0);
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@ -362,7 +388,7 @@ async fn test_content_length() {
StatusCode, StatusCode,
}; };
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|req: Request| { .h1(|req: Request| {
let indx: usize = req.uri().path()[1..].parse().unwrap(); let indx: usize = req.uri().path()[1..].parse().unwrap();
@ -400,6 +426,8 @@ async fn test_content_length() {
assert_eq!(response.headers().get(&header), Some(&value)); assert_eq!(response.headers().get(&header), Some(&value));
} }
} }
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@ -439,6 +467,8 @@ async fn test_h1_headers() {
// read response // read response
let bytes = srv.load_body(response).await.unwrap(); let bytes = srv.load_body(response).await.unwrap();
assert_eq!(bytes, Bytes::from(data2)); assert_eq!(bytes, Bytes::from(data2));
srv.stop().await;
} }
const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
@ -478,6 +508,8 @@ async fn test_h1_body() {
// read response // read response
let bytes = srv.load_body(response).await.unwrap(); let bytes = srv.load_body(response).await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@ -503,6 +535,8 @@ async fn test_h1_head_empty() {
// read response // read response
let bytes = srv.load_body(response).await.unwrap(); let bytes = srv.load_body(response).await.unwrap();
assert!(bytes.is_empty()); assert!(bytes.is_empty());
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@ -528,11 +562,13 @@ async fn test_h1_head_binary() {
// read response // read response
let bytes = srv.load_body(response).await.unwrap(); let bytes = srv.load_body(response).await.unwrap();
assert!(bytes.is_empty()); assert!(bytes.is_empty());
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_head_binary2() { async fn test_h1_head_binary2() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.tcp() .tcp()
@ -549,6 +585,8 @@ async fn test_h1_head_binary2() {
.unwrap(); .unwrap();
assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); assert_eq!(format!("{}", STR.len()), len.to_str().unwrap());
} }
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@ -571,6 +609,8 @@ async fn test_h1_body_length() {
// read response // read response
let bytes = srv.load_body(response).await.unwrap(); let bytes = srv.load_body(response).await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@ -606,6 +646,8 @@ async fn test_h1_body_chunked_explicit() {
// decode // decode
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@ -635,6 +677,8 @@ async fn test_h1_body_chunked_implicit() {
// read response // read response
let bytes = srv.load_body(response).await.unwrap(); let bytes = srv.load_body(response).await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@ -662,6 +706,8 @@ async fn test_h1_response_http_error_handling() {
bytes, bytes,
Bytes::from_static(b"error processing HTTP: failed to parse header value") Bytes::from_static(b"error processing HTTP: failed to parse header value")
); );
srv.stop().await;
} }
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
@ -678,7 +724,7 @@ impl From<BadRequest> for Response<AnyBody> {
async fn test_h1_service_error() { async fn test_h1_service_error() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| err::<Response<Body>, _>(BadRequest)) .h1(|_| err::<Response<AnyBody>, _>(BadRequest))
.tcp() .tcp()
}) })
.await; .await;
@ -689,11 +735,13 @@ async fn test_h1_service_error() {
// read response // read response
let bytes = srv.load_body(response).await.unwrap(); let bytes = srv.load_body(response).await.unwrap();
assert_eq!(bytes, Bytes::from_static(b"error")); assert_eq!(bytes, Bytes::from_static(b"error"));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_on_connect() { async fn test_h1_on_connect() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.on_connect_ext(|_, data| { .on_connect_ext(|_, data| {
data.insert(20isize); data.insert(20isize);
@ -708,4 +756,93 @@ async fn test_h1_on_connect() {
let response = srv.get("/").send().await.unwrap(); let response = srv.get("/").send().await.unwrap();
assert!(response.status().is_success()); assert!(response.status().is_success());
srv.stop().await;
}
/// Tests compliance with 304 Not Modified spec in RFC 7232 §4.1.
/// https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
#[actix_rt::test]
async fn test_not_modified_spec_h1() {
// TODO: this test needing a few seconds to complete reveals some weirdness with either the
// dispatcher or the client, though similar hangs occur on other tests in this file, only
// succeeding, it seems, because of the keepalive timer
static CL: header::HeaderName = header::CONTENT_LENGTH;
let mut srv = test_server(|| {
HttpService::build()
.h1(|req: Request| {
let res: Response<AnyBody> = match req.path() {
// with no content-length
"/none" => {
Response::with_body(StatusCode::NOT_MODIFIED, AnyBody::None)
}
// with no content-length
"/body" => Response::with_body(
StatusCode::NOT_MODIFIED,
AnyBody::from("1234"),
),
// with manual content-length header and specific None body
"/cl-none" => {
let mut res =
Response::with_body(StatusCode::NOT_MODIFIED, AnyBody::None);
res.headers_mut()
.insert(CL.clone(), header::HeaderValue::from_static("24"));
res
}
// with manual content-length header and ignore-able body
"/cl-body" => {
let mut res = Response::with_body(
StatusCode::NOT_MODIFIED,
AnyBody::from("1234"),
);
res.headers_mut()
.insert(CL.clone(), header::HeaderValue::from_static("4"));
res
}
_ => panic!("unknown route"),
};
ok::<_, Infallible>(res)
})
.tcp()
})
.await;
let res = srv.get("/none").send().await.unwrap();
assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED);
assert_eq!(res.headers().get(&CL), None);
assert!(srv.load_body(res).await.unwrap().is_empty());
let res = srv.get("/body").send().await.unwrap();
assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED);
assert_eq!(res.headers().get(&CL), None);
assert!(srv.load_body(res).await.unwrap().is_empty());
let res = srv.get("/cl-none").send().await.unwrap();
assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED);
assert_eq!(
res.headers().get(&CL),
Some(&header::HeaderValue::from_static("24")),
);
assert!(srv.load_body(res).await.unwrap().is_empty());
let res = srv.get("/cl-body").send().await.unwrap();
assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED);
assert_eq!(
res.headers().get(&CL),
Some(&header::HeaderValue::from_static("4")),
);
// server does not prevent payload from being sent but clients may choose not to read it
// TODO: this is probably a bug, especially since CL header can differ in length from the body
assert!(!srv.load_body(res).await.unwrap().is_empty());
// TODO: add stream response tests
srv.stop().await;
} }

View File

@ -3,6 +3,17 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.4.0-beta.8 - 2021-11-22
* Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451]
* Added `MultipartError::NoContentDisposition` variant. [#2451]
* Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451]
* Added `Field::name` method for getting the field name. [#2451]
* `MultipartError` now marks variants with inner errors as the source. [#2451]
* `MultipartError` is now marked as non-exhaustive. [#2451]
[#2451]: https://github.com/actix/actix-web/pull/2451
## 0.4.0-beta.7 - 2021-10-20 ## 0.4.0-beta.7 - 2021-10-20
* Minimum supported Rust version (MSRV) is now 1.52. * Minimum supported Rust version (MSRV) is now 1.52.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-multipart" name = "actix-multipart"
version = "0.4.0-beta.7" version = "0.4.0-beta.8"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Multipart form support for Actix Web" description = "Multipart form support for Actix Web"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
@ -14,7 +14,7 @@ name = "actix_multipart"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
actix-web = { version = "4.0.0-beta.10", default-features = false } actix-web = { version = "4.0.0-beta.11", default-features = false }
actix-utils = "3.0.0" actix-utils = "3.0.0"
bytes = "1" bytes = "1"
@ -29,6 +29,6 @@ twoway = "0.2"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-http = "3.0.0-beta.11" actix-http = "3.0.0-beta.13"
tokio = { version = "1", features = ["sync"] } tokio = { version = "1", features = ["sync"] }
tokio-stream = "0.1" tokio-stream = "0.1"

View File

@ -3,11 +3,11 @@
> Multipart form support for Actix Web. > Multipart form support for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart)
[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.7)](https://docs.rs/actix-multipart/0.4.0-beta.7) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.8)](https://docs.rs/actix-multipart/0.4.0-beta.8)
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.7/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.7) [![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.8/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.8)
[![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -2,39 +2,52 @@
use actix_web::error::{ParseError, PayloadError}; use actix_web::error::{ParseError, PayloadError};
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::ResponseError; use actix_web::ResponseError;
use derive_more::{Display, From}; use derive_more::{Display, Error, From};
/// A set of errors that can occur during parsing multipart streams /// A set of errors that can occur during parsing multipart streams
#[derive(Debug, Display, From)] #[non_exhaustive]
#[derive(Debug, Display, From, Error)]
pub enum MultipartError { pub enum MultipartError {
/// Content-Disposition header is not found or is not equal to "form-data".
///
/// According to [RFC 7578](https://tools.ietf.org/html/rfc7578#section-4.2) a
/// Content-Disposition header must always be present and equal to "form-data".
#[display(fmt = "No Content-Disposition `form-data` header")]
NoContentDisposition,
/// Content-Type header is not found /// Content-Type header is not found
#[display(fmt = "No Content-type header found")] #[display(fmt = "No Content-Type header found")]
NoContentType, NoContentType,
/// Can not parse Content-Type header /// Can not parse Content-Type header
#[display(fmt = "Can not parse Content-Type header")] #[display(fmt = "Can not parse Content-Type header")]
ParseContentType, ParseContentType,
/// Multipart boundary is not found /// Multipart boundary is not found
#[display(fmt = "Multipart boundary is not found")] #[display(fmt = "Multipart boundary is not found")]
Boundary, Boundary,
/// Nested multipart is not supported /// Nested multipart is not supported
#[display(fmt = "Nested multipart is not supported")] #[display(fmt = "Nested multipart is not supported")]
Nested, Nested,
/// Multipart stream is incomplete /// Multipart stream is incomplete
#[display(fmt = "Multipart stream is incomplete")] #[display(fmt = "Multipart stream is incomplete")]
Incomplete, Incomplete,
/// Error during field parsing /// Error during field parsing
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Parse(ParseError), Parse(ParseError),
/// Payload error /// Payload error
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Payload(PayloadError), Payload(PayloadError),
/// Not consumed /// Not consumed
#[display(fmt = "Multipart stream is not consumed")] #[display(fmt = "Multipart stream is not consumed")]
NotConsumed, NotConsumed,
} }
impl std::error::Error for MultipartError {}
/// Return `BadRequest` for `MultipartError` /// Return `BadRequest` for `MultipartError`
impl ResponseError for MultipartError { impl ResponseError for MultipartError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {

View File

@ -1,15 +1,20 @@
//! Multipart response payload support. //! Multipart response payload support.
use std::cell::{Cell, RefCell, RefMut}; use std::{
use std::convert::TryFrom; cell::{Cell, RefCell, RefMut},
use std::marker::PhantomData; cmp,
use std::pin::Pin; convert::TryFrom,
use std::rc::Rc; fmt,
use std::task::{Context, Poll}; marker::PhantomData,
use std::{cmp, fmt}; pin::Pin,
rc::Rc,
task::{Context, Poll},
};
use actix_web::error::{ParseError, PayloadError}; use actix_web::{
use actix_web::http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}; error::{ParseError, PayloadError},
http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue},
};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::stream::{LocalBoxStream, Stream}; use futures_core::stream::{LocalBoxStream, Stream};
use futures_util::stream::StreamExt as _; use futures_util::stream::StreamExt as _;
@ -40,10 +45,13 @@ enum InnerMultipartItem {
enum InnerState { enum InnerState {
/// Stream eof /// Stream eof
Eof, Eof,
/// Skip data until first boundary /// Skip data until first boundary
FirstBoundary, FirstBoundary,
/// Reading boundary /// Reading boundary
Boundary, Boundary,
/// Reading Headers, /// Reading Headers,
Headers, Headers,
} }
@ -332,31 +340,55 @@ impl InnerMultipart {
return Poll::Pending; return Poll::Pending;
}; };
// content type // According to [RFC 7578](https://tools.ietf.org/html/rfc7578#section-4.2) a
let mut mt = mime::APPLICATION_OCTET_STREAM; // Content-Disposition header must always be present and set to "form-data".
if let Some(content_type) = headers.get(&header::CONTENT_TYPE) {
if let Ok(content_type) = content_type.to_str() { let content_disposition = headers
if let Ok(ct) = content_type.parse::<mime::Mime>() { .get(&header::CONTENT_DISPOSITION)
mt = ct; .and_then(|cd| ContentDisposition::from_raw(cd).ok())
} .filter(|content_disposition| {
} let is_form_data =
} content_disposition.disposition == header::DispositionType::FormData;
let has_field_name = content_disposition
.parameters
.iter()
.any(|param| matches!(param, header::DispositionParam::Name(_)));
is_form_data && has_field_name
});
let cd = if let Some(content_disposition) = content_disposition {
content_disposition
} else {
return Poll::Ready(Some(Err(MultipartError::NoContentDisposition)));
};
let ct: mime::Mime = headers
.get(&header::CONTENT_TYPE)
.and_then(|ct| ct.to_str().ok())
.and_then(|ct| ct.parse().ok())
.unwrap_or(mime::APPLICATION_OCTET_STREAM);
self.state = InnerState::Boundary; self.state = InnerState::Boundary;
// nested multipart stream // nested multipart stream is not supported
if mt.type_() == mime::MULTIPART { if ct.type_() == mime::MULTIPART {
Poll::Ready(Some(Err(MultipartError::Nested))) return Poll::Ready(Some(Err(MultipartError::Nested)));
} else { }
let field = Rc::new(RefCell::new(InnerField::new(
self.payload.clone(), let field =
self.boundary.clone(), InnerField::new_in_rc(self.payload.clone(), self.boundary.clone(), &headers)?;
&headers,
)?));
self.item = InnerMultipartItem::Field(Rc::clone(&field)); self.item = InnerMultipartItem::Field(Rc::clone(&field));
Poll::Ready(Some(Ok(Field::new(safety.clone(cx), headers, mt, field)))) Poll::Ready(Some(Ok(Field::new(
} safety.clone(cx),
headers,
ct,
cd,
field,
))))
} }
} }
} }
@ -371,6 +403,7 @@ impl Drop for InnerMultipart {
/// A single field in a multipart stream /// A single field in a multipart stream
pub struct Field { pub struct Field {
ct: mime::Mime, ct: mime::Mime,
cd: ContentDisposition,
headers: HeaderMap, headers: HeaderMap,
inner: Rc<RefCell<InnerField>>, inner: Rc<RefCell<InnerField>>,
safety: Safety, safety: Safety,
@ -381,35 +414,51 @@ impl Field {
safety: Safety, safety: Safety,
headers: HeaderMap, headers: HeaderMap,
ct: mime::Mime, ct: mime::Mime,
cd: ContentDisposition,
inner: Rc<RefCell<InnerField>>, inner: Rc<RefCell<InnerField>>,
) -> Self { ) -> Self {
Field { Field {
ct, ct,
cd,
headers, headers,
inner, inner,
safety, safety,
} }
} }
/// Get a map of headers /// Returns a reference to the field's header map.
pub fn headers(&self) -> &HeaderMap { pub fn headers(&self) -> &HeaderMap {
&self.headers &self.headers
} }
/// Get the content type of the field /// Returns a reference to the field's content (mime) type.
pub fn content_type(&self) -> &mime::Mime { pub fn content_type(&self) -> &mime::Mime {
&self.ct &self.ct
} }
/// Get the content disposition of the field, if it exists /// Returns the field's Content-Disposition.
pub fn content_disposition(&self) -> Option<ContentDisposition> { ///
// RFC 7578: 'Each part MUST contain a Content-Disposition header field /// Per [RFC 7578 §4.2]: 'Each part MUST contain a Content-Disposition header field where the
// where the disposition type is "form-data".' /// disposition type is "form-data". The Content-Disposition header field MUST also contain an
if let Some(content_disposition) = self.headers.get(&header::CONTENT_DISPOSITION) { /// additional parameter of "name"; the value of the "name" parameter is the original field name
ContentDisposition::from_raw(content_disposition).ok() /// from the form.'
} else { ///
None /// This crate validates that it exists before returning a `Field`. As such, it is safe to
/// unwrap `.content_disposition().get_name()`. The [name](Self::name) method is provided as
/// a convenience.
///
/// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2
pub fn content_disposition(&self) -> &ContentDisposition {
&self.cd
} }
/// Returns the field's name.
///
/// See [content_disposition] regarding guarantees about
pub fn name(&self) -> &str {
self.content_disposition()
.get_name()
.expect("field name should be guaranteed to exist in multipart form-data")
} }
} }
@ -451,20 +500,23 @@ struct InnerField {
} }
impl InnerField { impl InnerField {
fn new_in_rc(
payload: PayloadRef,
boundary: String,
headers: &HeaderMap,
) -> Result<Rc<RefCell<InnerField>>, PayloadError> {
Self::new(payload, boundary, headers).map(|this| Rc::new(RefCell::new(this)))
}
fn new( fn new(
payload: PayloadRef, payload: PayloadRef,
boundary: String, boundary: String,
headers: &HeaderMap, headers: &HeaderMap,
) -> Result<InnerField, PayloadError> { ) -> Result<InnerField, PayloadError> {
let len = if let Some(len) = headers.get(&header::CONTENT_LENGTH) { let len = if let Some(len) = headers.get(&header::CONTENT_LENGTH) {
if let Ok(s) = len.to_str() { match len.to_str().ok().and_then(|len| len.parse::<u64>().ok()) {
if let Ok(len) = s.parse::<u64>() { Some(len) => Some(len),
Some(len) None => return Err(PayloadError::Incomplete(None)),
} else {
return Err(PayloadError::Incomplete(None));
}
} else {
return Err(PayloadError::Incomplete(None));
} }
} else { } else {
None None
@ -658,9 +710,8 @@ impl Clone for PayloadRef {
} }
} }
/// Counter. It tracks of number of clones of payloads and give access to /// Counter. It tracks of number of clones of payloads and give access to payload only to top most
/// payload only to top most task panics if Safety get destroyed and it not top /// task panics if Safety get destroyed and it not top most task.
/// most task.
#[derive(Debug)] #[derive(Debug)]
struct Safety { struct Safety {
task: LocalWaker, task: LocalWaker,
@ -707,11 +758,12 @@ impl Drop for Safety {
if Rc::strong_count(&self.payload) != self.level { if Rc::strong_count(&self.payload) != self.level {
self.clean.set(true); self.clean.set(true);
} }
self.task.wake(); self.task.wake();
} }
} }
/// Payload buffer /// Payload buffer.
struct PayloadBuffer { struct PayloadBuffer {
eof: bool, eof: bool,
buf: BytesMut, buf: BytesMut,
@ -719,7 +771,7 @@ struct PayloadBuffer {
} }
impl PayloadBuffer { impl PayloadBuffer {
/// Create new `PayloadBuffer` instance /// Constructs new `PayloadBuffer` instance.
fn new<S>(stream: S) -> Self fn new<S>(stream: S) -> Self
where where
S: Stream<Item = Result<Bytes, PayloadError>> + 'static, S: Stream<Item = Result<Bytes, PayloadError>> + 'static,
@ -767,7 +819,7 @@ impl PayloadBuffer {
} }
/// Read until specified ending /// Read until specified ending
pub fn read_until(&mut self, line: &[u8]) -> Result<Option<Bytes>, MultipartError> { fn read_until(&mut self, line: &[u8]) -> Result<Option<Bytes>, MultipartError> {
let res = twoway::find_bytes(&self.buf, line) let res = twoway::find_bytes(&self.buf, line)
.map(|idx| self.buf.split_to(idx + line.len()).freeze()); .map(|idx| self.buf.split_to(idx + line.len()).freeze());
@ -779,12 +831,12 @@ impl PayloadBuffer {
} }
/// Read bytes until new line delimiter /// Read bytes until new line delimiter
pub fn readline(&mut self) -> Result<Option<Bytes>, MultipartError> { fn readline(&mut self) -> Result<Option<Bytes>, MultipartError> {
self.read_until(b"\n") self.read_until(b"\n")
} }
/// Read bytes until new line delimiter or eof /// Read bytes until new line delimiter or eof
pub fn readline_or_eof(&mut self) -> Result<Option<Bytes>, MultipartError> { fn readline_or_eof(&mut self) -> Result<Option<Bytes>, MultipartError> {
match self.readline() { match self.readline() {
Err(MultipartError::Incomplete) if self.eof => Ok(Some(self.buf.split().freeze())), Err(MultipartError::Incomplete) if self.eof => Ok(Some(self.buf.split().freeze())),
line => line, line => line,
@ -792,7 +844,7 @@ impl PayloadBuffer {
} }
/// Put unprocessed data back to the buffer /// Put unprocessed data back to the buffer
pub fn unprocessed(&mut self, data: Bytes) { fn unprocessed(&mut self, data: Bytes) {
let buf = BytesMut::from(data.as_ref()); let buf = BytesMut::from(data.as_ref());
let buf = std::mem::replace(&mut self.buf, buf); let buf = std::mem::replace(&mut self.buf, buf);
self.buf.extend_from_slice(&buf); self.buf.extend_from_slice(&buf);
@ -914,6 +966,7 @@ mod tests {
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
test\r\n\ test\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
data\r\n\ data\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0--\r\n", --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n",
@ -965,7 +1018,7 @@ mod tests {
let mut multipart = Multipart::new(&headers, payload); let mut multipart = Multipart::new(&headers, payload);
match multipart.next().await { match multipart.next().await {
Some(Ok(mut field)) => { Some(Ok(mut field)) => {
let cd = field.content_disposition().unwrap(); let cd = field.content_disposition();
assert_eq!(cd.disposition, DispositionType::FormData); assert_eq!(cd.disposition, DispositionType::FormData);
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
@ -1027,7 +1080,7 @@ mod tests {
let mut multipart = Multipart::new(&headers, payload); let mut multipart = Multipart::new(&headers, payload);
match multipart.next().await.unwrap() { match multipart.next().await.unwrap() {
Ok(mut field) => { Ok(mut field) => {
let cd = field.content_disposition().unwrap(); let cd = field.content_disposition();
assert_eq!(cd.disposition, DispositionType::FormData); assert_eq!(cd.disposition, DispositionType::FormData);
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
@ -1182,4 +1235,59 @@ mod tests {
_ => unreachable!(), _ => unreachable!(),
} }
} }
#[actix_rt::test]
async fn no_content_disposition() {
let bytes = Bytes::from(
"testasdadsad\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
test\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n",
);
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(
"multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
),
);
let payload = SlowStream::new(bytes);
let mut multipart = Multipart::new(&headers, payload);
let res = multipart.next().await.unwrap();
assert!(res.is_err());
assert!(matches!(
res.unwrap_err(),
MultipartError::NoContentDisposition,
));
}
#[actix_rt::test]
async fn no_name_in_content_disposition() {
let bytes = Bytes::from(
"testasdadsad\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Disposition: form-data; filename=\"fn.txt\"\r\n\
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
test\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n",
);
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(
"multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
),
);
let payload = SlowStream::new(bytes);
let mut multipart = Multipart::new(&headers, payload);
let res = multipart.next().await.unwrap();
assert!(res.is_err());
assert!(matches!(
res.unwrap_err(),
MultipartError::NoContentDisposition,
));
}
} }

View File

@ -30,7 +30,7 @@ serde = "1"
[dev-dependencies] [dev-dependencies]
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }
firestorm = { version = "0.4", features = ["enable_system_time"] } firestorm = { version = "0.4", features = ["enable_system_time"] }
http = "0.2.3" http = "0.2.5"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
[[bench]] [[bench]]

View File

@ -3,6 +3,16 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.1.0-beta.7 - 2021-11-22
* Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
[#2408]: https://github.com/actix/actix-web/pull/2408
## 0.1.0-beta.6 - 2021-11-15
* No significant changes from `0.1.0-beta.5`.
## 0.1.0-beta.5 - 2021-10-20 ## 0.1.0-beta.5 - 2021-10-20
* Updated rustls to v0.20. [#2414] * Updated rustls to v0.20. [#2414]
* Minimum supported Rust version (MSRV) is now 1.52. * Minimum supported Rust version (MSRV) is now 1.52.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-test" name = "actix-test"
version = "0.1.0-beta.5" version = "0.1.0-beta.7"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -22,20 +22,20 @@ edition = "2018"
default = [] default = []
# rustls # rustls
rustls = ["tls-rustls", "actix-http/rustls"] rustls = ["tls-rustls", "actix-http/rustls", "awc/rustls"]
# openssl # openssl
openssl = ["tls-openssl", "actix-http/openssl"] openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-codec = "0.4.0" actix-codec = "0.4.1"
actix-http = "3.0.0-beta.11" actix-http = "3.0.0-beta.13"
actix-http-test = "3.0.0-beta.5" actix-http-test = "3.0.0-beta.7"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = { version = "4.0.0-beta.10", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] }
actix-rt = "2.1" actix-rt = "2.1"
awc = { version = "3.0.0-beta.9", default-features = false, features = ["cookies"] } awc = { version = "3.0.0-beta.11", default-features = false, features = ["cookies"] }
futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
futures-util = { version = "0.3.7", default-features = false, features = [] } futures-util = { version = "0.3.7", default-features = false, features = [] }
@ -45,3 +45,4 @@ serde_json = "1"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
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.20.0", optional = true } tls-rustls = { package = "rustls", version = "0.20.0", optional = true }
tokio = { version = "1.2", features = ["sync"] }

View File

@ -31,7 +31,7 @@ extern crate tls_openssl as openssl;
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
extern crate tls_rustls as rustls; extern crate tls_rustls as rustls;
use std::{error::Error as StdError, fmt, net, sync::mpsc, thread, time}; use std::{error::Error as StdError, fmt, net, thread, time::Duration};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
pub use actix_http::test::TestBuffer; pub use actix_http::test::TestBuffer;
@ -41,8 +41,9 @@ use actix_http::{
}; };
use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _}; use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _};
use actix_web::{ use actix_web::{
dev::{AppConfig, MessageBody, Server, Service}, dev::{AppConfig, MessageBody, Server, ServerHandle, Service},
rt, web, Error, rt::{self, System},
web, Error,
}; };
use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector}; use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector};
use futures_core::Stream; use futures_core::Stream;
@ -52,6 +53,7 @@ pub use actix_web::test::{
call_service, default_service, init_service, load_stream, ok_service, read_body, call_service, default_service, init_service, load_stream, ok_service, read_body,
read_body_json, read_response, read_response_json, TestRequest, read_body_json, read_response, read_response_json, TestRequest,
}; };
use tokio::sync::mpsc;
/// Start default [`TestServer`]. /// Start default [`TestServer`].
/// ///
@ -128,7 +130,11 @@ where
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>, B::Error: Into<Box<dyn StdError>>,
{ {
let (tx, rx) = mpsc::channel(); // for sending handles and server info back from the spawned thread
let (started_tx, started_rx) = std::sync::mpsc::channel();
// for signaling the shutdown of spawned server and system
let (thread_stop_tx, thread_stop_rx) = mpsc::channel(1);
let tls = match cfg.stream { let tls = match cfg.stream {
StreamType::Tcp => false, StreamType::Tcp => false,
@ -138,21 +144,25 @@ where
StreamType::Rustls(_) => true, StreamType::Rustls(_) => true,
}; };
// run server in separate thread // run server in separate orphaned thread
thread::spawn(move || { thread::spawn(move || {
let sys = rt::System::new(); rt::System::new().block_on(async move {
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
let local_addr = tcp.local_addr().unwrap(); let local_addr = tcp.local_addr().unwrap();
let factory = factory.clone(); let factory = factory.clone();
let srv_cfg = cfg.clone(); let srv_cfg = cfg.clone();
let timeout = cfg.client_timeout; let timeout = cfg.client_timeout;
let builder = Server::build().workers(1).disable_signals();
let builder = Server::build().workers(1).disable_signals().system_exit();
let srv = match srv_cfg.stream { let srv = match srv_cfg.stream {
StreamType::Tcp => match srv_cfg.tp { StreamType::Tcp => match srv_cfg.tp {
HttpVer::Http1 => builder.listen("test", tcp, move || { HttpVer::Http1 => builder.listen("test", tcp, move || {
let app_cfg = let app_cfg = AppConfig::__priv_test_new(
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); false,
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -164,8 +174,11 @@ where
.tcp() .tcp()
}), }),
HttpVer::Http2 => builder.listen("test", tcp, move || { HttpVer::Http2 => builder.listen("test", tcp, move || {
let app_cfg = let app_cfg = AppConfig::__priv_test_new(
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); false,
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -177,8 +190,11 @@ where
.tcp() .tcp()
}), }),
HttpVer::Both => builder.listen("test", tcp, move || { HttpVer::Both => builder.listen("test", tcp, move || {
let app_cfg = let app_cfg = AppConfig::__priv_test_new(
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); false,
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -193,8 +209,11 @@ where
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
StreamType::Openssl(acceptor) => match cfg.tp { StreamType::Openssl(acceptor) => match cfg.tp {
HttpVer::Http1 => builder.listen("test", tcp, move || { HttpVer::Http1 => builder.listen("test", tcp, move || {
let app_cfg = let app_cfg = AppConfig::__priv_test_new(
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); false,
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -206,8 +225,11 @@ where
.openssl(acceptor.clone()) .openssl(acceptor.clone())
}), }),
HttpVer::Http2 => builder.listen("test", tcp, move || { HttpVer::Http2 => builder.listen("test", tcp, move || {
let app_cfg = let app_cfg = AppConfig::__priv_test_new(
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); false,
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -219,8 +241,11 @@ where
.openssl(acceptor.clone()) .openssl(acceptor.clone())
}), }),
HttpVer::Both => builder.listen("test", tcp, move || { HttpVer::Both => builder.listen("test", tcp, move || {
let app_cfg = let app_cfg = AppConfig::__priv_test_new(
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); false,
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -235,8 +260,11 @@ where
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
StreamType::Rustls(config) => match cfg.tp { StreamType::Rustls(config) => match cfg.tp {
HttpVer::Http1 => builder.listen("test", tcp, move || { HttpVer::Http1 => builder.listen("test", tcp, move || {
let app_cfg = let app_cfg = AppConfig::__priv_test_new(
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); false,
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -248,8 +276,11 @@ where
.rustls(config.clone()) .rustls(config.clone())
}), }),
HttpVer::Http2 => builder.listen("test", tcp, move || { HttpVer::Http2 => builder.listen("test", tcp, move || {
let app_cfg = let app_cfg = AppConfig::__priv_test_new(
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); false,
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -261,8 +292,11 @@ where
.rustls(config.clone()) .rustls(config.clone())
}), }),
HttpVer::Both => builder.listen("test", tcp, move || { HttpVer::Both => builder.listen("test", tcp, move || {
let app_cfg = let app_cfg = AppConfig::__priv_test_new(
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); false,
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -275,17 +309,24 @@ where
}), }),
}, },
} }
.expect("test server could not be created");
let srv = srv.run();
started_tx
.send((System::current(), srv.handle(), local_addr))
.unwrap(); .unwrap();
sys.block_on(async { // drive server loop
let srv = srv.run(); srv.await.unwrap();
tx.send((rt::System::current(), srv, local_addr)).unwrap();
// notify TestServer that server and system have shut down
// all thread managed resources should be dropped at this point
}); });
sys.run() let _ = thread_stop_tx.send(());
}); });
let (system, server, addr) = rx.recv().unwrap(); let (system, server, addr) = started_rx.recv().unwrap();
let client = { let client = {
let connector = { let connector = {
@ -299,15 +340,15 @@ where
.set_alpn_protos(b"\x02h2\x08http/1.1") .set_alpn_protos(b"\x02h2\x08http/1.1")
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
Connector::new() Connector::new()
.conn_lifetime(time::Duration::from_secs(0)) .conn_lifetime(Duration::from_secs(0))
.timeout(time::Duration::from_millis(30000)) .timeout(Duration::from_millis(30000))
.ssl(builder.build()) .ssl(builder.build())
} }
#[cfg(not(feature = "openssl"))] #[cfg(not(feature = "openssl"))]
{ {
Connector::new() Connector::new()
.conn_lifetime(time::Duration::from_secs(0)) .conn_lifetime(Duration::from_secs(0))
.timeout(time::Duration::from_millis(30000)) .timeout(Duration::from_millis(30000))
} }
}; };
@ -315,11 +356,12 @@ where
}; };
TestServer { TestServer {
addr, server,
thread_stop_rx,
client, client,
system, system,
addr,
tls, tls,
server,
} }
} }
@ -405,11 +447,12 @@ impl TestServerConfig {
/// ///
/// See [`start`] for usage example. /// See [`start`] for usage example.
pub struct TestServer { pub struct TestServer {
addr: net::SocketAddr, server: ServerHandle,
thread_stop_rx: mpsc::Receiver<()>,
client: awc::Client, client: awc::Client,
system: rt::System, system: rt::System,
addr: net::SocketAddr,
tls: bool, tls: bool,
server: Server,
} }
impl TestServer { impl TestServer {
@ -504,16 +547,31 @@ impl TestServer {
self.client.headers() self.client.headers()
} }
/// Gracefully stop HTTP server. /// Stop HTTP server.
pub async fn stop(self) { ///
self.server.stop(true).await; /// Waits for spawned `Server` and `System` to shutdown (force) shutdown.
pub async fn stop(mut self) {
// signal server to stop
self.server.stop(false).await;
// also signal system to stop
// though this is handled by `ServerBuilder::exit_system` too
self.system.stop(); self.system.stop();
rt::time::sleep(time::Duration::from_millis(100)).await;
// wait for thread to be stopped but don't care about result
let _ = self.thread_stop_rx.recv().await;
} }
} }
impl Drop for TestServer { impl Drop for TestServer {
fn drop(&mut self) { fn drop(&mut self) {
self.system.stop() // calls in this Drop impl should be enough to shut down the server, system, and thread
// without needing to await anything
// signal server to stop
let _ = self.server.stop(true);
// signal system to stop
self.system.stop();
} }
} }

View File

@ -15,9 +15,9 @@ path = "src/lib.rs"
[dependencies] [dependencies]
actix = { version = "0.12.0", default-features = false } actix = { version = "0.12.0", default-features = false }
actix-codec = "0.4.0" actix-codec = "0.4.1"
actix-http = "3.0.0-beta.11" actix-http = "3.0.0-beta.13"
actix-web = { version = "4.0.0-beta.10", default-features = false } actix-web = { version = "4.0.0-beta.11", default-features = false }
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"
@ -27,8 +27,8 @@ tokio = { version = "1", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.5" actix-test = "0.1.0-beta.7"
awc = { version = "3.0.0-beta.9", default-features = false } awc = { version = "3.0.0-beta.11", default-features = false }
env_logger = "0.8" env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }

View File

@ -23,9 +23,9 @@ actix-router = "0.5.0-beta.2"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-macros = "0.2.3" actix-macros = "0.2.3"
actix-test = "0.1.0-beta.5" actix-test = "0.1.0-beta.7"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = "4.0.0-beta.10" actix-web = "4.0.0-beta.11"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
trybuild = "1" trybuild = "1"

View File

@ -3,6 +3,13 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.11 - 2021-11-22
## 3.0.0-beta.10 - 2021-11-15
* No significant changes from `3.0.0-beta.9`.
## 3.0.0-beta.9 - 2021-10-20 ## 3.0.0-beta.9 - 2021-10-20
* Updated rustls to v0.20. [#2414] * Updated rustls to v0.20. [#2414]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "awc" name = "awc"
version = "3.0.0-beta.9" version = "3.0.0-beta.11"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>", "fakeshadow <24548779@qq.com>",
@ -30,10 +30,10 @@ features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-z
default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
# openssl # openssl
openssl = ["tls-openssl", "actix-http/openssl"] openssl = ["tls-openssl", "actix-tls/openssl"]
# rustls # rustls
rustls = ["tls-rustls", "actix-http/rustls"] rustls = ["tls-rustls", "actix-tls/rustls"]
# Brotli algorithm content-encoding support # Brotli algorithm content-encoding support
compress-brotli = ["actix-http/compress-brotli", "__compress"] compress-brotli = ["actix-http/compress-brotli", "__compress"]
@ -46,24 +46,34 @@ compress-zstd = ["actix-http/compress-zstd", "__compress"]
cookies = ["cookie"] cookies = ["cookie"]
# trust-dns as dns resolver # trust-dns as dns resolver
trust-dns = ["actix-http/trust-dns"] trust-dns = ["trust-dns-resolver"]
# Internal (PRIVATE!) features used to aid testing and cheking feature status. # Internal (PRIVATE!) features used to aid testing and cheking feature status.
# Don't rely on these whatsoever. They may disappear at anytime. # Don't rely on these whatsoever. They may disappear at anytime.
__compress = [] __compress = []
[dependencies] # Enable dangerous feature for testing and local network usage:
actix-codec = "0.4.0" # - HTTP/2 over TCP(No Tls).
actix-service = "2.0.0" # DO NOT enable this over any internet use case.
actix-http = "3.0.0-beta.11" dangerous-h2c = []
actix-rt = { version = "2.1", default-features = false }
[dependencies]
actix-codec = "0.4.1"
actix-service = "2.0.0"
actix-http = "3.0.0-beta.13"
actix-rt = { version = "2.1", default-features = false }
actix-tls = { version = "3.0.0-beta.9", features = ["connect"] }
actix-utils = "3.0.0"
ahash = "0.7"
base64 = "0.13" base64 = "0.13"
bytes = "1" bytes = "1"
cfg-if = "1" cfg-if = "1"
cookie = { version = "0.15", features = ["percent-encode"], optional = true }
derive_more = "0.99.5" derive_more = "0.99.5"
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 }
h2 = "0.3"
http = "0.2.5"
itoa = "0.4" itoa = "0.4"
log =" 0.4" log =" 0.4"
mime = "0.3" mime = "0.3"
@ -73,20 +83,26 @@ rand = "0.8"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
tokio = { version = "1", features = ["sync"] }
cookie = { version = "0.15", features = ["percent-encode"], optional = true }
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.20.0", optional = true, features = ["dangerous_configuration"] } tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] }
trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0-beta.10", features = ["openssl"] } actix-web = { version = "4.0.0-beta.11", features = ["openssl"] }
actix-http = { version = "3.0.0-beta.11", features = ["openssl"] } actix-http = { version = "3.0.0-beta.13", features = ["openssl"] }
actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] }
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-beta.9"
actix-tls = { version = "3.0.0-beta.7", features = ["openssl", "rustls"] } actix-tls = { version = "3.0.0-beta.9", features = ["openssl", "rustls"] }
actix-test = { version = "0.1.0-beta.5", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.7", features = ["openssl", "rustls"] }
brotli2 = "0.3.2" brotli2 = "0.3.2"
env_logger = "0.8" env_logger = "0.9"
flate2 = "1.0.13" flate2 = "1.0.13"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }
rcgen = "0.8" rcgen = "0.8"

View File

@ -3,9 +3,9 @@
> Async HTTP and WebSocket client library. > Async HTTP and WebSocket client library.
[![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc)
[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.9)](https://docs.rs/awc/3.0.0-beta.9) [![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.11)](https://docs.rs/awc/3.0.0-beta.11)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc)
[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.9/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.9) [![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.11/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.11)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources ## Documentation & Resources

View File

@ -4,13 +4,11 @@ use std::net::IpAddr;
use std::rc::Rc; use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
use actix_http::{ use actix_http::http::{self, header, Error as HttpError, HeaderMap, HeaderName, Uri};
client::{Connector, ConnectorService, TcpConnect, TcpConnectError, TcpConnection},
http::{self, header, Error as HttpError, HeaderMap, HeaderName, Uri},
};
use actix_rt::net::{ActixStream, TcpStream}; use actix_rt::net::{ActixStream, TcpStream};
use actix_service::{boxed, Service}; use actix_service::{boxed, Service};
use crate::client::{Connector, ConnectorService, TcpConnect, TcpConnectError, TcpConnection};
use crate::connect::DefaultConnector; use crate::connect::DefaultConnector;
use crate::error::SendRequestError; use crate::error::SendRequestError;
use crate::middleware::{NestTransform, Redirect, Transform}; use crate::middleware::{NestTransform, Redirect, Transform};

View File

@ -1,5 +1,4 @@
use std::net::IpAddr; use std::{net::IpAddr, time::Duration};
use std::time::Duration;
const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2MB const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2MB
const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1MB const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1MB

View File

@ -12,10 +12,9 @@ use bytes::Bytes;
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use h2::client::SendRequest; use h2::client::SendRequest;
use crate::h1::ClientCodec; use actix_http::{
use crate::message::{RequestHeadType, ResponseHead}; body::MessageBody, h1::ClientCodec, Error, Payload, RequestHeadType, ResponseHead,
use crate::payload::Payload; };
use crate::{body::MessageBody, Error};
use super::error::SendRequestError; use super::error::SendRequestError;
use super::pool::Acquired; use super::pool::Acquired;
@ -174,6 +173,7 @@ impl H2ConnectionInner {
/// Cancel spawned connection task on drop. /// Cancel spawned connection task on drop.
impl Drop for H2ConnectionInner { impl Drop for H2ConnectionInner {
fn drop(&mut self) { fn drop(&mut self) {
// TODO: this can end up sending extraneous requests; see if there is a better way to handle
if self if self
.sender .sender
.send_request(http::Request::new(()), true) .send_request(http::Request::new(()), true)
@ -184,8 +184,8 @@ impl Drop for H2ConnectionInner {
} }
} }
/// Unified connection type cover HTTP/1 Plain/TLS and HTTP/2 protocols.
#[allow(dead_code)] #[allow(dead_code)]
/// Unified connection type cover Http1 Plain/Tls and Http2 protocols
pub enum Connection<A, B = Box<dyn ConnectionIo>> pub enum Connection<A, B = Box<dyn ConnectionIo>>
where where
A: ConnectionIo, A: ConnectionIo,
@ -219,11 +219,7 @@ impl<Io: ConnectionIo> ConnectionType<Io> {
} }
} }
pub(super) fn from_h1( pub(super) fn from_h1(io: Io, created: time::Instant, acquired: Acquired<Io>) -> Self {
io: Io,
created: time::Instant,
acquired: Acquired<Io>,
) -> Self {
Self::H1(H1Connection { Self::H1(H1Connection {
io: Some(io), io: Some(io),
created, created,
@ -271,9 +267,7 @@ where
Connection::Tls(ConnectionType::H2(conn)) => { Connection::Tls(ConnectionType::H2(conn)) => {
h2proto::send_request(conn, head.into(), body).await h2proto::send_request(conn, head.into(), body).await
} }
_ => unreachable!( _ => unreachable!("Plain Tcp connection can be used only in Http1 protocol"),
"Plain Tcp connection can be used only in Http1 protocol"
),
} }
}) })
} }
@ -301,9 +295,7 @@ where
Err(SendRequestError::TunnelNotSupported) Err(SendRequestError::TunnelNotSupported)
} }
Connection::Tcp(ConnectionType::H2(_)) => { Connection::Tcp(ConnectionType::H2(_)) => {
unreachable!( unreachable!("Plain Tcp connection can be used only in Http1 protocol")
"Plain Tcp connection can be used only in Http1 protocol"
)
} }
} }
}) })
@ -321,12 +313,8 @@ where
buf: &mut ReadBuf<'_>, buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> { ) -> Poll<io::Result<()>> {
match self.get_mut() { match self.get_mut() {
Connection::Tcp(ConnectionType::H1(conn)) => { Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_read(cx, buf),
Pin::new(conn).poll_read(cx, buf) Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_read(cx, buf),
}
Connection::Tls(ConnectionType::H1(conn)) => {
Pin::new(conn).poll_read(cx, buf)
}
_ => unreachable!("H2Connection can not impl AsyncRead trait"), _ => unreachable!("H2Connection can not impl AsyncRead trait"),
} }
} }
@ -345,12 +333,8 @@ where
buf: &[u8], buf: &[u8],
) -> Poll<io::Result<usize>> { ) -> Poll<io::Result<usize>> {
match self.get_mut() { match self.get_mut() {
Connection::Tcp(ConnectionType::H1(conn)) => { Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_write(cx, buf),
Pin::new(conn).poll_write(cx, buf) Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_write(cx, buf),
}
Connection::Tls(ConnectionType::H1(conn)) => {
Pin::new(conn).poll_write(cx, buf)
}
_ => unreachable!(H2_UNREACHABLE_WRITE), _ => unreachable!(H2_UNREACHABLE_WRITE),
} }
} }
@ -363,17 +347,10 @@ where
} }
} }
fn poll_shutdown( fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<io::Result<()>> {
match self.get_mut() { match self.get_mut() {
Connection::Tcp(ConnectionType::H1(conn)) => { Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_shutdown(cx),
Pin::new(conn).poll_shutdown(cx) Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_shutdown(cx),
}
Connection::Tls(ConnectionType::H1(conn)) => {
Pin::new(conn).poll_shutdown(cx)
}
_ => unreachable!(H2_UNREACHABLE_WRITE), _ => unreachable!(H2_UNREACHABLE_WRITE),
} }
} }

View File

@ -8,6 +8,7 @@ use std::{
time::Duration, time::Duration,
}; };
use actix_http::Protocol;
use actix_rt::{ use actix_rt::{
net::{ActixStream, TcpStream}, net::{ActixStream, TcpStream},
time::{sleep, Sleep}, time::{sleep, Sleep},
@ -19,14 +20,13 @@ use actix_tls::connect::{
}; };
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
use http::Uri; use http::Uri;
use pin_project::pin_project; use pin_project_lite::pin_project;
use super::config::ConnectorConfig; use super::config::ConnectorConfig;
use super::connection::{Connection, ConnectionIo}; use super::connection::{Connection, ConnectionIo};
use super::error::ConnectError; use super::error::ConnectError;
use super::pool::ConnectionPool; use super::pool::ConnectionPool;
use super::Connect; use super::Connect;
use super::Protocol;
enum SslConnector { enum SslConnector {
#[allow(dead_code)] #[allow(dead_code)]
@ -67,9 +67,9 @@ impl Connector<()> {
> + Clone, > + Clone,
> { > {
Connector { Connector {
ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]),
connector: new_connector(resolver::resolver()), connector: new_connector(resolver::resolver()),
config: ConnectorConfig::default(), config: ConnectorConfig::default(),
ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]),
} }
} }
@ -99,9 +99,7 @@ impl Connector<()> {
/// Build TLS connector with openssl, based on supplied ALPN protocols /// Build TLS connector with openssl, based on supplied ALPN protocols
#[cfg(all(feature = "openssl", not(feature = "rustls")))] #[cfg(all(feature = "openssl", not(feature = "rustls")))]
fn build_ssl(protocols: Vec<Vec<u8>>) -> SslConnector { fn build_ssl(protocols: Vec<Vec<u8>>) -> SslConnector {
use actix_tls::connect::tls::openssl::{ use actix_tls::connect::tls::openssl::{SslConnector as OpensslConnector, SslMethod};
SslConnector as OpensslConnector, SslMethod,
};
use bytes::{BufMut, BytesMut}; use bytes::{BufMut, BytesMut};
let mut alpn = BytesMut::with_capacity(20); let mut alpn = BytesMut::with_capacity(20);
@ -112,7 +110,7 @@ impl Connector<()> {
let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap(); let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap();
if let Err(err) = ssl.set_alpn_protos(&alpn) { if let Err(err) = ssl.set_alpn_protos(&alpn) {
error!("Can not set ALPN protocol: {:?}", err); log::error!("Can not set ALPN protocol: {:?}", err);
} }
SslConnector::Openssl(ssl.build()) SslConnector::Openssl(ssl.build())
@ -148,11 +146,8 @@ where
// This remap is to hide ActixStream's trait methods. They are not meant to be called // This remap is to hide ActixStream's trait methods. They are not meant to be called
// from user code. // from user code.
Io: ActixStream + fmt::Debug + 'static, Io: ActixStream + fmt::Debug + 'static,
S: Service< S: Service<TcpConnect<Uri>, Response = TcpConnection<Uri, Io>, Error = TcpConnectError>
TcpConnect<Uri>, + Clone
Response = TcpConnection<Uri, Io>,
Error = TcpConnectError,
> + Clone
+ 'static, + 'static,
{ {
/// Tcp connection timeout, i.e. max time to connect to remote host including dns name /// Tcp connection timeout, i.e. max time to connect to remote host including dns name
@ -171,10 +166,7 @@ where
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
/// Use custom `SslConnector` instance. /// Use custom `SslConnector` instance.
pub fn ssl( pub fn ssl(mut self, connector: actix_tls::connect::ssl::openssl::SslConnector) -> Self {
mut self,
connector: actix_tls::connect::ssl::openssl::SslConnector,
) -> Self {
self.ssl = SslConnector::Openssl(connector); self.ssl = SslConnector::Openssl(connector);
self self
} }
@ -197,7 +189,7 @@ where
http::Version::HTTP_11 => vec![b"http/1.1".to_vec()], http::Version::HTTP_11 => vec![b"http/1.1".to_vec()],
http::Version::HTTP_2 => vec![b"h2".to_vec(), b"http/1.1".to_vec()], http::Version::HTTP_2 => vec![b"h2".to_vec(), b"http/1.1".to_vec()],
_ => { _ => {
unimplemented!("actix-http:client: supported versions http/1.1, http/2") unimplemented!("actix-http client only supports versions http/1.1 & http/2")
} }
}; };
self.ssl = Connector::build_ssl(versions); self.ssl = Connector::build_ssl(versions);
@ -287,7 +279,63 @@ where
}; };
let tls_service = match self.ssl { let tls_service = match self.ssl {
SslConnector::None => None, SslConnector::None => {
#[cfg(not(feature = "dangerous-h2c"))]
{
None
}
#[cfg(feature = "dangerous-h2c")]
{
use std::{
future::{ready, Ready},
io,
};
use actix_tls::connect::Connection;
impl IntoConnectionIo for TcpConnection<Uri, Box<dyn ConnectionIo>> {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
let io = self.into_parts().0;
(io, Protocol::Http2)
}
}
/// With the `dangerous-h2c` feature enabled, this connector uses a no-op TLS
/// connection service that passes through plain TCP as a TLS connection.
///
/// The protocol version of this fake TLS connection is set to be HTTP/2.
#[derive(Clone)]
struct NoOpTlsConnectorService;
impl<T, U> Service<Connection<T, U>> for NoOpTlsConnectorService
where
U: ActixStream + 'static,
{
type Response = Connection<T, Box<dyn ConnectionIo>>;
type Error = io::Error;
type Future = Ready<Result<Self::Response, Self::Error>>;
actix_service::always_ready!();
fn call(&self, connection: Connection<T, U>) -> Self::Future {
let (io, connection) = connection.replace_io(());
let (_, connection) = connection.replace_io(Box::new(io) as _);
ready(Ok(connection))
}
}
let handshake_timeout = self.config.handshake_timeout;
let tls_service = TlsConnectorService {
tcp_service: tcp_service_inner,
tls_service: NoOpTlsConnectorService,
timeout: handshake_timeout,
};
Some(actix_service::boxed::rc_service(tls_service))
}
}
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
SslConnector::Openssl(tls) => { SslConnector::Openssl(tls) => {
const H2: &[u8] = b"h2"; const H2: &[u8] = b"h2";
@ -328,10 +376,11 @@ where
impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, TlsStream<Io>> { impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, TlsStream<Io>> {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) { fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
let sock = self.into_parts().0; let sock = self.into_parts().0;
let h2 = let h2 = sock
sock.get_ref().1.alpn_protocol().map_or(false, |protos| { .get_ref()
protos.windows(2).any(|w| w == H2) .1
}); .alpn_protocol()
.map_or(false, |protos| protos.windows(2).any(|w| w == H2));
if h2 { if h2 {
(Box::new(sock), Protocol::Http2) (Box::new(sock), Protocol::Http2)
} else { } else {
@ -357,8 +406,8 @@ where
let tcp_pool = ConnectionPool::new(tcp_service, tcp_config); let tcp_pool = ConnectionPool::new(tcp_service, tcp_config);
let tls_config = self.config; let tls_config = self.config;
let tls_pool = tls_service let tls_pool =
.map(move |tls_service| ConnectionPool::new(tls_service, tls_config)); tls_service.map(move |tls_service| ConnectionPool::new(tls_service, tls_config));
ConnectorServicePriv { tcp_pool, tls_pool } ConnectorServicePriv { tcp_pool, tls_pool }
} }
@ -389,10 +438,12 @@ where
} }
} }
#[pin_project] pin_project! {
pub struct TcpConnectorFuture<Fut> { #[project = TcpConnectorFutureProj]
pub struct TcpConnectorFuture<Fut> {
#[pin] #[pin]
fut: Fut, fut: Fut,
}
} }
impl<Fut, Io> Future for TcpConnectorFuture<Fut> impl<Fut, Io> Future for TcpConnectorFuture<Fut>
@ -451,9 +502,10 @@ where
} }
} }
#[pin_project(project = TlsConnectorProj)] pin_project! {
#[allow(clippy::large_enum_variant)] #[project = TlsConnectorProj]
enum TlsConnectorFuture<S, Fut1, Fut2> { #[allow(clippy::large_enum_variant)]
enum TlsConnectorFuture<S, Fut1, Fut2> {
TcpConnect { TcpConnect {
#[pin] #[pin]
fut: Fut1, fut: Fut1,
@ -466,8 +518,9 @@ enum TlsConnectorFuture<S, Fut1, Fut2> {
#[pin] #[pin]
timeout: Sleep, timeout: Sleep,
}, },
} }
}
/// helper trait for generic over different TlsStream types between tls crates. /// helper trait for generic over different TlsStream types between tls crates.
trait IntoConnectionIo { trait IntoConnectionIo {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol); fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol);
@ -475,12 +528,7 @@ trait IntoConnectionIo {
impl<S, Io, Fut1, Fut2, Res> Future for TlsConnectorFuture<S, Fut1, Fut2> impl<S, Io, Fut1, Fut2, Res> Future for TlsConnectorFuture<S, Fut1, Fut2>
where where
S: Service< S: Service<TcpConnection<Uri, Io>, Response = Res, Error = std::io::Error, Future = Fut2>,
TcpConnection<Uri, Io>,
Response = Res,
Error = std::io::Error,
Future = Fut2,
>,
S::Response: IntoConnectionIo, S::Response: IntoConnectionIo,
Fut1: Future<Output = Result<TcpConnection<Uri, Io>, ConnectError>>, Fut1: Future<Output = Result<TcpConnection<Uri, Io>, ConnectError>>,
Fut2: Future<Output = Result<S::Response, S::Error>>, Fut2: Future<Output = Result<S::Response, S::Error>>,
@ -522,11 +570,7 @@ pub struct TcpConnectorInnerService<S: Clone> {
} }
impl<S: Clone> TcpConnectorInnerService<S> { impl<S: Clone> TcpConnectorInnerService<S> {
fn new( fn new(service: S, timeout: Duration, local_address: Option<std::net::IpAddr>) -> Self {
service: S,
timeout: Duration,
local_address: Option<std::net::IpAddr>,
) -> Self {
Self { Self {
service, service,
timeout, timeout,
@ -537,11 +581,8 @@ impl<S: Clone> TcpConnectorInnerService<S> {
impl<S, Io> Service<Connect> for TcpConnectorInnerService<S> impl<S, Io> Service<Connect> for TcpConnectorInnerService<S>
where where
S: Service< S: Service<TcpConnect<Uri>, Response = TcpConnection<Uri, Io>, Error = TcpConnectError>
TcpConnect<Uri>, + Clone
Response = TcpConnection<Uri, Io>,
Error = TcpConnectError,
> + Clone
+ 'static, + 'static,
{ {
type Response = S::Response; type Response = S::Response;
@ -564,12 +605,14 @@ where
} }
} }
#[pin_project] pin_project! {
pub struct TcpConnectorInnerFuture<Fut> { #[project = TcpConnectorInnerFutureProj]
pub struct TcpConnectorInnerFuture<Fut> {
#[pin] #[pin]
fut: Fut, fut: Fut,
#[pin] #[pin]
timeout: Sleep, timeout: Sleep,
}
} }
impl<Fut, Io> Future for TcpConnectorInnerFuture<Fut> impl<Fut, Io> Future for TcpConnectorInnerFuture<Fut>
@ -618,12 +661,8 @@ where
impl<S1, S2, Io1, Io2> Service<Connect> for ConnectorServicePriv<S1, S2, Io1, Io2> impl<S1, S2, Io1, Io2> Service<Connect> for ConnectorServicePriv<S1, S2, Io1, Io2>
where where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + Clone + 'static,
+ Clone S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + Clone + 'static,
+ 'static,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>
+ Clone
+ 'static,
Io1: ConnectionIo, Io1: ConnectionIo,
Io2: ConnectionIo, Io2: ConnectionIo,
{ {
@ -643,38 +682,46 @@ where
match req.uri.scheme_str() { match req.uri.scheme_str() {
Some("https") | Some("wss") => match self.tls_pool { Some("https") | Some("wss") => match self.tls_pool {
None => ConnectorServiceFuture::SslIsNotSupported, None => ConnectorServiceFuture::SslIsNotSupported,
Some(ref pool) => ConnectorServiceFuture::Tls(pool.call(req)), Some(ref pool) => ConnectorServiceFuture::Tls {
fut: pool.call(req),
},
},
_ => ConnectorServiceFuture::Tcp {
fut: self.tcp_pool.call(req),
}, },
_ => ConnectorServiceFuture::Tcp(self.tcp_pool.call(req)),
} }
} }
} }
#[pin_project(project = ConnectorServiceProj)] pin_project! {
pub enum ConnectorServiceFuture<S1, S2, Io1, Io2> #[project = ConnectorServiceFutureProj]
where pub enum ConnectorServiceFuture<S1, S2, Io1, Io2>
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> where
+ Clone S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>,
+ 'static, S1: Clone,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> S1: 'static,
+ Clone S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>,
+ 'static, S2: Clone,
S2: 'static,
Io1: ConnectionIo, Io1: ConnectionIo,
Io2: ConnectionIo, Io2: ConnectionIo,
{ {
Tcp(#[pin] <ConnectionPool<S1, Io1> as Service<Connect>>::Future), Tcp {
Tls(#[pin] <ConnectionPool<S2, Io2> as Service<Connect>>::Future), #[pin]
SslIsNotSupported, fut: <ConnectionPool<S1, Io1> as Service<Connect>>::Future
},
Tls {
#[pin]
fut: <ConnectionPool<S2, Io2> as Service<Connect>>::Future
},
SslIsNotSupported
}
} }
impl<S1, S2, Io1, Io2> Future for ConnectorServiceFuture<S1, S2, Io1, Io2> impl<S1, S2, Io1, Io2> Future for ConnectorServiceFuture<S1, S2, Io1, Io2>
where where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + Clone + 'static,
+ Clone S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + Clone + 'static,
+ 'static,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>
+ Clone
+ 'static,
Io1: ConnectionIo, Io1: ConnectionIo,
Io2: ConnectionIo, Io2: ConnectionIo,
{ {
@ -682,9 +729,9 @@ where
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.project() { match self.project() {
ConnectorServiceProj::Tcp(fut) => fut.poll(cx).map_ok(Connection::Tcp), ConnectorServiceFutureProj::Tcp { fut } => fut.poll(cx).map_ok(Connection::Tcp),
ConnectorServiceProj::Tls(fut) => fut.poll(cx).map_ok(Connection::Tls), ConnectorServiceFutureProj::Tls { fut } => fut.poll(cx).map_ok(Connection::Tls),
ConnectorServiceProj::SslIsNotSupported => { ConnectorServiceFutureProj::SslIsNotSupported => {
Poll::Ready(Err(ConnectError::SslIsNotSupported)) Poll::Ready(Err(ConnectError::SslIsNotSupported))
} }
} }
@ -769,3 +816,42 @@ mod resolver {
}) })
} }
} }
#[cfg(feature = "dangerous-h2c")]
#[cfg(test)]
mod tests {
use std::convert::Infallible;
use actix_http::{HttpService, Request, Response, Version};
use actix_http_test::test_server;
use actix_service::ServiceFactoryExt as _;
use super::*;
use crate::Client;
#[actix_rt::test]
async fn h2c_connector() {
let mut srv = test_server(|| {
HttpService::build()
.h2(|_req: Request| async { Ok::<_, Infallible>(Response::ok()) })
.tcp()
.map_err(|_| ())
})
.await;
let connector = Connector {
connector: new_connector(resolver::resolver()),
config: ConnectorConfig::default(),
ssl: SslConnector::None,
};
let client = Client::builder().connector(connector).finish();
let request = client.get(srv.surl("/")).send();
let response = request.await.unwrap();
assert!(response.status().is_success());
assert_eq!(response.version(), Version::HTTP_2);
srv.stop().await;
}
}

View File

@ -2,12 +2,13 @@ use std::{error::Error as StdError, fmt, io};
use derive_more::{Display, From}; use derive_more::{Display, From};
use actix_http::{
error::{Error, ParseError},
http::Error as HttpError,
};
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
use actix_tls::accept::openssl::SslError; use actix_tls::accept::openssl::SslError;
use crate::error::{Error, ParseError};
use crate::http::Error as HttpError;
/// A set of errors that can occur while connecting to an HTTP host /// A set of errors that can occur while connecting to an HTTP host
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
#[non_exhaustive] #[non_exhaustive]

View File

@ -5,24 +5,25 @@ use std::{
}; };
use actix_codec::Framed; use actix_codec::Framed;
use actix_http::{
body::{BodySize, MessageBody},
error::PayloadError,
h1,
http::{
header::{HeaderMap, IntoHeaderValue, EXPECT, HOST},
StatusCode,
},
Error, Payload, RequestHeadType, ResponseHead,
};
use actix_utils::future::poll_fn; use actix_utils::future::poll_fn;
use bytes::buf::BufMut; use bytes::buf::BufMut;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use futures_util::SinkExt as _; use futures_util::SinkExt as _;
use pin_project_lite::pin_project;
use crate::h1;
use crate::http::{
header::{HeaderMap, IntoHeaderValue, EXPECT, HOST},
StatusCode,
};
use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::Payload;
use crate::{error::PayloadError, Error};
use super::connection::{ConnectionIo, H1Connection}; use super::connection::{ConnectionIo, H1Connection};
use super::error::{ConnectError, SendRequestError}; use super::error::{ConnectError, SendRequestError};
use crate::body::{BodySize, MessageBody};
pub(crate) async fn send_request<Io, B>( pub(crate) async fn send_request<Io, B>(
io: H1Connection<Io>, io: H1Connection<Io>,
@ -69,7 +70,7 @@ where
// RFC: https://tools.ietf.org/html/rfc7231#section-5.1.1 // RFC: https://tools.ietf.org/html/rfc7231#section-5.1.1
let is_expect = if head.as_ref().headers.contains_key(EXPECT) { let is_expect = if head.as_ref().headers.contains_key(EXPECT) {
match body.size() { match body.size() {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => { BodySize::None | BodySize::Sized(0) => {
let keep_alive = framed.codec_ref().keepalive(); let keep_alive = framed.codec_ref().keepalive();
framed.io_mut().on_release(keep_alive); framed.io_mut().on_release(keep_alive);
@ -103,7 +104,7 @@ where
if do_send { if do_send {
// send request body // send request body
match body.size() { match body.size() {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => {} BodySize::None | BodySize::Sized(0) => {}
_ => send_body(body, pin_framed.as_mut()).await?, _ => send_body(body, pin_framed.as_mut()).await?,
}; };
@ -194,10 +195,11 @@ where
Ok(()) Ok(())
} }
#[pin_project::pin_project] pin_project! {
pub(crate) struct PlStream<Io: ConnectionIo> { pub(crate) struct PlStream<Io: ConnectionIo> {
#[pin] #[pin]
framed: Framed<H1Connection<Io>, h1::ClientPayloadCodec>, framed: Framed<H1Connection<Io>, h1::ClientPayloadCodec>,
}
} }
impl<Io: ConnectionIo> PlStream<Io> { impl<Io: ConnectionIo> PlStream<Io> {
@ -211,10 +213,7 @@ impl<Io: ConnectionIo> PlStream<Io> {
impl<Io: ConnectionIo> Stream for PlStream<Io> { impl<Io: ConnectionIo> Stream for PlStream<Io> {
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 mut this = self.project(); let mut this = self.project();
match ready!(this.framed.as_mut().next_item(cx)?) { match ready!(this.framed.as_mut().next_item(cx)?) {

View File

@ -8,13 +8,12 @@ use h2::{
}; };
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING};
use http::{request::Request, Method, Version}; use http::{request::Request, Method, Version};
use log::trace;
use crate::{ use actix_http::{
body::{BodySize, MessageBody}, body::{BodySize, MessageBody},
header::HeaderMap, header::HeaderMap,
message::{RequestHeadType, ResponseHead}, Error, Payload, RequestHeadType, ResponseHead,
payload::Payload,
Error,
}; };
use super::{ use super::{
@ -37,10 +36,7 @@ where
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!(length, BodySize::None | BodySize::Sized(0));
length,
BodySize::None | BodySize::Empty | BodySize::Sized(0)
);
let mut req = Request::new(()); let mut req = Request::new(());
*req.uri_mut() = head.as_ref().uri.clone(); *req.uri_mut() = head.as_ref().uri.clone();
@ -53,13 +49,11 @@ where
// Content length // Content length
let _ = match length { let _ = match length {
BodySize::None => None, BodySize::None => None,
BodySize::Stream => {
skip_len = false; BodySize::Sized(0) => req
None
}
BodySize::Empty => req
.headers_mut() .headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")), .insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
BodySize::Sized(len) => { BodySize::Sized(len) => {
let mut buf = itoa::Buffer::new(); let mut buf = itoa::Buffer::new();
@ -68,6 +62,11 @@ where
HeaderValue::from_str(buf.format(len)).unwrap(), HeaderValue::from_str(buf.format(len)).unwrap(),
) )
} }
BodySize::Stream => {
skip_len = false;
None
}
}; };
// Extracting extra headers from RequestHeadType. HeaderMap::new() does not allocate. // Extracting extra headers from RequestHeadType. HeaderMap::new() does not allocate.
@ -131,10 +130,7 @@ where
Ok((head, payload)) Ok((head, payload))
} }
async fn send_body<B>( async fn send_body<B>(body: B, mut send: SendStream<Bytes>) -> Result<(), SendRequestError>
body: B,
mut send: SendStream<Bytes>,
) -> Result<(), SendRequestError>
where where
B: MessageBody, B: MessageBody,
B::Error: Into<Error>, B::Error: Into<Error>,
@ -184,8 +180,7 @@ where
pub(crate) fn handshake<Io: ConnectionIo>( pub(crate) fn handshake<Io: ConnectionIo>(
io: Io, io: Io,
config: &ConnectorConfig, config: &ConnectorConfig,
) -> impl Future<Output = Result<(SendRequest<Bytes>, Connection<Io, Bytes>), h2::Error>> ) -> impl Future<Output = Result<(SendRequest<Bytes>, Connection<Io, Bytes>), h2::Error>> {
{
let mut builder = Builder::new(); let mut builder = Builder::new();
builder builder
.initial_window_size(config.stream_window_size) .initial_window_size(config.stream_window_size)

View File

@ -17,7 +17,6 @@ pub use actix_tls::connect::{
pub use self::connection::{Connection, ConnectionIo}; pub use self::connection::{Connection, ConnectionIo};
pub use self::connector::{Connector, ConnectorService}; pub use self::connector::{Connector, ConnectorService};
pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};
pub use crate::Protocol;
#[derive(Clone)] #[derive(Clone)]
pub struct Connect { pub struct Connect {

View File

@ -14,22 +14,21 @@ use std::{
}; };
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf}; use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
use actix_http::Protocol;
use actix_rt::time::{sleep, Sleep}; use actix_rt::time::{sleep, Sleep};
use actix_service::Service; use actix_service::Service;
use ahash::AHashMap; use ahash::AHashMap;
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use futures_util::FutureExt;
use http::uri::Authority; use http::uri::Authority;
use pin_project::pin_project; use pin_project_lite::pin_project;
use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use tokio::sync::{OwnedSemaphorePermit, Semaphore};
use super::config::ConnectorConfig; use super::config::ConnectorConfig;
use super::connection::{ use super::connection::{ConnectionInnerType, ConnectionIo, ConnectionType, H2ConnectionInner};
ConnectionInnerType, ConnectionIo, ConnectionType, H2ConnectionInner,
};
use super::error::ConnectError; use super::error::ConnectError;
use super::h2proto::handshake; use super::h2proto::handshake;
use super::Connect; use super::Connect;
use super::Protocol;
#[derive(Hash, Eq, PartialEq, Clone, Debug)] #[derive(Hash, Eq, PartialEq, Clone, Debug)]
pub struct Key { pub struct Key {
@ -152,9 +151,7 @@ where
impl<S, Io> Service<Connect> for ConnectionPool<S, Io> impl<S, Io> Service<Connect> for ConnectionPool<S, Io>
where where
S: Service<Connect, Response = (Io, Protocol), Error = ConnectError> S: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + Clone + 'static,
+ Clone
+ 'static,
Io: ConnectionIo, Io: ConnectionIo,
{ {
type Response = ConnectionType<Io>; type Response = ConnectionType<Io>;
@ -195,8 +192,8 @@ where
let config = &inner.config; let config = &inner.config;
let idle_dur = now - c.used; let idle_dur = now - c.used;
let age = now - c.created; let age = now - c.created;
let conn_ineligible = idle_dur > config.conn_keep_alive let conn_ineligible =
|| age > config.conn_lifetime; idle_dur > config.conn_keep_alive || age > config.conn_lifetime;
if conn_ineligible { if conn_ineligible {
// drop connections that are too old // drop connections that are too old
@ -205,7 +202,7 @@ where
// check if the connection is still usable // check if the connection is still usable
if let ConnectionInnerType::H1(ref mut io) = c.conn { if let ConnectionInnerType::H1(ref mut io) = c.conn {
let check = ConnectionCheckFuture { io }; let check = ConnectionCheckFuture { io };
match check.await { match check.now_or_never().expect("ConnectionCheckFuture must never yield with Poll::Pending.") {
ConnectionState::Tainted => { ConnectionState::Tainted => {
inner.close(c.conn); inner.close(c.conn);
continue; continue;
@ -231,9 +228,7 @@ where
// match the connection and spawn new one if did not get anything. // match the connection and spawn new one if did not get anything.
match conn { match conn {
Some(conn) => { Some(conn) => Ok(ConnectionType::from_pool(conn.conn, conn.created, acquired)),
Ok(ConnectionType::from_pool(conn.conn, conn.created, acquired))
}
None => { None => {
let (io, proto) = connector.call(req).await?; let (io, proto) = connector.call(req).await?;
@ -284,9 +279,7 @@ where
let mut read_buf = ReadBuf::new(&mut buf); let mut read_buf = ReadBuf::new(&mut buf);
let state = match Pin::new(&mut this.io).poll_read(cx, &mut read_buf) { let state = match Pin::new(&mut this.io).poll_read(cx, &mut read_buf) {
Poll::Ready(Ok(())) if !read_buf.filled().is_empty() => { Poll::Ready(Ok(())) if !read_buf.filled().is_empty() => ConnectionState::Tainted,
ConnectionState::Tainted
}
Poll::Pending => ConnectionState::Live, Poll::Pending => ConnectionState::Live,
_ => ConnectionState::Skip, _ => ConnectionState::Skip,
@ -302,11 +295,13 @@ struct PooledConnection<Io> {
created: Instant, created: Instant,
} }
#[pin_project] pin_project! {
struct CloseConnection<Io> { #[project = CloseConnectionProj]
struct CloseConnection<Io> {
io: Io, io: Io,
#[pin] #[pin]
timeout: Sleep, timeout: Sleep,
}
} }
impl<Io> CloseConnection<Io> impl<Io> CloseConnection<Io>
@ -413,17 +408,11 @@ mod test {
unimplemented!() unimplemented!()
} }
fn poll_flush( fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<io::Result<()>> {
unimplemented!() unimplemented!()
} }
fn poll_shutdown( fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<io::Result<()>> {
Poll::Ready(Ok(())) Poll::Ready(Ok(()))
} }
} }

View File

@ -8,16 +8,14 @@ use std::{
use actix_codec::Framed; use actix_codec::Framed;
use actix_http::{ use actix_http::{
body::Body, body::AnyBody, h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead,
client::{
Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError,
},
h1::ClientCodec,
Payload, RequestHead, RequestHeadType, ResponseHead,
}; };
use actix_service::Service; use actix_service::Service;
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
use crate::client::{
Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError,
};
use crate::response::ClientResponse; use crate::response::ClientResponse;
pub type BoxConnectorService = Rc< pub type BoxConnectorService = Rc<
@ -32,7 +30,7 @@ pub type BoxConnectorService = Rc<
pub type BoxedSocket = Box<dyn ConnectionIo>; pub type BoxedSocket = Box<dyn ConnectionIo>;
pub enum ConnectRequest { pub enum ConnectRequest {
Client(RequestHeadType, Body, Option<net::SocketAddr>), Client(RequestHeadType, AnyBody, Option<net::SocketAddr>),
Tunnel(RequestHead, Option<net::SocketAddr>), Tunnel(RequestHead, Option<net::SocketAddr>),
} }

View File

@ -1,15 +1,15 @@
//! HTTP client errors //! HTTP client errors
pub use actix_http::client::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; pub use actix_http::{
pub use actix_http::error::PayloadError; error::PayloadError,
pub use actix_http::http::Error as HttpError; http::{header::HeaderValue, Error as HttpError, StatusCode},
pub use actix_http::ws::HandshakeError as WsHandshakeError; ws::{HandshakeError as WsHandshakeError, ProtocolError as WsProtocolError},
pub use actix_http::ws::ProtocolError as WsProtocolError; };
use derive_more::{Display, From};
use serde_json::error::Error as JsonError; use serde_json::error::Error as JsonError;
use actix_http::http::{header::HeaderValue, StatusCode}; pub use crate::client::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};
use derive_more::{Display, From};
/// Websocket client error /// Websocket client error
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]

View File

@ -5,7 +5,7 @@ use futures_core::Stream;
use serde::Serialize; use serde::Serialize;
use actix_http::{ use actix_http::{
body::Body, body::AnyBody,
http::{header::IntoHeaderValue, Error as HttpError, HeaderMap, HeaderName, Method, Uri}, http::{header::IntoHeaderValue, Error as HttpError, HeaderMap, HeaderName, Method, Uri},
RequestHead, RequestHead,
}; };
@ -45,7 +45,7 @@ impl FrozenClientRequest {
/// Send a body. /// Send a body.
pub fn send_body<B>(&self, body: B) -> SendClientRequest pub fn send_body<B>(&self, body: B) -> SendClientRequest
where where
B: Into<Body>, B: Into<AnyBody>,
{ {
RequestSender::Rc(self.head.clone(), None).send_body( RequestSender::Rc(self.head.clone(), None).send_body(
self.addr, self.addr,
@ -158,7 +158,7 @@ impl FrozenSendBuilder {
/// Complete request construction and send a body. /// Complete request construction and send a body.
pub fn send_body<B>(self, body: B) -> SendClientRequest pub fn send_body<B>(self, body: B) -> SendClientRequest
where where
B: Into<Body>, B: Into<AnyBody>,
{ {
if let Some(e) = self.err { if let Some(e) = self.err {
return e.into(); return e.into();

View File

@ -104,22 +104,8 @@
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
use std::{convert::TryFrom, rc::Rc, time::Duration};
#[cfg(feature = "cookies")]
pub use cookie;
pub use actix_http::{client::Connector, http};
use actix_http::{
client::{TcpConnect, TcpConnectError, TcpConnection},
http::{Error as HttpError, HeaderMap, Method, Uri},
RequestHead,
};
use actix_rt::net::TcpStream;
use actix_service::Service;
mod builder; mod builder;
mod client;
mod connect; mod connect;
pub mod error; pub mod error;
mod frozen; mod frozen;
@ -130,13 +116,29 @@ mod sender;
pub mod test; pub mod test;
pub mod ws; pub mod ws;
pub use actix_http::http;
#[cfg(feature = "cookies")]
pub use cookie;
pub use self::builder::ClientBuilder; pub use self::builder::ClientBuilder;
pub use self::client::Connector;
pub use self::connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse}; pub use self::connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse};
pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder}; pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder};
pub use self::request::ClientRequest; pub use self::request::ClientRequest;
pub use self::response::{ClientResponse, JsonBody, MessageBody}; pub use self::response::{ClientResponse, JsonBody, MessageBody};
pub use self::sender::SendClientRequest; pub use self::sender::SendClientRequest;
use std::{convert::TryFrom, rc::Rc, time::Duration};
use actix_http::{
http::{Error as HttpError, HeaderMap, Method, Uri},
RequestHead,
};
use actix_rt::net::TcpStream;
use actix_service::Service;
use self::client::{TcpConnect, TcpConnectError, TcpConnection};
/// An asynchronous HTTP and WebSocket client. /// An asynchronous HTTP and WebSocket client.
/// ///
/// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU /// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU

View File

@ -8,8 +8,7 @@ use std::{
}; };
use actix_http::{ use actix_http::{
body::Body, body::AnyBody,
client::{InvalidUrl, SendRequestError},
http::{header, Method, StatusCode, Uri}, http::{header, Method, StatusCode, Uri},
RequestHead, RequestHeadType, RequestHead, RequestHeadType,
}; };
@ -19,6 +18,7 @@ use futures_core::ready;
use super::Transform; use super::Transform;
use crate::client::{InvalidUrl, SendRequestError};
use crate::connect::{ConnectRequest, ConnectResponse}; use crate::connect::{ConnectRequest, ConnectResponse};
use crate::ClientResponse; use crate::ClientResponse;
@ -95,7 +95,7 @@ where
}; };
let body_opt = match body { let body_opt = match body {
Body::Bytes(ref b) => Some(b.clone()), AnyBody::Bytes(ref b) => Some(b.clone()),
_ => None, _ => None,
}; };
@ -192,14 +192,14 @@ where
let body_new = if is_redirect { let body_new = if is_redirect {
// try to reuse body // try to reuse body
match body { match body {
Some(ref bytes) => Body::Bytes(bytes.clone()), Some(ref bytes) => AnyBody::Bytes(bytes.clone()),
// TODO: should this be Body::Empty or Body::None. // TODO: should this be AnyBody::Empty or AnyBody::None.
_ => Body::Empty, _ => AnyBody::empty(),
} }
} else { } else {
body = None; body = None;
// remove body // remove body
Body::None AnyBody::None
}; };
let mut headers = headers.take().unwrap(); let mut headers = headers.take().unwrap();

View File

@ -5,7 +5,7 @@ use futures_core::Stream;
use serde::Serialize; use serde::Serialize;
use actix_http::{ use actix_http::{
body::Body, body::AnyBody,
http::{ http::{
header::{self, IntoHeaderPair}, header::{self, IntoHeaderPair},
ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, Version, ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, Version,
@ -115,10 +115,10 @@ impl ClientRequest {
&self.head.method &self.head.method
} }
#[doc(hidden)]
/// Set HTTP version of this request. /// Set HTTP version of this request.
/// ///
/// By default requests's HTTP version depends on network stream /// By default requests's HTTP version depends on network stream
#[doc(hidden)]
#[inline] #[inline]
pub fn version(mut self, version: Version) -> Self { pub fn version(mut self, version: Version) -> Self {
self.head.version = version; self.head.version = version;
@ -350,7 +350,7 @@ impl ClientRequest {
/// Complete request construction and send body. /// Complete request construction and send body.
pub fn send_body<B>(self, body: B) -> SendClientRequest pub fn send_body<B>(self, body: B) -> SendClientRequest
where where
B: Into<Body>, B: Into<AnyBody>,
{ {
let slf = match self.prep_for_sending() { let slf = match self.prep_for_sending() {
Ok(slf) => slf, Ok(slf) => slf,

View File

@ -9,7 +9,7 @@ use std::{
}; };
use actix_http::{ use actix_http::{
body::{Body, BodyStream}, body::{AnyBody, BodyStream},
http::{ http::{
header::{self, HeaderMap, HeaderName, IntoHeaderValue}, header::{self, HeaderMap, HeaderName, IntoHeaderValue},
Error as HttpError, Error as HttpError,
@ -196,7 +196,7 @@ impl RequestSender {
body: B, body: B,
) -> SendClientRequest ) -> SendClientRequest
where where
B: Into<Body>, B: Into<AnyBody>,
{ {
let req = match self { let req = match self {
RequestSender::Owned(head) => { RequestSender::Owned(head) => {
@ -236,7 +236,7 @@ impl RequestSender {
response_decompress, response_decompress,
timeout, timeout,
config, config,
Body::Bytes(Bytes::from(body)), AnyBody::Bytes(Bytes::from(body)),
) )
} }
@ -265,7 +265,7 @@ impl RequestSender {
response_decompress, response_decompress,
timeout, timeout,
config, config,
Body::Bytes(Bytes::from(body)), AnyBody::Bytes(Bytes::from(body)),
) )
} }
@ -286,7 +286,7 @@ impl RequestSender {
response_decompress, response_decompress,
timeout, timeout,
config, config,
Body::from_message(BodyStream::new(stream)), AnyBody::new_boxed(BodyStream::new(stream)),
) )
} }
@ -297,7 +297,7 @@ impl RequestSender {
timeout: Option<Duration>, timeout: Option<Duration>,
config: &ClientConfig, config: &ClientConfig,
) -> SendClientRequest { ) -> SendClientRequest {
self.send_body(addr, response_decompress, timeout, config, Body::Empty) self.send_body(addr, response_decompress, timeout, config, AnyBody::empty())
} }
fn set_header_if_none<V>(&mut self, key: HeaderName, value: V) -> Result<(), HttpError> fn set_header_if_none<V>(&mut self, key: HeaderName, value: V) -> Result<(), HttpError>

View File

@ -1,20 +1,26 @@
use std::collections::HashMap; use std::{
use std::io::{Read, Write}; collections::HashMap,
use std::net::{IpAddr, Ipv4Addr}; io::{Read, Write},
use std::sync::atomic::{AtomicUsize, Ordering}; net::{IpAddr, Ipv4Addr},
use std::sync::Arc; sync::{
use std::time::Duration; atomic::{AtomicUsize, Ordering},
Arc,
},
time::Duration,
};
use actix_utils::future::ok; use actix_utils::future::ok;
use brotli2::write::BrotliEncoder;
use bytes::Bytes; use bytes::Bytes;
use cookie::Cookie; use cookie::Cookie;
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use flate2::Compression;
use futures_util::stream; use futures_util::stream;
use rand::Rng; use rand::Rng;
#[cfg(feature = "compress-brotli")]
use brotli2::write::BrotliEncoder;
#[cfg(feature = "compress-gzip")]
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use actix_http::{ use actix_http::{
http::{self, StatusCode}, http::{self, StatusCode},
HttpService, HttpService,
@ -24,7 +30,6 @@ use actix_service::{fn_service, map_config, ServiceFactoryExt as _};
use actix_web::{ use actix_web::{
dev::{AppConfig, BodyEncoding}, dev::{AppConfig, BodyEncoding},
http::header, http::header,
middleware::Compress,
web, App, Error, HttpRequest, HttpResponse, web, App, Error, HttpRequest, HttpResponse,
}; };
use awc::error::{JsonPayloadError, PayloadError, SendRequestError}; use awc::error::{JsonPayloadError, PayloadError, SendRequestError};
@ -463,11 +468,12 @@ async fn test_with_query_parameter() {
assert!(res.status().is_success()); assert!(res.status().is_success());
} }
#[cfg(feature = "compress-gzip")]
#[actix_rt::test] #[actix_rt::test]
async fn test_no_decompress() { async fn test_no_decompress() {
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
App::new() App::new()
.wrap(Compress::default()) .wrap(actix_web::middleware::Compress::default())
.service(web::resource("/").route(web::to(|| { .service(web::resource("/").route(web::to(|| {
let mut res = HttpResponse::Ok().body(STR); let mut res = HttpResponse::Ok().body(STR);
res.encoding(header::ContentEncoding::Gzip); res.encoding(header::ContentEncoding::Gzip);
@ -507,6 +513,7 @@ async fn test_no_decompress() {
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
} }
#[cfg(feature = "compress-gzip")]
#[actix_rt::test] #[actix_rt::test]
async fn test_client_gzip_encoding() { async fn test_client_gzip_encoding() {
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
@ -530,6 +537,7 @@ async fn test_client_gzip_encoding() {
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
} }
#[cfg(feature = "compress-gzip")]
#[actix_rt::test] #[actix_rt::test]
async fn test_client_gzip_encoding_large() { async fn test_client_gzip_encoding_large() {
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
@ -553,6 +561,7 @@ async fn test_client_gzip_encoding_large() {
assert_eq!(bytes, Bytes::from(STR.repeat(10))); assert_eq!(bytes, Bytes::from(STR.repeat(10)));
} }
#[cfg(feature = "compress-gzip")]
#[actix_rt::test] #[actix_rt::test]
async fn test_client_gzip_encoding_large_random() { async fn test_client_gzip_encoding_large_random() {
let data = rand::thread_rng() let data = rand::thread_rng()
@ -581,6 +590,7 @@ async fn test_client_gzip_encoding_large_random() {
assert_eq!(bytes, Bytes::from(data)); assert_eq!(bytes, Bytes::from(data));
} }
#[cfg(feature = "compress-brotli")]
#[actix_rt::test] #[actix_rt::test]
async fn test_client_brotli_encoding() { async fn test_client_brotli_encoding() {
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
@ -603,6 +613,7 @@ async fn test_client_brotli_encoding() {
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
} }
#[cfg(feature = "compress-brotli")]
#[actix_rt::test] #[actix_rt::test]
async fn test_client_brotli_encoding_large_random() { async fn test_client_brotli_encoding_large_random() {
let data = rand::thread_rng() let data = rand::thread_rng()
@ -795,8 +806,7 @@ async fn client_unread_response() {
let lst = std::net::TcpListener::bind(addr).unwrap(); let lst = std::net::TcpListener::bind(addr).unwrap();
std::thread::spawn(move || { std::thread::spawn(move || {
for stream in lst.incoming() { let (mut stream, _) = lst.accept().unwrap();
let mut stream = stream.unwrap();
let mut b = [0; 1000]; let mut b = [0; 1000];
let _ = stream.read(&mut b).unwrap(); let _ = stream.read(&mut b).unwrap();
let _ = stream.write_all( let _ = stream.write_all(
@ -805,7 +815,6 @@ async fn client_unread_response() {
\r\n\ \r\n\
welcome!", welcome!",
); );
}
}); });
// client request // client request

View File

@ -39,7 +39,7 @@ fn tls_config() -> SslAcceptor {
#[actix_rt::test] #[actix_rt::test]
async fn test_connection_window_size() { async fn test_connection_window_size() {
let srv = test_server(move || { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h2(map_config( .h2(map_config(
App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))), App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))),

View File

@ -19,8 +19,7 @@ use actix_utils::future::ok;
use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse}; use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse};
use rustls::{ use rustls::{
client::{ServerCertVerified, ServerCertVerifier}, client::{ServerCertVerified, ServerCertVerifier},
Certificate, ClientConfig, OwnedTrustAnchor, PrivateKey, RootCertStore, ServerConfig, Certificate, ClientConfig, PrivateKey, ServerConfig, ServerName,
ServerName,
}; };
use rustls_pemfile::{certs, pkcs8_private_keys}; use rustls_pemfile::{certs, pkcs8_private_keys};

View File

@ -8,6 +8,7 @@ use std::{any::Any, io, net::SocketAddr};
use actix_web::{dev::Extensions, rt::net::TcpStream, web, App, HttpServer}; use actix_web::{dev::Extensions, rt::net::TcpStream, web, App, HttpServer};
#[allow(dead_code)]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct ConnectionInfo { struct ConnectionInfo {
bind: SocketAddr, bind: SocketAddr,

View File

@ -4,7 +4,7 @@ use std::future::Future;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::rc::Rc; use std::rc::Rc;
use actix_http::body::{Body, MessageBody}; use actix_http::body::{AnyBody, MessageBody};
use actix_http::{Extensions, Request}; use actix_http::{Extensions, Request};
use actix_service::boxed::{self, BoxServiceFactory}; use actix_service::boxed::{self, BoxServiceFactory};
use actix_service::{ use actix_service::{
@ -39,7 +39,7 @@ pub struct App<T, B> {
_phantom: PhantomData<B>, _phantom: PhantomData<B>,
} }
impl App<AppEntry, Body> { impl App<AppEntry, AnyBody> {
/// Create application builder. Application can be configured with a builder-like pattern. /// Create application builder. Application can be configured with a builder-like pattern.
#[allow(clippy::new_without_default)] #[allow(clippy::new_without_default)]
pub fn new() -> Self { pub fn new() -> Self {

View File

@ -137,7 +137,7 @@ impl<T: ?Sized + 'static> FromRequest for Data<T> {
type_name::<T>(), type_name::<T>(),
); );
err(ErrorInternalServerError( err(ErrorInternalServerError(
"App data is not configured, to configure use App::data()", "App data is not configured, to configure construct it with web::Data::new() and pass it to App::app_data()",
)) ))
} }
} }

View File

@ -14,13 +14,14 @@ pub use crate::types::form::UrlEncoded;
pub use crate::types::json::JsonBody; pub use crate::types::json::JsonBody;
pub use crate::types::readlines::Readlines; pub use crate::types::readlines::Readlines;
pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, ResponseBody, SizedStream}; #[allow(deprecated)]
pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, SizedStream};
#[cfg(feature = "__compress")] #[cfg(feature = "__compress")]
pub use actix_http::encoding::Decoder as Decompress; pub use actix_http::encoding::Decoder as Decompress;
pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, Response, ResponseHead}; pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, Response, ResponseHead};
pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_router::{Path, ResourceDef, ResourcePath, Url};
pub use actix_server::Server; pub use actix_server::{Server, ServerHandle};
pub use actix_service::{ pub use actix_service::{
always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform, always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform,
}; };

View File

@ -1,6 +1,6 @@
use std::{cell::RefCell, fmt, io::Write as _}; use std::{cell::RefCell, fmt, io::Write as _};
use actix_http::{body::Body, header, StatusCode}; use actix_http::{body::AnyBody, header, StatusCode};
use bytes::{BufMut as _, BytesMut}; use bytes::{BufMut as _, BytesMut};
use crate::{Error, HttpRequest, HttpResponse, Responder, ResponseError}; use crate::{Error, HttpRequest, HttpResponse, Responder, ResponseError};
@ -88,7 +88,7 @@ where
header::CONTENT_TYPE, header::CONTENT_TYPE,
header::HeaderValue::from_static("text/plain; charset=utf-8"), header::HeaderValue::from_static("text/plain; charset=utf-8"),
); );
res.set_body(Body::from(buf.into_inner())) res.set_body(AnyBody::from(buf.into_inner()))
} }
InternalErrorType::Response(ref resp) => { InternalErrorType::Response(ref resp) => {

View File

@ -1,16 +1,13 @@
use std::future::Future; use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::task::{Context, Poll};
use actix_service::{Service, ServiceFactory}; use actix_service::{
use actix_utils::future::{ready, Ready}; boxed::{self, BoxServiceFactory},
use futures_core::ready; fn_service,
use pin_project::pin_project; };
use crate::{ use crate::{
service::{ServiceRequest, ServiceResponse}, service::{ServiceRequest, ServiceResponse},
Error, FromRequest, HttpRequest, HttpResponse, Responder, Error, FromRequest, HttpResponse, Responder,
}; };
/// A request handler is an async function that accepts zero or more parameters that can be /// A request handler is an async function that accepts zero or more parameters that can be
@ -27,139 +24,26 @@ where
fn call(&self, param: T) -> R; fn call(&self, param: T) -> R;
} }
#[doc(hidden)] pub fn handler_service<F, T, R>(
/// Extract arguments from request, run factory function and make response. handler: F,
pub struct HandlerService<F, T, R> ) -> BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>
where where
F: Handler<T, R>, F: Handler<T, R>,
T: FromRequest, T: FromRequest,
R: Future, R: Future,
R::Output: Responder, R::Output: Responder,
{ {
hnd: F, boxed::factory(fn_service(move |req: ServiceRequest| {
_phantom: PhantomData<(T, R)>, let handler = handler.clone();
} async move {
impl<F, T, R> HandlerService<F, T, R>
where
F: Handler<T, R>,
T: FromRequest,
R: Future,
R::Output: Responder,
{
pub fn new(hnd: F) -> Self {
Self {
hnd,
_phantom: PhantomData,
}
}
}
impl<F, T, R> Clone for HandlerService<F, T, R>
where
F: Handler<T, R>,
T: FromRequest,
R: Future,
R::Output: Responder,
{
fn clone(&self) -> Self {
Self {
hnd: self.hnd.clone(),
_phantom: PhantomData,
}
}
}
impl<F, T, R> ServiceFactory<ServiceRequest> for HandlerService<F, T, R>
where
F: Handler<T, R>,
T: FromRequest,
R: Future,
R::Output: Responder,
{
type Response = ServiceResponse;
type Error = Error;
type Config = ();
type Service = Self;
type InitError = ();
type Future = Ready<Result<Self::Service, ()>>;
fn new_service(&self, _: ()) -> Self::Future {
ready(Ok(self.clone()))
}
}
/// HandlerService is both it's ServiceFactory and Service Type.
impl<F, T, R> Service<ServiceRequest> for HandlerService<F, T, R>
where
F: Handler<T, R>,
T: FromRequest,
R: Future,
R::Output: Responder,
{
type Response = ServiceResponse;
type Error = Error;
type Future = HandlerServiceFuture<F, T, R>;
actix_service::always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future {
let (req, mut payload) = req.into_parts(); let (req, mut payload) = req.into_parts();
let fut = T::from_request(&req, &mut payload); let res = match T::from_request(&req, &mut payload).await {
HandlerServiceFuture::Extract(fut, Some(req), self.hnd.clone()) Err(err) => HttpResponse::from_error(err),
} Ok(data) => handler.call(data).await.respond_to(&req),
}
#[doc(hidden)]
#[pin_project(project = HandlerProj)]
pub enum HandlerServiceFuture<F, T, R>
where
F: Handler<T, R>,
T: FromRequest,
R: Future,
R::Output: Responder,
{
Extract(#[pin] T::Future, Option<HttpRequest>, F),
Handle(#[pin] R, Option<HttpRequest>),
}
impl<F, T, R> Future for HandlerServiceFuture<F, T, R>
where
F: Handler<T, R>,
T: FromRequest,
R: Future,
R::Output: Responder,
{
// Error type in this future is a placeholder type.
// all instances of error must be converted to ServiceResponse and return in Ok.
type Output = Result<ServiceResponse, Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
loop {
match self.as_mut().project() {
HandlerProj::Extract(fut, req, handle) => {
match ready!(fut.poll(cx)) {
Ok(item) => {
let fut = handle.call(item);
let state = HandlerServiceFuture::Handle(fut, req.take());
self.as_mut().set(state);
}
Err(err) => {
let req = req.take().unwrap();
let res = HttpResponse::from_error(err.into());
return Poll::Ready(Ok(ServiceResponse::new(req, res)));
}
}; };
Ok(ServiceResponse::new(req, res))
} }
HandlerProj::Handle(fut, req) => { }))
let res = ready!(fut.poll(cx));
let req = req.take().unwrap();
let res = res.respond_to(&req);
return Poll::Ready(Ok(ServiceResponse::new(req, res)));
}
}
}
}
} }
/// FromRequest trait impl for tuples /// FromRequest trait impl for tuples

View File

@ -34,15 +34,18 @@ fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) {
/// The implied disposition of the content of the HTTP body. /// The implied disposition of the content of the HTTP body.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum DispositionType { pub enum DispositionType {
/// Inline implies default processing /// Inline implies default processing.
Inline, Inline,
/// Attachment implies that the recipient should prompt the user to save the response locally, /// Attachment implies that the recipient should prompt the user to save the response locally,
/// rather than process it normally (as per its media type). /// rather than process it normally (as per its media type).
Attachment, Attachment,
/// Used in *multipart/form-data* as defined in
/// [RFC7578](https://tools.ietf.org/html/rfc7578) to carry the field name and the file name. /// Used in *multipart/form-data* as defined in [RFC7578](https://tools.ietf.org/html/rfc7578)
/// to carry the field name and optional filename.
FormData, FormData,
/// Extension type. Should be handled by recipients the same way as Attachment
/// Extension type. Should be handled by recipients the same way as Attachment.
Ext(String), Ext(String),
} }
@ -76,6 +79,7 @@ pub enum DispositionParam {
/// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from /// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from
/// the form. /// the form.
Name(String), Name(String),
/// A plain file name. /// A plain file name.
/// ///
/// It is [not supposed](https://tools.ietf.org/html/rfc6266#appendix-D) to contain any /// It is [not supposed](https://tools.ietf.org/html/rfc6266#appendix-D) to contain any
@ -83,14 +87,17 @@ pub enum DispositionParam {
/// [`FilenameExt`](DispositionParam::FilenameExt) with charset UTF-8 may be used instead /// [`FilenameExt`](DispositionParam::FilenameExt) with charset UTF-8 may be used instead
/// in case there are Unicode characters in file names. /// in case there are Unicode characters in file names.
Filename(String), Filename(String),
/// An extended file name. It must not exist for `ContentType::Formdata` according to /// An extended file name. It must not exist for `ContentType::Formdata` according to
/// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2). /// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2).
FilenameExt(ExtendedValue), FilenameExt(ExtendedValue),
/// An unrecognized regular parameter as defined in /// An unrecognized regular parameter as defined in
/// [RFC5987](https://tools.ietf.org/html/rfc5987) as *reg-parameter*, in /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *reg-parameter*, in
/// [RFC6266](https://tools.ietf.org/html/rfc6266) as *token "=" value*. Recipients should /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *token "=" value*. Recipients should
/// ignore unrecognizable parameters. /// ignore unrecognizable parameters.
Unknown(String, String), Unknown(String, String),
/// An unrecognized extended parameter as defined in /// An unrecognized extended parameter as defined in
/// [RFC5987](https://tools.ietf.org/html/rfc5987) as *ext-parameter*, in /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *ext-parameter*, in
/// [RFC6266](https://tools.ietf.org/html/rfc6266) as *ext-token "=" ext-value*. The single /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *ext-token "=" ext-value*. The single
@ -205,7 +212,6 @@ impl DispositionParam {
/// itself, *Content-Disposition* has no effect. /// itself, *Content-Disposition* has no effect.
/// ///
/// # ABNF /// # ABNF
/// ```text /// ```text
/// content-disposition = "Content-Disposition" ":" /// content-disposition = "Content-Disposition" ":"
/// disposition-type *( ";" disposition-parm ) /// disposition-type *( ";" disposition-parm )
@ -289,10 +295,12 @@ impl DispositionParam {
/// If "filename" parameter is supplied, do not use the file name blindly, check and possibly /// If "filename" parameter is supplied, do not use the file name blindly, check and possibly
/// change to match local file system conventions if applicable, and do not use directory path /// change to match local file system conventions if applicable, and do not use directory path
/// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3). /// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3).
// TODO: private fields and use smallvec
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct ContentDisposition { pub struct ContentDisposition {
/// The disposition type /// The disposition type
pub disposition: DispositionType, pub disposition: DispositionType,
/// Disposition parameters /// Disposition parameters
pub parameters: Vec<DispositionParam>, pub parameters: Vec<DispositionParam>,
} }
@ -509,22 +517,28 @@ impl fmt::Display for DispositionParam {
// //
// //
// See also comments in test_from_raw_unnecessary_percent_decode. // See also comments in test_from_raw_unnecessary_percent_decode.
static RE: Lazy<Regex> = static RE: Lazy<Regex> =
Lazy::new(|| Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap()); Lazy::new(|| Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap());
match self { match self {
DispositionParam::Name(ref value) => write!(f, "name={}", value), DispositionParam::Name(ref value) => write!(f, "name={}", value),
DispositionParam::Filename(ref value) => { DispositionParam::Filename(ref value) => {
write!(f, "filename=\"{}\"", RE.replace_all(value, "\\$0").as_ref()) write!(f, "filename=\"{}\"", RE.replace_all(value, "\\$0").as_ref())
} }
DispositionParam::Unknown(ref name, ref value) => write!( DispositionParam::Unknown(ref name, ref value) => write!(
f, f,
"{}=\"{}\"", "{}=\"{}\"",
name, name,
&RE.replace_all(value, "\\$0").as_ref() &RE.replace_all(value, "\\$0").as_ref()
), ),
DispositionParam::FilenameExt(ref ext_value) => { DispositionParam::FilenameExt(ref ext_value) => {
write!(f, "filename*={}", ext_value) write!(f, "filename*={}", ext_value)
} }
DispositionParam::UnknownExt(ref name, ref ext_value) => { DispositionParam::UnknownExt(ref name, ref ext_value) => {
write!(f, "{}*={}", name, ext_value) write!(f, "{}*={}", name, ext_value)
} }

View File

@ -74,10 +74,11 @@ impl ContentType {
ContentType(mime::TEXT_PLAIN_UTF_8) ContentType(mime::TEXT_PLAIN_UTF_8)
} }
/// A constructor to easily create a `Content-Type: text/html` header. /// A constructor to easily create a `Content-Type: text/html; charset=utf-8`
/// header.
#[inline] #[inline]
pub fn html() -> ContentType { pub fn html() -> ContentType {
ContentType(mime::TEXT_HTML) ContentType(mime::TEXT_HTML_UTF_8)
} }
/// A constructor to easily create a `Content-Type: text/xml` header. /// A constructor to easily create a `Content-Type: text/xml` header.

View File

@ -7,7 +7,7 @@ use std::{
task::{Context, Poll}, task::{Context, Poll},
}; };
use actix_http::body::{Body, MessageBody}; use actix_http::body::{AnyBody, MessageBody};
use actix_service::{Service, Transform}; use actix_service::{Service, Transform};
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
@ -124,7 +124,7 @@ where
B::Error: Into<Box<dyn StdError + 'static>>, B::Error: Into<Box<dyn StdError + 'static>>,
{ {
fn map_body(self) -> ServiceResponse { fn map_body(self) -> ServiceResponse {
self.map_body(|_, body| Body::from_message(body)) self.map_body(|_, body| AnyBody::new_boxed(body))
} }
} }

View File

@ -10,13 +10,14 @@ use std::{
}; };
use actix_http::{ use actix_http::{
body::{MessageBody, ResponseBody}, body::{AnyBody, MessageBody},
encoding::Encoder, encoding::Encoder,
http::header::{ContentEncoding, ACCEPT_ENCODING}, http::header::{ContentEncoding, ACCEPT_ENCODING},
StatusCode, StatusCode,
}; };
use actix_service::{Service, Transform}; use actix_service::{Service, Transform};
use actix_utils::future::{ok, Either, Ready}; use actix_utils::future::{ok, Either, Ready};
use bytes::Bytes;
use futures_core::ready; use futures_core::ready;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use pin_project::pin_project; use pin_project::pin_project;
@ -61,7 +62,7 @@ where
B: MessageBody, B: MessageBody,
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
{ {
type Response = ServiceResponse<ResponseBody<Encoder<B>>>; type Response = ServiceResponse<AnyBody<Encoder<B>>>;
type Error = Error; type Error = Error;
type Transform = CompressMiddleware<S>; type Transform = CompressMiddleware<S>;
type InitError = (); type InitError = ();
@ -81,7 +82,8 @@ pub struct CompressMiddleware<S> {
} }
static SUPPORTED_ALGORITHM_NAMES: Lazy<String> = Lazy::new(|| { static SUPPORTED_ALGORITHM_NAMES: Lazy<String> = Lazy::new(|| {
let mut encoding = vec![]; #[allow(unused_mut)] // only unused when no compress features enabled
let mut encoding: Vec<&str> = vec![];
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
{ {
@ -110,7 +112,7 @@ where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody, B: MessageBody,
{ {
type Response = ServiceResponse<ResponseBody<Encoder<B>>>; type Response = ServiceResponse<AnyBody<Encoder<B>>>;
type Error = Error; type Error = Error;
type Future = Either<CompressResponse<S, B>, Ready<Result<Self::Response, Self::Error>>>; type Future = Either<CompressResponse<S, B>, Ready<Result<Self::Response, Self::Error>>>;
@ -142,15 +144,19 @@ where
// There is an HTTP header but we cannot match what client as asked for // There is an HTTP header but we cannot match what client as asked for
Some(Err(_)) => { Some(Err(_)) => {
let res = HttpResponse::with_body( let res = HttpResponse::new(StatusCode::NOT_ACCEPTABLE);
StatusCode::NOT_ACCEPTABLE,
SUPPORTED_ALGORITHM_NAMES.as_str(),
);
let enc = ContentEncoding::Identity;
Either::right(ok(req.into_response(res.map_body(move |head, body| { let res: HttpResponse<AnyBody<Encoder<B>>> = res.map_body(move |head, _| {
Encoder::response(enc, head, ResponseBody::Other(body.into())) let body_bytes = Bytes::from(SUPPORTED_ALGORITHM_NAMES.as_bytes());
}))))
Encoder::response(
ContentEncoding::Identity,
head,
AnyBody::Bytes(body_bytes),
)
});
Either::right(ok(req.into_response(res)))
} }
} }
} }
@ -172,7 +178,7 @@ where
B: MessageBody, B: MessageBody,
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
{ {
type Output = Result<ServiceResponse<ResponseBody<Encoder<B>>>, Error>; type Output = Result<ServiceResponse<AnyBody<Encoder<B>>>, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project(); let this = self.project();
@ -186,7 +192,7 @@ where
}; };
Poll::Ready(Ok(resp.map_body(move |head, body| { Poll::Ready(Ok(resp.map_body(move |head, body| {
Encoder::response(enc, head, ResponseBody::Body(body)) Encoder::response(enc, head, AnyBody::Body(body))
}))) })))
} }
Err(e) => Poll::Ready(Err(e)), Err(e) => Poll::Ready(Err(e)),

View File

@ -22,7 +22,7 @@ use time::{format_description::well_known::Rfc3339, OffsetDateTime};
use crate::{ use crate::{
dev::{BodySize, MessageBody}, dev::{BodySize, MessageBody},
http::{HeaderName, StatusCode}, http::HeaderName,
service::{ServiceRequest, ServiceResponse}, service::{ServiceRequest, ServiceResponse},
Error, HttpResponse, Result, Error, HttpResponse, Result,
}; };
@ -275,10 +275,8 @@ where
}; };
if let Some(error) = res.response().error() { if let Some(error) = res.response().error() {
if res.response().head().status != StatusCode::INTERNAL_SERVER_ERROR {
debug!("Error in response: {:?}", error); debug!("Error in response: {:?}", error);
} }
}
if let Some(ref mut format) = this.format { if let Some(ref mut format) = this.format {
for unit in &mut format.0 { for unit in &mut format.0 {

View File

@ -1,7 +1,7 @@
use std::borrow::Cow; use std::borrow::Cow;
use actix_http::{ use actix_http::{
body::Body, body::AnyBody,
http::{header::IntoHeaderPair, Error as HttpError, HeaderMap, StatusCode}, http::{header::IntoHeaderPair, Error as HttpError, HeaderMap, StatusCode},
}; };
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
@ -65,7 +65,7 @@ impl Responder for HttpResponse {
} }
} }
impl Responder for actix_http::Response<Body> { impl Responder for actix_http::Response<AnyBody> {
#[inline] #[inline]
fn respond_to(self, _: &HttpRequest) -> HttpResponse { fn respond_to(self, _: &HttpRequest) -> HttpResponse {
HttpResponse::from(self) HttpResponse::from(self)
@ -232,7 +232,7 @@ pub(crate) mod tests {
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use super::*; use super::*;
use crate::dev::{Body, ResponseBody}; use crate::dev::AnyBody;
use crate::http::{header::CONTENT_TYPE, HeaderValue, StatusCode}; use crate::http::{header::CONTENT_TYPE, HeaderValue, StatusCode};
use crate::test::{init_service, TestRequest}; use crate::test::{init_service, TestRequest};
use crate::{error, web, App}; use crate::{error, web, App};
@ -254,7 +254,7 @@ pub(crate) mod tests {
let resp = srv.call(req).await.unwrap(); let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
match resp.response().body() { match resp.response().body() {
Body::Bytes(ref b) => { AnyBody::Bytes(ref b) => {
let bytes = b.clone(); let bytes = b.clone();
assert_eq!(bytes, Bytes::from_static(b"some")); assert_eq!(bytes, Bytes::from_static(b"some"));
} }
@ -264,42 +264,21 @@ pub(crate) mod tests {
pub(crate) trait BodyTest { pub(crate) trait BodyTest {
fn bin_ref(&self) -> &[u8]; fn bin_ref(&self) -> &[u8];
fn body(&self) -> &Body; fn body(&self) -> &AnyBody;
} }
impl BodyTest for Body { impl BodyTest for AnyBody {
fn bin_ref(&self) -> &[u8] { fn bin_ref(&self) -> &[u8] {
match self { match self {
Body::Bytes(ref bin) => bin, AnyBody::Bytes(ref bin) => bin,
_ => unreachable!("bug in test impl"), _ => unreachable!("bug in test impl"),
} }
} }
fn body(&self) -> &Body { fn body(&self) -> &AnyBody {
self self
} }
} }
impl BodyTest for ResponseBody<Body> {
fn bin_ref(&self) -> &[u8] {
match self {
ResponseBody::Body(ref b) => match b {
Body::Bytes(ref bin) => bin,
_ => unreachable!("bug in test impl"),
},
ResponseBody::Other(ref b) => match b {
Body::Bytes(ref bin) => bin,
_ => unreachable!("bug in test impl"),
},
}
}
fn body(&self) -> &Body {
match self {
ResponseBody::Body(ref b) => b,
ResponseBody::Other(ref b) => b,
}
}
}
#[actix_rt::test] #[actix_rt::test]
async fn test_responder() { async fn test_responder() {
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();

View File

@ -354,10 +354,10 @@ impl HttpResponseBuilder {
#[inline] #[inline]
pub fn streaming<S, E>(&mut self, stream: S) -> HttpResponse pub fn streaming<S, E>(&mut self, stream: S) -> HttpResponse
where where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static, S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Box<dyn StdError>> + 'static, E: Into<Box<dyn StdError>> + 'static,
{ {
self.body(AnyBody::from_message(BodyStream::new(stream))) self.body(AnyBody::new_boxed(BodyStream::new(stream)))
} }
/// Set a json body and generate `Response` /// Set a json body and generate `Response`
@ -387,7 +387,7 @@ impl HttpResponseBuilder {
/// `HttpResponseBuilder` can not be used after this call. /// `HttpResponseBuilder` can not be used after this call.
#[inline] #[inline]
pub fn finish(&mut self) -> HttpResponse { pub fn finish(&mut self) -> HttpResponse {
self.body(AnyBody::Empty) self.body(AnyBody::empty())
} }
/// This method construct new `HttpResponseBuilder` /// This method construct new `HttpResponseBuilder`
@ -436,7 +436,7 @@ mod tests {
use super::*; use super::*;
use crate::{ use crate::{
dev::Body, dev::AnyBody,
http::{ http::{
header::{self, HeaderValue, CONTENT_TYPE}, header::{self, HeaderValue, CONTENT_TYPE},
StatusCode, StatusCode,
@ -475,7 +475,7 @@ mod tests {
fn test_content_type() { fn test_content_type() {
let resp = HttpResponseBuilder::new(StatusCode::OK) let resp = HttpResponseBuilder::new(StatusCode::OK)
.content_type("text/plain") .content_type("text/plain")
.body(Body::Empty); .body(AnyBody::empty());
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain")
} }

View File

@ -87,13 +87,12 @@ impl HttpResponse {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::dev::Body;
use crate::http::StatusCode; use crate::http::StatusCode;
use crate::HttpResponse; use crate::HttpResponse;
#[test] #[test]
fn test_build() { fn test_build() {
let resp = HttpResponse::Ok().body(Body::Empty); let resp = HttpResponse::Ok().finish();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
} }

View File

@ -8,7 +8,7 @@ use std::{
}; };
use actix_http::{ use actix_http::{
body::{AnyBody, Body, MessageBody}, body::{AnyBody, MessageBody},
http::{header::HeaderMap, StatusCode}, http::{header::HeaderMap, StatusCode},
Extensions, Response, ResponseHead, Extensions, Response, ResponseHead,
}; };
@ -227,6 +227,9 @@ impl<B> HttpResponse<B> {
} }
} }
// TODO: into_body equivalent
// TODO: into_boxed_body
/// Extract response body /// Extract response body
pub fn into_body(self) -> B { pub fn into_body(self) -> B {
self.res.into_body() self.res.into_body()
@ -270,14 +273,14 @@ impl<B> From<HttpResponse<B>> for Response<B> {
} }
} }
// Future is only implemented for Body payload type because it's the most useful for making simple // Future is only implemented for AnyBody payload type because it's the most useful for making
// handlers without async blocks. Making it generic over all MessageBody types requires a future // simple handlers without async blocks. Making it generic over all MessageBody types requires a
// impl on Response which would cause it's body field to be, undesirably, Option<B>. // future impl on Response which would cause it's body field to be, undesirably, Option<B>.
// //
// This impl is not particularly efficient due to the Response construction and should probably // This impl is not particularly efficient due to the Response construction and should probably
// not be invoked if performance is important. Prefer an async fn/block in such cases. // not be invoked if performance is important. Prefer an async fn/block in such cases.
impl Future for HttpResponse<Body> { impl Future for HttpResponse<AnyBody> {
type Output = Result<Response<Body>, Error>; type Output = Result<Response<AnyBody>, Error>;
fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> { fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
if let Some(err) = self.error.take() { if let Some(err) = self.error.take() {

View File

@ -11,7 +11,7 @@ use futures_core::future::LocalBoxFuture;
use crate::{ use crate::{
guard::{self, Guard}, guard::{self, Guard},
handler::{Handler, HandlerService}, handler::{handler_service, Handler},
service::{ServiceRequest, ServiceResponse}, service::{ServiceRequest, ServiceResponse},
Error, FromRequest, HttpResponse, Responder, Error, FromRequest, HttpResponse, Responder,
}; };
@ -30,7 +30,7 @@ impl Route {
#[allow(clippy::new_without_default)] #[allow(clippy::new_without_default)]
pub fn new() -> Route { pub fn new() -> Route {
Route { Route {
service: boxed::factory(HandlerService::new(HttpResponse::NotFound)), service: handler_service(HttpResponse::NotFound),
guards: Rc::new(Vec::new()), guards: Rc::new(Vec::new()),
} }
} }
@ -182,7 +182,7 @@ impl Route {
R: Future + 'static, R: Future + 'static,
R::Output: Responder + 'static, R::Output: Responder + 'static,
{ {
self.service = boxed::factory(HandlerService::new(handler)); self.service = handler_service(handler);
self self
} }

Some files were not shown because too many files have changed in this diff Show More