1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-08-21 13:15:38 +02:00

Compare commits

..

2 Commits

Author SHA1 Message Date
Rob Ede
e2a803d278 Merge branch 'master' into get-all-order 2021-11-28 00:20:08 +00:00
Rob Ede
67d2845882 guarantee ordering of header map get_all
closes #2466
2021-11-28 00:18:15 +00:00
178 changed files with 3484 additions and 5456 deletions

View File

@@ -5,10 +5,8 @@ lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dcli
# lib checking # lib checking
ci-check-min = "hack --workspace check --no-default-features" ci-check-min = "hack --workspace check --no-default-features"
ci-check-default = "hack --workspace check" ci-check-default = "hack --workspace check"
ci-check-default-tests = "check --workspace --tests"
ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,io-uring check" ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,io-uring check"
ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check" ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check"
# testing # testing
ci-doctest-default = "test --workspace --doc --no-fail-fast -- --nocapture"
ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture" ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture"

View File

@@ -1,43 +1,6 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
### Added
* Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480]
* `AcceptEncoding` typed header. [#2482]
* `Range` typed header. [#2485]
* `HttpResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468]
* `ServiceResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468]
* `HttpServer::on_connect` now receives a `CloneableExtensions` object. [#2327]
[#2325]: https://github.com/actix/actix-web/pull/2325
[#2327]: https://github.com/actix/actix-web/pull/2327
### Changed
* Rename `Accept::{mime_precedence => ranked}`. [#2480]
* Rename `Accept::{mime_preference => preference}`. [#2480]
* Un-deprecate `App::data_factory`. [#2484]
* `HttpRequest::url_for` no longer constructs URLs with query or fragment components. [#2430]
* `HttpServer::on_connect` now receives a `CloneableExtensions` object. [#2327]
### Fixed
* Accept wildcard `*` items in `AcceptLanguage`. [#2480]
* Re-exports `dev::{BodySize, MessageBody, SizedStream}`. They are exposed through the `body` module. [#2468]
* Typed headers containing lists that require one or more items now enforce this minimum. [#2482]
[#2327]: https://github.com/actix/actix-web/pull/2327
[#2430]: https://github.com/actix/actix-web/pull/2430
[#2468]: https://github.com/actix/actix-web/pull/2468
[#2480]: https://github.com/actix/actix-web/pull/2480
[#2482]: https://github.com/actix/actix-web/pull/2482
[#2484]: https://github.com/actix/actix-web/pull/2484
[#2485]: https://github.com/actix/actix-web/pull/2485
## 4.0.0-beta.13 - 2021-11-30
### Changed
* Update `actix-tls` to `3.0.0-rc.1`. [#2474]
[#2474]: https://github.com/actix/actix-web/pull/2474
## 4.0.0-beta.12 - 2021-11-22 ## 4.0.0-beta.12 - 2021-11-22

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.0.0-beta.13" version = "4.0.0-beta.12"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
@@ -72,12 +72,12 @@ experimental-io-uring = ["actix-server/io-uring"]
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-macros = "0.2.3" actix-macros = "0.2.3"
actix-rt = "2.3" actix-rt = "2.3"
actix-server = "2.0.0-rc.1" actix-server = "2.0.0-beta.9"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true } actix-tls = { version = "3.0.0-beta.9", default-features = false, optional = true }
actix-http = "3.0.0-beta.14" actix-http = "3.0.0-beta.13"
actix-router = "0.5.0-beta.2" actix-router = "0.5.0-beta.2"
actix-web-codegen = "0.5.0-beta.5" actix-web-codegen = "0.5.0-beta.5"
@@ -96,7 +96,7 @@ once_cell = "1.5"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
paste = "1" paste = "1"
pin-project-lite = "0.2.7" pin-project = "1.0.0"
regex = "1.4" regex = "1.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
@@ -143,15 +143,6 @@ actix-web-actors = { path = "actix-web-actors" }
actix-web-codegen = { path = "actix-web-codegen" } actix-web-codegen = { path = "actix-web-codegen" }
awc = { path = "awc" } awc = { path = "awc" }
# uncomment for quick testing against local actix-net repo
# actix-service = { path = "../actix-net/actix-service" }
# actix-macros = { path = "../actix-net/actix-macros" }
# actix-rt = { path = "../actix-net/actix-rt" }
# actix-codec = { path = "../actix-net/actix-codec" }
# actix-utils = { path = "../actix-net/actix-utils" }
# actix-tls = { path = "../actix-net/actix-tls" }
# actix-server = { path = "../actix-net/actix-server" }
[[test]] [[test]]
name = "test_server" name = "test_server"
required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]

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

View File

@@ -23,9 +23,8 @@ experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
[dependencies] [dependencies]
actix-web = { version = "4.0.0-beta.11", default-features = false } actix-web = { version = "4.0.0-beta.11", default-features = false }
actix-http = "3.0.0-beta.14" actix-http = "3.0.0-beta.13"
actix-service = "2" actix-service = "2.0.0"
actix-utils = "3"
askama_escape = "0.10" askama_escape = "0.10"
bitflags = "1" bitflags = "1"

View File

@@ -6,7 +6,8 @@ use std::{
task::{Context, Poll}, task::{Context, Poll},
}; };
use actix_web::{error::Error, web::Bytes}; use actix_web::error::Error;
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;

View File

@@ -10,19 +10,18 @@ use std::{
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use actix_http::body::AnyBody;
use actix_service::{Service, ServiceFactory}; use actix_service::{Service, ServiceFactory};
use actix_web::{ use actix_web::{
body::{self, BoxBody, SizedStream},
dev::{ dev::{
AppService, BodyEncoding, HttpServiceFactory, ResourceDef, ServiceRequest, AppService, BodyEncoding, HttpServiceFactory, ResourceDef, ServiceRequest,
ServiceResponse, ServiceResponse, SizedStream,
}, },
http::{ http::{
header::{ header::{
self, Charset, ContentDisposition, ContentEncoding, DispositionParam, self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
DispositionType, ExtendedValue,
}, },
StatusCode, ContentEncoding, StatusCode,
}, },
Error, HttpMessage, HttpRequest, HttpResponse, Responder, Error, HttpMessage, HttpRequest, HttpResponse, Responder,
}; };
@@ -114,8 +113,6 @@ pub(crate) use std::fs::File;
#[cfg(feature = "experimental-io-uring")] #[cfg(feature = "experimental-io-uring")]
pub(crate) use tokio_uring::fs::File; pub(crate) use tokio_uring::fs::File;
use super::chunked;
impl NamedFile { impl NamedFile {
/// Creates an instance from a previously opened file. /// Creates an instance from a previously opened file.
/// ///
@@ -397,7 +394,7 @@ impl NamedFile {
} }
/// Creates an `HttpResponse` with file as a streaming body. /// Creates an `HttpResponse` with file as a streaming body.
pub fn into_response(self, req: &HttpRequest) -> HttpResponse<BoxBody> { pub fn into_response(self, req: &HttpRequest) -> HttpResponse {
if self.status_code != StatusCode::OK { if self.status_code != StatusCode::OK {
let mut res = HttpResponse::build(self.status_code); let mut res = HttpResponse::build(self.status_code);
@@ -419,7 +416,7 @@ impl NamedFile {
res.encoding(current_encoding); res.encoding(current_encoding);
} }
let reader = chunked::new_chunked_read(self.md.len(), 0, self.file); let reader = super::chunked::new_chunked_read(self.md.len(), 0, self.file);
return res.streaming(reader); return res.streaming(reader);
} }
@@ -530,13 +527,10 @@ impl NamedFile {
if precondition_failed { if precondition_failed {
return resp.status(StatusCode::PRECONDITION_FAILED).finish(); return resp.status(StatusCode::PRECONDITION_FAILED).finish();
} else if not_modified { } else if not_modified {
return resp return resp.status(StatusCode::NOT_MODIFIED).body(AnyBody::None);
.status(StatusCode::NOT_MODIFIED)
.body(body::None::new())
.map_into_boxed_body();
} }
let reader = chunked::new_chunked_read(length, offset, self.file); let reader = super::chunked::new_chunked_read(length, offset, self.file);
if offset != 0 || length != self.md.len() { if offset != 0 || length != self.md.len() {
resp.status(StatusCode::PARTIAL_CONTENT); resp.status(StatusCode::PARTIAL_CONTENT);
@@ -601,9 +595,7 @@ impl DerefMut for NamedFile {
} }
impl Responder for NamedFile { impl Responder for NamedFile {
type Body = BoxBody; fn respond_to(self, req: &HttpRequest) -> HttpResponse {
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
self.into_response(req) self.into_response(req)
} }
} }

View File

@@ -1,9 +1,9 @@
use std::{ use std::{
future::{ready, Ready},
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
}; };
use actix_utils::future::{ready, Ready};
use actix_web::{dev::Payload, FromRequest, HttpRequest}; use actix_web::{dev::Payload, FromRequest, HttpRequest};
use crate::error::UriSegmentError; use crate::error::UriSegmentError;

View File

@@ -3,12 +3,6 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.8 - 2021-11-30
* Update `actix-tls` to `3.0.0-rc.1`. [#2474]
[#2474]: https://github.com/actix/actix-web/pull/2474
## 3.0.0-beta.7 - 2021-11-22 ## 3.0.0-beta.7 - 2021-11-22
* Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] * Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-http-test" name = "actix-http-test"
version = "3.0.0-beta.8" version = "3.0.0-beta.7"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Various helpers for Actix applications to use during testing" description = "Various helpers for Actix applications to use during testing"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
@@ -31,10 +31,10 @@ openssl = ["tls-openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-service = "2.0.0" actix-service = "2.0.0"
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-tls = "3.0.0-rc.1" actix-tls = "3.0.0-beta.9"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-rt = "2.2" actix-rt = "2.2"
actix-server = "2.0.0-rc.1" actix-server = "2.0.0-beta.9"
awc = { version = "3.0.0-beta.11", default-features = false } awc = { version = "3.0.0-beta.11", default-features = false }
base64 = "0.13" base64 = "0.13"
@@ -52,4 +52,4 @@ tokio = { version = "1.2", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-beta.14" actix-http = "3.0.0-beta.13"

View File

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

View File

@@ -13,8 +13,7 @@ use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::{net::TcpStream, System}; use actix_rt::{net::TcpStream, System};
use actix_server::{Server, ServiceFactory}; use actix_server::{Server, ServiceFactory};
use awc::{ use awc::{
error::PayloadError, http::header::HeaderMap, ws, Client, ClientRequest, ClientResponse, error::PayloadError, http::HeaderMap, ws, Client, ClientRequest, ClientResponse, Connector,
Connector,
}; };
use bytes::Bytes; use bytes::Bytes;
use futures_core::stream::Stream; use futures_core::stream::Stream;

View File

@@ -1,59 +1,11 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
### Added
* Add timeout for canceling HTTP/2 server side connection handshake. Default to 5 seconds. [#2483]
* HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483]
* `Response::map_into_boxed_body`. [#2468]
* `body::EitherBody` enum. [#2468]
* `body::None` struct. [#2468]
* Impl `MessageBody` for `bytestring::ByteString`. [#2468]
* `impl Clone for ws::HandshakeError`. [#2468]
* `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920]
* `impl Default ` for `ws::Codec`. [#1920]
* `header::QualityItem::{max, min}`. [#2486]
* `header::Quality::{MAX, MIN}`. [#2486]
* `impl Display` for `header::Quality`. [#2486]
* `CloneableExtensions` object for use in `on_connect` handlers. [#2327]
### Changed
* Rename `body::BoxBody::{from_body => new}`. [#2468]
* Body type for `Responses` returned from `Response::{new, ok, etc...}` is now `BoxBody`. [#2468]
* The `Error` associated type on `MessageBody` type now requires `impl Error` (or similar). [#2468]
* Error types using in service builders now require `Into<Response<BoxBody>>`. [#2468]
* `From` implementations on error types now return a `Response<BoxBody>`. [#2468]
* `ResponseBuilder::body(B)` now returns `Response<EitherBody<B>>`. [#2468]
* `ResponseBuilder::finish()` now returns `Response<EitherBody<()>>`. [#2468]
* `on_connect_ext` methods now receive a `CloneableExtensions` object. [#2327]
### Removed
* `ResponseBuilder::streaming`. [#2468]
* `impl Future` for `ResponseBuilder`. [#2468]
* Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468]
* Move `body::AnyBody` to `awc`. Replaced with `EitherBody` and `BoxBody`. [#2468]
* `impl Copy` for `ws::Codec`. [#1920]
* `header::qitem` helper. Replaced with `header::QualityItem::max`. [#2486]
* `impl TryFrom<u16>` for `header::Quality`. [#2486]
* `http` module. Most everything it contained is exported at the crate root. [#2488]
[#2327]: https://github.com/actix/actix-web/pull/2327
[#2483]: https://github.com/actix/actix-web/pull/2483
[#2468]: https://github.com/actix/actix-web/pull/2468
[#1920]: https://github.com/actix/actix-web/pull/1920
[#2486]: https://github.com/actix/actix-web/pull/2486
[#2488]: https://github.com/actix/actix-web/pull/2488
## 3.0.0-beta.14 - 2021-11-30
### Changed ### Changed
* Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467] * Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467]
* Expose `header::map` module. [#2467] * Expose `header::{GetAll, Removed}` iterators. [#2467]
* Implement `ExactSizeIterator` and `FusedIterator` for all `HeaderMap` iterators. [#2470]
* Update `actix-tls` to `3.0.0-rc.1`. [#2474]
[#2467]: https://github.com/actix/actix-web/pull/2467 [#2467]: https://github.com/actix/actix-web/pull/2467
[#2470]: https://github.com/actix/actix-web/pull/2470
[#2474]: https://github.com/actix/actix-web/pull/2474
## 3.0.0-beta.13 - 2021-11-22 ## 3.0.0-beta.13 - 2021-11-22

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.0.0-beta.14" version = "3.0.0-beta.13"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "HTTP primitives for the Actix ecosystem" description = "HTTP primitives for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"] keywords = ["actix", "http", "framework", "async", "futures"]
@@ -73,7 +73,7 @@ sha-1 = "0.9"
smallvec = "1.6.1" smallvec = "1.6.1"
# tls # tls
actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true } actix-tls = { version = "3.0.0-beta.9", default-features = false, optional = true }
# compression # compression
brotli2 = { version="0.3.2", optional = true } brotli2 = { version="0.3.2", optional = true }
@@ -81,9 +81,9 @@ flate2 = { version = "1.0.13", optional = true }
zstd = { version = "0.9", optional = true } zstd = { version = "0.9", optional = true }
[dev-dependencies] [dev-dependencies]
actix-server = "2.0.0-rc.1" actix-server = "2.0.0-beta.9"
actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] }
actix-tls = { version = "3.0.0-rc.1", features = ["openssl"] } actix-tls = { version = "3.0.0-beta.9", features = ["openssl"] }
async-stream = "0.3" async-stream = "0.3"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.9" env_logger = "0.9"
@@ -112,7 +112,3 @@ harness = false
[[bench]] [[bench]]
name = "uninit-headers" name = "uninit-headers"
harness = false harness = false
[[bench]]
name = "quality-value"
harness = false

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.14)](https://docs.rs/actix-http/3.0.0-beta.14) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.13)](https://docs.rs/actix-http/3.0.0-beta.13)
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.14/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.14) [![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.13/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.13)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@@ -1,90 +0,0 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
fn bench_quality_display_impls(c: &mut Criterion) {
let mut group = c.benchmark_group("quality value display impls");
for i in CODES.iter() {
group.bench_with_input(BenchmarkId::new("New (fast?)", i), i, |b, &i| {
b.iter(|| _new::Quality(i).to_string())
});
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
b.iter(|| _naive::Quality(i).to_string())
});
}
group.finish();
}
criterion_group!(benches, bench_quality_display_impls);
criterion_main!(benches);
mod _new {
use std::fmt;
pub struct Quality(pub(crate) u16);
impl fmt::Display for Quality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
0 => f.write_str("0"),
1000 => f.write_str("1"),
// some number in the range 1999
x => {
f.write_str("0.")?;
// this implementation avoids string allocation otherwise required
// for `.trim_end_matches('0')`
if x < 10 {
f.write_str("00")?;
// 0 is handled so it's not possible to have a trailing 0, we can just return
itoa::fmt(f, x)
} else if x < 100 {
f.write_str("0")?;
if x % 10 == 0 {
// trailing 0, divide by 10 and write
itoa::fmt(f, x / 10)
} else {
itoa::fmt(f, x)
}
} else {
// x is in range 101999
if x % 100 == 0 {
// two trailing 0s, divide by 100 and write
itoa::fmt(f, x / 100)
} else if x % 10 == 0 {
// one trailing 0, divide by 10 and write
itoa::fmt(f, x / 10)
} else {
itoa::fmt(f, x)
}
}
}
}
}
}
}
mod _naive {
use std::fmt;
pub struct Quality(pub(crate) u16);
impl fmt::Display for Quality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
0 => f.write_str("0"),
1000 => f.write_str("1"),
x => {
write!(f, "{}", format!("{:03}", x).trim_end_matches('0'))
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
use std::io; use std::io;
use actix_http::{Error, HttpService, Request, Response, StatusCode}; 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 _;

View File

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

View File

@@ -1,6 +1,6 @@
use std::{convert::Infallible, io}; use std::{convert::Infallible, io};
use actix_http::{HttpService, Response, StatusCode}; use actix_http::{http::StatusCode, HttpService, Response};
use actix_server::Server; use actix_server::Server;
use http::header::HeaderValue; use http::header::HeaderValue;

333
actix-http/src/body/body.rs Normal file
View File

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

View File

@@ -20,8 +20,6 @@ pin_project! {
} }
} }
// TODO: from_infallible method
impl<S, E> BodyStream<S> impl<S, E> BodyStream<S>
where where
S: Stream<Item = Result<Bytes, E>>, S: Stream<Item = Result<Bytes, E>>,
@@ -77,7 +75,6 @@ mod tests {
use derive_more::{Display, Error}; use derive_more::{Display, Error};
use futures_core::ready; use futures_core::ready;
use futures_util::{stream, FutureExt as _}; use futures_util::{stream, FutureExt as _};
use pin_project_lite::pin_project;
use static_assertions::{assert_impl_all, assert_not_impl_all}; use static_assertions::{assert_impl_all, assert_not_impl_all};
use super::*; use super::*;
@@ -169,15 +166,13 @@ mod tests {
BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)])); BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)]));
assert!(matches!(to_bytes(body).await, Err(StreamErr))); assert!(matches!(to_bytes(body).await, Err(StreamErr)));
pin_project! { #[pin_project::pin_project(project = TimeDelayStreamProj)]
#[derive(Debug)] #[derive(Debug)]
#[project = TimeDelayStreamProj]
enum TimeDelayStream { enum TimeDelayStream {
Start, Start,
Sleep { delay: Pin<Box<Sleep>> }, Sleep(Pin<Box<Sleep>>),
Done, Done,
} }
}
impl Stream for TimeDelayStream { impl Stream for TimeDelayStream {
type Item = Result<Bytes, StreamErr>; type Item = Result<Bytes, StreamErr>;
@@ -189,14 +184,12 @@ mod tests {
match self.as_mut().get_mut() { match self.as_mut().get_mut() {
TimeDelayStream::Start => { TimeDelayStream::Start => {
let sleep = sleep(Duration::from_millis(1)); let sleep = sleep(Duration::from_millis(1));
self.as_mut().set(TimeDelayStream::Sleep { self.as_mut().set(TimeDelayStream::Sleep(Box::pin(sleep)));
delay: Box::pin(sleep),
});
cx.waker().wake_by_ref(); cx.waker().wake_by_ref();
Poll::Pending Poll::Pending
} }
TimeDelayStream::Sleep { ref mut delay } => { TimeDelayStream::Sleep(ref mut delay) => {
ready!(delay.poll_unpin(cx)); ready!(delay.poll_unpin(cx));
self.set(TimeDelayStream::Done); self.set(TimeDelayStream::Done);
cx.waker().wake_by_ref(); cx.waker().wake_by_ref();

View File

@@ -1,80 +0,0 @@
use std::{
error::Error as StdError,
fmt,
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use super::{BodySize, MessageBody, MessageBodyMapErr};
use crate::Error;
/// A boxed message body with boxed errors.
pub struct BoxBody(Pin<Box<dyn MessageBody<Error = Box<dyn StdError>>>>);
impl BoxBody {
/// Boxes a `MessageBody` and any errors it generates.
pub fn new<B>(body: B) -> Self
where
B: MessageBody + 'static,
{
let body = MessageBodyMapErr::new(body, Into::into);
Self(Box::pin(body))
}
/// Returns a mutable pinned reference to the inner message body type.
pub fn as_pin_mut(
&mut self,
) -> Pin<&mut (dyn MessageBody<Error = Box<dyn StdError>>)> {
self.0.as_mut()
}
}
impl fmt::Debug for BoxBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("BoxBody(dyn MessageBody)")
}
}
impl MessageBody for BoxBody {
type Error = 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>>> {
self.0
.as_mut()
.poll_next(cx)
.map_err(|err| Error::new_body().with_cause(err))
}
}
#[cfg(test)]
mod tests {
use static_assertions::{assert_impl_all, assert_not_impl_all};
use super::*;
use crate::body::to_bytes;
assert_impl_all!(BoxBody: MessageBody, fmt::Debug, Unpin);
assert_not_impl_all!(BoxBody: Send, Sync, Unpin);
#[actix_rt::test]
async fn nested_boxed_body() {
let body = Bytes::from_static(&[1, 2, 3]);
let boxed_body = BoxBody::new(BoxBody::new(body));
assert_eq!(
to_bytes(boxed_body).await.unwrap(),
Bytes::from(vec![1, 2, 3]),
);
}
}

View File

@@ -1,83 +0,0 @@
use std::{
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use pin_project_lite::pin_project;
use super::{BodySize, BoxBody, MessageBody};
use crate::Error;
pin_project! {
#[project = EitherBodyProj]
#[derive(Debug, Clone)]
pub enum EitherBody<L, R = BoxBody> {
/// A body of type `L`.
Left { #[pin] body: L },
/// A body of type `R`.
Right { #[pin] body: R },
}
}
impl<L> EitherBody<L, BoxBody> {
/// Creates new `EitherBody` using left variant and boxed right variant.
pub fn new(body: L) -> Self {
Self::Left { body }
}
}
impl<L, R> EitherBody<L, R> {
/// Creates new `EitherBody` using left variant.
pub fn left(body: L) -> Self {
Self::Left { body }
}
/// Creates new `EitherBody` using right variant.
pub fn right(body: R) -> Self {
Self::Right { body }
}
}
impl<L, R> MessageBody for EitherBody<L, R>
where
L: MessageBody + 'static,
R: MessageBody + 'static,
{
type Error = Error;
fn size(&self) -> BodySize {
match self {
EitherBody::Left { body } => body.size(),
EitherBody::Right { body } => body.size(),
}
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
match self.project() {
EitherBodyProj::Left { body } => body
.poll_next(cx)
.map_err(|err| Error::new_body().with_cause(err)),
EitherBodyProj::Right { body } => body
.poll_next(cx)
.map_err(|err| Error::new_body().with_cause(err)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn type_parameter_inference() {
let _body: EitherBody<(), _> = EitherBody::new(());
let _body: EitherBody<_, ()> = EitherBody::left(());
let _body: EitherBody<(), _> = EitherBody::right(());
}
}

View File

@@ -2,7 +2,6 @@
use std::{ use std::{
convert::Infallible, convert::Infallible,
error::Error as StdError,
mem, mem,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
@@ -14,12 +13,9 @@ use pin_project_lite::pin_project;
use super::BodySize; use super::BodySize;
/// An interface types that can converted to bytes and used as response bodies. /// An interface for response bodies.
// TODO: examples
pub trait MessageBody { pub trait MessageBody {
// TODO: consider this bound to only fmt::Display since the error type is not really used type Error;
// and there is an impl for Into<Box<StdError>> on String
type Error: Into<Box<dyn StdError>>;
/// Body size hint. /// Body size hint.
fn size(&self) -> BodySize; fn size(&self) -> BodySize;
@@ -31,38 +27,16 @@ pub trait MessageBody {
) -> Poll<Option<Result<Bytes, Self::Error>>>; ) -> Poll<Option<Result<Bytes, Self::Error>>>;
} }
mod foreign_impls {
use super::*;
impl MessageBody for Infallible {
type Error = Infallible;
#[inline]
fn size(&self) -> BodySize {
match *self {}
}
#[inline]
fn poll_next(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
match *self {}
}
}
impl MessageBody for () { impl MessageBody for () {
type Error = Infallible; type Error = Infallible;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(0) BodySize::Sized(0)
} }
#[inline]
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_cx: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
Poll::Ready(None) Poll::Ready(None)
} }
@@ -74,12 +48,10 @@ mod foreign_impls {
{ {
type Error = B::Error; type Error = B::Error;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
self.as_ref().size() self.as_ref().size()
} }
#[inline]
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
@@ -94,12 +66,10 @@ mod foreign_impls {
{ {
type Error = B::Error; type Error = B::Error;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
self.as_ref().size() self.as_ref().size()
} }
#[inline]
fn poll_next( fn poll_next(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
@@ -108,27 +78,6 @@ mod foreign_impls {
} }
} }
impl MessageBody for &'static [u8] {
type Error = Infallible;
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
let bytes = mem::take(self.get_mut());
let bytes = Bytes::from_static(bytes);
Poll::Ready(Some(Ok(bytes)))
}
}
}
impl MessageBody for Bytes { impl MessageBody for Bytes {
type Error = Infallible; type Error = Infallible;
@@ -138,13 +87,12 @@ mod foreign_impls {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_cx: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
let bytes = mem::take(self.get_mut()); Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
Poll::Ready(Some(Ok(bytes)))
} }
} }
} }
@@ -158,33 +106,12 @@ mod foreign_impls {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_cx: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
let bytes = mem::take(self.get_mut()).freeze(); Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
Poll::Ready(Some(Ok(bytes)))
}
}
}
impl MessageBody for Vec<u8> {
type Error = Infallible;
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
let bytes = mem::take(self.get_mut());
Poll::Ready(Some(Ok(Bytes::from(bytes))))
} }
} }
} }
@@ -198,14 +125,33 @@ mod foreign_impls {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_cx: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
let string = mem::take(self.get_mut()); Poll::Ready(Some(Ok(Bytes::from_static(
let bytes = Bytes::from_static(string.as_bytes()); mem::take(self.get_mut()).as_ref(),
Poll::Ready(Some(Ok(bytes))) ))))
}
}
}
impl MessageBody for Vec<u8> {
type Error = Infallible;
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from(mem::take(self.get_mut())))))
} }
} }
} }
@@ -219,30 +165,14 @@ mod foreign_impls {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_cx: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
let string = mem::take(self.get_mut()); Poll::Ready(Some(Ok(Bytes::from(
Poll::Ready(Some(Ok(Bytes::from(string)))) mem::take(self.get_mut()).into_bytes(),
} ))))
}
}
impl MessageBody for bytestring::ByteString {
type Error = Infallible;
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
let string = mem::take(self.get_mut());
Poll::Ready(Some(Ok(string.into_bytes())))
} }
} }
} }
@@ -272,7 +202,6 @@ impl<B, F, E> MessageBody for MessageBodyMapErr<B, F>
where where
B: MessageBody, B: MessageBody,
F: FnOnce(B::Error) -> E, F: FnOnce(B::Error) -> E,
E: Into<Box<dyn StdError>>,
{ {
type Error = E; type Error = E;
@@ -297,129 +226,3 @@ where
} }
} }
} }
#[cfg(test)]
mod tests {
use actix_rt::pin;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut};
use super::*;
macro_rules! assert_poll_next {
($pin:expr, $exp:expr) => {
assert_eq!(
poll_fn(|cx| $pin.as_mut().poll_next(cx))
.await
.unwrap() // unwrap option
.unwrap(), // unwrap result
$exp
);
};
}
macro_rules! assert_poll_next_none {
($pin:expr) => {
assert!(poll_fn(|cx| $pin.as_mut().poll_next(cx)).await.is_none());
};
}
#[actix_rt::test]
async fn boxing_equivalence() {
assert_eq!(().size(), BodySize::Sized(0));
assert_eq!(().size(), Box::new(()).size());
assert_eq!(().size(), Box::pin(()).size());
let pl = Box::new(());
pin!(pl);
assert_poll_next_none!(pl);
let mut pl = Box::pin(());
assert_poll_next_none!(pl);
}
#[actix_rt::test]
async fn test_unit() {
let pl = ();
assert_eq!(pl.size(), BodySize::Sized(0));
pin!(pl);
assert_poll_next_none!(pl);
}
#[actix_rt::test]
async fn test_static_str() {
assert_eq!("".size(), BodySize::Sized(0));
assert_eq!("test".size(), BodySize::Sized(4));
let pl = "test";
pin!(pl);
assert_poll_next!(pl, Bytes::from("test"));
}
#[actix_rt::test]
async fn test_static_bytes() {
assert_eq!(b"".as_ref().size(), BodySize::Sized(0));
assert_eq!(b"test".as_ref().size(), BodySize::Sized(4));
let pl = b"test".as_ref();
pin!(pl);
assert_poll_next!(pl, Bytes::from("test"));
}
#[actix_rt::test]
async fn test_vec() {
assert_eq!(vec![0; 0].size(), BodySize::Sized(0));
assert_eq!(Vec::from("test").size(), BodySize::Sized(4));
let pl = Vec::from("test");
pin!(pl);
assert_poll_next!(pl, Bytes::from("test"));
}
#[actix_rt::test]
async fn test_bytes() {
assert_eq!(Bytes::new().size(), BodySize::Sized(0));
assert_eq!(Bytes::from_static(b"test").size(), BodySize::Sized(4));
let pl = Bytes::from_static(b"test");
pin!(pl);
assert_poll_next!(pl, Bytes::from("test"));
}
#[actix_rt::test]
async fn test_bytes_mut() {
assert_eq!(BytesMut::new().size(), BodySize::Sized(0));
assert_eq!(BytesMut::from(b"test".as_ref()).size(), BodySize::Sized(4));
let pl = BytesMut::from("test");
pin!(pl);
assert_poll_next!(pl, Bytes::from("test"));
}
#[actix_rt::test]
async fn test_string() {
assert_eq!(String::new().size(), BodySize::Sized(0));
assert_eq!("test".to_owned().size(), BodySize::Sized(4));
let pl = "test".to_owned();
pin!(pl);
assert_poll_next!(pl, Bytes::from("test"));
}
// down-casting used to be done with a method on MessageBody trait
// test is kept to demonstrate equivalence of Any trait
#[actix_rt::test]
async fn test_body_casting() {
let mut body = String::from("hello cast");
// let mut resp_body: &mut dyn MessageBody<Error = Error> = &mut body;
let resp_body: &mut dyn std::any::Any = &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

@@ -1,20 +1,272 @@
//! Traits and structures to aid consuming and writing HTTP payloads. //! Traits and structures to aid consuming and writing HTTP payloads.
use std::task::Poll;
use actix_rt::pin;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut};
use futures_core::ready;
#[allow(clippy::module_inception)]
mod body;
mod body_stream; mod body_stream;
mod boxed;
mod either;
mod message_body; mod message_body;
mod none;
mod size; mod size;
mod sized_stream; mod sized_stream;
mod utils;
#[allow(deprecated)]
pub use self::body::{AnyBody, Body, BoxBody};
pub use self::body_stream::BodyStream; pub use self::body_stream::BodyStream;
pub use self::boxed::BoxBody;
pub use self::either::EitherBody;
pub use self::message_body::MessageBody; pub use self::message_body::MessageBody;
pub(crate) use self::message_body::MessageBodyMapErr; pub(crate) use self::message_body::MessageBodyMapErr;
pub use self::none::None;
pub use self::size::BodySize; pub use self::size::BodySize;
pub use self::sized_stream::SizedStream; pub use self::sized_stream::SizedStream;
pub use self::utils::to_bytes;
/// Collects the body produced by a `MessageBody` implementation into `Bytes`.
///
/// Any errors produced by the body stream are returned immediately.
///
/// # Examples
/// ```
/// use actix_http::body::{AnyBody, to_bytes};
/// use bytes::Bytes;
///
/// # async fn test_to_bytes() {
/// let body = AnyBody::none();
/// let bytes = to_bytes(body).await.unwrap();
/// assert!(bytes.is_empty());
///
/// let body = AnyBody::copy_from_slice(b"123");
/// let bytes = to_bytes(body).await.unwrap();
/// assert_eq!(bytes, b"123"[..]);
/// # }
/// ```
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
let cap = match body.size() {
BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()),
BodySize::Sized(size) => size as usize,
// good enough first guess for chunk size
BodySize::Stream => 32_768,
};
let mut buf = BytesMut::with_capacity(cap);
pin!(body);
poll_fn(|cx| loop {
let body = body.as_mut();
match ready!(body.poll_next(cx)) {
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
None => return Poll::Ready(Ok(())),
Some(Err(err)) => return Poll::Ready(Err(err)),
}
})
.await?;
Ok(buf.freeze())
}
#[cfg(test)]
mod tests {
use std::pin::Pin;
use actix_rt::pin;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut};
use super::{to_bytes, AnyBody as TestAnyBody, BodySize, MessageBody as _};
impl AnyBody {
pub(crate) fn get_ref(&self) -> &[u8] {
match *self {
AnyBody::Bytes(ref bin) => bin,
_ => panic!(),
}
}
}
/// AnyBody alias because rustc does not (can not?) infer the default type parameter.
type AnyBody = TestAnyBody;
#[actix_rt::test]
async fn test_static_str() {
assert_eq!(AnyBody::from("").size(), BodySize::Sized(0));
assert_eq!(AnyBody::from("test").size(), BodySize::Sized(4));
assert_eq!(AnyBody::from("test").get_ref(), b"test");
assert_eq!("test".size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| Pin::new(&mut "test").poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_static_bytes() {
assert_eq!(AnyBody::from(b"test".as_ref()).size(), BodySize::Sized(4));
assert_eq!(AnyBody::from(b"test".as_ref()).get_ref(), b"test");
assert_eq!(
AnyBody::copy_from_slice(b"test".as_ref()).size(),
BodySize::Sized(4)
);
assert_eq!(
AnyBody::copy_from_slice(b"test".as_ref()).get_ref(),
b"test"
);
let sb = Bytes::from(&b"test"[..]);
pin!(sb);
assert_eq!(sb.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| sb.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_vec() {
assert_eq!(AnyBody::from(Vec::from("test")).size(), BodySize::Sized(4));
assert_eq!(AnyBody::from(Vec::from("test")).get_ref(), b"test");
let test_vec = Vec::from("test");
pin!(test_vec);
assert_eq!(test_vec.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| test_vec.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_bytes() {
let b = Bytes::from("test");
assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test");
pin!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_bytes_mut() {
let b = BytesMut::from("test");
assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test");
pin!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_string() {
let b = "test".to_owned();
assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test");
assert_eq!(AnyBody::from(&b).size(), BodySize::Sized(4));
assert_eq!(AnyBody::from(&b).get_ref(), b"test");
pin!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_unit() {
assert_eq!(().size(), BodySize::Sized(0));
assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx))
.await
.is_none());
}
#[actix_rt::test]
async fn test_box_and_pin() {
let val = Box::new(());
pin!(val);
assert_eq!(val.size(), BodySize::Sized(0));
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
let mut val = Box::pin(());
assert_eq!(val.size(), BodySize::Sized(0));
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
}
#[actix_rt::test]
async fn test_body_eq() {
assert!(
AnyBody::Bytes(Bytes::from_static(b"1"))
== AnyBody::Bytes(Bytes::from_static(b"1"))
);
assert!(AnyBody::Bytes(Bytes::from_static(b"1")) != AnyBody::None);
}
#[actix_rt::test]
async fn test_body_debug() {
assert!(format!("{:?}", AnyBody::None).contains("Body::None"));
assert!(format!("{:?}", AnyBody::from(Bytes::from_static(b"1"))).contains('1'));
}
#[actix_rt::test]
async fn test_serde_json() {
use serde_json::{json, Value};
assert_eq!(
AnyBody::from(
serde_json::to_vec(&Value::String("test".to_owned())).unwrap()
)
.size(),
BodySize::Sized(6)
);
assert_eq!(
AnyBody::from(
serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap()
)
.size(),
BodySize::Sized(25)
);
}
// down-casting used to be done with a method on MessageBody trait
// test is kept to demonstrate equivalence of Any trait
#[actix_rt::test]
async fn test_body_casting() {
let mut body = String::from("hello cast");
// let mut resp_body: &mut dyn MessageBody<Error = Error> = &mut body;
let resp_body: &mut dyn std::any::Any = &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());
}
#[actix_rt::test]
async fn test_to_bytes() {
let body = AnyBody::empty();
let bytes = to_bytes(body).await.unwrap();
assert!(bytes.is_empty());
let body = AnyBody::copy_from_slice(b"123");
let bytes = to_bytes(body).await.unwrap();
assert_eq!(bytes, b"123"[..]);
}
}

View File

@@ -1,43 +0,0 @@
use std::{
convert::Infallible,
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use super::{BodySize, MessageBody};
/// Body type for responses that forbid payloads.
///
/// Distinct from an empty response which would contain a Content-Length header.
///
/// For an "empty" body, use `()` or `Bytes::new()`.
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct None;
impl None {
/// Constructs new "none" body.
#[inline]
pub fn new() -> Self {
None
}
}
impl MessageBody for None {
type Error = Infallible;
#[inline]
fn size(&self) -> BodySize {
BodySize::None
}
#[inline]
fn poll_next(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
Poll::Ready(Option::None)
}
}

View File

@@ -18,7 +18,7 @@ pub enum BodySize {
} }
impl BodySize { impl BodySize {
/// Returns true if size hint indicates omitted or empty body. /// Returns true if size hint indicates no or empty body.
/// ///
/// Streams will return false because it cannot be known without reading the stream. /// Streams will return false because it cannot be known without reading the stream.
/// ///

View File

@@ -32,8 +32,6 @@ where
} }
} }
// TODO: from_infallible method
impl<S, E> MessageBody for SizedStream<S> impl<S, E> MessageBody for SizedStream<S>
where where
S: Stream<Item = Result<Bytes, E>>, S: Stream<Item = Result<Bytes, E>>,

View File

@@ -1,78 +0,0 @@
use std::task::Poll;
use actix_rt::pin;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut};
use futures_core::ready;
use super::{BodySize, MessageBody};
/// Collects the body produced by a `MessageBody` implementation into `Bytes`.
///
/// Any errors produced by the body stream are returned immediately.
///
/// # Examples
/// ```
/// use actix_http::body::{self, to_bytes};
/// use bytes::Bytes;
///
/// # async fn test_to_bytes() {
/// let body = body::None::new();
/// let bytes = to_bytes(body).await.unwrap();
/// assert!(bytes.is_empty());
///
/// let body = Bytes::from_static(b"123");
/// let bytes = to_bytes(body).await.unwrap();
/// assert_eq!(bytes, b"123"[..]);
/// # }
/// ```
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
let cap = match body.size() {
BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()),
BodySize::Sized(size) => size as usize,
// good enough first guess for chunk size
BodySize::Stream => 32_768,
};
let mut buf = BytesMut::with_capacity(cap);
pin!(body);
poll_fn(|cx| loop {
let body = body.as_mut();
match ready!(body.poll_next(cx)) {
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
None => return Poll::Ready(Ok(())),
Some(Err(err)) => return Poll::Ready(Err(err)),
}
})
.await?;
Ok(buf.freeze())
}
#[cfg(test)]
mod test {
use futures_util::{stream, StreamExt as _};
use super::*;
use crate::{body::BodyStream, Error};
#[actix_rt::test]
async fn test_to_bytes() {
let bytes = to_bytes(()).await.unwrap();
assert!(bytes.is_empty());
let body = Bytes::from_static(b"123");
let bytes = to_bytes(body).await.unwrap();
assert_eq!(bytes, b"123"[..]);
let stream =
stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")])
.map(Ok::<_, Error>);
let body = BodyStream::new(stream);
let bytes = to_bytes(body).await.unwrap();
assert_eq!(bytes, b"123abc"[..]);
}
}

View File

@@ -1,16 +1,15 @@
use std::{fmt, marker::PhantomData, net, rc::Rc}; use std::{error::Error as StdError, fmt, marker::PhantomData, net, rc::Rc};
use actix_codec::Framed; use actix_codec::Framed;
use actix_service::{IntoServiceFactory, Service, ServiceFactory}; use actix_service::{IntoServiceFactory, Service, ServiceFactory};
use crate::{ use crate::{
body::{BoxBody, MessageBody}, body::{AnyBody, MessageBody},
config::{KeepAlive, ServiceConfig}, config::{KeepAlive, ServiceConfig},
extensions::CloneableExtensions,
h1::{self, ExpectHandler, H1Service, UpgradeHandler}, h1::{self, ExpectHandler, H1Service, UpgradeHandler},
h2::H2Service, h2::H2Service,
service::HttpService, service::HttpService,
ConnectCallback, Request, Response, ConnectCallback, Extensions, Request, Response,
}; };
/// A HTTP service builder /// A HTTP service builder
@@ -32,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<Response<BoxBody>> + '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,
{ {
@@ -55,11 +54,11 @@ 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<Response<BoxBody>> + '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<Response<BoxBody>>, 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::Error: fmt::Display, U::Error: fmt::Display,
@@ -121,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<Response<BoxBody>>, X1::Error: Into<Response<AnyBody>>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
{ {
HttpServiceBuilder { HttpServiceBuilder {
@@ -168,7 +167,7 @@ where
/// and handlers. /// and handlers.
pub fn on_connect_ext<F>(mut self, f: F) -> Self pub fn on_connect_ext<F>(mut self, f: F) -> Self
where where
F: Fn(&T, &mut CloneableExtensions) + 'static, F: Fn(&T, &mut Extensions) + 'static,
{ {
self.on_connect_ext = Some(Rc::new(f)); self.on_connect_ext = Some(Rc::new(f));
self self
@@ -179,7 +178,7 @@ where
where where
B: MessageBody, B: MessageBody,
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S, Request>,
S::Error: Into<Response<BoxBody>>, S::Error: Into<Response<AnyBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
{ {
@@ -201,11 +200,12 @@ where
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
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S, Request>,
S::Error: Into<Response<BoxBody>> + '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: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,
@@ -223,11 +223,12 @@ where
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
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S, Request>,
S::Error: Into<Response<BoxBody>> + '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: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,

View File

@@ -23,7 +23,7 @@ use zstd::stream::write::Decoder as ZstdDecoder;
use crate::{ use crate::{
encoding::Writer, encoding::Writer,
error::{BlockingError, PayloadError}, error::{BlockingError, PayloadError},
header::{ContentEncoding, HeaderMap, CONTENT_ENCODING}, http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING},
}; };
const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049; const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049;

View File

@@ -12,7 +12,7 @@ use actix_rt::task::{spawn_blocking, JoinHandle};
use bytes::Bytes; use bytes::Bytes;
use derive_more::Display; use derive_more::Display;
use futures_core::ready; use futures_core::ready;
use pin_project_lite::pin_project; use pin_project::pin_project;
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
use brotli2::write::BrotliEncoder; use brotli2::write::BrotliEncoder;
@@ -23,100 +23,93 @@ use flate2::write::{GzEncoder, ZlibEncoder};
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
use zstd::stream::write::Encoder as ZstdEncoder; use zstd::stream::write::Encoder as ZstdEncoder;
use super::Writer;
use crate::{ use crate::{
body::{BodySize, MessageBody}, body::{AnyBody, BodySize, MessageBody},
error::BlockingError, http::{
header::{self, ContentEncoding, HeaderValue, CONTENT_ENCODING}, header::{ContentEncoding, CONTENT_ENCODING},
ResponseHead, StatusCode, HeaderValue, StatusCode,
},
ResponseHead,
}; };
use super::Writer;
use crate::error::BlockingError;
const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024; const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024;
pin_project! { #[pin_project]
pub struct Encoder<B> { pub struct Encoder<B> {
eof: bool,
#[pin] #[pin]
body: EncoderBody<B>, body: EncoderBody<B>,
encoder: Option<ContentEncoder>, encoder: Option<ContentEncoder>,
fut: Option<JoinHandle<Result<ContentEncoder, io::Error>>>, fut: Option<JoinHandle<Result<ContentEncoder, io::Error>>>,
eof: bool,
}
} }
impl<B: MessageBody> Encoder<B> { impl<B: MessageBody> Encoder<B> {
fn none() -> Self {
Encoder {
body: EncoderBody::None,
encoder: None,
fut: None,
eof: true,
}
}
pub fn response( pub fn response(
encoding: ContentEncoding, encoding: ContentEncoding,
head: &mut ResponseHead, head: &mut ResponseHead,
body: B, body: AnyBody<B>,
) -> Self { ) -> AnyBody<Encoder<B>> {
let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING) let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|| head.status == StatusCode::SWITCHING_PROTOCOLS || head.status == StatusCode::SWITCHING_PROTOCOLS
|| head.status == StatusCode::NO_CONTENT || head.status == StatusCode::NO_CONTENT
|| encoding == ContentEncoding::Identity || encoding == ContentEncoding::Identity
|| encoding == ContentEncoding::Auto); || encoding == ContentEncoding::Auto);
match body.size() { let body = match body {
// no need to compress an empty body AnyBody::None => return AnyBody::None,
BodySize::None => return Self::none(), AnyBody::Bytes(buf) => {
if can_encode {
// we cannot assume that Sized is not a stream EncoderBody::Bytes(buf)
BodySize::Sized(_) | BodySize::Stream => {} } else {
return AnyBody::Bytes(buf);
} }
}
// TODO potentially some optimisation for single-chunk responses here by trying to read the AnyBody::Body(body) => EncoderBody::Stream(body),
// payload eagerly, stopping after 2 polls if the first is a chunk and the second is None };
if can_encode { if can_encode {
// Modify response body only if encoder is set // Modify response body only if encoder is not None
if let Some(enc) = ContentEncoder::encoder(encoding) { if let Some(enc) = ContentEncoder::encoder(encoding) {
update_head(encoding, head); update_head(encoding, head);
head.no_chunking(false); head.no_chunking(false);
return Encoder { return AnyBody::Body(Encoder {
body: EncoderBody::Stream { body }, body,
eof: false,
fut: None,
encoder: Some(enc), encoder: Some(enc),
fut: None, });
eof: false,
};
} }
} }
Encoder { AnyBody::Body(Encoder {
body: EncoderBody::Stream { body }, body,
eof: false,
fut: None,
encoder: None, encoder: None,
fut: None, })
eof: false,
}
} }
} }
pin_project! { #[pin_project(project = EncoderBodyProj)]
#[project = EncoderBodyProj]
enum EncoderBody<B> { enum EncoderBody<B> {
None, Bytes(Bytes),
Stream { #[pin] body: B }, Stream(#[pin] B),
}
} }
impl<B> MessageBody for EncoderBody<B> impl<B> MessageBody for EncoderBody<B>
where where
B: MessageBody, B: MessageBody,
{ {
type Error = EncoderError; type Error = EncoderError<B::Error>;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
match self { match self {
EncoderBody::None => BodySize::None, EncoderBody::Bytes(ref b) => b.size(),
EncoderBody::Stream { body } => body.size(), EncoderBody::Stream(ref b) => b.size(),
} }
} }
@@ -125,11 +118,14 @@ where
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
match self.project() { match self.project() {
EncoderBodyProj::None => Poll::Ready(None), EncoderBodyProj::Bytes(b) => {
if b.is_empty() {
EncoderBodyProj::Stream { body } => body Poll::Ready(None)
.poll_next(cx) } else {
.map_err(|err| EncoderError::Body(err.into())), Poll::Ready(Some(Ok(std::mem::take(b))))
}
}
EncoderBodyProj::Stream(b) => b.poll_next(cx).map_err(EncoderError::Body),
} }
} }
} }
@@ -138,7 +134,7 @@ impl<B> MessageBody for Encoder<B>
where where
B: MessageBody, B: MessageBody,
{ {
type Error = EncoderError; type Error = EncoderError<B::Error>;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
if self.encoder.is_none() { if self.encoder.is_none() {
@@ -201,7 +197,6 @@ where
None => { None => {
if let Some(encoder) = this.encoder.take() { if let Some(encoder) = this.encoder.take() {
let chunk = encoder.finish().map_err(EncoderError::Io)?; 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 {
@@ -219,7 +214,7 @@ where
fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
head.headers_mut().insert( head.headers_mut().insert(
header::CONTENT_ENCODING, CONTENT_ENCODING,
HeaderValue::from_static(encoding.as_str()), HeaderValue::from_static(encoding.as_str()),
); );
} }
@@ -227,15 +222,12 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
enum ContentEncoder { enum ContentEncoder {
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
Deflate(ZlibEncoder<Writer>), Deflate(ZlibEncoder<Writer>),
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
Gzip(GzEncoder<Writer>), Gzip(GzEncoder<Writer>),
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
Br(BrotliEncoder<Writer>), Br(BrotliEncoder<Writer>),
// We need explicit 'static lifetime here because ZstdEncoder need lifetime
// Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we // argument, and we use `spawn_blocking` in `Encoder::poll_next` that require `FnOnce() -> R + Send + 'static`
// use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`.
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
Zstd(ZstdEncoder<'static, Writer>), Zstd(ZstdEncoder<'static, Writer>),
} }
@@ -248,24 +240,20 @@ impl ContentEncoder {
Writer::new(), Writer::new(),
flate2::Compression::fast(), flate2::Compression::fast(),
))), ))),
#[cfg(feature = "compress-gzip")] #[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")] #[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")] #[cfg(feature = "compress-zstd")]
ContentEncoding::Zstd => { ContentEncoding::Zstd => {
let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?; let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?;
Some(ContentEncoder::Zstd(encoder)) Some(ContentEncoder::Zstd(encoder))
} }
_ => None, _ => None,
} }
} }
@@ -275,13 +263,10 @@ impl ContentEncoder {
match *self { match *self {
#[cfg(feature = "compress-brotli")] #[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")] #[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")] #[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")] #[cfg(feature = "compress-zstd")]
ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(),
} }
@@ -294,19 +279,16 @@ impl ContentEncoder {
Ok(writer) => Ok(writer.buf.freeze()), Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err), Err(err) => Err(err),
}, },
#[cfg(feature = "compress-gzip")] #[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")] #[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")] #[cfg(feature = "compress-zstd")]
ContentEncoder::Zstd(encoder) => match encoder.finish() { ContentEncoder::Zstd(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()), Ok(writer) => Ok(writer.buf.freeze()),
@@ -325,7 +307,6 @@ impl ContentEncoder {
Err(err) Err(err)
} }
}, },
#[cfg(feature = "compress-gzip")] #[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(()),
@@ -334,7 +315,6 @@ impl ContentEncoder {
Err(err) Err(err)
} }
}, },
#[cfg(feature = "compress-gzip")] #[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(()),
@@ -343,7 +323,6 @@ impl ContentEncoder {
Err(err) Err(err)
} }
}, },
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
@@ -358,9 +337,9 @@ impl ContentEncoder {
#[derive(Debug, Display)] #[derive(Debug, Display)]
#[non_exhaustive] #[non_exhaustive]
pub enum EncoderError { pub enum EncoderError<E> {
#[display(fmt = "body")] #[display(fmt = "body")]
Body(Box<dyn StdError>), Body(E),
#[display(fmt = "blocking")] #[display(fmt = "blocking")]
Blocking(BlockingError), Blocking(BlockingError),
@@ -369,18 +348,18 @@ pub enum EncoderError {
Io(io::Error), Io(io::Error),
} }
impl StdError for EncoderError { impl<E: StdError + 'static> StdError for EncoderError<E> {
fn source(&self) -> Option<&(dyn StdError + 'static)> { fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self { match self {
EncoderError::Body(err) => Some(&**err), EncoderError::Body(err) => Some(err),
EncoderError::Blocking(err) => Some(err), EncoderError::Blocking(err) => Some(err),
EncoderError::Io(err) => Some(err), EncoderError::Io(err) => Some(err),
} }
} }
} }
impl From<EncoderError> for crate::Error { impl<E: StdError + 'static> From<EncoderError<E>> for crate::Error {
fn from(err: EncoderError) -> Self { fn from(err: EncoderError<E>) -> Self {
crate::Error::new_encoder().with_cause(err) crate::Error::new_encoder().with_cause(err)
} }
} }

View File

@@ -10,9 +10,6 @@ mod encoder;
pub use self::decoder::Decoder; pub use self::decoder::Decoder;
pub use self::encoder::Encoder; pub use self::encoder::Encoder;
/// Special-purpose writer for streaming (de-)compression.
///
/// Pre-allocates 8KiB of capacity.
pub(self) struct Writer { pub(self) struct Writer {
buf: BytesMut, buf: BytesMut,
} }

View File

@@ -5,7 +5,7 @@ use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Err
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
use http::{uri::InvalidUri, StatusCode}; use http::{uri::InvalidUri, StatusCode};
use crate::{body::BoxBody, ws, Response}; use crate::{body::AnyBody, ws, Response};
pub use http::Error as HttpError; pub use http::Error as HttpError;
@@ -66,15 +66,14 @@ impl Error {
} }
} }
impl From<Error> for Response<BoxBody> { impl<B> From<Error> for Response<AnyBody<B>> {
fn from(err: Error) -> Self { fn from(err: Error) -> Self {
// TODO: more appropriate error status codes, usage assessment needed
let status_code = match err.inner.kind { let status_code = match err.inner.kind {
Kind::Parse => StatusCode::BAD_REQUEST, Kind::Parse => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
}; };
Response::new(status_code).set_body(BoxBody::new(err.to_string())) Response::new(status_code).set_body(AnyBody::from(err.to_string()))
} }
} }
@@ -133,6 +132,12 @@ impl From<std::convert::Infallible> for Error {
} }
} }
impl From<ws::ProtocolError> for Error {
fn from(err: ws::ProtocolError) -> Self {
Self::new_ws().with_cause(err)
}
}
impl From<HttpError> for Error { impl From<HttpError> for Error {
fn from(err: HttpError) -> Self { fn from(err: HttpError) -> Self {
Self::new_http().with_cause(err) Self::new_http().with_cause(err)
@@ -145,12 +150,6 @@ impl From<ws::HandshakeError> for Error {
} }
} }
impl From<ws::ProtocolError> for Error {
fn from(err: ws::ProtocolError) -> Self {
Self::new_ws().with_cause(err)
}
}
/// A set of errors that can occur during parsing HTTP streams. /// A set of errors that can occur during parsing HTTP streams.
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[non_exhaustive] #[non_exhaustive]
@@ -241,7 +240,7 @@ impl From<ParseError> for Error {
} }
} }
impl From<ParseError> for Response<BoxBody> { impl From<ParseError> for Response<AnyBody> {
fn from(err: ParseError) -> Self { fn from(err: ParseError) -> Self {
Error::from(err).into() Error::from(err).into()
} }
@@ -338,7 +337,7 @@ pub enum DispatchError {
/// Service error /// Service error
// FIXME: display and error type // FIXME: display and error type
#[display(fmt = "Service Error")] #[display(fmt = "Service Error")]
Service(#[error(not(source))] Response<BoxBody>), Service(#[error(not(source))] Response<AnyBody>),
/// Body error /// Body error
// FIXME: display and error type // FIXME: display and error type
@@ -422,11 +421,11 @@ mod tests {
#[test] #[test]
fn test_into_response() { fn test_into_response() {
let resp: Response<BoxBody> = ParseError::Incomplete.into(); 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<BoxBody> = Error::new_http().with_cause(err).into(); 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);
} }
@@ -451,7 +450,7 @@ mod tests {
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 err = Error::new_io().with_cause(orig); let err = Error::new_io().with_cause(orig);
let resp: Response<BoxBody> = err.into(); let resp: Response<AnyBody> = err.into();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
} }

View File

@@ -1,6 +1,6 @@
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
fmt, fmt, mem,
}; };
use ahash::AHashMap; use ahash::AHashMap;
@@ -10,7 +10,8 @@ use ahash::AHashMap;
/// All entries into this map must be owned types (or static references). /// All entries into this map must be owned types (or static references).
#[derive(Default)] #[derive(Default)]
pub struct Extensions { pub struct Extensions {
/// Use AHasher with a std HashMap with for faster lookups on the small `TypeId` keys. /// Use FxHasher with a std HashMap with for faster
/// lookups on the small `TypeId` (u64 equivalent) keys.
map: AHashMap<TypeId, Box<dyn Any>>, map: AHashMap<TypeId, Box<dyn Any>>,
} }
@@ -123,11 +124,9 @@ impl Extensions {
self.map.extend(other.map); self.map.extend(other.map);
} }
/// Sets (or overrides) items from cloneable extensions map into this map. /// Sets (or overrides) items from `other` into this map.
pub(crate) fn clone_from(&mut self, other: &CloneableExtensions) { pub(crate) fn drain_from(&mut self, other: &mut Self) {
for (k, val) in &other.map { self.map.extend(mem::take(&mut other.map));
self.map.insert(*k, (**val).clone_to_any());
}
} }
} }
@@ -141,104 +140,6 @@ fn downcast_owned<T: 'static>(boxed: Box<dyn Any>) -> Option<T> {
boxed.downcast().ok().map(|boxed| *boxed) boxed.downcast().ok().map(|boxed| *boxed)
} }
#[doc(hidden)]
pub trait CloneToAny {
/// Cast `self` into an `Any` reference.
#[cfg(test)]
fn any_ref(&self) -> &dyn Any;
/// Clone `self` to a new `Box<Any>` object.
fn clone_to_any(&self) -> Box<dyn Any>;
/// Clone `self` to a new `Box<CloneAny>` object.
fn clone_to_clone_any(&self) -> Box<dyn CloneAny>;
}
impl<T: Clone + Any> CloneToAny for T {
#[cfg(test)]
fn any_ref(&self) -> &dyn Any {
&*self
}
#[inline]
fn clone_to_any(&self) -> Box<dyn Any> {
Box::new(self.clone())
}
#[inline]
fn clone_to_clone_any(&self) -> Box<dyn CloneAny> {
Box::new(self.clone())
}
}
/// An [`Any`] trait with an additional [`Clone`] requirement.
pub trait CloneAny: CloneToAny + Any {}
impl<T: Any + Clone> CloneAny for T {}
impl Clone for Box<dyn CloneAny> {
#[inline]
fn clone(&self) -> Self {
(**self).clone_to_clone_any()
}
}
trait UncheckedAnyExt {
/// # Safety
/// Caller must ensure type `T` is true type.
#[inline]
unsafe fn downcast_unchecked<T: 'static>(self: Box<Self>) -> Box<T> {
Box::from_raw(Box::into_raw(self) as *mut T)
}
}
impl UncheckedAnyExt for dyn CloneAny {}
/// A type map for `on_connect` extensions.
///
/// All entries into this map must be owned types and implement `Clone` trait.
///
/// Many requests can be processed for each connection but the `on_connect` will only be run once
/// when the connection is opened. Therefore, items added to this special map type need to be cloned
/// into the regular extensions map for each request. Most useful connection information types are
/// cloneable already but you can use reference counted wrappers if not.
#[derive(Default)]
pub struct CloneableExtensions {
/// Use AHasher with a std HashMap with for faster lookups on the small `TypeId` keys.
map: AHashMap<TypeId, Box<dyn CloneAny>>,
}
impl CloneableExtensions {
/// Insert an item into the map.
///
/// If an item of this type was already stored, it will be replaced and returned.
///
/// # Examples
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
/// assert_eq!(map.insert(""), None);
/// assert_eq!(map.insert(1u32), None);
/// assert_eq!(map.insert(2u32), Some(1u32));
/// assert_eq!(*map.get::<u32>().unwrap(), 2u32);
/// ```
pub fn insert<T: CloneAny>(&mut self, val: T) -> Option<T> {
self.map
.insert(TypeId::of::<T>(), Box::new(val))
.map(|boxed| {
// Safety:
// Box is owned and `T` is known to be true type from map.
*unsafe { UncheckedAnyExt::downcast_unchecked::<T>(boxed) }
})
}
#[cfg(test)]
fn get<T: CloneAny>(&self) -> Option<&T> {
self.map
.get(&TypeId::of::<T>())
.and_then(|boxed| boxed.as_ref().any_ref().downcast_ref())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -278,8 +179,6 @@ mod tests {
#[test] #[test]
fn test_integers() { fn test_integers() {
static A: u32 = 8;
let mut map = Extensions::new(); let mut map = Extensions::new();
map.insert::<i8>(8); map.insert::<i8>(8);
@@ -292,7 +191,6 @@ mod tests {
map.insert::<u32>(32); map.insert::<u32>(32);
map.insert::<u64>(64); map.insert::<u64>(64);
map.insert::<u128>(128); map.insert::<u128>(128);
map.insert::<&'static u32>(&A);
assert!(map.get::<i8>().is_some()); assert!(map.get::<i8>().is_some());
assert!(map.get::<i16>().is_some()); assert!(map.get::<i16>().is_some());
assert!(map.get::<i32>().is_some()); assert!(map.get::<i32>().is_some());
@@ -303,7 +201,6 @@ mod tests {
assert!(map.get::<u32>().is_some()); assert!(map.get::<u32>().is_some());
assert!(map.get::<u64>().is_some()); assert!(map.get::<u64>().is_some());
assert!(map.get::<u128>().is_some()); assert!(map.get::<u128>().is_some());
assert!(map.get::<&'static u32>().is_some());
} }
#[test] #[test]
@@ -384,41 +281,25 @@ mod tests {
} }
#[test] #[test]
fn test_clone_from() { fn test_drain_from() {
#[derive(Clone)]
struct NonCopy {
num: u8,
}
let mut ext = Extensions::new(); let mut ext = Extensions::new();
ext.insert(2isize); ext.insert(2isize);
let mut more_ext = Extensions::new();
more_ext.insert(5isize);
more_ext.insert(5usize);
assert_eq!(ext.get::<isize>(), Some(&2isize)); assert_eq!(ext.get::<isize>(), Some(&2isize));
assert_eq!(ext.get::<usize>(), None);
assert_eq!(more_ext.get::<isize>(), Some(&5isize));
assert_eq!(more_ext.get::<usize>(), Some(&5usize));
let mut more_ext = CloneableExtensions::default(); ext.drain_from(&mut more_ext);
more_ext.insert(3isize);
more_ext.insert(3usize);
more_ext.insert(NonCopy { num: 8 });
ext.clone_from(&more_ext); assert_eq!(ext.get::<isize>(), Some(&5isize));
assert_eq!(ext.get::<usize>(), Some(&5usize));
assert_eq!(ext.get::<isize>(), Some(&3isize)); assert_eq!(more_ext.get::<isize>(), None);
assert_eq!(ext.get::<usize>(), Some(&3usize)); assert_eq!(more_ext.get::<usize>(), None);
assert_eq!(more_ext.get::<isize>(), Some(&3isize));
assert_eq!(more_ext.get::<usize>(), Some(&3usize));
assert!(ext.get::<NonCopy>().is_some());
assert!(more_ext.get::<NonCopy>().is_some());
}
#[test]
fn boxes_not_aliased() {
let a: Box<dyn CloneAny> = Box::new(42);
let b = a.clone_to_clone_any();
assert_ne!(Box::into_raw(a) as *const (), Box::into_raw(b) as *const ());
let a: Box<dyn CloneAny> = Box::new(42);
let b = a.clone_to_any();
assert_ne!(Box::into_raw(a) as *const (), Box::into_raw(b) as *const ());
} }
} }

View File

@@ -174,7 +174,7 @@ pub(crate) trait MessageType: Sized {
self.set_expect() self.set_expect()
} }
// https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3 // https://tools.ietf.org/html/rfc7230#section-3.3.3
if chunked { if chunked {
// Chunked encoding // Chunked encoding
Ok(PayloadLength::Payload(PayloadType::Payload( Ok(PayloadLength::Payload(PayloadType::Payload(
@@ -511,7 +511,7 @@ mod tests {
use super::*; use super::*;
use crate::{ use crate::{
error::ParseError, error::ParseError,
header::{HeaderName, SET_COOKIE}, http::header::{HeaderName, SET_COOKIE},
HttpMessage as _, HttpMessage as _,
}; };

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,
@@ -18,7 +19,7 @@ use log::{error, trace};
use pin_project::pin_project; use pin_project::pin_project;
use crate::{ use crate::{
body::{BodySize, BoxBody, MessageBody}, body::{AnyBody, BodySize, MessageBody},
config::ServiceConfig, config::ServiceConfig,
error::{DispatchError, ParseError, PayloadError}, error::{DispatchError, ParseError, PayloadError},
service::HttpFlow, service::HttpFlow,
@@ -50,12 +51,13 @@ 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<Response<BoxBody>>, 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<Response<BoxBody>>, 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,
@@ -71,12 +73,13 @@ 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<Response<BoxBody>>, 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<Response<BoxBody>>, 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,
@@ -89,12 +92,13 @@ 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<Response<BoxBody>>, 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<Response<BoxBody>>, 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,
@@ -133,12 +137,13 @@ where
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] B), SendPayload(#[pin] B),
SendErrorPayload(#[pin] BoxBody), SendErrorPayload(#[pin] AnyBody),
} }
impl<S, B, X> State<S, B, X> impl<S, B, X> State<S, B, X>
@@ -148,6 +153,7 @@ where
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)
@@ -165,13 +171,14 @@ where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<BoxBody>>, 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<Response<BoxBody>>, 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,
@@ -225,13 +232,14 @@ where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<BoxBody>>, 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<Response<BoxBody>>, 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,
@@ -327,7 +335,7 @@ where
fn send_error_response( fn send_error_response(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
message: Response<()>, message: Response<()>,
body: BoxBody, body: AnyBody,
) -> Result<(), DispatchError> { ) -> Result<(), DispatchError> {
let size = self.as_mut().send_response_inner(message, &body)?; let size = self.as_mut().send_response_inner(message, &body)?;
let state = match size { let state = match size {
@@ -372,7 +380,7 @@ where
// send_response would update InnerDispatcher state to SendPayload or // send_response would update InnerDispatcher state to SendPayload or
// None(If response body is empty). // None(If response body is empty).
// continue loop to poll it. // continue loop to poll it.
self.as_mut().send_error_response(res, BoxBody::new(()))?; self.as_mut().send_error_response(res, AnyBody::empty())?;
} }
// return with upgrade request and poll it exclusively. // return with upgrade request and poll it exclusively.
@@ -392,7 +400,7 @@ where
// send service call error as response // send service call error as response
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res: Response<BoxBody> = err.into(); let res: Response<AnyBody> = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.as_mut().send_error_response(res, body)?; self.as_mut().send_error_response(res, body)?;
} }
@@ -489,7 +497,7 @@ where
// send expect error as response // send expect error as response
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res: Response<BoxBody> = err.into(); let res: Response<AnyBody> = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.as_mut().send_error_response(res, body)?; self.as_mut().send_error_response(res, body)?;
} }
@@ -538,7 +546,7 @@ 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<BoxBody> = err.into(); let res: Response<AnyBody> = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
return self.send_error_response(res, body); return self.send_error_response(res, body);
} }
@@ -558,7 +566,7 @@ 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<BoxBody> = err.into(); let res: Response<AnyBody> = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.send_error_response(res, body) self.send_error_response(res, body)
} }
@@ -764,7 +772,7 @@ where
trace!("Slow request timeout"); trace!("Slow request timeout");
let _ = self.as_mut().send_error_response( let _ = self.as_mut().send_error_response(
Response::with_body(StatusCode::REQUEST_TIMEOUT, ()), Response::with_body(StatusCode::REQUEST_TIMEOUT, ()),
BoxBody::new(()), AnyBody::empty(),
); );
this = self.project(); this = self.project();
this.flags.insert(Flags::STARTED | Flags::SHUTDOWN); this.flags.insert(Flags::STARTED | Flags::SHUTDOWN);
@@ -901,13 +909,14 @@ where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<BoxBody>>, 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<Response<BoxBody>>, 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,
@@ -1037,8 +1046,9 @@ mod tests {
use crate::{ use crate::{
error::Error, error::Error,
h1::{ExpectHandler, UpgradeHandler}, h1::{ExpectHandler, UpgradeHandler},
http::Method,
test::{TestBuffer, TestSeqBuffer}, test::{TestBuffer, TestSeqBuffer},
HttpMessage, KeepAlive, Method, HttpMessage, KeepAlive,
}; };
fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> { fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
@@ -1057,19 +1067,17 @@ mod tests {
} }
} }
fn ok_service( fn ok_service() -> impl Service<Request, Response = Response<AnyBody>, Error = Error>
) -> impl Service<Request, Response = Response<impl MessageBody>, 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<impl MessageBody>, 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(Bytes::copy_from_slice(path)), Response::ok().set_body(AnyBody::copy_from_slice(path)),
)) ))
}) })
} }

