Merge branch 'master' into service-request-mut

This commit is contained in:
Rob Ede 2021-06-19 22:51:44 +01:00 committed by GitHub
commit d6ba551526
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
127 changed files with 4151 additions and 2270 deletions

View File

@ -1,7 +1,8 @@
[alias] [alias]
chk = "hack check --workspace --all-features --tests --examples" chk = "check --workspace --all-features --tests --examples --bins"
lint = "hack --clean-per-run clippy --workspace --tests --examples" lint = "clippy --workspace --tests --examples"
ci-min = "hack check --workspace --no-default-features" ci-min = "hack check --workspace --no-default-features"
ci-min-test = "hack check --workspace --no-default-features --tests --examples" ci-min-test = "hack check --workspace --no-default-features --tests --examples"
ci-default = "hack check --workspace" ci-default = "hack check --workspace"
ci-full = "check --workspace --bins --examples --tests" ci-full = "check --workspace --bins --examples --tests"
ci-test = "test --workspace --all-features --no-fail-fast"

View File

@ -86,7 +86,7 @@ jobs:
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: test command: test
args: -v --workspace --all-features --no-fail-fast -- --nocapture args: --workspace --all-features --no-fail-fast -- --nocapture
--skip=test_h2_content_length --skip=test_h2_content_length
--skip=test_reading_deflate_encoding_large_random_rustls --skip=test_reading_deflate_encoding_large_random_rustls
@ -123,5 +123,5 @@ jobs:
- name: Clear the cargo caches - name: Clear the cargo caches
run: | run: |
cargo install cargo-cache --no-default-features --features ci-autoclean cargo install cargo-cache --version 0.6.2 --no-default-features --features ci-autoclean
cargo-cache cargo-cache

View File