View File

@@ -71,16 +71,15 @@ pub(crate) trait MessageType: Sized {
| StatusCode::PROCESSING | StatusCode::PROCESSING
| StatusCode::NO_CONTENT => { | StatusCode::NO_CONTENT => {
// skip content-length and transfer-encoding headers // skip content-length and transfer-encoding headers
// see https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.1 // see https://tools.ietf.org/html/rfc7230#section-3.3.1
// and https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2 // and https://tools.ietf.org/html/rfc7230#section-3.3.2
skip_len = true; skip_len = true;
length = BodySize::None length = BodySize::None
} }
StatusCode::NOT_MODIFIED => { StatusCode::NOT_MODIFIED => {
// 304 responses should never have a body but should retain a manually set // 304 responses should never have a body but should retain a manually set
// content-length header // content-length header see https://tools.ietf.org/html/rfc7232#section-4.1
// see https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
skip_len = false; skip_len = false;
length = BodySize::None; length = BodySize::None;
} }
@@ -531,10 +530,8 @@ mod tests {
use http::header::AUTHORIZATION; use http::header::AUTHORIZATION;
use super::*; use super::*;
use crate::{ use crate::http::header::{HeaderValue, CONTENT_TYPE};
header::{HeaderValue, CONTENT_TYPE}, use crate::RequestHead;
RequestHead,
};
#[test] #[test]
fn test_chunked_te() { fn test_chunked_te() {

View File

@@ -1,4 +1,5 @@
use std::{ use std::{
error::Error as StdError,
fmt, fmt,
marker::PhantomData, marker::PhantomData,
net, net,
@@ -15,7 +16,7 @@ use actix_utils::future::ready;
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use crate::{ use crate::{
body::{BoxBody, MessageBody}, body::{AnyBody, MessageBody},
config::ServiceConfig, config::ServiceConfig,
error::DispatchError, error::DispatchError,
service::HttpServiceHandler, service::HttpServiceHandler,
@@ -37,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<Response<BoxBody>>, 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,
@@ -62,20 +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<Response<BoxBody>>, 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<Response<BoxBody>>, 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<Response<BoxBody>>, 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
@@ -101,10 +103,7 @@ mod openssl {
use super::*; use super::*;
use actix_tls::accept::{ use actix_tls::accept::{
openssl::{ openssl::{Acceptor, SslAcceptor, SslError, TlsStream},
reexports::{Error as SslError, SslAcceptor},
Acceptor, TlsStream,
},
TlsError, TlsError,
}; };
@@ -112,15 +111,16 @@ mod openssl {
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<BoxBody>>, 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<Response<BoxBody>>, X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
@@ -129,7 +129,7 @@ mod openssl {
Response = (), Response = (),
>, >,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Response<BoxBody>>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create OpenSSL based service. /// Create OpenSSL based service.
@@ -164,7 +164,7 @@ mod rustls {
use actix_service::ServiceFactoryExt as _; use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{ use actix_tls::accept::{
rustls::{reexports::ServerConfig, Acceptor, TlsStream}, rustls::{Acceptor, ServerConfig, TlsStream},
TlsError, TlsError,
}; };
@@ -174,15 +174,16 @@ mod rustls {
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<BoxBody>>, 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<Response<BoxBody>>, X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
@@ -191,7 +192,7 @@ mod rustls {
Response = (), Response = (),
>, >,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Response<BoxBody>>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create Rustls based service. /// Create Rustls based service.
@@ -222,7 +223,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<Response<BoxBody>>, 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,
@@ -230,7 +231,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<Response<BoxBody>>, X1::Error: Into<Response<AnyBody>>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
{ {
H1Service { H1Service {
@@ -273,20 +274,21 @@ where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<BoxBody>>, 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<Response<BoxBody>>, 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<Response<BoxBody>>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
type Response = (); type Response = ();
@@ -342,16 +344,17 @@ where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<BoxBody>>, 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<Response<BoxBody>>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display + Into<Response<BoxBody>>, U::Error: fmt::Display + Into<Response<AnyBody>>,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;

View File

@@ -1,31 +1,23 @@
use std::{ use std::future::Future;
future::Future, use std::pin::Pin;
pin::Pin, use std::task::{Context, Poll};
task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use pin_project_lite::pin_project;
use crate::{ use crate::body::{BodySize, MessageBody};
body::{BodySize, MessageBody}, use crate::error::Error;
error::Error, use crate::h1::{Codec, Message};
h1::{Codec, Message}, use crate::response::Response;
response::Response,
};
pin_project! {
/// Send HTTP/1 response /// Send HTTP/1 response
#[pin_project::pin_project]
pub struct SendResponse<T, B> { pub struct SendResponse<T, B> {
res: Option<Message<(Response<()>, BodySize)>>, res: Option<Message<(Response<()>, BodySize)>>,
#[pin] #[pin]
body: Option<B>, body: Option<B>,
#[pin] #[pin]
framed: Option<Framed<T, Codec>>, framed: Option<Framed<T, Codec>>,
} }
}
impl<T, B> SendResponse<T, B> impl<T, B> SendResponse<T, B>
where where

View File

@@ -10,7 +10,7 @@ use std::{
}; };
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::time::{sleep, Sleep}; use actix_rt::time::Sleep;
use actix_service::Service; use actix_service::Service;
use actix_utils::future::poll_fn; use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
@@ -24,7 +24,7 @@ use log::{error, trace};
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use crate::{ use crate::{
body::{BodySize, BoxBody, MessageBody}, body::{AnyBody, BodySize, MessageBody},
config::ServiceConfig, config::ServiceConfig,
service::HttpFlow, service::HttpFlow,
OnConnectData, Payload, Request, Response, ResponseHead, OnConnectData, Payload, Request, Response, ResponseHead,
@@ -51,29 +51,22 @@ where
{ {
pub(crate) fn new( pub(crate) fn new(
flow: Rc<HttpFlow<S, X, U>>, flow: Rc<HttpFlow<S, X, U>>,
mut conn: Connection<T, Bytes>, mut connection: Connection<T, Bytes>,
on_connect_data: OnConnectData, on_connect_data: OnConnectData,
config: ServiceConfig, config: ServiceConfig,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
timer: Option<Pin<Box<Sleep>>>,
) -> Self { ) -> Self {
let ping_pong = config.keep_alive().map(|dur| H2PingPong { let ping_pong = config.keep_alive_timer().map(|timer| H2PingPong {
timer: timer timer: Box::pin(timer),
.map(|mut timer| {
// reset timer if it's received from new function.
timer.as_mut().reset(config.now() + dur);
timer
})
.unwrap_or_else(|| Box::pin(sleep(dur))),
on_flight: false, on_flight: false,
ping_pong: conn.ping_pong().unwrap(), ping_pong: connection.ping_pong().unwrap(),
}); });
Self { Self {
flow, flow,
config, config,
peer_addr, peer_addr,
connection: conn, connection,
on_connect_data, on_connect_data,
ping_pong, ping_pong,
_phantom: PhantomData, _phantom: PhantomData,
@@ -92,11 +85,12 @@ where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<BoxBody>>, S::Error: Into<Response<AnyBody>>,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{ {
type Output = Result<(), crate::error::DispatchError>; type Output = Result<(), crate::error::DispatchError>;
@@ -131,7 +125,7 @@ where
let res = match fut.await { let res = match fut.await {
Ok(res) => handle_response(res.into(), tx, config).await, Ok(res) => handle_response(res.into(), tx, config).await,
Err(err) => { Err(err) => {
let res: Response<BoxBody> = err.into(); let res: Response<AnyBody> = err.into();
handle_response(res, tx, config).await handle_response(res, tx, config).await
} }
}; };
@@ -206,6 +200,7 @@ async fn handle_response<B>(
) -> Result<(), DispatchError> ) -> Result<(), DispatchError>
where where
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{ {
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
@@ -309,7 +304,7 @@ fn prepare_response(
for (key, value) in head.headers.iter() { for (key, value) in head.headers.iter() {
match *key { match *key {
// TODO: consider skipping other headers according to: // TODO: consider skipping other headers according to:
// https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2 // https://tools.ietf.org/html/rfc7540#section-8.1.2.2
// omit HTTP/1.x only headers // omit HTTP/1.x only headers
CONNECTION | TRANSFER_ENCODING => continue, CONNECTION | TRANSFER_ENCODING => continue,
CONTENT_LENGTH if skip_len => continue, CONTENT_LENGTH if skip_len => continue,

View File

@@ -1,30 +1,20 @@
//! HTTP/2 protocol. //! HTTP/2 protocol.
use std::{ use std::{
future::Future,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::time::Sleep;
use bytes::Bytes; use bytes::Bytes;
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use h2::{ use h2::RecvStream;
server::{handshake, Connection, Handshake},
RecvStream,
};
mod dispatcher; mod dispatcher;
mod service; mod service;
pub use self::dispatcher::Dispatcher; pub use self::dispatcher::Dispatcher;
pub use self::service::H2Service; pub use self::service::H2Service;
use crate::error::PayloadError;
use crate::{
config::ServiceConfig,
error::{DispatchError, PayloadError},
};
/// HTTP/2 peer stream. /// HTTP/2 peer stream.
pub struct Payload { pub struct Payload {
@@ -60,44 +50,3 @@ impl Stream for Payload {
} }
} }
} }
pub(crate) fn handshake_with_timeout<T>(
io: T,
config: &ServiceConfig,
) -> HandshakeWithTimeout<T>
where
T: AsyncRead + AsyncWrite + Unpin,
{
HandshakeWithTimeout {
handshake: handshake(io),
timer: config.client_timer().map(Box::pin),
}
}
pub(crate) struct HandshakeWithTimeout<T: AsyncRead + AsyncWrite + Unpin> {
handshake: Handshake<T>,
timer: Option<Pin<Box<Sleep>>>,
}
impl<T> Future for HandshakeWithTimeout<T>
where
T: AsyncRead + AsyncWrite + Unpin,
{
type Output = Result<(Connection<T, Bytes>, Option<Pin<Box<Sleep>>>), DispatchError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
match Pin::new(&mut this.handshake).poll(cx)? {
// return the timer on success handshake. It can be re-used for h2 ping-pong.
Poll::Ready(conn) => Poll::Ready(Ok((conn, this.timer.take()))),
Poll::Pending => match this.timer.as_mut() {
Some(timer) => {
ready!(timer.as_mut().poll(cx));
Poll::Ready(Err(DispatchError::SlowRequestTimeout))
}
None => Poll::Pending,
},
}
}
}

View File

@@ -1,4 +1,5 @@
use std::{ use std::{
error::Error as StdError,
future::Future, future::Future,
marker::PhantomData, marker::PhantomData,
net, net,
@@ -14,18 +15,20 @@ use actix_service::{
ServiceFactoryExt as _, ServiceFactoryExt as _,
}; };
use actix_utils::future::ready; use actix_utils::future::ready;
use bytes::Bytes;
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
use h2::server::{handshake as h2_handshake, Handshake as H2Handshake};
use log::error; use log::error;
use crate::{ use crate::{
body::{BoxBody, MessageBody}, body::{AnyBody, MessageBody},
config::ServiceConfig, config::ServiceConfig,
error::DispatchError, error::DispatchError,
service::HttpFlow, service::HttpFlow,
ConnectCallback, OnConnectData, Request, Response, ConnectCallback, OnConnectData, Request, Response,
}; };
use super::{dispatcher::Dispatcher, handshake_with_timeout, HandshakeWithTimeout}; use super::dispatcher::Dispatcher;
/// `ServiceFactory` implementation for HTTP/2 transport /// `ServiceFactory` implementation for HTTP/2 transport
pub struct H2Service<T, S, B> { pub struct H2Service<T, S, B> {
@@ -38,11 +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<Response<BoxBody>> + '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>>(
@@ -68,11 +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<Response<BoxBody>> + '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(
@@ -98,10 +103,7 @@ where
mod openssl { mod openssl {
use actix_service::ServiceFactoryExt as _; use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{ use actix_tls::accept::{
openssl::{ openssl::{Acceptor, SslAcceptor, SslError, TlsStream},
reexports::{Error as SslError, SslAcceptor},
Acceptor, TlsStream,
},
TlsError, TlsError,
}; };
@@ -111,11 +113,12 @@ mod openssl {
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<BoxBody>> + '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(
@@ -148,7 +151,7 @@ mod rustls {
use actix_service::ServiceFactoryExt as _; use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{ use actix_tls::accept::{
rustls::{reexports::ServerConfig, Acceptor, TlsStream}, rustls::{Acceptor, ServerConfig, TlsStream},
TlsError, TlsError,
}; };
@@ -158,11 +161,12 @@ mod rustls {
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<BoxBody>> + '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(
@@ -199,11 +203,12 @@ where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<BoxBody>> + '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;
@@ -238,7 +243,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<Response<BoxBody>> + '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,
@@ -261,10 +266,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<Response<BoxBody>> + '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;
@@ -288,7 +294,7 @@ where
Some(self.cfg.clone()), Some(self.cfg.clone()),
addr, addr,
on_connect_data, on_connect_data,
handshake_with_timeout(io, &self.cfg), h2_handshake(io),
), ),
} }
} }
@@ -305,7 +311,7 @@ where
Option<ServiceConfig>, Option<ServiceConfig>,
Option<net::SocketAddr>, Option<net::SocketAddr>,
OnConnectData, OnConnectData,
HandshakeWithTimeout<T>, H2Handshake<T, Bytes>,
), ),
} }
@@ -313,7 +319,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<Response<BoxBody>> + '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,
@@ -325,10 +331,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<Response<BoxBody>> + '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>;
@@ -342,7 +349,7 @@ where
ref mut on_connect_data, ref mut on_connect_data,
ref mut handshake, ref mut handshake,
) => match ready!(Pin::new(handshake).poll(cx)) { ) => match ready!(Pin::new(handshake).poll(cx)) {
Ok((conn, timer)) => { Ok(conn) => {
let on_connect_data = std::mem::take(on_connect_data); let on_connect_data = std::mem::take(on_connect_data);
self.state = State::Incoming(Dispatcher::new( self.state = State::Incoming(Dispatcher::new(
srv.take().unwrap(), srv.take().unwrap(),
@@ -350,13 +357,12 @@ where
on_connect_data, on_connect_data,
config.take().unwrap(), config.take().unwrap(),
*peer_addr, *peer_addr,
timer,
)); ));
self.poll(cx) self.poll(cx)
} }
Err(err) => { Err(err) => {
trace!("H2 handshake error: {}", err); trace!("H2 handshake error: {}", err);
Poll::Ready(Err(err)) Poll::Ready(Err(err.into()))
} }
}, },
} }

View File

@@ -1,12 +1,11 @@
//! Sealed [`AsHeaderName`] trait and implementations. //! Helper trait for types that can be effectively borrowed as a [HeaderValue].
//!
//! [HeaderValue]: crate::http::HeaderValue
use std::{borrow::Cow, str::FromStr as _}; use std::{borrow::Cow, str::FromStr};
use http::header::{HeaderName, InvalidHeaderName}; use http::header::{HeaderName, InvalidHeaderName};
/// Sealed trait implemented for types that can be effectively borrowed as a [`HeaderValue`].
///
/// [`HeaderValue`]: crate::http::HeaderValue
pub trait AsHeaderName: Sealed {} pub trait AsHeaderName: Sealed {}
pub struct Seal; pub struct Seal;

View File

@@ -1,6 +1,4 @@
//! [`IntoHeaderPair`] trait and implementations. use std::convert::TryFrom;
use std::convert::TryFrom as _;
use http::{ use http::{
header::{HeaderName, InvalidHeaderName, InvalidHeaderValue}, header::{HeaderName, InvalidHeaderName, InvalidHeaderValue},
@@ -9,10 +7,7 @@ use http::{
use super::{Header, IntoHeaderValue}; use super::{Header, IntoHeaderValue};
/// An interface for types that can be converted into a [`HeaderName`]/[`HeaderValue`] pair for /// Transforms structures into header K/V pairs for inserting into `HeaderMap`s.
/// insertion into a [`HeaderMap`].
///
/// [`HeaderMap`]: crate::http::HeaderMap
pub trait IntoHeaderPair: Sized { pub trait IntoHeaderPair: Sized {
type Error: Into<HttpError>; type Error: Into<HttpError>;

View File

@@ -1,12 +1,10 @@
//! [`IntoHeaderValue`] trait and implementations. use std::convert::TryFrom;
use std::convert::TryFrom as _;
use bytes::Bytes; use bytes::Bytes;
use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue}; use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue};
use mime::Mime; use mime::Mime;
/// An interface for types that can be converted into a [`HeaderValue`]. /// A trait for any object that can be Converted to a `HeaderValue`
pub trait IntoHeaderValue: Sized { pub trait IntoHeaderValue: Sized {
/// The type returned in the event of a conversion error. /// The type returned in the event of a conversion error.
type Error: Into<HttpError>; type Error: Into<HttpError>;

View File

@@ -1,6 +1,6 @@
//! A multi-value [`HeaderMap`] and its iterators. //! A multi-value [`HeaderMap`] and its iterators.
use std::{borrow::Cow, collections::hash_map, iter, ops}; use std::{borrow::Cow, collections::hash_map, ops};
use ahash::AHashMap; use ahash::AHashMap;
use http::header::{HeaderName, HeaderValue}; use http::header::{HeaderName, HeaderValue};
@@ -14,7 +14,7 @@ use crate::header::AsHeaderName;
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use actix_http::header::{self, HeaderMap, HeaderValue}; /// use actix_http::http::{header, HeaderMap, HeaderValue};
/// ///
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
@@ -75,7 +75,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::HeaderMap; /// # use actix_http::http::HeaderMap;
/// let map = HeaderMap::new(); /// let map = HeaderMap::new();
/// ///
/// assert!(map.is_empty()); /// assert!(map.is_empty());
@@ -92,7 +92,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::HeaderMap; /// # use actix_http::http::HeaderMap;
/// let map = HeaderMap::with_capacity(16); /// let map = HeaderMap::with_capacity(16);
/// ///
/// assert!(map.is_empty()); /// assert!(map.is_empty());
@@ -139,7 +139,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// # use actix_http::http::{header, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// assert_eq!(map.len(), 0); /// assert_eq!(map.len(), 0);
/// ///
@@ -162,7 +162,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// # use actix_http::http::{header, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// assert_eq!(map.len_keys(), 0); /// assert_eq!(map.len_keys(), 0);
/// ///
@@ -181,7 +181,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// # use actix_http::http::{header, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// assert!(map.is_empty()); /// assert!(map.is_empty());
/// ///
@@ -198,7 +198,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// # use actix_http::http::{header, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); /// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain"));
@@ -231,7 +231,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// # use actix_http::http::{header, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1")); /// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1"));
@@ -264,7 +264,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// # use actix_http::http::{header, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1")); /// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1"));
@@ -293,7 +293,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// # use actix_http::http::{header, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// let mut none_iter = map.get_all(header::ORIGIN); /// let mut none_iter = map.get_all(header::ORIGIN);
@@ -319,7 +319,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// # use actix_http::http::{header, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// assert!(!map.contains_key(header::ACCEPT)); /// assert!(!map.contains_key(header::ACCEPT));
/// ///
@@ -342,7 +342,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// # use actix_http::http::{header, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); /// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain"));
@@ -359,7 +359,7 @@ impl HeaderMap {
/// A convenience method is provided on the returned iterator to check if the insertion replaced /// A convenience method is provided on the returned iterator to check if the insertion replaced
/// any values. /// any values.
/// ``` /// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// # use actix_http::http::{header, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// let removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); /// let removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/plain"));
@@ -381,7 +381,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// # use actix_http::http::{header, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// map.append(header::HOST, HeaderValue::from_static("example.com")); /// map.append(header::HOST, HeaderValue::from_static("example.com"));
@@ -411,7 +411,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// # use actix_http::http::{header, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1")); /// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1"));
@@ -430,7 +430,7 @@ impl HeaderMap {
/// A convenience method is provided on the returned iterator to check if the `remove` call /// A convenience method is provided on the returned iterator to check if the `remove` call
/// actually removed any values. /// actually removed any values.
/// ``` /// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// # use actix_http::http::{header, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// let removed = map.remove("accept"); /// let removed = map.remove("accept");
@@ -459,7 +459,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::HeaderMap; /// # use actix_http::http::HeaderMap;
/// let map = HeaderMap::with_capacity(16); /// let map = HeaderMap::with_capacity(16);
/// ///
/// assert!(map.is_empty()); /// assert!(map.is_empty());
@@ -479,7 +479,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::HeaderMap; /// # use actix_http::http::HeaderMap;
/// let mut map = HeaderMap::with_capacity(2); /// let mut map = HeaderMap::with_capacity(2);
/// assert!(map.capacity() >= 2); /// assert!(map.capacity() >= 2);
/// ///
@@ -499,7 +499,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// # use actix_http::http::{header, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// let mut iter = map.iter(); /// let mut iter = map.iter();
@@ -531,7 +531,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// # use actix_http::http::{header, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// let mut iter = map.keys(); /// let mut iter = map.keys();
@@ -559,7 +559,7 @@ impl HeaderMap {
/// Keeps the allocated memory for reuse. /// Keeps the allocated memory for reuse.
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue}; /// # use actix_http::http::{header, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// let mut iter = map.drain(); /// let mut iter = map.drain();
@@ -581,8 +581,7 @@ impl HeaderMap {
} }
} }
/// Note that this implementation will clone a [HeaderName] for each value. Consider using /// Note that this implementation will clone a [HeaderName] for each value.
/// [`drain`](Self::drain) to control header name cloning.
impl IntoIterator for HeaderMap { impl IntoIterator for HeaderMap {
type Item = (HeaderName, HeaderValue); type Item = (HeaderName, HeaderValue);
type IntoIter = IntoIter; type IntoIter = IntoIter;
@@ -603,7 +602,7 @@ impl<'a> IntoIterator for &'a HeaderMap {
} }
} }
/// Iterator over borrowed values with the same associated name. /// Iterator for references of [`HeaderValue`]s with the same associated [`HeaderName`].
/// ///
/// See [`HeaderMap::get_all`]. /// See [`HeaderMap::get_all`].
#[derive(Debug)] #[derive(Debug)]
@@ -645,14 +644,10 @@ impl<'a> Iterator for GetAll<'a> {
} }
} }
impl ExactSizeIterator for GetAll<'_> {} /// Iterator for owned [`HeaderValue`]s with the same associated [`HeaderName`] returned from
/// methods that remove or replace items.
impl iter::FusedIterator for GetAll<'_> {}
/// Iterator over removed, owned values with the same associated name.
/// ///
/// Returned from methods that remove or replace items. See [`HeaderMap::insert`] /// See [`HeaderMap::insert`] and [`HeaderMap::remove`].
/// and [`HeaderMap::remove`].
#[derive(Debug)] #[derive(Debug)]
pub struct Removed { pub struct Removed {
inner: Option<smallvec::IntoIter<[HeaderValue; 4]>>, inner: Option<smallvec::IntoIter<[HeaderValue; 4]>>,
@@ -694,11 +689,7 @@ impl Iterator for Removed {
} }
} }
impl ExactSizeIterator for Removed {} /// Iterator over all [`HeaderName`]s in the map.
impl iter::FusedIterator for Removed {}
/// Iterator over all names in the map.
#[derive(Debug)] #[derive(Debug)]
pub struct Keys<'a>(hash_map::Keys<'a, HeaderName, Value>); pub struct Keys<'a>(hash_map::Keys<'a, HeaderName, Value>);
@@ -716,11 +707,6 @@ impl<'a> Iterator for Keys<'a> {
} }
} }
impl ExactSizeIterator for Keys<'_> {}
impl iter::FusedIterator for Keys<'_> {}
/// Iterator over borrowed name-value pairs.
#[derive(Debug)] #[derive(Debug)]
pub struct Iter<'a> { pub struct Iter<'a> {
inner: hash_map::Iter<'a, HeaderName, Value>, inner: hash_map::Iter<'a, HeaderName, Value>,
@@ -772,10 +758,6 @@ impl<'a> Iterator for Iter<'a> {
} }
} }
impl ExactSizeIterator for Iter<'_> {}
impl iter::FusedIterator for Iter<'_> {}
/// Iterator over drained name-value pairs. /// Iterator over drained name-value pairs.
/// ///
/// Iterator items are `(Option<HeaderName>, HeaderValue)` to avoid cloning. /// Iterator items are `(Option<HeaderName>, HeaderValue)` to avoid cloning.
@@ -827,10 +809,6 @@ impl<'a> Iterator for Drain<'a> {
} }
} }
impl ExactSizeIterator for Drain<'_> {}
impl iter::FusedIterator for Drain<'_> {}
/// Iterator over owned name-value pairs. /// Iterator over owned name-value pairs.
/// ///
/// Implementation necessarily clones header names for each value. /// Implementation necessarily clones header names for each value.
@@ -881,27 +859,12 @@ impl Iterator for IntoIter {
} }
} }
impl ExactSizeIterator for IntoIter {}
impl iter::FusedIterator for IntoIter {}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::iter::FusedIterator;
use http::header; use http::header;
use static_assertions::assert_impl_all;
use super::*; use super::*;
assert_impl_all!(HeaderMap: IntoIterator);
assert_impl_all!(Keys<'_>: Iterator, ExactSizeIterator, FusedIterator);
assert_impl_all!(GetAll<'_>: Iterator, ExactSizeIterator, FusedIterator);
assert_impl_all!(Removed: Iterator, ExactSizeIterator, FusedIterator);
assert_impl_all!(Iter<'_>: Iterator, ExactSizeIterator, FusedIterator);
assert_impl_all!(IntoIter: Iterator, ExactSizeIterator, FusedIterator);
assert_impl_all!(Drain<'_>: Iterator, ExactSizeIterator, FusedIterator);
#[test] #[test]
fn create() { fn create() {
let map = HeaderMap::new(); let map = HeaderMap::new();
@@ -1072,9 +1035,6 @@ mod tests {
assert_eq!(vals.next().unwrap().as_bytes(), b"8"); assert_eq!(vals.next().unwrap().as_bytes(), b"8");
assert_eq!(vals.next().unwrap().as_bytes(), b"9"); assert_eq!(vals.next().unwrap().as_bytes(), b"9");
assert!(vals.next().is_none()); assert!(vals.next().is_none());
// check for fused-ness
assert!(vals.next().is_none());
} }
fn owned_pair<'a>( fn owned_pair<'a>(

View File

@@ -34,29 +34,28 @@ use crate::{error::ParseError, HttpMessage};
mod as_name; mod as_name;
mod into_pair; mod into_pair;
mod into_value; mod into_value;
pub mod map; pub(crate) mod map;
mod shared; mod shared;
mod utils; mod utils;
#[doc(hidden)]
pub use self::shared::*;
pub use self::as_name::AsHeaderName; pub use self::as_name::AsHeaderName;
pub use self::into_pair::IntoHeaderPair; pub use self::into_pair::IntoHeaderPair;
pub use self::into_value::IntoHeaderValue; pub use self::into_value::IntoHeaderValue;
pub use self::map::HeaderMap; pub use self::map::{GetAll, HeaderMap, Removed};
pub use self::shared::{
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate,
LanguageTag, Quality, QualityItem,
};
pub use self::utils::{ pub use self::utils::{
fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode, fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode,
}; };
/// An interface for types that already represent a valid header. /// A trait for any object that already represents a valid header field and value.
pub trait Header: IntoHeaderValue { pub trait Header: IntoHeaderValue {
/// Returns the name of the header field /// Returns the name of the header field
fn name() -> HeaderName; fn name() -> HeaderName;
/// Parse a header /// Parse a header
fn parse<M: HttpMessage>(msg: &M) -> Result<Self, ParseError>; fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>;
} }
/// Convert `http::HeaderMap` to our `HeaderMap`. /// Convert `http::HeaderMap` to our `HeaderMap`.
@@ -67,7 +66,7 @@ impl From<http::HeaderMap> for HeaderMap {
} }
/// This encode set is used for HTTP header values and is defined at /// This encode set is used for HTTP header values and is defined at
/// <https://datatracker.ietf.org/doc/html/rfc5987#section-3.2>. /// <https://tools.ietf.org/html/rfc5987#section-3.2>.
pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
.add(b' ') .add(b' ')
.add(b'"') .add(b'"')

View File

@@ -1,13 +1,14 @@
use std::{fmt, str}; use std::fmt::{self, Display};
use std::str::FromStr;
use self::Charset::*; use self::Charset::*;
/// A MIME character set. /// A Mime charset.
/// ///
/// The string representation is normalized to upper case. /// The string representation is normalized to upper case.
/// ///
/// See <http://www.iana.org/assignments/character-sets/character-sets.xhtml>. /// See <http://www.iana.org/assignments/character-sets/character-sets.xhtml>.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub enum Charset { pub enum Charset {
/// US ASCII /// US ASCII
@@ -94,13 +95,13 @@ impl Charset {
} }
} }
impl fmt::Display for Charset { impl Display for Charset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }
impl str::FromStr for Charset { impl FromStr for Charset {
type Err = crate::Error; type Err = crate::Error;
fn from_str(s: &str) -> Result<Charset, crate::Error> { fn from_str(s: &str) -> Result<Charset, crate::Error> {

View File

@@ -9,17 +9,14 @@ use crate::{
HttpMessage, HttpMessage,
}; };
/// Error returned when a content encoding is unknown. /// Error return when a content encoding is unknown.
///
/// Example: 'compress'
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[display(fmt = "unsupported content encoding")] #[display(fmt = "unsupported content encoding")]
pub struct ContentEncodingParseError; pub struct ContentEncodingParseError;
/// Represents a supported content encoding. /// Represents a supported content encoding.
///
/// Includes a commonly-used subset of media types appropriate for use as HTTP content encodings.
/// See [IANA HTTP Content Coding Registry].
///
/// [IANA HTTP Content Coding Registry]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive] #[non_exhaustive]
pub enum ContentEncoding { pub enum ContentEncoding {
@@ -35,7 +32,7 @@ pub enum ContentEncoding {
/// Gzip algorithm. /// Gzip algorithm.
Gzip, Gzip,
/// Zstd algorithm. // Zstd algorithm.
Zstd, Zstd,
/// Indicates the identity function (i.e. no compression, nor modification). /// Indicates the identity function (i.e. no compression, nor modification).

View File

@@ -1,17 +1,17 @@
//! Originally taken from `hyper::header::parsing`.
use std::{fmt, str::FromStr}; use std::{fmt, str::FromStr};
use language_tags::LanguageTag; use language_tags::LanguageTag;
use crate::header::{Charset, HTTP_VALUE}; use crate::header::{Charset, HTTP_VALUE};
// From hyper v0.11.27 src/header/parsing.rs
/// The value part of an extended parameter consisting of three parts: /// The value part of an extended parameter consisting of three parts:
/// - The REQUIRED character set name (`charset`). /// - The REQUIRED character set name (`charset`).
/// - The OPTIONAL language information (`language_tag`). /// - The OPTIONAL language information (`language_tag`).
/// - A character sequence representing the actual value (`value`), separated by single quotes. /// - A character sequence representing the actual value (`value`), separated by single quotes.
/// ///
/// It is defined in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2). /// It is defined in [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2).
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct ExtendedValue { pub struct ExtendedValue {
/// The character set that is used to encode the `value` to a string. /// The character set that is used to encode the `value` to a string.
@@ -24,17 +24,17 @@ pub struct ExtendedValue {
pub value: Vec<u8>, pub value: Vec<u8>,
} }
/// Parses extended header parameter values (`ext-value`), as defined /// Parses extended header parameter values (`ext-value`), as defined in
/// in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2). /// [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2).
/// ///
/// Extended values are denoted by parameter names that end with `*`. /// Extended values are denoted by parameter names that end with `*`.
/// ///
/// ## ABNF /// ## ABNF
/// ///
/// ```plain /// ```text
/// ext-value = charset "'" [ language ] "'" value-chars /// ext-value = charset "'" [ language ] "'" value-chars
/// ; like RFC 2231's <extended-initial-value> /// ; like RFC 2231's <extended-initial-value>
/// ; (see [RFC 2231 §7]) /// ; (see [RFC2231], Section 7)
/// ///
/// charset = "UTF-8" / "ISO-8859-1" / mime-charset /// charset = "UTF-8" / "ISO-8859-1" / mime-charset
/// ///
@@ -43,26 +43,22 @@ pub struct ExtendedValue {
/// / "!" / "#" / "$" / "%" / "&" /// / "!" / "#" / "$" / "%" / "&"
/// / "+" / "-" / "^" / "_" / "`" /// / "+" / "-" / "^" / "_" / "`"
/// / "{" / "}" / "~" /// / "{" / "}" / "~"
/// ; as <mime-charset> in [RFC 2978 §2.3] /// ; as <mime-charset> in Section 2.3 of [RFC2978]
/// ; except that the single quote is not included /// ; except that the single quote is not included
/// ; SHOULD be registered in the IANA charset registry /// ; SHOULD be registered in the IANA charset registry
/// ///
/// language = <Language-Tag, defined in [RFC 5646 §2.1]> /// language = <Language-Tag, defined in [RFC5646], Section 2.1>
/// ///
/// value-chars = *( pct-encoded / attr-char ) /// value-chars = *( pct-encoded / attr-char )
/// ///
/// pct-encoded = "%" HEXDIG HEXDIG /// pct-encoded = "%" HEXDIG HEXDIG
/// ; see [RFC 3986 §2.1] /// ; see [RFC3986], Section 2.1
/// ///
/// attr-char = ALPHA / DIGIT /// attr-char = ALPHA / DIGIT
/// / "!" / "#" / "$" / "&" / "+" / "-" / "." /// / "!" / "#" / "$" / "&" / "+" / "-" / "."
/// / "^" / "_" / "`" / "|" / "~" /// / "^" / "_" / "`" / "|" / "~"
/// ; token except ( "*" / "'" / "%" ) /// ; token except ( "*" / "'" / "%" )
/// ``` /// ```
///
/// [RFC 2231 §7]: https://datatracker.ietf.org/doc/html/rfc2231#section-7
/// [RFC 2978 §2.3]: https://datatracker.ietf.org/doc/html/rfc2978#section-2.3
/// [RFC 3986 §2.1]: https://datatracker.ietf.org/doc/html/rfc5646#section-2.1
pub fn parse_extended_value( pub fn parse_extended_value(
val: &str, val: &str,
) -> Result<ExtendedValue, crate::error::ParseError> { ) -> Result<ExtendedValue, crate::error::ParseError> {

View File

@@ -8,7 +8,7 @@ use crate::{
helpers::MutWriter, helpers::MutWriter,
}; };
/// A timestamp with HTTP-style formatting and parsing. /// A timestamp with HTTP formatting and parsing.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct HttpDate(SystemTime); pub struct HttpDate(SystemTime);

View File

@@ -4,13 +4,11 @@ mod charset;
mod content_encoding; mod content_encoding;
mod extended; mod extended;
mod http_date; mod http_date;
mod quality;
mod quality_item; mod quality_item;
pub use self::charset::Charset; pub use self::charset::Charset;
pub use self::content_encoding::ContentEncoding; pub use self::content_encoding::ContentEncoding;
pub use self::extended::{parse_extended_value, ExtendedValue}; pub use self::extended::{parse_extended_value, ExtendedValue};
pub use self::http_date::HttpDate; pub use self::http_date::HttpDate;
pub use self::quality::{q, Quality}; pub use self::quality_item::{q, qitem, Quality, QualityItem};
pub use self::quality_item::QualityItem;
pub use language_tags::LanguageTag; pub use language_tags::LanguageTag;

View File

@@ -1,208 +0,0 @@
use std::{
convert::{TryFrom, TryInto},
fmt,
};
use derive_more::{Display, Error};
const MAX_QUALITY_INT: u16 = 1000;
const MAX_QUALITY_FLOAT: f32 = 1.0;
/// Represents a quality used in q-factor values.
///
/// The default value is equivalent to `q=1.0` (the [max](Self::MAX) value).
///
/// # Implementation notes
/// The quality value is defined as a number between 0.0 and 1.0 with three decimal places.
/// This means there are 1001 possible values. Since floating point numbers are not exact and the
/// smallest floating point data type (`f32`) consumes four bytes, we use an `u16` value to store
/// the quality internally.
///
/// [RFC 7231 §5.3.1] gives more information on quality values in HTTP header fields.
///
/// # Examples
/// ```
/// use actix_http::header::{Quality, q};
/// assert_eq!(q(1.0), Quality::MAX);
///
/// assert_eq!(q(0.42).to_string(), "0.42");
/// assert_eq!(q(1.0).to_string(), "1");
/// assert_eq!(Quality::MIN.to_string(), "0");
/// ```
///
/// [RFC 7231 §5.3.1]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Quality(pub(super) u16);
impl Quality {
/// The maximum quality value, equivalent to `q=1.0`.
pub const MAX: Quality = Quality(MAX_QUALITY_INT);
/// The minimum quality value, equivalent to `q=0.0`.
pub const MIN: Quality = Quality(0);
/// Converts a float in the range 0.01.0 to a `Quality`.
///
/// Intentionally private. External uses should rely on the `TryFrom` impl.
///
/// # Panics
/// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0.
fn from_f32(value: f32) -> Self {
// Check that `value` is within range should be done before calling this method.
// Just in case, this debug_assert should catch if we were forgetful.
debug_assert!(
(0.0f32..=1.0f32).contains(&value),
"q value must be between 0.0 and 1.0"
);
Quality((value * MAX_QUALITY_INT as f32) as u16)
}
}
/// The default value is [`Quality::MAX`].
impl Default for Quality {
fn default() -> Quality {
Quality::MAX
}
}
impl fmt::Display for Quality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
0 => f.write_str("0"),
MAX_QUALITY_INT => f.write_str("1"),
// some number in the range 1999
x => {
f.write_str("0.")?;
// This implementation avoids string allocation for removing trailing zeroes.
// In benchmarks it is twice as fast as approach using something like
// `format!("{}").trim_end_matches('0')` for non-fast-path quality values.
if x < 10 {
// x in is range 19
f.write_str("00")?;
// 0 is already handled so it's not possible to have a trailing 0 in this range
// we can just write the integer
itoa::fmt(f, x)
} else if x < 100 {
// x in is range 1099
f.write_str("0")?;
if x % 10 == 0 {
// trailing 0, divide by 10 and write
itoa::fmt(f, x / 10)
} else {
itoa::fmt(f, x)
}
} else {
// x is in range 100999
if x % 100 == 0 {
// two trailing 0s, divide by 100 and write
itoa::fmt(f, x / 100)
} else if x % 10 == 0 {
// one trailing 0, divide by 10 and write
itoa::fmt(f, x / 10)
} else {
itoa::fmt(f, x)
}
}
}
}
}
}
#[derive(Debug, Clone, Display, Error)]
#[display(fmt = "quality out of bounds")]
#[non_exhaustive]
pub struct QualityOutOfBounds;
impl TryFrom<f32> for Quality {
type Error = QualityOutOfBounds;
#[inline]
fn try_from(value: f32) -> Result<Self, Self::Error> {
if (0.0..=MAX_QUALITY_FLOAT).contains(&value) {
Ok(Quality::from_f32(value))
} else {
Err(QualityOutOfBounds)
}
}
}
/// Convenience function to create a [`Quality`] from an `f32` (0.01.0).
///
/// Not recommended for use with user input. Rely on the `TryFrom` impls where possible.
///
/// # Panics
/// Panics if value is out of range.
///
/// # Examples
/// ```
/// # use actix_http::header::{q, Quality};
/// let q1 = q(1.0);
/// assert_eq!(q1, Quality::MAX);
///
/// let q2 = q(0.0);
/// assert_eq!(q2, Quality::MIN);
///
/// let q3 = q(0.42);
/// ```
///
/// An out-of-range `f32` quality will panic.
/// ```should_panic
/// # use actix_http::header::q;
/// let _q2 = q(1.42);
/// ```
#[inline]
pub fn q<T>(quality: T) -> Quality
where
T: TryInto<Quality>,
T::Error: fmt::Debug,
{
quality.try_into().expect("quality value was out of bounds")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn q_helper() {
assert_eq!(q(0.5), Quality(500));
}
#[test]
fn display_output() {
assert_eq!(q(0.0).to_string(), "0");
assert_eq!(q(1.0).to_string(), "1");
assert_eq!(q(0.001).to_string(), "0.001");
assert_eq!(q(0.5).to_string(), "0.5");
assert_eq!(q(0.22).to_string(), "0.22");
assert_eq!(q(0.123).to_string(), "0.123");
assert_eq!(q(0.999).to_string(), "0.999");
for x in 0..=1000 {
// if trailing zeroes are handled correctly, we would not expect the serialized length
// to ever exceed "0." + 3 decimal places = 5 in length
assert!(q(x as f32 / 1000.0).to_string().len() <= 5);
}
}
#[test]
#[should_panic]
fn negative_quality() {
q(-1.0);
}
#[test]
#[should_panic]
fn quality_out_of_bounds() {
q(2.0);
}
}

View File

@@ -1,65 +1,104 @@
use std::{cmp, convert::TryFrom as _, fmt, str}; use std::{
cmp,
convert::{TryFrom, TryInto},
fmt,
str::{self, FromStr},
};
use derive_more::{Display, Error};
use crate::error::ParseError; use crate::error::ParseError;
use super::Quality; const MAX_QUALITY: u16 = 1000;
const MAX_FLOAT_QUALITY: f32 = 1.0;
/// Represents an item with a quality value as defined /// Represents a quality used in quality values.
/// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1).
/// ///
/// # Parsing and Formatting /// Can be created with the [`q`] function.
/// This wrapper be used to parse header value items that have a q-factor annotation as well as
/// serialize items with a their q-factor.
/// ///
/// # Ordering /// # Implementation notes
/// Since this context of use for this type is header value items, ordering is defined for
/// `QualityItem`s but _only_ considers the item's quality. Order of appearance should be used as
/// the secondary sorting parameter; i.e., a stable sort over the quality values will produce a
/// correctly sorted sequence.
/// ///
/// # Examples /// The quality value is defined as a number between 0 and 1 with three decimal
/// ``` /// places. This means there are 1001 possible values. Since floating point
/// # use actix_http::header::{QualityItem, q}; /// numbers are not exact and the smallest floating point data type (`f32`)
/// let q_item: QualityItem<String> = "hello;q=0.3".parse().unwrap(); /// consumes four bytes, hyper uses an `u16` value to store the
/// assert_eq!(&q_item.item, "hello"); /// quality internally. For performance reasons you may set quality directly to
/// assert_eq!(q_item.quality, q(0.3)); /// a value between 0 and 1000 e.g. `Quality(532)` matches the quality
/// `q=0.532`.
/// ///
/// // note that format is normalized compared to parsed item /// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1)
/// assert_eq!(q_item.to_string(), "hello; q=0.3"); /// gives more information on quality values in HTTP header fields.
/// #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
/// // item with q=0.3 is greater than item with q=0.1 pub struct Quality(u16);
/// let q_item_fallback: QualityItem<String> = "abc;q=0.1".parse().unwrap();
/// assert!(q_item > q_item_fallback); impl Quality {
/// ``` /// # Panics
#[derive(Debug, Clone, PartialEq, Eq)] /// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0.
fn from_f32(value: f32) -> Self {
// Check that `value` is within range should be done before calling this method.
// Just in case, this debug_assert should catch if we were forgetful.
debug_assert!(
(0.0f32..=1.0f32).contains(&value),
"q value must be between 0.0 and 1.0"
);
Quality((value * MAX_QUALITY as f32) as u16)
}
}
impl Default for Quality {
fn default() -> Quality {
Quality(MAX_QUALITY)
}
}
#[derive(Debug, Clone, Display, Error)]
pub struct QualityOutOfBounds;
impl TryFrom<u16> for Quality {
type Error = QualityOutOfBounds;
fn try_from(value: u16) -> Result<Self, Self::Error> {
if (0..=MAX_QUALITY).contains(&value) {
Ok(Quality(value))
} else {
Err(QualityOutOfBounds)
}
}
}
impl TryFrom<f32> for Quality {
type Error = QualityOutOfBounds;
fn try_from(value: f32) -> Result<Self, Self::Error> {
if (0.0..=MAX_FLOAT_QUALITY).contains(&value) {
Ok(Quality::from_f32(value))
} else {
Err(QualityOutOfBounds)
}
}
}
/// Represents an item with a quality value as defined in
/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-5.3.1).
#[derive(Clone, PartialEq, Debug)]
pub struct QualityItem<T> { pub struct QualityItem<T> {
/// The wrapped contents of the field. /// The actual contents of the field.
pub item: T, pub item: T,
/// The quality (client or server preference) for the value. /// The quality (client or server preference) for the value.
pub quality: Quality, pub quality: Quality,
} }
impl<T> QualityItem<T> { impl<T> QualityItem<T> {
/// Constructs a new `QualityItem` from an item and a quality value. /// Creates a new `QualityItem` from an item and a quality.
/// /// The item can be of any type.
/// The item can be of any type. The quality should be a value in the range [0, 1]. /// The quality should be a value in the range [0, 1].
pub fn new(item: T, quality: Quality) -> Self { pub fn new(item: T, quality: Quality) -> QualityItem<T> {
QualityItem { item, quality } QualityItem { item, quality }
} }
/// Constructs a new `QualityItem` from an item, using the maximum q-value.
pub fn max(item: T) -> Self {
Self::new(item, Quality::MAX)
} }
/// Constructs a new `QualityItem` from an item, using the minimum q-value. impl<T: PartialEq> cmp::PartialOrd for QualityItem<T> {
pub fn min(item: T) -> Self {
Self::new(item, Quality::MIN)
}
}
impl<T: PartialEq> PartialOrd for QualityItem<T> {
fn partial_cmp(&self, other: &QualityItem<T>) -> Option<cmp::Ordering> { fn partial_cmp(&self, other: &QualityItem<T>) -> Option<cmp::Ordering> {
self.quality.partial_cmp(&other.quality) self.quality.partial_cmp(&other.quality)
} }
@@ -69,71 +108,88 @@ impl<T: fmt::Display> fmt::Display for QualityItem<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.item, f)?; fmt::Display::fmt(&self.item, f)?;
match self.quality { match self.quality.0 {
// q-factor value is implied for max value MAX_QUALITY => Ok(()),
Quality::MAX => Ok(()), 0 => f.write_str("; q=0"),
x => write!(f, "; q=0.{}", format!("{:03}", x).trim_end_matches('0')),
Quality::MIN => f.write_str("; q=0"),
q => write!(f, "; q={}", q),
} }
} }
} }
impl<T: str::FromStr> str::FromStr for QualityItem<T> { impl<T: FromStr> FromStr for QualityItem<T> {
type Err = ParseError; type Err = ParseError;
fn from_str(q_item_str: &str) -> Result<Self, Self::Err> { fn from_str(qitem_str: &str) -> Result<Self, Self::Err> {
if !q_item_str.is_ascii() { if !qitem_str.is_ascii() {
return Err(ParseError::Header); return Err(ParseError::Header);
} }
// set defaults used if quality-item parsing fails, i.e., item has no q attribute // Set defaults used if parsing fails.
let mut raw_item = q_item_str; let mut raw_item = qitem_str;
let mut quality = Quality::MAX; let mut quality = 1f32;
let parts = q_item_str let parts: Vec<_> = qitem_str.rsplitn(2, ';').map(str::trim).collect();
.rsplit_once(';')
.map(|(item, q_attr)| (item.trim(), q_attr.trim()));
if let Some((val, q_attr)) = parts { if parts.len() == 2 {
// example for item with q-factor: // example for item with q-factor:
// //
// gzip; q=0.65 // gzip; q=0.65
// ^^^^ val // ^^^^^^ parts[0]
// ^^^^^^ q_attr // ^^ start
// ^^ q
// ^^^^ q_val // ^^^^ q_val
// ^^^^ parts[1]
if q_attr.len() < 2 { if parts[0].len() < 2 {
// Can't possibly be an attribute since an attribute needs at least a name followed // Can't possibly be an attribute since an attribute needs at least a name followed
// by an equals sign. And bare identifiers are forbidden. // by an equals sign. And bare identifiers are forbidden.
return Err(ParseError::Header); return Err(ParseError::Header);
} }
let q = &q_attr[0..2]; let start = &parts[0][0..2];
if q == "q=" || q == "Q=" { if start == "q=" || start == "Q=" {
let q_val = &q_attr[2..]; let q_val = &parts[0][2..];
if q_val.len() > 5 { if q_val.len() > 5 {
// longer than 5 indicates an over-precise q-factor // longer than 5 indicates an over-precise q-factor
return Err(ParseError::Header); return Err(ParseError::Header);
} }
let q_value = q_val.parse::<f32>().map_err(|_| ParseError::Header)?; let q_value = q_val.parse::<f32>().map_err(|_| ParseError::Header)?;
let q_value =
Quality::try_from(q_value).map_err(|_| ParseError::Header)?;
if (0f32..=1f32).contains(&q_value) {
quality = q_value; quality = q_value;
raw_item = val; raw_item = parts[1];
} else {
return Err(ParseError::Header);
}
} }
} }
let item = raw_item.parse::<T>().map_err(|_| ParseError::Header)?; let item = raw_item.parse::<T>().map_err(|_| ParseError::Header)?;
Ok(QualityItem::new(item, quality)) // we already checked above that the quality is within range
Ok(QualityItem::new(item, Quality::from_f32(quality)))
} }
} }
/// Convenience function to wrap a value in a `QualityItem`
/// Sets `q` to the default 1.0
pub fn qitem<T>(item: T) -> QualityItem<T> {
QualityItem::new(item, Quality::default())
}
/// Convenience function to create a `Quality` from a float or integer.
///
/// Implemented for `u16` and `f32`. Panics if value is out of range.
pub fn q<T>(val: T) -> Quality
where
T: TryInto<Quality>,
T::Error: fmt::Debug,
{
// TODO: on next breaking change, handle unwrap differently
val.try_into().unwrap()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -168,7 +224,7 @@ mod tests {
} }
} }
impl str::FromStr for Encoding { impl FromStr for Encoding {
type Err = crate::error::ParseError; type Err = crate::error::ParseError;
fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> { fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> {
use Encoding::*; use Encoding::*;
@@ -188,7 +244,7 @@ mod tests {
#[test] #[test]
fn test_quality_item_fmt_q_1() { fn test_quality_item_fmt_q_1() {
use Encoding::*; use Encoding::*;
let x = QualityItem::max(Chunked); let x = qitem(Chunked);
assert_eq!(format!("{}", x), "chunked"); assert_eq!(format!("{}", x), "chunked");
} }
#[test] #[test]
@@ -287,8 +343,25 @@ mod tests {
fn test_quality_item_ordering() { fn test_quality_item_ordering() {
let x: QualityItem<Encoding> = "gzip; q=0.5".parse().ok().unwrap(); let x: QualityItem<Encoding> = "gzip; q=0.5".parse().ok().unwrap();
let y: QualityItem<Encoding> = "gzip; q=0.273".parse().ok().unwrap(); let y: QualityItem<Encoding> = "gzip; q=0.273".parse().ok().unwrap();
let comparison_result: bool = x.gt(&y); let comparision_result: bool = x.gt(&y);
assert!(comparison_result) assert!(comparision_result)
}
#[test]
fn test_quality() {
assert_eq!(q(0.5), Quality(500));
}
#[test]
#[should_panic]
fn test_quality_invalid() {
q(-1.0);
}
#[test]
#[should_panic]
fn test_quality_invalid2() {
q(2.0);
} }
#[test] #[test]

View File

@@ -1,5 +1,3 @@
//! Header parsing utilities.
use std::{fmt, str::FromStr}; use std::{fmt, str::FromStr};
use super::HeaderValue; use super::HeaderValue;
@@ -12,12 +10,9 @@ where
I: Iterator<Item = &'a HeaderValue> + 'a, I: Iterator<Item = &'a HeaderValue> + 'a,
T: FromStr, T: FromStr,
{ {
let size_guess = all.size_hint().1.unwrap_or(2); let mut result = Vec::new();
let mut result = Vec::with_capacity(size_guess);
for h in all { for h in all {
let s = h.to_str().map_err(|_| ParseError::Header)?; let s = h.to_str().map_err(|_| ParseError::Header)?;
result.extend( result.extend(
s.split(',') s.split(',')
.filter_map(|x| match x.trim() { .filter_map(|x| match x.trim() {
@@ -27,7 +22,6 @@ where
.filter_map(|x| x.trim().parse().ok()), .filter_map(|x| x.trim().parse().ok()),
) )
} }
Ok(result) Ok(result)
} }
@@ -36,12 +30,10 @@ where
pub fn from_one_raw_str<T: FromStr>(val: Option<&HeaderValue>) -> Result<T, ParseError> { pub fn from_one_raw_str<T: FromStr>(val: Option<&HeaderValue>) -> Result<T, ParseError> {
if let Some(line) = val { if let Some(line) = val {
let line = line.to_str().map_err(|_| ParseError::Header)?; let line = line.to_str().map_err(|_| ParseError::Header)?;
if !line.is_empty() { if !line.is_empty() {
return T::from_str(line).or(Err(ParseError::Header)); return T::from_str(line).or(Err(ParseError::Header));
} }
} }
Err(ParseError::Header) Err(ParseError::Header)
} }
@@ -52,53 +44,20 @@ where
T: fmt::Display, T: fmt::Display,
{ {
let mut iter = parts.iter(); let mut iter = parts.iter();
if let Some(part) = iter.next() { if let Some(part) = iter.next() {
fmt::Display::fmt(part, f)?; fmt::Display::fmt(part, f)?;
} }
for part in iter { for part in iter {
f.write_str(", ")?; f.write_str(", ")?;
fmt::Display::fmt(part, f)?; fmt::Display::fmt(part, f)?;
} }
Ok(()) Ok(())
} }
/// Percent encode a sequence of bytes with a character set defined in [RFC 5987 §3.2]. /// Percent encode a sequence of bytes with a character set defined in
/// /// <https://tools.ietf.org/html/rfc5987#section-3.2>
/// [RFC 5987 §3.2]: https://datatracker.ietf.org/doc/html/rfc5987#section-3.2
#[inline] #[inline]
pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result { pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE); let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
fmt::Display::fmt(&encoded, f) fmt::Display::fmt(&encoded, f)
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn comma_delimited_parsing() {
let headers = vec![];
let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap();
assert_eq!(res, vec![0; 0]);
let headers = vec![
HeaderValue::from_static("1, 2"),
HeaderValue::from_static("3,4"),
];
let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap();
assert_eq!(res, vec![1, 2, 3, 4]);
let headers = vec![
HeaderValue::from_static(""),
HeaderValue::from_static(","),
HeaderValue::from_static(" "),
HeaderValue::from_static("1 ,"),
HeaderValue::from_static(""),
];
let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap();
assert_eq!(res, vec![1]);
}
}

View File

@@ -53,7 +53,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; pub use self::error::Error;
pub use self::extensions::{CloneableExtensions, 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;
pub use self::message::ConnectionType; pub use self::message::ConnectionType;
@@ -67,6 +67,26 @@ pub use self::service::HttpService;
pub use ::http::{uri, uri::Uri}; pub use ::http::{uri, uri::Uri};
pub use ::http::{Method, StatusCode, Version}; pub use ::http::{Method, StatusCode, Version};
// TODO: deprecate this mish-mash of random items
pub mod http {
//! Various HTTP related types.
// re-exports
pub use http::header::{HeaderName, HeaderValue};
pub use http::uri::PathAndQuery;
pub use http::{uri, Error, Uri};
pub use http::{Method, StatusCode, Version};
pub use crate::header::HeaderMap;
/// A collection of HTTP headers and helpers.
pub mod header {
pub use crate::header::*;
}
pub use crate::header::ContentEncoding;
pub use crate::message::ConnectionType;
}
/// A major HTTP protocol version. /// A major HTTP protocol version.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive] #[non_exhaustive]
@@ -76,14 +96,14 @@ pub enum Protocol {
Http3, Http3,
} }
type ConnectCallback<IO> = dyn Fn(&IO, &mut CloneableExtensions); type ConnectCallback<IO> = dyn Fn(&IO, &mut Extensions);
/// Container for data that extract with ConnectCallback. /// Container for data that extract with ConnectCallback.
/// ///
/// # Implementation Details /// # Implementation Details
/// Uses Option to reduce necessary allocations when merging with request extensions. /// Uses Option to reduce necessary allocations when merging with request extensions.
#[derive(Default)] #[derive(Default)]
pub(crate) struct OnConnectData(Option<CloneableExtensions>); pub(crate) struct OnConnectData(Option<Extensions>);
impl OnConnectData { impl OnConnectData {
/// Construct by calling the on-connect callback with the underlying transport I/O. /// Construct by calling the on-connect callback with the underlying transport I/O.
@@ -92,7 +112,7 @@ impl OnConnectData {
on_connect_ext: Option<&ConnectCallback<T>>, on_connect_ext: Option<&ConnectCallback<T>>,
) -> Self { ) -> Self {
let ext = on_connect_ext.map(|handler| { let ext = on_connect_ext.map(|handler| {
let mut extensions = CloneableExtensions::default(); let mut extensions = Extensions::new();
handler(io, &mut extensions); handler(io, &mut extensions);
extensions extensions
}); });
@@ -103,8 +123,8 @@ impl OnConnectData {
/// Merge self into given request's extensions. /// Merge self into given request's extensions.
#[inline] #[inline]
pub(crate) fn merge_into(&mut self, req: &mut Request) { pub(crate) fn merge_into(&mut self, req: &mut Request) {
if let Some(ref ext) = self.0 { if let Some(ref mut ext) = self.0 {
req.head.extensions.get_mut().clone_from(ext); req.head.extensions.get_mut().drain_from(ext);
} }
} }
} }

View File

@@ -46,8 +46,8 @@ pub trait Head: Default + 'static {
#[derive(Debug)] #[derive(Debug)]
pub struct RequestHead { pub struct RequestHead {
pub method: Method,
pub uri: Uri, pub uri: Uri,
pub method: Method,
pub version: Version, pub version: Version,
pub headers: HeaderMap, pub headers: HeaderMap,
pub extensions: RefCell<Extensions>, pub extensions: RefCell<Extensions>,
@@ -58,13 +58,13 @@ pub struct RequestHead {
impl Default for RequestHead { impl Default for RequestHead {
fn default() -> RequestHead { fn default() -> RequestHead {
RequestHead { RequestHead {
method: Method::default(),
uri: Uri::default(), uri: Uri::default(),
method: Method::default(),
version: Version::HTTP_11, version: Version::HTTP_11,
headers: HeaderMap::with_capacity(16), headers: HeaderMap::with_capacity(16),
extensions: RefCell::new(Extensions::new()),
peer_addr: None,
flags: Flags::empty(), flags: Flags::empty(),
peer_addr: None,
extensions: RefCell::new(Extensions::new()),
} }
} }
} }
@@ -192,7 +192,6 @@ impl RequestHead {
} }
#[derive(Debug)] #[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum RequestHeadType { pub enum RequestHeadType {
Owned(RequestHead), Owned(RequestHead),
Rc(Rc<RequestHead>, Option<HeaderMap>), Rc(Rc<RequestHead>, Option<HeaderMap>),

View File

@@ -6,14 +6,14 @@ use std::{
}; };
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use bytestring::ByteString;
use crate::{ use crate::{
body::{BoxBody, MessageBody}, body::{AnyBody, MessageBody},
error::Error,
extensions::Extensions, extensions::Extensions,
header::{self, HeaderMap, IntoHeaderValue}, http::{HeaderMap, StatusCode},
message::{BoxedResponseHead, ResponseHead}, message::{BoxedResponseHead, ResponseHead},
Error, ResponseBuilder, StatusCode, ResponseBuilder,
}; };
/// An HTTP response. /// An HTTP response.
@@ -22,13 +22,13 @@ pub struct Response<B> {
pub(crate) body: B, pub(crate) body: B,
} }
impl Response<BoxBody> { impl Response<AnyBody> {
/// Constructs a new response with default body. /// Constructs a new response with default body.
#[inline] #[inline]
pub fn new(status: StatusCode) -> Self { pub fn new(status: StatusCode) -> Self {
Response { Response {
head: BoxedResponseHead::new(status), head: BoxedResponseHead::new(status),
body: BoxBody::new(()), body: AnyBody::empty(),
} }
} }
@@ -189,14 +189,6 @@ impl<B> Response<B> {
} }
} }
#[inline]
pub fn map_into_boxed_body(self) -> Response<BoxBody>
where
B: MessageBody + 'static,
{
self.map_body(|_, body| BoxBody::new(body))
}
/// Returns body, consuming this response. /// Returns body, consuming this response.
pub fn into_body(self) -> B { pub fn into_body(self) -> B {
self.body self.body
@@ -231,99 +223,81 @@ impl<B: Default> Default for Response<B> {
} }
} }
impl<I: Into<Response<BoxBody>>, E: Into<Error>> From<Result<I, E>> impl<I: Into<Response<AnyBody>>, E: Into<Error>> From<Result<I, E>>
for Response<BoxBody> 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(),
Err(err) => Response::from(err.into()), Err(err) => err.into().into(),
} }
} }
} }
impl From<ResponseBuilder> for Response<BoxBody> { impl From<ResponseBuilder> for Response<AnyBody> {
fn from(mut builder: ResponseBuilder) -> Self { fn from(mut builder: ResponseBuilder) -> Self {
builder.finish().map_into_boxed_body() builder.finish()
} }
} }
impl From<std::convert::Infallible> for Response<BoxBody> { impl From<std::convert::Infallible> for Response<AnyBody> {
fn from(val: std::convert::Infallible) -> Self { fn from(val: std::convert::Infallible) -> Self {
match val {} match val {}
} }
} }
impl From<&'static str> for Response<&'static str> { impl From<&'static str> for Response<AnyBody> {
fn from(val: &'static str) -> Self { fn from(val: &'static str) -> Self {
let mut res = Response::with_body(StatusCode::OK, val); Response::build(StatusCode::OK)
let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); .content_type(mime::TEXT_PLAIN_UTF_8)
res.headers_mut().insert(header::CONTENT_TYPE, mime); .body(val)
res
} }
} }
impl From<&'static [u8]> for Response<&'static [u8]> { impl From<&'static [u8]> for Response<AnyBody> {
fn from(val: &'static [u8]) -> Self { fn from(val: &'static [u8]) -> Self {
let mut res = Response::with_body(StatusCode::OK, val); Response::build(StatusCode::OK)
let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap(); .content_type(mime::APPLICATION_OCTET_STREAM)
res.headers_mut().insert(header::CONTENT_TYPE, mime); .body(val)
res
} }
} }
impl From<String> for Response<String> { impl From<String> for Response<AnyBody> {
fn from(val: String) -> Self { fn from(val: String) -> Self {
let mut res = Response::with_body(StatusCode::OK, val); Response::build(StatusCode::OK)
let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); .content_type(mime::TEXT_PLAIN_UTF_8)
res.headers_mut().insert(header::CONTENT_TYPE, mime); .body(val)
res
} }
} }
impl From<&String> for Response<String> { impl<'a> From<&'a String> for Response<AnyBody> {
fn from(val: &String) -> Self { fn from(val: &'a String) -> Self {
let mut res = Response::with_body(StatusCode::OK, val.clone()); Response::build(StatusCode::OK)
let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); .content_type(mime::TEXT_PLAIN_UTF_8)
res.headers_mut().insert(header::CONTENT_TYPE, mime); .body(val)
res
} }
} }
impl From<Bytes> for Response<Bytes> { impl From<Bytes> for Response<AnyBody> {
fn from(val: Bytes) -> Self { fn from(val: Bytes) -> Self {
let mut res = Response::with_body(StatusCode::OK, val); Response::build(StatusCode::OK)
let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap(); .content_type(mime::APPLICATION_OCTET_STREAM)
res.headers_mut().insert(header::CONTENT_TYPE, mime); .body(val)
res
} }
} }
impl From<BytesMut> for Response<BytesMut> { impl From<BytesMut> for Response<AnyBody> {
fn from(val: BytesMut) -> Self { fn from(val: BytesMut) -> Self {
let mut res = Response::with_body(StatusCode::OK, val); Response::build(StatusCode::OK)
let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap(); .content_type(mime::APPLICATION_OCTET_STREAM)
res.headers_mut().insert(header::CONTENT_TYPE, mime); .body(val)
res
}
}
impl From<ByteString> for Response<ByteString> {
fn from(val: ByteString) -> Self {
let mut res = Response::with_body(StatusCode::OK, val);
let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap();
res.headers_mut().insert(header::CONTENT_TYPE, mime);
res
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{ use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE};
body::to_bytes,
header::{HeaderValue, CONTENT_TYPE, COOKIE},
};
#[test] #[test]
fn test_debug() { fn test_debug() {
@@ -335,73 +309,73 @@ mod tests {
assert!(dbg.contains("Response")); assert!(dbg.contains("Response"));
} }
#[actix_rt::test] #[test]
async fn test_into_response() { fn test_into_response() {
let res = Response::from("test"); let resp: Response<AnyBody> = "test".into();
assert_eq!(res.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(), resp.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("text/plain; charset=utf-8") HeaderValue::from_static("text/plain; charset=utf-8")
); );
assert_eq!(res.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); assert_eq!(resp.body().get_ref(), b"test");
let res = Response::from(b"test".as_ref()); let resp: Response<AnyBody> = b"test".as_ref().into();
assert_eq!(res.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(), resp.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("application/octet-stream") HeaderValue::from_static("application/octet-stream")
); );
assert_eq!(res.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); assert_eq!(resp.body().get_ref(), b"test");
let res = Response::from("test".to_owned()); let resp: Response<AnyBody> = "test".to_owned().into();
assert_eq!(res.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(), resp.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("text/plain; charset=utf-8") HeaderValue::from_static("text/plain; charset=utf-8")
); );
assert_eq!(res.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); assert_eq!(resp.body().get_ref(), b"test");
let res = Response::from("test".to_owned()); let resp: Response<AnyBody> = (&"test".to_owned()).into();
assert_eq!(res.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(), resp.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("text/plain; charset=utf-8") HeaderValue::from_static("text/plain; charset=utf-8")
); );
assert_eq!(res.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(to_bytes(res.into_body()).await.unwrap(), &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 res = Response::from(b); let resp: Response<AnyBody> = b.into();
assert_eq!(res.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(), resp.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("application/octet-stream") HeaderValue::from_static("application/octet-stream")
); );
assert_eq!(res.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(to_bytes(res.into_body()).await.unwrap(), &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 res = Response::from(b); let resp: Response<AnyBody> = b.into();
assert_eq!(res.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(), resp.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("application/octet-stream") HeaderValue::from_static("application/octet-stream")
); );
assert_eq!(res.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); assert_eq!(resp.body().get_ref(), b"test");
let b = BytesMut::from("test"); let b = BytesMut::from("test");
let res = Response::from(b); let resp: Response<AnyBody> = b.into();
assert_eq!(res.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(), resp.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("application/octet-stream") HeaderValue::from_static("application/octet-stream")
); );
assert_eq!(res.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); assert_eq!(resp.body().get_ref(), b"test");
} }
} }

View File

@@ -2,11 +2,19 @@
use std::{ use std::{
cell::{Ref, RefMut}, cell::{Ref, RefMut},
fmt, str, error::Error as StdError,
fmt,
future::Future,
pin::Pin,
str,
task::{Context, Poll},
}; };
use bytes::Bytes;
use futures_core::Stream;
use crate::{ use crate::{
body::{EitherBody, MessageBody}, 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},
@@ -20,7 +28,7 @@ use crate::{
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use actix_http::{Response, ResponseBuilder, StatusCode, body, header}; /// use actix_http::{Response, ResponseBuilder, body, http::StatusCode, http::header};
/// ///
/// # actix_rt::System::new().block_on(async { /// # actix_rt::System::new().block_on(async {
/// let mut res: Response<_> = Response::build(StatusCode::OK) /// let mut res: Response<_> = Response::build(StatusCode::OK)
@@ -47,7 +55,9 @@ impl ResponseBuilder {
/// Create response builder /// Create response builder
/// ///
/// # Examples /// # Examples
// /// use actix_http::{Response, ResponseBuilder, StatusCode};, / `` /// ```
/// use actix_http::{Response, ResponseBuilder, http::StatusCode};
///
/// let res: Response<_> = ResponseBuilder::default().finish(); /// let res: Response<_> = ResponseBuilder::default().finish();
/// assert_eq!(res.status(), StatusCode::OK); /// assert_eq!(res.status(), StatusCode::OK);
/// ``` /// ```
@@ -62,7 +72,9 @@ impl ResponseBuilder {
/// Set HTTP status code of this response. /// Set HTTP status code of this response.
/// ///
/// # Examples /// # Examples
// /// use actix_http::{ResponseBuilder, StatusCode};, / `` /// ```
/// use actix_http::{ResponseBuilder, http::StatusCode};
///
/// let res = ResponseBuilder::default().status(StatusCode::NOT_FOUND).finish(); /// let res = ResponseBuilder::default().status(StatusCode::NOT_FOUND).finish();
/// assert_eq!(res.status(), StatusCode::NOT_FOUND); /// assert_eq!(res.status(), StatusCode::NOT_FOUND);
/// ``` /// ```
@@ -78,7 +90,7 @@ impl ResponseBuilder {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use actix_http::{ResponseBuilder, header}; /// use actix_http::{ResponseBuilder, http::header};
/// ///
/// let res = ResponseBuilder::default() /// let res = ResponseBuilder::default()
/// .insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)) /// .insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON))
@@ -108,7 +120,7 @@ impl ResponseBuilder {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use actix_http::{ResponseBuilder, header}; /// use actix_http::{ResponseBuilder, http::header};
/// ///
/// let res = ResponseBuilder::default() /// let res = ResponseBuilder::default()
/// .append_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)) /// .append_header((header::CONTENT_TYPE, mime::APPLICATION_JSON))
@@ -223,14 +235,10 @@ impl ResponseBuilder {
/// Generate response with a wrapped body. /// Generate response with a wrapped body.
/// ///
/// This `ResponseBuilder` will be left in a useless state. /// This `ResponseBuilder` will be left in a useless state.
pub fn body<B>(&mut self, body: B) -> Response<EitherBody<B>> #[inline]
where pub fn body<B: Into<AnyBody>>(&mut self, body: B) -> Response<AnyBody> {
B: MessageBody + 'static, self.message_body(body.into())
{ .unwrap_or_else(Response::from)
match self.message_body(body) {
Ok(res) => res.map_body(|_, body| EitherBody::left(body)),
Err(err) => Response::from(err).map_body(|_, body| EitherBody::right(body)),
}
} }
/// Generate response with a body. /// Generate response with a body.
@@ -245,12 +253,24 @@ impl ResponseBuilder {
Ok(Response { head, body }) Ok(Response { head, body })
} }
/// Generate response with a streaming body.
///
/// This `ResponseBuilder` will be left in a useless state.
#[inline]
pub fn streaming<S, E>(&mut self, stream: S) -> Response<AnyBody>
where
S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Box<dyn StdError>> + 'static,
{
self.body(AnyBody::new_boxed(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<EitherBody<()>> { pub fn finish(&mut self) -> Response<AnyBody> {
self.body(()) 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.
@@ -307,6 +327,14 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder {
} }
} }
impl Future for ResponseBuilder {
type Output = Result<Response<AnyBody>, Error>;
fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(Ok(self.finish()))
}
}
impl fmt::Debug for ResponseBuilder { impl fmt::Debug for ResponseBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let head = self.head.as_ref().unwrap(); let head = self.head.as_ref().unwrap();
@@ -328,10 +356,9 @@ impl fmt::Debug for ResponseBuilder {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use bytes::Bytes;
use super::*; use super::*;
use crate::header::{HeaderName, HeaderValue, CONTENT_TYPE}; use crate::body::AnyBody;
use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE};
#[test] #[test]
fn test_basic_builder() { fn test_basic_builder() {
@@ -356,28 +383,20 @@ mod tests {
#[test] #[test]
fn test_force_close() { fn test_force_close() {
let resp = Response::build(StatusCode::OK).force_close().finish(); let resp = Response::build(StatusCode::OK).force_close().finish();
assert!(!resp.keep_alive()); assert!(!resp.keep_alive())
} }
#[test] #[test]
fn test_content_type() { fn test_content_type() {
let resp = Response::build(StatusCode::OK) let resp = Response::build(StatusCode::OK)
.content_type("text/plain") .content_type("text/plain")
.body(Bytes::new()); .body(AnyBody::empty());
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain"); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain")
let resp = Response::build(StatusCode::OK)
.content_type(mime::APPLICATION_JAVASCRIPT_UTF_8)
.body(Bytes::new());
assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(),
"application/javascript; charset=utf-8"
);
} }
#[test] #[test]
fn test_into_builder() { fn test_into_builder() {
let mut resp: Response<_> = "test".into(); let mut resp: Response<AnyBody> = "test".into();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
resp.headers_mut().insert( resp.headers_mut().insert(

View File

@@ -1,4 +1,5 @@
use std::{ use std::{
error::Error as StdError,
fmt, fmt,
future::Future, future::Future,
marker::PhantomData, marker::PhantomData,
@@ -8,16 +9,18 @@ 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::{
fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _, fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _,
}; };
use bytes::Bytes;
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
use pin_project_lite::pin_project; use pin_project::pin_project;
use crate::{ use crate::{
body::{BoxBody, MessageBody}, body::{AnyBody, MessageBody},
builder::HttpServiceBuilder, builder::HttpServiceBuilder,
config::{KeepAlive, ServiceConfig}, config::{KeepAlive, ServiceConfig},
error::DispatchError, error::DispatchError,
@@ -37,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<Response<BoxBody>> + '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,
@@ -52,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<Response<BoxBody>> + '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 {
@@ -91,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<Response<BoxBody>> + '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,
@@ -105,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<Response<BoxBody>>, X1::Error: Into<Response<AnyBody>>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
{ {
HttpService { HttpService {
@@ -149,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<Response<BoxBody>> + '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<Response<BoxBody>>, X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
@@ -167,7 +172,7 @@ where
Response = (), Response = (),
>, >,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Response<BoxBody>>, 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
@@ -192,10 +197,7 @@ where
mod openssl { mod openssl {
use actix_service::ServiceFactoryExt as _; use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{ use actix_tls::accept::{
openssl::{ openssl::{Acceptor, SslAcceptor, SslError, TlsStream},
reexports::{Error as SslError, SslAcceptor},
Acceptor, TlsStream,
},
TlsError, TlsError,
}; };
@@ -205,16 +207,17 @@ mod openssl {
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<BoxBody>> + '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<Response<BoxBody>>, X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
@@ -223,7 +226,7 @@ mod openssl {
Response = (), Response = (),
>, >,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Response<BoxBody>>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create OpenSSL based service. /// Create OpenSSL based service.
@@ -267,7 +270,7 @@ mod rustls {
use actix_service::ServiceFactoryExt as _; use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{ use actix_tls::accept::{
rustls::{reexports::ServerConfig, Acceptor, TlsStream}, rustls::{Acceptor, ServerConfig, TlsStream},
TlsError, TlsError,
}; };
@@ -277,16 +280,17 @@ mod rustls {
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<BoxBody>> + '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<Response<BoxBody>>, X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
@@ -295,7 +299,7 @@ mod rustls {
Response = (), Response = (),
>, >,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Response<BoxBody>>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create Rustls based service. /// Create Rustls based service.
@@ -343,21 +347,22 @@ where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<BoxBody>> + '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<Response<BoxBody>>, 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<Response<BoxBody>>, U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
type Response = (); type Response = ();
@@ -420,11 +425,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<Response<BoxBody>>, S::Error: Into<Response<AnyBody>>,
X: Service<Request>, X: Service<Request>,
X::Error: Into<Response<BoxBody>>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, h1::Codec>)>, U: Service<(Request, Framed<T, h1::Codec>)>,
U::Error: Into<Response<BoxBody>>, U::Error: Into<Response<AnyBody>>,
{ {
pub(super) fn new( pub(super) fn new(
cfg: ServiceConfig, cfg: ServiceConfig,
@@ -444,7 +449,7 @@ where
pub(super) fn _poll_ready( pub(super) fn _poll_ready(
&self, &self,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Result<(), Response<BoxBody>>> { ) -> 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))?;
@@ -480,17 +485,18 @@ where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<BoxBody>> + '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<Response<BoxBody>>, 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<Response<BoxBody>>, U::Error: fmt::Display + Into<Response<AnyBody>>,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
@@ -512,27 +518,23 @@ where
match proto { match proto {
Protocol::Http2 => HttpServiceHandlerResponse { Protocol::Http2 => HttpServiceHandlerResponse {
state: State::H2Handshake { state: State::H2Handshake(Some((
handshake: Some(( h2_handshake(io),
h2::handshake_with_timeout(io, &self.cfg),
self.cfg.clone(), self.cfg.clone(),
self.flow.clone(), self.flow.clone(),
on_connect_data, on_connect_data,
peer_addr, peer_addr,
)), ))),
},
}, },
Protocol::Http1 => HttpServiceHandlerResponse { Protocol::Http1 => HttpServiceHandlerResponse {
state: State::H1 { state: State::H1(h1::Dispatcher::new(
dispatcher: h1::Dispatcher::new(
io, io,
self.cfg.clone(), self.cfg.clone(),
self.flow.clone(), self.flow.clone(),
on_connect_data, on_connect_data,
peer_addr, peer_addr,
), )),
},
}, },
proto => unimplemented!("Unsupported HTTP version: {:?}.", proto), proto => unimplemented!("Unsupported HTTP version: {:?}.", proto),
@@ -540,58 +542,52 @@ where
} }
} }
pin_project! { #[pin_project(project = StateProj)]
#[project = StateProj]
enum State<T, S, B, X, U> enum State<T, S, B, X, U>
where where
T: AsyncRead, T: AsyncRead + AsyncWrite + Unpin,
T: AsyncWrite,
T: Unpin,
S: Service<Request>, S: Service<Request>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<BoxBody>>, 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<Response<BoxBody>>, 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] dispatcher: h1::Dispatcher<T, S, B, X, U> }, H1(#[pin] h1::Dispatcher<T, S, B, X, U>),
H2 { #[pin] dispatcher: h2::Dispatcher<T, S, B, X, U> }, H2(#[pin] h2::Dispatcher<T, S, B, X, U>),
H2Handshake { H2Handshake(
handshake: Option<( Option<(
h2::HandshakeWithTimeout<T>, H2Handshake<T, Bytes>,
ServiceConfig, ServiceConfig,
Rc<HttpFlow<S, X, U>>, Rc<HttpFlow<S, X, U>>,
OnConnectData, OnConnectData,
Option<net::SocketAddr>, Option<net::SocketAddr>,
)>, )>,
}, ),
}
} }
pin_project! { #[pin_project]
pub struct HttpServiceHandlerResponse<T, S, B, X, U> pub struct HttpServiceHandlerResponse<T, S, B, X, U>
where where
T: AsyncRead, T: AsyncRead + AsyncWrite + Unpin,
T: AsyncWrite,
T: Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<BoxBody>>, S::Error: Into<Response<AnyBody>> + 'static,
S::Error: 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>> + 'static,
S::Response: 'static,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Response<BoxBody>>, 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,
@@ -599,21 +595,21 @@ pin_project! {
#[pin] #[pin]
state: State<T, S, B, X, U>, state: State<T, S, B, X, U>,
} }
}
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<Response<BoxBody>> + '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<Response<BoxBody>>, 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,
@@ -622,29 +618,27 @@ where
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.as_mut().project().state.project() { match self.as_mut().project().state.project() {
StateProj::H1 { dispatcher } => dispatcher.poll(cx), StateProj::H1(disp) => disp.poll(cx),
StateProj::H2 { dispatcher } => dispatcher.poll(cx), StateProj::H2(disp) => disp.poll(cx),
StateProj::H2Handshake { handshake: data } => { StateProj::H2Handshake(data) => {
match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) { match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) {
Ok((conn, timer)) => { Ok(conn) => {
let (_, config, flow, 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(
self.as_mut().project().state.set(State::H2 { h2::Dispatcher::new(
dispatcher: h2::Dispatcher::new( srv,
flow,
conn, conn,
on_connect_data, on_connect_data,
config, cfg,
peer_addr, peer_addr,
timer,
), ),
}); ));
self.poll(cx) self.poll(cx)
} }
Err(err) => { Err(err) => {
trace!("H2 handshake error: {}", err); trace!("H2 handshake error: {}", err);
Poll::Ready(Err(err)) Poll::Ready(Err(err.into()))
} }
} }
} }

View File

@@ -63,8 +63,8 @@ pub enum Item {
Last(Bytes), Last(Bytes),
} }
#[derive(Debug, Copy, Clone)]
/// WebSocket protocol codec. /// WebSocket protocol codec.
#[derive(Debug, Clone)]
pub struct Codec { pub struct Codec {
flags: Flags, flags: Flags,
max_size: usize, max_size: usize,
@@ -89,8 +89,7 @@ impl Codec {
/// Set max frame size. /// Set max frame size.
/// ///
/// By default max size is set to 64KiB. /// By default max size is set to 64kB.
#[must_use = "This returns the a new Codec, without modifying the original."]
pub fn max_size(mut self, size: usize) -> Self { pub fn max_size(mut self, size: usize) -> Self {
self.max_size = size; self.max_size = size;
self self
@@ -99,19 +98,12 @@ impl Codec {
/// Set decoder to client mode. /// Set decoder to client mode.
/// ///
/// By default decoder works in server mode. /// By default decoder works in server mode.
#[must_use = "This returns the a new Codec, without modifying the original."]
pub fn client_mode(mut self) -> Self { pub fn client_mode(mut self) -> Self {
self.flags.remove(Flags::SERVER); self.flags.remove(Flags::SERVER);
self self
} }
} }
impl Default for Codec {
fn default() -> Self {
Self::new()
}
}
impl Encoder<Message> for Codec { impl Encoder<Message> for Codec {
type Error = ProtocolError; type Error = ProtocolError;

View File

@@ -4,22 +4,18 @@ use std::task::{Context, Poll};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_service::{IntoService, Service}; use actix_service::{IntoService, Service};
use pin_project_lite::pin_project;
use super::{Codec, Frame, Message}; use super::{Codec, Frame, Message};
pin_project! { #[pin_project::pin_project]
pub struct Dispatcher<S, T> pub struct Dispatcher<S, T>
where where
S: Service<Frame, Response = Message>, S: Service<Frame, Response = Message> + 'static,
S: 'static, T: AsyncRead + AsyncWrite,
T: AsyncRead,
T: AsyncWrite,
{ {
#[pin] #[pin]
inner: inner::Dispatcher<S, T, Codec, Message>, inner: inner::Dispatcher<S, T, Codec, Message>,
} }
}
impl<S, T> Dispatcher<S, T> impl<S, T> Dispatcher<S, T>
where where
@@ -76,7 +72,7 @@ mod inner {
use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed};
use crate::{body::BoxBody, Response}; use crate::{body::AnyBody, Response};
/// Framed transport errors /// Framed transport errors
pub enum DispatcherError<E, U, I> pub enum DispatcherError<E, U, I>
@@ -140,7 +136,7 @@ mod inner {
} }
} }
impl<E, U, I> From<DispatcherError<E, U, I>> for Response<BoxBody> 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,
@@ -148,7 +144,7 @@ mod inner {
<U as Decoder>::Error: fmt::Debug, <U as Decoder>::Error: fmt::Debug,
{ {
fn from(err: DispatcherError<E, U, I>) -> Self { fn from(err: DispatcherError<E, U, I>) -> Self {
Response::internal_server_error().set_body(BoxBody::new(err.to_string())) Response::internal_server_error().set_body(AnyBody::from(err.to_string()))
} }
} }

View File

@@ -8,9 +8,9 @@ use std::io;
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
use http::{header, Method, StatusCode}; use http::{header, Method, StatusCode};
use crate::body::BoxBody;
use crate::{ use crate::{
header::HeaderValue, message::RequestHead, response::Response, ResponseBuilder, body::AnyBody, header::HeaderValue, message::RequestHead, response::Response,
ResponseBuilder,
}; };
mod codec; mod codec;
@@ -69,7 +69,7 @@ pub enum ProtocolError {
} }
/// WebSocket handshake errors /// WebSocket handshake errors
#[derive(Debug, Clone, Copy, PartialEq, Display, Error)] #[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.")]
@@ -96,8 +96,8 @@ pub enum HandshakeError {
BadWebsocketKey, BadWebsocketKey,
} }
impl From<HandshakeError> for Response<BoxBody> { impl From<&HandshakeError> for Response<AnyBody> {
fn from(err: HandshakeError) -> Self { fn from(err: &HandshakeError) -> Self {
match err { match err {
HandshakeError::GetMethodRequired => { HandshakeError::GetMethodRequired => {
let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED); let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED);
@@ -139,9 +139,9 @@ impl From<HandshakeError> for Response<BoxBody> {
} }
} }
impl From<&HandshakeError> for Response<BoxBody> { impl From<HandshakeError> for Response<AnyBody> {
fn from(err: &HandshakeError) -> Self { fn from(err: HandshakeError) -> Self {
(*err).into() (&err).into()
} }
} }
@@ -220,10 +220,9 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{header, Method};
use super::*; use super::*;
use crate::test::TestRequest; use crate::{body::AnyBody, test::TestRequest};
use http::{header, Method};
#[test] #[test]
fn test_handshake() { fn test_handshake() {
@@ -337,17 +336,17 @@ mod tests {
#[test] #[test]
fn test_ws_error_http_response() { fn test_ws_error_http_response() {
let resp: Response<BoxBody> = HandshakeError::GetMethodRequired.into(); 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: Response<BoxBody> = HandshakeError::NoWebsocketUpgrade.into(); let resp: Response<AnyBody> = HandshakeError::NoWebsocketUpgrade.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp: Response<BoxBody> = HandshakeError::NoConnectionUpgrade.into(); let resp: Response<AnyBody> = HandshakeError::NoConnectionUpgrade.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp: Response<BoxBody> = HandshakeError::NoVersionHeader.into(); let resp: Response<AnyBody> = HandshakeError::NoVersionHeader.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp: Response<BoxBody> = HandshakeError::UnsupportedVersion.into(); let resp: Response<AnyBody> = HandshakeError::UnsupportedVersion.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp: Response<BoxBody> = HandshakeError::BadWebsocketKey.into(); let resp: Response<AnyBody> = HandshakeError::BadWebsocketKey.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
} }
} }

View File

@@ -3,9 +3,7 @@ use std::{
fmt, fmt,
}; };
/// Operation codes defined in [RFC 6455 §11.8]. /// Operation codes as part of RFC6455.
///
/// [RFC 6455]: https://datatracker.ietf.org/doc/html/rfc6455#section-11.8
#[derive(Debug, Eq, PartialEq, Clone, Copy)] #[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum OpCode { pub enum OpCode {
/// Indicates a continuation frame of a fragmented message. /// Indicates a continuation frame of a fragmented message.
@@ -222,8 +220,7 @@ impl<T: Into<String>> From<(CloseCode, T)> for CloseReason {
} }
} }
/// The WebSocket GUID as stated in the spec. /// The WebSocket GUID as stated in the spec. See <https://tools.ietf.org/html/rfc6455#section-1.3>.
/// See <https://datatracker.ietf.org/doc/html/rfc6455#section-1.3>.
static WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; static WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
/// Hashes the `Sec-WebSocket-Key` header according to the WebSocket spec. /// Hashes the `Sec-WebSocket-Key` header according to the WebSocket spec.

View File

@@ -1,7 +1,7 @@
use std::convert::Infallible; use std::convert::Infallible;
use actix_http::{ use actix_http::{
body::BoxBody, HttpMessage, HttpService, Request, Response, StatusCode, 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;
@@ -99,7 +99,7 @@ async fn test_with_query_parameter() {
#[display(fmt = "expect failed")] #[display(fmt = "expect failed")]
struct ExpectFailed; struct ExpectFailed;
impl From<ExpectFailed> for Response<BoxBody> { impl From<ExpectFailed> for Response<AnyBody> {
fn from(_: ExpectFailed) -> Self { fn from(_: ExpectFailed) -> Self {
Response::new(StatusCode::EXPECTATION_FAILED) Response::new(StatusCode::EXPECTATION_FAILED)
} }

View File

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

View File

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

View File

@@ -5,10 +5,13 @@ extern crate tls_openssl as openssl;
use std::{convert::Infallible, io}; use std::{convert::Infallible, io};
use actix_http::{ use actix_http::{
body::{BodyStream, BoxBody, SizedStream}, body::{AnyBody, SizedStream},
error::PayloadError, error::PayloadError,
http::{
header::{self, HeaderValue}, header::{self, HeaderValue},
Error, HttpMessage, HttpService, Method, Request, Response, StatusCode, Version, Method, StatusCode, Version,
},
Error, HttpMessage, HttpService, Request, Response,
}; };
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_service::{fn_service, ServiceFactoryExt}; use actix_service::{fn_service, ServiceFactoryExt};
@@ -345,7 +348,7 @@ async fn test_h2_body_chunked_explicit() {
ok::<_, Infallible>( ok::<_, Infallible>(
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header((header::TRANSFER_ENCODING, "chunked"))
.body(BodyStream::new(body)), .streaming(body),
) )
}) })
.openssl(tls_config()) .openssl(tls_config())
@@ -396,11 +399,9 @@ async fn test_h2_response_http_error_handling() {
#[display(fmt = "error")] #[display(fmt = "error")]
struct BadRequest; struct BadRequest;
impl From<BadRequest> for Response<BoxBody> { impl From<BadRequest> for Response<AnyBody> {
fn from(err: BadRequest) -> Self { fn from(err: BadRequest) -> Self {
Response::build(StatusCode::BAD_REQUEST) Response::build(StatusCode::BAD_REQUEST).body(err.to_string())
.body(err.to_string())
.map_into_boxed_body()
} }
} }
@@ -408,7 +409,7 @@ impl From<BadRequest> for Response<BoxBody> {
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<BoxBody>, _>(BadRequest)) .h2(|_| err::<Response<AnyBody>, _>(BadRequest))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })

View File

@@ -10,14 +10,17 @@ use std::{
}; };
use actix_http::{ use actix_http::{
body::{BodyStream, BoxBody, SizedStream}, body::{AnyBody, SizedStream},
error::PayloadError, error::PayloadError,
http::{
header::{self, HeaderName, HeaderValue}, header::{self, HeaderName, HeaderValue},
Error, HttpService, Method, Request, Response, StatusCode, Version, Method, StatusCode, Version,
},
Error, HttpService, Request, Response,
}; };
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_tls::connect::rustls::webpki_roots_cert_store; use actix_tls::connect::tls::rustls::webpki_roots_cert_store;
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 derive_more::{Display, Error};
@@ -413,7 +416,7 @@ async fn test_h2_body_chunked_explicit() {
ok::<_, Infallible>( ok::<_, Infallible>(
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header((header::TRANSFER_ENCODING, "chunked"))
.body(BodyStream::new(body)), .streaming(body),
) )
}) })
.rustls(tls_config()) .rustls(tls_config())
@@ -464,9 +467,9 @@ async fn test_h2_response_http_error_handling() {
#[display(fmt = "error")] #[display(fmt = "error")]
struct BadRequest; struct BadRequest;
impl From<BadRequest> for Response<BoxBody> { impl From<BadRequest> for Response<AnyBody> {
fn from(_: BadRequest) -> Self { fn from(_: BadRequest) -> Self {
Response::bad_request().set_body(BoxBody::new("error")) Response::bad_request().set_body(AnyBody::from("error"))
} }
} }
@@ -474,7 +477,7 @@ impl From<BadRequest> for Response<BoxBody> {
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<BoxBody>, _>(BadRequest)) .h2(|_| err::<Response<AnyBody>, _>(BadRequest))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@@ -491,7 +494,7 @@ async fn test_h2_service_error() {
async fn test_h1_service_error() { async fn test_h1_service_error() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h1(|_| err::<Response<BoxBody>, _>(BadRequest)) .h1(|_| err::<Response<AnyBody>, _>(BadRequest))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;

View File

@@ -6,8 +6,9 @@ use std::{
}; };
use actix_http::{ use actix_http::{
body::{self, BodyStream, BoxBody, SizedStream}, body::{AnyBody, SizedStream},
header, Error, HttpMessage, HttpService, KeepAlive, Request, Response, StatusCode, 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;
@@ -68,7 +69,7 @@ async fn test_h1_2() {
#[display(fmt = "expect failed")] #[display(fmt = "expect failed")]
struct ExpectFailed; struct ExpectFailed;
impl From<ExpectFailed> for Response<BoxBody> { impl From<ExpectFailed> for Response<AnyBody> {
fn from(_: ExpectFailed) -> Self { fn from(_: ExpectFailed) -> Self {
Response::new(StatusCode::EXPECTATION_FAILED) Response::new(StatusCode::EXPECTATION_FAILED)
} }
@@ -382,7 +383,7 @@ async fn test_http1_keepalive_disabled() {
#[actix_rt::test] #[actix_rt::test]
async fn test_content_length() { async fn test_content_length() {
use actix_http::{ use actix_http::http::{
header::{HeaderName, HeaderValue}, header::{HeaderName, HeaderValue},
StatusCode, StatusCode,
}; };
@@ -621,7 +622,7 @@ async fn test_h1_body_chunked_explicit() {
ok::<_, Infallible>( ok::<_, Infallible>(
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header((header::TRANSFER_ENCODING, "chunked"))
.body(BodyStream::new(body)), .streaming(body),
) )
}) })
.tcp() .tcp()
@@ -655,9 +656,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::<_, Infallible>( ok::<_, Infallible>(Response::build(StatusCode::OK).streaming(body))
Response::build(StatusCode::OK).body(BodyStream::new(body)),
)
}) })
.tcp() .tcp()
}) })
@@ -715,9 +714,9 @@ async fn test_h1_response_http_error_handling() {
#[display(fmt = "error")] #[display(fmt = "error")]
struct BadRequest; struct BadRequest;
impl From<BadRequest> for Response<BoxBody> { impl From<BadRequest> for Response<AnyBody> {
fn from(_: BadRequest) -> Self { fn from(_: BadRequest) -> Self {
Response::bad_request().set_body(BoxBody::new("error")) Response::bad_request().set_body(AnyBody::from("error"))
} }
} }
@@ -725,7 +724,7 @@ impl From<BadRequest> for Response<BoxBody> {
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<()>, _>(BadRequest)) .h1(|_| err::<Response<AnyBody>, _>(BadRequest))
.tcp() .tcp()
}) })
.await; .await;
@@ -774,35 +773,36 @@ async fn test_not_modified_spec_h1() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|req: Request| { .h1(|req: Request| {
let res: Response<BoxBody> = match req.path() { let res: Response<AnyBody> = match req.path() {
// with no content-length // with no content-length
"/none" => { "/none" => {
Response::with_body(StatusCode::NOT_MODIFIED, body::None::new()) Response::with_body(StatusCode::NOT_MODIFIED, AnyBody::None)
.map_into_boxed_body()
} }
// with no content-length // with no content-length
"/body" => Response::with_body(StatusCode::NOT_MODIFIED, "1234") "/body" => Response::with_body(
.map_into_boxed_body(), StatusCode::NOT_MODIFIED,
AnyBody::from("1234"),
),
// with manual content-length header and specific None body // with manual content-length header and specific None body
"/cl-none" => { "/cl-none" => {
let mut res = Response::with_body( let mut res =
StatusCode::NOT_MODIFIED, Response::with_body(StatusCode::NOT_MODIFIED, AnyBody::None);
body::None::new(),
);
res.headers_mut() res.headers_mut()
.insert(CL.clone(), header::HeaderValue::from_static("24")); .insert(CL.clone(), header::HeaderValue::from_static("24"));
res.map_into_boxed_body() res
} }
// with manual content-length header and ignore-able body // with manual content-length header and ignore-able body
"/cl-body" => { "/cl-body" => {
let mut res = let mut res = Response::with_body(
Response::with_body(StatusCode::NOT_MODIFIED, "1234"); StatusCode::NOT_MODIFIED,
AnyBody::from("1234"),
);
res.headers_mut() res.headers_mut()
.insert(CL.clone(), header::HeaderValue::from_static("4")); .insert(CL.clone(), header::HeaderValue::from_static("4"));
res.map_into_boxed_body() res
} }
_ => panic!("unknown route"), _ => panic!("unknown route"),

View File

@@ -6,7 +6,7 @@ use std::{
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_http::{ use actix_http::{
body::{BodySize, BoxBody}, body::{AnyBody, BodySize},
h1, h1,
ws::{self, CloseCode, Frame, Item, Message}, ws::{self, CloseCode, Frame, Item, Message},
Error, HttpService, Request, Response, Error, HttpService, Request, Response,
@@ -50,14 +50,14 @@ enum WsServiceError {
Dispatcher, Dispatcher,
} }
impl From<WsServiceError> for Response<BoxBody> { impl From<WsServiceError> for Response<AnyBody> {
fn from(err: WsServiceError) -> Self { fn from(err: WsServiceError) -> Self {
match err { match err {
WsServiceError::Http(err) => err.into(), WsServiceError::Http(err) => err.into(),
WsServiceError::Ws(err) => err.into(), WsServiceError::Ws(err) => err.into(),
WsServiceError::Io(_err) => unreachable!(), WsServiceError::Io(_err) => unreachable!(),
WsServiceError::Dispatcher => Response::internal_server_error() WsServiceError::Dispatcher => Response::internal_server_error()
.set_body(BoxBody::new(format!("{}", err))), .set_body(AnyBody::from(format!("{}", err))),
} }
} }
} }

View File

@@ -3,12 +3,6 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.4.0-beta.9 - 2021-12-01
* Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463]
[#2463]: https://github.com/actix/actix-web/pull/2463
## 0.4.0-beta.8 - 2021-11-22 ## 0.4.0-beta.8 - 2021-11-22
* Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451] * Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451]
* Added `MultipartError::NoContentDisposition` variant. [#2451] * Added `MultipartError::NoContentDisposition` variant. [#2451]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-multipart" name = "actix-multipart"
version = "0.4.0-beta.9" version = "0.4.0-beta.8"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Multipart form support for Actix Web" description = "Multipart form support for Actix Web"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
@@ -28,7 +28,7 @@ twoway = "0.2"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-http = "3.0.0-beta.14" actix-http = "3.0.0-beta.13"
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
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.9)](https://docs.rs/actix-multipart/0.4.0-beta.9) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.8)](https://docs.rs/actix-multipart/0.4.0-beta.8)
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.9/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.9) [![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.8/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.8)
[![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@@ -10,7 +10,7 @@ use derive_more::{Display, Error, From};
pub enum MultipartError { pub enum MultipartError {
/// Content-Disposition header is not found or is not equal to "form-data". /// Content-Disposition header is not found or is not equal to "form-data".
/// ///
/// According to [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) a /// According to [RFC 7578](https://tools.ietf.org/html/rfc7578#section-4.2) a
/// Content-Disposition header must always be present and equal to "form-data". /// Content-Disposition header must always be present and equal to "form-data".
#[display(fmt = "No Content-Disposition `form-data` header")] #[display(fmt = "No Content-Disposition `form-data` header")]
NoContentDisposition, NoContentDisposition,

View File

@@ -337,8 +337,8 @@ impl InnerMultipart {
return Poll::Pending; return Poll::Pending;
}; };
// According to RFC 7578 §4.2, a Content-Disposition header must always be present and // According to [RFC 7578](https://tools.ietf.org/html/rfc7578#section-4.2) a
// set to "form-data". // Content-Disposition header must always be present and set to "form-data".
let content_disposition = headers let content_disposition = headers
.get(&header::CONTENT_DISPOSITION) .get(&header::CONTENT_DISPOSITION)
@@ -435,10 +435,10 @@ impl Field {
/// Returns the field's Content-Disposition. /// Returns the field's Content-Disposition.
/// ///
/// Per [RFC 7578 §4.2]: "Each part MUST contain a Content-Disposition header field where the /// Per [RFC 7578 §4.2]: 'Each part MUST contain a Content-Disposition header field where the
/// disposition type is `form-data`. The Content-Disposition header field MUST also contain an /// disposition type is "form-data". The Content-Disposition header field MUST also contain an
/// additional parameter of `name`; the value of the `name` parameter is the original field name /// additional parameter of "name"; the value of the "name" parameter is the original field name
/// from the form." /// from the form.'
/// ///
/// This crate validates that it exists before returning a `Field`. As such, it is safe to /// This crate validates that it exists before returning a `Field`. As such, it is safe to
/// unwrap `.content_disposition().get_name()`. The [name](Self::name) method is provided as /// unwrap `.content_disposition().get_name()`. The [name](Self::name) method is provided as
@@ -451,8 +451,7 @@ impl Field {
/// Returns the field's name. /// Returns the field's name.
/// ///
/// See [content_disposition](Self::content_disposition) regarding guarantees about existence of /// See [content_disposition] regarding guarantees about
/// the name field.
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
self.content_disposition() self.content_disposition()
.get_name() .get_name()
@@ -707,11 +706,8 @@ impl Clone for PayloadRef {
} }
} }
/// Counter. It tracks of number of clones of payloads and give access to payload only to top most. /// Counter. It tracks of number of clones of payloads and give access to payload only to top most
/// * When dropped, parent task is awakened. This is to support the case where Field is /// task panics if Safety get destroyed and it not top most task.
/// dropped in a separate task than Multipart.
/// * Assumes that parent owners don't move to different tasks; only the top-most is allowed to.
/// * If dropped and is not top most owner, is_clean flag is set to false.
#[derive(Debug)] #[derive(Debug)]
struct Safety { struct Safety {
task: LocalWaker, task: LocalWaker,
@@ -754,9 +750,9 @@ impl Safety {
impl Drop for Safety { impl Drop for Safety {
fn drop(&mut self) { fn drop(&mut self) {
// parent task is dead
if Rc::strong_count(&self.payload) != self.level { if Rc::strong_count(&self.payload) != self.level {
// Multipart dropped leaving a Field self.clean.set(true);
self.clean.set(false);
} }
self.task.wake(); self.task.wake();
@@ -857,12 +853,10 @@ mod tests {
use actix_http::h1::Payload; use actix_http::h1::Payload;
use actix_web::http::header::{DispositionParam, DispositionType}; use actix_web::http::header::{DispositionParam, DispositionType};
use actix_web::rt;
use actix_web::test::TestRequest; use actix_web::test::TestRequest;
use actix_web::FromRequest; use actix_web::FromRequest;
use bytes::Bytes; use bytes::Bytes;
use futures_util::{future::lazy, StreamExt}; use futures_util::{future::lazy, StreamExt};
use std::time::Duration;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
@@ -1292,44 +1286,4 @@ mod tests {
MultipartError::NoContentDisposition, MultipartError::NoContentDisposition,
)); ));
} }
#[actix_rt::test]
async fn test_drop_multipart_dont_hang() {
let (sender, payload) = create_stream();
let (bytes, headers) = create_simple_request_with_header();
sender.send(Ok(bytes)).unwrap();
drop(sender); // eof
let mut multipart = Multipart::new(&headers, payload);
let mut field = multipart.next().await.unwrap().unwrap();
drop(multipart);
// should fail immediately
match field.next().await {
Some(Err(MultipartError::NotConsumed)) => {}
_ => panic!(),
};
}
#[actix_rt::test]
async fn test_drop_field_awaken_multipart() {
let (sender, payload) = create_stream();
let (bytes, headers) = create_simple_request_with_header();
sender.send(Ok(bytes)).unwrap();
drop(sender); // eof
let mut multipart = Multipart::new(&headers, payload);
let mut field = multipart.next().await.unwrap().unwrap();
let task = rt::spawn(async move {
rt::time::sleep(Duration::from_secs(1)).await;
assert_eq!(field.next().await.unwrap().unwrap(), "test");
drop(field);
});
// dropping field should awaken current task
let _ = multipart.next().await.unwrap().unwrap();
task.await.unwrap();
}
} }

View File

@@ -29,7 +29,7 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-http = "3.0.0-beta.14" actix-http = "3.0.0-beta.13"
actix-http-test = "3.0.0-beta.7" actix-http-test = "3.0.0-beta.7"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"

View File

@@ -31,15 +31,17 @@ 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, thread, time::Duration}; use std::{error::Error as StdError, fmt, net, thread, time::Duration};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
pub use actix_http::test::TestBuffer; pub use actix_http::test::TestBuffer;
use actix_http::{header::HeaderMap, ws, HttpService, Method, Request, Response}; use actix_http::{
http::{HeaderMap, Method},
ws, HttpService, Request, Response,
};
use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _}; use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _};
use actix_web::{ use actix_web::{
body::MessageBody, dev::{AppConfig, MessageBody, Server, ServerHandle, Service},
dev::{AppConfig, Server, ServerHandle, Service},
rt::{self, System}, rt::{self, System},
web, Error, web, Error,
}; };
@@ -86,6 +88,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 +128,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>>,
{ {
// for sending handles and server info back from the spawned thread // for sending handles and server info back from the spawned thread
let (started_tx, started_rx) = std::sync::mpsc::channel(); let (started_tx, started_rx) = std::sync::mpsc::channel();

View File

@@ -1,12 +1,8 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
* Add `ws:WsResponseBuilder` for building WebSocket session response. [#1920]
* Deprecate `ws::{start_with_addr, start_with_protocols}`. [#1920]
* Minimum supported Rust version (MSRV) is now 1.52. * Minimum supported Rust version (MSRV) is now 1.52.
[#1920]: https://github.com/actix/actix-web/pull/1920
## 4.0.0-beta.7 - 2021-09-09 ## 4.0.0-beta.7 - 2021-09-09
* Minimum supported Rust version (MSRV) is now 1.51. * Minimum supported Rust version (MSRV) is now 1.51.

View File

@@ -16,13 +16,13 @@ path = "src/lib.rs"
[dependencies] [dependencies]
actix = { version = "0.12.0", default-features = false } actix = { version = "0.12.0", default-features = false }
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-http = "3.0.0-beta.14" actix-http = "3.0.0-beta.13"
actix-web = { version = "4.0.0-beta.11", default-features = false } actix-web = { version = "4.0.0-beta.11", default-features = false }
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"
futures-core = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.7", default-features = false }
pin-project-lite = "0.2" pin-project = "1.0.0"
tokio = { version = "1", features = ["sync"] } tokio = { version = "1", features = ["sync"] }
[dev-dependencies] [dev-dependencies]

View File

@@ -1,222 +1,38 @@
//! Websocket integration. //! Websocket integration.
use std::{ use std::future::Future;
collections::VecDeque, use std::io;
convert::TryFrom, use std::pin::Pin;
future::Future, use std::task::{Context, Poll};
io, mem, use std::{collections::VecDeque, convert::TryFrom};
pin::Pin,
task::{Context, Poll},
};
use actix::dev::{
AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, ToEnvelope,
};
use actix::fut::ActorFuture;
use actix::{ use actix::{
dev::{
AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler,
ToEnvelope,
},
fut::ActorFuture,
Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message as ActixMessage, Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message as ActixMessage,
SpawnHandle, SpawnHandle,
}; };
use actix_codec::{Decoder as _, Encoder as _}; use actix_codec::{Decoder, Encoder};
use actix_http::ws::{hash_key, Codec};
pub use actix_http::ws::{ pub use actix_http::ws::{
CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError, CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError,
}; };
use actix_http::{
http::HeaderValue,
ws::{hash_key, Codec},
};
use actix_web::{ use actix_web::{
error::{Error, PayloadError}, error::{Error, PayloadError},
http::{ http::{header, Method, StatusCode},
header::{self, HeaderValue},
Method, StatusCode,
},
HttpRequest, HttpResponse, HttpResponseBuilder, 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;
use pin_project_lite::pin_project; use tokio::sync::oneshot::Sender;
use tokio::sync::oneshot;
/// Builder for Websocket session response.
///
/// # Examples
///
/// Create a Websocket session response with default configuration.
/// ```ignore
/// WsResponseBuilder::new(WsActor, &req, stream).start()
/// ```
///
/// Create a Websocket session with a specific max frame size, [`Codec`], and protocols.
/// ```ignore
/// const MAX_FRAME_SIZE: usize = 16_384; // 16KiB
///
/// ws::WsResponseBuilder::new(WsActor, &req, stream)
/// .codec(Codec::new())
/// .protocols(&["A", "B"])
/// .frame_size(MAX_FRAME_SIZE)
/// .start()
/// ```
pub struct WsResponseBuilder<'a, A, T>
where
A: Actor<Context = WebsocketContext<A>> + StreamHandler<Result<Message, ProtocolError>>,
T: Stream<Item = Result<Bytes, PayloadError>> + 'static,
{
actor: A,
req: &'a HttpRequest,
stream: T,
codec: Option<Codec>,
protocols: Option<&'a [&'a str]>,
frame_size: Option<usize>,
}
impl<'a, A, T> WsResponseBuilder<'a, A, T>
where
A: Actor<Context = WebsocketContext<A>> + StreamHandler<Result<Message, ProtocolError>>,
T: Stream<Item = Result<Bytes, PayloadError>> + 'static,
{
/// Construct a new `WsResponseBuilder` with actor, request, and payload stream.
///
/// For usage example, see docs on [`WsResponseBuilder`] struct.
pub fn new(actor: A, req: &'a HttpRequest, stream: T) -> Self {
WsResponseBuilder {
actor,
req,
stream,
codec: None,
protocols: None,
frame_size: None,
}
}
/// Set the protocols for the session.
pub fn protocols(mut self, protocols: &'a [&'a str]) -> Self {
self.protocols = Some(protocols);
self
}
/// Set the max frame size for each message (in bytes).
///
/// **Note**: This will override any given [`Codec`]'s max frame size.
pub fn frame_size(mut self, frame_size: usize) -> Self {
self.frame_size = Some(frame_size);
self
}
/// Set the [`Codec`] for the session. If [`Self::frame_size`] is also set, the given
/// [`Codec`]'s max frame size will be overridden.
pub fn codec(mut self, codec: Codec) -> Self {
self.codec = Some(codec);
self
}
fn handshake_resp(&self) -> Result<HttpResponseBuilder, HandshakeError> {
match self.protocols {
Some(protocols) => handshake_with_protocols(self.req, protocols),
None => handshake(self.req),
}
}
fn set_frame_size(&mut self) {
if let Some(frame_size) = self.frame_size {
match &mut self.codec {
Some(codec) => {
// modify existing codec's max frame size
let orig_codec = mem::take(codec);
*codec = orig_codec.max_size(frame_size);
}
None => {
// create a new codec with the given size
self.codec = Some(Codec::new().max_size(frame_size));
}
}
}
}
/// Create a new Websocket context from an actor, request stream, and codec.
///
/// Returns a pair, where the first item is an addr for the created actor, and the second item
/// is a stream intended to be set as part of the response
/// via [`HttpResponseBuilder::streaming()`].
fn create_with_codec_addr<S>(
actor: A,
stream: S,
codec: Codec,
) -> (Addr<A>, impl Stream<Item = Result<Bytes, Error>>)
where
A: StreamHandler<Result<Message, ProtocolError>>,
S: Stream<Item = Result<Bytes, PayloadError>> + 'static,
{
let mb = Mailbox::default();
let mut ctx = WebsocketContext {
inner: ContextParts::new(mb.sender_producer()),
messages: VecDeque::new(),
};
ctx.add_stream(WsStream::new(stream, codec.clone()));
let addr = ctx.address();
(addr, WebsocketContextFut::new(ctx, actor, mb, codec))
}
/// Perform WebSocket handshake and start actor. /// Perform WebSocket handshake and start actor.
///
/// `req` is an [`HttpRequest`] that should be requesting a websocket protocol change.
/// `stream` should be a [`Bytes`] stream (such as `actix_web::web::Payload`) that contains a
/// stream of the body request.
///
/// If there is a problem with the handshake, an error is returned.
///
/// If successful, consume the [`WsResponseBuilder`] and return a [`HttpResponse`] wrapped in
/// a [`Result`].
pub fn start(mut self) -> Result<HttpResponse, Error> {
let mut res = self.handshake_resp()?;
self.set_frame_size();
match self.codec {
Some(codec) => {
let out_stream = WebsocketContext::with_codec(self.actor, self.stream, codec);
Ok(res.streaming(out_stream))
}
None => {
let out_stream = WebsocketContext::create(self.actor, self.stream);
Ok(res.streaming(out_stream))
}
}
}
/// Perform WebSocket handshake and start actor.
///
/// `req` is an [`HttpRequest`] that should be requesting a websocket protocol change.
/// `stream` should be a [`Bytes`] stream (such as `actix_web::web::Payload`) that contains a
/// stream of the body request.
///
/// If there is a problem with the handshake, an error is returned.
///
/// If successful, returns a pair where the first item is an address for the created actor and
/// the second item is the [`HttpResponse`] that should be returned from the websocket request.
pub fn start_with_addr(mut self) -> Result<(Addr<A>, HttpResponse), Error> {
let mut res = self.handshake_resp()?;
self.set_frame_size();
match self.codec {
Some(codec) => {
let (addr, out_stream) =
Self::create_with_codec_addr(self.actor, self.stream, codec);
Ok((addr, res.streaming(out_stream)))
}
None => {
let (addr, out_stream) =
WebsocketContext::create_with_addr(self.actor, self.stream);
Ok((addr, res.streaming(out_stream)))
}
}
}
}
/// Perform WebSocket handshake and start actor.
///
/// To customize options, see [`WsResponseBuilder`].
pub fn start<A, T>(actor: A, req: &HttpRequest, stream: T) -> Result<HttpResponse, Error> pub fn start<A, T>(actor: A, req: &HttpRequest, stream: T) -> Result<HttpResponse, Error>
where where
A: Actor<Context = WebsocketContext<A>> + StreamHandler<Result<Message, ProtocolError>>, A: Actor<Context = WebsocketContext<A>> + StreamHandler<Result<Message, ProtocolError>>,
@@ -228,15 +44,15 @@ where
/// Perform WebSocket handshake and start actor. /// Perform WebSocket handshake and start actor.
/// ///
/// `req` is an HTTP Request that should be requesting a websocket protocol change. `stream` should /// `req` is an HTTP Request that should be requesting a websocket protocol
/// be a `Bytes` stream (such as `actix_web::web::Payload`) that contains a stream of the /// change. `stream` should be a `Bytes` stream (such as
/// body request. /// `actix_web::web::Payload`) that contains a stream of the body request.
/// ///
/// If there is a problem with the handshake, an error is returned. /// If there is a problem with the handshake, an error is returned.
/// ///
/// If successful, returns a pair where the first item is an address for the created actor and the /// If successful, returns a pair where the first item is an address for the
/// second item is the response that should be returned from the WebSocket request. /// created actor and the second item is the response that should be returned
#[deprecated(since = "4.0.0", note = "Prefer `WsResponseBuilder::start_with_addr`.")] /// from the WebSocket request.
pub fn start_with_addr<A, T>( pub fn start_with_addr<A, T>(
actor: A, actor: A,
req: &HttpRequest, req: &HttpRequest,
@@ -254,10 +70,6 @@ where
/// Do WebSocket handshake and start ws actor. /// Do WebSocket handshake and start ws actor.
/// ///
/// `protocols` is a sequence of known protocols. /// `protocols` is a sequence of known protocols.
#[deprecated(
since = "4.0.0",
note = "Prefer `WsResponseBuilder` for setting protocols."
)]
pub fn start_with_protocols<A, T>( pub fn start_with_protocols<A, T>(
actor: A, actor: A,
protocols: &[&str], protocols: &[&str],
@@ -274,19 +86,20 @@ where
/// Prepare WebSocket handshake response. /// Prepare WebSocket handshake response.
/// ///
/// This function returns handshake `HttpResponse`, ready to send to peer. It does not perform /// This function returns handshake `HttpResponse`, ready to send to peer.
/// any IO. /// It does not perform any IO.
pub fn handshake(req: &HttpRequest) -> Result<HttpResponseBuilder, HandshakeError> { pub fn handshake(req: &HttpRequest) -> Result<HttpResponseBuilder, HandshakeError> {
handshake_with_protocols(req, &[]) handshake_with_protocols(req, &[])
} }
/// Prepare WebSocket handshake response. /// Prepare WebSocket handshake response.
/// ///
/// This function returns handshake `HttpResponse`, ready to send to peer. It does not perform /// This function returns handshake `HttpResponse`, ready to send to peer.
/// any IO. /// It does not perform any IO.
/// ///
/// `protocols` is a sequence of known protocols. On successful handshake, the returned response /// `protocols` is a sequence of known protocols. On successful handshake,
/// headers contain the first protocol in this list which the server also knows. /// the returned response headers contain the first protocol in this list
/// which the server also knows.
pub fn handshake_with_protocols( pub fn handshake_with_protocols(
req: &HttpRequest, req: &HttpRequest,
protocols: &[&str], protocols: &[&str],
@@ -433,8 +246,8 @@ impl<A> WebsocketContext<A>
where where
A: Actor<Context = Self>, A: Actor<Context = Self>,
{ {
/// Create a new Websocket context from a request and an actor.
#[inline] #[inline]
/// Create a new Websocket context from a request and an actor
pub fn create<S>(actor: A, stream: S) -> impl Stream<Item = Result<Bytes, Error>> pub fn create<S>(actor: A, stream: S) -> impl Stream<Item = Result<Bytes, Error>>
where where
A: StreamHandler<Result<Message, ProtocolError>>, A: StreamHandler<Result<Message, ProtocolError>>,
@@ -444,11 +257,12 @@ where
stream stream
} }
#[inline]
/// Create a new Websocket context from a request and an actor. /// Create a new Websocket context from a request and an actor.
/// ///
/// Returns a pair, where the first item is an addr for the created actor, and the second item /// Returns a pair, where the first item is an addr for the created actor,
/// is a stream intended to be set as part of the response /// and the second item is a stream intended to be set as part of the
/// via [`HttpResponseBuilder::streaming()`]. /// response via `HttpResponseBuilder::streaming()`.
pub fn create_with_addr<S>( pub fn create_with_addr<S>(
actor: A, actor: A,
stream: S, stream: S,
@@ -469,6 +283,7 @@ where
(addr, WebsocketContextFut::new(ctx, actor, mb, Codec::new())) (addr, WebsocketContextFut::new(ctx, actor, mb, Codec::new()))
} }
#[inline]
/// Create a new Websocket context from a request, an actor, and a codec /// Create a new Websocket context from a request, an actor, and a codec
pub fn with_codec<S>( pub fn with_codec<S>(
actor: A, actor: A,
@@ -484,7 +299,7 @@ where
inner: ContextParts::new(mb.sender_producer()), inner: ContextParts::new(mb.sender_producer()),
messages: VecDeque::new(), messages: VecDeque::new(),
}; };
ctx.add_stream(WsStream::new(stream, codec.clone())); ctx.add_stream(WsStream::new(stream, codec));
WebsocketContextFut::new(ctx, actor, mb, codec) WebsocketContextFut::new(ctx, actor, mb, codec)
} }
@@ -642,13 +457,12 @@ where
M: ActixMessage + Send + 'static, M: ActixMessage + Send + 'static,
M::Result: Send, M::Result: Send,
{ {
fn pack(msg: M, tx: Option<oneshot::Sender<M::Result>>) -> Envelope<A> { fn pack(msg: M, tx: Option<Sender<M::Result>>) -> Envelope<A> {
Envelope::new(msg, tx) Envelope::new(msg, tx)
} }
} }
pin_project! { #[pin_project::pin_project]
#[derive(Debug)]
struct WsStream<S> { struct WsStream<S> {
#[pin] #[pin]
stream: S, stream: S,
@@ -656,7 +470,6 @@ pin_project! {
buf: BytesMut, buf: BytesMut,
closed: bool, closed: bool,
} }
}
impl<S> WsStream<S> impl<S> WsStream<S>
where where
@@ -734,12 +547,9 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use actix_web::{
http::{header, Method},
test::TestRequest,
};
use super::*; use super::*;
use actix_web::http::{header, Method};
use actix_web::test::TestRequest;
#[test] #[test]
fn test_handshake() { fn test_handshake() {

View File

@@ -1,9 +1,11 @@
use actix::prelude::*; use actix::prelude::*;
use actix_http::ws::Codec; use actix_web::{
use actix_web::{web, App, HttpRequest}; http::{header, StatusCode},
use actix_web_actors::ws; web, App, HttpRequest, HttpResponse,
};
use actix_web_actors::*;
use bytes::Bytes; use bytes::Bytes;
use futures_util::{SinkExt, StreamExt}; use futures_util::{SinkExt as _, StreamExt as _};
struct Ws; struct Ws;
@@ -13,34 +15,37 @@ impl Actor for Ws {
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for Ws { impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for Ws {
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) { fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
match msg { match msg.unwrap() {
Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), ws::Message::Ping(msg) => ctx.pong(&msg),
Ok(ws::Message::Text(text)) => ctx.text(text), ws::Message::Text(text) => ctx.text(text),
Ok(ws::Message::Binary(bin)) => ctx.binary(bin), ws::Message::Binary(bin) => ctx.binary(bin),
Ok(ws::Message::Close(reason)) => ctx.close(reason), ws::Message::Close(reason) => ctx.close(reason),
_ => ctx.close(Some(ws::CloseCode::Error.into())), _ => {}
} }
} }
} }
const MAX_FRAME_SIZE: usize = 10_000; #[actix_rt::test]
const DEFAULT_FRAME_SIZE: usize = 10; async fn test_simple() {
let mut srv = actix_test::start(|| {
App::new().service(web::resource("/").to(
|req: HttpRequest, stream: web::Payload| async move { ws::start(Ws, &req, stream) },
))
});
async fn common_test_code(mut srv: actix_test::TestServer, frame_size: usize) {
// 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(ws::Message::Text("text".into())).await.unwrap();
let item = framed.next().await.unwrap().unwrap(); let item = framed.next().await.unwrap().unwrap();
assert_eq!(item, ws::Frame::Text(Bytes::from_static(b"text"))); assert_eq!(item, ws::Frame::Text(Bytes::from_static(b"text")));
let bytes = Bytes::from(vec![0; frame_size]);
framed framed
.send(ws::Message::Binary(bytes.clone())) .send(ws::Message::Binary("text".into()))
.await .await
.unwrap(); .unwrap();
let item = framed.next().await.unwrap().unwrap(); let item = framed.next().await.unwrap().unwrap();
assert_eq!(item, ws::Frame::Binary(bytes)); assert_eq!(item, ws::Frame::Binary(Bytes::from_static(b"text")));
framed.send(ws::Message::Ping("text".into())).await.unwrap(); framed.send(ws::Message::Ping("text".into())).await.unwrap();
let item = framed.next().await.unwrap().unwrap(); let item = framed.next().await.unwrap().unwrap();
@@ -50,137 +55,55 @@ async fn common_test_code(mut srv: actix_test::TestServer, frame_size: usize) {
.send(ws::Message::Close(Some(ws::CloseCode::Normal.into()))) .send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))
.await .await
.unwrap(); .unwrap();
let item = framed.next().await.unwrap().unwrap(); let item = framed.next().await.unwrap().unwrap();
assert_eq!(item, ws::Frame::Close(Some(ws::CloseCode::Normal.into()))); assert_eq!(item, ws::Frame::Close(Some(ws::CloseCode::Normal.into())));
} }
#[actix_rt::test] #[actix_rt::test]
async fn simple_builder() { async fn test_with_credentials() {
let srv = actix_test::start(|| {
App::new().service(web::resource("/").to(
|req: HttpRequest, stream: web::Payload| async move {
ws::WsResponseBuilder::new(Ws, &req, stream).start()
},
))
});
common_test_code(srv, DEFAULT_FRAME_SIZE).await;
}
#[actix_rt::test]
async fn builder_with_frame_size() {
let srv = actix_test::start(|| {
App::new().service(web::resource("/").to(
|req: HttpRequest, stream: web::Payload| async move {
ws::WsResponseBuilder::new(Ws, &req, stream)
.frame_size(MAX_FRAME_SIZE)
.start()
},
))
});
common_test_code(srv, MAX_FRAME_SIZE).await;
}
#[actix_rt::test]
async fn builder_with_frame_size_exceeded() {
const MAX_FRAME_SIZE: usize = 64;
let mut srv = actix_test::start(|| { let mut srv = actix_test::start(|| {
App::new().service(web::resource("/").to( App::new().service(web::resource("/").to(
|req: HttpRequest, stream: web::Payload| async move { |req: HttpRequest, stream: web::Payload| async move {
ws::WsResponseBuilder::new(Ws, &req, stream) if req.headers().contains_key("Authorization") {
.frame_size(MAX_FRAME_SIZE) ws::start(Ws, &req, stream)
.start() } else {
Ok(HttpResponse::new(StatusCode::UNAUTHORIZED))
}
}, },
)) ))
}); });
// client service // client service without credentials
let mut framed = srv.ws().await.unwrap(); match srv.ws().await {
Ok(_) => panic!("WebSocket client without credentials should panic"),
// create a request with a frame size larger than expected Err(awc::error::WsClientError::InvalidResponseStatus(status)) => {
let bytes = Bytes::from(vec![0; MAX_FRAME_SIZE + 1]); assert_eq!(status, StatusCode::UNAUTHORIZED)
framed.send(ws::Message::Binary(bytes)).await.unwrap(); }
Err(e) => panic!("Invalid error from WebSocket client: {}", e),
let frame = framed.next().await.unwrap().unwrap();
let close_reason = match frame {
ws::Frame::Close(Some(reason)) => reason,
_ => panic!("close frame expected"),
};
assert_eq!(close_reason.code, ws::CloseCode::Error);
} }
#[actix_rt::test] let headers = srv.client_headers().unwrap();
async fn builder_with_codec() { headers.insert(
let srv = actix_test::start(|| { header::AUTHORIZATION,
App::new().service(web::resource("/").to( header::HeaderValue::from_static("Bearer Something"),
|req: HttpRequest, stream: web::Payload| async move { );
ws::WsResponseBuilder::new(Ws, &req, stream)
.codec(Codec::new())
.start()
},
))
});
common_test_code(srv, DEFAULT_FRAME_SIZE).await; // client service with credentials
} let client = srv.ws();
#[actix_rt::test] let mut framed = client.await.unwrap();
async fn builder_with_protocols() {
let srv = actix_test::start(|| { framed.send(ws::Message::Text("text".into())).await.unwrap();
App::new().service(web::resource("/").to(
|req: HttpRequest, stream: web::Payload| async move { let item = framed.next().await.unwrap().unwrap();
ws::WsResponseBuilder::new(Ws, &req, stream) assert_eq!(item, ws::Frame::Text(Bytes::from_static(b"text")));
.protocols(&["A", "B"])
.start() framed
}, .send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))
)) .await
}); .unwrap();
common_test_code(srv, DEFAULT_FRAME_SIZE).await; let item = framed.next().await.unwrap().unwrap();
} assert_eq!(item, ws::Frame::Close(Some(ws::CloseCode::Normal.into())));
#[actix_rt::test]
async fn builder_with_codec_and_frame_size() {
let srv = actix_test::start(|| {
App::new().service(web::resource("/").to(
|req: HttpRequest, stream: web::Payload| async move {
ws::WsResponseBuilder::new(Ws, &req, stream)
.codec(Codec::new())
.frame_size(MAX_FRAME_SIZE)
.start()
},
))
});
common_test_code(srv, DEFAULT_FRAME_SIZE).await;
}
#[actix_rt::test]
async fn builder_full() {
let srv = actix_test::start(|| {
App::new().service(web::resource("/").to(
|req: HttpRequest, stream: web::Payload| async move {
ws::WsResponseBuilder::new(Ws, &req, stream)
.frame_size(MAX_FRAME_SIZE)
.codec(Codec::new())
.protocols(&["A", "B"])
.start()
},
))
});
common_test_code(srv, MAX_FRAME_SIZE).await;
}
#[actix_rt::test]
async fn simple_start() {
let srv = actix_test::start(|| {
App::new().service(web::resource("/").to(
|req: HttpRequest, stream: web::Payload| async move { ws::start(Ws, &req, stream) },
))
});
common_test_code(srv, DEFAULT_FRAME_SIZE).await;
} }

View File

@@ -66,7 +66,7 @@ mod route;
/// Creates resource handler, allowing multiple HTTP method guards. /// Creates resource handler, allowing multiple HTTP method guards.
/// ///
/// # Syntax /// # Syntax
/// ```plain /// ```text
/// #[route("path", method="HTTP_METHOD"[, attributes])] /// #[route("path", method="HTTP_METHOD"[, attributes])]
/// ``` /// ```
/// ///
@@ -112,7 +112,7 @@ concat!("
Creates route handler with `actix_web::guard::", stringify!($variant), "`. Creates route handler with `actix_web::guard::", stringify!($variant), "`.
# Syntax # Syntax
```plain ```text
#[", stringify!($method), r#"("path"[, attributes])] #[", stringify!($method), r#"("path"[, attributes])]
``` ```

View File

@@ -3,12 +3,6 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.12 - 2021-11-30
* Update `actix-tls` to `3.0.0-rc.1`. [#2474]
[#2474]: https://github.com/actix/actix-web/pull/2474
## 3.0.0-beta.11 - 2021-11-22 ## 3.0.0-beta.11 - 2021-11-22
* No significant changes from `3.0.0-beta.10`. * No significant changes from `3.0.0-beta.10`.

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "awc" name = "awc"
version = "3.0.0-beta.12" version = "3.0.0-beta.11"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>", "fakeshadow <24548779@qq.com>",
@@ -60,9 +60,9 @@ dangerous-h2c = []
[dependencies] [dependencies]
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-http = "3.0.0-beta.14" actix-http = "3.0.0-beta.13"
actix-rt = { version = "2.1", default-features = false } actix-rt = { version = "2.1", default-features = false }
actix-tls = { version = "3.0.0-rc.1", features = ["connect", "uri"] } actix-tls = { version = "3.0.0-beta.9", features = ["connect"] }
actix-utils = "3.0.0" actix-utils = "3.0.0"
ahash = "0.7" ahash = "0.7"
@@ -94,18 +94,17 @@ trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0-beta.11", features = ["openssl"] } actix-web = { version = "4.0.0-beta.11", features = ["openssl"] }
actix-http = { version = "3.0.0-beta.14", features = ["openssl"] } actix-http = { version = "3.0.0-beta.13", features = ["openssl"] }
actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] }
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-server = "2.0.0-rc.1" actix-server = "2.0.0-beta.9"
actix-tls = { version = "3.0.0-rc.1", features = ["openssl", "rustls"] } actix-tls = { version = "3.0.0-beta.9", features = ["openssl", "rustls"] }
actix-test = { version = "0.1.0-beta.7", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.7", features = ["openssl", "rustls"] }
brotli2 = "0.3.2" brotli2 = "0.3.2"
env_logger = "0.9" env_logger = "0.9"
flate2 = "1.0.13" flate2 = "1.0.13"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }
static_assertions = "1.1"
rcgen = "0.8" rcgen = "0.8"
rustls-pemfile = "0.2" rustls-pemfile = "0.2"

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.12)](https://docs.rs/awc/3.0.0-beta.12) [![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.11)](https://docs.rs/awc/3.0.0-beta.11)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc)
[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.12/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.12) [![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.11/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.11)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources ## Documentation & Resources

View File

@@ -1,266 +0,0 @@
use std::{
borrow::Cow,
fmt, mem,
pin::Pin,
task::{Context, Poll},
};
use bytes::{Bytes, BytesMut};
use futures_core::Stream;
use pin_project_lite::pin_project;
use actix_http::body::{BodySize, BodyStream, BoxBody, MessageBody, SizedStream};
use crate::BoxError;
pin_project! {
/// Represents various types of HTTP message body.
#[derive(Clone)]
#[project = AnyBodyProj]
pub enum AnyBody<B = BoxBody> {
/// Empty response. `Content-Length` header is not set.
None,
/// Complete, in-memory response body.
Bytes { body: Bytes },
/// Generic / Other message body.
Body { #[pin] body: B },
}
}
impl AnyBody {
/// Constructs a "body" representing an empty response.
pub fn none() -> Self {
Self::None
}
/// Constructs a new, 0-length body.
pub fn empty() -> Self {
Self::Bytes { body: Bytes::new() }
}
/// Create boxed body from generic message body.
pub fn new_boxed<B>(body: B) -> Self
where
B: MessageBody + 'static,
{
Self::Body {
body: BoxBody::new(body),
}
}
/// Constructs new `AnyBody` instance from a slice of bytes by copying it.
///
/// If your bytes container is owned, it may be cheaper to use a `From` impl.
pub fn copy_from_slice(s: &[u8]) -> Self {
Self::Bytes {
body: Bytes::copy_from_slice(s),
}
}
#[doc(hidden)]
#[deprecated(since = "4.0.0", note = "Renamed to `copy_from_slice`.")]
pub fn from_slice(s: &[u8]) -> Self {
Self::Bytes {
body: Bytes::copy_from_slice(s),
}
}
}
impl<B> AnyBody<B> {
/// Create body from generic message body.
pub fn new(body: B) -> Self {
Self::Body { body }
}
}
impl<B> AnyBody<B>
where
B: MessageBody + 'static,
{
pub fn into_boxed(self) -> AnyBody {
match self {
Self::None => AnyBody::None,
Self::Bytes { body: bytes } => AnyBody::Bytes { body: bytes },
Self::Body { body } => AnyBody::new_boxed(body),
}
}
}
impl<B> MessageBody for AnyBody<B>
where
B: MessageBody,
{
type Error = crate::BoxError;
fn size(&self) -> BodySize {
match self {
AnyBody::None => BodySize::None,
AnyBody::Bytes { ref body } => BodySize::Sized(body.len() as u64),
AnyBody::Body { ref body } => body.size(),
}
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
match self.project() {
AnyBodyProj::None => Poll::Ready(None),
AnyBodyProj::Bytes { body } => {
let len = body.len();
if len == 0 {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::take(body))))
}
}
AnyBodyProj::Body { body } => body.poll_next(cx).map_err(|err| err.into()),
}
}
}
impl PartialEq for AnyBody {
fn eq(&self, other: &AnyBody) -> bool {
match self {
AnyBody::None => matches!(*other, AnyBody::None),
AnyBody::Bytes { body } => match other {
AnyBody::Bytes { body: b2 } => body == b2,
_ => false,
},
AnyBody::Body { .. } => false,
}
}
}
impl<S: fmt::Debug> fmt::Debug for AnyBody<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
AnyBody::None => write!(f, "AnyBody::None"),
AnyBody::Bytes { ref body } => write!(f, "AnyBody::Bytes({:?})", body),
AnyBody::Body { ref body } => write!(f, "AnyBody::Message({:?})", body),
}
}
}
impl<B> From<&'static str> for AnyBody<B> {
fn from(string: &'static str) -> Self {
Self::Bytes {
body: Bytes::from_static(string.as_ref()),
}
}
}
impl<B> From<&'static [u8]> for AnyBody<B> {
fn from(bytes: &'static [u8]) -> Self {
Self::Bytes {
body: Bytes::from_static(bytes),
}
}
}
impl<B> From<Vec<u8>> for AnyBody<B> {
fn from(vec: Vec<u8>) -> Self {
Self::Bytes {
body: Bytes::from(vec),
}
}
}
impl<B> From<String> for AnyBody<B> {
fn from(string: String) -> Self {
Self::Bytes {
body: Bytes::from(string),
}
}
}
impl<B> From<&'_ String> for AnyBody<B> {
fn from(string: &String) -> Self {
Self::Bytes {
body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string)),
}
}
}
impl<B> From<Cow<'_, str>> for AnyBody<B> {
fn from(string: Cow<'_, str>) -> Self {
match string {
Cow::Owned(s) => Self::from(s),
Cow::Borrowed(s) => Self::Bytes {
body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)),
},
}
}
}
impl<B> From<Bytes> for AnyBody<B> {
fn from(bytes: Bytes) -> Self {
Self::Bytes { body: bytes }
}
}
impl<B> From<BytesMut> for AnyBody<B> {
fn from(bytes: BytesMut) -> Self {
Self::Bytes {
body: bytes.freeze(),
}
}
}
impl<S, E> From<SizedStream<S>> for AnyBody
where
S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<BoxError> + 'static,
{
fn from(stream: SizedStream<S>) -> Self {
AnyBody::new_boxed(stream)
}
}
impl<S, E> From<BodyStream<S>> for AnyBody
where
S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<BoxError> + 'static,
{
fn from(stream: BodyStream<S>) -> Self {
AnyBody::new_boxed(stream)
}
}
#[cfg(test)]
mod tests {
use std::marker::PhantomPinned;
use static_assertions::{assert_impl_all, assert_not_impl_all};
use super::*;
struct PinType(PhantomPinned);
impl MessageBody for PinType {
type Error = crate::BoxError;
fn size(&self) -> BodySize {
unimplemented!()
}
fn poll_next(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
unimplemented!()
}
}
assert_impl_all!(AnyBody<()>: MessageBody, fmt::Debug, Send, Sync, Unpin);
assert_impl_all!(AnyBody<AnyBody<()>>: MessageBody, fmt::Debug, Send, Sync, Unpin);
assert_impl_all!(AnyBody<Bytes>: MessageBody, fmt::Debug, Send, Sync, Unpin);
assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Unpin);
assert_impl_all!(AnyBody<PinType>: MessageBody);
assert_not_impl_all!(AnyBody: Send, Sync, Unpin);
assert_not_impl_all!(AnyBody<PinType>: Send, Sync, Unpin);
}

View File

@@ -1,15 +1,11 @@
use std::{convert::TryFrom, fmt, net::IpAddr, rc::Rc, time::Duration}; use std::{convert::TryFrom, fmt, net::IpAddr, rc::Rc, time::Duration};
use actix_http::{ use actix_http::http::{self, header, Error as HttpError, HeaderMap, HeaderName, Uri};
error::HttpError,
header::{self, HeaderMap, HeaderName},
Uri,
};
use actix_rt::net::{ActixStream, TcpStream}; use actix_rt::net::{ActixStream, TcpStream};
use actix_service::{boxed, Service}; use actix_service::{boxed, Service};
use crate::{ use crate::{
client::{ConnectInfo, Connector, ConnectorService, TcpConnectError, TcpConnection}, client::{Connector, ConnectorService, TcpConnect, TcpConnectError, TcpConnection},
connect::DefaultConnector, connect::DefaultConnector,
error::SendRequestError, error::SendRequestError,
middleware::{NestTransform, Redirect, Transform}, middleware::{NestTransform, Redirect, Transform},
@@ -37,7 +33,7 @@ impl ClientBuilder {
#[allow(clippy::new_ret_no_self)] #[allow(clippy::new_ret_no_self)]
pub fn new() -> ClientBuilder< pub fn new() -> ClientBuilder<
impl Service< impl Service<
ConnectInfo<Uri>, TcpConnect<Uri>,
Response = TcpConnection<Uri, TcpStream>, Response = TcpConnection<Uri, TcpStream>,
Error = TcpConnectError, Error = TcpConnectError,
> + Clone, > + Clone,
@@ -60,7 +56,7 @@ impl ClientBuilder {
impl<S, Io, M> ClientBuilder<S, M> impl<S, Io, M> ClientBuilder<S, M>
where where
S: Service<ConnectInfo<Uri>, Response = TcpConnection<Uri, Io>, Error = TcpConnectError> S: Service<TcpConnect<Uri>, Response = TcpConnection<Uri, Io>, Error = TcpConnectError>
+ Clone + Clone
+ 'static, + 'static,
Io: ActixStream + fmt::Debug + 'static, Io: ActixStream + fmt::Debug + 'static,
@@ -69,7 +65,7 @@ where
pub fn connector<S1, Io1>(self, connector: Connector<S1>) -> ClientBuilder<S1, M> pub fn connector<S1, Io1>(self, connector: Connector<S1>) -> ClientBuilder<S1, M>
where where
S1: Service< S1: Service<
ConnectInfo<Uri>, TcpConnect<Uri>,
Response = TcpConnection<Uri, Io1>, Response = TcpConnection<Uri, Io1>,
Error = TcpConnectError, Error = TcpConnectError,
> + Clone > + Clone

View File

@@ -12,9 +12,9 @@ use bytes::Bytes;
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use h2::client::SendRequest; use h2::client::SendRequest;
use actix_http::{body::MessageBody, h1::ClientCodec, Payload, RequestHeadType, ResponseHead}; use actix_http::{
body::MessageBody, h1::ClientCodec, Error, Payload, RequestHeadType, ResponseHead,
use crate::BoxError; };
use super::error::SendRequestError; use super::error::SendRequestError;
use super::pool::Acquired; use super::pool::Acquired;
@@ -254,7 +254,7 @@ where
where where
H: Into<RequestHeadType> + 'static, H: Into<RequestHeadType> + 'static,
RB: MessageBody + 'static, RB: MessageBody + 'static,
RB::Error: Into<BoxError>, RB::Error: Into<Error>,
{ {
Box::pin(async move { Box::pin(async move {
match self { match self {

View File

@@ -15,8 +15,8 @@ use actix_rt::{
}; };
use actix_service::Service; use actix_service::Service;
use actix_tls::connect::{ use actix_tls::connect::{
ConnectError as TcpConnectError, ConnectInfo, Connection as TcpConnection, new_connector, Connect as TcpConnect, ConnectError as TcpConnectError,
Connector as TcpConnector, Resolver, Connection as TcpConnection, Resolver,
}; };
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
use http::Uri; use http::Uri;
@@ -28,15 +28,13 @@ use super::error::ConnectError;
use super::pool::ConnectionPool; use super::pool::ConnectionPool;
use super::Connect; use super::Connect;
enum OurTlsConnector { enum SslConnector {
#[allow(dead_code)] // only dead when no TLS feature is enabled #[allow(dead_code)]
None, None,
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
Openssl(actix_tls::connect::openssl::reexports::SslConnector), Openssl(actix_tls::connect::ssl::openssl::SslConnector),
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
Rustls(std::sync::Arc<actix_tls::connect::rustls::reexports::ClientConfig>), Rustls(std::sync::Arc<actix_tls::connect::ssl::rustls::ClientConfig>),
} }
/// Manages HTTP client network connectivity. /// Manages HTTP client network connectivity.
@@ -55,22 +53,21 @@ enum OurTlsConnector {
pub struct Connector<T> { pub struct Connector<T> {
connector: T, connector: T,
config: ConnectorConfig, config: ConnectorConfig,
#[allow(dead_code)]
#[allow(dead_code)] // only dead when no TLS feature is enabled ssl: SslConnector,
ssl: OurTlsConnector,
} }
impl Connector<()> { impl Connector<()> {
#[allow(clippy::new_ret_no_self, clippy::let_unit_value)] #[allow(clippy::new_ret_no_self, clippy::let_unit_value)]
pub fn new() -> Connector< pub fn new() -> Connector<
impl Service< impl Service<
ConnectInfo<Uri>, TcpConnect<Uri>,
Response = TcpConnection<Uri, TcpStream>, Response = TcpConnection<Uri, TcpStream>,
Error = actix_tls::connect::ConnectError, Error = actix_tls::connect::ConnectError,
> + Clone, > + Clone,
> { > {
Connector { Connector {
connector: TcpConnector::new(resolver::resolver()).service(), connector: new_connector(resolver::resolver()),
config: ConnectorConfig::default(), config: ConnectorConfig::default(),
ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]), ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]),
} }
@@ -78,16 +75,16 @@ impl Connector<()> {
/// Provides an empty TLS connector when no TLS feature is enabled. /// Provides an empty TLS connector when no TLS feature is enabled.
#[cfg(not(any(feature = "openssl", feature = "rustls")))] #[cfg(not(any(feature = "openssl", feature = "rustls")))]
fn build_ssl(_: Vec<Vec<u8>>) -> OurTlsConnector { fn build_ssl(_: Vec<Vec<u8>>) -> SslConnector {
OurTlsConnector::None SslConnector::None
} }
/// Build TLS connector with rustls, based on supplied ALPN protocols /// Build TLS connector with rustls, based on supplied ALPN protocols
/// ///
/// Note that if both `openssl` and `rustls` features are enabled, rustls will be used. /// Note that if both `openssl` and `rustls` features are enabled, rustls will be used.
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
fn build_ssl(protocols: Vec<Vec<u8>>) -> OurTlsConnector { fn build_ssl(protocols: Vec<Vec<u8>>) -> SslConnector {
use actix_tls::connect::rustls::{reexports::ClientConfig, webpki_roots_cert_store}; use actix_tls::connect::tls::rustls::{webpki_roots_cert_store, ClientConfig};
let mut config = ClientConfig::builder() let mut config = ClientConfig::builder()
.with_safe_defaults() .with_safe_defaults()
@@ -96,13 +93,13 @@ impl Connector<()> {
config.alpn_protocols = protocols; config.alpn_protocols = protocols;
OurTlsConnector::Rustls(std::sync::Arc::new(config)) SslConnector::Rustls(std::sync::Arc::new(config))
} }
/// Build TLS connector with openssl, based on supplied ALPN protocols /// Build TLS connector with openssl, based on supplied ALPN protocols
#[cfg(all(feature = "openssl", not(feature = "rustls")))] #[cfg(all(feature = "openssl", not(feature = "rustls")))]
fn build_ssl(protocols: Vec<Vec<u8>>) -> OurTlsConnector { fn build_ssl(protocols: Vec<Vec<u8>>) -> SslConnector {
use actix_tls::connect::openssl::reexports::{SslConnector, SslMethod}; use actix_tls::connect::tls::openssl::{SslConnector as OpensslConnector, SslMethod};
use bytes::{BufMut, BytesMut}; use bytes::{BufMut, BytesMut};
let mut alpn = BytesMut::with_capacity(20); let mut alpn = BytesMut::with_capacity(20);
@@ -111,12 +108,12 @@ impl Connector<()> {
alpn.put(proto.as_slice()); alpn.put(proto.as_slice());
} }
let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap(); let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap();
if let Err(err) = ssl.set_alpn_protos(&alpn) { if let Err(err) = ssl.set_alpn_protos(&alpn) {
log::error!("Can not set ALPN protocol: {:?}", err); log::error!("Can not set ALPN protocol: {:?}", err);
} }
OurTlsConnector::Openssl(ssl.build()) SslConnector::Openssl(ssl.build())
} }
} }
@@ -126,7 +123,7 @@ impl<S> Connector<S> {
where where
Io1: ActixStream + fmt::Debug + 'static, Io1: ActixStream + fmt::Debug + 'static,
S1: Service< S1: Service<
ConnectInfo<Uri>, TcpConnect<Uri>,
Response = TcpConnection<Uri, Io1>, Response = TcpConnection<Uri, Io1>,
Error = TcpConnectError, Error = TcpConnectError,
> + Clone, > + Clone,
@@ -139,7 +136,7 @@ impl<S> Connector<S> {
} }
} }
impl<S, IO> Connector<S> impl<S, Io> Connector<S>
where where
// Note: // Note:
// Input Io type is bound to ActixStream trait but internally in client module they // Input Io type is bound to ActixStream trait but internally in client module they
@@ -148,8 +145,8 @@ where
// //
// This remap is to hide ActixStream's trait methods. They are not meant to be called // This remap is to hide ActixStream's trait methods. They are not meant to be called
// from user code. // from user code.
IO: ActixStream + fmt::Debug + 'static, Io: ActixStream + fmt::Debug + 'static,
S: Service<ConnectInfo<Uri>, Response = TcpConnection<Uri, IO>, Error = TcpConnectError> S: Service<TcpConnect<Uri>, Response = TcpConnection<Uri, Io>, Error = TcpConnectError>
+ Clone + Clone
+ 'static, + 'static,
{ {
@@ -169,21 +166,18 @@ where
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
/// Use custom `SslConnector` instance. /// Use custom `SslConnector` instance.
pub fn ssl( pub fn ssl(mut self, connector: actix_tls::connect::ssl::openssl::SslConnector) -> Self {
mut self, self.ssl = SslConnector::Openssl(connector);
connector: actix_tls::connect::openssl::reexports::SslConnector,
) -> Self {
self.ssl = OurTlsConnector::Openssl(connector);
self self
} }
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
/// Use custom `ClientConfig` instance. /// Use custom `SslConnector` instance.
pub fn rustls( pub fn rustls(
mut self, mut self,
connector: std::sync::Arc<actix_tls::connect::rustls::reexports::ClientConfig>, connector: std::sync::Arc<actix_tls::connect::ssl::rustls::ClientConfig>,
) -> Self { ) -> Self {
self.ssl = OurTlsConnector::Rustls(connector); self.ssl = SslConnector::Rustls(connector);
self self
} }
@@ -272,7 +266,7 @@ where
/// Finish configuration process and create connector service. /// Finish configuration process and create connector service.
/// The Connector builder always concludes by calling `finish()` last in /// The Connector builder always concludes by calling `finish()` last in
/// its combinator chain. /// its combinator chain.
pub fn finish(self) -> ConnectorService<S, IO> { pub fn finish(self) -> ConnectorService<S, Io> {
let local_address = self.config.local_address; let local_address = self.config.local_address;
let timeout = self.config.timeout; let timeout = self.config.timeout;
@@ -285,18 +279,19 @@ where
}; };
let tls_service = match self.ssl { let tls_service = match self.ssl {
OurTlsConnector::None => { SslConnector::None => {
#[cfg(not(feature = "dangerous-h2c"))] #[cfg(not(feature = "dangerous-h2c"))]
{ {
None None
} }
#[cfg(feature = "dangerous-h2c")] #[cfg(feature = "dangerous-h2c")]
{ {
use std::io; use std::{
future::{ready, Ready},
io,
};
use actix_tls::connect::Connection; use actix_tls::connect::Connection;
use actix_utils::future::{ready, Ready};
impl IntoConnectionIo for TcpConnection<Uri, Box<dyn ConnectionIo>> { impl IntoConnectionIo for TcpConnection<Uri, Box<dyn ConnectionIo>> {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) { fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
@@ -312,17 +307,17 @@ where
#[derive(Clone)] #[derive(Clone)]
struct NoOpTlsConnectorService; struct NoOpTlsConnectorService;
impl<R, IO> Service<Connection<R, IO>> for NoOpTlsConnectorService impl<T, U> Service<Connection<T, U>> for NoOpTlsConnectorService
where where
IO: ActixStream + 'static, U: ActixStream + 'static,
{ {
type Response = Connection<R, Box<dyn ConnectionIo>>; type Response = Connection<T, Box<dyn ConnectionIo>>;
type Error = io::Error; type Error = io::Error;
type Future = Ready<Result<Self::Response, Self::Error>>; type Future = Ready<Result<Self::Response, Self::Error>>;
actix_service::always_ready!(); actix_service::always_ready!();
fn call(&self, connection: Connection<R, IO>) -> Self::Future { fn call(&self, connection: Connection<T, U>) -> Self::Future {
let (io, connection) = connection.replace_io(()); let (io, connection) = connection.replace_io(());
let (_, connection) = connection.replace_io(Box::new(io) as _); let (_, connection) = connection.replace_io(Box::new(io) as _);
@@ -341,14 +336,13 @@ where
Some(actix_service::boxed::rc_service(tls_service)) Some(actix_service::boxed::rc_service(tls_service))
} }
} }
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
OurTlsConnector::Openssl(tls) => { SslConnector::Openssl(tls) => {
const H2: &[u8] = b"h2"; const H2: &[u8] = b"h2";
use actix_tls::connect::openssl::{reexports::AsyncSslStream, TlsConnector}; use actix_tls::connect::ssl::openssl::{OpensslConnector, SslStream};
impl<IO: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, AsyncSslStream<IO>> { impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, SslStream<Io>> {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) { fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
let sock = self.into_parts().0; let sock = self.into_parts().0;
let h2 = sock let h2 = sock
@@ -367,20 +361,19 @@ where
let tls_service = TlsConnectorService { let tls_service = TlsConnectorService {
tcp_service: tcp_service_inner, tcp_service: tcp_service_inner,
tls_service: TlsConnector::service(tls), tls_service: OpensslConnector::service(tls),
timeout: handshake_timeout, timeout: handshake_timeout,
}; };
Some(actix_service::boxed::rc_service(tls_service)) Some(actix_service::boxed::rc_service(tls_service))
} }
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
OurTlsConnector::Rustls(tls) => { SslConnector::Rustls(tls) => {
const H2: &[u8] = b"h2"; const H2: &[u8] = b"h2";
use actix_tls::connect::rustls::{reexports::AsyncTlsStream, TlsConnector}; use actix_tls::connect::ssl::rustls::{RustlsConnector, TlsStream};
impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, AsyncTlsStream<Io>> { impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, TlsStream<Io>> {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) { fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
let sock = self.into_parts().0; let sock = self.into_parts().0;
let h2 = sock let h2 = sock
@@ -400,7 +393,7 @@ where
let tls_service = TlsConnectorService { let tls_service = TlsConnectorService {
tcp_service: tcp_service_inner, tcp_service: tcp_service_inner,
tls_service: TlsConnector::service(tls), tls_service: RustlsConnector::service(tls),
timeout: handshake_timeout, timeout: handshake_timeout,
}; };
@@ -469,28 +462,26 @@ where
/// service for establish tcp connection and do client tls handshake. /// service for establish tcp connection and do client tls handshake.
/// operation is canceled when timeout limit reached. /// operation is canceled when timeout limit reached.
struct TlsConnectorService<Tcp, Tls> { struct TlsConnectorService<S, St> {
/// TCP connection is canceled on `TcpConnectorInnerService`'s timeout setting. /// tcp connection is canceled on `TcpConnectorInnerService`'s timeout setting.
tcp_service: Tcp, tcp_service: S,
/// tls connection is canceled on `TlsConnectorService`'s timeout setting.
/// TLS connection is canceled on `TlsConnectorService`'s timeout setting. tls_service: St,
tls_service: Tls,
timeout: Duration, timeout: Duration,
} }
impl<Tcp, Tls, IO> Service<Connect> for TlsConnectorService<Tcp, Tls> impl<S, St, Io> Service<Connect> for TlsConnectorService<S, St>
where where
Tcp: Service<Connect, Response = TcpConnection<Uri, IO>, Error = ConnectError> S: Service<Connect, Response = TcpConnection<Uri, Io>, Error = ConnectError>
+ Clone + Clone
+ 'static, + 'static,
Tls: Service<TcpConnection<Uri, IO>, Error = std::io::Error> + Clone + 'static, St: Service<TcpConnection<Uri, Io>, Error = std::io::Error> + Clone + 'static,
Tls::Response: IntoConnectionIo, Io: ConnectionIo,
IO: ConnectionIo, St::Response: IntoConnectionIo,
{ {
type Response = (Box<dyn ConnectionIo>, Protocol); type Response = (Box<dyn ConnectionIo>, Protocol);
type Error = ConnectError; type Error = ConnectError;
type Future = TlsConnectorFuture<Tls, Tcp::Future, Tls::Future>; type Future = TlsConnectorFuture<St, S::Future, St::Future>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
ready!(self.tcp_service.poll_ready(cx))?; ready!(self.tcp_service.poll_ready(cx))?;
@@ -590,7 +581,7 @@ impl<S: Clone> TcpConnectorInnerService<S> {
impl<S, Io> Service<Connect> for TcpConnectorInnerService<S> impl<S, Io> Service<Connect> for TcpConnectorInnerService<S>
where where
S: Service<ConnectInfo<Uri>, Response = TcpConnection<Uri, Io>, Error = TcpConnectError> S: Service<TcpConnect<Uri>, Response = TcpConnection<Uri, Io>, Error = TcpConnectError>
+ Clone + Clone
+ 'static, + 'static,
{ {
@@ -601,7 +592,7 @@ where
actix_service::forward_ready!(service); actix_service::forward_ready!(service);
fn call(&self, req: Connect) -> Self::Future { fn call(&self, req: Connect) -> Self::Future {
let mut req = ConnectInfo::new(req.uri).set_addr(req.addr); let mut req = TcpConnect::new(req.uri).set_addr(req.addr);
if let Some(local_addr) = self.local_address { if let Some(local_addr) = self.local_address {
req = req.set_local_addr(local_addr); req = req.set_local_addr(local_addr);
@@ -640,8 +631,8 @@ where
} }
/// Connector service for pooled Plain/Tls Tcp connections. /// Connector service for pooled Plain/Tls Tcp connections.
pub type ConnectorService<Svc, IO> = ConnectorServicePriv< pub type ConnectorService<S, Io> = ConnectorServicePriv<
TcpConnectorService<TcpConnectorInnerService<Svc>>, TcpConnectorService<TcpConnectorInnerService<S>>,
Rc< Rc<
dyn Service< dyn Service<
Connect, Connect,
@@ -653,7 +644,7 @@ pub type ConnectorService<Svc, IO> = ConnectorServicePriv<
>, >,
>, >,
>, >,
IO, Io,
Box<dyn ConnectionIo>, Box<dyn ConnectionIo>,
>; >;
@@ -752,7 +743,7 @@ mod resolver {
use super::*; use super::*;
pub(super) fn resolver() -> Resolver { pub(super) fn resolver() -> Resolver {
Resolver::default() Resolver::Default
} }
} }
@@ -794,7 +785,8 @@ mod resolver {
} }
} }
// resolver struct is cached in thread local so new clients can reuse the existing instance // dns struct is cached in thread local.
// so new client constructor can reuse the existing dns resolver.
thread_local! { thread_local! {
static TRUST_DNS_RESOLVER: RefCell<Option<Resolver>> = RefCell::new(None); static TRUST_DNS_RESOLVER: RefCell<Option<Resolver>> = RefCell::new(None);
} }
@@ -802,10 +794,8 @@ mod resolver {
// get from thread local or construct a new trust-dns resolver. // get from thread local or construct a new trust-dns resolver.
TRUST_DNS_RESOLVER.with(|local| { TRUST_DNS_RESOLVER.with(|local| {
let resolver = local.borrow().as_ref().map(Clone::clone); let resolver = local.borrow().as_ref().map(Clone::clone);
match resolver { match resolver {
Some(resolver) => resolver, Some(resolver) => resolver,
None => { None => {
let (cfg, opts) = match read_system_conf() { let (cfg, opts) = match read_system_conf() {
Ok((cfg, opts)) => (cfg, opts), Ok((cfg, opts)) => (cfg, opts),
@@ -818,9 +808,8 @@ mod resolver {
let resolver = TokioAsyncResolver::tokio(cfg, opts).unwrap(); let resolver = TokioAsyncResolver::tokio(cfg, opts).unwrap();
// box trust dns resolver and put it in thread local. // box trust dns resolver and put it in thread local.
let resolver = Resolver::custom(TrustDnsResolver(resolver)); let resolver = Resolver::new_custom(TrustDnsResolver(resolver));
*local.borrow_mut() = Some(resolver.clone()); *local.borrow_mut() = Some(resolver.clone());
resolver resolver
} }
} }
@@ -851,9 +840,9 @@ mod tests {
.await; .await;
let connector = Connector { let connector = Connector {
connector: TcpConnector::new(resolver::resolver()).service(), connector: new_connector(resolver::resolver()),
config: ConnectorConfig::default(), config: ConnectorConfig::default(),
ssl: OurTlsConnector::None, ssl: SslConnector::None,
}; };
let client = Client::builder().connector(connector).finish(); let client = Client::builder().connector(connector).finish();

View File

@@ -1,13 +1,13 @@
use std::{fmt, io}; use std::{error::Error as StdError, fmt, io};
use derive_more::{Display, From}; use derive_more::{Display, From};
use actix_http::error::{HttpError, ParseError}; use actix_http::{
error::{Error, ParseError},
http::Error as HttpError,
};
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
use actix_tls::accept::openssl::reexports::Error as OpensslError; use actix_tls::accept::openssl::SslError;
use crate::BoxError;
/// 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)]
@@ -20,7 +20,7 @@ pub enum ConnectError {
/// SSL error /// SSL error
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
SslError(OpensslError), SslError(SslError),
/// Failed to resolve the hostname /// Failed to resolve the hostname
#[display(fmt = "Failed resolving hostname: {}", _0)] #[display(fmt = "Failed resolving hostname: {}", _0)]
@@ -118,11 +118,11 @@ pub enum SendRequestError {
TunnelNotSupported, TunnelNotSupported,
/// Error sending request body /// Error sending request body
Body(BoxError), Body(Error),
/// Other errors that can occur after submitting a request. /// Other errors that can occur after submitting a request.
#[display(fmt = "{:?}: {}", _1, _0)] #[display(fmt = "{:?}: {}", _1, _0)]
Custom(BoxError, Box<dyn fmt::Debug>), Custom(Box<dyn StdError>, Box<dyn fmt::Debug>),
} }
impl std::error::Error for SendRequestError {} impl std::error::Error for SendRequestError {}
@@ -141,7 +141,7 @@ pub enum FreezeRequestError {
/// Other errors that can occur after submitting a request. /// Other errors that can occur after submitting a request.
#[display(fmt = "{:?}: {}", _1, _0)] #[display(fmt = "{:?}: {}", _1, _0)]
Custom(BoxError, Box<dyn fmt::Debug>), Custom(Box<dyn StdError>, Box<dyn fmt::Debug>),
} }
impl std::error::Error for FreezeRequestError {} impl std::error::Error for FreezeRequestError {}

View File

@@ -9,8 +9,11 @@ use actix_http::{
body::{BodySize, MessageBody}, body::{BodySize, MessageBody},
error::PayloadError, error::PayloadError,
h1, h1,
http::{
header::{HeaderMap, IntoHeaderValue, EXPECT, HOST}, header::{HeaderMap, IntoHeaderValue, EXPECT, HOST},
Payload, RequestHeadType, ResponseHead, StatusCode, StatusCode,
},
Error, Payload, RequestHeadType, ResponseHead,
}; };
use actix_utils::future::poll_fn; use actix_utils::future::poll_fn;
use bytes::buf::BufMut; use bytes::buf::BufMut;
@@ -19,8 +22,6 @@ use futures_core::{ready, Stream};
use futures_util::SinkExt as _; use futures_util::SinkExt as _;
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use crate::BoxError;
use super::connection::{ConnectionIo, H1Connection}; use super::connection::{ConnectionIo, H1Connection};
use super::error::{ConnectError, SendRequestError}; use super::error::{ConnectError, SendRequestError};
@@ -32,7 +33,7 @@ pub(crate) async fn send_request<Io, B>(
where where
Io: ConnectionIo, Io: ConnectionIo,
B: MessageBody, B: MessageBody,
B::Error: Into<BoxError>, 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)
@@ -65,7 +66,8 @@ where
let mut framed = Framed::new(io, h1::ClientCodec::default()); let mut framed = Framed::new(io, h1::ClientCodec::default());
// Check EXPECT header and enable expect handle flag accordingly. // Check EXPECT header and enable expect handle flag accordingly.
// See https://datatracker.ietf.org/doc/html/rfc7231#section-5.1.1 //
// RFC: https://tools.ietf.org/html/rfc7231#section-5.1.1
let is_expect = if head.as_ref().headers.contains_key(EXPECT) { let is_expect = if head.as_ref().headers.contains_key(EXPECT) {
match body.size() { match body.size() {
BodySize::None | BodySize::Sized(0) => { BodySize::None | BodySize::Sized(0) => {
@@ -154,7 +156,7 @@ pub(crate) async fn send_body<Io, B>(
where where
Io: ConnectionIo, Io: ConnectionIo,
B: MessageBody, B: MessageBody,
B::Error: Into<BoxError>, B::Error: Into<Error>,
{ {
actix_rt::pin!(body); actix_rt::pin!(body);
@@ -165,7 +167,7 @@ where
Some(Ok(chunk)) => { Some(Ok(chunk)) => {
framed.as_mut().write(h1::Message::Chunk(Some(chunk)))?; framed.as_mut().write(h1::Message::Chunk(Some(chunk)))?;
} }
Some(Err(err)) => return Err(SendRequestError::Body(err.into())), 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

@@ -13,11 +13,9 @@ use log::trace;
use actix_http::{ use actix_http::{
body::{BodySize, MessageBody}, body::{BodySize, MessageBody},
header::HeaderMap, header::HeaderMap,
Payload, RequestHeadType, ResponseHead, Error, Payload, RequestHeadType, ResponseHead,
}; };
use crate::BoxError;
use super::{ use super::{
config::ConnectorConfig, config::ConnectorConfig,
connection::{ConnectionIo, H2Connection}, connection::{ConnectionIo, H2Connection},
@@ -32,7 +30,7 @@ pub(crate) async fn send_request<Io, B>(
where where
Io: ConnectionIo, Io: ConnectionIo,
B: MessageBody, B: MessageBody,
B::Error: Into<BoxError>, B::Error: Into<Error>,
{ {
trace!("Sending client request: {:?} {:?}", head, body.size()); trace!("Sending client request: {:?} {:?}", head, body.size());
@@ -92,7 +90,7 @@ where
for (key, value) in headers { for (key, value) in headers {
match *key { match *key {
// TODO: consider skipping other headers according to: // TODO: consider skipping other headers according to:
// https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2 // https://tools.ietf.org/html/rfc7540#section-8.1.2.2
// omit HTTP/1.x only headers // omit HTTP/1.x only headers
CONNECTION | TRANSFER_ENCODING => continue, CONNECTION | TRANSFER_ENCODING => continue,
CONTENT_LENGTH if skip_len => continue, CONTENT_LENGTH if skip_len => continue,
@@ -135,12 +133,10 @@ where
async fn send_body<B>(body: B, mut send: SendStream<Bytes>) -> Result<(), SendRequestError> async fn send_body<B>(body: B, mut send: SendStream<Bytes>) -> Result<(), SendRequestError>
where where
B: MessageBody, B: MessageBody,
B::Error: Into<BoxError>, B::Error: Into<Error>,
{ {
let mut buf = None; let mut buf = None;
actix_rt::pin!(body); actix_rt::pin!(body);
loop { loop {
if buf.is_none() { if buf.is_none() {
match poll_fn(|cx| body.as_mut().poll_next(cx)).await { match poll_fn(|cx| body.as_mut().poll_next(cx)).await {
@@ -148,10 +144,10 @@ where
send.reserve_capacity(b.len()); send.reserve_capacity(b.len());
buf = Some(b); buf = Some(b);
} }
Some(Err(err)) => return Err(SendRequestError::Body(err.into())), Some(Err(e)) => return Err(e.into().into()),
None => { None => {
if let Err(err) = send.send_data(Bytes::new(), true) { if let Err(e) = send.send_data(Bytes::new(), true) {
return Err(err.into()); return Err(e.into());
} }
send.reserve_capacity(0); send.reserve_capacity(0);
return Ok(()); return Ok(());

View File

@@ -11,7 +11,7 @@ mod h2proto;
mod pool; mod pool;
pub use actix_tls::connect::{ pub use actix_tls::connect::{
ConnectError as TcpConnectError, ConnectInfo, Connection as TcpConnection, Connect as TcpConnect, ConnectError as TcpConnectError, Connection as TcpConnection,
}; };
pub use self::connection::{Connection, ConnectionIo}; pub use self::connection::{Connection, ConnectionIo};

View File

@@ -7,17 +7,16 @@ use std::{
}; };
use actix_codec::Framed; use actix_codec::Framed;
use actix_http::{h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead}; use actix_http::{
body::AnyBody, h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead,
};
use actix_service::Service; use actix_service::Service;
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
use crate::{ use crate::client::{
any_body::AnyBody,
client::{
Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError, Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError,
},
response::ClientResponse,
}; };
use crate::response::ClientResponse;
pub type BoxConnectorService = Rc< pub type BoxConnectorService = Rc<
dyn Service< dyn Service<

View File

@@ -1,10 +1,9 @@
//! HTTP client errors //! HTTP client errors
pub use actix_http::{ pub use actix_http::{
error::{HttpError, PayloadError}, error::PayloadError,
header::HeaderValue, http::{header::HeaderValue, Error as HttpError, StatusCode},
ws::{HandshakeError as WsHandshakeError, ProtocolError as WsProtocolError}, ws::{HandshakeError as WsHandshakeError, ProtocolError as WsProtocolError},
StatusCode,
}; };
use derive_more::{Display, From}; use derive_more::{Display, From};
@@ -12,8 +11,6 @@ use serde_json::error::Error as JsonError;
pub use crate::client::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; pub use crate::client::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};
// TODO: address display, error, and from impls
/// Websocket client error /// Websocket client error
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
pub enum WsClientError { pub enum WsClientError {

View File

@@ -1,19 +1,18 @@
use std::{convert::TryFrom, net, rc::Rc, time::Duration}; use std::{convert::TryFrom, error::Error as StdError, net, rc::Rc, 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::{ use actix_http::{
error::HttpError, body::AnyBody,
header::{HeaderMap, HeaderName, IntoHeaderValue}, http::{header::IntoHeaderValue, Error as HttpError, HeaderMap, HeaderName, Method, Uri},
Method, RequestHead, Uri, RequestHead,
}; };
use crate::{ use crate::{
any_body::AnyBody,
sender::{RequestSender, SendClientRequest}, sender::{RequestSender, SendClientRequest},
BoxError, ClientConfig, ClientConfig,
}; };
/// `FrozenClientRequest` struct represents cloneable client request. /// `FrozenClientRequest` struct represents cloneable client request.
@@ -83,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<BoxError> + '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,
@@ -208,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<BoxError> + '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

@@ -104,7 +104,6 @@
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
mod any_body;
mod builder; mod builder;
mod client; mod client;
mod connect; mod connect;
@@ -117,8 +116,7 @@ mod sender;
pub mod test; pub mod test;
pub mod ws; pub mod ws;
// TODO: hmmmmmm pub use actix_http::http;
pub use actix_http as http;
#[cfg(feature = "cookies")] #[cfg(feature = "cookies")]
pub use cookie; pub use cookie;
@@ -132,13 +130,14 @@ pub use self::sender::SendClientRequest;
use std::{convert::TryFrom, rc::Rc, time::Duration}; use std::{convert::TryFrom, rc::Rc, time::Duration};
use actix_http::{error::HttpError, header::HeaderMap, Method, RequestHead, Uri}; use actix_http::{
http::{Error as HttpError, HeaderMap, Method, Uri},
RequestHead,
};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
use actix_service::Service; use actix_service::Service;
use self::client::{ConnectInfo, TcpConnectError, TcpConnection}; use self::client::{TcpConnect, TcpConnectError, TcpConnection};
pub(crate) type BoxError = Box<dyn std::error::Error>;
/// An asynchronous HTTP and WebSocket client. /// An asynchronous HTTP and WebSocket client.
/// ///
@@ -187,7 +186,7 @@ impl Client {
/// This function is equivalent of `ClientBuilder::new()`. /// This function is equivalent of `ClientBuilder::new()`.
pub fn builder() -> ClientBuilder< pub fn builder() -> ClientBuilder<
impl Service< impl Service<
ConnectInfo<Uri>, TcpConnect<Uri>,
Response = TcpConnection<Uri, TcpStream>, Response = TcpConnection<Uri, TcpStream>,
Error = TcpConnectError, Error = TcpConnectError,
> + Clone, > + Clone,

View File

@@ -7,18 +7,20 @@ use std::{
task::{Context, Poll}, task::{Context, Poll},
}; };
use actix_http::{header, Method, RequestHead, RequestHeadType, StatusCode, Uri}; use actix_http::{
body::AnyBody,
http::{header, Method, StatusCode, Uri},
RequestHead, RequestHeadType,
};
use actix_service::Service; use actix_service::Service;
use bytes::Bytes; use bytes::Bytes;
use futures_core::ready; use futures_core::ready;
use super::Transform; use super::Transform;
use crate::{
any_body::AnyBody, use crate::client::{InvalidUrl, SendRequestError};
client::{InvalidUrl, SendRequestError}, use crate::connect::{ConnectRequest, ConnectResponse};
connect::{ConnectRequest, ConnectResponse}, use crate::ClientResponse;
ClientResponse,
};
pub struct Redirect { pub struct Redirect {
max_redirect_times: u8, max_redirect_times: u8,
@@ -93,7 +95,7 @@ where
}; };
let body_opt = match body { let body_opt = match body {
AnyBody::Bytes { ref body } => Some(body.clone()), AnyBody::Bytes(ref b) => Some(b.clone()),
_ => None, _ => None,
}; };
@@ -190,9 +192,7 @@ where
let body_new = if is_redirect { let body_new = if is_redirect {
// try to reuse body // try to reuse body
match body { match body {
Some(ref bytes) => AnyBody::Bytes { Some(ref bytes) => AnyBody::Bytes(bytes.clone()),
body: bytes.clone(),
},
// TODO: should this be AnyBody::Empty or AnyBody::None. // TODO: should this be AnyBody::Empty or AnyBody::None.
_ => AnyBody::empty(), _ => AnyBody::empty(),
} }
@@ -281,12 +281,12 @@ fn remove_sensitive_headers(headers: &mut header::HeaderMap, prev_uri: &Uri, nex
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::str::FromStr;
use actix_web::{web, App, Error, HttpRequest, HttpResponse}; use actix_web::{web, App, Error, HttpRequest, HttpResponse};
use super::*; use super::*;
use crate::{http::header::HeaderValue, ClientBuilder}; use crate::http::HeaderValue;
use crate::ClientBuilder;
use std::str::FromStr;
#[actix_rt::test] #[actix_rt::test]
async fn test_basic_redirect() { async fn test_basic_redirect() {

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