@ -1,6 +1,36 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
### Changed
* Change compression algorithm features flags. [#2250]
[#2250]: https://github.com/actix/actix-web/pull/2250
## 4.0.0-beta.7 - 2021-06-17
### Added
* `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200]
### Changed
* Adjusted default JSON payload limit to 2MB (from 32kb) and included size and limits in the `JsonPayloadError::Overflow` error variant. [#2162]
[#2162]: (https://github.com/actix/actix-web/pull/2162)
* `ServiceResponse::error_response` now uses body type of `Body`. [#2201]
* `ServiceResponse::checked_expr` now returns a `Result`. [#2201]
* Update `language-tags` to `0.3`.
* `ServiceResponse::take_body`. [#2201]
* `ServiceResponse::map_body` closure receives and returns `B` instead of `ResponseBody<B>` types. [#2201]
* All error trait bounds in server service builders have changed from `Into<Error>` to `Into<Response<AnyBody>>`. [#2253]
* All error trait bounds in message body and stream impls changed from `Into<Error>` to `Into<Box<dyn std::error::Error>>`. [#2253]
* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
* `middleware::normalize` now will not try to normalize URIs with no valid path [#2246]
### Removed
* `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201]
[#2200]: https://github.com/actix/actix-web/pull/2200
[#2201]: https://github.com/actix/actix-web/pull/2201
[#2253]: https://github.com/actix/actix-web/pull/2253
[#2246]: https://github.com/actix/actix-web/pull/2246
## 4.0.0-beta.6 - 2021-04-17 ## 4.0.0-beta.6 - 2021-04-17

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.0.0-beta.6" version = "4.0.0-beta.7"
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"]
@ -17,7 +17,7 @@ edition = "2018"
[package.metadata.docs.rs] [package.metadata.docs.rs]
# features that docs.rs will build with # features that docs.rs will build with
features = ["openssl", "rustls", "compress", "cookies", "secure-cookies"] features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"]
[lib] [lib]
name = "actix_web" name = "actix_web"
@ -39,10 +39,14 @@ members = [
# resolver = "2" # resolver = "2"
[features] [features]
default = ["compress", "cookies"] default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
# content-encoding support # Brotli algorithm content-encoding support
compress = ["actix-http/compress"] compress-brotli = ["actix-http/compress-brotli", "__compress"]
# Gzip and deflate algorithms content-encoding support
compress-gzip = ["actix-http/compress-gzip", "__compress"]
# Zstd algorithm content-encoding support
compress-zstd = ["actix-http/compress-zstd", "__compress"]
# support for cookies # support for cookies
cookies = ["cookie"] cookies = ["cookie"]
@ -56,9 +60,13 @@ openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
# rustls # rustls
rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"]
# Internal (PRIVATE!) features used to aid testing and cheking feature status.
# Don't rely on these whatsoever. They may disappear at anytime.
__compress = []
[dependencies] [dependencies]
actix-codec = "0.4.0-beta.1" actix-codec = "0.4.0"
actix-macros = "0.2.0" actix-macros = "0.2.1"
actix-router = "0.2.7" actix-router = "0.2.7"
actix-rt = "2.2" actix-rt = "2.2"
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-beta.3"
@ -67,10 +75,11 @@ actix-utils = "3.0.0"
actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true } actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true }
actix-web-codegen = "0.5.0-beta.2" actix-web-codegen = "0.5.0-beta.2"
actix-http = "3.0.0-beta.6" actix-http = "3.0.0-beta.7"
ahash = "0.7" ahash = "0.7"
bytes = "1" bytes = "1"
cfg-if = "1"
cookie = { version = "0.15", features = ["percent-encode"], optional = true } cookie = { version = "0.15", features = ["percent-encode"], optional = true }
derive_more = "0.99.5" derive_more = "0.99.5"
either = "1.5.3" either = "1.5.3"
@ -78,10 +87,11 @@ encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }
itoa = "0.4" itoa = "0.4"
language-tags = "0.2" language-tags = "0.3"
once_cell = "1.5" once_cell = "1.5"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
paste = "1"
pin-project = "1.0.0" pin-project = "1.0.0"
regex = "1.4" regex = "1.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
@ -94,12 +104,13 @@ url = "2.1"
[dev-dependencies] [dev-dependencies]
actix-test = { version = "0.1.0-beta.2", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.2", features = ["openssl", "rustls"] }
awc = { version = "3.0.0-beta.5", features = ["openssl"] } awc = { version = "3.0.0-beta.6", features = ["openssl"] }
brotli2 = "0.3.2" brotli2 = "0.3.2"
criterion = "0.3" criterion = "0.3"
env_logger = "0.8" env_logger = "0.8"
flate2 = "1.0.13" flate2 = "1.0.13"
zstd = "0.7"
rand = "0.8" rand = "0.8"
rcgen = "0.8" rcgen = "0.8"
serde_derive = "1.0" serde_derive = "1.0"
@ -124,15 +135,15 @@ awc = { path = "awc" }
[[test]] [[test]]
name = "test_server" name = "test_server"
required-features = ["compress", "cookies"] required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
[[example]] [[example]]
name = "basic" name = "basic"
required-features = ["compress"] required-features = ["compress-gzip"]
[[example]] [[example]]
name = "uds" name = "uds"
required-features = ["compress"] required-features = ["compress-gzip"]
[[example]] [[example]]
name = "on_connect" name = "on_connect"

View File

@ -10,6 +10,18 @@
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`. Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
* Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd).
By default all compression algorithms are enabled.
To select algorithm you want to include with `middleware::Compress` use following flags:
- `compress-brotli`
- `compress-gzip`
- `compress-zstd`
If you have set in your `Cargo.toml` dedicated `actix-web` features and you still want
to have compression enabled. Please change features selection like bellow:
Before: `"compress"`
After: `"compress-brotli", "compress-gzip", "compress-zstd"`
## 3.0.0 ## 3.0.0

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.5)](https://docs.rs/actix-web/4.0.0-beta.5) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.7)](https://docs.rs/actix-web/4.0.0-beta.7)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.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.5/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.5) [![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.7)
<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)
@ -25,13 +25,13 @@
* Streaming and pipelining * Streaming and pipelining
* Keep-alive and slow requests handling * Keep-alive and slow requests handling
* Client/server [WebSockets](https://actix.rs/docs/websockets/) support * Client/server [WebSockets](https://actix.rs/docs/websockets/) support
* Transparent content compression/decompression (br, gzip, deflate) * Transparent content compression/decompression (br, gzip, deflate, zstd)
* Powerful [request routing](https://actix.rs/docs/url-dispatch/) * Powerful [request routing](https://actix.rs/docs/url-dispatch/)
* Multipart streams * Multipart streams
* Static assets * Static assets
* SSL support using OpenSSL or Rustls * SSL support using OpenSSL or Rustls
* Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
* Includes an async [HTTP client](https://docs.rs/actix-web/latest/actix_web/client/index.html) * Includes an async [HTTP client](https://docs.rs/awc/)
* Runs on stable Rust 1.46+ * Runs on stable Rust 1.46+
## Documentation ## Documentation

View File

@ -1,16 +1,28 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.6.0-beta.5 - 2021-06-17
* `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135] * `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135]
* For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156] * For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156]
* `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225]
* `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257]
* `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228]
[#2135]: https://github.com/actix/actix-web/pull/2135 [#2135]: https://github.com/actix/actix-web/pull/2135
[#2156]: https://github.com/actix/actix-web/pull/2156 [#2156]: https://github.com/actix/actix-web/pull/2156
[#2225]: https://github.com/actix/actix-web/pull/2225
[#2257]: https://github.com/actix/actix-web/pull/2257
[#2228]: https://github.com/actix/actix-web/pull/2228
## 0.6.0-beta.4 - 2021-04-02 ## 0.6.0-beta.4 - 2021-04-02
* No notable changes. * No notable changes.
* Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046]
[#2046]: https://github.com/actix/actix-web/pull/2046
## 0.6.0-beta.3 - 2021-03-09 ## 0.6.0-beta.3 - 2021-03-09
* No notable changes. * No notable changes.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.0-beta.4" version = "0.6.0-beta.5"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Static file serving for Actix Web" description = "Static file serving for Actix Web"
readme = "README.md" readme = "README.md"
@ -17,7 +17,8 @@ name = "actix_files"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
actix-web = { version = "4.0.0-beta.6", default-features = false } actix-web = { version = "4.0.0-beta.7", default-features = false }
actix-http = "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"
@ -34,5 +35,5 @@ percent-encoding = "2.1"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-web = "4.0.0-beta.6" actix-web = "4.0.0-beta.7"
actix-test = "0.1.0-beta.2" actix-test = "0.1.0-beta.2"

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.4)](https://docs.rs/actix-files/0.6.0-beta.4) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.5)](https://docs.rs/actix-files/0.6.0-beta.5)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.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.4/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.4) [![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.5/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.5)
[![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)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

View File

@ -37,7 +37,8 @@ pub struct Files {
renderer: Rc<DirectoryRenderer>, renderer: Rc<DirectoryRenderer>,
mime_override: Option<Rc<MimeOverride>>, mime_override: Option<Rc<MimeOverride>>,
file_flags: named::Flags, file_flags: named::Flags,
guards: Option<Rc<dyn Guard>>, use_guards: Option<Rc<dyn Guard>>,
guards: Vec<Rc<dyn Guard>>,
hidden_files: bool, hidden_files: bool,
} }
@ -59,6 +60,7 @@ impl Clone for Files {
file_flags: self.file_flags, file_flags: self.file_flags,
path: self.path.clone(), path: self.path.clone(),
mime_override: self.mime_override.clone(), mime_override: self.mime_override.clone(),
use_guards: self.use_guards.clone(),
guards: self.guards.clone(), guards: self.guards.clone(),
hidden_files: self.hidden_files, hidden_files: self.hidden_files,
} }
@ -80,10 +82,9 @@ impl Files {
/// If the mount path is set as the root path `/`, services registered after this one will /// If the mount path is set as the root path `/`, services registered after this one will
/// be inaccessible. Register more specific handlers and services first. /// be inaccessible. Register more specific handlers and services first.
/// ///
/// `Files` uses a threadpool for blocking filesystem operations. By default, the pool uses a /// `Files` utilizes the existing Tokio thread-pool for blocking filesystem operations.
/// max number of threads equal to `512 * HttpServer::worker`. Real time thread count are /// The number of running threads is adjusted over time as needed, up to a maximum of 512 times
/// adjusted with work load. More threads would spawn when need and threads goes idle for a /// the number of server [workers](actix_web::HttpServer::workers), by default.
/// period of time would be de-spawned.
pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files { pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
let orig_dir = serve_from.into(); let orig_dir = serve_from.into();
let dir = match orig_dir.canonicalize() { let dir = match orig_dir.canonicalize() {
@ -104,7 +105,8 @@ impl Files {
renderer: Rc::new(directory_listing), renderer: Rc::new(directory_listing),
mime_override: None, mime_override: None,
file_flags: named::Flags::default(), file_flags: named::Flags::default(),
guards: None, use_guards: None,
guards: Vec::new(),
hidden_files: false, hidden_files: false,
} }
} }
@ -112,6 +114,9 @@ impl Files {
/// Show files listing for directories. /// Show files listing for directories.
/// ///
/// By default show files listing is disabled. /// By default show files listing is disabled.
///
/// When used with [`Files::index_file()`], files listing is shown as a fallback
/// when the index file is not found.
pub fn show_files_listing(mut self) -> Self { pub fn show_files_listing(mut self) -> Self {
self.show_index = true; self.show_index = true;
self self
@ -146,8 +151,11 @@ impl Files {
/// Set index file /// Set index file
/// ///
/// Shows specific index file for directory "/" instead of /// Shows specific index file for directories instead of
/// showing files listing. /// showing files listing.
///
/// If the index file is not found, files listing is shown as a fallback if
/// [`Files::show_files_listing()`] is set.
pub fn index_file<T: Into<String>>(mut self, index: T) -> Self { pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
self.index = Some(index.into()); self.index = Some(index.into());
self self
@ -156,7 +164,6 @@ impl Files {
/// Specifies whether to use ETag or not. /// Specifies whether to use ETag or not.
/// ///
/// Default is true. /// Default is true.
#[inline]
pub fn use_etag(mut self, value: bool) -> Self { pub fn use_etag(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::ETAG, value); self.file_flags.set(named::Flags::ETAG, value);
self self
@ -165,7 +172,6 @@ impl Files {
/// Specifies whether to use Last-Modified or not. /// Specifies whether to use Last-Modified or not.
/// ///
/// Default is true. /// Default is true.
#[inline]
pub fn use_last_modified(mut self, value: bool) -> Self { pub fn use_last_modified(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::LAST_MD, value); self.file_flags.set(named::Flags::LAST_MD, value);
self self
@ -174,25 +180,55 @@ impl Files {
/// Specifies whether text responses should signal a UTF-8 encoding. /// Specifies whether text responses should signal a UTF-8 encoding.
/// ///
/// Default is false (but will default to true in a future version). /// Default is false (but will default to true in a future version).
#[inline]
pub fn prefer_utf8(mut self, value: bool) -> Self { pub fn prefer_utf8(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::PREFER_UTF8, value); self.file_flags.set(named::Flags::PREFER_UTF8, value);
self self
} }
/// Specifies custom guards to use for directory listings and files. /// Adds a routing guard.
/// ///
/// Default behaviour allows GET and HEAD. /// Use this to allow multiple chained file services that respond to strictly different
#[inline] /// properties of a request. Due to the way routing works, if a guard check returns true and the
pub fn use_guards<G: Guard + 'static>(mut self, guards: G) -> Self { /// request starts being handled by the file service, it will not be able to back-out and try
self.guards = Some(Rc::new(guards)); /// the next service, you will simply get a 404 (or 405) error response.
///
/// To allow `POST` requests to retrieve files, see [`Files::use_guards`].
///
/// # Examples
/// ```
/// use actix_web::{guard::Header, App};
/// use actix_files::Files;
///
/// App::new().service(
/// Files::new("/","/my/site/files")
/// .guard(Header("Host", "example.com"))
/// );
/// ```
pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
self.guards.push(Rc::new(guard));
self self
} }
/// Specifies guard to check before fetching directory listings or files.
///
/// Note that this guard has no effect on routing; it's main use is to guard on the request's
/// method just before serving the file, only allowing `GET` and `HEAD` requests by default.
/// See [`Files::guard`] for routing guards.
pub fn method_guard<G: Guard + 'static>(mut self, guard: G) -> Self {
self.use_guards = Some(Rc::new(guard));
self
}
#[doc(hidden)]
#[deprecated(since = "0.6.0", note = "Renamed to `method_guard`.")]
/// See [`Files::method_guard`].
pub fn use_guards<G: Guard + 'static>(self, guard: G) -> Self {
self.method_guard(guard)
}
/// Disable `Content-Disposition` header. /// Disable `Content-Disposition` header.
/// ///
/// By default Content-Disposition` header is enabled. /// By default Content-Disposition` header is enabled.
#[inline]
pub fn disable_content_disposition(mut self) -> Self { pub fn disable_content_disposition(mut self) -> Self {
self.file_flags.remove(named::Flags::CONTENT_DISPOSITION); self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
self self
@ -200,8 +236,9 @@ impl Files {
/// Sets default handler which is used when no matched file could be found. /// Sets default handler which is used when no matched file could be found.
/// ///
/// For example, you could set a fall back static file handler: /// # Examples
/// ```rust /// Setting a fallback static file handler:
/// ```
/// use actix_files::{Files, NamedFile}; /// use actix_files::{Files, NamedFile};
/// ///
/// # fn run() -> Result<(), actix_web::Error> { /// # fn run() -> Result<(), actix_web::Error> {
@ -230,7 +267,6 @@ impl Files {
} }
/// Enables serving hidden files and directories, allowing a leading dots in url fragments. /// Enables serving hidden files and directories, allowing a leading dots in url fragments.
#[inline]
pub fn use_hidden_files(mut self) -> Self { pub fn use_hidden_files(mut self) -> Self {
self.hidden_files = true; self.hidden_files = true;
self self
@ -238,7 +274,19 @@ impl Files {
} }
impl HttpServiceFactory for Files { impl HttpServiceFactory for Files {
fn register(self, config: &mut AppService) { fn register(mut self, config: &mut AppService) {
let guards = if self.guards.is_empty() {
None
} else {
let guards = std::mem::take(&mut self.guards);
Some(
guards
.into_iter()
.map(|guard| -> Box<dyn Guard> { Box::new(guard) })
.collect::<Vec<_>>(),
)
};
if self.default.borrow().is_none() { if self.default.borrow().is_none() {
*self.default.borrow_mut() = Some(config.default_service()); *self.default.borrow_mut() = Some(config.default_service());
} }
@ -249,7 +297,7 @@ impl HttpServiceFactory for Files {
ResourceDef::prefix(&self.path) ResourceDef::prefix(&self.path)
}; };
config.register_service(rdef, None, self, None) config.register_service(rdef, guards, self, None)
} }
} }
@ -271,7 +319,7 @@ impl ServiceFactory<ServiceRequest> for Files {
renderer: self.renderer.clone(), renderer: self.renderer.clone(),
mime_override: self.mime_override.clone(), mime_override: self.mime_override.clone(),
file_flags: self.file_flags, file_flags: self.file_flags,
guards: self.guards.clone(), guards: self.use_guards.clone(),
hidden_files: self.hidden_files, hidden_files: self.hidden_files,
}; };

View File

@ -279,6 +279,22 @@ mod tests {
); );
} }
#[actix_rt::test]
async fn test_named_file_javascript() {
let file = NamedFile::open("tests/test.js").unwrap();
let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).await.unwrap();
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"application/javascript"
);
assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"inline; filename=\"test.js\""
);
}
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_image_attachment() { async fn test_named_file_image_attachment() {
let cd = ContentDisposition { let cd = ContentDisposition {
@ -532,7 +548,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_files_guards() { async fn test_files_guards() {
let srv = test::init_service( let srv = test::init_service(
App::new().service(Files::new("/", ".").use_guards(guard::Post())), App::new().service(Files::new("/", ".").method_guard(guard::Post())),
) )
.await; .await;
@ -632,7 +648,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_redirect_to_slash_directory() { async fn test_redirect_to_slash_directory() {
// should not redirect if no index // should not redirect if no index and files listing is disabled
let srv = test::init_service( let srv = test::init_service(
App::new().service(Files::new("/", ".").redirect_to_slash_directory()), App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
) )
@ -654,6 +670,19 @@ mod tests {
let resp = test::call_service(&srv, req).await; let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::FOUND);
// should redirect if files listing is enabled
let srv = test::init_service(
App::new().service(
Files::new("/", ".")
.show_files_listing()
.redirect_to_slash_directory(),
),
)
.await;
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
// should not redirect if the path is wrong // should not redirect if the path is wrong
let req = TestRequest::with_uri("/not_existing").to_request(); let req = TestRequest::with_uri("/not_existing").to_request();
let resp = test::call_service(&srv, req).await; let resp = test::call_service(&srv, req).await;
@ -843,4 +872,33 @@ mod tests {
"inline; filename=\"symlink-test.png\"" "inline; filename=\"symlink-test.png\""
); );
} }
#[actix_rt::test]
async fn test_index_with_show_files_listing() {
let service = Files::new(".", ".")
.index_file("lib.rs")
.show_files_listing()
.new_service(())
.await
.unwrap();
// Serve the index if exists
let req = TestRequest::default().uri("/src").to_srv_request();
let resp = test::call_service(&service, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/x-rust"
);
// Show files listing, otherwise.
let req = TestRequest::default().uri("/tests").to_srv_request();
let resp = test::call_service(&service, req).await;
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/html; charset=utf-8"
);
let bytes = test::read_body(resp).await;
assert!(format!("{:?}", bytes).contains("/tests/test.png"));
}
} }

View File

@ -120,6 +120,11 @@ impl NamedFile {
let disposition = match ct.type_() { let disposition = match ct.type_() {
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
mime::APPLICATION => match ct.subtype() {
mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
name if name == "wasm" => DispositionType::Inline,
_ => DispositionType::Attachment,
},
_ => DispositionType::Attachment, _ => DispositionType::Attachment,
}; };
@ -213,9 +218,11 @@ impl NamedFile {
/// Set the Content-Disposition for serving this file. This allows /// Set the Content-Disposition for serving this file. This allows
/// changing the inline/attachment disposition as well as the filename /// changing the inline/attachment disposition as well as the filename
/// sent to the peer. By default the disposition is `inline` for text, /// sent to the peer.
/// image, and video content types, and `attachment` otherwise, and ///
/// the filename is taken from the path provided in the `open` method /// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and
/// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise,
/// and the filename is taken from the path provided in the `open` method
/// after converting it to UTF-8 using. /// after converting it to UTF-8 using.
/// [`std::ffi::OsStr::to_string_lossy`] /// [`std::ffi::OsStr::to_string_lossy`]
#[inline] #[inline]
@ -235,6 +242,8 @@ impl NamedFile {
} }
/// Set content encoding for serving this file /// Set content encoding for serving this file
///
/// Must be used with [`actix_web::middleware::Compress`] to take effect.
#[inline] #[inline]
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
self.encoding = Some(enc); self.encoding = Some(enc);

View File

@ -89,37 +89,33 @@ impl Service<ServiceRequest> for FilesService {
} }
if path.is_dir() { if path.is_dir() {
if let Some(ref redir_index) = self.index { if self.redirect_to_slash
if self.redirect_to_slash && !req.path().ends_with('/') { && !req.path().ends_with('/')
let redirect_to = format!("{}/", req.path()); && (self.index.is_some() || self.show_index)
{
let redirect_to = format!("{}/", req.path());
return Box::pin(ok(req.into_response( return Box::pin(ok(req.into_response(
HttpResponse::Found() HttpResponse::Found()
.insert_header((header::LOCATION, redirect_to)) .insert_header((header::LOCATION, redirect_to))
.body("") .finish(),
.into_body(), )));
))); }
let serve_named_file = |req: ServiceRequest, mut named_file: NamedFile| {
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 path = path.join(redir_index); let (req, _) = req.into_parts();
let res = named_file.into_response(&req);
Box::pin(ok(ServiceResponse::new(req, res)))
};
match NamedFile::open(path) { let show_index = |req: ServiceRequest| {
Ok(mut named_file) => { let dir = Directory::new(self.directory.clone(), path.clone());
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);
Box::pin(ok(ServiceResponse::new(req, res)))
}
Err(err) => self.handle_err(err, req),
}
} else if self.show_index {
let dir = Directory::new(self.directory.clone(), path);
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
let x = (self.renderer)(&dir, &req); let x = (self.renderer)(&dir, &req);
@ -128,11 +124,19 @@ impl Service<ServiceRequest> for FilesService {
Ok(resp) => ok(resp), Ok(resp) => ok(resp),
Err(err) => ok(ServiceResponse::from_err(err, req)), Err(err) => ok(ServiceResponse::from_err(err, req)),
}) })
} else { };
Box::pin(ok(ServiceResponse::from_err(
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(path) {

View File

@ -0,0 +1 @@
first

View File

@ -0,0 +1 @@
second

View File

@ -0,0 +1,36 @@
use actix_files::Files;
use actix_web::{
guard::Host,
http::StatusCode,
test::{self, TestRequest},
App,
};
use bytes::Bytes;
#[actix_rt::test]
async fn test_guard_filter() {
let srv = test::init_service(
App::new()
.service(Files::new("/", "./tests/fixtures/guards/first").guard(Host("first.com")))
.service(
Files::new("/", "./tests/fixtures/guards/second").guard(Host("second.com")),
),
)
.await;
let req = TestRequest::with_uri("/index.txt")
.append_header(("Host", "first.com"))
.to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(test::read_body(res).await, Bytes::from("first"));
let req = TestRequest::with_uri("/index.txt")
.append_header(("Host", "second.com"))
.to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(test::read_body(res).await, Bytes::from("second"));
}

View File

@ -0,0 +1 @@
// this file is empty.

View File

@ -30,12 +30,12 @@ openssl = ["tls-openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-service = "2.0.0" actix-service = "2.0.0"
actix-codec = "0.4.0-beta.1" actix-codec = "0.4.0"
actix-tls = "3.0.0-beta.5" actix-tls = "3.0.0-beta.5"
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.3"
awc = { version = "3.0.0-beta.5", default-features = false } awc = { version = "3.0.0-beta.6", default-features = false }
base64 = "0.13" base64 = "0.13"
bytes = "1" bytes = "1"
@ -51,5 +51,5 @@ time = { version = "0.2.23", default-features = false, features = ["std"] }
tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0-beta.6", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-beta.7", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-beta.6" actix-http = "3.0.0-beta.7"

View File

@ -1,19 +1,55 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
### Changed
* Change compression algorithm features flags. [#2250]
[#2250]: https://github.com/actix/actix-web/pull/2250
## 3.0.0-beta.7 - 2021-06-17
### Added ### Added
* Alias `body::Body` as `body::AnyBody`. [#2215]
* `BoxAnyBody`: a boxed message body with boxed errors. [#2183]
* Re-export `http` crate's `Error` type as `error::HttpError`. [#2171] * Re-export `http` crate's `Error` type as `error::HttpError`. [#2171]
* Re-export `StatusCode`, `Method`, `Version` and `Uri` at the crate root. [#2171] * Re-export `StatusCode`, `Method`, `Version` and `Uri` at the crate root. [#2171]
* Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171] * Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171]
* `Response::into_body` that consumes response and returns body type. [#2201]
* `impl Default` for `Response`. [#2201]
* Add zstd support for `ContentEncoding`. [#2244]
### Changed ### Changed
* The `MessageBody` trait now has an associated `Error` type. [#2183]
* All error trait bounds in server service builders have changed from `Into<Error>` to `Into<Response<AnyBody>>`. [#2253]
* All error trait bounds in message body and stream impls changed from `Into<Error>` to `Into<Box<dyn std::error::Error>>`. [#2253]
* Places in `Response` where `ResponseBody<B>` was received or returned now simply use `B`. [#2201]
* `header` mod is now public. [#2171] * `header` mod is now public. [#2171]
* `uri` mod is now public. [#2171] * `uri` mod is now public. [#2171]
* Update `language-tags` to `0.3`.
* Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201]
* `ResponseBuilder::message_body` now returns a `Result`. [#2201]
* Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253]
* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
### Removed ### Removed
* Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171] * Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171]
* Down-casting for `MessageBody` types. [#2183]
* `error::Result` alias. [#2201]
* Error field from `Response` and `Response::error`. [#2205]
* `impl Future` for `Response`. [#2201]
* `Response::take_body` and old `Response::into_body` method that casted body type. [#2201]
* `InternalError` and all the error types it constructed. [#2215]
* Conversion (`impl Into`) of `Response<Body>` and `ResponseBuilder` to `Error`. [#2215]
[#2171]: https://github.com/actix/actix-web/pull/2171 [#2171]: https://github.com/actix/actix-web/pull/2171
[#2183]: https://github.com/actix/actix-web/pull/2183
[#2196]: https://github.com/actix/actix-web/pull/2196
[#2201]: https://github.com/actix/actix-web/pull/2201
[#2205]: https://github.com/actix/actix-web/pull/2205
[#2215]: https://github.com/actix/actix-web/pull/2215
[#2253]: https://github.com/actix/actix-web/pull/2253
[#2244]: https://github.com/actix/actix-web/pull/2244
## 3.0.0-beta.6 - 2021-04-17 ## 3.0.0-beta.6 - 2021-04-17

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.0.0-beta.6" version = "3.0.0-beta.7"
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"
readme = "README.md" readme = "README.md"
@ -16,7 +16,7 @@ edition = "2018"
[package.metadata.docs.rs] [package.metadata.docs.rs]
# features that docs.rs will build with # features that docs.rs will build with
features = ["openssl", "rustls", "compress"] features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"]
[lib] [lib]
name = "actix_http" name = "actix_http"
@ -32,14 +32,20 @@ openssl = ["actix-tls/openssl"]
rustls = ["actix-tls/rustls"] rustls = ["actix-tls/rustls"]
# enable compression support # enable compression support
compress = ["flate2", "brotli2"] compress-brotli = ["brotli2", "__compress"]
compress-gzip = ["flate2", "__compress"]
compress-zstd = ["zstd", "__compress"]
# trust-dns as client dns resolver # trust-dns as client dns resolver
trust-dns = ["trust-dns-resolver"] trust-dns = ["trust-dns-resolver"]
# Internal (PRIVATE!) features used to aid testing and cheking feature status.
# Don't rely on these whatsoever. They may disappear at anytime.
__compress = []
[dependencies] [dependencies]
actix-service = "2.0.0" actix-service = "2.0.0"
actix-codec = "0.4.0-beta.1" actix-codec = "0.4.0"
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.5", features = ["accept", "connect"] } actix-tls = { version = "3.0.0-beta.5", features = ["accept", "connect"] }
@ -57,12 +63,11 @@ h2 = "0.3.1"
http = "0.2.2" http = "0.2.2"
httparse = "1.3" httparse = "1.3"
itoa = "0.4" itoa = "0.4"
language-tags = "0.2" language-tags = "0.3"
local-channel = "0.1" local-channel = "0.1"
once_cell = "1.5" once_cell = "1.5"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
paste = "1"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project = "1.0.0" pin-project = "1.0.0"
pin-project-lite = "0.2" pin-project-lite = "0.2"
@ -77,6 +82,7 @@ tokio = { version = "1.2", features = ["sync"] }
# 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 }
trust-dns-resolver = { version = "0.20.0", optional = true } trust-dns-resolver = { version = "0.20.0", optional = true }
@ -84,13 +90,15 @@ trust-dns-resolver = { version = "0.20.0", optional = true }
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-beta.3"
actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] }
actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] } actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] }
criterion = "0.3" async-stream = "0.3"
criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.8" env_logger = "0.8"
rcgen = "0.8" rcgen = "0.8"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tls-openssl = { version = "0.10", package = "openssl" } tls-openssl = { version = "0.10", package = "openssl" }
tls-rustls = { version = "0.19", package = "rustls" } tls-rustls = { version = "0.19", package = "rustls" }
webpki = { version = "0.21.0" }
[[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.6)](https://docs.rs/actix-http/3.0.0-beta.6) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.7)](https://docs.rs/actix-http/3.0.0-beta.7)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.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.6/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.6) [![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.7)
[![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)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

View File

@ -1,19 +1,17 @@
use std::{env, io}; use std::io;
use actix_http::{http::StatusCode, Error, HttpService, Request, Response}; use actix_http::{http::StatusCode, 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 _;
use http::header::HeaderValue; use http::header::HeaderValue;
use log::info;
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "echo=info"); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
env_logger::init();
Server::build() Server::build()
.bind("echo", "127.0.0.1:8080", || { .bind("echo", ("127.0.0.1", 8080), || {
HttpService::build() HttpService::build()
.client_timeout(1000) .client_timeout(1000)
.client_disconnect(1000) .client_disconnect(1000)
@ -23,7 +21,8 @@ async fn main() -> io::Result<()> {
body.extend_from_slice(&item?); body.extend_from_slice(&item?);
} }
info!("request body: {:?}", body); log::info!("request body: {:?}", body);
Ok::<_, Error>( Ok::<_, Error>(
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.insert_header(( .insert_header((

View File

@ -1,11 +1,10 @@
use std::{env, io}; use std::io;
use actix_http::{body::Body, http::HeaderValue, http::StatusCode}; use actix_http::{body::Body, 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 _;
use log::info;
async fn handle_request(mut req: Request) -> Result<Response<Body>, Error> { async fn handle_request(mut req: Request) -> Result<Response<Body>, Error> {
let mut body = BytesMut::new(); let mut body = BytesMut::new();
@ -13,7 +12,8 @@ async fn handle_request(mut req: Request) -> Result<Response<Body>, Error> {
body.extend_from_slice(&item?) body.extend_from_slice(&item?)
} }
info!("request body: {:?}", body); log::info!("request body: {:?}", body);
Ok(Response::build(StatusCode::OK) Ok(Response::build(StatusCode::OK)
.insert_header(("x-head", HeaderValue::from_static("dummy value!"))) .insert_header(("x-head", HeaderValue::from_static("dummy value!")))
.body(body)) .body(body))
@ -21,11 +21,10 @@ async fn handle_request(mut req: Request) -> Result<Response<Body>, Error> {
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "echo=info"); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
env_logger::init();
Server::build() Server::build()
.bind("echo", "127.0.0.1:8080", || { .bind("echo", ("127.0.0.1", 8080), || {
HttpService::build().finish(handle_request).tcp() HttpService::build().finish(handle_request).tcp()
})? })?
.run() .run()

View File

@ -1,29 +1,28 @@
use std::{env, io}; use std::{convert::Infallible, io};
use actix_http::{http::StatusCode, HttpService, Response}; use actix_http::{http::StatusCode, HttpService, Response};
use actix_server::Server; use actix_server::Server;
use actix_utils::future;
use http::header::HeaderValue; use http::header::HeaderValue;
use log::info;
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "hello_world=info"); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
env_logger::init();
Server::build() Server::build()
.bind("hello-world", "127.0.0.1:8080", || { .bind("hello-world", ("127.0.0.1", 8080), || {
HttpService::build() HttpService::build()
.client_timeout(1000) .client_timeout(1000)
.client_disconnect(1000) .client_disconnect(1000)
.finish(|_req| { .finish(|req| async move {
info!("{:?}", _req); log::info!("{:?}", req);
let mut res = Response::build(StatusCode::OK); let mut res = Response::build(StatusCode::OK);
res.insert_header(( res.insert_header((
"x-head", "x-head",
HeaderValue::from_static("dummy value!"), HeaderValue::from_static("dummy value!"),
)); ));
future::ok::<_, ()>(res.body("Hello world!"))
Ok::<_, Infallible>(res.body("Hello world!"))
}) })
.tcp() .tcp()
})? })?

View File

@ -0,0 +1,40 @@
//! Example showing response body (chunked) stream erroring.
//!
//! Test using `nc` or `curl`.
//! ```sh
//! $ curl -vN 127.0.0.1:8080
//! $ echo 'GET / HTTP/1.1\n\n' | nc 127.0.0.1 8080
//! ```
use std::{convert::Infallible, io, time::Duration};
use actix_http::{body::BodyStream, HttpService, Response};
use actix_server::Server;
use async_stream::stream;
use bytes::Bytes;
#[actix_rt::main]
async fn main() -> io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
Server::build()
.bind("streaming-error", ("127.0.0.1", 8080), || {
HttpService::build()
.finish(|req| async move {
log::info!("{:?}", req);
let res = Response::ok();
Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! {
yield Ok(Bytes::from("123"));
yield Ok(Bytes::from("456"));
actix_rt::time::sleep(Duration::from_millis(1000)).await;
yield Err(io::Error::new(io::ErrorKind::Other, ""));
})))
})
.tcp()
})?
.run()
.await
}

View File

@ -4,7 +4,7 @@
extern crate tls_rustls as rustls; extern crate tls_rustls as rustls;
use std::{ use std::{
env, io, io,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
time::Duration, time::Duration,
@ -20,8 +20,7 @@ use futures_core::{ready, Stream};
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "actix=info,h2_ws=info"); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
env_logger::init();
Server::build() Server::build()
.bind("tcp", ("127.0.0.1", 8080), || { .bind("tcp", ("127.0.0.1", 8080), || {
@ -41,7 +40,7 @@ async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error>
// handshake will always fail under HTTP/2 // handshake will always fail under HTTP/2
log::info!("responding"); log::info!("responding");
Ok(res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))) Ok(res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))?)
} }
struct Heartbeat { struct Heartbeat {

View File

@ -1,20 +1,22 @@
use std::{ use std::{
borrow::Cow, borrow::Cow,
error::Error as StdError,
fmt, mem, fmt, mem,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::Stream; use futures_core::{ready, Stream};
use crate::error::Error; use crate::error::Error;
use super::{BodySize, BodyStream, MessageBody, SizedStream}; use super::{BodySize, BodyStream, MessageBody, MessageBodyMapErr, SizedStream};
pub type Body = AnyBody;
/// Represents various types of HTTP message body. /// Represents various types of HTTP message body.
// #[deprecated(since = "4.0.0", note = "Use body types directly.")] pub enum AnyBody {
pub enum Body {
/// Empty response. `Content-Length` header is not set. /// Empty response. `Content-Length` header is not set.
None, None,
@ -25,39 +27,45 @@ pub enum Body {
Bytes(Bytes), Bytes(Bytes),
/// Generic message body. /// Generic message body.
Message(Pin<Box<dyn MessageBody>>), Message(BoxAnyBody),
} }
impl Body { impl AnyBody {
/// Create body from slice (copy) /// Create body from slice (copy)
pub fn from_slice(s: &[u8]) -> Body { pub fn from_slice(s: &[u8]) -> Self {
Body::Bytes(Bytes::copy_from_slice(s)) Self::Bytes(Bytes::copy_from_slice(s))
} }
/// Create body from generic message body. /// Create body from generic message body.
pub fn from_message<B: MessageBody + 'static>(body: B) -> Body { pub fn from_message<B>(body: B) -> Self
Body::Message(Box::pin(body)) where
B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError + 'static>>,
{
Self::Message(BoxAnyBody::from_body(body))
} }
} }
impl MessageBody for Body { impl MessageBody for AnyBody {
type Error = Error;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
match self { match self {
Body::None => BodySize::None, AnyBody::None => BodySize::None,
Body::Empty => BodySize::Empty, AnyBody::Empty => BodySize::Empty,
Body::Bytes(ref bin) => BodySize::Sized(bin.len() as u64), AnyBody::Bytes(ref bin) => BodySize::Sized(bin.len() as u64),
Body::Message(ref body) => body.size(), AnyBody::Message(ref body) => body.size(),
} }
} }
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
match self.get_mut() { match self.get_mut() {
Body::None => Poll::Ready(None), AnyBody::None => Poll::Ready(None),
Body::Empty => Poll::Ready(None), AnyBody::Empty => Poll::Ready(None),
Body::Bytes(ref mut 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)
@ -65,104 +73,161 @@ impl MessageBody for Body {
Poll::Ready(Some(Ok(mem::take(bin)))) Poll::Ready(Some(Ok(mem::take(bin))))
} }
} }
Body::Message(body) => body.as_mut().poll_next(cx),
// TODO: MSRV 1.51: poll_map_err
AnyBody::Message(body) => match ready!(body.as_pin_mut().poll_next(cx)) {
Some(Err(err)) => {
Poll::Ready(Some(Err(Error::new_body().with_cause(err))))
}
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
},
} }
} }
} }
impl PartialEq for Body { impl PartialEq for AnyBody {
fn eq(&self, other: &Body) -> bool { fn eq(&self, other: &Body) -> bool {
match *self { match *self {
Body::None => matches!(*other, Body::None), AnyBody::None => matches!(*other, AnyBody::None),
Body::Empty => matches!(*other, Body::Empty), AnyBody::Empty => matches!(*other, AnyBody::Empty),
Body::Bytes(ref b) => match *other { AnyBody::Bytes(ref b) => match *other {
Body::Bytes(ref b2) => b == b2, AnyBody::Bytes(ref b2) => b == b2,
_ => false, _ => false,
}, },
Body::Message(_) => false, AnyBody::Message(_) => false,
} }
} }
} }
impl fmt::Debug for Body { impl fmt::Debug for AnyBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self { match *self {
Body::None => write!(f, "Body::None"), AnyBody::None => write!(f, "AnyBody::None"),
Body::Empty => write!(f, "Body::Empty"), AnyBody::Empty => write!(f, "AnyBody::Empty"),
Body::Bytes(ref b) => write!(f, "Body::Bytes({:?})", b), AnyBody::Bytes(ref b) => write!(f, "AnyBody::Bytes({:?})", b),
Body::Message(_) => write!(f, "Body::Message(_)"), AnyBody::Message(_) => write!(f, "AnyBody::Message(_)"),
} }
} }
} }
impl From<&'static str> for Body { impl From<&'static str> for AnyBody {
fn from(s: &'static str) -> Body { fn from(s: &'static str) -> Body {
Body::Bytes(Bytes::from_static(s.as_ref())) AnyBody::Bytes(Bytes::from_static(s.as_ref()))
} }
} }
impl From<&'static [u8]> for Body { impl From<&'static [u8]> for AnyBody {
fn from(s: &'static [u8]) -> Body { fn from(s: &'static [u8]) -> Body {
Body::Bytes(Bytes::from_static(s)) AnyBody::Bytes(Bytes::from_static(s))
} }
} }
impl From<Vec<u8>> for Body { impl From<Vec<u8>> for AnyBody {
fn from(vec: Vec<u8>) -> Body { fn from(vec: Vec<u8>) -> Body {
Body::Bytes(Bytes::from(vec)) AnyBody::Bytes(Bytes::from(vec))
} }
} }
impl From<String> for Body { impl From<String> for AnyBody {
fn from(s: String) -> Body { fn from(s: String) -> Body {
s.into_bytes().into() s.into_bytes().into()
} }
} }
impl From<&'_ String> for Body { impl From<&'_ String> for AnyBody {
fn from(s: &String) -> Body { fn from(s: &String) -> Body {
Body::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s))) AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s)))
} }
} }
impl From<Cow<'_, str>> for Body { impl From<Cow<'_, str>> for AnyBody {
fn from(s: Cow<'_, str>) -> Body { fn from(s: Cow<'_, str>) -> Body {
match s { match s {
Cow::Owned(s) => Body::from(s), Cow::Owned(s) => AnyBody::from(s),
Cow::Borrowed(s) => { Cow::Borrowed(s) => {
Body::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s))) AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)))
} }
} }
} }
} }
impl From<Bytes> for Body { impl From<Bytes> for AnyBody {
fn from(s: Bytes) -> Body { fn from(s: Bytes) -> Body {
Body::Bytes(s) AnyBody::Bytes(s)
} }
} }
impl From<BytesMut> for Body { impl From<BytesMut> for AnyBody {
fn from(s: BytesMut) -> Body { fn from(s: BytesMut) -> Body {
Body::Bytes(s.freeze()) AnyBody::Bytes(s.freeze())
} }
} }
impl<S> From<SizedStream<S>> for Body impl<S, E> From<SizedStream<S>> for AnyBody
where
S: Stream<Item = Result<Bytes, Error>> + 'static,
{
fn from(s: SizedStream<S>) -> Body {
Body::from_message(s)
}
}
impl<S, E> From<BodyStream<S>> for Body
where where
S: Stream<Item = Result<Bytes, E>> + 'static, S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Error> + 'static, E: Into<Box<dyn StdError>> + 'static,
{ {
fn from(s: BodyStream<S>) -> Body { fn from(s: SizedStream<S>) -> Body {
Body::from_message(s) AnyBody::from_message(s)
}
}
impl<S, E> From<BodyStream<S>> for AnyBody
where
S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Box<dyn StdError>> + 'static,
{
fn from(s: BodyStream<S>) -> Body {
AnyBody::from_message(s)
}
}
/// A boxed message body with boxed errors.
pub struct BoxAnyBody(Pin<Box<dyn MessageBody<Error = Box<dyn StdError + 'static>>>>);
impl BoxAnyBody {
/// Boxes a `MessageBody` and any errors it generates.
pub fn from_body<B>(body: B) -> Self
where
B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError + 'static>>,
{
let body = MessageBodyMapErr::new(body, Into::into);
Self(Box::pin(body))
}
/// Returns a mutable pinned reference to the inner message body type.
pub fn as_pin_mut(
&mut self,
) -> Pin<&mut (dyn MessageBody<Error = Box<dyn StdError + 'static>>)> {
self.0.as_mut()
}
}
impl fmt::Debug for BoxAnyBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("BoxAnyBody(dyn MessageBody)")
}
}
impl MessageBody for BoxAnyBody {
type Error = Error;
fn size(&self) -> BodySize {
self.0.size()
}
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
// TODO: MSRV 1.51: poll_map_err
match ready!(self.0.as_mut().poll_next(cx)) {
Some(Err(err)) => Poll::Ready(Some(Err(Error::new_body().with_cause(err)))),
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
}
} }
} }

View File

@ -1,4 +1,5 @@
use std::{ use std::{
error::Error as StdError,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
@ -7,8 +8,6 @@ use bytes::Bytes;
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use crate::error::Error;
use super::{BodySize, MessageBody}; use super::{BodySize, MessageBody};
pin_project! { pin_project! {
@ -24,7 +23,7 @@ pin_project! {
impl<S, E> BodyStream<S> impl<S, E> BodyStream<S>
where where
S: Stream<Item = Result<Bytes, E>>, S: Stream<Item = Result<Bytes, E>>,
E: Into<Error>, E: Into<Box<dyn StdError>> + 'static,
{ {
pub fn new(stream: S) -> Self { pub fn new(stream: S) -> Self {
BodyStream { stream } BodyStream { stream }
@ -34,8 +33,10 @@ where
impl<S, E> MessageBody for BodyStream<S> impl<S, E> MessageBody for BodyStream<S>
where where
S: Stream<Item = Result<Bytes, E>>, S: Stream<Item = Result<Bytes, E>>,
E: Into<Error>, E: Into<Box<dyn StdError>> + 'static,
{ {
type Error = E;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Stream BodySize::Stream
} }
@ -48,13 +49,13 @@ where
fn poll_next( fn poll_next(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
loop { loop {
let stream = self.as_mut().project().stream; let stream = self.as_mut().project().stream;
let chunk = match ready!(stream.poll_next(cx)) { let chunk = match ready!(stream.poll_next(cx)) {
Some(Ok(ref bytes)) if bytes.is_empty() => continue, Some(Ok(ref bytes)) if bytes.is_empty() => continue,
opt => opt.map(|res| res.map_err(Into::into)), opt => opt,
}; };
return Poll::Ready(chunk); return Poll::Ready(chunk);
@ -64,9 +65,16 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use actix_rt::pin; use std::{convert::Infallible, time::Duration};
use actix_rt::{
pin,
time::{sleep, Sleep},
};
use actix_utils::future::poll_fn; use actix_utils::future::poll_fn;
use futures_util::stream; use derive_more::{Display, Error};
use futures_core::ready;
use futures_util::{stream, FutureExt as _};
use super::*; use super::*;
use crate::body::to_bytes; use crate::body::to_bytes;
@ -76,7 +84,7 @@ mod tests {
let body = BodyStream::new(stream::iter( let body = BodyStream::new(stream::iter(
["1", "", "2"] ["1", "", "2"]
.iter() .iter()
.map(|&v| Ok(Bytes::from(v)) as Result<Bytes, ()>), .map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
)); ));
pin!(body); pin!(body);
@ -101,9 +109,63 @@ mod tests {
let body = BodyStream::new(stream::iter( let body = BodyStream::new(stream::iter(
["1", "", "2"] ["1", "", "2"]
.iter() .iter()
.map(|&v| Ok(Bytes::from(v)) as Result<Bytes, ()>), .map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
)); ));
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12"))); assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
} }
#[derive(Debug, Display, Error)]
#[display(fmt = "stream error")]
struct StreamErr;
#[actix_rt::test]
async fn stream_immediate_error() {
let body = BodyStream::new(stream::once(async { Err(StreamErr) }));
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
}
#[actix_rt::test]
async fn stream_delayed_error() {
let body =
BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)]));
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
#[pin_project::pin_project(project = TimeDelayStreamProj)]
#[derive(Debug)]
enum TimeDelayStream {
Start,
Sleep(Pin<Box<Sleep>>),
Done,
}
impl Stream for TimeDelayStream {
type Item = Result<Bytes, StreamErr>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
match self.as_mut().get_mut() {
TimeDelayStream::Start => {
let sleep = sleep(Duration::from_millis(1));
self.as_mut().set(TimeDelayStream::Sleep(Box::pin(sleep)));
cx.waker().wake_by_ref();
Poll::Pending
}
TimeDelayStream::Sleep(ref mut delay) => {
ready!(delay.poll_unpin(cx));
self.set(TimeDelayStream::Done);
cx.waker().wake_by_ref();
Poll::Pending
}
TimeDelayStream::Done => Poll::Ready(Some(Err(StreamErr))),
}
}
}
let body = BodyStream::new(TimeDelayStream::Start);
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
}
} }

View File

@ -1,12 +1,15 @@
//! [`MessageBody`] trait and foreign implementations. //! [`MessageBody`] trait and foreign implementations.
use std::{ use std::{
convert::Infallible,
mem, mem,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::ready;
use pin_project_lite::pin_project;
use crate::error::Error; use crate::error::Error;
@ -14,6 +17,8 @@ use super::BodySize;
/// An interface for response bodies. /// An interface for response bodies.
pub trait MessageBody { pub trait MessageBody {
type Error;
/// Body size hint. /// Body size hint.
fn size(&self) -> BodySize; fn size(&self) -> BodySize;
@ -21,14 +26,12 @@ pub trait MessageBody {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>>; ) -> Poll<Option<Result<Bytes, Self::Error>>>;
downcast_get_type_id!();
} }
downcast!(MessageBody);
impl MessageBody for () { impl MessageBody for () {
type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Empty BodySize::Empty
} }
@ -36,12 +39,18 @@ impl MessageBody for () {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
Poll::Ready(None) Poll::Ready(None)
} }
} }
impl<T: MessageBody + Unpin> MessageBody for Box<T> { impl<B> MessageBody for Box<B>
where
B: MessageBody + Unpin,
B::Error: Into<Error>,
{
type Error = B::Error;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
self.as_ref().size() self.as_ref().size()
} }
@ -49,12 +58,18 @@ impl<T: MessageBody + Unpin> MessageBody for Box<T> {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
Pin::new(self.get_mut().as_mut()).poll_next(cx) Pin::new(self.get_mut().as_mut()).poll_next(cx)
} }
} }
impl<T: MessageBody> MessageBody for Pin<Box<T>> { impl<B> MessageBody for Pin<Box<B>>
where
B: MessageBody,
B::Error: Into<Error>,
{
type Error = B::Error;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
self.as_ref().size() self.as_ref().size()
} }
@ -62,12 +77,14 @@ impl<T: MessageBody> MessageBody for Pin<Box<T>> {
fn poll_next( fn poll_next(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
self.as_mut().poll_next(cx) self.as_mut().poll_next(cx)
} }
} }
impl MessageBody for Bytes { impl MessageBody for Bytes {
type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
@ -75,7 +92,7 @@ impl MessageBody for Bytes {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
@ -85,6 +102,8 @@ impl MessageBody for Bytes {
} }
impl MessageBody for BytesMut { impl MessageBody for BytesMut {
type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
@ -92,7 +111,7 @@ impl MessageBody for BytesMut {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
@ -102,6 +121,8 @@ impl MessageBody for BytesMut {
} }
impl MessageBody for &'static str { impl MessageBody for &'static str {
type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
@ -109,7 +130,7 @@ impl MessageBody for &'static str {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
@ -121,6 +142,8 @@ impl MessageBody for &'static str {
} }
impl MessageBody for Vec<u8> { impl MessageBody for Vec<u8> {
type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
@ -128,7 +151,7 @@ impl MessageBody for Vec<u8> {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
@ -138,6 +161,8 @@ impl MessageBody for Vec<u8> {
} }
impl MessageBody for String { impl MessageBody for String {
type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
@ -145,7 +170,7 @@ impl MessageBody for String {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
@ -155,3 +180,53 @@ impl MessageBody for String {
} }
} }
} }
pin_project! {
pub(crate) struct MessageBodyMapErr<B, F> {
#[pin]
body: B,
mapper: Option<F>,
}
}
impl<B, F, E> MessageBodyMapErr<B, F>
where
B: MessageBody,
F: FnOnce(B::Error) -> E,
{
pub(crate) fn new(body: B, mapper: F) -> Self {
Self {
body,
mapper: Some(mapper),
}
}
}
impl<B, F, E> MessageBody for MessageBodyMapErr<B, F>
where
B: MessageBody,
F: FnOnce(B::Error) -> E,
{
type Error = E;
fn size(&self) -> BodySize {
self.body.size()
}
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
let this = self.as_mut().project();
match ready!(this.body.poll_next(cx)) {
Some(Err(err)) => {
let f = self.as_mut().project().mapper.take().unwrap();
let mapped_err = (f)(err);
Poll::Ready(Some(Err(mapped_err)))
}
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
}
}
}

View File

@ -15,9 +15,10 @@ mod response_body;
mod size; mod size;
mod sized_stream; mod sized_stream;
pub use self::body::Body; pub use self::body::{AnyBody, Body, BoxAnyBody};
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 use self::response_body::ResponseBody; 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;
@ -41,7 +42,7 @@ pub use self::sized_stream::SizedStream;
/// assert_eq!(bytes, b"123"[..]); /// assert_eq!(bytes, b"123"[..]);
/// # } /// # }
/// ``` /// ```
pub async fn to_bytes(body: impl MessageBody) -> Result<Bytes, crate::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::Empty | BodySize::Sized(0) => return Ok(Bytes::new()),
BodySize::Sized(size) => size as usize, BodySize::Sized(size) => size as usize,
@ -85,15 +86,6 @@ mod tests {
} }
} }
impl ResponseBody<Body> {
pub(crate) fn get_ref(&self) -> &[u8] {
match *self {
ResponseBody::Body(ref b) => b.get_ref(),
ResponseBody::Other(ref b) => b.get_ref(),
}
}
}
#[actix_rt::test] #[actix_rt::test]
async fn test_static_str() { async fn test_static_str() {
assert_eq!(Body::from("").size(), BodySize::Sized(0)); assert_eq!(Body::from("").size(), BodySize::Sized(0));
@ -199,11 +191,15 @@ mod tests {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_box() { 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::Empty);
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(());
assert_eq!(val.size(), BodySize::Empty);
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
} }
#[actix_rt::test] #[actix_rt::test]
@ -237,10 +233,13 @@ mod tests {
); );
} }
// down-casting used to be done with a method on MessageBody trait
// test is kept to demonstrate equivalence of Any trait
#[actix_rt::test] #[actix_rt::test]
async fn test_body_casting() { async fn test_body_casting() {
let mut body = String::from("hello cast"); let mut body = String::from("hello cast");
let resp_body: &mut dyn MessageBody = &mut body; // let mut resp_body: &mut dyn MessageBody<Error = Error> = &mut body;
let resp_body: &mut dyn std::any::Any = &mut body;
let body = resp_body.downcast_ref::<String>().unwrap(); let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast"); assert_eq!(body, "hello cast");
let body = &mut resp_body.downcast_mut::<String>().unwrap(); let body = &mut resp_body.downcast_mut::<String>().unwrap();

View File

@ -5,7 +5,7 @@ use std::{
}; };
use bytes::Bytes; use bytes::Bytes;
use futures_core::Stream; use futures_core::{ready, Stream};
use pin_project::pin_project; use pin_project::pin_project;
use crate::error::Error; use crate::error::Error;
@ -43,7 +43,13 @@ impl<B: MessageBody> ResponseBody<B> {
} }
} }
impl<B: MessageBody> MessageBody for ResponseBody<B> { impl<B> MessageBody for ResponseBody<B>
where
B: MessageBody,
B::Error: Into<Error>,
{
type Error = Error;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
match self { match self {
ResponseBody::Body(ref body) => body.size(), ResponseBody::Body(ref body) => body.size(),
@ -54,12 +60,16 @@ impl<B: MessageBody> MessageBody for ResponseBody<B> {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
Stream::poll_next(self, cx) Stream::poll_next(self, cx)
} }
} }
impl<B: MessageBody> Stream for ResponseBody<B> { impl<B> Stream for ResponseBody<B>
where
B: MessageBody,
B::Error: Into<Error>,
{
type Item = Result<Bytes, Error>; type Item = Result<Bytes, Error>;
fn poll_next( fn poll_next(
@ -67,7 +77,12 @@ impl<B: MessageBody> Stream for ResponseBody<B> {
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> { ) -> Poll<Option<Self::Item>> {
match self.project() { match self.project() {
ResponseBodyProj::Body(body) => body.poll_next(cx), // TODO: MSRV 1.51: poll_map_err
ResponseBodyProj::Body(body) => match ready!(body.poll_next(cx)) {
Some(Err(err)) => Poll::Ready(Some(Err(err.into()))),
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
},
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx), ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
} }
} }

View File

@ -1,4 +1,5 @@
use std::{ use std::{
error::Error as StdError,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
@ -7,15 +8,13 @@ use bytes::Bytes;
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use crate::error::Error;
use super::{BodySize, MessageBody}; use super::{BodySize, MessageBody};
pin_project! { pin_project! {
/// Known sized streaming response wrapper. /// Known sized streaming response wrapper.
/// ///
/// This body implementation should be used if total size of stream is known. Data get sent as is /// This body implementation should be used if total size of stream is known. Data is sent as-is
/// without using transfer encoding. /// without using chunked transfer encoding.
pub struct SizedStream<S> { pub struct SizedStream<S> {
size: u64, size: u64,
#[pin] #[pin]
@ -23,19 +22,23 @@ pin_project! {
} }
} }
impl<S> SizedStream<S> impl<S, E> SizedStream<S>
where where
S: Stream<Item = Result<Bytes, Error>>, S: Stream<Item = Result<Bytes, E>>,
E: Into<Box<dyn StdError>> + 'static,
{ {
pub fn new(size: u64, stream: S) -> Self { pub fn new(size: u64, stream: S) -> Self {
SizedStream { size, stream } SizedStream { size, stream }
} }
} }
impl<S> MessageBody for SizedStream<S> impl<S, E> MessageBody for SizedStream<S>
where where
S: Stream<Item = Result<Bytes, Error>>, S: Stream<Item = Result<Bytes, E>>,
E: Into<Box<dyn StdError>> + 'static,
{ {
type Error = E;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.size as u64) BodySize::Sized(self.size as u64)
} }
@ -48,7 +51,7 @@ where
fn poll_next( fn poll_next(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
loop { loop {
let stream = self.as_mut().project().stream; let stream = self.as_mut().project().stream;
@ -64,6 +67,8 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::convert::Infallible;
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;
@ -75,7 +80,11 @@ mod tests {
async fn skips_empty_chunks() { async fn skips_empty_chunks() {
let body = SizedStream::new( let body = SizedStream::new(
2, 2,
stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))), stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
),
); );
pin!(body); pin!(body);
@ -101,7 +110,11 @@ mod tests {
async fn read_to_bytes() { async fn read_to_bytes() {
let body = SizedStream::new( let body = SizedStream::new(
2, 2,
stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))), stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
),
); );
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12"))); assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));

View File

@ -1,19 +1,16 @@
use std::marker::PhantomData; use std::{error::Error as StdError, fmt, marker::PhantomData, net, rc::Rc};
use std::rc::Rc;
use std::{fmt, net};
use actix_codec::Framed; use actix_codec::Framed;
use actix_service::{IntoServiceFactory, Service, ServiceFactory}; use actix_service::{IntoServiceFactory, Service, ServiceFactory};
use crate::body::MessageBody; use crate::{
use crate::config::{KeepAlive, ServiceConfig}; body::{AnyBody, MessageBody},
use crate::error::Error; config::{KeepAlive, ServiceConfig},
use crate::h1::{Codec, ExpectHandler, H1Service, UpgradeHandler}; h1::{self, ExpectHandler, H1Service, UpgradeHandler},
use crate::h2::H2Service; h2::H2Service,
use crate::request::Request; service::HttpService,
use crate::response::Response; ConnectCallback, Extensions, Request, Response,
use crate::service::HttpService; };
use crate::{ConnectCallback, Extensions};
/// A HTTP service builder /// A HTTP service builder
/// ///
@ -34,7 +31,7 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler> impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
{ {
@ -57,13 +54,13 @@ where
impl<T, S, X, U> HttpServiceBuilder<T, S, X, U> impl<T, S, X, U> HttpServiceBuilder<T, S, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>, U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
@ -123,7 +120,7 @@ where
where where
F: IntoServiceFactory<X1, Request>, F: IntoServiceFactory<X1, Request>,
X1: ServiceFactory<Request, Config = (), Response = Request>, X1: ServiceFactory<Request, Config = (), Response = Request>,
X1::Error: Into<Error>, X1::Error: Into<Response<AnyBody>>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
{ {
HttpServiceBuilder { HttpServiceBuilder {
@ -145,8 +142,8 @@ where
/// and this service get called with original request and framed object. /// and this service get called with original request and framed object.
pub fn upgrade<F, U1>(self, upgrade: F) -> HttpServiceBuilder<T, S, X, U1> pub fn upgrade<F, U1>(self, upgrade: F) -> HttpServiceBuilder<T, S, X, U1>
where where
F: IntoServiceFactory<U1, (Request, Framed<T, Codec>)>, F: IntoServiceFactory<U1, (Request, Framed<T, h1::Codec>)>,
U1: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>, U1: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
U1::Error: fmt::Display, U1::Error: fmt::Display,
U1::InitError: fmt::Debug, U1::InitError: fmt::Debug,
{ {
@ -181,7 +178,7 @@ where
where where
B: MessageBody, B: MessageBody,
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S, Request>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
{ {
@ -202,11 +199,13 @@ where
/// Finish service configuration and create a HTTP service for HTTP/2 protocol. /// Finish service configuration and create a HTTP service for HTTP/2 protocol.
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B> pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
where where
B: MessageBody + 'static,
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S, Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,
@ -223,11 +222,13 @@ where
/// Finish service configuration and create `HttpService` instance. /// Finish service configuration and create `HttpService` instance.
pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U> pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
where where
B: MessageBody + 'static,
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S, Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,

View File

@ -12,10 +12,10 @@ use bytes::Bytes;
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use h2::client::SendRequest; use h2::client::SendRequest;
use crate::body::MessageBody;
use crate::h1::ClientCodec; use crate::h1::ClientCodec;
use crate::message::{RequestHeadType, ResponseHead}; use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::Payload; 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;
@ -256,8 +256,9 @@ where
body: RB, body: RB,
) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>> ) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>
where where
RB: MessageBody + 'static,
H: Into<RequestHeadType> + 'static, H: Into<RequestHeadType> + 'static,
RB: MessageBody + 'static,
RB::Error: Into<Error>,
{ {
Box::pin(async move { Box::pin(async move {
match self { match self {

View File

@ -1,15 +1,16 @@
use std::io; use std::{error::Error as StdError, fmt, io};
use derive_more::{Display, From}; use derive_more::{Display, From};
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
use actix_tls::accept::openssl::SslError; use actix_tls::accept::openssl::SslError;
use crate::error::{Error, ParseError, ResponseError}; use crate::error::{Error, ParseError};
use crate::http::{Error as HttpError, StatusCode}; 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]
pub enum ConnectError { pub enum ConnectError {
/// SSL feature is not enabled /// SSL feature is not enabled
#[display(fmt = "SSL is not supported")] #[display(fmt = "SSL is not supported")]
@ -64,6 +65,7 @@ impl From<actix_tls::connect::ConnectError> for ConnectError {
} }
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
#[non_exhaustive]
pub enum InvalidUrl { pub enum InvalidUrl {
#[display(fmt = "Missing URL scheme")] #[display(fmt = "Missing URL scheme")]
MissingScheme, MissingScheme,
@ -82,6 +84,7 @@ impl std::error::Error for InvalidUrl {}
/// A set of errors that can occur during request sending and response reading /// A set of errors that can occur during request sending and response reading
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
#[non_exhaustive]
pub enum SendRequestError { pub enum SendRequestError {
/// Invalid URL /// Invalid URL
#[display(fmt = "Invalid URL: {}", _0)] #[display(fmt = "Invalid URL: {}", _0)]
@ -115,25 +118,17 @@ pub enum SendRequestError {
/// Error sending request body /// Error sending request body
Body(Error), Body(Error),
/// Other errors that can occur after submitting a request.
#[display(fmt = "{:?}: {}", _1, _0)]
Custom(Box<dyn StdError>, Box<dyn fmt::Debug>),
} }
impl std::error::Error for SendRequestError {} impl std::error::Error for SendRequestError {}
/// Convert `SendRequestError` to a server `Response`
impl ResponseError for SendRequestError {
fn status_code(&self) -> StatusCode {
match *self {
SendRequestError::Connect(ConnectError::Timeout) => {
StatusCode::GATEWAY_TIMEOUT
}
SendRequestError::Connect(_) => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
/// A set of errors that can occur during freezing a request /// A set of errors that can occur during freezing a request
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
#[non_exhaustive]
pub enum FreezeRequestError { pub enum FreezeRequestError {
/// Invalid URL /// Invalid URL
#[display(fmt = "Invalid URL: {}", _0)] #[display(fmt = "Invalid URL: {}", _0)]
@ -142,15 +137,20 @@ pub enum FreezeRequestError {
/// HTTP error /// HTTP error
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Http(HttpError), Http(HttpError),
/// Other errors that can occur after submitting a request.
#[display(fmt = "{:?}: {}", _1, _0)]
Custom(Box<dyn StdError>, Box<dyn fmt::Debug>),
} }
impl std::error::Error for FreezeRequestError {} impl std::error::Error for FreezeRequestError {}
impl From<FreezeRequestError> for SendRequestError { impl From<FreezeRequestError> for SendRequestError {
fn from(e: FreezeRequestError) -> Self { fn from(err: FreezeRequestError) -> Self {
match e { match err {
FreezeRequestError::Url(e) => e.into(), FreezeRequestError::Url(err) => err.into(),
FreezeRequestError::Http(e) => e.into(), FreezeRequestError::Http(err) => err.into(),
FreezeRequestError::Custom(err, msg) => SendRequestError::Custom(err, msg),
} }
} }
} }

View File

@ -11,7 +11,6 @@ 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 crate::error::PayloadError;
use crate::h1; use crate::h1;
use crate::http::{ use crate::http::{
header::{HeaderMap, IntoHeaderValue, EXPECT, HOST}, header::{HeaderMap, IntoHeaderValue, EXPECT, HOST},
@ -19,6 +18,7 @@ use crate::http::{
}; };
use crate::message::{RequestHeadType, ResponseHead}; use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::Payload; 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};
@ -32,6 +32,7 @@ pub(crate) async fn send_request<Io, B>(
where where
Io: ConnectionIo, Io: ConnectionIo,
B: MessageBody, B: MessageBody,
B::Error: Into<Error>,
{ {
// set request host header // set request host header
if !head.as_ref().headers.contains_key(HOST) if !head.as_ref().headers.contains_key(HOST)
@ -154,6 +155,7 @@ pub(crate) async fn send_body<Io, B>(
where where
Io: ConnectionIo, Io: ConnectionIo,
B: MessageBody, B: MessageBody,
B::Error: Into<Error>,
{ {
actix_rt::pin!(body); actix_rt::pin!(body);
@ -161,9 +163,10 @@ where
while !eof { while !eof {
while !eof && !framed.as_ref().is_write_buf_full() { while !eof && !framed.as_ref().is_write_buf_full() {
match poll_fn(|cx| body.as_mut().poll_next(cx)).await { match poll_fn(|cx| body.as_mut().poll_next(cx)).await {
Some(result) => { Some(Ok(chunk)) => {
framed.as_mut().write(h1::Message::Chunk(Some(result?)))?; framed.as_mut().write(h1::Message::Chunk(Some(chunk)))?;
} }
Some(Err(err)) => return Err(err.into().into()),
None => { None => {
eof = true; eof = true;
framed.as_mut().write(h1::Message::Chunk(None))?; framed.as_mut().write(h1::Message::Chunk(None))?;

View File

@ -9,14 +9,19 @@ 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 crate::body::{BodySize, MessageBody}; use crate::{
use crate::header::HeaderMap; body::{BodySize, MessageBody},
use crate::message::{RequestHeadType, ResponseHead}; header::HeaderMap,
use crate::payload::Payload; message::{RequestHeadType, ResponseHead},
payload::Payload,
Error,
};
use super::config::ConnectorConfig; use super::{
use super::connection::{ConnectionIo, H2Connection}; config::ConnectorConfig,
use super::error::SendRequestError; connection::{ConnectionIo, H2Connection},
error::SendRequestError,
};
pub(crate) async fn send_request<Io, B>( pub(crate) async fn send_request<Io, B>(
mut io: H2Connection<Io>, mut io: H2Connection<Io>,
@ -26,6 +31,7 @@ pub(crate) async fn send_request<Io, B>(
where where
Io: ConnectionIo, Io: ConnectionIo,
B: MessageBody, B: MessageBody,
B::Error: Into<Error>,
{ {
trace!("Sending client request: {:?} {:?}", head, body.size()); trace!("Sending client request: {:?} {:?}", head, body.size());
@ -125,10 +131,14 @@ where
Ok((head, payload)) Ok((head, payload))
} }
async fn send_body<B: MessageBody>( async fn send_body<B>(
body: B, body: B,
mut send: SendStream<Bytes>, mut send: SendStream<Bytes>,
) -> Result<(), SendRequestError> { ) -> Result<(), SendRequestError>
where
B: MessageBody,
B::Error: Into<Error>,
{
let mut buf = None; let mut buf = None;
actix_rt::pin!(body); actix_rt::pin!(body);
loop { loop {
@ -138,7 +148,7 @@ async fn send_body<B: MessageBody>(
send.reserve_capacity(b.len()); send.reserve_capacity(b.len());
buf = Some(b); buf = Some(b);
} }
Some(Err(e)) => return Err(e.into()), Some(Err(e)) => return Err(e.into().into()),
None => { None => {
if let Err(e) = send.send_data(Bytes::new(), true) { if let Err(e) = send.send_data(Bytes::new(), true) {
return Err(e.into()); return Err(e.into());

View File

@ -8,11 +8,18 @@ use std::{
}; };
use actix_rt::task::{spawn_blocking, JoinHandle}; use actix_rt::task::{spawn_blocking, JoinHandle};
use brotli2::write::BrotliDecoder;
use bytes::Bytes; use bytes::Bytes;
use flate2::write::{GzDecoder, ZlibDecoder};
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
#[cfg(feature = "compress-brotli")]
use brotli2::write::BrotliDecoder;
#[cfg(feature = "compress-gzip")]
use flate2::write::{GzDecoder, ZlibDecoder};
#[cfg(feature = "compress-zstd")]
use zstd::stream::write::Decoder as ZstdDecoder;
use crate::{ use crate::{
encoding::Writer, encoding::Writer,
error::{BlockingError, PayloadError}, error::{BlockingError, PayloadError},
@ -36,15 +43,25 @@ where
#[inline] #[inline]
pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> { pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> {
let decoder = match encoding { let decoder = match encoding {
#[cfg(feature = "compress-brotli")]
ContentEncoding::Br => Some(ContentDecoder::Br(Box::new( ContentEncoding::Br => Some(ContentDecoder::Br(Box::new(
BrotliDecoder::new(Writer::new()), BrotliDecoder::new(Writer::new()),
))), ))),
#[cfg(feature = "compress-gzip")]
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
ZlibDecoder::new(Writer::new()), ZlibDecoder::new(Writer::new()),
))), ))),
#[cfg(feature = "compress-gzip")]
ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new( ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(
GzDecoder::new(Writer::new()), GzDecoder::new(Writer::new()),
))), ))),
#[cfg(feature = "compress-zstd")]
ContentEncoding::Zstd => Some(ContentDecoder::Zstd(Box::new(
ZstdDecoder::new(Writer::new()).expect(
"Failed to create zstd decoder. This is a bug. \
Please report it to the actix-web repository.",
),
))),
_ => None, _ => None,
}; };
@ -141,14 +158,22 @@ where
} }
enum ContentDecoder { enum ContentDecoder {
#[cfg(feature = "compress-gzip")]
Deflate(Box<ZlibDecoder<Writer>>), Deflate(Box<ZlibDecoder<Writer>>),
#[cfg(feature = "compress-gzip")]
Gzip(Box<GzDecoder<Writer>>), Gzip(Box<GzDecoder<Writer>>),
#[cfg(feature = "compress-brotli")]
Br(Box<BrotliDecoder<Writer>>), Br(Box<BrotliDecoder<Writer>>),
// We need explicit 'static lifetime here because ZstdDecoder need lifetime
// argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static`
#[cfg(feature = "compress-zstd")]
Zstd(Box<ZstdDecoder<'static, Writer>>),
} }
impl ContentDecoder { impl ContentDecoder {
fn feed_eof(&mut self) -> io::Result<Option<Bytes>> { fn feed_eof(&mut self) -> io::Result<Option<Bytes>> {
match self { match self {
#[cfg(feature = "compress-brotli")]
ContentDecoder::Br(ref mut decoder) => match decoder.flush() { ContentDecoder::Br(ref mut decoder) => match decoder.flush() {
Ok(()) => { Ok(()) => {
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
@ -162,6 +187,7 @@ impl ContentDecoder {
Err(e) => Err(e), Err(e) => Err(e),
}, },
#[cfg(feature = "compress-gzip")]
ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() { ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() {
Ok(_) => { Ok(_) => {
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
@ -175,6 +201,7 @@ impl ContentDecoder {
Err(e) => Err(e), Err(e) => Err(e),
}, },
#[cfg(feature = "compress-gzip")]
ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() { ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() {
Ok(_) => { Ok(_) => {
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
@ -186,11 +213,25 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
#[cfg(feature = "compress-zstd")]
ContentDecoder::Zstd(ref mut decoder) => match decoder.flush() {
Ok(_) => {
let b = decoder.get_mut().take();
if !b.is_empty() {
Ok(Some(b))
} else {
Ok(None)
}
}
Err(e) => Err(e),
},
} }
} }
fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> { fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> {
match self { match self {
#[cfg(feature = "compress-brotli")]
ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) { ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => { Ok(_) => {
decoder.flush()?; decoder.flush()?;
@ -205,6 +246,7 @@ impl ContentDecoder {
Err(e) => Err(e), Err(e) => Err(e),
}, },
#[cfg(feature = "compress-gzip")]
ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) { ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => { Ok(_) => {
decoder.flush()?; decoder.flush()?;
@ -219,6 +261,7 @@ impl ContentDecoder {
Err(e) => Err(e), Err(e) => Err(e),
}, },
#[cfg(feature = "compress-gzip")]
ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) { ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => { Ok(_) => {
decoder.flush()?; decoder.flush()?;
@ -232,6 +275,21 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
#[cfg(feature = "compress-zstd")]
ContentDecoder::Zstd(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => {
decoder.flush()?;
let b = decoder.get_mut().take();
if !b.is_empty() {
Ok(Some(b))
} else {
Ok(None)
}
}
Err(e) => Err(e),
},
} }
} }
} }

View File

@ -1,6 +1,7 @@
//! Stream encoders. //! Stream encoders.
use std::{ use std::{
error::Error as StdError,
future::Future, future::Future,
io::{self, Write as _}, io::{self, Write as _},
pin::Pin, pin::Pin,
@ -8,14 +9,22 @@ use std::{
}; };
use actix_rt::task::{spawn_blocking, JoinHandle}; use actix_rt::task::{spawn_blocking, JoinHandle};
use brotli2::write::BrotliEncoder;
use bytes::Bytes; use bytes::Bytes;
use flate2::write::{GzEncoder, ZlibEncoder}; use derive_more::Display;
use futures_core::ready; use futures_core::ready;
use pin_project::pin_project; use pin_project::pin_project;
#[cfg(feature = "compress-brotli")]
use brotli2::write::BrotliEncoder;
#[cfg(feature = "compress-gzip")]
use flate2::write::{GzEncoder, ZlibEncoder};
#[cfg(feature = "compress-zstd")]
use zstd::stream::write::Encoder as ZstdEncoder;
use crate::{ use crate::{
body::{Body, BodySize, MessageBody, ResponseBody}, body::{Body, BodySize, BoxAnyBody, MessageBody, ResponseBody},
http::{ http::{
header::{ContentEncoding, CONTENT_ENCODING}, header::{ContentEncoding, CONTENT_ENCODING},
HeaderValue, StatusCode, HeaderValue, StatusCode,
@ -92,10 +101,16 @@ impl<B: MessageBody> Encoder<B> {
enum EncoderBody<B> { enum EncoderBody<B> {
Bytes(Bytes), Bytes(Bytes),
Stream(#[pin] B), Stream(#[pin] B),
BoxedStream(Pin<Box<dyn MessageBody>>), BoxedStream(BoxAnyBody),
} }
impl<B: MessageBody> MessageBody for EncoderBody<B> { impl<B> MessageBody for EncoderBody<B>
where
B: MessageBody,
B::Error: Into<Error>,
{
type Error = EncoderError<B::Error>;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
match self { match self {
EncoderBody::Bytes(ref b) => b.size(), EncoderBody::Bytes(ref b) => b.size(),
@ -107,7 +122,7 @@ impl<B: MessageBody> MessageBody for EncoderBody<B> {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
match self.project() { match self.project() {
EncoderBodyProj::Bytes(b) => { EncoderBodyProj::Bytes(b) => {
if b.is_empty() { if b.is_empty() {
@ -116,13 +131,30 @@ impl<B: MessageBody> MessageBody for EncoderBody<B> {
Poll::Ready(Some(Ok(std::mem::take(b)))) Poll::Ready(Some(Ok(std::mem::take(b))))
} }
} }
EncoderBodyProj::Stream(b) => b.poll_next(cx), // TODO: MSRV 1.51: poll_map_err
EncoderBodyProj::BoxedStream(ref mut b) => b.as_mut().poll_next(cx), EncoderBodyProj::Stream(b) => match ready!(b.poll_next(cx)) {
Some(Err(err)) => Poll::Ready(Some(Err(EncoderError::Body(err)))),
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
},
EncoderBodyProj::BoxedStream(ref mut b) => {
match ready!(b.as_pin_mut().poll_next(cx)) {
Some(Err(err)) => Poll::Ready(Some(Err(EncoderError::Boxed(err)))),
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
}
}
} }
} }
} }
impl<B: MessageBody> MessageBody for Encoder<B> { impl<B> MessageBody for Encoder<B>
where
B: MessageBody,
B::Error: Into<Error>,
{
type Error = EncoderError<B::Error>;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
if self.encoder.is_none() { if self.encoder.is_none() {
self.body.size() self.body.size()
@ -134,7 +166,7 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
let mut this = self.project(); let mut this = self.project();
loop { loop {
if *this.eof { if *this.eof {
@ -142,8 +174,9 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
} }
if let Some(ref mut fut) = this.fut { if let Some(ref mut fut) = this.fut {
let mut encoder = let mut encoder = ready!(Pin::new(fut).poll(cx))
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??; .map_err(|_| EncoderError::Blocking(BlockingError))?
.map_err(EncoderError::Io)?;
let chunk = encoder.take(); let chunk = encoder.take();
*this.encoder = Some(encoder); *this.encoder = Some(encoder);
@ -162,7 +195,7 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
Some(Ok(chunk)) => { Some(Ok(chunk)) => {
if let Some(mut encoder) = this.encoder.take() { if let Some(mut encoder) = this.encoder.take() {
if chunk.len() < MAX_CHUNK_SIZE_ENCODE_IN_PLACE { if chunk.len() < MAX_CHUNK_SIZE_ENCODE_IN_PLACE {
encoder.write(&chunk)?; encoder.write(&chunk).map_err(EncoderError::Io)?;
let chunk = encoder.take(); let chunk = encoder.take();
*this.encoder = Some(encoder); *this.encoder = Some(encoder);
@ -182,7 +215,7 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
None => { None => {
if let Some(encoder) = this.encoder.take() { if let Some(encoder) = this.encoder.take() {
let chunk = encoder.finish()?; let chunk = encoder.finish().map_err(EncoderError::Io)?;
if chunk.is_empty() { if chunk.is_empty() {
return Poll::Ready(None); return Poll::Ready(None);
} else { } else {
@ -206,25 +239,40 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
} }
enum ContentEncoder { enum ContentEncoder {
#[cfg(feature = "compress-gzip")]
Deflate(ZlibEncoder<Writer>), Deflate(ZlibEncoder<Writer>),
#[cfg(feature = "compress-gzip")]
Gzip(GzEncoder<Writer>), Gzip(GzEncoder<Writer>),
#[cfg(feature = "compress-brotli")]
Br(BrotliEncoder<Writer>), Br(BrotliEncoder<Writer>),
// We need explicit 'static lifetime here because ZstdEncoder need lifetime
// argument, and we use `spawn_blocking` in `Encoder::poll_next` that require `FnOnce() -> R + Send + 'static`
#[cfg(feature = "compress-zstd")]
Zstd(ZstdEncoder<'static, Writer>),
} }
impl ContentEncoder { impl ContentEncoder {
fn encoder(encoding: ContentEncoding) -> Option<Self> { fn encoder(encoding: ContentEncoding) -> Option<Self> {
match encoding { match encoding {
#[cfg(feature = "compress-gzip")]
ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new( ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new(
Writer::new(), Writer::new(),
flate2::Compression::fast(), flate2::Compression::fast(),
))), ))),
#[cfg(feature = "compress-gzip")]
ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new( ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new(
Writer::new(), Writer::new(),
flate2::Compression::fast(), flate2::Compression::fast(),
))), ))),
#[cfg(feature = "compress-brotli")]
ContentEncoding::Br => { ContentEncoding::Br => {
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3)))
} }
#[cfg(feature = "compress-zstd")]
ContentEncoding::Zstd => {
let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?;
Some(ContentEncoder::Zstd(encoder))
}
_ => None, _ => None,
} }
} }
@ -232,31 +280,45 @@ impl ContentEncoder {
#[inline] #[inline]
pub(crate) fn take(&mut self) -> Bytes { pub(crate) fn take(&mut self) -> Bytes {
match *self { match *self {
#[cfg(feature = "compress-brotli")]
ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(),
#[cfg(feature = "compress-gzip")]
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
#[cfg(feature = "compress-gzip")]
ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(),
#[cfg(feature = "compress-zstd")]
ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(),
} }
} }
fn finish(self) -> Result<Bytes, io::Error> { fn finish(self) -> Result<Bytes, io::Error> {
match self { match self {
#[cfg(feature = "compress-brotli")]
ContentEncoder::Br(encoder) => match encoder.finish() { ContentEncoder::Br(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()), Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err), Err(err) => Err(err),
}, },
#[cfg(feature = "compress-gzip")]
ContentEncoder::Gzip(encoder) => match encoder.finish() { ContentEncoder::Gzip(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()), Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err), Err(err) => Err(err),
}, },
#[cfg(feature = "compress-gzip")]
ContentEncoder::Deflate(encoder) => match encoder.finish() { ContentEncoder::Deflate(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()), Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err), Err(err) => Err(err),
}, },
#[cfg(feature = "compress-zstd")]
ContentEncoder::Zstd(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err),
},
} }
} }
fn write(&mut self, data: &[u8]) -> Result<(), io::Error> { fn write(&mut self, data: &[u8]) -> Result<(), io::Error> {
match *self { match *self {
#[cfg(feature = "compress-brotli")]
ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
@ -264,6 +326,7 @@ impl ContentEncoder {
Err(err) Err(err)
} }
}, },
#[cfg(feature = "compress-gzip")]
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
@ -271,6 +334,7 @@ impl ContentEncoder {
Err(err) Err(err)
} }
}, },
#[cfg(feature = "compress-gzip")]
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
@ -278,6 +342,47 @@ impl ContentEncoder {
Err(err) Err(err)
} }
}, },
#[cfg(feature = "compress-zstd")]
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()),
Err(err) => {
trace!("Error decoding ztsd encoding: {}", err);
Err(err)
}
},
} }
} }
} }
#[derive(Debug, Display)]
#[non_exhaustive]
pub enum EncoderError<E> {
#[display(fmt = "body")]
Body(E),
#[display(fmt = "boxed")]
Boxed(Box<dyn StdError>),
#[display(fmt = "blocking")]
Blocking(BlockingError),
#[display(fmt = "io")]
Io(io::Error),
}
impl<E: StdError + 'static> StdError for EncoderError<E> {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
EncoderError::Body(err) => Some(err),
EncoderError::Boxed(err) => Some(&**err),
EncoderError::Blocking(err) => Some(err),
EncoderError::Io(err) => Some(err),
}
}
}
impl<E: StdError + 'static> From<EncoderError<E>> for crate::Error {
fn from(err: EncoderError<E>) -> Self {
crate::Error::new_encoder().with_cause(err)
}
}

View File

@ -1,193 +1,155 @@
//! Error and Result module //! Error and Result module
use std::{ use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Error};
cell::RefCell,
fmt,
io::{self, Write as _},
str::Utf8Error,
string::FromUtf8Error,
};
use bytes::BytesMut;
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
use http::{header, uri::InvalidUri, StatusCode}; use http::{uri::InvalidUri, StatusCode};
use serde::de::value::Error as DeError;
use crate::{body::Body, helpers::Writer, Response, ResponseBuilder}; use crate::{
body::{AnyBody, Body},
ws, Response,
};
pub use http::Error as HttpError; pub use http::Error as HttpError;
/// A specialized [`std::result::Result`] for Actix Web operations.
///
/// This typedef is generally used to avoid writing out `actix_http::error::Error` directly and is
/// otherwise a direct mapping to `Result`.
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// General purpose actix web error.
///
/// An actix web error is used to carry errors from `std::error`
/// through actix in a convenient way. It can be created through
/// converting errors with `into()`.
///
/// Whenever it is created from an external object a response error is created
/// for it that can be used to create an HTTP response from it this means that
/// if you have access to an actix `Error` you can always get a
/// `ResponseError` reference from it.
pub struct Error { pub struct Error {
cause: Box<dyn ResponseError>, inner: Box<ErrorInner>,
}
pub(crate) struct ErrorInner {
#[allow(dead_code)]
kind: Kind,
cause: Option<Box<dyn StdError>>,
} }
impl Error { impl Error {
/// Returns the reference to the underlying `ResponseError`. fn new(kind: Kind) -> Self {
pub fn as_response_error(&self) -> &dyn ResponseError { Self {
self.cause.as_ref() inner: Box::new(ErrorInner { kind, cause: None }),
}
} }
/// Similar to `as_response_error` but downcasts. pub(crate) fn new_http() -> Self {
pub fn as_error<T: ResponseError + 'static>(&self) -> Option<&T> { Self::new(Kind::Http)
<dyn ResponseError>::downcast_ref(self.cause.as_ref()) }
pub(crate) fn new_parse() -> Self {
Self::new(Kind::Parse)
}
pub(crate) fn new_payload() -> Self {
Self::new(Kind::Payload)
}
pub(crate) fn new_body() -> Self {
Self::new(Kind::Body)
}
pub(crate) fn new_send_response() -> Self {
Self::new(Kind::SendResponse)
}
// TODO: remove allow
#[allow(dead_code)]
pub(crate) fn new_io() -> Self {
Self::new(Kind::Io)
}
pub(crate) fn new_encoder() -> Self {
Self::new(Kind::Encoder)
}
pub(crate) fn new_ws() -> Self {
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
} }
} }
/// Errors that can generate responses. impl From<Error> for Response<AnyBody> {
pub trait ResponseError: fmt::Debug + fmt::Display { fn from(err: Error) -> Self {
/// Returns appropriate status code for error. let status_code = match err.inner.kind {
/// Kind::Parse => StatusCode::BAD_REQUEST,
/// A 500 Internal Server Error is used by default. If [error_response](Self::error_response) is _ => StatusCode::INTERNAL_SERVER_ERROR,
/// also implemented and does not call `self.status_code()`, then this will not be used. };
fn status_code(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
/// Creates full response for error. Response::new(status_code).set_body(Body::from(err.to_string()))
///
/// By default, the generated response uses a 500 Internal Server Error status code, a
/// `Content-Type` of `text/plain`, and the body is set to `Self`'s `Display` impl.
fn error_response(&self) -> Response<Body> {
let mut resp = Response::new(self.status_code());
let mut buf = BytesMut::new();
let _ = write!(Writer(&mut buf), "{}", self);
resp.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("text/plain; charset=utf-8"),
);
resp.set_body(Body::from(buf))
} }
downcast_get_type_id!();
} }
downcast!(ResponseError); #[derive(Debug, Clone, Copy, PartialEq, Eq, Display)]
pub enum Kind {
#[display(fmt = "error processing HTTP")]
Http,
impl fmt::Display for Error { #[display(fmt = "error parsing HTTP message")]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Parse,
fmt::Display::fmt(&self.cause, f)
} #[display(fmt = "request payload read error")]
Payload,
#[display(fmt = "response body write error")]
Body,
#[display(fmt = "send response error")]
SendResponse,
#[display(fmt = "error in WebSocket process")]
Ws,
#[display(fmt = "connection error")]
Io,
#[display(fmt = "encoder error")]
Encoder,
} }
impl fmt::Debug for Error { impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", &self.cause) // TODO: more detail
f.write_str("actix_http::Error")
} }
} }
impl std::error::Error for Error { impl fmt::Display for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
None match self.inner.cause.as_ref() {
Some(err) => write!(f, "{}: {}", &self.inner.kind, err),
None => write!(f, "{}", &self.inner.kind),
}
} }
} }
impl From<()> for Error { impl StdError for Error {
fn from(_: ()) -> Self { fn source(&self) -> Option<&(dyn StdError + 'static)> {
Error::from(UnitError) self.inner.cause.as_ref().map(|err| err.as_ref())
} }
} }
impl From<std::convert::Infallible> for Error { impl From<std::convert::Infallible> for Error {
fn from(_: std::convert::Infallible) -> Self { fn from(err: std::convert::Infallible) -> Self {
// `std::convert::Infallible` indicates an error match err {}
// that will never happen
unreachable!()
} }
} }
/// Convert `Error` to a `Response` instance impl From<ws::ProtocolError> for Error {
impl From<Error> for Response<Body> { fn from(err: ws::ProtocolError) -> Self {
fn from(err: Error) -> Self { Self::new_ws().with_cause(err)
Response::from_error(err)
} }
} }
/// `Error` for any error that implements `ResponseError` impl From<HttpError> for Error {
impl<T: ResponseError + 'static> From<T> for Error { fn from(err: HttpError) -> Self {
fn from(err: T) -> Error { Self::new_http().with_cause(err)
Error {
cause: Box::new(err),
}
} }
} }
/// Convert Response to a Error impl From<ws::HandshakeError> for Error {
impl From<Response<Body>> for Error { fn from(err: ws::HandshakeError) -> Self {
fn from(res: Response<Body>) -> Error { Self::new_ws().with_cause(err)
InternalError::from_response("", res).into()
}
}
/// Convert ResponseBuilder to a Error
impl From<ResponseBuilder> for Error {
fn from(mut res: ResponseBuilder) -> Error {
InternalError::from_response("", res.finish()).into()
}
}
#[derive(Debug, Display, Error)]
#[display(fmt = "Unknown Error")]
struct UnitError;
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`UnitError`].
impl ResponseError for UnitError {}
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`actix_tls::accept::openssl::SslError`].
#[cfg(feature = "openssl")]
impl ResponseError for actix_tls::accept::openssl::SslError {}
/// Returns [`StatusCode::BAD_REQUEST`] for [`DeError`].
impl ResponseError for DeError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
/// Returns [`StatusCode::BAD_REQUEST`] for [`Utf8Error`].
impl ResponseError for Utf8Error {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`HttpError`].
impl ResponseError for HttpError {}
/// Inspects the underlying [`io::ErrorKind`] and returns an appropriate status code.
///
/// If the error is [`io::ErrorKind::NotFound`], [`StatusCode::NOT_FOUND`] is returned. If the
/// error is [`io::ErrorKind::PermissionDenied`], [`StatusCode::FORBIDDEN`] is returned. Otherwise,
/// [`StatusCode::INTERNAL_SERVER_ERROR`] is returned.
impl ResponseError for io::Error {
fn status_code(&self) -> StatusCode {
match self.kind() {
io::ErrorKind::NotFound => StatusCode::NOT_FOUND,
io::ErrorKind::PermissionDenied => StatusCode::FORBIDDEN,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
/// Returns [`StatusCode::BAD_REQUEST`] for [`header::InvalidHeaderValue`].
impl ResponseError for header::InvalidHeaderValue {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
} }
} }
@ -237,13 +199,6 @@ pub enum ParseError {
Utf8(Utf8Error), Utf8(Utf8Error),
} }
/// Return `BadRequest` for `ParseError`
impl ResponseError for ParseError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
impl From<io::Error> for ParseError { impl From<io::Error> for ParseError {
fn from(err: io::Error) -> ParseError { fn from(err: io::Error) -> ParseError {
ParseError::Io(err) ParseError::Io(err)
@ -282,14 +237,23 @@ impl From<httparse::Error> for ParseError {
} }
} }
impl From<ParseError> for Error {
fn from(err: ParseError) -> Self {
Self::new_parse().with_cause(err)
}
}
impl From<ParseError> for Response<AnyBody> {
fn from(err: ParseError) -> Self {
Error::from(err).into()
}
}
/// A set of errors that can occur running blocking tasks in thread pool. /// A set of errors that can occur running blocking tasks in thread pool.
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[display(fmt = "Blocking thread pool is gone")] #[display(fmt = "Blocking thread pool is gone")]
pub struct BlockingError; pub struct BlockingError;
/// `InternalServerError` for `BlockingError`
impl ResponseError for BlockingError {}
/// A set of errors that can occur during payload parsing. /// A set of errors that can occur during payload parsing.
#[derive(Debug, Display)] #[derive(Debug, Display)]
#[non_exhaustive] #[non_exhaustive]
@ -363,16 +327,9 @@ impl From<BlockingError> for PayloadError {
} }
} }
/// `PayloadError` returns two possible results: impl From<PayloadError> for Error {
/// fn from(err: PayloadError) -> Self {
/// - `Overflow` returns `PayloadTooLarge` Self::new_payload().with_cause(err)
/// - Other errors returns `BadRequest`
impl ResponseError for PayloadError {
fn status_code(&self) -> StatusCode {
match *self {
PayloadError::Overflow => StatusCode::PAYLOAD_TOO_LARGE,
_ => StatusCode::BAD_REQUEST,
}
} }
} }
@ -381,13 +338,19 @@ impl ResponseError for PayloadError {
#[non_exhaustive] #[non_exhaustive]
pub enum DispatchError { pub enum DispatchError {
/// Service error /// Service error
Service(Error), // FIXME: display and error type
#[display(fmt = "Service Error")]
Service(#[error(not(source))] Response<AnyBody>),
/// Body error
// FIXME: display and error type
#[display(fmt = "Body Error")]
Body(#[error(not(source))] Box<dyn StdError>),
/// Upgrade service error /// Upgrade service error
Upgrade, Upgrade,
/// An `io::Error` that occurred while trying to read or write to a network /// An `io::Error` that occurred while trying to read or write to a network stream.
/// stream.
#[display(fmt = "IO error: {}", _0)] #[display(fmt = "IO error: {}", _0)]
Io(io::Error), Io(io::Error),
@ -453,180 +416,6 @@ mod content_type_test_impls {
} }
} }
/// Return `BadRequest` for `ContentTypeError`
impl ResponseError for ContentTypeError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
/// Helper type that can wrap any error and generate custom response.
///
/// In following example any `io::Error` will be converted into "BAD REQUEST"
/// response as opposite to *INTERNAL SERVER ERROR* which is defined by
/// default.
///
/// ```
/// # use std::io;
/// # use actix_http::*;
///
/// fn index(req: Request) -> Result<&'static str> {
/// Err(error::ErrorBadRequest(io::Error::new(io::ErrorKind::Other, "error")))
/// }
/// ```
pub struct InternalError<T> {
cause: T,
status: InternalErrorType,
}
enum InternalErrorType {
Status(StatusCode),
Response(RefCell<Option<Response<Body>>>),
}
impl<T> InternalError<T> {
/// Create `InternalError` instance
pub fn new(cause: T, status: StatusCode) -> Self {
InternalError {
cause,
status: InternalErrorType::Status(status),
}
}
/// Create `InternalError` with predefined `Response`.
pub fn from_response(cause: T, response: Response<Body>) -> Self {
InternalError {
cause,
status: InternalErrorType::Response(RefCell::new(Some(response))),
}
}
}
impl<T> fmt::Debug for InternalError<T>
where
T: fmt::Debug + 'static,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.cause, f)
}
}
impl<T> fmt::Display for InternalError<T>
where
T: fmt::Display + 'static,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.cause, f)
}
}
impl<T> ResponseError for InternalError<T>
where
T: fmt::Debug + fmt::Display + 'static,
{
fn status_code(&self) -> StatusCode {
match self.status {
InternalErrorType::Status(st) => st,
InternalErrorType::Response(ref resp) => {
if let Some(resp) = resp.borrow().as_ref() {
resp.head().status
} else {
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
}
fn error_response(&self) -> Response<Body> {
match self.status {
InternalErrorType::Status(st) => {
let mut res = Response::new(st);
let mut buf = BytesMut::new();
let _ = write!(Writer(&mut buf), "{}", self);
res.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("text/plain; charset=utf-8"),
);
res.set_body(Body::from(buf))
}
InternalErrorType::Response(ref resp) => {
if let Some(resp) = resp.borrow_mut().take() {
resp
} else {
Response::new(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
}
}
macro_rules! error_helper {
($name:ident, $status:ident) => {
paste::paste! {
#[doc = "Helper function that wraps any error and generates a `" $status "` response."]
#[allow(non_snake_case)]
pub fn $name<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::$status).into()
}
}
}
}
error_helper!(ErrorBadRequest, BAD_REQUEST);
error_helper!(ErrorUnauthorized, UNAUTHORIZED);
error_helper!(ErrorPaymentRequired, PAYMENT_REQUIRED);
error_helper!(ErrorForbidden, FORBIDDEN);
error_helper!(ErrorNotFound, NOT_FOUND);
error_helper!(ErrorMethodNotAllowed, METHOD_NOT_ALLOWED);
error_helper!(ErrorNotAcceptable, NOT_ACCEPTABLE);
error_helper!(
ErrorProxyAuthenticationRequired,
PROXY_AUTHENTICATION_REQUIRED
);
error_helper!(ErrorRequestTimeout, REQUEST_TIMEOUT);
error_helper!(ErrorConflict, CONFLICT);
error_helper!(ErrorGone, GONE);
error_helper!(ErrorLengthRequired, LENGTH_REQUIRED);
error_helper!(ErrorPayloadTooLarge, PAYLOAD_TOO_LARGE);
error_helper!(ErrorUriTooLong, URI_TOO_LONG);
error_helper!(ErrorUnsupportedMediaType, UNSUPPORTED_MEDIA_TYPE);
error_helper!(ErrorRangeNotSatisfiable, RANGE_NOT_SATISFIABLE);
error_helper!(ErrorImATeapot, IM_A_TEAPOT);
error_helper!(ErrorMisdirectedRequest, MISDIRECTED_REQUEST);
error_helper!(ErrorUnprocessableEntity, UNPROCESSABLE_ENTITY);
error_helper!(ErrorLocked, LOCKED);
error_helper!(ErrorFailedDependency, FAILED_DEPENDENCY);
error_helper!(ErrorUpgradeRequired, UPGRADE_REQUIRED);
error_helper!(ErrorPreconditionFailed, PRECONDITION_FAILED);
error_helper!(ErrorPreconditionRequired, PRECONDITION_REQUIRED);
error_helper!(ErrorTooManyRequests, TOO_MANY_REQUESTS);
error_helper!(
ErrorRequestHeaderFieldsTooLarge,
REQUEST_HEADER_FIELDS_TOO_LARGE
);
error_helper!(
ErrorUnavailableForLegalReasons,
UNAVAILABLE_FOR_LEGAL_REASONS
);
error_helper!(ErrorExpectationFailed, EXPECTATION_FAILED);
error_helper!(ErrorInternalServerError, INTERNAL_SERVER_ERROR);
error_helper!(ErrorNotImplemented, NOT_IMPLEMENTED);
error_helper!(ErrorBadGateway, BAD_GATEWAY);
error_helper!(ErrorServiceUnavailable, SERVICE_UNAVAILABLE);
error_helper!(ErrorGatewayTimeout, GATEWAY_TIMEOUT);
error_helper!(ErrorHttpVersionNotSupported, HTTP_VERSION_NOT_SUPPORTED);
error_helper!(ErrorVariantAlsoNegotiates, VARIANT_ALSO_NEGOTIATES);
error_helper!(ErrorInsufficientStorage, INSUFFICIENT_STORAGE);
error_helper!(ErrorLoopDetected, LOOP_DETECTED);
error_helper!(ErrorNotExtended, NOT_EXTENDED);
error_helper!(
ErrorNetworkAuthenticationRequired,
NETWORK_AUTHENTICATION_REQUIRED
);
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -635,42 +424,36 @@ mod tests {
#[test] #[test]
fn test_into_response() { fn test_into_response() {
let resp: Response<Body> = ParseError::Incomplete.error_response(); let resp: Response<AnyBody> = ParseError::Incomplete.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into(); let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into();
let resp: Response<Body> = err.error_response(); let resp: Response<AnyBody> = Error::new_http().with_cause(err).into();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
} }
#[test] #[test]
fn test_as_response() { fn test_as_response() {
let orig = io::Error::new(io::ErrorKind::Other, "other"); let orig = io::Error::new(io::ErrorKind::Other, "other");
let e: Error = ParseError::Io(orig).into(); let err: Error = ParseError::Io(orig).into();
assert_eq!(format!("{}", e.as_response_error()), "IO error: other"); assert_eq!(
} format!("{}", err),
"error parsing HTTP message: IO error: other"
#[test] );
fn test_error_cause() {
let orig = io::Error::new(io::ErrorKind::Other, "other");
let desc = orig.to_string();
let e = Error::from(orig);
assert_eq!(format!("{}", e.as_response_error()), desc);
} }
#[test] #[test]
fn test_error_display() { fn test_error_display() {
let orig = io::Error::new(io::ErrorKind::Other, "other"); let orig = io::Error::new(io::ErrorKind::Other, "other");
let desc = orig.to_string(); let err = Error::new_io().with_cause(orig);
let e = Error::from(orig); assert_eq!("connection error: other", err.to_string());
assert_eq!(format!("{}", e), desc);
} }
#[test] #[test]
fn test_error_http_response() { fn test_error_http_response() {
let orig = io::Error::new(io::ErrorKind::Other, "other"); let orig = io::Error::new(io::ErrorKind::Other, "other");
let e = Error::from(orig); let err = Error::new_io().with_cause(orig);
let resp: Response<Body> = e.into(); let resp: Response<AnyBody> = err.into();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
} }
@ -722,141 +505,4 @@ mod tests {
from!(httparse::Error::TooManyHeaders => ParseError::TooLarge); from!(httparse::Error::TooManyHeaders => ParseError::TooLarge);
from!(httparse::Error::Version => ParseError::Version); from!(httparse::Error::Version => ParseError::Version);
} }
#[test]
fn test_internal_error() {
let err = InternalError::from_response(ParseError::Method, Response::ok());
let resp: Response<Body> = err.error_response();
assert_eq!(resp.status(), StatusCode::OK);
}
#[test]
fn test_error_casting() {
let err = PayloadError::Overflow;
let resp_err: &dyn ResponseError = &err;
let err = resp_err.downcast_ref::<PayloadError>().unwrap();
assert_eq!(err.to_string(), "Payload reached size limit.");
let not_err = resp_err.downcast_ref::<ContentTypeError>();
assert!(not_err.is_none());
}
#[test]
fn test_error_helpers() {
let res: Response<Body> = ErrorBadRequest("err").into();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let res: Response<Body> = ErrorUnauthorized("err").into();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
let res: Response<Body> = ErrorPaymentRequired("err").into();
assert_eq!(res.status(), StatusCode::PAYMENT_REQUIRED);
let res: Response<Body> = ErrorForbidden("err").into();
assert_eq!(res.status(), StatusCode::FORBIDDEN);
let res: Response<Body> = ErrorNotFound("err").into();
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let res: Response<Body> = ErrorMethodNotAllowed("err").into();
assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED);
let res: Response<Body> = ErrorNotAcceptable("err").into();
assert_eq!(res.status(), StatusCode::NOT_ACCEPTABLE);
let res: Response<Body> = ErrorProxyAuthenticationRequired("err").into();
assert_eq!(res.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED);
let res: Response<Body> = ErrorRequestTimeout("err").into();
assert_eq!(res.status(), StatusCode::REQUEST_TIMEOUT);
let res: Response<Body> = ErrorConflict("err").into();
assert_eq!(res.status(), StatusCode::CONFLICT);
let res: Response<Body> = ErrorGone("err").into();
assert_eq!(res.status(), StatusCode::GONE);
let res: Response<Body> = ErrorLengthRequired("err").into();
assert_eq!(res.status(), StatusCode::LENGTH_REQUIRED);
let res: Response<Body> = ErrorPreconditionFailed("err").into();
assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED);
let res: Response<Body> = ErrorPayloadTooLarge("err").into();
assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
let res: Response<Body> = ErrorUriTooLong("err").into();
assert_eq!(res.status(), StatusCode::URI_TOO_LONG);
let res: Response<Body> = ErrorUnsupportedMediaType("err").into();
assert_eq!(res.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
let res: Response<Body> = ErrorRangeNotSatisfiable("err").into();
assert_eq!(res.status(), StatusCode::RANGE_NOT_SATISFIABLE);
let res: Response<Body> = ErrorExpectationFailed("err").into();
assert_eq!(res.status(), StatusCode::EXPECTATION_FAILED);
let res: Response<Body> = ErrorImATeapot("err").into();
assert_eq!(res.status(), StatusCode::IM_A_TEAPOT);
let res: Response<Body> = ErrorMisdirectedRequest("err").into();
assert_eq!(res.status(), StatusCode::MISDIRECTED_REQUEST);
let res: Response<Body> = ErrorUnprocessableEntity("err").into();
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
let res: Response<Body> = ErrorLocked("err").into();
assert_eq!(res.status(), StatusCode::LOCKED);
let res: Response<Body> = ErrorFailedDependency("err").into();
assert_eq!(res.status(), StatusCode::FAILED_DEPENDENCY);
let res: Response<Body> = ErrorUpgradeRequired("err").into();
assert_eq!(res.status(), StatusCode::UPGRADE_REQUIRED);
let res: Response<Body> = ErrorPreconditionRequired("err").into();
assert_eq!(res.status(), StatusCode::PRECONDITION_REQUIRED);
let res: Response<Body> = ErrorTooManyRequests("err").into();
assert_eq!(res.status(), StatusCode::TOO_MANY_REQUESTS);
let res: Response<Body> = ErrorRequestHeaderFieldsTooLarge("err").into();
assert_eq!(res.status(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE);
let res: Response<Body> = ErrorUnavailableForLegalReasons("err").into();
assert_eq!(res.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS);
let res: Response<Body> = ErrorInternalServerError("err").into();
assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
let res: Response<Body> = ErrorNotImplemented("err").into();
assert_eq!(res.status(), StatusCode::NOT_IMPLEMENTED);
let res: Response<Body> = ErrorBadGateway("err").into();
assert_eq!(res.status(), StatusCode::BAD_GATEWAY);
let res: Response<Body> = ErrorServiceUnavailable("err").into();
assert_eq!(res.status(), StatusCode::SERVICE_UNAVAILABLE);
let res: Response<Body> = ErrorGatewayTimeout("err").into();
assert_eq!(res.status(), StatusCode::GATEWAY_TIMEOUT);
let res: Response<Body> = ErrorHttpVersionNotSupported("err").into();
assert_eq!(res.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED);
let res: Response<Body> = ErrorVariantAlsoNegotiates("err").into();
assert_eq!(res.status(), StatusCode::VARIANT_ALSO_NEGOTIATES);
let res: Response<Body> = ErrorInsufficientStorage("err").into();
assert_eq!(res.status(), StatusCode::INSUFFICIENT_STORAGE);
let res: Response<Body> = ErrorLoopDetected("err").into();
assert_eq!(res.status(), StatusCode::LOOP_DETECTED);
let res: Response<Body> = ErrorNotExtended("err").into();
assert_eq!(res.status(), StatusCode::NOT_EXTENDED);
let res: Response<Body> = ErrorNetworkAuthenticationRequired("err").into();
assert_eq!(res.status(), StatusCode::NETWORK_AUTHENTICATION_REQUIRED);
}
} }

View File

@ -1,5 +1,6 @@
use std::{ use std::{
collections::VecDeque, collections::VecDeque,
error::Error as StdError,
fmt, fmt,
future::Future, future::Future,
io, mem, net, io, mem, net,
@ -17,19 +18,19 @@ use futures_core::ready;
use log::{error, trace}; use log::{error, trace};
use pin_project::pin_project; use pin_project::pin_project;
use crate::body::{Body, BodySize, MessageBody, ResponseBody}; use crate::{
use crate::config::ServiceConfig; body::{AnyBody, BodySize, MessageBody},
use crate::error::{DispatchError, Error}; config::ServiceConfig,
use crate::error::{ParseError, PayloadError}; error::{DispatchError, ParseError, PayloadError},
use crate::http::StatusCode; service::HttpFlow,
use crate::request::Request; OnConnectData, Request, Response, StatusCode,
use crate::response::Response; };
use crate::service::HttpFlow;
use crate::OnConnectData;
use super::codec::Codec; use super::{
use super::payload::{Payload, PayloadSender, PayloadStatus}; codec::Codec,
use super::{Message, MessageType}; payload::{Payload, PayloadSender, PayloadStatus},
Message, MessageType,
};
const LW_BUFFER_SIZE: usize = 1024; const LW_BUFFER_SIZE: usize = 1024;
const HW_BUFFER_SIZE: usize = 1024 * 8; const HW_BUFFER_SIZE: usize = 1024 * 8;
@ -50,10 +51,14 @@ bitflags! {
pub struct Dispatcher<T, S, B, X, U> pub struct Dispatcher<T, S, B, X, U>
where where
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
@ -68,10 +73,14 @@ where
enum DispatcherState<T, S, B, X, U> enum DispatcherState<T, S, B, X, U>
where where
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
@ -83,10 +92,14 @@ where
struct InnerDispatcher<T, S, B, X, U> struct InnerDispatcher<T, S, B, X, U>
where where
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
@ -122,19 +135,25 @@ enum State<S, B, X>
where where
S: Service<Request>, S: Service<Request>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{ {
None, None,
ExpectCall(#[pin] X::Future), ExpectCall(#[pin] X::Future),
ServiceCall(#[pin] S::Future), ServiceCall(#[pin] S::Future),
SendPayload(#[pin] ResponseBody<B>), SendPayload(#[pin] B),
SendErrorPayload(#[pin] AnyBody),
} }
impl<S, B, X> State<S, B, X> impl<S, B, X> State<S, B, X>
where where
S: Service<Request>, S: Service<Request>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{ {
fn is_empty(&self) -> bool { fn is_empty(&self) -> bool {
matches!(self, State::None) matches!(self, State::None)
@ -150,12 +169,17 @@ enum PollResponse {
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 where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
@ -206,12 +230,17 @@ where
impl<T, S, B, X, U> InnerDispatcher<T, S, B, X, U> impl<T, S, B, X, U> InnerDispatcher<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
@ -268,11 +297,11 @@ where
io.poll_flush(cx) io.poll_flush(cx)
} }
fn send_response( fn send_response_inner(
self: Pin<&mut Self>, self: Pin<&mut Self>,
message: Response<()>, message: Response<()>,
body: ResponseBody<B>, body: &impl MessageBody,
) -> Result<(), DispatchError> { ) -> Result<BodySize, DispatchError> {
let size = body.size(); let size = body.size();
let mut this = self.project(); let mut this = self.project();
this.codec this.codec
@ -285,10 +314,35 @@ where
})?; })?;
this.flags.set(Flags::KEEPALIVE, this.codec.keepalive()); this.flags.set(Flags::KEEPALIVE, this.codec.keepalive());
match size {
BodySize::None | BodySize::Empty => this.state.set(State::None), Ok(size)
_ => this.state.set(State::SendPayload(body)), }
fn send_response(
mut self: Pin<&mut Self>,
message: Response<()>,
body: B,
) -> Result<(), DispatchError> {
let size = self.as_mut().send_response_inner(message, &body)?;
let state = match size {
BodySize::None | BodySize::Empty => State::None,
_ => State::SendPayload(body),
}; };
self.project().state.set(state);
Ok(())
}
fn send_error_response(
mut self: Pin<&mut Self>,
message: Response<()>,
body: AnyBody,
) -> Result<(), DispatchError> {
let size = self.as_mut().send_response_inner(message, &body)?;
let state = match size {
BodySize::None | BodySize::Empty => State::None,
_ => State::SendErrorPayload(body),
};
self.project().state.set(state);
Ok(()) Ok(())
} }
@ -326,8 +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() self.as_mut().send_error_response(res, AnyBody::Empty)?;
.send_response(res, ResponseBody::Other(Body::Empty))?;
} }
// return with upgrade request and poll it exclusively. // return with upgrade request and poll it exclusively.
@ -347,9 +400,9 @@ where
// send service call error as response // send service call error as response
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res = Response::from_error(err.into()); let res: Response<AnyBody> = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.as_mut().send_response(res, body.into_body())?; self.as_mut().send_error_response(res, body)?;
} }
// service call pending and could be waiting for more chunk messages. // service call pending and could be waiting for more chunk messages.
@ -386,7 +439,42 @@ where
} }
Poll::Ready(Some(Err(err))) => { Poll::Ready(Some(Err(err))) => {
return Err(DispatchError::Service(err)) return Err(DispatchError::Body(err.into()))
}
Poll::Pending => return Ok(PollResponse::DoNothing),
}
}
// buffer is beyond max size.
// return and try to write the whole buffer to io stream.
return Ok(PollResponse::DrainWriteBuf);
}
StateProj::SendErrorPayload(mut stream) => {
// TODO: de-dupe impl with SendPayload
// keep populate writer buffer until buffer size limit hit,
// get blocked or finished.
while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE {
match stream.as_mut().poll_next(cx) {
Poll::Ready(Some(Ok(item))) => {
this.codec.encode(
Message::Chunk(Some(item)),
&mut this.write_buf,
)?;
}
Poll::Ready(None) => {
this.codec
.encode(Message::Chunk(None), &mut this.write_buf)?;
// payload stream finished.
// set state to None and handle next message
this.state.set(State::None);
continue 'res;
}
Poll::Ready(Some(Err(err))) => {
return Err(DispatchError::Service(err.into()))
} }
Poll::Pending => return Ok(PollResponse::DoNothing), Poll::Pending => return Ok(PollResponse::DoNothing),
@ -406,12 +494,14 @@ where
let fut = this.flow.service.call(req); let fut = this.flow.service.call(req);
this.state.set(State::ServiceCall(fut)); this.state.set(State::ServiceCall(fut));
} }
// send expect error as response // send expect error as response
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res = Response::from_error(err.into()); let res: Response<AnyBody> = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.as_mut().send_response(res, body.into_body())?; self.as_mut().send_error_response(res, body)?;
} }
// expect must be solved before progress can be made. // expect must be solved before progress can be made.
Poll::Pending => return Ok(PollResponse::DoNothing), Poll::Pending => return Ok(PollResponse::DoNothing),
}, },
@ -457,9 +547,9 @@ where
// to notify the dispatcher a new state is set and the outer loop // to notify the dispatcher a new state is set and the outer loop
// should be continue. // should be continue.
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res = Response::from_error(err.into()); let res: Response<AnyBody> = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
return self.send_response(res, body.into_body()); return self.send_error_response(res, body);
} }
} }
} }
@ -477,9 +567,9 @@ where
Poll::Pending => Ok(()), Poll::Pending => Ok(()),
// see the comment on ExpectCall state branch's Ready(Err(err)). // see the comment on ExpectCall state branch's Ready(Err(err)).
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res = Response::from_error(err.into()); let res: Response<AnyBody> = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.send_response(res, body.into_body()) self.send_error_response(res, body)
} }
}; };
} }
@ -599,8 +689,10 @@ where
} }
// Requests overflow buffer size should be responded with 431 // Requests overflow buffer size should be responded with 431
this.messages.push_back(DispatcherMessage::Error( this.messages.push_back(DispatcherMessage::Error(
Response::new(StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE) Response::with_body(
.drop_body(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE,
(),
),
)); ));
this.flags.insert(Flags::READ_DISCONNECT); this.flags.insert(Flags::READ_DISCONNECT);
*this.error = Some(ParseError::TooLarge.into()); *this.error = Some(ParseError::TooLarge.into());
@ -679,10 +771,9 @@ where
} else { } else {
// timeout on first request (slow request) return 408 // timeout on first request (slow request) return 408
trace!("Slow request timeout"); trace!("Slow request timeout");
let _ = self.as_mut().send_response( let _ = self.as_mut().send_error_response(
Response::new(StatusCode::REQUEST_TIMEOUT) Response::with_body(StatusCode::REQUEST_TIMEOUT, ()),
.drop_body(), AnyBody::Empty,
ResponseBody::Other(Body::Empty),
); );
this = self.project(); this = self.project();
this.flags.insert(Flags::STARTED | Flags::SHUTDOWN); this.flags.insert(Flags::STARTED | Flags::SHUTDOWN);
@ -817,12 +908,17 @@ where
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,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
@ -972,16 +1068,17 @@ mod tests {
} }
} }
fn ok_service() -> impl Service<Request, Response = Response<Body>, Error = Error> { fn ok_service() -> impl Service<Request, Response = Response<AnyBody>, Error = Error>
{
fn_service(|_req: Request| ready(Ok::<_, Error>(Response::ok()))) fn_service(|_req: Request| ready(Ok::<_, Error>(Response::ok())))
} }
fn echo_path_service( fn echo_path_service(
) -> impl Service<Request, Response = Response<Body>, Error = Error> { ) -> impl Service<Request, Response = Response<AnyBody>, Error = Error> {
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(Body::from_slice(path)), Response::ok().set_body(AnyBody::from_slice(path)),
)) ))
}) })
} }

View File

@ -6,14 +6,15 @@ use std::{cmp, io};
use bytes::{BufMut, BytesMut}; use bytes::{BufMut, BytesMut};
use crate::body::BodySize; use crate::{
use crate::config::ServiceConfig; body::BodySize,
use crate::header::{map::Value, HeaderName}; config::ServiceConfig,
use crate::helpers; header::{map::Value, HeaderMap, HeaderName},
use crate::http::header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING},
use crate::http::{HeaderMap, StatusCode, Version}; helpers,
use crate::message::{ConnectionType, RequestHeadType}; message::{ConnectionType, RequestHeadType},
use crate::response::Response; Response, StatusCode, Version,
};
const AVERAGE_HEADER_SIZE: usize = 30; const AVERAGE_HEADER_SIZE: usize = 30;
@ -287,7 +288,7 @@ impl MessageType for RequestHeadType {
let head = self.as_ref(); let head = self.as_ref();
dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE); dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE);
write!( write!(
helpers::Writer(dst), helpers::MutWriter(dst),
"{} {} {}", "{} {} {}",
head.method, head.method,
head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"), head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"),
@ -420,7 +421,7 @@ impl TransferEncoding {
*eof = true; *eof = true;
buf.extend_from_slice(b"0\r\n\r\n"); buf.extend_from_slice(b"0\r\n\r\n");
} else { } else {
writeln!(helpers::Writer(buf), "{:X}\r", msg.len()) writeln!(helpers::MutWriter(buf), "{:X}\r", msg.len())
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
buf.reserve(msg.len() + 2); buf.reserve(msg.len() + 2);
@ -630,8 +631,7 @@ mod tests {
async fn test_no_content_length() { async fn test_no_content_length() {
let mut bytes = BytesMut::with_capacity(2048); let mut bytes = BytesMut::with_capacity(2048);
let mut res: Response<()> = let mut res = Response::with_body(StatusCode::SWITCHING_PROTOCOLS, ());
Response::new(StatusCode::SWITCHING_PROTOCOLS).into_body::<()>();
res.headers_mut().insert(DATE, HeaderValue::from_static("")); res.headers_mut().insert(DATE, HeaderValue::from_static(""));
res.headers_mut() res.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")); .insert(CONTENT_LENGTH, HeaderValue::from_static("0"));

View File

@ -1,7 +1,11 @@
use std::marker::PhantomData; use std::{
use std::rc::Rc; error::Error as StdError,
use std::task::{Context, Poll}; fmt,
use std::{fmt, net}; marker::PhantomData,
net,
rc::Rc,
task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
@ -11,17 +15,15 @@ use actix_service::{
use actix_utils::future::ready; use actix_utils::future::ready;
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use crate::body::MessageBody; use crate::{
use crate::config::ServiceConfig; body::{AnyBody, MessageBody},
use crate::error::{DispatchError, Error}; config::ServiceConfig,
use crate::request::Request; error::DispatchError,
use crate::response::Response; service::HttpServiceHandler,
use crate::service::HttpServiceHandler; ConnectCallback, OnConnectData, Request, Response,
use crate::{ConnectCallback, OnConnectData}; };
use super::codec::Codec; use super::{codec::Codec, dispatcher::Dispatcher, ExpectHandler, UpgradeHandler};
use super::dispatcher::Dispatcher;
use super::{ExpectHandler, UpgradeHandler};
/// `ServiceFactory` implementation for HTTP1 transport /// `ServiceFactory` implementation for HTTP1 transport
pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> { pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> {
@ -36,7 +38,7 @@ pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> {
impl<T, S, B> H1Service<T, S, B> impl<T, S, B> H1Service<T, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
@ -61,17 +63,21 @@ impl<S, B, X, U> H1Service<TcpStream, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static, X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>, U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create simple tcp stream service /// Create simple tcp stream service
@ -106,21 +112,25 @@ mod openssl {
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static, X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, Codec>), (Request, Framed<TlsStream<TcpStream>, Codec>),
Config = (), Config = (),
Response = (), Response = (),
>, >,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create openssl based service /// Create openssl based service
@ -162,21 +172,25 @@ mod rustls {
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static, X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, Codec>), (Request, Framed<TlsStream<TcpStream>, Codec>),
Config = (), Config = (),
Response = (), Response = (),
>, >,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create rustls based service /// Create rustls based service
@ -205,7 +219,7 @@ mod rustls {
impl<T, S, B, X, U> H1Service<T, S, B, X, U> impl<T, S, B, X, U> H1Service<T, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
B: MessageBody, B: MessageBody,
@ -213,7 +227,7 @@ where
pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U> pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U>
where where
X1: ServiceFactory<Request, Response = Request>, X1: ServiceFactory<Request, Response = Request>,
X1::Error: Into<Error>, X1::Error: Into<Response<AnyBody>>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
{ {
H1Service { H1Service {
@ -253,19 +267,24 @@ impl<T, S, B, X, U> ServiceFactory<(T, Option<net::SocketAddr>)>
for H1Service<T, S, B, X, U> for H1Service<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, T: AsyncRead + AsyncWrite + Unpin + 'static,
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static, X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>, U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
type Response = (); type Response = ();
@ -319,14 +338,19 @@ impl<T, S, B, X, U> Service<(T, Option<net::SocketAddr>)>
for HttpServiceHandler<T, S, B, X, U> for HttpServiceHandler<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Response<AnyBody>>,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;

View File

@ -4,7 +4,7 @@ use std::task::{Context, Poll};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use crate::body::{BodySize, MessageBody, ResponseBody}; use crate::body::{BodySize, MessageBody};
use crate::error::Error; use crate::error::Error;
use crate::h1::{Codec, Message}; use crate::h1::{Codec, Message};
use crate::response::Response; use crate::response::Response;
@ -14,7 +14,7 @@ use crate::response::Response;
pub struct SendResponse<T, B> { pub struct SendResponse<T, B> {
res: Option<Message<(Response<()>, BodySize)>>, res: Option<Message<(Response<()>, BodySize)>>,
#[pin] #[pin]
body: Option<ResponseBody<B>>, body: Option<B>,
#[pin] #[pin]
framed: Option<Framed<T, Codec>>, framed: Option<Framed<T, Codec>>,
} }
@ -22,6 +22,7 @@ pub struct SendResponse<T, B> {
impl<T, B> SendResponse<T, B> impl<T, B> SendResponse<T, B>
where where
B: MessageBody, B: MessageBody,
B::Error: Into<Error>,
{ {
pub fn new(framed: Framed<T, Codec>, response: Response<B>) -> Self { pub fn new(framed: Framed<T, Codec>, response: Response<B>) -> Self {
let (res, body) = response.into_parts(); let (res, body) = response.into_parts();
@ -38,6 +39,7 @@ impl<T, B> Future for SendResponse<T, B>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
B: MessageBody + Unpin, B: MessageBody + Unpin,
B::Error: Into<Error>,
{ {
type Output = Result<Framed<T, Codec>, Error>; type Output = Result<Framed<T, Codec>, Error>;
@ -60,7 +62,18 @@ where
.unwrap() .unwrap()
.is_write_buf_full() .is_write_buf_full()
{ {
match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx)? { let next =
// TODO: MSRV 1.51: poll_map_err
match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx) {
Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(item)),
Poll::Ready(Some(Err(err))) => {
return Poll::Ready(Err(err.into()))
}
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
};
match next {
Poll::Ready(item) => { Poll::Ready(item) => {
// body is done when item is None // body is done when item is None
body_done = item.is_none(); body_done = item.is_none();
@ -68,7 +81,9 @@ where
let _ = this.body.take(); let _ = this.body.take();
} }
let framed = this.framed.as_mut().as_pin_mut().unwrap(); let framed = this.framed.as_mut().as_pin_mut().unwrap();
framed.write(Message::Chunk(item))?; framed.write(Message::Chunk(item)).map_err(|err| {
Error::new_send_response().with_cause(err)
})?;
} }
Poll::Pending => body_ready = false, Poll::Pending => body_ready = false,
} }
@ -79,7 +94,10 @@ where
// flush write buffer // flush write buffer
if !framed.is_write_buf_empty() { if !framed.is_write_buf_empty() {
match framed.flush(cx)? { match framed
.flush(cx)
.map_err(|err| Error::new_send_response().with_cause(err))?
{
Poll::Ready(_) => { Poll::Ready(_) => {
if body_ready { if body_ready {
continue; continue;
@ -93,7 +111,9 @@ where
// send response // send response
if let Some(res) = this.res.take() { if let Some(res) = this.res.take() {
framed.write(res)?; framed
.write(res)
.map_err(|err| Error::new_send_response().with_cause(err))?;
continue; continue;
} }

View File

@ -1,53 +1,46 @@
use std::task::{Context, Poll}; use std::{
use std::{cmp, future::Future, marker::PhantomData, net, pin::Pin, rc::Rc}; cmp,
error::Error as StdError,
future::Future,
marker::PhantomData,
net,
pin::Pin,
rc::Rc,
task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_service::Service; use actix_service::Service;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::ready; use futures_core::ready;
use h2::{ use h2::server::{Connection, SendResponse};
server::{Connection, SendResponse},
SendStream,
};
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 crate::body::{BodySize, MessageBody, ResponseBody}; use crate::{
use crate::config::ServiceConfig; body::{AnyBody, BodySize, MessageBody},
use crate::error::{DispatchError, Error}; config::ServiceConfig,
use crate::message::ResponseHead; service::HttpFlow,
use crate::payload::Payload; OnConnectData, Payload, Request, Response, ResponseHead,
use crate::request::Request; };
use crate::response::Response;
use crate::service::HttpFlow;
use crate::OnConnectData;
const CHUNK_SIZE: usize = 16_384; const CHUNK_SIZE: usize = 16_384;
/// Dispatcher for HTTP/2 protocol. pin_project! {
#[pin_project::pin_project] /// Dispatcher for HTTP/2 protocol.
pub struct Dispatcher<T, S, B, X, U> pub struct Dispatcher<T, S, B, X, U> {
where flow: Rc<HttpFlow<S, X, U>>,
T: AsyncRead + AsyncWrite + Unpin, connection: Connection<T, Bytes>,
S: Service<Request>, on_connect_data: OnConnectData,
B: MessageBody, config: ServiceConfig,
{ peer_addr: Option<net::SocketAddr>,
flow: Rc<HttpFlow<S, X, U>>, _phantom: PhantomData<B>,
connection: Connection<T, Bytes>, }
on_connect_data: OnConnectData,
config: ServiceConfig,
peer_addr: Option<net::SocketAddr>,
_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,
S: Service<Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
B: MessageBody,
{
pub(crate) fn new( pub(crate) fn new(
flow: Rc<HttpFlow<S, X, U>>, flow: Rc<HttpFlow<S, X, U>>,
connection: Connection<T, Bytes>, connection: Connection<T, Bytes>,
@ -55,7 +48,7 @@ where
config: ServiceConfig, config: ServiceConfig,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
) -> Self { ) -> Self {
Dispatcher { Self {
flow, flow,
config, config,
peer_addr, peer_addr,
@ -69,269 +62,208 @@ where
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,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>>,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>>,
B: MessageBody + 'static,
B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{ {
type Output = Result<(), DispatchError>; type Output = Result<(), crate::error::DispatchError>;
#[inline] #[inline]
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();
loop { while let Some((req, tx)) =
match ready!(Pin::new(&mut this.connection).poll_accept(cx)) { ready!(Pin::new(&mut this.connection).poll_accept(cx)?)
None => return Poll::Ready(Ok(())), {
let (parts, body) = req.into_parts();
let pl = crate::h2::Payload::new(body);
let pl = Payload::<crate::payload::PayloadStream>::H2(pl);
let mut req = Request::with_payload(pl);
Some(Err(err)) => return Poll::Ready(Err(err.into())), let head = req.head_mut();
head.uri = parts.uri;
head.method = parts.method;
head.version = parts.version;
head.headers = parts.headers.into();
head.peer_addr = this.peer_addr;
Some(Ok((req, res))) => { // merge on_connect_ext data into request extensions
let (parts, body) = req.into_parts(); this.on_connect_data.merge_into(&mut req);
let pl = crate::h2::Payload::new(body);
let pl = Payload::<crate::payload::PayloadStream>::H2(pl);
let mut req = Request::with_payload(pl);
let head = req.head_mut(); let fut = this.flow.service.call(req);
head.uri = parts.uri; let config = this.config.clone();
head.method = parts.method;
head.version = parts.version;
head.headers = parts.headers.into();
head.peer_addr = this.peer_addr;
// merge on_connect_ext data into request extensions // multiplex request handling with spawn task
this.on_connect_data.merge_into(&mut req); actix_rt::spawn(async move {
// resolve service call and send response.
let res = match fut.await {
Ok(res) => handle_response(res.into(), tx, config).await,
Err(err) => {
let res: Response<AnyBody> = err.into();
handle_response(res, tx, config).await
}
};
let svc = ServiceResponse { // log error.
state: ServiceResponseState::ServiceCall( if let Err(err) = res {
this.flow.service.call(req), match err {
Some(res), DispatchError::SendResponse(err) => {
), trace!("Error sending HTTP/2 response: {:?}", err)
config: this.config.clone(), }
buffer: None, DispatchError::SendData(err) => warn!("{:?}", err),
_phantom: PhantomData, DispatchError::ResponseBody(err) => {
}; error!("Response payload stream error: {:?}", err)
}
}
}
});
}
actix_rt::spawn(svc); Poll::Ready(Ok(()))
}
}
enum DispatchError {
SendResponse(h2::Error),
SendData(h2::Error),
ResponseBody(Box<dyn StdError>),
}
async fn handle_response<B>(
res: Response<B>,
mut tx: SendResponse<Bytes>,
config: ServiceConfig,
) -> Result<(), DispatchError>
where
B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{
let (res, body) = res.replace_body(());
// prepare response.
let mut size = body.size();
let res = prepare_response(config, res.head(), &mut size);
let eof = size.is_eof();
// send response head and return on eof.
let mut stream = tx
.send_response(res, eof)
.map_err(DispatchError::SendResponse)?;
if eof {
return Ok(());
}
// poll response body and send chunks to client.
actix_rt::pin!(body);
while let Some(res) = poll_fn(|cx| body.as_mut().poll_next(cx)).await {
let mut chunk = res.map_err(|err| DispatchError::ResponseBody(err.into()))?;
'send: loop {
// reserve enough space and wait for stream ready.
stream.reserve_capacity(cmp::min(chunk.len(), CHUNK_SIZE));
match poll_fn(|cx| stream.poll_capacity(cx)).await {
// No capacity left. drop body and return.
None => return Ok(()),
Some(res) => {
// Split chuck to writeable size and send to client.
let cap = res.map_err(DispatchError::SendData)?;
let len = chunk.len();
let bytes = chunk.split_to(cmp::min(cap, len));
stream
.send_data(bytes, false)
.map_err(DispatchError::SendData)?;
// Current chuck completely sent. break send loop and poll next one.
if chunk.is_empty() {
break 'send;
}
} }
} }
} }
} }
// response body streaming finished. send end of stream and return.
stream
.send_data(Bytes::new(), true)
.map_err(DispatchError::SendData)?;
Ok(())
} }
#[pin_project::pin_project] fn prepare_response(
struct ServiceResponse<F, I, E, B> {
#[pin]
state: ServiceResponseState<F, B>,
config: ServiceConfig, config: ServiceConfig,
buffer: Option<Bytes>, head: &ResponseHead,
_phantom: PhantomData<(I, E)>, size: &mut BodySize,
} ) -> http::Response<()> {
let mut has_date = false;
let mut skip_len = size != &BodySize::Stream;
#[pin_project::pin_project(project = ServiceResponseStateProj)] let mut res = http::Response::new(());
enum ServiceResponseState<F, B> { *res.status_mut() = head.status;
ServiceCall(#[pin] F, Option<SendResponse<Bytes>>), *res.version_mut() = http::Version::HTTP_2;
SendPayload(SendStream<Bytes>, #[pin] ResponseBody<B>),
}
impl<F, I, E, B> ServiceResponse<F, I, E, B> // Content length
where match head.status {
F: Future<Output = Result<I, E>>, http::StatusCode::NO_CONTENT
E: Into<Error>, | http::StatusCode::CONTINUE
I: Into<Response<B>>, | http::StatusCode::PROCESSING => *size = BodySize::None,
B: MessageBody, http::StatusCode::SWITCHING_PROTOCOLS => {
{ skip_len = true;
fn prepare_response( *size = BodySize::Stream;
&self, }
head: &ResponseHead, _ => {}
size: &mut BodySize, }
) -> http::Response<()> {
let mut has_date = false;
let mut skip_len = size != &BodySize::Stream;
let mut res = http::Response::new(()); let _ = match size {
*res.status_mut() = head.status; BodySize::None | BodySize::Stream => None,
*res.version_mut() = http::Version::HTTP_2; BodySize::Empty => res
.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
BodySize::Sized(len) => {
let mut buf = itoa::Buffer::new();
// Content length res.headers_mut().insert(
match head.status { CONTENT_LENGTH,
http::StatusCode::NO_CONTENT HeaderValue::from_str(buf.format(*len)).unwrap(),
| http::StatusCode::CONTINUE )
| http::StatusCode::PROCESSING => *size = BodySize::None, }
http::StatusCode::SWITCHING_PROTOCOLS => { };
skip_len = true;
*size = BodySize::Stream; // copy headers
} for (key, value) in head.headers.iter() {
match *key {
// TODO: consider skipping other headers according to:
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2
// omit HTTP/1.x only headers
CONNECTION | TRANSFER_ENCODING => continue,
CONTENT_LENGTH if skip_len => continue,
DATE => has_date = true,
_ => {} _ => {}
} }
let _ = match size { res.headers_mut().append(key, value.clone());
BodySize::None | BodySize::Stream => None,
BodySize::Empty => res
.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
BodySize::Sized(len) => {
let mut buf = itoa::Buffer::new();
res.headers_mut().insert(
CONTENT_LENGTH,
HeaderValue::from_str(buf.format(*len)).unwrap(),
)
}
};
// copy headers
for (key, value) in head.headers.iter() {
match *key {
// TODO: consider skipping other headers according to:
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2
// omit HTTP/1.x only headers
CONNECTION | TRANSFER_ENCODING => continue,
CONTENT_LENGTH if skip_len => continue,
DATE => has_date = true,
_ => {}
}
res.headers_mut().append(key, value.clone());
}
// set date header
if !has_date {
let mut bytes = BytesMut::with_capacity(29);
self.config.set_date_header(&mut bytes);
res.headers_mut().insert(
DATE,
// SAFETY: serialized date-times are known ASCII strings
unsafe { HeaderValue::from_maybe_shared_unchecked(bytes.freeze()) },
);
}
res
} }
}
impl<F, I, E, B> Future for ServiceResponse<F, I, E, B> // set date header
where if !has_date {
F: Future<Output = Result<I, E>>, let mut bytes = BytesMut::with_capacity(29);
E: Into<Error>, config.set_date_header(&mut bytes);
I: Into<Response<B>>, res.headers_mut().insert(
B: MessageBody, DATE,
{ // SAFETY: serialized date-times are known ASCII strings
type Output = (); unsafe { HeaderValue::from_maybe_shared_unchecked(bytes.freeze()) },
);
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut this = self.as_mut().project();
match this.state.project() {
ServiceResponseStateProj::ServiceCall(call, send) => {
match ready!(call.poll(cx)) {
Ok(res) => {
let (res, body) = res.into().replace_body(());
let mut send = send.take().unwrap();
let mut size = body.size();
let h2_res =
self.as_mut().prepare_response(res.head(), &mut size);
this = self.as_mut().project();
let stream = match send.send_response(h2_res, size.is_eof()) {
Err(e) => {
trace!("Error sending HTTP/2 response: {:?}", e);
return Poll::Ready(());
}
Ok(stream) => stream,
};
if size.is_eof() {
Poll::Ready(())
} else {
this.state
.set(ServiceResponseState::SendPayload(stream, body));
self.poll(cx)
}
}
Err(err) => {
let res = Response::from_error(err.into());
let (res, body) = res.replace_body(());
let mut send = send.take().unwrap();
let mut size = body.size();
let h2_res =
self.as_mut().prepare_response(res.head(), &mut size);
this = self.as_mut().project();
let stream = match send.send_response(h2_res, size.is_eof()) {
Err(e) => {
trace!("Error sending HTTP/2 response: {:?}", e);
return Poll::Ready(());
}
Ok(stream) => stream,
};
if size.is_eof() {
Poll::Ready(())
} else {
this.state.set(ServiceResponseState::SendPayload(
stream,
body.into_body(),
));
self.poll(cx)
}
}
}
}
ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => {
loop {
match this.buffer {
Some(ref mut buffer) => match ready!(stream.poll_capacity(cx)) {
None => return Poll::Ready(()),
Some(Ok(cap)) => {
let len = buffer.len();
let bytes = buffer.split_to(cmp::min(cap, len));
if let Err(e) = stream.send_data(bytes, false) {
warn!("{:?}", e);
return Poll::Ready(());
} else if !buffer.is_empty() {
let cap = cmp::min(buffer.len(), CHUNK_SIZE);
stream.reserve_capacity(cap);
} else {
this.buffer.take();
}
}
Some(Err(e)) => {
warn!("{:?}", e);
return Poll::Ready(());
}
},
None => match ready!(body.as_mut().poll_next(cx)) {
None => {
if let Err(e) = stream.send_data(Bytes::new(), true) {
warn!("{:?}", e);
}
return Poll::Ready(());
}
Some(Ok(chunk)) => {
stream
.reserve_capacity(cmp::min(chunk.len(), CHUNK_SIZE));
*this.buffer = Some(chunk);
}
Some(Err(e)) => {
error!("Response payload stream error: {:?}", e);
return Poll::Ready(());
}
},
}
}
}
}
} }
res
} }

View File

@ -1,8 +1,12 @@
use std::future::Future; use std::{
use std::marker::PhantomData; error::Error as StdError,
use std::pin::Pin; future::Future,
use std::task::{Context, Poll}; marker::PhantomData,
use std::{net, rc::Rc}; net,
pin::Pin,
rc::Rc,
task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
@ -13,16 +17,16 @@ use actix_service::{
use actix_utils::future::ready; use actix_utils::future::ready;
use bytes::Bytes; use bytes::Bytes;
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
use h2::server::{handshake, Handshake}; use h2::server::{handshake as h2_handshake, Handshake as H2Handshake};
use log::error; use log::error;
use crate::body::MessageBody; use crate::{
use crate::config::ServiceConfig; body::{AnyBody, MessageBody},
use crate::error::{DispatchError, Error}; config::ServiceConfig,
use crate::request::Request; error::DispatchError,
use crate::response::Response; service::HttpFlow,
use crate::service::HttpFlow; ConnectCallback, OnConnectData, Request, Response,
use crate::{ConnectCallback, OnConnectData}; };
use super::dispatcher::Dispatcher; use super::dispatcher::Dispatcher;
@ -37,10 +41,12 @@ pub struct H2Service<T, S, B> {
impl<T, S, B> H2Service<T, S, B> impl<T, S, B> H2Service<T, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
/// Create new `H2Service` instance with config. /// Create new `H2Service` instance with config.
pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>( pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
@ -66,10 +72,12 @@ impl<S, B> H2Service<TcpStream, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
/// Create plain TCP based service /// Create plain TCP based service
pub fn tcp( pub fn tcp(
@ -103,10 +111,12 @@ mod openssl {
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
/// Create OpenSSL based service /// Create OpenSSL based service
pub fn openssl( pub fn openssl(
@ -147,10 +157,12 @@ mod rustls {
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
/// Create Rustls based service /// Create Rustls based service
pub fn rustls( pub fn rustls(
@ -163,7 +175,8 @@ mod rustls {
Error = TlsError<io::Error, DispatchError>, Error = TlsError<io::Error, DispatchError>,
InitError = S::InitError, InitError = S::InitError,
> { > {
let protos = vec!["h2".to_string().into()]; let mut protos = vec![b"h2".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.set_protocols(&protos); config.set_protocols(&protos);
Acceptor::new(config) Acceptor::new(config)
@ -185,12 +198,15 @@ mod rustls {
impl<T, S, B> ServiceFactory<(T, Option<net::SocketAddr>)> for H2Service<T, S, B> impl<T, S, B> ServiceFactory<(T, Option<net::SocketAddr>)> for H2Service<T, S, B>
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, T: AsyncRead + AsyncWrite + Unpin + 'static,
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
@ -225,7 +241,7 @@ where
impl<T, S, B> H2ServiceHandler<T, S, B> impl<T, S, B> H2ServiceHandler<T, S, B>
where where
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
@ -248,10 +264,11 @@ impl<T, S, B> Service<(T, Option<net::SocketAddr>)> for H2ServiceHandler<T, S, B
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
@ -275,7 +292,7 @@ where
Some(self.cfg.clone()), Some(self.cfg.clone()),
addr, addr,
on_connect_data, on_connect_data,
handshake(io), h2_handshake(io),
), ),
} }
} }
@ -292,7 +309,7 @@ where
Option<ServiceConfig>, Option<ServiceConfig>,
Option<net::SocketAddr>, Option<net::SocketAddr>,
OnConnectData, OnConnectData,
Handshake<T, Bytes>, H2Handshake<T, Bytes>,
), ),
} }
@ -300,7 +317,7 @@ pub struct H2ServiceHandlerResponse<T, S, B>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
@ -312,10 +329,11 @@ impl<T, S, B> Future for H2ServiceHandlerResponse<T, S, B>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{ {
type Output = Result<(), DispatchError>; type Output = Result<(), DispatchError>;

View File

@ -8,40 +8,42 @@ use http::header::{HeaderName, InvalidHeaderName};
pub trait AsHeaderName: Sealed {} pub trait AsHeaderName: Sealed {}
pub struct Seal;
pub trait Sealed { pub trait Sealed {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName>; fn try_as_name(&self, seal: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName>;
} }
impl Sealed for HeaderName { impl Sealed for HeaderName {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> { fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
Ok(Cow::Borrowed(self)) Ok(Cow::Borrowed(self))
} }
} }
impl AsHeaderName for HeaderName {} impl AsHeaderName for HeaderName {}
impl Sealed for &HeaderName { impl Sealed for &HeaderName {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> { fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
Ok(Cow::Borrowed(*self)) Ok(Cow::Borrowed(*self))
} }
} }
impl AsHeaderName for &HeaderName {} impl AsHeaderName for &HeaderName {}
impl Sealed for &str { impl Sealed for &str {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> { fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned) HeaderName::from_str(self).map(Cow::Owned)
} }
} }
impl AsHeaderName for &str {} impl AsHeaderName for &str {}
impl Sealed for String { impl Sealed for String {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> { fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned) HeaderName::from_str(self).map(Cow::Owned)
} }
} }
impl AsHeaderName for String {} impl AsHeaderName for String {}
impl Sealed for &String { impl Sealed for &String {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> { fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned) HeaderName::from_str(self).map(Cow::Owned)
} }
} }

View File

@ -213,7 +213,7 @@ impl HeaderMap {
} }
fn get_value(&self, key: impl AsHeaderName) -> Option<&Value> { fn get_value(&self, key: impl AsHeaderName) -> Option<&Value> {
match key.try_as_name().ok()? { match key.try_as_name(super::as_name::Seal).ok()? {
Cow::Borrowed(name) => self.inner.get(name), Cow::Borrowed(name) => self.inner.get(name),
Cow::Owned(name) => self.inner.get(&name), Cow::Owned(name) => self.inner.get(&name),
} }
@ -279,7 +279,7 @@ impl HeaderMap {
/// assert!(map.get("INVALID HEADER NAME").is_none()); /// assert!(map.get("INVALID HEADER NAME").is_none());
/// ``` /// ```
pub fn get_mut(&mut self, key: impl AsHeaderName) -> Option<&mut HeaderValue> { pub fn get_mut(&mut self, key: impl AsHeaderName) -> Option<&mut HeaderValue> {
match key.try_as_name().ok()? { match key.try_as_name(super::as_name::Seal).ok()? {
Cow::Borrowed(name) => self.inner.get_mut(name).map(|v| v.first_mut()), Cow::Borrowed(name) => self.inner.get_mut(name).map(|v| v.first_mut()),
Cow::Owned(name) => self.inner.get_mut(&name).map(|v| v.first_mut()), Cow::Owned(name) => self.inner.get_mut(&name).map(|v| v.first_mut()),
} }
@ -327,7 +327,7 @@ impl HeaderMap {
/// assert!(map.contains_key(header::ACCEPT)); /// assert!(map.contains_key(header::ACCEPT));
/// ``` /// ```
pub fn contains_key(&self, key: impl AsHeaderName) -> bool { pub fn contains_key(&self, key: impl AsHeaderName) -> bool {
match key.try_as_name() { match key.try_as_name(super::as_name::Seal) {
Ok(Cow::Borrowed(name)) => self.inner.contains_key(name), Ok(Cow::Borrowed(name)) => self.inner.contains_key(name),
Ok(Cow::Owned(name)) => self.inner.contains_key(&name), Ok(Cow::Owned(name)) => self.inner.contains_key(&name),
Err(_) => false, Err(_) => false,
@ -410,7 +410,7 @@ impl HeaderMap {
/// ///
/// assert!(map.is_empty()); /// assert!(map.is_empty());
pub fn remove(&mut self, key: impl AsHeaderName) -> Removed { pub fn remove(&mut self, key: impl AsHeaderName) -> Removed {
let value = match key.try_as_name() { let value = match key.try_as_name(super::as_name::Seal) {
Ok(Cow::Borrowed(name)) => self.inner.remove(name), Ok(Cow::Borrowed(name)) => self.inner.remove(name),
Ok(Cow::Owned(name)) => self.inner.remove(&name), Ok(Cow::Owned(name)) => self.inner.remove(&name),
Err(_) => None, Err(_) => None,

View File

@ -104,7 +104,7 @@ impl Display for Charset {
impl FromStr for Charset { impl FromStr for Charset {
type Err = crate::Error; type Err = crate::Error;
fn from_str(s: &str) -> crate::Result<Charset> { fn from_str(s: &str) -> Result<Charset, crate::Error> {
Ok(match s.to_ascii_uppercase().as_ref() { Ok(match s.to_ascii_uppercase().as_ref() {
"US-ASCII" => Us_Ascii, "US-ASCII" => Us_Ascii,
"ISO-8859-1" => Iso_8859_1, "ISO-8859-1" => Iso_8859_1,

View File

@ -23,6 +23,9 @@ pub enum ContentEncoding {
/// Gzip algorithm. /// Gzip algorithm.
Gzip, Gzip,
// Zstd algorithm.
Zstd,
/// Indicates the identity function (i.e. no compression, nor modification). /// Indicates the identity function (i.e. no compression, nor modification).
Identity, Identity,
} }
@ -41,6 +44,7 @@ impl ContentEncoding {
ContentEncoding::Br => "br", ContentEncoding::Br => "br",
ContentEncoding::Gzip => "gzip", ContentEncoding::Gzip => "gzip",
ContentEncoding::Deflate => "deflate", ContentEncoding::Deflate => "deflate",
ContentEncoding::Zstd => "zstd",
ContentEncoding::Identity | ContentEncoding::Auto => "identity", ContentEncoding::Identity | ContentEncoding::Auto => "identity",
} }
} }
@ -53,6 +57,7 @@ impl ContentEncoding {
ContentEncoding::Gzip => 1.0, ContentEncoding::Gzip => 1.0,
ContentEncoding::Deflate => 0.9, ContentEncoding::Deflate => 0.9,
ContentEncoding::Identity | ContentEncoding::Auto => 0.1, ContentEncoding::Identity | ContentEncoding::Auto => 0.1,
ContentEncoding::Zstd => 0.0,
} }
} }
} }
@ -81,6 +86,8 @@ impl From<&str> for ContentEncoding {
ContentEncoding::Gzip ContentEncoding::Gzip
} else if val.eq_ignore_ascii_case("deflate") { } else if val.eq_ignore_ascii_case("deflate") {
ContentEncoding::Deflate ContentEncoding::Deflate
} else if val.eq_ignore_ascii_case("zstd") {
ContentEncoding::Zstd
} else { } else {
ContentEncoding::default() ContentEncoding::default()
} }

View File

@ -27,7 +27,9 @@ pub(crate) fn write_status_line<B: BufMut>(version: Version, n: u16, buf: &mut B
buf.put_u8(b' '); buf.put_u8(b' ');
} }
/// NOTE: bytes object has to contain enough space /// Write out content length header.
///
/// Buffer must to contain enough space or be implicitly extendable.
pub fn write_content_length<B: BufMut>(n: u64, buf: &mut B) { pub fn write_content_length<B: BufMut>(n: u64, buf: &mut B) {
if n == 0 { if n == 0 {
buf.put_slice(b"\r\ncontent-length: 0\r\n"); buf.put_slice(b"\r\ncontent-length: 0\r\n");
@ -41,10 +43,15 @@ pub fn write_content_length<B: BufMut>(n: u64, buf: &mut B) {
buf.put_slice(b"\r\n"); buf.put_slice(b"\r\n");
} }
// TODO: bench why this is needed /// An `io::Write`r that only requires mutable reference and assumes that there is space available
pub(crate) struct Writer<'a, B>(pub &'a mut B); /// in the buffer for every write operation or that it can be extended implicitly (like
/// `bytes::BytesMut`, for example).
///
/// This is slightly faster (~10%) than `bytes::buf::Writer` in such cases because it does not
/// perform a remaining length check before writing.
pub(crate) struct MutWriter<'a, B>(pub(crate) &'a mut B);
impl<'a, B> io::Write for Writer<'a, B> impl<'a, B> io::Write for MutWriter<'a, B>
where where
B: BufMut, B: BufMut,
{ {

View File

@ -1,12 +1,14 @@
//! HTTP primitives for the Actix ecosystem. //! HTTP primitives for the Actix ecosystem.
//! //!
//! ## Crate Features //! ## Crate Features
//! | Feature | Functionality | //! | Feature | Functionality |
//! | ---------------- | ----------------------------------------------------- | //! | ------------------- | ------------------------------------------- |
//! | `openssl` | TLS support via [OpenSSL]. | //! | `openssl` | TLS support via [OpenSSL]. |
//! | `rustls` | TLS support via [rustls]. | //! | `rustls` | TLS support via [rustls]. |
//! | `compress` | Payload compression support. (Deflate, Gzip & Brotli) | //! | `compress-brotli` | Payload compression support: Brotli. |
//! | `trust-dns` | Use [trust-dns] as the client DNS resolver. | //! | `compress-gzip` | Payload compression support: Deflate, Gzip. |
//! | `compress-zstd` | Payload compression support: Zstd. |
//! | `trust-dns` | Use [trust-dns] as the client DNS resolver. |
//! //!
//! [OpenSSL]: https://crates.io/crates/openssl //! [OpenSSL]: https://crates.io/crates/openssl
//! [rustls]: https://crates.io/crates/rustls //! [rustls]: https://crates.io/crates/rustls
@ -32,7 +34,8 @@ pub mod body;
mod builder; mod builder;
pub mod client; pub mod client;
mod config; mod config;
#[cfg(feature = "compress")]
#[cfg(feature = "__compress")]
pub mod encoding; pub mod encoding;
mod extensions; mod extensions;
pub mod header; pub mod header;
@ -54,7 +57,7 @@ pub mod ws;
pub use self::builder::HttpServiceBuilder; pub use self::builder::HttpServiceBuilder;
pub use self::config::{KeepAlive, ServiceConfig}; pub use self::config::{KeepAlive, ServiceConfig};
pub use self::error::{Error, ResponseError, Result}; pub use self::error::Error;
pub use self::extensions::Extensions; pub use self::extensions::Extensions;
pub use self::header::ContentEncoding; pub use self::header::ContentEncoding;
pub use self::http_message::HttpMessage; pub use self::http_message::HttpMessage;

View File

@ -15,8 +15,15 @@ macro_rules! downcast_get_type_id {
/// making it impossible for safe code to construct outside of /// making it impossible for safe code to construct outside of
/// this module. This ensures that safe code cannot violate /// this module. This ensures that safe code cannot violate
/// type-safety by implementing this method. /// type-safety by implementing this method.
///
/// We also take `PrivateHelper` as a parameter, to ensure that
/// safe code cannot obtain a `PrivateHelper` instance by
/// delegating to an existing implementation of `__private_get_type_id__`
#[doc(hidden)] #[doc(hidden)]
fn __private_get_type_id__(&self) -> (std::any::TypeId, PrivateHelper) fn __private_get_type_id__(
&self,
_: PrivateHelper,
) -> (std::any::TypeId, PrivateHelper)
where where
Self: 'static, Self: 'static,
{ {
@ -39,7 +46,9 @@ macro_rules! downcast {
impl dyn $name + 'static { impl dyn $name + 'static {
/// Downcasts generic body to a specific type. /// Downcasts generic body to a specific type.
pub fn downcast_ref<T: $name + 'static>(&self) -> Option<&T> { pub fn downcast_ref<T: $name + 'static>(&self) -> Option<&T> {
if self.__private_get_type_id__().0 == std::any::TypeId::of::<T>() { if self.__private_get_type_id__(PrivateHelper(())).0
== std::any::TypeId::of::<T>()
{
// SAFETY: external crates cannot override the default // SAFETY: external crates cannot override the default
// implementation of `__private_get_type_id__`, since // implementation of `__private_get_type_id__`, since
// it requires returning a private type. We can therefore // it requires returning a private type. We can therefore
@ -53,7 +62,9 @@ macro_rules! downcast {
/// Downcasts a generic body to a mutable specific type. /// Downcasts a generic body to a mutable specific type.
pub fn downcast_mut<T: $name + 'static>(&mut self) -> Option<&mut T> { pub fn downcast_mut<T: $name + 'static>(&mut self) -> Option<&mut T> {
if self.__private_get_type_id__().0 == std::any::TypeId::of::<T>() { if self.__private_get_type_id__(PrivateHelper(())).0
== std::any::TypeId::of::<T>()
{
// SAFETY: external crates cannot override the default // SAFETY: external crates cannot override the default
// implementation of `__private_get_type_id__`, since // implementation of `__private_get_type_id__`, since
// it requires returning a private type. We can therefore // it requires returning a private type. We can therefore

View File

@ -293,14 +293,14 @@ impl ResponseHead {
} }
} }
#[inline]
/// Check if keep-alive is enabled /// Check if keep-alive is enabled
#[inline]
pub fn keep_alive(&self) -> bool { pub fn keep_alive(&self) -> bool {
self.connection_type() == ConnectionType::KeepAlive self.connection_type() == ConnectionType::KeepAlive
} }
#[inline]
/// Check upgrade status of this message /// Check upgrade status of this message
#[inline]
pub fn upgrade(&self) -> bool { pub fn upgrade(&self) -> bool {
self.connection_type() == ConnectionType::Upgrade self.connection_type() == ConnectionType::Upgrade
} }
@ -389,12 +389,6 @@ impl BoxedResponseHead {
pub fn new(status: StatusCode) -> Self { pub fn new(status: StatusCode) -> Self {
RESPONSE_POOL.with(|p| p.get_message(status)) RESPONSE_POOL.with(|p| p.get_message(status))
} }
pub(crate) fn take(&mut self) -> Self {
BoxedResponseHead {
head: self.head.take(),
}
}
} }
impl std::ops::Deref for BoxedResponseHead { impl std::ops::Deref for BoxedResponseHead {

View File

@ -2,17 +2,13 @@
use std::{ use std::{
cell::{Ref, RefMut}, cell::{Ref, RefMut},
fmt, fmt, str,
future::Future,
pin::Pin,
str,
task::{Context, Poll},
}; };
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use crate::{ use crate::{
body::{Body, MessageBody, ResponseBody}, body::{AnyBody, MessageBody},
error::Error, error::Error,
extensions::Extensions, extensions::Extensions,
http::{HeaderMap, StatusCode}, http::{HeaderMap, StatusCode},
@ -23,22 +19,20 @@ use crate::{
/// An HTTP response. /// An HTTP response.
pub struct Response<B> { pub struct Response<B> {
pub(crate) head: BoxedResponseHead, pub(crate) head: BoxedResponseHead,
pub(crate) body: ResponseBody<B>, pub(crate) body: B,
pub(crate) error: Option<Error>,
} }
impl Response<Body> { impl Response<AnyBody> {
/// Constructs a response /// Constructs a new response with default body.
#[inline] #[inline]
pub fn new(status: StatusCode) -> Response<Body> { pub fn new(status: StatusCode) -> Self {
Response { Response {
head: BoxedResponseHead::new(status), head: BoxedResponseHead::new(status),
body: ResponseBody::Body(Body::Empty), body: AnyBody::Empty,
error: None,
} }
} }
/// Create HTTP response builder with specific status. /// Constructs a new response builder.
#[inline] #[inline]
pub fn build(status: StatusCode) -> ResponseBuilder { pub fn build(status: StatusCode) -> ResponseBuilder {
ResponseBuilder::new(status) ResponseBuilder::new(status)
@ -47,202 +41,164 @@ impl Response<Body> {
// just a couple frequently used shortcuts // just a couple frequently used shortcuts
// this list should not grow larger than a few // this list should not grow larger than a few
/// Creates a new response with status 200 OK. /// Constructs a new response with status 200 OK.
#[inline] #[inline]
pub fn ok() -> Response<Body> { pub fn ok() -> Self {
Response::new(StatusCode::OK) Response::new(StatusCode::OK)
} }
/// Creates a new response with status 400 Bad Request. /// Constructs a new response with status 400 Bad Request.
#[inline] #[inline]
pub fn bad_request() -> Response<Body> { pub fn bad_request() -> Self {
Response::new(StatusCode::BAD_REQUEST) Response::new(StatusCode::BAD_REQUEST)
} }
/// Creates a new response with status 404 Not Found. /// Constructs a new response with status 404 Not Found.
#[inline] #[inline]
pub fn not_found() -> Response<Body> { pub fn not_found() -> Self {
Response::new(StatusCode::NOT_FOUND) Response::new(StatusCode::NOT_FOUND)
} }
/// Creates a new response with status 500 Internal Server Error. /// Constructs a new response with status 500 Internal Server Error.
#[inline] #[inline]
pub fn internal_server_error() -> Response<Body> { pub fn internal_server_error() -> Self {
Response::new(StatusCode::INTERNAL_SERVER_ERROR) Response::new(StatusCode::INTERNAL_SERVER_ERROR)
} }
// end shortcuts // end shortcuts
/// Constructs an error response
#[inline]
pub fn from_error(error: Error) -> Response<Body> {
let mut resp = error.as_response_error().error_response();
if resp.head.status == StatusCode::INTERNAL_SERVER_ERROR {
error!("Internal Server Error: {:?}", error);
}
resp.error = Some(error);
resp
}
/// Convert response to response with body
pub fn into_body<B>(self) -> Response<B> {
let b = match self.body {
ResponseBody::Body(b) => b,
ResponseBody::Other(b) => b,
};
Response {
head: self.head,
error: self.error,
body: ResponseBody::Other(b),
}
}
} }
impl<B> Response<B> { impl<B> Response<B> {
/// Constructs a response with body /// Constructs a new response with given body.
#[inline] #[inline]
pub fn with_body(status: StatusCode, body: B) -> Response<B> { pub fn with_body(status: StatusCode, body: B) -> Response<B> {
Response { Response {
head: BoxedResponseHead::new(status), head: BoxedResponseHead::new(status),
body: ResponseBody::Body(body), body,
error: None,
} }
} }
/// Returns a reference to the head of this response.
#[inline] #[inline]
/// Http message part of the response
pub fn head(&self) -> &ResponseHead { pub fn head(&self) -> &ResponseHead {
&*self.head &*self.head
} }
/// Returns a mutable reference to the head of this response.
#[inline] #[inline]
/// Mutable reference to a HTTP message part of the response
pub fn head_mut(&mut self) -> &mut ResponseHead { pub fn head_mut(&mut self) -> &mut ResponseHead {
&mut *self.head &mut *self.head
} }
/// The source `error` for this response /// Returns the status code of this response.
#[inline]
pub fn error(&self) -> Option<&Error> {
self.error.as_ref()
}
/// Get the response status code
#[inline] #[inline]
pub fn status(&self) -> StatusCode { pub fn status(&self) -> StatusCode {
self.head.status self.head.status
} }
/// Set the `StatusCode` for this response /// Returns a mutable reference the status code of this response.
#[inline] #[inline]
pub fn status_mut(&mut self) -> &mut StatusCode { pub fn status_mut(&mut self) -> &mut StatusCode {
&mut self.head.status &mut self.head.status
} }
/// Get the headers from the response /// Returns a reference to response headers.
#[inline] #[inline]
pub fn headers(&self) -> &HeaderMap { pub fn headers(&self) -> &HeaderMap {
&self.head.headers &self.head.headers
} }
/// Get a mutable reference to the headers /// Returns a mutable reference to response headers.
#[inline] #[inline]
pub fn headers_mut(&mut self) -> &mut HeaderMap { pub fn headers_mut(&mut self) -> &mut HeaderMap {
&mut self.head.headers &mut self.head.headers
} }
/// Connection upgrade status /// Returns true if connection upgrade is enabled.
#[inline] #[inline]
pub fn upgrade(&self) -> bool { pub fn upgrade(&self) -> bool {
self.head.upgrade() self.head.upgrade()
} }
/// Keep-alive status for this connection /// Returns true if keep-alive is enabled.
pub fn keep_alive(&self) -> bool { pub fn keep_alive(&self) -> bool {
self.head.keep_alive() self.head.keep_alive()
} }
/// Responses extensions /// Returns a reference to the extensions of this response.
#[inline] #[inline]
pub fn extensions(&self) -> Ref<'_, Extensions> { pub fn extensions(&self) -> Ref<'_, Extensions> {
self.head.extensions.borrow() self.head.extensions.borrow()
} }
/// Mutable reference to a the response's extensions /// Returns a mutable reference to the extensions of this response.
#[inline] #[inline]
pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> { pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> {
self.head.extensions.borrow_mut() self.head.extensions.borrow_mut()
} }
/// Get body of this response /// Returns a reference to the body of this response.
#[inline] #[inline]
pub fn body(&self) -> &ResponseBody<B> { pub fn body(&self) -> &B {
&self.body &self.body
} }
/// Set a body /// Sets new body.
pub fn set_body<B2>(self, body: B2) -> Response<B2> { pub fn set_body<B2>(self, body: B2) -> Response<B2> {
Response { Response {
head: self.head, head: self.head,
body: ResponseBody::Body(body), body,
error: None,
} }
} }
/// Split response and body /// Drops body and returns new response.
pub fn into_parts(self) -> (Response<()>, ResponseBody<B>) {
(
Response {
head: self.head,
body: ResponseBody::Body(()),
error: self.error,
},
self.body,
)
}
/// Drop request's body
pub fn drop_body(self) -> Response<()> { pub fn drop_body(self) -> Response<()> {
Response { self.set_body(())
head: self.head,
body: ResponseBody::Body(()),
error: None,
}
} }
/// Set a body and return previous body value /// Sets new body, returning new response and previous body value.
pub(crate) fn replace_body<B2>(self, body: B2) -> (Response<B2>, ResponseBody<B>) { pub(crate) fn replace_body<B2>(self, body: B2) -> (Response<B2>, B) {
( (
Response { Response {
head: self.head, head: self.head,
body: ResponseBody::Body(body), body,
error: self.error,
}, },
self.body, self.body,
) )
} }
/// Set a body and return previous body value /// Returns split head and body.
///
/// # Implementation Notes
/// Due to internal performance optimisations, the first element of the returned tuple is a
/// `Response` as well but only contains the head of the response this was called on.
pub fn into_parts(self) -> (Response<()>, B) {
self.replace_body(())
}
/// Returns new response with mapped body.
pub fn map_body<F, B2>(mut self, f: F) -> Response<B2> pub fn map_body<F, B2>(mut self, f: F) -> Response<B2>
where where
F: FnOnce(&mut ResponseHead, ResponseBody<B>) -> ResponseBody<B2>, F: FnOnce(&mut ResponseHead, B) -> B2,
{ {
let body = f(&mut self.head, self.body); let body = f(&mut self.head, self.body);
Response { Response {
body,
head: self.head, head: self.head,
error: self.error, body,
} }
} }
/// Extract response body /// Returns body, consuming this response.
pub fn take_body(&mut self) -> ResponseBody<B> { pub fn into_body(self) -> B {
self.body.take_body() self.body
} }
} }
impl<B: MessageBody> fmt::Debug for Response<B> { impl<B> fmt::Debug for Response<B>
where
B: MessageBody,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let res = writeln!( let res = writeln!(
f, f,
@ -260,20 +216,16 @@ impl<B: MessageBody> fmt::Debug for Response<B> {
} }
} }
impl<B: Unpin> Future for Response<B> { impl<B: Default> Default for Response<B> {
type Output = Result<Response<B>, Error>; #[inline]
fn default() -> Response<B> {
fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> { Response::with_body(StatusCode::default(), B::default())
Poll::Ready(Ok(Response {
head: self.head.take(),
body: self.body.take_body(),
error: self.error.take(),
}))
} }
} }
/// Helper converters impl<I: Into<Response<AnyBody>>, E: Into<Error>> From<Result<I, E>>
impl<I: Into<Response<Body>>, E: Into<Error>> From<Result<I, E>> for Response<Body> { for Response<AnyBody>
{
fn from(res: Result<I, E>) -> Self { fn from(res: Result<I, E>) -> Self {
match res { match res {
Ok(val) => val.into(), Ok(val) => val.into(),
@ -282,13 +234,19 @@ impl<I: Into<Response<Body>>, E: Into<Error>> From<Result<I, E>> for Response<Bo
} }
} }
impl From<ResponseBuilder> for Response<Body> { impl From<ResponseBuilder> for Response<AnyBody> {
fn from(mut builder: ResponseBuilder) -> Self { fn from(mut builder: ResponseBuilder) -> Self {
builder.finish() builder.finish()
} }
} }
impl From<&'static str> for Response<Body> { impl From<std::convert::Infallible> for Response<AnyBody> {
fn from(val: std::convert::Infallible) -> Self {
match val {}
}
}
impl From<&'static str> for Response<AnyBody> {
fn from(val: &'static str) -> Self { fn from(val: &'static str) -> Self {
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.content_type(mime::TEXT_PLAIN_UTF_8) .content_type(mime::TEXT_PLAIN_UTF_8)
@ -296,7 +254,7 @@ impl From<&'static str> for Response<Body> {
} }
} }
impl From<&'static [u8]> for Response<Body> { impl From<&'static [u8]> for Response<AnyBody> {
fn from(val: &'static [u8]) -> Self { fn from(val: &'static [u8]) -> Self {
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.content_type(mime::APPLICATION_OCTET_STREAM) .content_type(mime::APPLICATION_OCTET_STREAM)
@ -304,7 +262,7 @@ impl From<&'static [u8]> for Response<Body> {
} }
} }
impl From<String> for Response<Body> { impl From<String> for Response<AnyBody> {
fn from(val: String) -> Self { fn from(val: String) -> Self {
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.content_type(mime::TEXT_PLAIN_UTF_8) .content_type(mime::TEXT_PLAIN_UTF_8)
@ -312,7 +270,7 @@ impl From<String> for Response<Body> {
} }
} }
impl<'a> From<&'a String> for Response<Body> { impl<'a> From<&'a String> for Response<AnyBody> {
fn from(val: &'a String) -> Self { fn from(val: &'a String) -> Self {
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.content_type(mime::TEXT_PLAIN_UTF_8) .content_type(mime::TEXT_PLAIN_UTF_8)
@ -320,7 +278,7 @@ impl<'a> From<&'a String> for Response<Body> {
} }
} }
impl From<Bytes> for Response<Body> { impl From<Bytes> for Response<AnyBody> {
fn from(val: Bytes) -> Self { fn from(val: Bytes) -> Self {
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.content_type(mime::APPLICATION_OCTET_STREAM) .content_type(mime::APPLICATION_OCTET_STREAM)
@ -328,7 +286,7 @@ impl From<Bytes> for Response<Body> {
} }
} }
impl From<BytesMut> for Response<Body> { impl From<BytesMut> for Response<AnyBody> {
fn from(val: BytesMut) -> Self { fn from(val: BytesMut) -> Self {
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.content_type(mime::APPLICATION_OCTET_STREAM) .content_type(mime::APPLICATION_OCTET_STREAM)
@ -339,7 +297,6 @@ impl From<BytesMut> for Response<Body> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::body::Body;
use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE};
#[test] #[test]
@ -354,7 +311,7 @@ mod tests {
#[test] #[test]
fn test_into_response() { fn test_into_response() {
let resp: Response<Body> = "test".into(); let resp: Response<AnyBody> = "test".into();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(), resp.headers().get(CONTENT_TYPE).unwrap(),
@ -363,7 +320,7 @@ mod tests {
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.body().get_ref(), b"test"); assert_eq!(resp.body().get_ref(), b"test");
let resp: Response<Body> = b"test".as_ref().into(); let resp: Response<AnyBody> = b"test".as_ref().into();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(), resp.headers().get(CONTENT_TYPE).unwrap(),
@ -372,7 +329,7 @@ mod tests {
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.body().get_ref(), b"test"); assert_eq!(resp.body().get_ref(), b"test");
let resp: Response<Body> = "test".to_owned().into(); let resp: Response<AnyBody> = "test".to_owned().into();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(), resp.headers().get(CONTENT_TYPE).unwrap(),
@ -381,7 +338,7 @@ mod tests {
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.body().get_ref(), b"test"); assert_eq!(resp.body().get_ref(), b"test");
let resp: Response<Body> = (&"test".to_owned()).into(); let resp: Response<AnyBody> = (&"test".to_owned()).into();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(), resp.headers().get(CONTENT_TYPE).unwrap(),
@ -391,7 +348,7 @@ mod tests {
assert_eq!(resp.body().get_ref(), b"test"); assert_eq!(resp.body().get_ref(), b"test");
let b = Bytes::from_static(b"test"); let b = Bytes::from_static(b"test");
let resp: Response<Body> = b.into(); let resp: Response<AnyBody> = b.into();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(), resp.headers().get(CONTENT_TYPE).unwrap(),
@ -401,7 +358,7 @@ mod tests {
assert_eq!(resp.body().get_ref(), b"test"); assert_eq!(resp.body().get_ref(), b"test");
let b = Bytes::from_static(b"test"); let b = Bytes::from_static(b"test");
let resp: Response<Body> = b.into(); let resp: Response<AnyBody> = b.into();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(), resp.headers().get(CONTENT_TYPE).unwrap(),
@ -411,7 +368,7 @@ mod tests {
assert_eq!(resp.body().get_ref(), b"test"); assert_eq!(resp.body().get_ref(), b"test");
let b = BytesMut::from("test"); let b = BytesMut::from("test");
let resp: Response<Body> = b.into(); let resp: Response<AnyBody> = b.into();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(), resp.headers().get(CONTENT_TYPE).unwrap(),

View File

@ -2,6 +2,7 @@
use std::{ use std::{
cell::{Ref, RefMut}, cell::{Ref, RefMut},
error::Error as StdError,
fmt, fmt,
future::Future, future::Future,
pin::Pin, pin::Pin,
@ -13,7 +14,7 @@ use bytes::Bytes;
use futures_core::Stream; use futures_core::Stream;
use crate::{ use crate::{
body::{Body, BodyStream, ResponseBody}, body::{AnyBody, BodyStream},
error::{Error, HttpError}, error::{Error, HttpError},
header::{self, IntoHeaderPair, IntoHeaderValue}, header::{self, IntoHeaderPair, IntoHeaderValue},
message::{BoxedResponseHead, ConnectionType, ResponseHead}, message::{BoxedResponseHead, ConnectionType, ResponseHead},
@ -38,10 +39,11 @@ use crate::{
/// .body("1234"); /// .body("1234");
/// ///
/// assert_eq!(res.status(), StatusCode::OK); /// assert_eq!(res.status(), StatusCode::OK);
/// assert_eq!(body::to_bytes(res.take_body()).await.unwrap(), &b"1234"[..]);
/// ///
/// assert!(res.headers().contains_key("server")); /// assert!(res.headers().contains_key("server"));
/// assert_eq!(res.headers().get_all("set-cookie").count(), 2); /// assert_eq!(res.headers().get_all("set-cookie").count(), 2);
///
/// assert_eq!(body::to_bytes(res.into_body()).await.unwrap(), &b"1234"[..]);
/// # }) /// # })
/// ``` /// ```
pub struct ResponseBuilder { pub struct ResponseBuilder {
@ -234,45 +236,41 @@ impl ResponseBuilder {
/// ///
/// This `ResponseBuilder` will be left in a useless state. /// This `ResponseBuilder` will be left in a useless state.
#[inline] #[inline]
pub fn body<B: Into<Body>>(&mut self, body: B) -> Response<Body> { pub fn body<B: Into<AnyBody>>(&mut self, body: B) -> Response<AnyBody> {
self.message_body(body.into()) self.message_body(body.into())
.unwrap_or_else(Response::from)
} }
/// Generate response with a body. /// Generate response with a body.
/// ///
/// This `ResponseBuilder` will be left in a useless state. /// This `ResponseBuilder` will be left in a useless state.
pub fn message_body<B>(&mut self, body: B) -> Response<B> { pub fn message_body<B>(&mut self, body: B) -> Result<Response<B>, Error> {
if let Some(e) = self.err.take() { if let Some(err) = self.err.take() {
return Response::from(Error::from(e)).into_body(); return Err(Error::new_http().with_cause(err));
} }
let response = self.head.take().expect("cannot reuse response builder"); let head = self.head.take().expect("cannot reuse response builder");
Ok(Response { head, body })
Response {
head: response,
body: ResponseBody::Body(body),
error: None,
}
} }
/// Generate response with a streaming body. /// Generate response with a streaming body.
/// ///
/// This `ResponseBuilder` will be left in a useless state. /// This `ResponseBuilder` will be left in a useless state.
#[inline] #[inline]
pub fn streaming<S, E>(&mut self, stream: S) -> Response<Body> pub fn streaming<S, E>(&mut self, stream: S) -> Response<AnyBody>
where where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static, S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Error> + 'static, E: Into<Box<dyn StdError>> + 'static,
{ {
self.body(Body::from_message(BodyStream::new(stream))) self.body(AnyBody::from_message(BodyStream::new(stream)))
} }
/// Generate response with an empty body. /// Generate response with an empty body.
/// ///
/// 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<Body> { pub fn finish(&mut self) -> Response<AnyBody> {
self.body(Body::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.
@ -330,7 +328,7 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder {
} }
impl Future for ResponseBuilder { impl Future for ResponseBuilder {
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> {
Poll::Ready(Ok(self.finish())) Poll::Ready(Ok(self.finish()))

View File

@ -1,4 +1,5 @@
use std::{ use std::{
error::Error as StdError,
fmt, fmt,
future::Future, future::Future,
marker::PhantomData, marker::PhantomData,
@ -8,6 +9,7 @@ use std::{
task::{Context, Poll}, task::{Context, Poll},
}; };
use ::h2::server::{handshake as h2_handshake, Handshake as H2Handshake};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
use actix_service::{ use actix_service::{
@ -15,16 +17,15 @@ use actix_service::{
}; };
use bytes::Bytes; use bytes::Bytes;
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
use h2::server::{handshake, Handshake};
use pin_project::pin_project; use pin_project::pin_project;
use crate::body::MessageBody; use crate::{
use crate::builder::HttpServiceBuilder; body::{AnyBody, MessageBody},
use crate::config::{KeepAlive, ServiceConfig}; builder::HttpServiceBuilder,
use crate::error::{DispatchError, Error}; config::{KeepAlive, ServiceConfig},
use crate::request::Request; error::DispatchError,
use crate::response::Response; h1, h2, ConnectCallback, OnConnectData, Protocol, Request, Response,
use crate::{h1, h2::Dispatcher, ConnectCallback, OnConnectData, Protocol}; };
/// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol. /// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol.
pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> { pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> {
@ -39,7 +40,7 @@ pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> {
impl<T, S, B> HttpService<T, S, B> impl<T, S, B> HttpService<T, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
@ -54,11 +55,12 @@ where
impl<T, S, B> HttpService<T, S, B> impl<T, S, B> HttpService<T, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
/// Create new `HttpService` instance. /// Create new `HttpService` instance.
pub fn new<F: IntoServiceFactory<S, Request>>(service: F) -> Self { pub fn new<F: IntoServiceFactory<S, Request>>(service: F) -> Self {
@ -93,7 +95,7 @@ where
impl<T, S, B, X, U> HttpService<T, S, B, X, U> impl<T, S, B, X, U> HttpService<T, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
@ -107,7 +109,7 @@ where
pub fn expect<X1>(self, expect: X1) -> HttpService<T, S, B, X1, U> pub fn expect<X1>(self, expect: X1) -> HttpService<T, S, B, X1, U>
where where
X1: ServiceFactory<Request, Config = (), Response = Request>, X1: ServiceFactory<Request, Config = (), Response = Request>,
X1::Error: Into<Error>, X1::Error: Into<Response<AnyBody>>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
{ {
HttpService { HttpService {
@ -151,16 +153,17 @@ impl<S, B, X, U> HttpService<TcpStream, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static, X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
@ -169,7 +172,7 @@ where
Response = (), Response = (),
>, >,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create simple tcp stream service /// Create simple tcp stream service
@ -202,16 +205,17 @@ mod openssl {
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static, X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
@ -220,7 +224,7 @@ mod openssl {
Response = (), Response = (),
>, >,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create openssl based service /// Create openssl based service
@ -269,16 +273,17 @@ mod rustls {
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static, X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
@ -287,7 +292,7 @@ mod rustls {
Response = (), Response = (),
>, >,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create rustls based service /// Create rustls based service
@ -301,7 +306,8 @@ mod rustls {
Error = TlsError<io::Error, DispatchError>, Error = TlsError<io::Error, DispatchError>,
InitError = (), InitError = (),
> { > {
let protos = vec!["h2".to_string().into(), "http/1.1".to_string().into()]; let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.set_protocols(&protos); config.set_protocols(&protos);
Acceptor::new(config) Acceptor::new(config)
@ -333,21 +339,22 @@ where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static, X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>, U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
type Response = (); type Response = ();
@ -410,11 +417,11 @@ where
impl<T, S, B, X, U> HttpServiceHandler<T, S, B, X, U> impl<T, S, B, X, U> HttpServiceHandler<T, S, B, X, U>
where where
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
X: Service<Request>, X: Service<Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, h1::Codec>)>, U: Service<(Request, Framed<T, h1::Codec>)>,
U::Error: Into<Error>, U::Error: Into<Response<AnyBody>>,
{ {
pub(super) fn new( pub(super) fn new(
cfg: ServiceConfig, cfg: ServiceConfig,
@ -431,7 +438,10 @@ where
} }
} }
pub(super) fn _poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Error>> { pub(super) fn _poll_ready(
&self,
cx: &mut Context<'_>,
) -> Poll<Result<(), Response<AnyBody>>> {
ready!(self.flow.expect.poll_ready(cx).map_err(Into::into))?; ready!(self.flow.expect.poll_ready(cx).map_err(Into::into))?;
ready!(self.flow.service.poll_ready(cx).map_err(Into::into))?; ready!(self.flow.service.poll_ready(cx).map_err(Into::into))?;
@ -465,15 +475,20 @@ impl<T, S, B, X, U> Service<(T, Protocol, Option<net::SocketAddr>)>
for HttpServiceHandler<T, S, B, X, U> for HttpServiceHandler<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>, U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Response<AnyBody>>,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
@ -496,7 +511,7 @@ where
match proto { match proto {
Protocol::Http2 => HttpServiceHandlerResponse { Protocol::Http2 => HttpServiceHandlerResponse {
state: State::H2Handshake(Some(( state: State::H2Handshake(Some((
handshake(io), h2_handshake(io),
self.cfg.clone(), self.cfg.clone(),
self.flow.clone(), self.flow.clone(),
on_connect_data, on_connect_data,
@ -522,21 +537,26 @@ where
#[pin_project(project = StateProj)] #[pin_project(project = StateProj)]
enum State<T, S, B, X, U> enum State<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
T: AsyncRead + AsyncWrite + Unpin,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>, U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
H1(#[pin] h1::Dispatcher<T, S, B, X, U>), H1(#[pin] h1::Dispatcher<T, S, B, X, U>),
H2(#[pin] Dispatcher<T, S, B, X, U>), H2(#[pin] h2::Dispatcher<T, S, B, X, U>),
H2Handshake( H2Handshake(
Option<( Option<(
Handshake<T, Bytes>, H2Handshake<T, Bytes>,
ServiceConfig, ServiceConfig,
Rc<HttpFlow<S, X, U>>, Rc<HttpFlow<S, X, U>>,
OnConnectData, OnConnectData,
@ -549,13 +569,18 @@ where
pub struct HttpServiceHandlerResponse<T, S, B, X, U> pub struct HttpServiceHandlerResponse<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static,
B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>, U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
@ -566,13 +591,18 @@ where
impl<T, S, B, X, U> Future for HttpServiceHandlerResponse<T, S, B, X, U> impl<T, S, B, X, U> Future for HttpServiceHandlerResponse<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody,
B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>, U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
@ -587,13 +617,15 @@ where
Ok(conn) => { Ok(conn) => {
let (_, cfg, srv, on_connect_data, peer_addr) = let (_, cfg, srv, on_connect_data, peer_addr) =
data.take().unwrap(); data.take().unwrap();
self.as_mut().project().state.set(State::H2(Dispatcher::new( self.as_mut().project().state.set(State::H2(
srv, h2::Dispatcher::new(
conn, srv,
on_connect_data, conn,
cfg, on_connect_data,
peer_addr, cfg,
))); peer_addr,
),
));
self.poll(cx) self.poll(cx)
} }
Err(err) => { Err(err) => {

View File

@ -72,7 +72,7 @@ mod inner {
use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed};
use crate::ResponseError; use crate::{body::AnyBody, Response};
/// Framed transport errors /// Framed transport errors
pub enum DispatcherError<E, U, I> pub enum DispatcherError<E, U, I>
@ -136,13 +136,16 @@ mod inner {
} }
} }
impl<E, U, I> ResponseError for DispatcherError<E, U, I> impl<E, U, I> From<DispatcherError<E, U, I>> for Response<AnyBody>
where where
E: fmt::Debug + fmt::Display, E: fmt::Debug + fmt::Display,
U: Encoder<I> + Decoder, U: Encoder<I> + Decoder,
<U as Encoder<I>>::Error: fmt::Debug, <U as Encoder<I>>::Error: fmt::Debug,
<U as Decoder>::Error: fmt::Debug, <U as Decoder>::Error: fmt::Debug,
{ {
fn from(err: DispatcherError<E, U, I>) -> Self {
Response::internal_server_error().set_body(AnyBody::from(err.to_string()))
}
} }
/// Message type wrapper for signalling end of message stream. /// Message type wrapper for signalling end of message stream.

View File

@ -9,8 +9,8 @@ use derive_more::{Display, Error, From};
use http::{header, Method, StatusCode}; use http::{header, Method, StatusCode};
use crate::{ use crate::{
body::Body, error::ResponseError, header::HeaderValue, message::RequestHead, body::AnyBody, header::HeaderValue, message::RequestHead, response::Response,
response::Response, ResponseBuilder, ResponseBuilder,
}; };
mod codec; mod codec;
@ -25,7 +25,7 @@ pub use self::frame::Parser;
pub use self::proto::{hash_key, CloseCode, CloseReason, OpCode}; pub use self::proto::{hash_key, CloseCode, CloseReason, OpCode};
/// WebSocket protocol errors. /// WebSocket protocol errors.
#[derive(Debug, Display, From, Error)] #[derive(Debug, Display, Error, From)]
pub enum ProtocolError { pub enum ProtocolError {
/// Received an unmasked frame from client. /// Received an unmasked frame from client.
#[display(fmt = "Received an unmasked frame from client.")] #[display(fmt = "Received an unmasked frame from client.")]
@ -68,10 +68,8 @@ pub enum ProtocolError {
Io(io::Error), Io(io::Error),
} }
impl ResponseError for ProtocolError {}
/// WebSocket handshake errors /// WebSocket handshake errors
#[derive(PartialEq, Debug, Display)] #[derive(Debug, PartialEq, Display, Error)]
pub enum HandshakeError { pub enum HandshakeError {
/// Only get method is allowed. /// Only get method is allowed.
#[display(fmt = "Method not allowed.")] #[display(fmt = "Method not allowed.")]
@ -98,44 +96,55 @@ pub enum HandshakeError {
BadWebsocketKey, BadWebsocketKey,
} }
impl ResponseError for HandshakeError { impl From<&HandshakeError> for Response<AnyBody> {
fn error_response(&self) -> Response<Body> { fn from(err: &HandshakeError) -> Self {
match self { match err {
HandshakeError::GetMethodRequired => { HandshakeError::GetMethodRequired => {
Response::build(StatusCode::METHOD_NOT_ALLOWED) let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED);
.insert_header((header::ALLOW, "GET")) res.headers_mut()
.finish() .insert(header::ALLOW, HeaderValue::from_static("GET"));
res
} }
HandshakeError::NoWebsocketUpgrade => { HandshakeError::NoWebsocketUpgrade => {
Response::build(StatusCode::BAD_REQUEST) let mut res = Response::bad_request();
.reason("No WebSocket Upgrade header found") res.head_mut().reason = Some("No WebSocket Upgrade header found");
.finish() res
} }
HandshakeError::NoConnectionUpgrade => { HandshakeError::NoConnectionUpgrade => {
Response::build(StatusCode::BAD_REQUEST) let mut res = Response::bad_request();
.reason("No Connection upgrade") res.head_mut().reason = Some("No Connection upgrade");
.finish() res
} }
HandshakeError::NoVersionHeader => Response::build(StatusCode::BAD_REQUEST) HandshakeError::NoVersionHeader => {
.reason("WebSocket version header is required") let mut res = Response::bad_request();
.finish(), res.head_mut().reason = Some("WebSocket version header is required");
res
}
HandshakeError::UnsupportedVersion => { HandshakeError::UnsupportedVersion => {
Response::build(StatusCode::BAD_REQUEST) let mut res = Response::bad_request();
.reason("Unsupported WebSocket version") res.head_mut().reason = Some("Unsupported WebSocket version");
.finish() res
} }
HandshakeError::BadWebsocketKey => Response::build(StatusCode::BAD_REQUEST) HandshakeError::BadWebsocketKey => {
.reason("Handshake error") let mut res = Response::bad_request();
.finish(), res.head_mut().reason = Some("Handshake error");
res
}
} }
} }
} }
impl From<HandshakeError> for Response<AnyBody> {
fn from(err: HandshakeError) -> Self {
(&err).into()
}
}
/// Verify WebSocket handshake request and create handshake response. /// Verify WebSocket handshake request and create handshake response.
pub fn handshake(req: &RequestHead) -> Result<ResponseBuilder, HandshakeError> { pub fn handshake(req: &RequestHead) -> Result<ResponseBuilder, HandshakeError> {
verify_handshake(req)?; verify_handshake(req)?;
@ -213,7 +222,7 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::test::TestRequest; use crate::{body::AnyBody, test::TestRequest};
use http::{header, Method}; use http::{header, Method};
#[test] #[test]
@ -327,18 +336,18 @@ mod tests {
} }
#[test] #[test]
fn test_wserror_http_response() { fn test_ws_error_http_response() {
let resp = HandshakeError::GetMethodRequired.error_response(); let resp: Response<AnyBody> = HandshakeError::GetMethodRequired.into();
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let resp = HandshakeError::NoWebsocketUpgrade.error_response(); let resp: Response<AnyBody> = HandshakeError::NoWebsocketUpgrade.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp = HandshakeError::NoConnectionUpgrade.error_response(); let resp: Response<AnyBody> = HandshakeError::NoConnectionUpgrade.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp = HandshakeError::NoVersionHeader.error_response(); let resp: Response<AnyBody> = HandshakeError::NoVersionHeader.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp = HandshakeError::UnsupportedVersion.error_response(); let resp: Response<AnyBody> = HandshakeError::UnsupportedVersion.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp = HandshakeError::BadWebsocketKey.error_response(); let resp: Response<AnyBody> = HandshakeError::BadWebsocketKey.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
} }
} }

View File

@ -1,10 +1,13 @@
use std::convert::Infallible;
use actix_http::{ use actix_http::{
error, http, http::StatusCode, HttpMessage, HttpService, Request, Response, body::AnyBody, http, http::StatusCode, HttpMessage, HttpService, Request, Response,
}; };
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_service::ServiceFactoryExt; use actix_service::ServiceFactoryExt;
use actix_utils::future; use actix_utils::future;
use bytes::Bytes; use bytes::Bytes;
use derive_more::{Display, Error};
use futures_util::StreamExt as _; use futures_util::StreamExt as _;
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 \
@ -33,7 +36,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
async fn test_h1_v2() { async fn test_h1_v2() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|_| future::ok::<_, ()>(Response::ok().set_body(STR))) .finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR)))
.tcp() .tcp()
}) })
.await; .await;
@ -61,7 +64,7 @@ async fn test_h1_v2() {
async fn test_connection_close() { async fn test_connection_close() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|_| future::ok::<_, ()>(Response::ok().set_body(STR))) .finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR)))
.tcp() .tcp()
.map(|_| ()) .map(|_| ())
}) })
@ -75,11 +78,11 @@ async fn test_connection_close() {
async fn test_with_query_parameter() { async fn test_with_query_parameter() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|req: Request| { .finish(|req: Request| async move {
if req.uri().query().unwrap().contains("qp=") { if req.uri().query().unwrap().contains("qp=") {
future::ok::<_, ()>(Response::ok()) Ok::<_, Infallible>(Response::ok())
} else { } else {
future::ok::<_, ()>(Response::bad_request()) Ok(Response::bad_request())
} }
}) })
.tcp() .tcp()
@ -92,6 +95,16 @@ async fn test_with_query_parameter() {
assert!(response.status().is_success()); assert!(response.status().is_success());
} }
#[derive(Debug, Display, Error)]
#[display(fmt = "expect failed")]
struct ExpectFailed;
impl From<ExpectFailed> for Response<AnyBody> {
fn from(_: ExpectFailed) -> Self {
Response::new(StatusCode::EXPECTATION_FAILED)
}
}
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_expect() { async fn test_h1_expect() {
let srv = test_server(move || { let srv = test_server(move || {
@ -100,7 +113,7 @@ async fn test_h1_expect() {
if req.headers().contains_key("AUTH") { if req.headers().contains_key("AUTH") {
Ok(req) Ok(req)
} else { } else {
Err(error::ErrorExpectationFailed("expect failed")) Err(ExpectFailed)
} }
}) })
.h1(|req: Request| async move { .h1(|req: Request| async move {
@ -112,7 +125,7 @@ async fn test_h1_expect() {
let str = std::str::from_utf8(&buf).unwrap(); let str = std::str::from_utf8(&buf).unwrap();
assert_eq!(str, "expect body"); assert_eq!(str, "expect body");
Ok::<_, ()>(Response::ok()) Ok::<_, Infallible>(Response::ok())
}) })
.tcp() .tcp()
}) })
@ -134,7 +147,7 @@ async fn test_h1_expect() {
let response = request.send_body("expect body").await.unwrap(); let response = request.send_body("expect body").await.unwrap();
assert_eq!(response.status(), StatusCode::EXPECTATION_FAILED); assert_eq!(response.status(), StatusCode::EXPECTATION_FAILED);
// test exepct would continue // test expect would continue
let request = srv let request = srv
.request(http::Method::GET, srv.url("/")) .request(http::Method::GET, srv.url("/"))
.insert_header(("Expect", "100-continue")) .insert_header(("Expect", "100-continue"))

View File

@ -2,11 +2,11 @@
extern crate tls_openssl as openssl; extern crate tls_openssl as openssl;
use std::io; use std::{convert::Infallible, io};
use actix_http::{ use actix_http::{
body::{Body, SizedStream}, body::{AnyBody, Body, SizedStream},
error::{ErrorBadRequest, PayloadError}, error::PayloadError,
http::{ http::{
header::{self, HeaderName, HeaderValue}, header::{self, HeaderName, HeaderValue},
Method, StatusCode, Version, Method, StatusCode, Version,
@ -17,6 +17,7 @@ use actix_http_test::test_server;
use actix_service::{fn_service, ServiceFactoryExt}; use actix_service::{fn_service, ServiceFactoryExt};
use actix_utils::future::{err, ok, ready}; use actix_utils::future::{err, ok, ready};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
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 openssl::{ use openssl::{
@ -135,7 +136,7 @@ async fn test_h2_content_length() {
StatusCode::OK, StatusCode::OK,
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
]; ];
ok::<_, ()>(Response::new(statuses[idx])) ok::<_, Infallible>(Response::new(statuses[idx]))
}) })
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
@ -205,7 +206,7 @@ async fn test_h2_headers() {
TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ",
)); ));
} }
ok::<_, ()>(builder.body(data.clone())) ok::<_, Infallible>(builder.body(data.clone()))
}) })
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
@ -245,7 +246,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
async fn test_h2_body2() { async fn test_h2_body2() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, ()>(Response::ok().set_body(STR))) .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -263,7 +264,7 @@ async fn test_h2_body2() {
async fn test_h2_head_empty() { async fn test_h2_head_empty() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|_| ok::<_, ()>(Response::ok().set_body(STR))) .finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -287,7 +288,7 @@ async fn test_h2_head_empty() {
async fn test_h2_head_binary() { async fn test_h2_head_binary() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, ()>(Response::ok().set_body(STR))) .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -310,7 +311,7 @@ async fn test_h2_head_binary() {
async fn test_h2_head_binary2() { async fn test_h2_head_binary2() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, ()>(Response::ok().set_body(STR))) .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -329,9 +330,12 @@ async fn test_h2_head_binary2() {
async fn test_h2_body_length() { async fn test_h2_body_length() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| { .h2(|_| async {
let body = once(ok(Bytes::from_static(STR.as_ref()))); let body = once(async {
ok::<_, ()>( Ok::<_, Infallible>(Bytes::from_static(STR.as_ref()))
});
Ok::<_, Infallible>(
Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), Response::ok().set_body(SizedStream::new(STR.len() as u64, body)),
) )
}) })
@ -354,7 +358,7 @@ async fn test_h2_body_chunked_explicit() {
HttpService::build() HttpService::build()
.h2(|_| { .h2(|_| {
let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref())));
ok::<_, ()>( ok::<_, Infallible>(
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header((header::TRANSFER_ENCODING, "chunked"))
.streaming(body), .streaming(body),
@ -382,7 +386,7 @@ async fn test_h2_response_http_error_handling() {
HttpService::build() HttpService::build()
.h2(fn_service(|_| { .h2(fn_service(|_| {
let broken_header = Bytes::from_static(b"\0\0\0"); let broken_header = Bytes::from_static(b"\0\0\0");
ok::<_, ()>( ok::<_, Infallible>(
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.insert_header((header::CONTENT_TYPE, broken_header)) .insert_header((header::CONTENT_TYPE, broken_header))
.body(STR), .body(STR),
@ -398,14 +402,27 @@ async fn test_h2_response_http_error_handling() {
// 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"failed to parse header value")); assert_eq!(
bytes,
Bytes::from_static(b"error processing HTTP: failed to parse header value")
);
}
#[derive(Debug, Display, Error)]
#[display(fmt = "error")]
struct BadRequest;
impl From<BadRequest> for Response<AnyBody> {
fn from(err: BadRequest) -> Self {
Response::build(StatusCode::BAD_REQUEST).body(err.to_string())
}
} }
#[actix_rt::test] #[actix_rt::test]
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>, Error>(ErrorBadRequest("error"))) .h2(|_| err::<Response<Body>, _>(BadRequest))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -428,7 +445,7 @@ async fn test_h2_on_connect() {
}) })
.h2(|req: Request| { .h2(|req: Request| {
assert!(req.extensions().contains::<isize>()); assert!(req.extensions().contains::<isize>());
ok::<_, ()>(Response::ok()) ok::<_, Infallible>(Response::ok())
}) })
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())

View File

@ -2,9 +2,16 @@
extern crate tls_rustls as rustls; extern crate tls_rustls as rustls;
use std::{
convert::Infallible,
io::{self, BufReader, Write},
net::{SocketAddr, TcpStream as StdTcpStream},
sync::Arc,
};
use actix_http::{ use actix_http::{
body::{Body, SizedStream}, body::{AnyBody, Body, SizedStream},
error::{self, PayloadError}, error::PayloadError,
http::{ http::{
header::{self, HeaderName, HeaderValue}, header::{self, HeaderName, HeaderValue},
Method, StatusCode, Version, Method, StatusCode, Version,
@ -14,16 +21,15 @@ use actix_http::{
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_service::{fn_factory_with_config, fn_service}; use actix_service::{fn_factory_with_config, fn_service};
use actix_utils::future::{err, ok}; use actix_utils::future::{err, ok};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
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::{
internal::pemfile::{certs, pkcs8_private_keys}, internal::pemfile::{certs, pkcs8_private_keys},
NoClientAuth, ServerConfig as RustlsServerConfig, NoClientAuth, ServerConfig as RustlsServerConfig, Session,
}; };
use webpki::DNSNameRef;
use std::io::{self, BufReader};
async fn load_body<S>(mut stream: S) -> Result<BytesMut, PayloadError> async fn load_body<S>(mut stream: S) -> Result<BytesMut, PayloadError>
where where
@ -52,6 +58,25 @@ fn tls_config() -> RustlsServerConfig {
config config
} }
pub fn get_negotiated_alpn_protocol(
addr: SocketAddr,
client_alpn_protocol: &[u8],
) -> Option<Vec<u8>> {
let mut config = rustls::ClientConfig::new();
config.alpn_protocols.push(client_alpn_protocol.to_vec());
let mut sess = rustls::ClientSession::new(
&Arc::new(config),
DNSNameRef::try_from_ascii_str("localhost").unwrap(),
);
let mut sock = StdTcpStream::connect(addr).unwrap();
let mut stream = rustls::Stream::new(&mut sess, &mut sock);
// The handshake will fails because the client will not be able to verify the server
// certificate, but it doesn't matter here as we are just interested in the negotiated ALPN
// protocol
let _ = stream.flush();
sess.get_alpn_protocol().map(|proto| proto.to_vec())
}
#[actix_rt::test] #[actix_rt::test]
async fn test_h1() -> io::Result<()> { async fn test_h1() -> io::Result<()> {
let srv = test_server(move || { let srv = test_server(move || {
@ -149,7 +174,7 @@ async fn test_h2_content_length() {
StatusCode::OK, StatusCode::OK,
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
]; ];
ok::<_, ()>(Response::new(statuses[indx])) ok::<_, Infallible>(Response::new(statuses[indx]))
}) })
.rustls(tls_config()) .rustls(tls_config())
}) })
@ -218,7 +243,7 @@ async fn test_h2_headers() {
TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ",
)); ));
} }
ok::<_, ()>(config.body(data.clone())) ok::<_, Infallible>(config.body(data.clone()))
}) })
.rustls(tls_config()) .rustls(tls_config())
}).await; }).await;
@ -257,7 +282,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
async fn test_h2_body2() { async fn test_h2_body2() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, ()>(Response::ok().set_body(STR))) .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -274,7 +299,7 @@ async fn test_h2_body2() {
async fn test_h2_head_empty() { async fn test_h2_head_empty() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|_| ok::<_, ()>(Response::ok().set_body(STR))) .finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -300,7 +325,7 @@ async fn test_h2_head_empty() {
async fn test_h2_head_binary() { async fn test_h2_head_binary() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, ()>(Response::ok().set_body(STR))) .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -325,7 +350,7 @@ async fn test_h2_head_binary() {
async fn test_h2_head_binary2() { async fn test_h2_head_binary2() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, ()>(Response::ok().set_body(STR))) .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -347,8 +372,8 @@ async fn test_h2_body_length() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| { .h2(|_| {
let body = once(ok(Bytes::from_static(STR.as_ref()))); let body = once(ok::<_, Infallible>(Bytes::from_static(STR.as_ref())));
ok::<_, ()>( ok::<_, Infallible>(
Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), Response::ok().set_body(SizedStream::new(STR.len() as u64, body)),
) )
}) })
@ -370,7 +395,7 @@ async fn test_h2_body_chunked_explicit() {
HttpService::build() HttpService::build()
.h2(|_| { .h2(|_| {
let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref())));
ok::<_, ()>( ok::<_, Infallible>(
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header((header::TRANSFER_ENCODING, "chunked"))
.streaming(body), .streaming(body),
@ -396,9 +421,9 @@ async fn test_h2_response_http_error_handling() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(fn_factory_with_config(|_: ()| { .h2(fn_factory_with_config(|_: ()| {
ok::<_, ()>(fn_service(|_| { ok::<_, Infallible>(fn_service(|_| {
let broken_header = Bytes::from_static(b"\0\0\0"); let broken_header = Bytes::from_static(b"\0\0\0");
ok::<_, ()>( ok::<_, Infallible>(
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.insert_header((http::header::CONTENT_TYPE, broken_header)) .insert_header((http::header::CONTENT_TYPE, broken_header))
.body(STR), .body(STR),
@ -414,14 +439,27 @@ async fn test_h2_response_http_error_handling() {
// 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"failed to parse header value")); assert_eq!(
bytes,
Bytes::from_static(b"error processing HTTP: failed to parse header value")
);
}
#[derive(Debug, Display, Error)]
#[display(fmt = "error")]
struct BadRequest;
impl From<BadRequest> for Response<AnyBody> {
fn from(_: BadRequest) -> Self {
Response::bad_request().set_body(AnyBody::from("error"))
}
} }
#[actix_rt::test] #[actix_rt::test]
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>, Error>(error::ErrorBadRequest("error"))) .h2(|_| err::<Response<Body>, _>(BadRequest))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -438,7 +476,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>, Error>(error::ErrorBadRequest("error"))) .h1(|_| err::<Response<Body>, _>(BadRequest))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -450,3 +488,85 @@ async fn test_h1_service_error() {
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"));
} }
const H2_ALPN_PROTOCOL: &[u8] = b"h2";
const HTTP1_1_ALPN_PROTOCOL: &[u8] = b"http/1.1";
const CUSTOM_ALPN_PROTOCOL: &[u8] = b"custom";
#[actix_rt::test]
async fn test_alpn_h1() -> io::Result<()> {
let srv = test_server(move || {
let mut config = tls_config();
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.h1(|_| ok::<_, Error>(Response::ok()))
.rustls(config)
})
.await;
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL),
Some(CUSTOM_ALPN_PROTOCOL.to_vec())
);
let response = srv.sget("/").send().await.unwrap();
assert!(response.status().is_success());
Ok(())
}
#[actix_rt::test]
async fn test_alpn_h2() -> io::Result<()> {
let srv = test_server(move || {
let mut config = tls_config();
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.h2(|_| ok::<_, Error>(Response::ok()))
.rustls(config)
})
.await;
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), H2_ALPN_PROTOCOL),
Some(H2_ALPN_PROTOCOL.to_vec())
);
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL),
Some(CUSTOM_ALPN_PROTOCOL.to_vec())
);
let response = srv.sget("/").send().await.unwrap();
assert!(response.status().is_success());
Ok(())
}
#[actix_rt::test]
async fn test_alpn_h2_1() -> io::Result<()> {
let srv = test_server(move || {
let mut config = tls_config();
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.finish(|_| ok::<_, Error>(Response::ok()))
.rustls(config)
})
.await;
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), H2_ALPN_PROTOCOL),
Some(H2_ALPN_PROTOCOL.to_vec())
);
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), HTTP1_1_ALPN_PROTOCOL),
Some(HTTP1_1_ALPN_PROTOCOL.to_vec())
);
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL),
Some(CUSTOM_ALPN_PROTOCOL.to_vec())
);
let response = srv.sget("/").send().await.unwrap();
assert!(response.status().is_success());
Ok(())
}

View File

@ -1,23 +1,26 @@
use std::io::{Read, Write}; use std::{
use std::time::Duration; convert::Infallible,
use std::{net, thread}; io::{Read, Write},
net, thread,
time::Duration,
};
use actix_http::{
body::{AnyBody, Body, SizedStream},
header, http, Error, HttpMessage, HttpService, KeepAlive, Request, Response,
StatusCode,
};
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_rt::time::sleep; use actix_rt::time::sleep;
use actix_service::fn_service; use actix_service::fn_service;
use actix_utils::future::{err, ok, ready}; use actix_utils::future::{err, ok, ready};
use bytes::Bytes; use bytes::Bytes;
use futures_util::stream::{once, StreamExt as _}; use derive_more::{Display, Error};
use futures_util::FutureExt as _; use futures_util::{
use regex::Regex; stream::{once, StreamExt as _},
FutureExt as _,
use actix_http::HttpMessage;
use actix_http::{
body::{Body, SizedStream},
error,
http::{self, header, StatusCode},
Error, HttpService, KeepAlive, Request, Response,
}; };
use regex::Regex;
#[actix_rt::test] #[actix_rt::test]
async fn test_h1() { async fn test_h1() {
@ -28,7 +31,7 @@ async fn test_h1() {
.client_disconnect(1000) .client_disconnect(1000)
.h1(|req: Request| { .h1(|req: Request| {
assert!(req.peer_addr().is_some()); assert!(req.peer_addr().is_some());
ok::<_, ()>(Response::ok()) ok::<_, Infallible>(Response::ok())
}) })
.tcp() .tcp()
}) })
@ -48,7 +51,7 @@ async fn test_h1_2() {
.finish(|req: Request| { .finish(|req: Request| {
assert!(req.peer_addr().is_some()); assert!(req.peer_addr().is_some());
assert_eq!(req.version(), http::Version::HTTP_11); assert_eq!(req.version(), http::Version::HTTP_11);
ok::<_, ()>(Response::ok()) ok::<_, Infallible>(Response::ok())
}) })
.tcp() .tcp()
}) })
@ -58,6 +61,16 @@ async fn test_h1_2() {
assert!(response.status().is_success()); assert!(response.status().is_success());
} }
#[derive(Debug, Display, Error)]
#[display(fmt = "expect failed")]
struct ExpectFailed;
impl From<ExpectFailed> for Response<AnyBody> {
fn from(_: ExpectFailed) -> Self {
Response::new(StatusCode::EXPECTATION_FAILED)
}
}
#[actix_rt::test] #[actix_rt::test]
async fn test_expect_continue() { async fn test_expect_continue() {
let srv = test_server(|| { let srv = test_server(|| {
@ -66,10 +79,10 @@ async fn test_expect_continue() {
if req.head().uri.query() == Some("yes=") { if req.head().uri.query() == Some("yes=") {
ok(req) ok(req)
} else { } else {
err(error::ErrorPreconditionFailed("error")) err(ExpectFailed)
} }
})) }))
.finish(|_| ok::<_, ()>(Response::ok())) .finish(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -78,7 +91,7 @@ async fn test_expect_continue() {
let _ = stream.write_all(b"GET /test HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); let _ = stream.write_all(b"GET /test HTTP/1.1\r\nexpect: 100-continue\r\n\r\n");
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 412 Precondition Failed\r\ncontent-length")); assert!(data.starts_with("HTTP/1.1 417 Expectation Failed\r\ncontent-length"));
let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
let _ = stream.write_all(b"GET /test?yes= HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); let _ = stream.write_all(b"GET /test?yes= HTTP/1.1\r\nexpect: 100-continue\r\n\r\n");
@ -96,11 +109,11 @@ async fn test_expect_continue_h1() {
if req.head().uri.query() == Some("yes=") { if req.head().uri.query() == Some("yes=") {
ok(req) ok(req)
} else { } else {
err(error::ErrorPreconditionFailed("error")) err(ExpectFailed)
} }
}) })
})) }))
.h1(fn_service(|_| ok::<_, ()>(Response::ok()))) .h1(fn_service(|_| ok::<_, Infallible>(Response::ok())))
.tcp() .tcp()
}) })
.await; .await;
@ -109,7 +122,7 @@ async fn test_expect_continue_h1() {
let _ = stream.write_all(b"GET /test HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); let _ = stream.write_all(b"GET /test HTTP/1.1\r\nexpect: 100-continue\r\n\r\n");
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 412 Precondition Failed\r\ncontent-length")); assert!(data.starts_with("HTTP/1.1 417 Expectation Failed\r\ncontent-length"));
let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
let _ = stream.write_all(b"GET /test?yes= HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); let _ = stream.write_all(b"GET /test?yes= HTTP/1.1\r\nexpect: 100-continue\r\n\r\n");
@ -181,7 +194,7 @@ async fn test_slow_request() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.client_timeout(100) .client_timeout(100)
.finish(|_| ok::<_, ()>(Response::ok())) .finish(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -197,7 +210,7 @@ async fn test_slow_request() {
async fn test_http1_malformed_request() { async fn test_http1_malformed_request() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -213,7 +226,7 @@ async fn test_http1_malformed_request() {
async fn test_http1_keepalive() { async fn test_http1_keepalive() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -235,7 +248,7 @@ async fn test_http1_keepalive_timeout() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(1) .keep_alive(1)
.h1(|_| ok::<_, ()>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -256,7 +269,7 @@ async fn test_http1_keepalive_timeout() {
async fn test_http1_keepalive_close() { async fn test_http1_keepalive_close() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -277,7 +290,7 @@ async fn test_http1_keepalive_close() {
async fn test_http10_keepalive_default_close() { async fn test_http10_keepalive_default_close() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -297,7 +310,7 @@ async fn test_http10_keepalive_default_close() {
async fn test_http10_keepalive() { async fn test_http10_keepalive() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -325,7 +338,7 @@ async fn test_http1_keepalive_disabled() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(KeepAlive::Disabled) .keep_alive(KeepAlive::Disabled)
.h1(|_| ok::<_, ()>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -360,7 +373,7 @@ async fn test_content_length() {
StatusCode::OK, StatusCode::OK,
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
]; ];
ok::<_, ()>(Response::new(statuses[indx])) ok::<_, Infallible>(Response::new(statuses[indx]))
}) })
.tcp() .tcp()
}) })
@ -415,7 +428,7 @@ async fn test_h1_headers() {
TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ",
)); ));
} }
ok::<_, ()>(builder.body(data.clone())) ok::<_, Infallible>(builder.body(data.clone()))
}).tcp() }).tcp()
}).await; }).await;
@ -453,7 +466,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
async fn test_h1_body() { async fn test_h1_body() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::ok().set_body(STR))) .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.tcp() .tcp()
}) })
.await; .await;
@ -470,7 +483,7 @@ async fn test_h1_body() {
async fn test_h1_head_empty() { async fn test_h1_head_empty() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::ok().set_body(STR))) .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.tcp() .tcp()
}) })
.await; .await;
@ -495,7 +508,7 @@ async fn test_h1_head_empty() {
async fn test_h1_head_binary() { async fn test_h1_head_binary() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::ok().set_body(STR))) .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.tcp() .tcp()
}) })
.await; .await;
@ -520,7 +533,7 @@ async fn test_h1_head_binary() {
async fn test_h1_head_binary2() { async fn test_h1_head_binary2() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::ok().set_body(STR))) .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.tcp() .tcp()
}) })
.await; .await;
@ -542,8 +555,8 @@ async fn test_h1_body_length() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| { .h1(|_| {
let body = once(ok(Bytes::from_static(STR.as_ref()))); let body = once(ok::<_, Infallible>(Bytes::from_static(STR.as_ref())));
ok::<_, ()>( ok::<_, Infallible>(
Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), Response::ok().set_body(SizedStream::new(STR.len() as u64, body)),
) )
}) })
@ -565,7 +578,7 @@ async fn test_h1_body_chunked_explicit() {
HttpService::build() HttpService::build()
.h1(|_| { .h1(|_| {
let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref())));
ok::<_, ()>( ok::<_, Infallible>(
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header((header::TRANSFER_ENCODING, "chunked"))
.streaming(body), .streaming(body),
@ -600,7 +613,7 @@ async fn test_h1_body_chunked_implicit() {
HttpService::build() HttpService::build()
.h1(|_| { .h1(|_| {
let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref())));
ok::<_, ()>(Response::build(StatusCode::OK).streaming(body)) ok::<_, Infallible>(Response::build(StatusCode::OK).streaming(body))
}) })
.tcp() .tcp()
}) })
@ -629,7 +642,7 @@ async fn test_h1_response_http_error_handling() {
HttpService::build() HttpService::build()
.h1(fn_service(|_| { .h1(fn_service(|_| {
let broken_header = Bytes::from_static(b"\0\0\0"); let broken_header = Bytes::from_static(b"\0\0\0");
ok::<_, ()>( ok::<_, Infallible>(
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.insert_header((http::header::CONTENT_TYPE, broken_header)) .insert_header((http::header::CONTENT_TYPE, broken_header))
.body(STR), .body(STR),
@ -644,14 +657,27 @@ async fn test_h1_response_http_error_handling() {
// 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"failed to parse header value")); assert_eq!(
bytes,
Bytes::from_static(b"error processing HTTP: failed to parse header value")
);
}
#[derive(Debug, Display, Error)]
#[display(fmt = "error")]
struct BadRequest;
impl From<BadRequest> for Response<AnyBody> {
fn from(_: BadRequest) -> Self {
Response::bad_request().set_body(AnyBody::from("error"))
}
} }
#[actix_rt::test] #[actix_rt::test]
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>, _>(error::ErrorBadRequest("error"))) .h1(|_| err::<Response<Body>, _>(BadRequest))
.tcp() .tcp()
}) })
.await; .await;
@ -673,7 +699,7 @@ async fn test_h1_on_connect() {
}) })
.h1(|req: Request| { .h1(|req: Request| {
assert!(req.extensions().contains::<isize>()); assert!(req.extensions().contains::<isize>());
ok::<_, ()>(Response::ok()) ok::<_, Infallible>(Response::ok())
}) })
.tcp() .tcp()
}) })

View File

@ -1,193 +1,196 @@
use std::cell::Cell; use std::{
use std::future::Future; cell::Cell,
use std::marker::PhantomData; convert::Infallible,
use std::pin::Pin; task::{Context, Poll},
use std::sync::{Arc, Mutex}; };
use std::task::{Context, Poll};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_http::{body, h1, ws, Error, HttpService, Request, Response}; use actix_http::{
body::{AnyBody, BodySize},
h1,
ws::{self, CloseCode, Frame, Item, Message},
Error, HttpService, Request, Response,
};
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_service::{fn_factory, Service}; use actix_service::{fn_factory, Service};
use actix_utils::future;
use bytes::Bytes; use bytes::Bytes;
use derive_more::{Display, Error, From};
use futures_core::future::LocalBoxFuture;
use futures_util::{SinkExt as _, StreamExt as _}; use futures_util::{SinkExt as _, StreamExt as _};
use crate::ws::Dispatcher; #[derive(Clone)]
struct WsService(Cell<bool>);
struct WsService<T>(Arc<Mutex<(PhantomData<T>, Cell<bool>)>>); impl WsService {
impl<T> WsService<T> {
fn new() -> Self { fn new() -> Self {
WsService(Arc::new(Mutex::new((PhantomData, Cell::new(false))))) WsService(Cell::new(false))
} }
fn set_polled(&self) { fn set_polled(&self) {
*self.0.lock().unwrap().1.get_mut() = true; self.0.set(true);
} }
fn was_polled(&self) -> bool { fn was_polled(&self) -> bool {
self.0.lock().unwrap().1.get() self.0.get()
} }
} }
impl<T> Clone for WsService<T> { #[derive(Debug, Display, Error, From)]
fn clone(&self) -> Self { enum WsServiceError {
WsService(self.0.clone()) #[display(fmt = "http error")]
Http(actix_http::Error),
#[display(fmt = "ws handshake error")]
Ws(actix_http::ws::HandshakeError),
#[display(fmt = "io error")]
Io(std::io::Error),
#[display(fmt = "dispatcher error")]
Dispatcher,
}
impl From<WsServiceError> for Response<AnyBody> {
fn from(err: WsServiceError) -> Self {
match err {
WsServiceError::Http(err) => err.into(),
WsServiceError::Ws(err) => err.into(),
WsServiceError::Io(_err) => unreachable!(),
WsServiceError::Dispatcher => Response::internal_server_error()
.set_body(AnyBody::from(format!("{}", err))),
}
} }
} }
impl<T> Service<(Request, Framed<T, h1::Codec>)> for WsService<T> impl<T> Service<(Request, Framed<T, h1::Codec>)> for WsService
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, T: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
type Response = (); type Response = ();
type Error = Error; type Error = WsServiceError;
type Future = Pin<Box<dyn Future<Output = Result<(), Error>>>>; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&self, _ctx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.set_polled(); self.set_polled();
Poll::Ready(Ok(())) Poll::Ready(Ok(()))
} }
fn call(&self, (req, mut framed): (Request, Framed<T, h1::Codec>)) -> Self::Future { fn call(&self, (req, mut framed): (Request, Framed<T, h1::Codec>)) -> Self::Future {
let fut = async move { assert!(self.was_polled());
let res = ws::handshake(req.head()).unwrap().message_body(());
framed Box::pin(async move {
.send((res, body::BodySize::None).into()) let res = ws::handshake(req.head())?.message_body(())?;
framed.send((res, BodySize::None).into()).await?;
let framed = framed.replace_codec(ws::Codec::new());
ws::Dispatcher::with(framed, service)
.await .await
.unwrap(); .map_err(|_| WsServiceError::Dispatcher)?;
Dispatcher::with(framed.replace_codec(ws::Codec::new()), service) Ok(())
.await })
.map_err(|_| panic!())
};
Box::pin(fut)
} }
} }
async fn service(msg: ws::Frame) -> Result<ws::Message, Error> { async fn service(msg: Frame) -> Result<Message, Error> {
let msg = match msg { let msg = match msg {
ws::Frame::Ping(msg) => ws::Message::Pong(msg), Frame::Ping(msg) => Message::Pong(msg),
ws::Frame::Text(text) => { Frame::Text(text) => {
ws::Message::Text(String::from_utf8_lossy(&text).into_owned().into()) Message::Text(String::from_utf8_lossy(&text).into_owned().into())
} }
ws::Frame::Binary(bin) => ws::Message::Binary(bin), Frame::Binary(bin) => Message::Binary(bin),
ws::Frame::Continuation(item) => ws::Message::Continuation(item), Frame::Continuation(item) => Message::Continuation(item),
ws::Frame::Close(reason) => ws::Message::Close(reason), Frame::Close(reason) => Message::Close(reason),
_ => panic!(), _ => return Err(ws::ProtocolError::BadOpCode.into()),
}; };
Ok(msg) Ok(msg)
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_simple() { async fn test_simple() {
let ws_service = WsService::new(); let mut srv = test_server(|| {
let mut srv = test_server({ HttpService::build()
let ws_service = ws_service.clone(); .upgrade(fn_factory(|| async {
move || { Ok::<_, Infallible>(WsService::new())
let ws_service = ws_service.clone(); }))
HttpService::build() .finish(|_| async { Ok::<_, Infallible>(Response::not_found()) })
.upgrade(fn_factory(move || future::ok::<_, ()>(ws_service.clone()))) .tcp()
.finish(|_| future::ok::<_, ()>(Response::not_found()))
.tcp()
}
}) })
.await; .await;
// client service // client service
let mut framed = srv.ws().await.unwrap(); let mut framed = srv.ws().await.unwrap();
framed.send(ws::Message::Text("text".into())).await.unwrap(); framed.send(Message::Text("text".into())).await.unwrap();
let (item, mut framed) = framed.into_future().await;
assert_eq!( let item = framed.next().await.unwrap().unwrap();
item.unwrap().unwrap(), assert_eq!(item, Frame::Text(Bytes::from_static(b"text")));
ws::Frame::Text(Bytes::from_static(b"text"))
); framed.send(Message::Binary("text".into())).await.unwrap();
let item = framed.next().await.unwrap().unwrap();
assert_eq!(item, Frame::Binary(Bytes::from_static(&b"text"[..])));
framed.send(Message::Ping("text".into())).await.unwrap();
let item = framed.next().await.unwrap().unwrap();
assert_eq!(item, Frame::Pong("text".to_string().into()));
framed framed
.send(ws::Message::Binary("text".into())) .send(Message::Continuation(Item::FirstText("text".into())))
.await .await
.unwrap(); .unwrap();
let (item, mut framed) = framed.into_future().await; let item = framed.next().await.unwrap().unwrap();
assert_eq!( assert_eq!(
item.unwrap().unwrap(), item,
ws::Frame::Binary(Bytes::from_static(&b"text"[..])) Frame::Continuation(Item::FirstText(Bytes::from_static(b"text")))
);
framed.send(ws::Message::Ping("text".into())).await.unwrap();
let (item, mut framed) = framed.into_future().await;
assert_eq!(
item.unwrap().unwrap(),
ws::Frame::Pong("text".to_string().into())
);
framed
.send(ws::Message::Continuation(ws::Item::FirstText(
"text".into(),
)))
.await
.unwrap();
let (item, mut framed) = framed.into_future().await;
assert_eq!(
item.unwrap().unwrap(),
ws::Frame::Continuation(ws::Item::FirstText(Bytes::from_static(b"text")))
); );
assert!(framed assert!(framed
.send(ws::Message::Continuation(ws::Item::FirstText( .send(Message::Continuation(Item::FirstText("text".into())))
"text".into()
)))
.await .await
.is_err()); .is_err());
assert!(framed assert!(framed
.send(ws::Message::Continuation(ws::Item::FirstBinary( .send(Message::Continuation(Item::FirstBinary("text".into())))
"text".into()
)))
.await .await
.is_err()); .is_err());
framed framed
.send(ws::Message::Continuation(ws::Item::Continue("text".into()))) .send(Message::Continuation(Item::Continue("text".into())))
.await .await
.unwrap(); .unwrap();
let (item, mut framed) = framed.into_future().await; let item = framed.next().await.unwrap().unwrap();
assert_eq!( assert_eq!(
item.unwrap().unwrap(), item,
ws::Frame::Continuation(ws::Item::Continue(Bytes::from_static(b"text"))) Frame::Continuation(Item::Continue(Bytes::from_static(b"text")))
); );
framed framed
.send(ws::Message::Continuation(ws::Item::Last("text".into()))) .send(Message::Continuation(Item::Last("text".into())))
.await .await
.unwrap(); .unwrap();
let (item, mut framed) = framed.into_future().await; let item = framed.next().await.unwrap().unwrap();
assert_eq!( assert_eq!(
item.unwrap().unwrap(), item,
ws::Frame::Continuation(ws::Item::Last(Bytes::from_static(b"text"))) Frame::Continuation(Item::Last(Bytes::from_static(b"text")))
); );
assert!(framed assert!(framed
.send(ws::Message::Continuation(ws::Item::Continue("text".into()))) .send(Message::Continuation(Item::Continue("text".into())))
.await .await
.is_err()); .is_err());
assert!(framed assert!(framed
.send(ws::Message::Continuation(ws::Item::Last("text".into()))) .send(Message::Continuation(Item::Last("text".into())))
.await .await
.is_err()); .is_err());
framed framed
.send(ws::Message::Close(Some(ws::CloseCode::Normal.into()))) .send(Message::Close(Some(CloseCode::Normal.into())))
.await .await
.unwrap(); .unwrap();
let (item, _framed) = framed.into_future().await; let item = framed.next().await.unwrap().unwrap();
assert_eq!( assert_eq!(item, Frame::Close(Some(CloseCode::Normal.into())));
item.unwrap().unwrap(),
ws::Frame::Close(Some(ws::CloseCode::Normal.into()))
);
assert!(ws_service.was_polled());
} }

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.4.0-beta.5 - 2021-06-17
* No notable changes.
## 0.4.0-beta.4 - 2021-04-02 ## 0.4.0-beta.4 - 2021-04-02
* No notable changes. * No notable changes.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-multipart" name = "actix-multipart"
version = "0.4.0-beta.4" version = "0.4.0-beta.5"
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"
readme = "README.md" readme = "README.md"
@ -16,7 +16,7 @@ name = "actix_multipart"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
actix-web = { version = "4.0.0-beta.6", default-features = false } actix-web = { version = "4.0.0-beta.7", default-features = false }
actix-utils = "3.0.0" actix-utils = "3.0.0"
bytes = "1" bytes = "1"
@ -31,6 +31,6 @@ twoway = "0.2"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-http = "3.0.0-beta.6" actix-http = "3.0.0-beta.7"
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.4)](https://docs.rs/actix-multipart/0.4.0-beta.4) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.5)](https://docs.rs/actix-multipart/0.4.0-beta.5)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.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.4/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.4) [![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.5/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.5)
[![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)
## Documentation & Resources ## Documentation & Resources

View File

@ -19,14 +19,14 @@ rustls = ["tls-rustls", "actix-http/rustls"]
openssl = ["tls-openssl", "actix-http/openssl"] openssl = ["tls-openssl", "actix-http/openssl"]
[dependencies] [dependencies]
actix-codec = "0.4.0-beta.1" actix-codec = "0.4.0"
actix-http = "3.0.0-beta.6" actix-http = "3.0.0-beta.7"
actix-http-test = { version = "3.0.0-beta.4", features = [] } actix-http-test = { version = "3.0.0-beta.4", features = [] }
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.6", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-beta.7", default-features = false, features = ["cookies"] }
actix-rt = "2.1" actix-rt = "2.1"
awc = { version = "3.0.0-beta.5", default-features = false, features = ["cookies"] } awc = { version = "3.0.0-beta.6", 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 = [] }

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::{fmt, net, sync::mpsc, thread, time}; use std::{error::Error as StdError, fmt, net, sync::mpsc, thread, time};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
pub use actix_http::test::TestBuffer; pub use actix_http::test::TestBuffer;
@ -39,7 +39,7 @@ use actix_http::{
http::{HeaderMap, Method}, http::{HeaderMap, Method},
ws, HttpService, Request, Response, ws, HttpService, Request, Response,
}; };
use actix_service::{map_config, IntoServiceFactory, ServiceFactory}; use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _};
use actix_web::{ use actix_web::{
dev::{AppConfig, MessageBody, Server, Service}, dev::{AppConfig, MessageBody, Server, Service},
rt, web, Error, rt, web, Error,
@ -86,6 +86,7 @@ where
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
start_with(TestServerConfig::default(), factory) start_with(TestServerConfig::default(), factory)
} }
@ -125,6 +126,7 @@ where
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
@ -151,25 +153,40 @@ where
HttpVer::Http1 => builder.listen("test", tcp, move || { HttpVer::Http1 => builder.listen("test", tcp, move || {
let app_cfg = let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_timeout(timeout)
.h1(map_config(factory(), move |_| app_cfg.clone())) .h1(map_config(fac, move |_| app_cfg.clone()))
.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(false, local_addr.to_string(), local_addr); AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_timeout(timeout)
.h2(map_config(factory(), move |_| app_cfg.clone())) .h2(map_config(fac, move |_| app_cfg.clone()))
.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(false, local_addr.to_string(), local_addr); AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_timeout(timeout)
.finish(map_config(factory(), move |_| app_cfg.clone())) .finish(map_config(fac, move |_| app_cfg.clone()))
.tcp() .tcp()
}), }),
}, },
@ -178,25 +195,40 @@ where
HttpVer::Http1 => builder.listen("test", tcp, move || { HttpVer::Http1 => builder.listen("test", tcp, move || {
let app_cfg = let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_timeout(timeout)
.h1(map_config(factory(), move |_| app_cfg.clone())) .h1(map_config(fac, move |_| app_cfg.clone()))
.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(false, local_addr.to_string(), local_addr); AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_timeout(timeout)
.h2(map_config(factory(), move |_| app_cfg.clone())) .h2(map_config(fac, move |_| app_cfg.clone()))
.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(false, local_addr.to_string(), local_addr); AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_timeout(timeout)
.finish(map_config(factory(), move |_| app_cfg.clone())) .finish(map_config(fac, move |_| app_cfg.clone()))
.openssl(acceptor.clone()) .openssl(acceptor.clone())
}), }),
}, },
@ -205,25 +237,40 @@ where
HttpVer::Http1 => builder.listen("test", tcp, move || { HttpVer::Http1 => builder.listen("test", tcp, move || {
let app_cfg = let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_timeout(timeout)
.h1(map_config(factory(), move |_| app_cfg.clone())) .h1(map_config(fac, move |_| app_cfg.clone()))
.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(false, local_addr.to_string(), local_addr); AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_timeout(timeout)
.h2(map_config(factory(), move |_| app_cfg.clone())) .h2(map_config(fac, move |_| app_cfg.clone()))
.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(false, local_addr.to_string(), local_addr); AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build() HttpService::build()
.client_timeout(timeout) .client_timeout(timeout)
.finish(map_config(factory(), move |_| app_cfg.clone())) .finish(map_config(fac, move |_| app_cfg.clone()))
.rustls(config.clone()) .rustls(config.clone())
}), }),
}, },

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 4.0.0-beta.5 - 2021-06-17
* No notable changes.
## 4.0.0-beta.4 - 2021-04-02 ## 4.0.0-beta.4 - 2021-04-02
* No notable changes. * No notable changes.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-actors" name = "actix-web-actors"
version = "4.0.0-beta.4" version = "4.0.0-beta.5"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix actors support for Actix Web" description = "Actix actors support for Actix Web"
readme = "README.md" readme = "README.md"
@ -17,9 +17,9 @@ path = "src/lib.rs"
[dependencies] [dependencies]
actix = { version = "0.11.0-beta.3", default-features = false } actix = { version = "0.11.0-beta.3", default-features = false }
actix-codec = "0.4.0-beta.1" actix-codec = "0.4.0"
actix-http = "3.0.0-beta.6" actix-http = "3.0.0-beta.7"
actix-web = { version = "4.0.0-beta.6", default-features = false } actix-web = { version = "4.0.0-beta.7", default-features = false }
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"
@ -31,6 +31,6 @@ tokio = { version = "1", features = ["sync"] }
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.2" actix-test = "0.1.0-beta.2"
awc = { version = "3.0.0-beta.5", default-features = false } awc = { version = "3.0.0-beta.6", default-features = false }
env_logger = "0.8" env_logger = "0.8"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }

View File

@ -3,11 +3,11 @@
> Actix actors support for Actix Web. > Actix actors support for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors)
[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.4)](https://docs.rs/actix-web-actors/4.0.0-beta.4) [![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.5)](https://docs.rs/actix-web-actors/4.0.0-beta.5)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
![License](https://img.shields.io/crates/l/actix-web-actors.svg) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.4) [![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.5/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.5)
[![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) [![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

View File

@ -22,10 +22,11 @@ use actix_http::{
http::HeaderValue, http::HeaderValue,
ws::{hash_key, Codec}, ws::{hash_key, Codec},
}; };
use actix_web::error::{Error, PayloadError}; use actix_web::{
use actix_web::http::{header, Method, StatusCode}; error::{Error, PayloadError},
use actix_web::HttpResponseBuilder; http::{header, Method, StatusCode},
use actix_web::{HttpRequest, HttpResponse}; HttpRequest, HttpResponse, HttpResponseBuilder,
};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use bytestring::ByteString; use bytestring::ByteString;
use futures_core::Stream; use futures_core::Stream;

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.5.0-beta.3 - 2021-06-17
* No notable changes.
## 0.5.0-beta.2 - 2021-03-09 ## 0.5.0-beta.2 - 2021-03-09
* Preserve doc comments when using route macros. [#2022] * Preserve doc comments when using route macros. [#2022]
* Add `name` attribute to `route` macro. [#1934] * Add `name` attribute to `route` macro. [#1934]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-codegen" name = "actix-web-codegen"
version = "0.5.0-beta.2" version = "0.5.0-beta.3"
description = "Routing and runtime macros for Actix Web" description = "Routing and runtime macros for Actix Web"
readme = "README.md" readme = "README.md"
homepage = "https://actix.rs" homepage = "https://actix.rs"
@ -22,7 +22,7 @@ proc-macro2 = "1"
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.2" actix-test = "0.1.0-beta.2"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = "4.0.0-beta.6" actix-web = "4.0.0-beta.7"
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,11 +3,11 @@
> Routing and runtime macros for Actix Web. > Routing and runtime macros for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen)
[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.2)](https://docs.rs/actix-web-codegen/0.5.0-beta.2) [![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.3)](https://docs.rs/actix-web-codegen/0.5.0-beta.3)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
![License](https://img.shields.io/crates/l/actix-web-codegen.svg) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.2/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.2) [![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3)
[![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen) [![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

View File

@ -171,27 +171,10 @@ method_macro! {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
use quote::quote; use quote::quote;
let input = syn::parse_macro_input!(item as syn::ItemFn);
let mut input = syn::parse_macro_input!(item as syn::ItemFn);
let attrs = &input.attrs;
let vis = &input.vis;
let sig = &mut input.sig;
let body = &input.block;
if sig.asyncness.is_none() {
return syn::Error::new_spanned(sig.fn_token, "only async fn is supported")
.to_compile_error()
.into();
}
sig.asyncness = None;
(quote! { (quote! {
#(#attrs)* #[actix_web::rt::main(system = "::actix_web::rt::System")]
#vis #sig { #input
actix_web::rt::System::new()
.block_on(async move { #body })
}
}) })
.into() .into()
} }

View File

@ -1,3 +1,4 @@
#[rustversion::stable(1.46)] // MSRV
#[test] #[test]
fn compile_macros() { fn compile_macros() {
let t = trybuild::TestCases::new(); let t = trybuild::TestCases::new();
@ -12,11 +13,3 @@ fn compile_macros() {
t.pass("tests/trybuild/docstring-ok.rs"); t.pass("tests/trybuild/docstring-ok.rs");
} }
// #[rustversion::not(nightly)]
// fn skip_on_nightly(t: &trybuild::TestCases) {
//
// }
// #[rustversion::nightly]
// fn skip_on_nightly(_t: &trybuild::TestCases) {}

View File

@ -1,6 +1,14 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
### Changed
* Change compression algorithm features flags. [#2250]
[#2250]: https://github.com/actix/actix-web/pull/2250
## 3.0.0-beta.6 - 2021-06-17
* No significant changes since 3.0.0-beta.5.
## 3.0.0-beta.5 - 2021-04-17 ## 3.0.0-beta.5 - 2021-04-17

View File

@ -1,6 +1,6 @@
[package] [package]
name = "awc" name = "awc"
version = "3.0.0-beta.5" version = "3.0.0-beta.6"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>", "fakeshadow <24548779@qq.com>",
@ -24,10 +24,10 @@ path = "src/lib.rs"
[package.metadata.docs.rs] [package.metadata.docs.rs]
# features that docs.rs will build with # features that docs.rs will build with
features = ["openssl", "rustls", "compress", "cookies"] features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
[features] [features]
default = ["compress", "cookies"] default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
# openssl # openssl
openssl = ["tls-openssl", "actix-http/openssl"] openssl = ["tls-openssl", "actix-http/openssl"]
@ -35,8 +35,12 @@ openssl = ["tls-openssl", "actix-http/openssl"]
# rustls # rustls
rustls = ["tls-rustls", "actix-http/rustls"] rustls = ["tls-rustls", "actix-http/rustls"]
# content-encoding support # Brotli algorithm content-encoding support
compress = ["actix-http/compress"] compress-brotli = ["actix-http/compress-brotli", "__compress"]
# Gzip and deflate algorithms content-encoding support
compress-gzip = ["actix-http/compress-gzip", "__compress"]
# Zstd algorithm content-encoding support
compress-zstd = ["actix-http/compress-zstd", "__compress"]
# cookie parsing and cookie jar # cookie parsing and cookie jar
cookies = ["cookie"] cookies = ["cookie"]
@ -44,14 +48,19 @@ cookies = ["cookie"]
# trust-dns as dns resolver # trust-dns as dns resolver
trust-dns = ["actix-http/trust-dns"] trust-dns = ["actix-http/trust-dns"]
# Internal (PRIVATE!) features used to aid testing and cheking feature status.
# Don't rely on these whatsoever. They may disappear at anytime.
__compress = []
[dependencies] [dependencies]
actix-codec = "0.4.0-beta.1" actix-codec = "0.4.0"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-http = "3.0.0-beta.6" actix-http = "3.0.0-beta.7"
actix-rt = { version = "2.1", default-features = false } actix-rt = { version = "2.1", default-features = false }
base64 = "0.13" base64 = "0.13"
bytes = "1" bytes = "1"
cfg-if = "1"
cookie = { version = "0.15", features = ["percent-encode"], optional = true } 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 }
@ -68,8 +77,8 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
tls-rustls = { version = "0.19.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } tls-rustls = { version = "0.19.0", package = "rustls", optional = true, features = ["dangerous_configuration"] }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0-beta.6", features = ["openssl"] } actix-web = { version = "4.0.0-beta.7", features = ["openssl"] }
actix-http = { version = "3.0.0-beta.6", features = ["openssl"] } actix-http = { version = "3.0.0-beta.7", features = ["openssl"] }
actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.4", 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.3"

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.4)](https://docs.rs/awc/3.0.0-beta.4) [![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.6)](https://docs.rs/awc/3.0.0-beta.6)
![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.4/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.4) [![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.6/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.6)
[![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & Resources ## Documentation & Resources

View File

@ -1,18 +1,20 @@
use actix_http::Error; use std::error::Error as StdError;
#[actix_web::main] #[actix_web::main]
async fn main() -> Result<(), Error> { async fn main() -> Result<(), Box<dyn StdError>> {
std::env::set_var("RUST_LOG", "actix_http=trace"); std::env::set_var("RUST_LOG", "client=trace,awc=trace,actix_http=trace");
env_logger::init(); env_logger::init();
let client = awc::Client::new(); let client = awc::Client::new();
// Create request builder, configure request and send // Create request builder, configure request and send
let mut response = client let request = client
.get("https://www.rust-lang.org/") .get("https://www.rust-lang.org/")
.append_header(("User-Agent", "Actix-web")) .append_header(("User-Agent", "Actix-web"));
.send()
.await?; println!("Request: {:?}", request);
let mut response = request.send().await?;
// server http response // server http response
println!("Response: {:?}", response); println!("Response: {:?}", response);

View File

@ -6,7 +6,6 @@ pub use actix_http::http::Error as HttpError;
pub use actix_http::ws::HandshakeError as WsHandshakeError; pub use actix_http::ws::HandshakeError as WsHandshakeError;
pub use actix_http::ws::ProtocolError as WsProtocolError; pub use actix_http::ws::ProtocolError as WsProtocolError;
use actix_http::ResponseError;
use serde_json::error::Error as JsonError; use serde_json::error::Error as JsonError;
use actix_http::http::{header::HeaderValue, StatusCode}; use actix_http::http::{header::HeaderValue, StatusCode};
@ -77,6 +76,3 @@ pub enum JsonPayloadError {
} }
impl std::error::Error for JsonPayloadError {} impl std::error::Error for JsonPayloadError {}
/// Return `InternalServerError` for `JsonPayloadError`
impl ResponseError for JsonPayloadError {}

View File

@ -1,21 +1,21 @@
use std::convert::TryFrom; use std::{convert::TryFrom, error::Error as StdError, net, rc::Rc, time::Duration};
use std::net;
use std::rc::Rc;
use std::time::Duration;
use bytes::Bytes; use bytes::Bytes;
use futures_core::Stream; use futures_core::Stream;
use serde::Serialize; use serde::Serialize;
use actix_http::body::Body; use actix_http::{
use actix_http::http::header::IntoHeaderValue; body::Body,
use actix_http::http::{Error as HttpError, HeaderMap, HeaderName, Method, Uri}; http::{header::IntoHeaderValue, Error as HttpError, HeaderMap, HeaderName, Method, Uri},
use actix_http::{Error, RequestHead}; RequestHead,
};
use crate::sender::{RequestSender, SendClientRequest}; use crate::{
use crate::ClientConfig; sender::{RequestSender, SendClientRequest},
ClientConfig,
};
/// `FrozenClientRequest` struct represents clonable client request. /// `FrozenClientRequest` struct represents cloneable client request.
/// It could be used to send same request multiple times. /// It could be used to send same request multiple times.
#[derive(Clone)] #[derive(Clone)]
pub struct FrozenClientRequest { pub struct FrozenClientRequest {
@ -82,7 +82,7 @@ impl FrozenClientRequest {
pub fn send_stream<S, E>(&self, stream: S) -> SendClientRequest pub fn send_stream<S, E>(&self, stream: S) -> SendClientRequest
where where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static, S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
E: Into<Error> + 'static, E: Into<Box<dyn StdError>> + 'static,
{ {
RequestSender::Rc(self.head.clone(), None).send_stream( RequestSender::Rc(self.head.clone(), None).send_stream(
self.addr, self.addr,
@ -207,7 +207,7 @@ impl FrozenSendBuilder {
pub fn send_stream<S, E>(self, stream: S) -> SendClientRequest pub fn send_stream<S, E>(self, stream: S) -> SendClientRequest
where where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static, S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
E: Into<Error> + 'static, E: Into<Box<dyn StdError>> + 'static,
{ {
if let Some(e) = self.err { if let Some(e) = self.err {
return e.into(); return e.into();

View File

@ -1,7 +1,6 @@
//! `awc` is a HTTP and WebSocket client library built on the Actix ecosystem. //! `awc` is a HTTP and WebSocket client library built on the Actix ecosystem.
//! //!
//! ## Making a GET request //! # Making a GET request
//!
//! ```no_run //! ```no_run
//! # #[actix_rt::main] //! # #[actix_rt::main]
//! # async fn main() -> Result<(), awc::error::SendRequestError> { //! # async fn main() -> Result<(), awc::error::SendRequestError> {
@ -16,10 +15,8 @@
//! # } //! # }
//! ``` //! ```
//! //!
//! ## Making POST requests //! # Making POST requests
//! //! ## Raw body contents
//! ### Raw body contents
//!
//! ```no_run //! ```no_run
//! # #[actix_rt::main] //! # #[actix_rt::main]
//! # async fn main() -> Result<(), awc::error::SendRequestError> { //! # async fn main() -> Result<(), awc::error::SendRequestError> {
@ -31,8 +28,7 @@
//! # } //! # }
//! ``` //! ```
//! //!
//! ### Forms //! ## Forms
//!
//! ```no_run //! ```no_run
//! # #[actix_rt::main] //! # #[actix_rt::main]
//! # async fn main() -> Result<(), awc::error::SendRequestError> { //! # async fn main() -> Result<(), awc::error::SendRequestError> {
@ -46,8 +42,7 @@
//! # } //! # }
//! ``` //! ```
//! //!
//! ### JSON //! ## JSON
//!
//! ```no_run //! ```no_run
//! # #[actix_rt::main] //! # #[actix_rt::main]
//! # async fn main() -> Result<(), awc::error::SendRequestError> { //! # async fn main() -> Result<(), awc::error::SendRequestError> {
@ -64,8 +59,24 @@
//! # } //! # }
//! ``` //! ```
//! //!
//! ## WebSocket support //! # Response Compression
//! All [official][iana-encodings] and common content encoding codecs are supported, optionally.
//! //!
//! The `Accept-Encoding` header will automatically be populated with enabled codecs and added to
//! outgoing requests, allowing servers to select their `Content-Encoding` accordingly.
//!
//! Feature flags enable these codecs according to the table below. By default, all `compress-*`
//! features are enabled.
//!
//! | Feature | Codecs |
//! | ----------------- | ------------- |
//! | `compress-brotli` | brotli |
//! | `compress-gzip` | gzip, deflate |
//! | `compress-zstd` | zstd |
//!
//! [iana-encodings]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding
//!
//! # WebSocket support
//! ```no_run //! ```no_run
//! # #[actix_rt::main] //! # #[actix_rt::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> { //! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
@ -128,8 +139,10 @@ pub use self::sender::SendClientRequest;
/// An asynchronous HTTP and WebSocket client. /// An asynchronous HTTP and WebSocket client.
/// ///
/// ## Examples /// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU
/// and memory usage.
/// ///
/// # Examples
/// ``` /// ```
/// use awc::Client; /// use awc::Client;
/// ///
@ -137,10 +150,10 @@ pub use self::sender::SendClientRequest;
/// async fn main() { /// async fn main() {
/// let mut client = Client::default(); /// let mut client = Client::default();
/// ///
/// let res = client.get("http://www.rust-lang.org") // <- Create request builder /// let res = client.get("http://www.rust-lang.org")
/// .insert_header(("User-Agent", "Actix-web")) /// .insert_header(("User-Agent", "my-app/1.2"))
/// .send() // <- Send HTTP request /// .send()
/// .await; // <- send request and wait for response /// .await;
/// ///
/// println!("Response: {:?}", res); /// println!("Response: {:?}", res);
/// } /// }

View File

@ -1,30 +1,26 @@
use std::convert::TryFrom; use std::{convert::TryFrom, error::Error as StdError, fmt, net, rc::Rc, time::Duration};
use std::rc::Rc;
use std::time::Duration;
use std::{fmt, net};
use bytes::Bytes; use bytes::Bytes;
use futures_core::Stream; use futures_core::Stream;
use serde::Serialize; use serde::Serialize;
use actix_http::body::Body; use actix_http::{
use actix_http::http::header::{self, IntoHeaderPair}; body::Body,
use actix_http::http::{ http::{
uri, ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, Version, header::{self, IntoHeaderPair},
ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, Version,
},
RequestHead,
}; };
use actix_http::{Error, RequestHead};
#[cfg(feature = "cookies")] #[cfg(feature = "cookies")]
use crate::cookie::{Cookie, CookieJar}; use crate::cookie::{Cookie, CookieJar};
use crate::error::{FreezeRequestError, InvalidUrl}; use crate::{
use crate::frozen::FrozenClientRequest; error::{FreezeRequestError, InvalidUrl},
use crate::sender::{PrepForSendingError, RequestSender, SendClientRequest}; frozen::FrozenClientRequest,
use crate::ClientConfig; sender::{PrepForSendingError, RequestSender, SendClientRequest},
ClientConfig,
#[cfg(feature = "compress")] };
const HTTPS_ENCODING: &str = "br, gzip, deflate";
#[cfg(not(feature = "compress"))]
const HTTPS_ENCODING: &str = "br";
/// An HTTP Client request builder /// An HTTP Client request builder
/// ///
@ -408,7 +404,7 @@ impl ClientRequest {
pub fn send_stream<S, E>(self, stream: S) -> SendClientRequest pub fn send_stream<S, E>(self, stream: S) -> SendClientRequest
where where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static, S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
E: Into<Error> + 'static, E: Into<Box<dyn StdError>> + 'static,
{ {
let slf = match self.prep_for_sending() { let slf = match self.prep_for_sending() {
Ok(slf) => slf, Ok(slf) => slf,
@ -479,22 +475,37 @@ impl ClientRequest {
let mut slf = self; let mut slf = self;
// Set Accept-Encoding HTTP header depending on enabled feature.
// If decompress is not ask, then we are not able to find which encoding is
// supported, so we cannot guess Accept-Encoding HTTP header.
if slf.response_decompress { if slf.response_decompress {
let https = slf // Set Accept-Encoding with compression algorithm awc is built with.
.head #[cfg(feature = "__compress")]
.uri let accept_encoding = {
.scheme() let mut encoding = vec![];
.map(|s| s == &uri::Scheme::HTTPS)
.unwrap_or(true);
if https { #[cfg(feature = "compress-brotli")]
slf = slf.insert_header_if_none((header::ACCEPT_ENCODING, HTTPS_ENCODING)); encoding.push("br");
} else {
#[cfg(feature = "compress")] #[cfg(feature = "compress-gzip")]
{ {
slf = slf.insert_header_if_none((header::ACCEPT_ENCODING, "gzip, deflate")); encoding.push("gzip");
encoding.push("deflate");
} }
#[cfg(feature = "compress-zstd")]
encoding.push("zstd");
assert!(!encoding.is_empty(), "encoding cannot be empty unless __compress feature has been explictily enabled.");
encoding.join(", ")
}; };
// Otherwise tell the server, we do not support any compression algorithm.
// So we clearly indicate that we do want identity encoding.
#[cfg(not(feature = "__compress"))]
let accept_encoding = "identity";
slf = slf.insert_header_if_none((header::ACCEPT_ENCODING, accept_encoding));
} }
Ok(slf) Ok(slf)

View File

@ -1,6 +1,7 @@
use std::{ use std::{
error::Error as StdError,
future::Future, future::Future,
io, net, net,
pin::Pin, pin::Pin,
rc::Rc, rc::Rc,
task::{Context, Poll}, task::{Context, Poll},
@ -21,25 +22,33 @@ use derive_more::From;
use futures_core::Stream; use futures_core::Stream;
use serde::Serialize; use serde::Serialize;
#[cfg(feature = "compress")] #[cfg(feature = "__compress")]
use actix_http::{encoding::Decoder, http::header::ContentEncoding, Payload, PayloadStream}; use actix_http::{encoding::Decoder, http::header::ContentEncoding, Payload, PayloadStream};
use crate::connect::{ConnectRequest, ConnectResponse}; use crate::{
use crate::error::{FreezeRequestError, InvalidUrl, SendRequestError}; error::{FreezeRequestError, InvalidUrl, SendRequestError},
use crate::response::ClientResponse; ClientConfig, ClientResponse, ConnectRequest, ConnectResponse,
use crate::ClientConfig; };
#[derive(Debug, From)] #[derive(Debug, From)]
pub(crate) enum PrepForSendingError { pub(crate) enum PrepForSendingError {
Url(InvalidUrl), Url(InvalidUrl),
Http(HttpError), Http(HttpError),
Json(serde_json::Error),
Form(serde_urlencoded::ser::Error),
} }
impl From<PrepForSendingError> for FreezeRequestError { impl From<PrepForSendingError> for FreezeRequestError {
fn from(err: PrepForSendingError) -> FreezeRequestError { fn from(err: PrepForSendingError) -> FreezeRequestError {
match err { match err {
PrepForSendingError::Url(e) => FreezeRequestError::Url(e), PrepForSendingError::Url(err) => FreezeRequestError::Url(err),
PrepForSendingError::Http(e) => FreezeRequestError::Http(e), PrepForSendingError::Http(err) => FreezeRequestError::Http(err),
PrepForSendingError::Json(err) => {
FreezeRequestError::Custom(Box::new(err), Box::new("json serialization error"))
}
PrepForSendingError::Form(err) => {
FreezeRequestError::Custom(Box::new(err), Box::new("form serialization error"))
}
} }
} }
} }
@ -49,6 +58,12 @@ impl From<PrepForSendingError> for SendRequestError {
match err { match err {
PrepForSendingError::Url(e) => SendRequestError::Url(e), PrepForSendingError::Url(e) => SendRequestError::Url(e),
PrepForSendingError::Http(e) => SendRequestError::Http(e), PrepForSendingError::Http(e) => SendRequestError::Http(e),
PrepForSendingError::Json(err) => {
SendRequestError::Custom(Box::new(err), Box::new("json serialization error"))
}
PrepForSendingError::Form(err) => {
SendRequestError::Custom(Box::new(err), Box::new("form serialization error"))
}
} }
} }
} }
@ -76,7 +91,7 @@ impl SendClientRequest {
} }
} }
#[cfg(feature = "compress")] #[cfg(feature = "__compress")]
impl Future for SendClientRequest { impl Future for SendClientRequest {
type Output = Result<ClientResponse<Decoder<Payload<PayloadStream>>>, SendRequestError>; type Output = Result<ClientResponse<Decoder<Payload<PayloadStream>>>, SendRequestError>;
@ -116,7 +131,7 @@ impl Future for SendClientRequest {
} }
} }
#[cfg(not(feature = "compress"))] #[cfg(not(feature = "__compress"))]
impl Future for SendClientRequest { impl Future for SendClientRequest {
type Output = Result<ClientResponse, SendRequestError>; type Output = Result<ClientResponse, SendRequestError>;
@ -209,8 +224,7 @@ impl RequestSender {
) -> SendClientRequest { ) -> SendClientRequest {
let body = match serde_json::to_string(value) { let body = match serde_json::to_string(value) {
Ok(body) => body, Ok(body) => body,
// TODO: own error type Err(err) => return PrepForSendingError::Json(err).into(),
Err(e) => return Error::from(io::Error::new(io::ErrorKind::Other, e)).into(),
}; };
if let Err(e) = self.set_header_if_none(header::CONTENT_TYPE, "application/json") { if let Err(e) = self.set_header_if_none(header::CONTENT_TYPE, "application/json") {
@ -236,8 +250,7 @@ impl RequestSender {
) -> SendClientRequest { ) -> SendClientRequest {
let body = match serde_urlencoded::to_string(value) { let body = match serde_urlencoded::to_string(value) {
Ok(body) => body, Ok(body) => body,
// TODO: own error type Err(err) => return PrepForSendingError::Form(err).into(),
Err(e) => return Error::from(io::Error::new(io::ErrorKind::Other, e)).into(),
}; };
// set content-type // set content-type
@ -266,7 +279,7 @@ impl RequestSender {
) -> SendClientRequest ) -> SendClientRequest
where where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static, S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
E: Into<Error> + 'static, E: Into<Box<dyn StdError>> + 'static,
{ {
self.send_body( self.send_body(
addr, addr,

View File

@ -1,4 +1,4 @@
use actix_web::{test, web, App, HttpResponse}; use actix_web::{web, App, HttpResponse};
use awc::Client; use awc::Client;
use criterion::{criterion_group, criterion_main, Criterion}; use criterion::{criterion_group, criterion_main, Criterion};
use futures_util::future::join_all; use futures_util::future::join_all;

View File

@ -166,8 +166,7 @@ impl AppInitServiceState {
Rc::new(AppInitServiceState { Rc::new(AppInitServiceState {
rmap, rmap,
config, config,
// TODO: AppConfig can be used to pass user defined HttpRequestPool // TODO: AppConfig can be used to pass user defined HttpRequestPool capacity.
// capacity.
pool: HttpRequestPool::default(), pool: HttpRequestPool::default(),
}) })
} }

View File

@ -1,16 +1,14 @@
use std::any::type_name; use std::{any::type_name, ops::Deref, sync::Arc};
use std::ops::Deref;
use std::sync::Arc;
use actix_http::error::{Error, ErrorInternalServerError};
use actix_http::Extensions; use actix_http::Extensions;
use actix_utils::future::{err, ok, Ready}; use actix_utils::future::{err, ok, Ready};
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use serde::Serialize; use serde::Serialize;
use crate::dev::Payload; use crate::{
use crate::extract::FromRequest; dev::Payload, error::ErrorInternalServerError, extract::FromRequest, request::HttpRequest,
use crate::request::HttpRequest; Error,
};
/// Data factory. /// Data factory.
pub(crate) trait DataFactory { pub(crate) trait DataFactory {

76
src/error/error.rs Normal file
View File

@ -0,0 +1,76 @@
use std::{error::Error as StdError, fmt};
use actix_http::{body::AnyBody, Response};
use crate::{HttpResponse, ResponseError};
/// General purpose actix web error.
///
/// An actix web error is used to carry errors from `std::error`
/// through actix in a convenient way. It can be created through
/// converting errors with `into()`.
///
/// Whenever it is created from an external object a response error is created
/// for it that can be used to create an HTTP response from it this means that
/// if you have access to an actix `Error` you can always get a
/// `ResponseError` reference from it.
pub struct Error {
cause: Box<dyn ResponseError>,
}
impl Error {
/// Returns the reference to the underlying `ResponseError`.
pub fn as_response_error(&self) -> &dyn ResponseError {
self.cause.as_ref()
}
/// Similar to `as_response_error` but downcasts.
pub fn as_error<T: ResponseError + 'static>(&self) -> Option<&T> {
<dyn ResponseError>::downcast_ref(self.cause.as_ref())
}
/// Shortcut for creating an `HttpResponse`.
pub fn error_response(&self) -> HttpResponse {
self.cause.error_response()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.cause, f)
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", &self.cause)
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
// TODO: populate if replacement for Box<dyn Error> is found
None
}
}
impl From<std::convert::Infallible> for Error {
fn from(val: std::convert::Infallible) -> Self {
match val {}
}
}
/// `Error` for any error that implements `ResponseError`
impl<T: ResponseError + 'static> From<T> for Error {
fn from(err: T) -> Error {
Error {
cause: Box::new(err),
}
}
}
impl From<Error> for Response<AnyBody> {
fn from(err: Error) -> Response<AnyBody> {
err.error_response().into()
}
}

313
src/error/internal.rs Normal file
View File

@ -0,0 +1,313 @@
use std::{cell::RefCell, fmt, io::Write as _};
use actix_http::{body::Body, header, StatusCode};
use bytes::{BufMut as _, BytesMut};
use crate::{Error, HttpRequest, HttpResponse, Responder, ResponseError};
/// Wraps errors to alter the generated response status code.
///
/// In following example, the `io::Error` is wrapped into `ErrorBadRequest` which will generate a
/// response with the 400 Bad Request status code instead of the usual status code generated by
/// an `io::Error`.
///
/// # Examples
/// ```
/// # use std::io;
/// # use actix_web::{error, HttpRequest};
/// async fn handler_error() -> Result<String, actix_web::Error> {
/// let err = io::Error::new(io::ErrorKind::Other, "error");
/// Err(error::ErrorBadRequest(err))
/// }
/// ```
pub struct InternalError<T> {
cause: T,
status: InternalErrorType,
}
enum InternalErrorType {
Status(StatusCode),
Response(RefCell<Option<HttpResponse>>),
}
impl<T> InternalError<T> {
/// Constructs an `InternalError` with given status code.
pub fn new(cause: T, status: StatusCode) -> Self {
InternalError {
cause,
status: InternalErrorType::Status(status),
}
}
/// Constructs an `InternalError` with pre-defined response.
pub fn from_response(cause: T, response: HttpResponse) -> Self {
InternalError {
cause,
status: InternalErrorType::Response(RefCell::new(Some(response))),
}
}
}
impl<T: fmt::Debug> fmt::Debug for InternalError<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.cause.fmt(f)
}
}
impl<T: fmt::Display> fmt::Display for InternalError<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.cause.fmt(f)
}
}
impl<T> ResponseError for InternalError<T>
where
T: fmt::Debug + fmt::Display,
{
fn status_code(&self) -> StatusCode {
match self.status {
InternalErrorType::Status(st) => st,
InternalErrorType::Response(ref resp) => {
if let Some(resp) = resp.borrow().as_ref() {
resp.head().status
} else {
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
}
fn error_response(&self) -> HttpResponse {
match self.status {
InternalErrorType::Status(status) => {
let mut res = HttpResponse::new(status);
let mut buf = BytesMut::new().writer();
let _ = write!(buf, "{}", self);
res.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("text/plain; charset=utf-8"),
);
res.set_body(Body::from(buf.into_inner()))
}
InternalErrorType::Response(ref resp) => {
if let Some(resp) = resp.borrow_mut().take() {
resp
} else {
HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
}
}
impl<T> Responder for InternalError<T>
where
T: fmt::Debug + fmt::Display + 'static,
{
fn respond_to(self, _: &HttpRequest) -> HttpResponse {
HttpResponse::from_error(self)
}
}
macro_rules! error_helper {
($name:ident, $status:ident) => {
paste::paste! {
#[doc = "Helper function that wraps any error and generates a `" $status "` response."]
#[allow(non_snake_case)]
pub fn $name<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::$status).into()
}
}
}
}
error_helper!(ErrorBadRequest, BAD_REQUEST);
error_helper!(ErrorUnauthorized, UNAUTHORIZED);
error_helper!(ErrorPaymentRequired, PAYMENT_REQUIRED);
error_helper!(ErrorForbidden, FORBIDDEN);
error_helper!(ErrorNotFound, NOT_FOUND);
error_helper!(ErrorMethodNotAllowed, METHOD_NOT_ALLOWED);
error_helper!(ErrorNotAcceptable, NOT_ACCEPTABLE);
error_helper!(
ErrorProxyAuthenticationRequired,
PROXY_AUTHENTICATION_REQUIRED
);
error_helper!(ErrorRequestTimeout, REQUEST_TIMEOUT);
error_helper!(ErrorConflict, CONFLICT);
error_helper!(ErrorGone, GONE);
error_helper!(ErrorLengthRequired, LENGTH_REQUIRED);
error_helper!(ErrorPayloadTooLarge, PAYLOAD_TOO_LARGE);
error_helper!(ErrorUriTooLong, URI_TOO_LONG);
error_helper!(ErrorUnsupportedMediaType, UNSUPPORTED_MEDIA_TYPE);
error_helper!(ErrorRangeNotSatisfiable, RANGE_NOT_SATISFIABLE);
error_helper!(ErrorImATeapot, IM_A_TEAPOT);
error_helper!(ErrorMisdirectedRequest, MISDIRECTED_REQUEST);
error_helper!(ErrorUnprocessableEntity, UNPROCESSABLE_ENTITY);
error_helper!(ErrorLocked, LOCKED);
error_helper!(ErrorFailedDependency, FAILED_DEPENDENCY);
error_helper!(ErrorUpgradeRequired, UPGRADE_REQUIRED);
error_helper!(ErrorPreconditionFailed, PRECONDITION_FAILED);
error_helper!(ErrorPreconditionRequired, PRECONDITION_REQUIRED);
error_helper!(ErrorTooManyRequests, TOO_MANY_REQUESTS);
error_helper!(
ErrorRequestHeaderFieldsTooLarge,
REQUEST_HEADER_FIELDS_TOO_LARGE
);
error_helper!(
ErrorUnavailableForLegalReasons,
UNAVAILABLE_FOR_LEGAL_REASONS
);
error_helper!(ErrorExpectationFailed, EXPECTATION_FAILED);
error_helper!(ErrorInternalServerError, INTERNAL_SERVER_ERROR);
error_helper!(ErrorNotImplemented, NOT_IMPLEMENTED);
error_helper!(ErrorBadGateway, BAD_GATEWAY);
error_helper!(ErrorServiceUnavailable, SERVICE_UNAVAILABLE);
error_helper!(ErrorGatewayTimeout, GATEWAY_TIMEOUT);
error_helper!(ErrorHttpVersionNotSupported, HTTP_VERSION_NOT_SUPPORTED);
error_helper!(ErrorVariantAlsoNegotiates, VARIANT_ALSO_NEGOTIATES);
error_helper!(ErrorInsufficientStorage, INSUFFICIENT_STORAGE);
error_helper!(ErrorLoopDetected, LOOP_DETECTED);
error_helper!(ErrorNotExtended, NOT_EXTENDED);
error_helper!(
ErrorNetworkAuthenticationRequired,
NETWORK_AUTHENTICATION_REQUIRED
);
#[cfg(test)]
mod tests {
use actix_http::error::ParseError;
use super::*;
#[test]
fn test_internal_error() {
let err = InternalError::from_response(ParseError::Method, HttpResponse::Ok().finish());
let resp: HttpResponse = err.error_response();
assert_eq!(resp.status(), StatusCode::OK);
}
#[test]
fn test_error_helpers() {
let res: HttpResponse = ErrorBadRequest("err").into();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let res: HttpResponse = ErrorUnauthorized("err").into();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
let res: HttpResponse = ErrorPaymentRequired("err").into();
assert_eq!(res.status(), StatusCode::PAYMENT_REQUIRED);
let res: HttpResponse = ErrorForbidden("err").into();
assert_eq!(res.status(), StatusCode::FORBIDDEN);
let res: HttpResponse = ErrorNotFound("err").into();
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let res: HttpResponse = ErrorMethodNotAllowed("err").into();
assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED);
let res: HttpResponse = ErrorNotAcceptable("err").into();
assert_eq!(res.status(), StatusCode::NOT_ACCEPTABLE);
let res: HttpResponse = ErrorProxyAuthenticationRequired("err").into();
assert_eq!(res.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED);
let res: HttpResponse = ErrorRequestTimeout("err").into();
assert_eq!(res.status(), StatusCode::REQUEST_TIMEOUT);
let res: HttpResponse = ErrorConflict("err").into();
assert_eq!(res.status(), StatusCode::CONFLICT);
let res: HttpResponse = ErrorGone("err").into();
assert_eq!(res.status(), StatusCode::GONE);
let res: HttpResponse = ErrorLengthRequired("err").into();
assert_eq!(res.status(), StatusCode::LENGTH_REQUIRED);
let res: HttpResponse = ErrorPreconditionFailed("err").into();
assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED);
let res: HttpResponse = ErrorPayloadTooLarge("err").into();
assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
let res: HttpResponse = ErrorUriTooLong("err").into();
assert_eq!(res.status(), StatusCode::URI_TOO_LONG);
let res: HttpResponse = ErrorUnsupportedMediaType("err").into();
assert_eq!(res.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
let res: HttpResponse = ErrorRangeNotSatisfiable("err").into();
assert_eq!(res.status(), StatusCode::RANGE_NOT_SATISFIABLE);
let res: HttpResponse = ErrorExpectationFailed("err").into();
assert_eq!(res.status(), StatusCode::EXPECTATION_FAILED);
let res: HttpResponse = ErrorImATeapot("err").into();
assert_eq!(res.status(), StatusCode::IM_A_TEAPOT);
let res: HttpResponse = ErrorMisdirectedRequest("err").into();
assert_eq!(res.status(), StatusCode::MISDIRECTED_REQUEST);
let res: HttpResponse = ErrorUnprocessableEntity("err").into();
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
let res: HttpResponse = ErrorLocked("err").into();
assert_eq!(res.status(), StatusCode::LOCKED);
let res: HttpResponse = ErrorFailedDependency("err").into();
assert_eq!(res.status(), StatusCode::FAILED_DEPENDENCY);
let res: HttpResponse = ErrorUpgradeRequired("err").into();
assert_eq!(res.status(), StatusCode::UPGRADE_REQUIRED);
let res: HttpResponse = ErrorPreconditionRequired("err").into();
assert_eq!(res.status(), StatusCode::PRECONDITION_REQUIRED);
let res: HttpResponse = ErrorTooManyRequests("err").into();
assert_eq!(res.status(), StatusCode::TOO_MANY_REQUESTS);
let res: HttpResponse = ErrorRequestHeaderFieldsTooLarge("err").into();
assert_eq!(res.status(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE);
let res: HttpResponse = ErrorUnavailableForLegalReasons("err").into();
assert_eq!(res.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS);
let res: HttpResponse = ErrorInternalServerError("err").into();
assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
let res: HttpResponse = ErrorNotImplemented("err").into();
assert_eq!(res.status(), StatusCode::NOT_IMPLEMENTED);
let res: HttpResponse = ErrorBadGateway("err").into();
assert_eq!(res.status(), StatusCode::BAD_GATEWAY);
let res: HttpResponse = ErrorServiceUnavailable("err").into();
assert_eq!(res.status(), StatusCode::SERVICE_UNAVAILABLE);
let res: HttpResponse = ErrorGatewayTimeout("err").into();
assert_eq!(res.status(), StatusCode::GATEWAY_TIMEOUT);
let res: HttpResponse = ErrorHttpVersionNotSupported("err").into();
assert_eq!(res.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED);
let res: HttpResponse = ErrorVariantAlsoNegotiates("err").into();
assert_eq!(res.status(), StatusCode::VARIANT_ALSO_NEGOTIATES);
let res: HttpResponse = ErrorInsufficientStorage("err").into();
assert_eq!(res.status(), StatusCode::INSUFFICIENT_STORAGE);
let res: HttpResponse = ErrorLoopDetected("err").into();
assert_eq!(res.status(), StatusCode::LOOP_DETECTED);
let res: HttpResponse = ErrorNotExtended("err").into();
assert_eq!(res.status(), StatusCode::NOT_EXTENDED);
let res: HttpResponse = ErrorNetworkAuthenticationRequired("err").into();
assert_eq!(res.status(), StatusCode::NETWORK_AUTHENTICATION_REQUIRED);
}
}

109
src/error/macros.rs Normal file
View File

@ -0,0 +1,109 @@
#[macro_export]
#[doc(hidden)]
macro_rules! __downcast_get_type_id {
() => {
/// A helper method to get the type ID of the type
/// this trait is implemented on.
/// This method is unsafe to *implement*, since `downcast_ref` relies
/// on the returned `TypeId` to perform a cast.
///
/// Unfortunately, Rust has no notion of a trait method that is
/// unsafe to implement (marking it as `unsafe` makes it unsafe
/// to *call*). As a workaround, we require this method
/// to return a private type along with the `TypeId`. This
/// private type (`PrivateHelper`) has a private constructor,
/// making it impossible for safe code to construct outside of
/// this module. This ensures that safe code cannot violate
/// type-safety by implementing this method.
///
/// We also take `PrivateHelper` as a parameter, to ensure that
/// safe code cannot obtain a `PrivateHelper` instance by
/// delegating to an existing implementation of `__private_get_type_id__`
#[doc(hidden)]
#[allow(dead_code)]
fn __private_get_type_id__(&self, _: PrivateHelper) -> (std::any::TypeId, PrivateHelper)
where
Self: 'static,
{
(std::any::TypeId::of::<Self>(), PrivateHelper(()))
}
};
}
//Generate implementation for dyn $name
#[doc(hidden)]
#[macro_export]
macro_rules! __downcast_dyn {
($name:ident) => {
/// A struct with a private constructor, for use with
/// `__private_get_type_id__`. Its single field is private,
/// ensuring that it can only be constructed from this module
#[doc(hidden)]
#[allow(dead_code)]
pub struct PrivateHelper(());
impl dyn $name + 'static {
/// Downcasts generic body to a specific type.
#[allow(dead_code)]
pub fn downcast_ref<T: $name + 'static>(&self) -> Option<&T> {
if self.__private_get_type_id__(PrivateHelper(())).0
== std::any::TypeId::of::<T>()
{
// SAFETY: external crates cannot override the default
// implementation of `__private_get_type_id__`, since
// it requires returning a private type. We can therefore
// rely on the returned `TypeId`, which ensures that this
// case is correct.
unsafe { Some(&*(self as *const dyn $name as *const T)) }
} else {
None
}
}
/// Downcasts a generic body to a mutable specific type.
#[allow(dead_code)]
pub fn downcast_mut<T: $name + 'static>(&mut self) -> Option<&mut T> {
if self.__private_get_type_id__(PrivateHelper(())).0
== std::any::TypeId::of::<T>()
{
// SAFETY: external crates cannot override the default
// implementation of `__private_get_type_id__`, since
// it requires returning a private type. We can therefore
// rely on the returned `TypeId`, which ensures that this
// case is correct.
unsafe { Some(&mut *(self as *const dyn $name as *const T as *mut T)) }
} else {
None
}
}
}
};
}
#[cfg(test)]
mod tests {
#![allow(clippy::upper_case_acronyms)]
trait MB {
__downcast_get_type_id!();
}
__downcast_dyn!(MB);
impl MB for String {}
impl MB for () {}
#[actix_rt::test]
async fn test_any_casting() {
let mut body = String::from("hello cast");
let resp_body: &mut dyn MB = &mut body;
let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast");
let body = &mut resp_body.downcast_mut::<String>().unwrap();
body.push('!');
let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast!");
let not_body = resp_body.downcast_ref::<()>();
assert!(not_body.is_none());
}
}

View File

@ -9,6 +9,21 @@ use url::ParseError as UrlParseError;
use crate::http::StatusCode; use crate::http::StatusCode;
#[allow(clippy::module_inception)]
mod error;
mod internal;
mod macros;
mod response_error;
pub use self::error::Error;
pub use self::internal::*;
pub use self::response_error::ResponseError;
/// A convenience [`Result`](std::result::Result) for Actix Web operations.
///
/// This type alias is generally used to avoid writing out `actix_http::Error` directly.
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Errors which can occur when attempting to generate resource uri. /// Errors which can occur when attempting to generate resource uri.
#[derive(Debug, PartialEq, Display, Error, From)] #[derive(Debug, PartialEq, Display, Error, From)]
#[non_exhaustive] #[non_exhaustive]
@ -26,7 +41,6 @@ pub enum UrlGenerationError {
ParseError(UrlParseError), ParseError(UrlParseError),
} }
/// `InternalServerError` for `UrlGeneratorError`
impl ResponseError for UrlGenerationError {} impl ResponseError for UrlGenerationError {}
/// A set of errors that can occur during parsing urlencoded payloads /// A set of errors that can occur during parsing urlencoded payloads
@ -70,7 +84,6 @@ pub enum UrlencodedError {
Payload(PayloadError), Payload(PayloadError),
} }
/// Return `BadRequest` for `UrlencodedError`
impl ResponseError for UrlencodedError { impl ResponseError for UrlencodedError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
match self { match self {
@ -86,9 +99,17 @@ impl ResponseError for UrlencodedError {
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[non_exhaustive] #[non_exhaustive]
pub enum JsonPayloadError { pub enum JsonPayloadError {
/// Payload size is bigger than allowed. (default: 32kB) /// Payload size is bigger than allowed & content length header set. (default: 2MB)
#[display(fmt = "Json payload size is bigger than allowed")] #[display(
Overflow, fmt = "JSON payload ({} bytes) is larger than allowed (limit: {} bytes).",
length,
limit
)]
OverflowKnownLength { length: usize, limit: usize },
/// Payload size is bigger than allowed but no content length header set. (default: 2MB)
#[display(fmt = "JSON payload has exceeded limit ({} bytes).", limit)]
Overflow { limit: usize },
/// Content type error /// Content type error
#[display(fmt = "Content type error")] #[display(fmt = "Content type error")]
@ -116,7 +137,11 @@ impl From<PayloadError> for JsonPayloadError {
impl ResponseError for JsonPayloadError { impl ResponseError for JsonPayloadError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
match self { match self {
Self::Overflow => StatusCode::PAYLOAD_TOO_LARGE, Self::OverflowKnownLength {
length: _,
limit: _,
} => StatusCode::PAYLOAD_TOO_LARGE,
Self::Overflow { limit: _ } => StatusCode::PAYLOAD_TOO_LARGE,
Self::Serialize(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::Serialize(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Payload(err) => err.status_code(), Self::Payload(err) => err.status_code(),
_ => StatusCode::BAD_REQUEST, _ => StatusCode::BAD_REQUEST,
@ -149,7 +174,6 @@ pub enum QueryPayloadError {
Deserialize(serde::de::value::Error), Deserialize(serde::de::value::Error),
} }
/// Return `BadRequest` for `QueryPayloadError`
impl ResponseError for QueryPayloadError { impl ResponseError for QueryPayloadError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST StatusCode::BAD_REQUEST
@ -177,7 +201,6 @@ pub enum ReadlinesError {
ContentTypeError(ContentTypeError), ContentTypeError(ContentTypeError),
} }
/// Return `BadRequest` for `ReadlinesError`
impl ResponseError for ReadlinesError { impl ResponseError for ReadlinesError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
match *self { match *self {
@ -203,7 +226,13 @@ mod tests {
#[test] #[test]
fn test_json_payload_error() { fn test_json_payload_error() {
let resp = JsonPayloadError::Overflow.error_response(); let resp = JsonPayloadError::OverflowKnownLength {
length: 0,
limit: 0,
}
.error_response();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
let resp = JsonPayloadError::Overflow { limit: 0 }.error_response();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
let resp = JsonPayloadError::ContentType.error_response(); let resp = JsonPayloadError::ContentType.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);

144
src/error/response_error.rs Normal file
View File

@ -0,0 +1,144 @@
//! `ResponseError` trait and foreign impls.
use std::{
error::Error as StdError,
fmt,
io::{self, Write as _},
};
use actix_http::{body::AnyBody, header, Response, StatusCode};
use bytes::BytesMut;
use crate::{__downcast_dyn, __downcast_get_type_id};
use crate::{helpers, HttpResponse};
/// Errors that can generate responses.
// TODO: add std::error::Error bound when replacement for Box<dyn Error> is found
pub trait ResponseError: fmt::Debug + fmt::Display {
/// Returns appropriate status code for error.
///
/// A 500 Internal Server Error is used by default. If [error_response](Self::error_response) is
/// also implemented and does not call `self.status_code()`, then this will not be used.
fn status_code(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
/// Creates full response for error.
///
/// By default, the generated response uses a 500 Internal Server Error status code, a
/// `Content-Type` of `text/plain`, and the body is set to `Self`'s `Display` impl.
fn error_response(&self) -> HttpResponse {
let mut res = HttpResponse::new(self.status_code());
let mut buf = BytesMut::new();
let _ = write!(helpers::MutWriter(&mut buf), "{}", self);
res.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("text/plain; charset=utf-8"),
);
res.set_body(AnyBody::from(buf))
}
__downcast_get_type_id!();
}
__downcast_dyn!(ResponseError);
impl ResponseError for Box<dyn StdError + 'static> {}
#[cfg(feature = "openssl")]
impl ResponseError for actix_tls::accept::openssl::SslError {}
impl ResponseError for serde::de::value::Error {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
impl ResponseError for std::str::Utf8Error {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
impl ResponseError for std::io::Error {
fn status_code(&self) -> StatusCode {
// TODO: decide if these errors should consider not found or permission errors
match self.kind() {
io::ErrorKind::NotFound => StatusCode::NOT_FOUND,
io::ErrorKind::PermissionDenied => StatusCode::FORBIDDEN,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl ResponseError for actix_http::error::HttpError {}
impl ResponseError for actix_http::Error {
fn status_code(&self) -> StatusCode {
// TODO: map error kinds to status code better
StatusCode::INTERNAL_SERVER_ERROR
}
fn error_response(&self) -> HttpResponse {
HttpResponse::new(self.status_code()).set_body(self.to_string().into())
}
}
impl ResponseError for actix_http::header::InvalidHeaderValue {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
impl ResponseError for actix_http::error::ParseError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
impl ResponseError for actix_http::error::BlockingError {}
impl ResponseError for actix_http::error::PayloadError {
fn status_code(&self) -> StatusCode {
match *self {
actix_http::error::PayloadError::Overflow => StatusCode::PAYLOAD_TOO_LARGE,
_ => StatusCode::BAD_REQUEST,
}
}
}
impl ResponseError for actix_http::ws::ProtocolError {}
impl ResponseError for actix_http::error::ContentTypeError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
impl ResponseError for actix_http::ws::HandshakeError {
fn error_response(&self) -> HttpResponse {
Response::from(self).into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_casting() {
use actix_http::error::{ContentTypeError, PayloadError};
let err = PayloadError::Overflow;
let resp_err: &dyn ResponseError = &err;
let err = resp_err.downcast_ref::<PayloadError>().unwrap();
assert_eq!(err.to_string(), "Payload reached size limit.");
let not_err = resp_err.downcast_ref::<ContentTypeError>();
assert!(not_err.is_none());
}
}

View File

@ -47,8 +47,7 @@ pub trait FromRequest: Sized {
/// ///
/// If the FromRequest for T fails, return None rather than returning an error response /// If the FromRequest for T fails, return None rather than returning an error response
/// ///
/// ## Example /// # Examples
///
/// ``` /// ```
/// use actix_web::{web, dev, App, Error, HttpRequest, FromRequest}; /// use actix_web::{web, dev, App, Error, HttpRequest, FromRequest};
/// use actix_web::error::ErrorBadRequest; /// use actix_web::error::ErrorBadRequest;
@ -139,8 +138,7 @@ where
/// ///
/// If the `FromRequest` for T fails, inject Err into handler rather than returning an error response /// If the `FromRequest` for T fails, inject Err into handler rather than returning an error response
/// ///
/// ## Example /// # Examples
///
/// ``` /// ```
/// use actix_web::{web, dev, App, Result, Error, HttpRequest, FromRequest}; /// use actix_web::{web, dev, App, Result, Error, HttpRequest, FromRequest};
/// use actix_web::error::ErrorBadRequest; /// use actix_web::error::ErrorBadRequest;

View File

@ -26,6 +26,8 @@
//! ``` //! ```
#![allow(non_snake_case)] #![allow(non_snake_case)]
use std::convert::TryFrom; use std::convert::TryFrom;
use std::ops::Deref;
use std::rc::Rc;
use actix_http::http::{self, header, uri::Uri}; use actix_http::http::{self, header, uri::Uri};
use actix_http::RequestHead; use actix_http::RequestHead;
@ -40,6 +42,12 @@ pub trait Guard {
fn check(&self, request: &RequestHead) -> bool; fn check(&self, request: &RequestHead) -> bool;
} }
impl Guard for Rc<dyn Guard> {
fn check(&self, request: &RequestHead) -> bool {
self.deref().check(request)
}
}
/// Create guard object for supplied function. /// Create guard object for supplied function.
/// ///
/// ``` /// ```

View File

@ -3,18 +3,14 @@ use std::marker::PhantomData;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use actix_http::Error;
use actix_service::{Service, ServiceFactory}; use actix_service::{Service, ServiceFactory};
use actix_utils::future::{ready, Ready}; use actix_utils::future::{ready, Ready};
use futures_core::ready; use futures_core::ready;
use pin_project::pin_project; use pin_project::pin_project;
use crate::{ use crate::{
extract::FromRequest,
request::HttpRequest,
responder::Responder,
response::HttpResponse,
service::{ServiceRequest, ServiceResponse}, service::{ServiceRequest, ServiceResponse},
Error, FromRequest, HttpRequest, 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

25
src/helpers.rs Normal file
View File

@ -0,0 +1,25 @@
use std::io;
use bytes::BufMut;
/// An `io::Write`r that only requires mutable reference and assumes that there is space available
/// in the buffer for every write operation or that it can be extended implicitly (like
/// `bytes::BytesMut`, for example).
///
/// This is slightly faster (~10%) than `bytes::buf::Writer` in such cases because it does not
/// perform a remaining length check before writing.
pub(crate) struct MutWriter<'a, B>(pub(crate) &'a mut B);
impl<'a, B> io::Write for MutWriter<'a, B>
where
B: BufMut,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.put_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}

View File

@ -24,14 +24,11 @@ crate::__define_common_header! {
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// use language_tags::langtag;
/// use actix_web::HttpResponse; /// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptLanguage, LanguageTag, qitem}; /// use actix_web::http::header::{AcceptLanguage, LanguageTag, qitem};
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = HttpResponse::Ok();
/// let mut langtag: LanguageTag = Default::default(); /// let langtag = LanguageTag::parse("en-US").unwrap();
/// langtag.language = Some("en".to_owned());
/// langtag.region = Some("US".to_owned());
/// builder.insert_header( /// builder.insert_header(
/// AcceptLanguage(vec![ /// AcceptLanguage(vec![
/// qitem(langtag), /// qitem(langtag),
@ -40,16 +37,15 @@ crate::__define_common_header! {
/// ``` /// ```
/// ///
/// ``` /// ```
/// use language_tags::langtag;
/// use actix_web::HttpResponse; /// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptLanguage, QualityItem, q, qitem}; /// use actix_web::http::header::{AcceptLanguage, LanguageTag, QualityItem, q, qitem};
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = HttpResponse::Ok();
/// builder.insert_header( /// builder.insert_header(
/// AcceptLanguage(vec![ /// AcceptLanguage(vec![
/// qitem(langtag!(da)), /// qitem(LanguageTag::parse("da").unwrap()),
/// QualityItem::new(langtag!(en;;;GB), q(800)), /// QualityItem::new(LanguageTag::parse("en-GB").unwrap(), q(800)),
/// QualityItem::new(langtag!(en), q(700)), /// QualityItem::new(LanguageTag::parse("en").unwrap(), q(700)),
/// ]) /// ])
/// ); /// );
/// ``` /// ```

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