Merge branch 'master' into feat/awc_response_timeout

This commit is contained in:
fakeshadow 2021-02-12 21:22:58 -08:00 committed by GitHub
commit 9684c9f4e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
131 changed files with 2473 additions and 2184 deletions

View File

@ -1,24 +1,27 @@
name: CI (Linux) name: CI
on: on:
pull_request: pull_request:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
push: push:
branches: branches: [master]
- master
jobs: jobs:
build_and_test: build_and_test:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
target:
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
version: version:
- 1.46.0 # MSRV - 1.46.0 # MSRV
- stable - stable
- nightly - nightly
name: ${{ matrix.version }} - x86_64-unknown-linux-gnu name: ${{ matrix.target.name }} / ${{ matrix.version }}
runs-on: ubuntu-latest runs-on: ${{ matrix.target.os }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -26,7 +29,7 @@ jobs:
- name: Install ${{ matrix.version }} - name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
profile: minimal profile: minimal
override: true override: true
@ -37,18 +40,25 @@ jobs:
- name: Cache Dependencies - name: Cache Dependencies
uses: Swatinem/rust-cache@v1.0.1 uses: Swatinem/rust-cache@v1.0.1
- name: check build - name: check minimal
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: check command: check
args: --all --bins --examples --tests args: --workspace --no-default-features --tests
- name: check full
uses: actions-rs/cargo@v1
with:
command: check
args: --workspace --bins --examples --tests
- name: tests - name: tests
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
timeout-minutes: 40
with: with:
command: test command: test
args: --all --all-features --no-fail-fast -- --nocapture args: -v --workspace --all-features --no-fail-fast -- --nocapture
--skip=test_h2_content_length
--skip=test_reading_deflate_encoding_large_random_rustls
- name: tests (actix-http) - name: tests (actix-http)
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@ -65,12 +75,18 @@ jobs:
args: --package=awc --no-default-features --features=rustls -- --nocapture args: --package=awc --no-default-features --features=rustls -- --nocapture
- name: Generate coverage file - name: Generate coverage file
if: matrix.version == 'stable' && github.ref == 'refs/heads/master' if: >
matrix.target.os == 'ubuntu-latest'
&& matrix.version == 'stable'
&& github.ref == 'refs/heads/master'
run: | run: |
cargo install cargo-tarpaulin --vers "^0.13" cargo install cargo-tarpaulin --vers "^0.13"
cargo tarpaulin --out Xml cargo tarpaulin --out Xml
- name: Upload to Codecov - name: Upload to Codecov
if: matrix.version == 'stable' && github.ref == 'refs/heads/master' if: >
matrix.target.os == 'ubuntu-latest'
&& matrix.version == 'stable'
&& github.ref == 'refs/heads/master'
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v1
with: with:
file: cobertura.xml file: cobertura.xml

View File

@ -1,56 +0,0 @@
name: CI (macOS)
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- master
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
version:
- stable
- nightly
name: ${{ matrix.version }} - x86_64-apple-darwin
runs-on: macOS-latest
steps:
- uses: actions/checkout@v2
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-x86_64-apple-darwin
profile: minimal
override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
command: generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.0.1
- name: check build
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all --all-features --no-fail-fast -- --nocapture
--skip=test_h2_content_length
--skip=test_reading_deflate_encoding_large_random_rustls
- name: Clear the cargo caches
run: |
cargo install cargo-cache --no-default-features --features ci-autoclean
cargo-cache

View File

@ -1,76 +0,0 @@
name: CI (Windows)
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- master
env:
VCPKGRS_DYNAMIC: 1
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
version:
- stable
- nightly
name: ${{ matrix.version }} - x86_64-pc-windows-msvc
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-x86_64-pc-windows-msvc
profile: minimal
override: true
- name: Install OpenSSL
run: |
vcpkg integrate install
vcpkg install openssl:x64-windows
Copy-Item C:\vcpkg\installed\x64-windows\bin\libcrypto-1_1-x64.dll C:\vcpkg\installed\x64-windows\bin\libcrypto.dll
Copy-Item C:\vcpkg\installed\x64-windows\bin\libssl-1_1-x64.dll C:\vcpkg\installed\x64-windows\bin\libssl.dll
Get-ChildItem C:\vcpkg\installed\x64-windows\bin
Get-ChildItem C:\vcpkg\installed\x64-windows\lib
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
command: generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.0.1
- name: check build
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all --all-features --no-fail-fast -- --nocapture
--skip=test_h2_content_length
--skip=test_reading_deflate_encoding_large_random_rustls
--skip=test_params
--skip=test_simple
--skip=test_expect_continue
--skip=test_http10_keepalive
--skip=test_slow_request
--skip=test_connection_force_close
--skip=test_connection_server_close
--skip=test_connection_wait_queue_force_close
- name: Clear the cargo caches
run: |
cargo install cargo-cache --no-default-features --features ci-autoclean
cargo-cache

View File

@ -1,26 +1,33 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 4.0.0-beta.3 - 2021-02-10
* Update `actix-web-codegen` to `0.5.0-beta.1`.
## 4.0.0-beta.2 - 2021-02-10
### Added ### Added
* The method `Either<web::Json<T>, web::Form<T>>::into_inner()` which returns the inner type for * The method `Either<web::Json<T>, web::Form<T>>::into_inner()` which returns the inner type for
whichever variant was created. Also works for `Either<web::Form<T>, web::Json<T>>`. [#1894] whichever variant was created. Also works for `Either<web::Form<T>, web::Json<T>>`. [#1894]
* Add `services!` macro for helping register multiple services to `App`. [#1933] * Add `services!` macro for helping register multiple services to `App`. [#1933]
* Enable registering vector of same type of `HttpServiceFactory` to `App` [#1933] * Enable registering a vec of services of the same type to `App` [#1933]
### Changed ### Changed
* Rework `Responder` trait to be sync and returns `Response`/`HttpResponse` directly. * Rework `Responder` trait to be sync and returns `Response`/`HttpResponse` directly.
Making it more simple and performant. [#1891] Making it simpler and more performant. [#1891]
* `ServiceRequest::into_parts` and `ServiceRequest::from_parts` would not fail. * `ServiceRequest::into_parts` and `ServiceRequest::from_parts` can no longer fail. [#1893]
`ServiceRequest::from_request` would not fail and no payload would be generated [#1893] * `ServiceRequest::from_request` can no longer fail. [#1893]
* Our `Either` type now uses `Left`/`Right` variants (instead of `A`/`B`) [#1894] * Our `Either` type now uses `Left`/`Right` variants (instead of `A`/`B`) [#1894]
* `test::{call_service, read_response, read_response_json, send_request}` take `&Service` * `test::{call_service, read_response, read_response_json, send_request}` take `&Service`
in argument [#1905] in argument [#1905]
* `App::wrap_fn`, `Resource::wrap_fn` and `Scope::wrap_fn` would give `&Service` in closure * `App::wrap_fn`, `Resource::wrap_fn` and `Scope::wrap_fn` provide `&Service` in closure
argument [#1905] argument. [#1905]
* `web::block` accept any closure that has an output bound to `Send` and `'static`. [#1957] * `web::block` no longer requires the output is a Result. [#1957]
### Fixed ### Fixed
* Multiple calls `App::data` with the same type now keeps the latest call's data. [#1906] * Multiple calls to `App::data` with the same type now keeps the latest call's data. [#1906]
### Removed ### Removed
* Public field of `web::Path` has been made private. [#1894] * Public field of `web::Path` has been made private. [#1894]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.0.0-beta.1" version = "4.0.0-beta.3"
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"
readme = "README.md" readme = "README.md"
@ -47,10 +47,10 @@ compress = ["actix-http/compress", "awc/compress"]
secure-cookies = ["actix-http/secure-cookies"] secure-cookies = ["actix-http/secure-cookies"]
# openssl # openssl
openssl = ["tls_openssl", "actix-tls/accept", "actix-tls/openssl", "awc/openssl"] openssl = ["tls-openssl", "actix-tls/accept", "actix-tls/openssl", "awc/openssl"]
# rustls # rustls
rustls = ["tls_rustls", "actix-tls/accept", "actix-tls/rustls", "awc/rustls"] rustls = ["tls-rustls", "actix-tls/accept", "actix-tls/rustls", "awc/rustls"]
[[example]] [[example]]
name = "basic" name = "basic"
@ -82,9 +82,9 @@ actix-service = "2.0.0-beta.4"
actix-utils = "3.0.0-beta.2" actix-utils = "3.0.0-beta.2"
actix-tls = { version = "3.0.0-beta.3", default-features = false, optional = true } actix-tls = { version = "3.0.0-beta.3", default-features = false, optional = true }
actix-web-codegen = "0.4.0" actix-web-codegen = "0.5.0-beta.1"
actix-http = "3.0.0-beta.1" actix-http = "3.0.0-beta.3"
awc = { version = "3.0.0-beta.1", default-features = false } awc = { version = "3.0.0-beta.2", default-features = false }
ahash = "0.7" ahash = "0.7"
bytes = "1" bytes = "1"
@ -103,13 +103,18 @@ serde_json = "1.0"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
time = { version = "0.2.23", default-features = false, features = ["std"] } time = { version = "0.2.23", default-features = false, features = ["std"] }
url = "2.1" url = "2.1"
tls_openssl = { package = "openssl", version = "0.10.9", optional = true } tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
tls_rustls = { package = "rustls", version = "0.19.0", optional = true } tls-rustls = { package = "rustls", version = "0.19.0", optional = true }
smallvec = "1.6" smallvec = "1.6"
[target.'cfg(windows)'.dependencies.tls-openssl]
version = "0.10.9"
package = "openssl"
features = ["vendored"]
optional = true
[dev-dependencies] [dev-dependencies]
actix = { version = "0.11.0-beta.2", default-features = false } actix = { version = "0.11.0-beta.2", default-features = false }
actix-http = { version = "3.0.0-beta.1", features = ["actors"] }
rand = "0.8" rand = "0.8"
env_logger = "0.8" env_logger = "0.8"
serde_derive = "1.0" serde_derive = "1.0"
@ -117,6 +122,9 @@ brotli2 = "0.3.2"
flate2 = "1.0.13" flate2 = "1.0.13"
criterion = "0.3" criterion = "0.3"
[profile.dev]
debug = false
[profile.release] [profile.release]
lto = true lto = true
opt-level = 3 opt-level = 3

View File

@ -1,15 +1,15 @@
<div align="center"> <div align="center">
<h1>Actix web</h1> <h1>Actix Web</h1>
<p> <p>
<strong>Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust</strong> <strong>Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust</strong>
</p> </p>
<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=3.3.2)](https://docs.rs/actix-web/3.3.2) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.2)](https://docs.rs/actix-web/4.0.0-beta.2)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
![License](https://img.shields.io/crates/l/actix-web.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/3.3.2/status.svg)](https://deps.rs/crate/actix-web/3.3.2) [![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.2/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.2)
<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)
@ -32,7 +32,6 @@
* SSL support using OpenSSL or Rustls * SSL support using OpenSSL or Rustls
* Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
* Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) * Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html)
* Supports [Actix actor framework](https://github.com/actix/actix)
* Runs on stable Rust 1.46+ * Runs on stable Rust 1.46+
## Documentation ## Documentation

View File

@ -1,12 +1,16 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.6.0-beta.2 - 2021-02-10
* Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887] * Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887]
* Replace `v_htmlescape` with `askama_escape`. [#1953] * Replace `v_htmlescape` with `askama_escape`. [#1953]
[#1887]: https://github.com/actix/actix-web/pull/1887 [#1887]: https://github.com/actix/actix-web/pull/1887
[#1953]: https://github.com/actix/actix-web/pull/1953 [#1953]: https://github.com/actix/actix-web/pull/1953
## 0.6.0-beta.1 - 2021-01-07 ## 0.6.0-beta.1 - 2021-01-07
* `HttpRange::parse` now has its own error type. * `HttpRange::parse` now has its own error type.
* Update `bytes` to `1.0`. [#1813] * Update `bytes` to `1.0`. [#1813]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.0-beta.1" version = "0.6.0-beta.2"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Static file serving for Actix Web" description = "Static file serving for Actix Web"
readme = "README.md" readme = "README.md"
@ -17,7 +17,7 @@ name = "actix_files"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
actix-web = { version = "4.0.0-beta.1", default-features = false } actix-web = { version = "4.0.0-beta.3", default-features = false }
actix-service = "2.0.0-beta.4" actix-service = "2.0.0-beta.4"
askama_escape = "0.10" askama_escape = "0.10"
@ -33,4 +33,4 @@ percent-encoding = "2.1"
[dev-dependencies] [dev-dependencies]
actix-rt = "2" actix-rt = "2"
actix-web = "4.0.0-beta.1" actix-web = "4.0.0-beta.3"

View File

@ -49,10 +49,7 @@ impl fmt::Debug for ChunkedReadFile {
impl Stream for ChunkedReadFile { impl Stream for ChunkedReadFile {
type Item = Result<Bytes, Error>; type Item = Result<Bytes, Error>;
fn poll_next( fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
let this = self.as_mut().get_mut(); let this = self.as_mut().get_mut();
match this.state { match this.state {
ChunkedReadFileState::File(ref mut file) => { ChunkedReadFileState::File(ref mut file) => {
@ -68,16 +65,13 @@ impl Stream for ChunkedReadFile {
.expect("ChunkedReadFile polled after completion"); .expect("ChunkedReadFile polled after completion");
let fut = spawn_blocking(move || { let fut = spawn_blocking(move || {
let max_bytes = let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
cmp::min(size.saturating_sub(counter), 65_536) as usize;
let mut buf = Vec::with_capacity(max_bytes); let mut buf = Vec::with_capacity(max_bytes);
file.seek(io::SeekFrom::Start(offset))?; file.seek(io::SeekFrom::Start(offset))?;
let n_bytes = file let n_bytes =
.by_ref() file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
.take(max_bytes as u64)
.read_to_end(&mut buf)?;
if n_bytes == 0 { if n_bytes == 0 {
return Err(io::ErrorKind::UnexpectedEof.into()); return Err(io::ErrorKind::UnexpectedEof.into());

View File

@ -66,9 +66,7 @@ pub(crate) fn directory_listing(
if dir.is_visible(&entry) { if dir.is_visible(&entry) {
let entry = entry.unwrap(); let entry = entry.unwrap();
let p = match entry.path().strip_prefix(&dir.path) { let p = match entry.path().strip_prefix(&dir.path) {
Ok(p) if cfg!(windows) => { Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace("\\", "/"),
base.join(p).to_string_lossy().replace("\\", "/")
}
Ok(p) => base.join(p).to_string_lossy().into_owned(), Ok(p) => base.join(p).to_string_lossy().into_owned(),
Err(_) => continue, Err(_) => continue,
}; };

View File

@ -2,9 +2,7 @@ use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc};
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt}; use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
use actix_web::{ use actix_web::{
dev::{ dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse},
AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse,
},
error::Error, error::Error,
guard::Guard, guard::Guard,
http::header::DispositionType, http::header::DispositionType,
@ -13,8 +11,8 @@ use actix_web::{
use futures_util::future::{ok, FutureExt, LocalBoxFuture}; use futures_util::future::{ok, FutureExt, LocalBoxFuture};
use crate::{ use crate::{
directory_listing, named, Directory, DirectoryRenderer, FilesService, directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService,
HttpNewService, MimeOverride, MimeOverride,
}; };
/// Static files handling service. /// Static files handling service.
@ -129,8 +127,8 @@ impl Files {
/// Set custom directory renderer /// Set custom directory renderer
pub fn files_listing_renderer<F>(mut self, f: F) -> Self pub fn files_listing_renderer<F>(mut self, f: F) -> Self
where where
for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error> for<'r, 's> F:
+ 'static, Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error> + 'static,
{ {
self.renderer = Rc::new(f); self.renderer = Rc::new(f);
self self

View File

@ -98,8 +98,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_if_modified_since_without_if_none_match() { async fn test_if_modified_since_without_if_none_match() {
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open("Cargo.toml").unwrap();
let since = let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default() let req = TestRequest::default()
.insert_header((header::IF_MODIFIED_SINCE, since)) .insert_header((header::IF_MODIFIED_SINCE, since))
@ -123,8 +122,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_if_modified_since_with_if_none_match() { async fn test_if_modified_since_with_if_none_match() {
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open("Cargo.toml").unwrap();
let since = let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default() let req = TestRequest::default()
.insert_header((header::IF_NONE_MATCH, "miss_etag")) .insert_header((header::IF_NONE_MATCH, "miss_etag"))
@ -212,8 +210,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_non_ascii_file_name() { async fn test_named_file_non_ascii_file_name() {
let mut file = let mut file =
NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml") NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml").unwrap();
.unwrap();
{ {
file.file(); file.file();
let _f: &File = &file; let _f: &File = &file;
@ -605,10 +602,9 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_static_files() { async fn test_static_files() {
let srv = test::init_service( let srv =
App::new().service(Files::new("/", ".").show_files_listing()), test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
) .await;
.await;
let req = TestRequest::with_uri("/missing").to_request(); let req = TestRequest::with_uri("/missing").to_request();
let resp = test::call_service(&srv, req).await; let resp = test::call_service(&srv, req).await;
@ -620,10 +616,9 @@ mod tests {
let resp = test::call_service(&srv, req).await; let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND); assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let srv = test::init_service( let srv =
App::new().service(Files::new("/", ".").show_files_listing()), test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
) .await;
.await;
let req = TestRequest::with_uri("/tests").to_request(); let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await; let resp = test::call_service(&srv, req).await;
assert_eq!( assert_eq!(

View File

@ -11,8 +11,7 @@ use actix_web::{
dev::{BodyEncoding, SizedStream}, dev::{BodyEncoding, SizedStream},
http::{ http::{
header::{ header::{
self, Charset, ContentDisposition, DispositionParam, DispositionType, self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
ExtendedValue,
}, },
ContentEncoding, StatusCode, ContentEncoding, StatusCode,
}, },
@ -395,18 +394,10 @@ impl NamedFile {
resp.encoding(ContentEncoding::Identity); resp.encoding(ContentEncoding::Identity);
resp.insert_header(( resp.insert_header((
header::CONTENT_RANGE, header::CONTENT_RANGE,
format!( format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
"bytes {}-{}/{}",
offset,
offset + length - 1,
self.md.len()
),
)); ));
} else { } else {
resp.insert_header(( resp.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length)));
header::CONTENT_RANGE,
format!("bytes */{}", length),
));
return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish(); return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
}; };
} else { } else {

View File

@ -46,8 +46,7 @@ impl HttpRange {
if start_str.is_empty() { if start_str.is_empty() {
// If no start is specified, end specifies the // If no start is specified, end specifies the
// range start relative to the end of the file. // range start relative to the end of the file.
let mut length: i64 = let mut length: i64 = end_str.parse().map_err(|_| ParseRangeErr(()))?;
end_str.parse().map_err(|_| ParseRangeErr(()))?;
if length > size_sig { if length > size_sig {
length = size_sig; length = size_sig;
@ -72,8 +71,7 @@ impl HttpRange {
// If no end is specified, range extends to end of the file. // If no end is specified, range extends to end of the file.
size_sig - start size_sig - start
} else { } else {
let mut end: i64 = let mut end: i64 = end_str.parse().map_err(|_| ParseRangeErr(()))?;
end_str.parse().map_err(|_| ParseRangeErr(()))?;
if start > end { if start > end {
return Err(ParseRangeErr(())); return Err(ParseRangeErr(()));

View File

@ -11,8 +11,8 @@ use actix_web::{
use futures_util::future::{ok, Either, LocalBoxFuture, Ready}; use futures_util::future::{ok, Either, LocalBoxFuture, Ready};
use crate::{ use crate::{
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile,
NamedFile, PathBufWrap, PathBufWrap,
}; };
/// Assembled file serving service. /// Assembled file serving service.
@ -138,8 +138,7 @@ impl Service<ServiceRequest> for FilesService {
match NamedFile::open(path) { match NamedFile::open(path) {
Ok(mut named_file) => { Ok(mut named_file) => {
if let Some(ref mime_override) = self.mime_override { if let Some(ref mime_override) = self.mime_override {
let new_disposition = let new_disposition = mime_override(&named_file.content_type.type_());
mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition; named_file.content_disposition.disposition = new_disposition;
} }
named_file.flags = self.file_flags; named_file.flags = self.file_flags;

View File

@ -23,10 +23,9 @@ async fn test_utf8_file_contents() {
); );
// prefer UTF-8 encoding // prefer UTF-8 encoding
let srv = test::init_service( let srv =
App::new().service(Files::new("/", "./tests").prefer_utf8(true)), test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(true)))
) .await;
.await;
let req = TestRequest::with_uri("/utf8.txt").to_request(); let req = TestRequest::with_uri("/utf8.txt").to_request();
let res = test::call_service(&srv, req).await; let res = test::call_service(&srv, req).await;

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.2 - 2021-02-10
* No notable changes.
## 3.0.0-beta.1 - 2021-01-07 ## 3.0.0-beta.1 - 2021-01-07
* Update `bytes` to `1.0`. [#1813] * Update `bytes` to `1.0`. [#1813]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http-test" name = "actix-http-test"
version = "3.0.0-beta.1" version = "3.0.0-beta.2"
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"
readme = "README.md" readme = "README.md"
@ -35,7 +35,7 @@ actix-tls = "3.0.0-beta.3"
actix-utils = "3.0.0-beta.2" actix-utils = "3.0.0-beta.2"
actix-rt = "2" actix-rt = "2"
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-beta.3"
awc = "3.0.0-beta.1" awc = "3.0.0-beta.2"
base64 = "0.13" base64 = "0.13"
bytes = "1" bytes = "1"
@ -50,6 +50,12 @@ serde_urlencoded = "0.7"
time = { version = "0.2.23", default-features = false, features = ["std"] } time = { version = "0.2.23", default-features = false, features = ["std"] }
tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
[target.'cfg(windows)'.dependencies.tls-openssl]
version = "0.10.9"
package = "openssl"
features = ["vendored"]
optional = true
[dev-dependencies] [dev-dependencies]
actix-web = "4.0.0-beta.1" actix-web = "4.0.0-beta.3"
actix-http = "3.0.0-beta.1" actix-http = "3.0.0-beta.3"

View File

@ -4,7 +4,7 @@
[![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=2.1.0)](https://docs.rs/actix-http-test/2.1.0) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=2.1.0)](https://docs.rs/actix-http-test/2.1.0)
![Apache 2.0 or MIT 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)
[![Dependency Status](https://deps.rs/crate/actix-http-test/2.1.0/status.svg)](https://deps.rs/crate/actix-http-test/2.1.0) [![Dependency Status](https://deps.rs/crate/actix-http-test/2.1.0/status.svg)](https://deps.rs/crate/actix-http-test/2.1.0)
[![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

View File

@ -120,8 +120,7 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
/// Get first available unused address /// Get first available unused address
pub fn unused_addr() -> net::SocketAddr { pub fn unused_addr() -> net::SocketAddr {
let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
let socket = let socket = Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap();
Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap();
socket.bind(&addr.into()).unwrap(); socket.bind(&addr.into()).unwrap();
socket.set_reuse_address(true).unwrap(); socket.set_reuse_address(true).unwrap();
let tcp = socket.into_tcp_listener(); let tcp = socket.into_tcp_listener();
@ -150,7 +149,7 @@ impl TestServer {
} }
} }
/// Construct test https server url /// Construct test HTTPS server URL.
pub fn surl(&self, uri: &str) -> String { pub fn surl(&self, uri: &str) -> String {
if uri.starts_with('/') { if uri.starts_with('/') {
format!("https://localhost:{}{}", self.addr.port(), uri) format!("https://localhost:{}{}", self.addr.port(), uri)
@ -164,7 +163,7 @@ impl TestServer {
self.client.get(self.url(path.as_ref()).as_str()) self.client.get(self.url(path.as_ref()).as_str())
} }
/// Create https `GET` request /// Create HTTPS `GET` request
pub fn sget<S: AsRef<str>>(&self, path: S) -> ClientRequest { pub fn sget<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.get(self.surl(path.as_ref()).as_str()) self.client.get(self.surl(path.as_ref()).as_str())
} }
@ -174,7 +173,7 @@ impl TestServer {
self.client.post(self.url(path.as_ref()).as_str()) self.client.post(self.url(path.as_ref()).as_str())
} }
/// Create https `POST` request /// Create HTTPS `POST` request
pub fn spost<S: AsRef<str>>(&self, path: S) -> ClientRequest { pub fn spost<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.post(self.surl(path.as_ref()).as_str()) self.client.post(self.surl(path.as_ref()).as_str())
} }
@ -184,7 +183,7 @@ impl TestServer {
self.client.head(self.url(path.as_ref()).as_str()) self.client.head(self.url(path.as_ref()).as_str())
} }
/// Create https `HEAD` request /// Create HTTPS `HEAD` request
pub fn shead<S: AsRef<str>>(&self, path: S) -> ClientRequest { pub fn shead<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.head(self.surl(path.as_ref()).as_str()) self.client.head(self.surl(path.as_ref()).as_str())
} }
@ -194,7 +193,7 @@ impl TestServer {
self.client.put(self.url(path.as_ref()).as_str()) self.client.put(self.url(path.as_ref()).as_str())
} }
/// Create https `PUT` request /// Create HTTPS `PUT` request
pub fn sput<S: AsRef<str>>(&self, path: S) -> ClientRequest { pub fn sput<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.put(self.surl(path.as_ref()).as_str()) self.client.put(self.surl(path.as_ref()).as_str())
} }
@ -204,7 +203,7 @@ impl TestServer {
self.client.patch(self.url(path.as_ref()).as_str()) self.client.patch(self.url(path.as_ref()).as_str())
} }
/// Create https `PATCH` request /// Create HTTPS `PATCH` request
pub fn spatch<S: AsRef<str>>(&self, path: S) -> ClientRequest { pub fn spatch<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.patch(self.surl(path.as_ref()).as_str()) self.client.patch(self.surl(path.as_ref()).as_str())
} }
@ -214,7 +213,7 @@ impl TestServer {
self.client.delete(self.url(path.as_ref()).as_str()) self.client.delete(self.url(path.as_ref()).as_str())
} }
/// Create https `DELETE` request /// Create HTTPS `DELETE` request
pub fn sdelete<S: AsRef<str>>(&self, path: S) -> ClientRequest { pub fn sdelete<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.delete(self.surl(path.as_ref()).as_str()) self.client.delete(self.surl(path.as_ref()).as_str())
} }
@ -224,12 +223,12 @@ impl TestServer {
self.client.options(self.url(path.as_ref()).as_str()) self.client.options(self.url(path.as_ref()).as_str())
} }
/// Create https `OPTIONS` request /// Create HTTPS `OPTIONS` request
pub fn soptions<S: AsRef<str>>(&self, path: S) -> ClientRequest { pub fn soptions<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.options(self.surl(path.as_ref()).as_str()) self.client.options(self.surl(path.as_ref()).as_str())
} }
/// Connect to test http server /// Connect to test HTTP server
pub fn request<S: AsRef<str>>(&self, method: Method, path: S) -> ClientRequest { pub fn request<S: AsRef<str>>(&self, method: Method, path: S) -> ClientRequest {
self.client.request(method, path.as_ref()) self.client.request(method, path.as_ref())
} }
@ -244,26 +243,24 @@ impl TestServer {
response.body().limit(10_485_760).await response.body().limit(10_485_760).await
} }
/// Connect to websocket server at a given path /// Connect to WebSocket server at a given path.
pub async fn ws_at( pub async fn ws_at(
&mut self, &mut self,
path: &str, path: &str,
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> ) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
{
let url = self.url(path); let url = self.url(path);
let connect = self.client.ws(url).connect(); let connect = self.client.ws(url).connect();
connect.await.map(|(_, framed)| framed) connect.await.map(|(_, framed)| framed)
} }
/// Connect to a websocket server /// Connect to a WebSocket server.
pub async fn ws( pub async fn ws(
&mut self, &mut self,
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> ) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
{
self.ws_at("/").await self.ws_at("/").await
} }
/// Stop http server /// Stop HTTP server
fn stop(&mut self) { fn stop(&mut self) {
self.system.stop(); self.system.stop();
} }

View File

@ -1,12 +1,23 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.3 - 2021-02-10
* No notable changes.
## 3.0.0-beta.2 - 2021-02-10
### Added ### Added
* `IntoHeaderPair` trait that allows using typed and untyped headers in the same methods. [#1869] * `IntoHeaderPair` trait that allows using typed and untyped headers in the same methods. [#1869]
* `ResponseBuilder::insert_header` method which allows using typed headers. [#1869] * `ResponseBuilder::insert_header` method which allows using typed headers. [#1869]
* `ResponseBuilder::append_header` method which allows using typed headers. [#1869] * `ResponseBuilder::append_header` method which allows using typed headers. [#1869]
* `TestRequest::insert_header` method which allows using typed headers. [#1869] * `TestRequest::insert_header` method which allows using typed headers. [#1869]
* `ContentEncoding` implements all necessary header traits. [#1912] * `ContentEncoding` implements all necessary header traits. [#1912]
* `HeaderMap::len_keys` has the behavior of the old `len` method. [#1964]
* `HeaderMap::drain` as an efficient draining iterator. [#1964]
* Implement `IntoIterator` for owned `HeaderMap`. [#1964]
* `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969]
### Changed ### Changed
* `ResponseBuilder::content_type` now takes an `impl IntoHeaderValue` to support using typed * `ResponseBuilder::content_type` now takes an `impl IntoHeaderValue` to support using typed
@ -14,11 +25,15 @@
* Renamed `IntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std * Renamed `IntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std
`TryInto` trait. [#1894] `TryInto` trait. [#1894]
* `Extensions::insert` returns Option of replaced item. [#1904] * `Extensions::insert` returns Option of replaced item. [#1904]
* Remove `HttpResponseBuilder::json2()` and make `HttpResponseBuilder::json()` take a value by * Remove `HttpResponseBuilder::json2()`. [#1903]
reference. [#1903] * Enable `HttpResponseBuilder::json()` to receive data by value and reference. [#1903]
* `client::error::ConnectError` Resolver variant contains `Box<dyn std::error::Error>` type [#1905] * `client::error::ConnectError` Resolver variant contains `Box<dyn std::error::Error>` type. [#1905]
* `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905] * `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905]
* Simplify `BlockingError` type to a struct. It's only triggered with blocking thread pool is dead. [#1957] * Simplify `BlockingError` type to a unit struct. It's now only triggered when blocking thread pool
is dead. [#1957]
* `HeaderMap::len` now returns number of values instead of number of keys. [#1964]
* `HeaderMap::insert` now returns iterator of removed values. [#1964]
* `HeaderMap::remove` now returns iterator of removed values. [#1964]
### Removed ### Removed
* `ResponseBuilder::set`; use `ResponseBuilder::insert_header`. [#1869] * `ResponseBuilder::set`; use `ResponseBuilder::insert_header`. [#1869]
@ -26,6 +41,11 @@
* `ResponseBuilder::header`; use `ResponseBuilder::append_header`. [#1869] * `ResponseBuilder::header`; use `ResponseBuilder::append_header`. [#1869]
* `TestRequest::with_hdr`; use `TestRequest::default().insert_header()`. [#1869] * `TestRequest::with_hdr`; use `TestRequest::default().insert_header()`. [#1869]
* `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869] * `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869]
* `actors` optional feature. [#1969]
* `ResponseError` impl for `actix::MailboxError`. [#1969]
### Documentation
* Vastly improve docs and add examples for `HeaderMap`. [#1964]
[#1869]: https://github.com/actix/actix-web/pull/1869 [#1869]: https://github.com/actix/actix-web/pull/1869
[#1894]: https://github.com/actix/actix-web/pull/1894 [#1894]: https://github.com/actix/actix-web/pull/1894
@ -34,6 +54,8 @@
[#1905]: https://github.com/actix/actix-web/pull/1905 [#1905]: https://github.com/actix/actix-web/pull/1905
[#1912]: https://github.com/actix/actix-web/pull/1912 [#1912]: https://github.com/actix/actix-web/pull/1912
[#1957]: https://github.com/actix/actix-web/pull/1957 [#1957]: https://github.com/actix/actix-web/pull/1957
[#1964]: https://github.com/actix/actix-web/pull/1964
[#1969]: https://github.com/actix/actix-web/pull/1969
## 3.0.0-beta.1 - 2021-01-07 ## 3.0.0-beta.1 - 2021-01-07
@ -109,15 +131,14 @@
* Update actix-connect and actix-tls dependencies. * Update actix-connect and actix-tls dependencies.
## [2.0.0-beta.3] - 2020-08-14 ## 2.0.0-beta.3 - 2020-08-14
### Fixed ### Fixed
* Memory leak of `client::pool::ConnectorPoolSupport`. [#1626] * Memory leak of `client::pool::ConnectorPoolSupport`. [#1626]
[#1626]: https://github.com/actix/actix-web/pull/1626 [#1626]: https://github.com/actix/actix-web/pull/1626
## [2.0.0-beta.2] - 2020-07-21 ## 2.0.0-beta.2 - 2020-07-21
### Fixed ### Fixed
* Potential UB in h1 decoder using uninitialized memory. [#1614] * Potential UB in h1 decoder using uninitialized memory. [#1614]
@ -128,10 +149,8 @@
[#1615]: https://github.com/actix/actix-web/pull/1615 [#1615]: https://github.com/actix/actix-web/pull/1615
## [2.0.0-beta.1] - 2020-07-11 ## 2.0.0-beta.1 - 2020-07-11
### Changed ### Changed
* Migrate cookie handling to `cookie` crate. [#1558] * Migrate cookie handling to `cookie` crate. [#1558]
* Update `sha-1` to 0.9. [#1586] * Update `sha-1` to 0.9. [#1586]
* Fix leak in client pool. [#1580] * Fix leak in client pool. [#1580]
@ -141,33 +160,30 @@
[#1586]: https://github.com/actix/actix-web/pull/1586 [#1586]: https://github.com/actix/actix-web/pull/1586
[#1580]: https://github.com/actix/actix-web/pull/1580 [#1580]: https://github.com/actix/actix-web/pull/1580
## [2.0.0-alpha.4] - 2020-05-21
## 2.0.0-alpha.4 - 2020-05-21
### Changed ### Changed
* Bump minimum supported Rust version to 1.40 * Bump minimum supported Rust version to 1.40
* content_length function is removed, and you can set Content-Length by calling no_chunking function [#1439] * content_length function is removed, and you can set Content-Length by calling
no_chunking function [#1439]
* `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a * `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
`u64` instead of a `usize`. `u64` instead of a `usize`.
* Update `base64` dependency to 0.12 * Update `base64` dependency to 0.12
### Fixed ### Fixed
* Support parsing of `SameSite=None` [#1503] * Support parsing of `SameSite=None` [#1503]
[#1439]: https://github.com/actix/actix-web/pull/1439 [#1439]: https://github.com/actix/actix-web/pull/1439
[#1503]: https://github.com/actix/actix-web/pull/1503 [#1503]: https://github.com/actix/actix-web/pull/1503
## [2.0.0-alpha.3] - 2020-05-08
## 2.0.0-alpha.3 - 2020-05-08
### Fixed ### Fixed
* Correct spelling of ConnectError::Unresolved [#1487] * Correct spelling of ConnectError::Unresolved [#1487]
* Fix a mistake in the encoding of websocket continuation messages wherein * Fix a mistake in the encoding of websocket continuation messages wherein
Item::FirstText and Item::FirstBinary are each encoded as the other. Item::FirstText and Item::FirstBinary are each encoded as the other.
### Changed ### Changed
* Implement `std::error::Error` for our custom errors [#1422] * Implement `std::error::Error` for our custom errors [#1422]
* Remove `failure` support for `ResponseError` since that crate * Remove `failure` support for `ResponseError` since that crate
will be deprecated in the near future. will be deprecated in the near future.
@ -175,338 +191,247 @@
[#1422]: https://github.com/actix/actix-web/pull/1422 [#1422]: https://github.com/actix/actix-web/pull/1422
[#1487]: https://github.com/actix/actix-web/pull/1487 [#1487]: https://github.com/actix/actix-web/pull/1487
## [2.0.0-alpha.2] - 2020-03-07
## 2.0.0-alpha.2 - 2020-03-07
### Changed ### Changed
* Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395] * Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395]
* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB
* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB respectively respectively to improve download speed for awc when downloading large objects. [#1394]
to improve download speed for awc when downloading large objects. [#1394] * client::Connector accepts initial_window_size and initial_connection_window_size
HTTP2 configuration. [#1394]
* client::Connector accepts initial_window_size and initial_connection_window_size HTTP2 configuration. [#1394]
* client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394] * client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394]
[#1394]: https://github.com/actix/actix-web/pull/1394 [#1394]: https://github.com/actix/actix-web/pull/1394
[#1395]: https://github.com/actix/actix-web/pull/1395 [#1395]: https://github.com/actix/actix-web/pull/1395
## [2.0.0-alpha.1] - 2020-02-27
## 2.0.0-alpha.1 - 2020-02-27
### Changed ### Changed
* Update the `time` dependency to 0.2.7. * Update the `time` dependency to 0.2.7.
* Moved actors messages support from actix crate, enabled with feature `actors`. * Moved actors messages support from actix crate, enabled with feature `actors`.
* Breaking change: trait MessageBody requires Unpin and accepting Pin<&mut Self> instead of &mut self in the poll_next(). * Breaking change: trait MessageBody requires Unpin and accepting `Pin<&mut Self>` instead of
`&mut self` in the poll_next().
* MessageBody is not implemented for &'static [u8] anymore. * MessageBody is not implemented for &'static [u8] anymore.
### Fixed ### Fixed
* Allow `SameSite=None` cookies to be sent in a response. * Allow `SameSite=None` cookies to be sent in a response.
## [1.0.1] - 2019-12-20
## 1.0.1 - 2019-12-20
### Fixed ### Fixed
* Poll upgrade service's readiness from HTTP service handlers * Poll upgrade service's readiness from HTTP service handlers
* Replace brotli with brotli2 #1224 * Replace brotli with brotli2 #1224
## [1.0.0] - 2019-12-13
## 1.0.0 - 2019-12-13
### Added ### Added
* Add websockets continuation frame support * Add websockets continuation frame support
### Changed ### Changed
* Replace `flate2-xxx` features with `compress` * Replace `flate2-xxx` features with `compress`
## [1.0.0-alpha.5] - 2019-12-09
## 1.0.0-alpha.5 - 2019-12-09
### Fixed ### Fixed
* Check `Upgrade` service readiness before calling it * Check `Upgrade` service readiness before calling it
* Fix buffer remaining capacity calculation
* Fix buffer remaining capacity calcualtion
### Changed ### Changed
* Websockets: Ping and Pong should have binary data #1049 * Websockets: Ping and Pong should have binary data #1049
## [1.0.0-alpha.4] - 2019-12-08
## 1.0.0-alpha.4 - 2019-12-08
### Added ### Added
* Add impl ResponseBuilder for Error * Add impl ResponseBuilder for Error
### Changed ### Changed
* Use rust based brotli compression library * Use rust based brotli compression library
## [1.0.0-alpha.3] - 2019-12-07 ## 1.0.0-alpha.3 - 2019-12-07
### Changed ### Changed
* Migrate to tokio 0.2 * Migrate to tokio 0.2
* Migrate to `std::future` * Migrate to `std::future`
## [0.2.11] - 2019-11-06 ## 0.2.11 - 2019-11-06
### Added ### Added
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body() * Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()
* Add an additional `filename*` param in the `Content-Disposition` header of
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151) `actix_files::NamedFile` to be more compatible. (#1151)
* Allow to use `std::convert::Infallible` as `actix_http::error::Error` * Allow to use `std::convert::Infallible` as `actix_http::error::Error`
### Fixed ### Fixed
* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain;
charset=utf-8` header [#1118]
* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain; charset=utf-8` header #1118 [#1878]: https://github.com/actix/actix-web/pull/1878
## [0.2.10] - 2019-09-11 ## 0.2.10 - 2019-09-11
### Added ### Added
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests with `RequestHead` with `RequestHead`
### Fixed ### Fixed
* h2 will use error response #1080 * h2 will use error response #1080
* on_connect result isn't added to request extensions for http2 requests #1009 * on_connect result isn't added to request extensions for http2 requests #1009
## [0.2.9] - 2019-08-13 ## 0.2.9 - 2019-08-13
### Changed ### Changed
* Dropped the `byteorder`-dependency in favor of `stdlib`-implementation * Dropped the `byteorder`-dependency in favor of `stdlib`-implementation
* Update percent-encoding to 2.1 * Update percent-encoding to 2.1
* Update serde_urlencoded to 0.6.1 * Update serde_urlencoded to 0.6.1
### Fixed ### Fixed
* Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031) * Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031)
## [0.2.8] - 2019-08-01 ## 0.2.8 - 2019-08-01
### Added ### Added
* Add `rustls` support * Add `rustls` support
* Add `Clone` impl for `HeaderMap` * Add `Clone` impl for `HeaderMap`
### Fixed ### Fixed
* awc client panic #1016 * awc client panic #1016
* Invalid response with compression middleware enabled, but compression-related features
* Invalid response with compression middleware enabled, but compression-related features disabled #997 disabled #997
## [0.2.7] - 2019-07-18 ## 0.2.7 - 2019-07-18
### Added ### Added
* Add support for downcasting response errors #986 * Add support for downcasting response errors #986
## [0.2.6] - 2019-07-17 ## 0.2.6 - 2019-07-17
### Changed ### Changed
* Replace `ClonableService` with local copy * Replace `ClonableService` with local copy
* Upgrade `rand` dependency version to 0.7 * Upgrade `rand` dependency version to 0.7
## [0.2.5] - 2019-06-28 ## 0.2.5 - 2019-06-28
### Added ### Added
* Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946 * Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946
### Changed ### Changed
* Use `encoding_rs` crate instead of unmaintained `encoding` crate * Use `encoding_rs` crate instead of unmaintained `encoding` crate
* Add `Copy` and `Clone` impls for `ws::Codec` * Add `Copy` and `Clone` impls for `ws::Codec`
## [0.2.4] - 2019-06-16 ## 0.2.4 - 2019-06-16
### Fixed ### Fixed
* Do not compress NoContent (204) responses #918 * Do not compress NoContent (204) responses #918
## [0.2.3] - 2019-06-02 ## 0.2.3 - 2019-06-02
### Added ### Added
* Debug impl for ResponseBuilder * Debug impl for ResponseBuilder
* From SizedStream and BodyStream for Body * From SizedStream and BodyStream for Body
### Changed ### Changed
* SizedStream uses u64 * SizedStream uses u64
## [0.2.2] - 2019-05-29 ## 0.2.2 - 2019-05-29
### Fixed ### Fixed
* Parse incoming stream before closing stream on disconnect #868 * Parse incoming stream before closing stream on disconnect #868
## [0.2.1] - 2019-05-25 ## 0.2.1 - 2019-05-25
### Fixed ### Fixed
* Handle socket read disconnect * Handle socket read disconnect
## [0.2.0] - 2019-05-12 ## 0.2.0 - 2019-05-12
### Changed ### Changed
* Update actix-service to 0.4 * Update actix-service to 0.4
* Expect and upgrade services accept `ServerConfig` config. * Expect and upgrade services accept `ServerConfig` config.
### Deleted ### Deleted
* `OneRequest` service * `OneRequest` service
## [0.1.5] - 2019-05-04 ## 0.1.5 - 2019-05-04
### Fixed ### Fixed
* Clean up response extensions in response pool #817 * Clean up response extensions in response pool #817
## [0.1.4] - 2019-04-24 ## 0.1.4 - 2019-04-24
### Added ### Added
* Allow to render h1 request headers in `Camel-Case` * Allow to render h1 request headers in `Camel-Case`
### Fixed ### Fixed
* Read until eof for http/1.0 responses #771 * Read until eof for http/1.0 responses #771
## [0.1.3] - 2019-04-23 ## 0.1.3 - 2019-04-23
### Fixed ### Fixed
* Fix http client pool management * Fix http client pool management
* Fix http client wait queue management #794 * Fix http client wait queue management #794
## [0.1.2] - 2019-04-23 ## 0.1.2 - 2019-04-23
### Fixed ### Fixed
* Fix BorrowMutError panic in client connector #793 * Fix BorrowMutError panic in client connector #793
## [0.1.1] - 2019-04-19 ## 0.1.1 - 2019-04-19
### Changed ### Changed
* Cookie::max_age() accepts value in seconds * Cookie::max_age() accepts value in seconds
* Cookie::max_age_time() accepts value in time::Duration * Cookie::max_age_time() accepts value in time::Duration
* Allow to specify server address for client connector * Allow to specify server address for client connector
## [0.1.0] - 2019-04-16 ## 0.1.0 - 2019-04-16
### Added ### Added
* Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr` * Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr`
### Changed ### Changed
* `actix_http::encoding` always available * `actix_http::encoding` always available
* use trust-dns-resolver 0.11.0 * use trust-dns-resolver 0.11.0
## [0.1.0-alpha.5] - 2019-04-12 ## 0.1.0-alpha.5 - 2019-04-12
### Added ### Added
* Allow to use custom service for upgrade requests * Allow to use custom service for upgrade requests
* Added `h1::SendResponse` future. * Added `h1::SendResponse` future.
### Changed ### Changed
* MessageBody::length() renamed to MessageBody::size() for consistency * MessageBody::length() renamed to MessageBody::size() for consistency
* ws handshake verification functions take RequestHead instead of Request * ws handshake verification functions take RequestHead instead of Request
## [0.1.0-alpha.4] - 2019-04-08 ## 0.1.0-alpha.4 - 2019-04-08
### Added ### Added
* Allow to use custom `Expect` handler * Allow to use custom `Expect` handler
* Add minimal `std::error::Error` impl for `Error` * Add minimal `std::error::Error` impl for `Error`
### Changed ### Changed
* Export IntoHeaderValue * Export IntoHeaderValue
* Render error and return as response body * Render error and return as response body
* Use thread pool for response body compression
* Use thread pool for response body comression
### Deleted ### Deleted
* Removed PayloadBuffer * Removed PayloadBuffer
## [0.1.0-alpha.3] - 2019-04-02 ## 0.1.0-alpha.3 - 2019-04-02
### Added ### Added
* Warn when an unsealed private cookie isn't valid UTF-8 * Warn when an unsealed private cookie isn't valid UTF-8
### Fixed ### Fixed
* Rust 1.31.0 compatibility * Rust 1.31.0 compatibility
* Preallocate read buffer for h1 codec * Preallocate read buffer for h1 codec
* Detect socket disconnection during protocol selection * Detect socket disconnection during protocol selection
## [0.1.0-alpha.2] - 2019-03-29 ## 0.1.0-alpha.2 - 2019-03-29
### Added ### Added
* Added ws::Message::Nop, no-op websockets message * Added ws::Message::Nop, no-op websockets message
### Changed ### Changed
* Do not use thread pool for decompression if chunk size is smaller than 2048.
* Do not use thread pool for decomression if chunk size is smaller than 2048.
## [0.1.0-alpha.1] - 2019-03-28 ## 0.1.0-alpha.1 - 2019-03-28
* Initial impl * Initial impl

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.0.0-beta.1" version = "3.0.0-beta.3"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "HTTP primitives for the Actix ecosystem" description = "HTTP primitives for the Actix ecosystem"
readme = "README.md" readme = "README.md"
@ -15,7 +15,7 @@ license = "MIT OR Apache-2.0"
edition = "2018" edition = "2018"
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = ["openssl", "rustls", "compress", "secure-cookies", "actors"] features = ["openssl", "rustls", "compress", "secure-cookies"]
[lib] [lib]
name = "actix_http" name = "actix_http"
@ -36,8 +36,8 @@ compress = ["flate2", "brotli2"]
# support for secure cookies # support for secure cookies
secure-cookies = ["cookie/secure"] secure-cookies = ["cookie/secure"]
# support for actix Actor messages # trust-dns as client dns resolver
actors = ["actix"] trust-dns = ["trust-dns-resolver"]
[dependencies] [dependencies]
actix-service = "2.0.0-beta.4" actix-service = "2.0.0-beta.4"
@ -45,7 +45,6 @@ actix-codec = "0.4.0-beta.1"
actix-utils = "3.0.0-beta.2" actix-utils = "3.0.0-beta.2"
actix-rt = "2" actix-rt = "2"
actix-tls = "3.0.0-beta.2" actix-tls = "3.0.0-beta.2"
actix = { version = "0.11.0-beta.2", default-features = false, optional = true }
base64 = "0.13" base64 = "0.13"
bitflags = "1.2" bitflags = "1.2"
@ -53,7 +52,6 @@ bytes = "1"
bytestring = "1" bytestring = "1"
cookie = { version = "0.14.1", features = ["percent-encode"] } cookie = { version = "0.14.1", features = ["percent-encode"] }
derive_more = "0.99.5" derive_more = "0.99.5"
either = "1.5.3"
encoding_rs = "0.8" encoding_rs = "0.8"
futures-channel = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-channel = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
@ -84,9 +82,11 @@ time = { version = "0.2.23", default-features = false, features = ["std"] }
brotli2 = { version="0.3.2", optional = true } brotli2 = { version="0.3.2", optional = true }
flate2 = { version = "1.0.13", optional = true } flate2 = { version = "1.0.13", optional = true }
trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies] [dev-dependencies]
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-beta.3"
actix-http-test = { version = "3.0.0-beta.1", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.2", features = ["openssl"] }
actix-tls = { version = "3.0.0-beta.2", features = ["openssl"] } actix-tls = { version = "3.0.0-beta.2", features = ["openssl"] }
criterion = "0.3" criterion = "0.3"
env_logger = "0.8" env_logger = "0.8"
@ -94,6 +94,11 @@ serde_derive = "1.0"
tls-openssl = { version = "0.10", package = "openssl" } tls-openssl = { version = "0.10", package = "openssl" }
tls-rustls = { version = "0.19", package = "rustls" } tls-rustls = { version = "0.19", package = "rustls" }
[target.'cfg(windows)'.dev-dependencies.tls-openssl]
version = "0.10.9"
package = "openssl"
features = ["vendored"]
[[bench]] [[bench]]
name = "write-camel-case" name = "write-camel-case"
harness = false harness = false

View File

@ -3,10 +3,13 @@
> 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=2.2.0)](https://docs.rs/actix-http/2.2.0) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.3)](https://docs.rs/actix-http/3.0.0-beta.3)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-http) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
[![Dependency Status](https://deps.rs/crate/actix-http/2.2.0/status.svg)](https://deps.rs/crate/actix-http/2.2.0) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
[![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) <br />
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.3/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.3)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & Resources ## Documentation & Resources

View File

@ -1,6 +1,10 @@
use std::pin::Pin; //! Traits and structures to aid consuming and writing HTTP payloads.
use std::task::{Context, Poll};
use std::{fmt, mem}; use std::{
fmt, mem,
pin::Pin,
task::{Context, Poll},
};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
@ -8,8 +12,8 @@ use pin_project::pin_project;
use crate::error::Error; use crate::error::Error;
/// Body size hint.
#[derive(Debug, PartialEq, Copy, Clone)] #[derive(Debug, PartialEq, Copy, Clone)]
/// Body size hint
pub enum BodySize { pub enum BodySize {
None, None,
Empty, Empty,
@ -23,7 +27,7 @@ impl BodySize {
} }
} }
/// Type that provides this trait can be streamed to a peer. /// Type that implement this trait can be streamed to a peer.
pub trait MessageBody { pub trait MessageBody {
fn size(&self) -> BodySize; fn size(&self) -> BodySize;
@ -80,7 +84,7 @@ impl ResponseBody<Body> {
impl<B> ResponseBody<B> { impl<B> ResponseBody<B> {
pub fn take_body(&mut self) -> ResponseBody<B> { pub fn take_body(&mut self) -> ResponseBody<B> {
std::mem::replace(self, ResponseBody::Other(Body::None)) mem::replace(self, ResponseBody::Other(Body::None))
} }
} }
@ -127,7 +131,7 @@ impl<B: MessageBody> Stream for ResponseBody<B> {
} }
} }
/// Represents various types of http message body. /// Represents various types of HTTP message body.
pub enum Body { pub enum Body {
/// Empty response. `Content-Length` header is not set. /// Empty response. `Content-Length` header is not set.
None, None,

View File

@ -6,7 +6,7 @@ use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
use actix_service::{apply_fn, Service, ServiceExt}; use actix_service::{apply_fn, Service, ServiceExt};
use actix_tls::connect::{ use actix_tls::connect::{
default_connector, Connect as TcpConnect, Connection as TcpConnection, new_connector, Connect as TcpConnect, Connection as TcpConnection, Resolver,
}; };
use actix_utils::timeout::{TimeoutError, TimeoutService}; use actix_utils::timeout::{TimeoutError, TimeoutService};
use http::Uri; use http::Uri;
@ -19,7 +19,6 @@ use super::Connect;
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector; use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector;
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
use actix_tls::connect::ssl::rustls::ClientConfig; use actix_tls::connect::ssl::rustls::ClientConfig;
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
@ -35,7 +34,8 @@ enum SslConnector {
#[cfg(not(any(feature = "openssl", feature = "rustls")))] #[cfg(not(any(feature = "openssl", feature = "rustls")))]
type SslConnector = (); type SslConnector = ();
/// Manages http client network connectivity /// Manages HTTP client network connectivity.
///
/// The `Connector` type uses a builder-like combinator pattern for service /// The `Connector` type uses a builder-like combinator pattern for service
/// construction that finishes by calling the `.finish()` method. /// construction that finishes by calling the `.finish()` method.
/// ///
@ -70,7 +70,7 @@ impl Connector<(), ()> {
> { > {
Connector { Connector {
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()]),
connector: default_connector(), connector: new_connector(resolver::resolver()),
config: ConnectorConfig::default(), config: ConnectorConfig::default(),
_phantom: PhantomData, _phantom: PhantomData,
} }
@ -161,8 +161,9 @@ where
self self
} }
/// Maximum supported http major version /// Maximum supported HTTP major version.
/// Supported versions http/1.1, http/2 ///
/// Supported versions are HTTP/1.1 and HTTP/2.
pub fn max_http_version(mut self, val: http::Version) -> Self { pub fn max_http_version(mut self, val: http::Version) -> Self {
let versions = match val { let versions = match val {
http::Version::HTTP_11 => vec![b"http/1.1".to_vec()], http::Version::HTTP_11 => vec![b"http/1.1".to_vec()],
@ -532,3 +533,82 @@ mod connect_impl {
} }
} }
} }
#[cfg(not(feature = "trust-dns"))]
mod resolver {
use super::*;
pub(super) fn resolver() -> Resolver {
Resolver::Default
}
}
#[cfg(feature = "trust-dns")]
mod resolver {
use std::{cell::RefCell, net::SocketAddr};
use actix_tls::connect::Resolve;
use futures_core::future::LocalBoxFuture;
use trust_dns_resolver::{
config::{ResolverConfig, ResolverOpts},
system_conf::read_system_conf,
TokioAsyncResolver,
};
use super::*;
pub(super) fn resolver() -> Resolver {
// new type for impl Resolve trait for TokioAsyncResolver.
struct TrustDnsResolver(TokioAsyncResolver);
impl Resolve for TrustDnsResolver {
fn lookup<'a>(
&'a self,
host: &'a str,
port: u16,
) -> LocalBoxFuture<'a, Result<Vec<SocketAddr>, Box<dyn std::error::Error>>>
{
Box::pin(async move {
let res = self
.0
.lookup_ip(host)
.await?
.iter()
.map(|ip| SocketAddr::new(ip, port))
.collect();
Ok(res)
})
}
}
// dns struct is cached in thread local.
// so new client constructor can reuse the existing dns resolver.
thread_local! {
static TRUST_DNS_RESOLVER: RefCell<Option<Resolver>> = RefCell::new(None);
}
// get from thread local or construct a new trust-dns resolver.
TRUST_DNS_RESOLVER.with(|local| {
let resolver = local.borrow().as_ref().map(Clone::clone);
match resolver {
Some(resolver) => resolver,
None => {
let (cfg, opts) = match read_system_conf() {
Ok((cfg, opts)) => (cfg, opts),
Err(e) => {
log::error!("TRust-DNS can not load system config: {}", e);
(ResolverConfig::default(), ResolverOpts::default())
}
};
let resolver = TokioAsyncResolver::tokio(cfg, opts).unwrap();
// box trust dns resolver and put it in thread local.
let resolver = Resolver::new_custom(TrustDnsResolver(resolver));
*local.borrow_mut() = Some(resolver.clone());
resolver
}
}
})
}
}

View File

@ -65,13 +65,16 @@ impl From<actix_tls::connect::ConnectError> for ConnectError {
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
pub enum InvalidUrl { pub enum InvalidUrl {
#[display(fmt = "Missing url scheme")] #[display(fmt = "Missing URL scheme")]
MissingScheme, MissingScheme,
#[display(fmt = "Unknown url scheme")]
#[display(fmt = "Unknown URL scheme")]
UnknownScheme, UnknownScheme,
#[display(fmt = "Missing host name")] #[display(fmt = "Missing host name")]
MissingHost, MissingHost,
#[display(fmt = "Url parse error: {}", _0)]
#[display(fmt = "URL parse error: {}", _0)]
HttpError(http::Error), HttpError(http::Error),
} }
@ -83,25 +86,33 @@ pub enum SendRequestError {
/// Invalid URL /// Invalid URL
#[display(fmt = "Invalid URL: {}", _0)] #[display(fmt = "Invalid URL: {}", _0)]
Url(InvalidUrl), Url(InvalidUrl),
/// Failed to connect to host /// Failed to connect to host
#[display(fmt = "Failed to connect to host: {}", _0)] #[display(fmt = "Failed to connect to host: {}", _0)]
Connect(ConnectError), Connect(ConnectError),
/// Error sending request /// Error sending request
Send(io::Error), Send(io::Error),
/// Error parsing response /// Error parsing response
Response(ParseError), Response(ParseError),
/// Http error /// Http error
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Http(HttpError), Http(HttpError),
/// Http2 error /// Http2 error
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
H2(h2::Error), H2(h2::Error),
/// Response took too long /// Response took too long
#[display(fmt = "Timeout while waiting for response")] #[display(fmt = "Timeout while waiting for response")]
Timeout, Timeout,
/// Tunnels are not supported for http2 connection
/// Tunnels are not supported for HTTP/2 connection
#[display(fmt = "Tunnels are not supported for http2 connection")] #[display(fmt = "Tunnels are not supported for http2 connection")]
TunnelNotSupported, TunnelNotSupported,
/// Error sending request body /// Error sending request body
Body(Error), Body(Error),
} }
@ -127,7 +138,8 @@ pub enum FreezeRequestError {
/// Invalid URL /// Invalid URL
#[display(fmt = "Invalid URL: {}", _0)] #[display(fmt = "Invalid URL: {}", _0)]
Url(InvalidUrl), Url(InvalidUrl),
/// Http error
/// HTTP error
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Http(HttpError), Http(HttpError),
} }

View File

@ -48,11 +48,11 @@ where
match wrt.get_mut().split().freeze().try_into_value() { match wrt.get_mut().split().freeze().try_into_value() {
Ok(value) => match head { Ok(value) => match head {
RequestHeadType::Owned(ref mut head) => { RequestHeadType::Owned(ref mut head) => {
head.headers.insert(HOST, value) head.headers.insert(HOST, value);
} }
RequestHeadType::Rc(_, ref mut extra_headers) => { RequestHeadType::Rc(_, ref mut extra_headers) => {
let headers = extra_headers.get_or_insert(HeaderMap::new()); let headers = extra_headers.get_or_insert(HeaderMap::new());
headers.insert(HOST, value) headers.insert(HOST, value);
} }
}, },
Err(e) => log::error!("Can not set HOST header {}", e), Err(e) => log::error!("Can not set HOST header {}", e),

View File

@ -1,4 +1,5 @@
//! Http client api //! HTTP client.
use http::Uri; use http::Uri;
mod config; mod config;

View File

@ -4,9 +4,11 @@ use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
use std::{fmt, net}; use std::{fmt, net};
use actix_rt::time::{sleep, sleep_until, Instant, Sleep}; use actix_rt::{
task::JoinHandle,
time::{interval, sleep_until, Instant, Sleep},
};
use bytes::BytesMut; use bytes::BytesMut;
use futures_util::{future, FutureExt};
use time::OffsetDateTime; use time::OffsetDateTime;
/// "Sun, 06 Nov 1994 08:49:37 GMT".len() /// "Sun, 06 Nov 1994 08:49:37 GMT".len()
@ -49,7 +51,7 @@ struct Inner {
ka_enabled: bool, ka_enabled: bool,
secure: bool, secure: bool,
local_addr: Option<std::net::SocketAddr>, local_addr: Option<std::net::SocketAddr>,
timer: DateService, date_service: DateService,
} }
impl Clone for ServiceConfig { impl Clone for ServiceConfig {
@ -91,41 +93,41 @@ impl ServiceConfig {
client_disconnect, client_disconnect,
secure, secure,
local_addr, local_addr,
timer: DateService::new(), date_service: DateService::new(),
})) }))
} }
/// Returns true if connection is secure (HTTPS)
#[inline] #[inline]
/// Returns true if connection is secure(https)
pub fn secure(&self) -> bool { pub fn secure(&self) -> bool {
self.0.secure self.0.secure
} }
#[inline]
/// Returns the local address that this server is bound to. /// Returns the local address that this server is bound to.
#[inline]
pub fn local_addr(&self) -> Option<net::SocketAddr> { pub fn local_addr(&self) -> Option<net::SocketAddr> {
self.0.local_addr self.0.local_addr
} }
#[inline]
/// Keep alive duration if configured. /// Keep alive duration if configured.
#[inline]
pub fn keep_alive(&self) -> Option<Duration> { pub fn keep_alive(&self) -> Option<Duration> {
self.0.keep_alive self.0.keep_alive
} }
#[inline]
/// Return state of connection keep-alive functionality /// Return state of connection keep-alive functionality
#[inline]
pub fn keep_alive_enabled(&self) -> bool { pub fn keep_alive_enabled(&self) -> bool {
self.0.ka_enabled self.0.ka_enabled
} }
#[inline]
/// Client timeout for first request. /// Client timeout for first request.
#[inline]
pub fn client_timer(&self) -> Option<Sleep> { pub fn client_timer(&self) -> Option<Sleep> {
let delay_time = self.0.client_timeout; let delay_time = self.0.client_timeout;
if delay_time != 0 { if delay_time != 0 {
Some(sleep_until( Some(sleep_until(
self.0.timer.now() + Duration::from_millis(delay_time), self.0.date_service.now() + Duration::from_millis(delay_time),
)) ))
} else { } else {
None None
@ -136,7 +138,7 @@ impl ServiceConfig {
pub fn client_timer_expire(&self) -> Option<Instant> { pub fn client_timer_expire(&self) -> Option<Instant> {
let delay = self.0.client_timeout; let delay = self.0.client_timeout;
if delay != 0 { if delay != 0 {
Some(self.0.timer.now() + Duration::from_millis(delay)) Some(self.0.date_service.now() + Duration::from_millis(delay))
} else { } else {
None None
} }
@ -146,7 +148,7 @@ impl ServiceConfig {
pub fn client_disconnect_timer(&self) -> Option<Instant> { pub fn client_disconnect_timer(&self) -> Option<Instant> {
let delay = self.0.client_disconnect; let delay = self.0.client_disconnect;
if delay != 0 { if delay != 0 {
Some(self.0.timer.now() + Duration::from_millis(delay)) Some(self.0.date_service.now() + Duration::from_millis(delay))
} else { } else {
None None
} }
@ -156,7 +158,7 @@ impl ServiceConfig {
/// Return keep-alive timer delay is configured. /// Return keep-alive timer delay is configured.
pub fn keep_alive_timer(&self) -> Option<Sleep> { pub fn keep_alive_timer(&self) -> Option<Sleep> {
if let Some(ka) = self.0.keep_alive { if let Some(ka) = self.0.keep_alive {
Some(sleep_until(self.0.timer.now() + ka)) Some(sleep_until(self.0.date_service.now() + ka))
} else { } else {
None None
} }
@ -165,7 +167,7 @@ impl ServiceConfig {
/// Keep-alive expire time /// Keep-alive expire time
pub fn keep_alive_expire(&self) -> Option<Instant> { pub fn keep_alive_expire(&self) -> Option<Instant> {
if let Some(ka) = self.0.keep_alive { if let Some(ka) = self.0.keep_alive {
Some(self.0.timer.now() + ka) Some(self.0.date_service.now() + ka)
} else { } else {
None None
} }
@ -173,7 +175,7 @@ impl ServiceConfig {
#[inline] #[inline]
pub(crate) fn now(&self) -> Instant { pub(crate) fn now(&self) -> Instant {
self.0.timer.now() self.0.date_service.now()
} }
#[doc(hidden)] #[doc(hidden)]
@ -181,7 +183,7 @@ impl ServiceConfig {
let mut buf: [u8; 39] = [0; 39]; let mut buf: [u8; 39] = [0; 39];
buf[..6].copy_from_slice(b"date: "); buf[..6].copy_from_slice(b"date: ");
self.0 self.0
.timer .date_service
.set_date(|date| buf[6..35].copy_from_slice(&date.bytes)); .set_date(|date| buf[6..35].copy_from_slice(&date.bytes));
buf[35..].copy_from_slice(b"\r\n\r\n"); buf[35..].copy_from_slice(b"\r\n\r\n");
dst.extend_from_slice(&buf); dst.extend_from_slice(&buf);
@ -189,7 +191,7 @@ impl ServiceConfig {
pub(crate) fn set_date_header(&self, dst: &mut BytesMut) { pub(crate) fn set_date_header(&self, dst: &mut BytesMut) {
self.0 self.0
.timer .date_service
.set_date(|date| dst.extend_from_slice(&date.bytes)); .set_date(|date| dst.extend_from_slice(&date.bytes));
} }
} }
@ -230,57 +232,103 @@ impl fmt::Write for Date {
} }
} }
#[derive(Clone)] /// Service for update Date and Instant periodically at 500 millis interval.
struct DateService(Rc<DateServiceInner>); struct DateService {
current: Rc<Cell<(Date, Instant)>>,
struct DateServiceInner { handle: JoinHandle<()>,
current: Cell<Option<(Date, Instant)>>,
} }
impl DateServiceInner { impl Drop for DateService {
fn new() -> Self { fn drop(&mut self) {
DateServiceInner { // stop the timer update async task on drop.
current: Cell::new(None), self.handle.abort();
}
}
fn reset(&self) {
self.current.take();
}
fn update(&self) {
let now = Instant::now();
let date = Date::new();
self.current.set(Some((date, now)));
} }
} }
impl DateService { impl DateService {
fn new() -> Self { fn new() -> Self {
DateService(Rc::new(DateServiceInner::new())) // shared date and timer for DateService and update async task.
} let current = Rc::new(Cell::new((Date::new(), Instant::now())));
let current_clone = Rc::clone(&current);
// spawn an async task sleep for 500 milli and update current date/timer in a loop.
// handle is used to stop the task on DateService drop.
let handle = actix_rt::spawn(async move {
#[cfg(test)]
let _notify = notify_on_drop::NotifyOnDrop::new();
fn check_date(&self) { let mut interval = interval(Duration::from_millis(500));
if self.0.current.get().is_none() { loop {
self.0.update(); let now = interval.tick().await;
let date = Date::new();
current_clone.set((date, now));
}
});
// periodic date update DateService { current, handle }
let s = self.clone();
actix_rt::spawn(sleep(Duration::from_millis(500)).then(move |_| {
s.0.reset();
future::ready(())
}));
}
} }
fn now(&self) -> Instant { fn now(&self) -> Instant {
self.check_date(); self.current.get().1
self.0.current.get().unwrap().1
} }
fn set_date<F: FnMut(&Date)>(&self, mut f: F) { fn set_date<F: FnMut(&Date)>(&self, mut f: F) {
self.check_date(); f(&self.current.get().0);
f(&self.0.current.get().unwrap().0); }
}
// TODO: move to a util module for testing all spawn handle drop style tasks.
#[cfg(test)]
/// Test Module for checking the drop state of certain async tasks that are spawned
/// with `actix_rt::spawn`
///
/// The target task must explicitly generate `NotifyOnDrop` when spawn the task
mod notify_on_drop {
use std::cell::RefCell;
thread_local! {
static NOTIFY_DROPPED: RefCell<Option<bool>> = RefCell::new(None);
}
/// Check if the spawned task is dropped.
///
/// # Panic:
///
/// When there was no `NotifyOnDrop` instance on current thread
pub(crate) fn is_dropped() -> bool {
NOTIFY_DROPPED.with(|bool| {
bool.borrow()
.expect("No NotifyOnDrop existed on current thread")
})
}
pub(crate) struct NotifyOnDrop;
impl NotifyOnDrop {
/// # Panic:
///
/// When construct multiple instances on any given thread.
pub(crate) fn new() -> Self {
NOTIFY_DROPPED.with(|bool| {
let mut bool = bool.borrow_mut();
if bool.is_some() {
panic!("NotifyOnDrop existed on current thread");
} else {
*bool = Some(false);
}
});
NotifyOnDrop
}
}
impl Drop for NotifyOnDrop {
fn drop(&mut self) {
NOTIFY_DROPPED.with(|bool| {
if let Some(b) = bool.borrow_mut().as_mut() {
*b = true;
}
});
}
} }
} }
@ -288,14 +336,53 @@ impl DateService {
mod tests { mod tests {
use super::*; use super::*;
// Test modifying the date from within the closure use actix_rt::task::yield_now;
// passed to `set_date`
#[test] #[actix_rt::test]
fn test_evil_date() { async fn test_date_service_update() {
let service = DateService::new(); let settings = ServiceConfig::new(KeepAlive::Os, 0, 0, false, None);
// Make sure that `check_date` doesn't try to spawn a task
service.0.update(); yield_now().await;
service.set_date(|_| service.0.reset());
let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf1);
let now1 = settings.now();
sleep_until(Instant::now() + Duration::from_secs(2)).await;
yield_now().await;
let now2 = settings.now();
let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf2);
assert_ne!(now1, now2);
assert_ne!(buf1, buf2);
drop(settings);
assert!(notify_on_drop::is_dropped());
}
#[actix_rt::test]
async fn test_date_service_drop() {
let service = Rc::new(DateService::new());
// yield so date service have a chance to register the spawned timer update task.
yield_now().await;
let clone1 = service.clone();
let clone2 = service.clone();
let clone3 = service.clone();
drop(clone1);
assert_eq!(false, notify_on_drop::is_dropped());
drop(clone2);
assert_eq!(false, notify_on_drop::is_dropped());
drop(clone3);
assert_eq!(false, notify_on_drop::is_dropped());
drop(service);
assert!(notify_on_drop::is_dropped());
} }
#[test] #[test]

View File

@ -1,7 +1,11 @@
use std::future::Future; //! Stream decoders.
use std::io::{self, Write};
use std::pin::Pin; use std::{
use std::task::{Context, Poll}; future::Future,
io::{self, Write as _},
pin::Pin,
task::{Context, Poll},
};
use actix_rt::task::{spawn_blocking, JoinHandle}; use actix_rt::task::{spawn_blocking, JoinHandle};
use brotli2::write::BrotliDecoder; use brotli2::write::BrotliDecoder;
@ -9,11 +13,13 @@ use bytes::Bytes;
use flate2::write::{GzDecoder, ZlibDecoder}; use flate2::write::{GzDecoder, ZlibDecoder};
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use super::Writer; use crate::{
use crate::error::{BlockingError, PayloadError}; encoding::Writer,
use crate::http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING}; error::{BlockingError, PayloadError},
http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING},
};
const INPLACE: usize = 2049; const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049;
pub struct Decoder<S> { pub struct Decoder<S> {
decoder: Option<ContentDecoder>, decoder: Option<ContentDecoder>,
@ -41,6 +47,7 @@ where
))), ))),
_ => None, _ => None,
}; };
Decoder { Decoder {
decoder, decoder,
stream, stream,
@ -53,15 +60,11 @@ where
#[inline] #[inline]
pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder<S> { pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder<S> {
// check content-encoding // check content-encoding
let encoding = if let Some(enc) = headers.get(&CONTENT_ENCODING) { let encoding = headers
if let Ok(enc) = enc.to_str() { .get(&CONTENT_ENCODING)
ContentEncoding::from(enc) .and_then(|val| val.to_str().ok())
} else { .map(ContentEncoding::from)
ContentEncoding::Identity .unwrap_or(ContentEncoding::Identity);
}
} else {
ContentEncoding::Identity
};
Self::new(stream, encoding) Self::new(stream, encoding)
} }
@ -81,8 +84,10 @@ where
if let Some(ref mut fut) = self.fut { if let Some(ref mut fut) = self.fut {
let (chunk, decoder) = let (chunk, decoder) =
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??; ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
self.decoder = Some(decoder); self.decoder = Some(decoder);
self.fut.take(); self.fut.take();
if let Some(chunk) = chunk { if let Some(chunk) = chunk {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
@ -92,13 +97,15 @@ where
return Poll::Ready(None); return Poll::Ready(None);
} }
match Pin::new(&mut self.stream).poll_next(cx) { match ready!(Pin::new(&mut self.stream).poll_next(cx)) {
Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))), Some(Err(err)) => return Poll::Ready(Some(Err(err))),
Poll::Ready(Some(Ok(chunk))) => {
Some(Ok(chunk)) => {
if let Some(mut decoder) = self.decoder.take() { if let Some(mut decoder) = self.decoder.take() {
if chunk.len() < INPLACE { if chunk.len() < MAX_CHUNK_SIZE_DECODE_IN_PLACE {
let chunk = decoder.feed_data(chunk)?; let chunk = decoder.feed_data(chunk)?;
self.decoder = Some(decoder); self.decoder = Some(decoder);
if let Some(chunk) = chunk { if let Some(chunk) = chunk {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
@ -108,13 +115,16 @@ where
Ok((chunk, decoder)) Ok((chunk, decoder))
})); }));
} }
continue; continue;
} else { } else {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
} }
Poll::Ready(None) => {
None => {
self.eof = true; self.eof = true;
return if let Some(mut decoder) = self.decoder.take() { return if let Some(mut decoder) = self.decoder.take() {
match decoder.feed_eof() { match decoder.feed_eof() {
Ok(Some(res)) => Poll::Ready(Some(Ok(res))), Ok(Some(res)) => Poll::Ready(Some(Ok(res))),
@ -125,10 +135,8 @@ where
Poll::Ready(None) Poll::Ready(None)
}; };
} }
Poll::Pending => break,
} }
} }
Poll::Pending
} }
} }
@ -144,6 +152,7 @@ impl ContentDecoder {
ContentDecoder::Br(ref mut decoder) => match decoder.flush() { ContentDecoder::Br(ref mut decoder) => match decoder.flush() {
Ok(()) => { Ok(()) => {
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
if !b.is_empty() { if !b.is_empty() {
Ok(Some(b)) Ok(Some(b))
} else { } else {
@ -152,9 +161,11 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() { ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() {
Ok(_) => { Ok(_) => {
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
if !b.is_empty() { if !b.is_empty() {
Ok(Some(b)) Ok(Some(b))
} else { } else {
@ -163,6 +174,7 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() { ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() {
Ok(_) => { Ok(_) => {
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
@ -183,6 +195,7 @@ impl ContentDecoder {
Ok(_) => { Ok(_) => {
decoder.flush()?; decoder.flush()?;
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
if !b.is_empty() { if !b.is_empty() {
Ok(Some(b)) Ok(Some(b))
} else { } else {
@ -191,10 +204,12 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) { ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => { Ok(_) => {
decoder.flush()?; decoder.flush()?;
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
if !b.is_empty() { if !b.is_empty() {
Ok(Some(b)) Ok(Some(b))
} else { } else {
@ -203,9 +218,11 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) { ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => { Ok(_) => {
decoder.flush()?; decoder.flush()?;
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
if !b.is_empty() { if !b.is_empty() {
Ok(Some(b)) Ok(Some(b))

View File

@ -1,8 +1,11 @@
//! Stream encoder //! Stream encoders.
use std::future::Future;
use std::io::{self, Write}; use std::{
use std::pin::Pin; future::Future,
use std::task::{Context, Poll}; io::{self, Write as _},
pin::Pin,
task::{Context, Poll},
};
use actix_rt::task::{spawn_blocking, JoinHandle}; use actix_rt::task::{spawn_blocking, JoinHandle};
use brotli2::write::BrotliEncoder; use brotli2::write::BrotliEncoder;
@ -11,15 +14,19 @@ use flate2::write::{GzEncoder, ZlibEncoder};
use futures_core::ready; use futures_core::ready;
use pin_project::pin_project; use pin_project::pin_project;
use crate::body::{Body, BodySize, MessageBody, ResponseBody}; use crate::{
use crate::http::header::{ContentEncoding, CONTENT_ENCODING}; body::{Body, BodySize, MessageBody, ResponseBody},
use crate::http::{HeaderValue, StatusCode}; http::{
use crate::{Error, ResponseHead}; header::{ContentEncoding, CONTENT_ENCODING},
HeaderValue, StatusCode,
},
Error, ResponseHead,
};
use super::Writer; use super::Writer;
use crate::error::BlockingError; use crate::error::BlockingError;
const INPLACE: usize = 1024; const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024;
#[pin_project] #[pin_project]
pub struct Encoder<B> { pub struct Encoder<B> {
@ -138,23 +145,28 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
if let Some(ref mut fut) = this.fut { if let Some(ref mut fut) = this.fut {
let mut encoder = let mut encoder =
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??; ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
let chunk = encoder.take(); let chunk = encoder.take();
*this.encoder = Some(encoder); *this.encoder = Some(encoder);
this.fut.take(); this.fut.take();
if !chunk.is_empty() { if !chunk.is_empty() {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
} }
let result = this.body.as_mut().poll_next(cx); let result = ready!(this.body.as_mut().poll_next(cx));
match result { match result {
Poll::Ready(Some(Ok(chunk))) => { Some(Err(err)) => return Poll::Ready(Some(Err(err))),
Some(Ok(chunk)) => {
if let Some(mut encoder) = this.encoder.take() { if let Some(mut encoder) = this.encoder.take() {
if chunk.len() < INPLACE { if chunk.len() < MAX_CHUNK_SIZE_ENCODE_IN_PLACE {
encoder.write(&chunk)?; encoder.write(&chunk)?;
let chunk = encoder.take(); let chunk = encoder.take();
*this.encoder = Some(encoder); *this.encoder = Some(encoder);
if !chunk.is_empty() { if !chunk.is_empty() {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
@ -168,7 +180,8 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
} }
Poll::Ready(None) => {
None => {
if let Some(encoder) = this.encoder.take() { if let Some(encoder) = this.encoder.take() {
let chunk = encoder.finish()?; let chunk = encoder.finish()?;
if chunk.is_empty() { if chunk.is_empty() {
@ -181,7 +194,6 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
return Poll::Ready(None); return Poll::Ready(None);
} }
} }
val => return val,
} }
} }
} }

View File

@ -1,4 +1,5 @@
//! Content-Encoding support //! Content-Encoding support.
use std::io; use std::io;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};

View File

@ -38,7 +38,7 @@ pub type Result<T, E = Error> = result::Result<T, E>;
/// converting errors with `into()`. /// converting errors with `into()`.
/// ///
/// Whenever it is created from an external object a response error is created /// Whenever it is created from an external object a response error is created
/// for it that can be used to create an http response from it this means that /// for it that can be used to create an HTTP response from it this means that
/// if you have access to an actix `Error` you can always get a /// if you have access to an actix `Error` you can always get a
/// `ResponseError` reference from it. /// `ResponseError` reference from it.
pub struct Error { pub struct Error {
@ -404,7 +404,7 @@ impl ResponseError for crate::cookie::ParseError {
} }
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
/// A set of errors that can occur during dispatching http requests /// A set of errors that can occur during dispatching HTTP requests
pub enum DispatchError { pub enum DispatchError {
/// Service error /// Service error
Service(Error), Service(Error),
@ -968,12 +968,6 @@ where
InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into() InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into()
} }
#[cfg(feature = "actors")]
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`actix::MailboxError`].
///
/// This is only supported when the feature `actors` is enabled.
impl ResponseError for actix::MailboxError {}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -199,10 +199,10 @@ mod tests {
use http::Method; use http::Method;
use super::*; use super::*;
use crate::httpmessage::HttpMessage; use crate::HttpMessage;
#[test] #[actix_rt::test]
fn test_http_request_chunked_payload_and_next_message() { async fn test_http_request_chunked_payload_and_next_message() {
let mut codec = Codec::default(); let mut codec = Codec::default();
let mut buf = BytesMut::from( let mut buf = BytesMut::from(

View File

@ -224,7 +224,7 @@ impl MessageType for Request {
let decoder = match length { let decoder = match length {
PayloadLength::Payload(pl) => pl, PayloadLength::Payload(pl) => pl,
PayloadLength::UpgradeWebSocket => { PayloadLength::UpgradeWebSocket => {
// upgrade(websocket) // upgrade (WebSocket)
PayloadType::Stream(PayloadDecoder::eof()) PayloadType::Stream(PayloadDecoder::eof())
} }
PayloadLength::None => { PayloadLength::None => {
@ -652,7 +652,7 @@ mod tests {
use super::*; use super::*;
use crate::error::ParseError; use crate::error::ParseError;
use crate::http::header::{HeaderName, SET_COOKIE}; use crate::http::header::{HeaderName, SET_COOKIE};
use crate::httpmessage::HttpMessage; use crate::HttpMessage;
impl PayloadType { impl PayloadType {
fn unwrap(self) -> PayloadDecoder { fn unwrap(self) -> PayloadDecoder {
@ -830,8 +830,8 @@ mod tests {
.get_all(SET_COOKIE) .get_all(SET_COOKIE)
.map(|v| v.to_str().unwrap().to_owned()) .map(|v| v.to_str().unwrap().to_owned())
.collect(); .collect();
assert_eq!(val[1], "c1=cookie1"); assert_eq!(val[0], "c1=cookie1");
assert_eq!(val[0], "c2=cookie2"); assert_eq!(val[1], "c2=cookie2");
} }
#[test] #[test]

View File

@ -37,15 +37,14 @@ bitflags! {
pub struct Flags: u8 { pub struct Flags: u8 {
const STARTED = 0b0000_0001; const STARTED = 0b0000_0001;
const KEEPALIVE = 0b0000_0010; const KEEPALIVE = 0b0000_0010;
const POLLED = 0b0000_0100; const SHUTDOWN = 0b0000_0100;
const SHUTDOWN = 0b0000_1000; const READ_DISCONNECT = 0b0000_1000;
const READ_DISCONNECT = 0b0001_0000; const WRITE_DISCONNECT = 0b0001_0000;
const WRITE_DISCONNECT = 0b0010_0000; const UPGRADE = 0b0010_0000;
const UPGRADE = 0b0100_0000;
} }
} }
#[pin_project::pin_project] #[pin_project]
/// Dispatcher for HTTP/1.1 protocol /// Dispatcher for HTTP/1.1 protocol
pub struct Dispatcher<T, S, B, X, U> pub struct Dispatcher<T, S, B, X, U>
where where
@ -139,27 +138,14 @@ where
fn is_empty(&self) -> bool { fn is_empty(&self) -> bool {
matches!(self, State::None) matches!(self, State::None)
} }
fn is_call(&self) -> bool {
matches!(self, State::ServiceCall(_))
}
} }
enum PollResponse { enum PollResponse {
Upgrade(Request), Upgrade(Request),
DoNothing, DoNothing,
DrainWriteBuf, DrainWriteBuf,
} }
impl PartialEq for PollResponse {
fn eq(&self, other: &PollResponse) -> bool {
match self {
PollResponse::DrainWriteBuf => matches!(other, PollResponse::DrainWriteBuf),
PollResponse::DoNothing => matches!(other, PollResponse::DoNothing),
_ => false,
}
}
}
impl<T, S, B, X, U> Dispatcher<T, S, B, X, U> impl<T, S, B, X, U> Dispatcher<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
@ -174,62 +160,35 @@ where
{ {
/// Create HTTP/1 dispatcher. /// Create HTTP/1 dispatcher.
pub(crate) fn new( pub(crate) fn new(
stream: T,
config: ServiceConfig,
services: Rc<HttpFlow<S, X, U>>,
on_connect_data: OnConnectData,
peer_addr: Option<net::SocketAddr>,
) -> Self {
Dispatcher::with_timeout(
stream,
Codec::new(config.clone()),
config,
BytesMut::with_capacity(HW_BUFFER_SIZE),
None,
services,
on_connect_data,
peer_addr,
)
}
/// Create http/1 dispatcher with slow request timeout.
pub(crate) fn with_timeout(
io: T, io: T,
codec: Codec,
config: ServiceConfig, config: ServiceConfig,
read_buf: BytesMut, flow: Rc<HttpFlow<S, X, U>>,
timeout: Option<Sleep>,
services: Rc<HttpFlow<S, X, U>>,
on_connect_data: OnConnectData, on_connect_data: OnConnectData,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
) -> Self { ) -> Self {
let keepalive = config.keep_alive_enabled(); let flags = if config.keep_alive_enabled() {
let flags = if keepalive {
Flags::KEEPALIVE Flags::KEEPALIVE
} else { } else {
Flags::empty() Flags::empty()
}; };
// keep-alive timer // keep-alive timer
let (ka_expire, ka_timer) = if let Some(delay) = timeout { let (ka_expire, ka_timer) = match config.keep_alive_timer() {
(delay.deadline(), Some(delay)) Some(delay) => (delay.deadline(), Some(delay)),
} else if let Some(delay) = config.keep_alive_timer() { None => (config.now(), None),
(delay.deadline(), Some(delay))
} else {
(config.now(), None)
}; };
Dispatcher { Dispatcher {
inner: DispatcherState::Normal(InnerDispatcher { inner: DispatcherState::Normal(InnerDispatcher {
read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
payload: None, payload: None,
state: State::None, state: State::None,
error: None, error: None,
messages: VecDeque::new(), messages: VecDeque::new(),
io: Some(io), io: Some(io),
codec, codec: Codec::new(config),
read_buf, flow,
flow: services,
on_connect_data, on_connect_data,
flags, flags,
peer_addr, peer_addr,
@ -286,15 +245,12 @@ where
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Result<bool, DispatchError> { ) -> Result<bool, DispatchError> {
let len = self.write_buf.len();
if len == 0 {
return Ok(false);
}
let InnerDispatcherProj { io, write_buf, .. } = self.project(); let InnerDispatcherProj { io, write_buf, .. } = self.project();
let mut io = Pin::new(io.as_mut().unwrap()); let mut io = Pin::new(io.as_mut().unwrap());
let len = write_buf.len();
let mut written = 0; let mut written = 0;
while written < len { while written < len {
match io.as_mut().poll_write(cx, &write_buf[written..]) { match io.as_mut().poll_write(cx, &write_buf[written..]) {
Poll::Ready(Ok(0)) => { Poll::Ready(Ok(0)) => {
@ -312,11 +268,13 @@ where
} }
} }
// SAFETY: setting length to 0 is safe // everything has written to io. clear buffer.
// skips one length check vs truncate write_buf.clear();
unsafe { write_buf.set_len(0) }
Ok(false) // flush the io and check if get blocked.
let blocked = io.poll_flush(cx)?.is_pending();
Ok(blocked)
} }
fn send_response( fn send_response(
@ -324,9 +282,10 @@ where
message: Response<()>, message: Response<()>,
body: ResponseBody<B>, body: ResponseBody<B>,
) -> Result<(), DispatchError> { ) -> Result<(), DispatchError> {
let size = body.size();
let mut this = self.project(); let mut this = self.project();
this.codec this.codec
.encode(Message::Item((message, body.size())), &mut this.write_buf) .encode(Message::Item((message, size)), &mut this.write_buf)
.map_err(|err| { .map_err(|err| {
if let Some(mut payload) = this.payload.take() { if let Some(mut payload) = this.payload.take() {
payload.set_error(PayloadError::Incomplete(None)); payload.set_error(PayloadError::Incomplete(None));
@ -335,7 +294,7 @@ where
})?; })?;
this.flags.set(Flags::KEEPALIVE, this.codec.keepalive()); this.flags.set(Flags::KEEPALIVE, this.codec.keepalive());
match body.size() { match size {
BodySize::None | BodySize::Empty => this.state.set(State::None), BodySize::None | BodySize::Empty => this.state.set(State::None),
_ => this.state.set(State::SendPayload(body)), _ => this.state.set(State::SendPayload(body)),
}; };
@ -352,109 +311,121 @@ where
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Result<PollResponse, DispatchError> { ) -> Result<PollResponse, DispatchError> {
loop { 'res: loop {
let mut this = self.as_mut().project(); let mut this = self.as_mut().project();
// state is not changed on Poll::Pending. match this.state.as_mut().project() {
// other variant and conditions always trigger a state change(or an error). // no future is in InnerDispatcher state. pop next message.
let state_change = match this.state.project() {
StateProj::None => match this.messages.pop_front() { StateProj::None => match this.messages.pop_front() {
// handle request message.
Some(DispatcherMessage::Item(req)) => { Some(DispatcherMessage::Item(req)) => {
self.as_mut().handle_request(req, cx)?; // Handle `EXPECT: 100-Continue` header
true if req.head().expect() {
// set InnerDispatcher state and continue loop to poll it.
let task = this.flow.expect.call(req);
this.state.set(State::ExpectCall(task));
} else {
// the same as expect call.
let task = this.flow.service.call(req);
this.state.set(State::ServiceCall(task));
};
} }
// handle error message.
Some(DispatcherMessage::Error(res)) => { Some(DispatcherMessage::Error(res)) => {
// send_response would update InnerDispatcher state to SendPayload or
// None(If response body is empty).
// continue loop to poll it.
self.as_mut() self.as_mut()
.send_response(res, ResponseBody::Other(Body::Empty))?; .send_response(res, ResponseBody::Other(Body::Empty))?;
true
} }
// return with upgrade request and poll it exclusively.
Some(DispatcherMessage::Upgrade(req)) => { Some(DispatcherMessage::Upgrade(req)) => {
return Ok(PollResponse::Upgrade(req)); return Ok(PollResponse::Upgrade(req));
} }
None => false,
}, // all messages are dealt with.
StateProj::ExpectCall(fut) => match fut.poll(cx) { None => return Ok(PollResponse::DoNothing),
Poll::Ready(Ok(req)) => {
self.as_mut().send_continue();
this = self.as_mut().project();
let fut = this.flow.service.call(req);
this.state.set(State::ServiceCall(fut));
continue;
}
Poll::Ready(Err(e)) => {
let res: Response = e.into().into();
let (res, body) = res.replace_body(());
self.as_mut().send_response(res, body.into_body())?;
true
}
Poll::Pending => false,
}, },
StateProj::ServiceCall(fut) => match fut.poll(cx) { StateProj::ServiceCall(fut) => match fut.poll(cx) {
// service call resolved. send response.
Poll::Ready(Ok(res)) => { Poll::Ready(Ok(res)) => {
let (res, body) = res.into().replace_body(()); let (res, body) = res.into().replace_body(());
self.as_mut().send_response(res, body)?; self.as_mut().send_response(res, body)?;
continue;
} }
Poll::Ready(Err(e)) => {
let res: Response = e.into().into(); // send service call error as response
Poll::Ready(Err(err)) => {
let res: Response = err.into().into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.as_mut().send_response(res, body.into_body())?; self.as_mut().send_response(res, body.into_body())?;
true
} }
Poll::Pending => false,
}, // service call pending and could be waiting for more chunk messages.
StateProj::SendPayload(mut stream) => { // (pipeline message limit and/or payload can_read limit)
loop { Poll::Pending => {
if this.write_buf.len() < super::payload::MAX_BUFFER_SIZE { // no new message is decoded and no new payload is feed.
match stream.as_mut().poll_next(cx) { // nothing to do except waiting for new incoming data from client.
Poll::Ready(Some(Ok(item))) => { if !self.as_mut().poll_request(cx)? {
this.codec.encode( return Ok(PollResponse::DoNothing);
Message::Chunk(Some(item)),
&mut this.write_buf,
)?;
continue;
}
Poll::Ready(None) => {
this.codec.encode(
Message::Chunk(None),
&mut this.write_buf,
)?;
this = self.as_mut().project();
this.state.set(State::None);
}
Poll::Ready(Some(Err(_))) => {
return Err(DispatchError::Unknown)
}
Poll::Pending => return Ok(PollResponse::DoNothing),
}
} else {
return Ok(PollResponse::DrainWriteBuf);
} }
break; // otherwise keep loop.
} }
continue; },
}
};
// state is changed and continue when the state is not Empty StateProj::SendPayload(mut stream) => {
if state_change { // keep populate writer buffer until buffer size limit hit,
if !self.state.is_empty() { // get blocked or finished.
continue; while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE {
} match stream.as_mut().poll_next(cx) {
} else { Poll::Ready(Some(Ok(item))) => {
// if read-backpressure is enabled and we consumed some data. this.codec.encode(
// we may read more data and retry Message::Chunk(Some(item)),
if self.state.is_call() { &mut this.write_buf,
if self.as_mut().poll_request(cx)? { )?;
continue; }
Poll::Ready(None) => {
this.codec
.encode(Message::Chunk(None), &mut this.write_buf)?;
// payload stream finished.
// set state to None and handle next message
this.state.set(State::None);
continue 'res;
}
Poll::Ready(Some(Err(err))) => {
return Err(DispatchError::Service(err))
}
Poll::Pending => return Ok(PollResponse::DoNothing),
}
} }
} else if !self.messages.is_empty() { // buffer is beyond max size.
continue; // return and try to write the whole buffer to io stream.
return Ok(PollResponse::DrainWriteBuf);
} }
StateProj::ExpectCall(fut) => match fut.poll(cx) {
// expect resolved. write continue to buffer and set InnerDispatcher state
// to service call.
Poll::Ready(Ok(req)) => {
this.write_buf
.extend_from_slice(b"HTTP/1.1 100 Continue\r\n\r\n");
let fut = this.flow.service.call(req);
this.state.set(State::ServiceCall(fut));
}
// send expect error as response
Poll::Ready(Err(err)) => {
let res: Response = err.into().into();
let (res, body) = res.replace_body(());
self.as_mut().send_response(res, body.into_body())?;
}
// expect must be solved before progress can be made.
Poll::Pending => return Ok(PollResponse::DoNothing),
},
} }
break;
} }
Ok(PollResponse::DoNothing)
} }
fn handle_request( fn handle_request(
@ -494,9 +465,9 @@ where
// future is error. send response and return a result. On success // future is error. send response and return a result. On success
// 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(e)) => { Poll::Ready(Err(err)) => {
let e = e.into(); let err = err.into();
let res: Response = e.into(); let res: Response = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
return self.send_response(res, body.into_body()); return self.send_response(res, body.into_body());
} }
@ -514,9 +485,9 @@ where
} }
// see the comment on ExpectCall state branch's Pending. // see the comment on ExpectCall state branch's Pending.
Poll::Pending => Ok(()), Poll::Pending => Ok(()),
// see the comment on ExpectCall state branch's Ready(Err(e)). // see the comment on ExpectCall state branch's Ready(Err(err)).
Poll::Ready(Err(e)) => { Poll::Ready(Err(err)) => {
let res: Response = e.into().into(); let res: Response = err.into().into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.send_response(res, body.into_body()) self.send_response(res, body.into_body())
} }
@ -608,25 +579,25 @@ where
// decode is partial and buffer is not full yet. // decode is partial and buffer is not full yet.
// break and wait for more read. // break and wait for more read.
Ok(None) => break, Ok(None) => break,
Err(ParseError::Io(e)) => { Err(ParseError::Io(err)) => {
self.as_mut().client_disconnected(); self.as_mut().client_disconnected();
this = self.as_mut().project(); this = self.as_mut().project();
*this.error = Some(DispatchError::Io(e)); *this.error = Some(DispatchError::Io(err));
break; break;
} }
Err(ParseError::TooLarge) => { Err(ParseError::TooLarge) => {
if let Some(mut payload) = this.payload.take() { if let Some(mut payload) = this.payload.take() {
payload.set_error(PayloadError::Overflow); payload.set_error(PayloadError::Overflow);
} }
// Requests overflow buffer size should be responded with 413 // Requests overflow buffer size should be responded with 431
this.messages.push_back(DispatcherMessage::Error( this.messages.push_back(DispatcherMessage::Error(
Response::PayloadTooLarge().finish().drop_body(), Response::RequestHeaderFieldsTooLarge().finish().drop_body(),
)); ));
this.flags.insert(Flags::READ_DISCONNECT); this.flags.insert(Flags::READ_DISCONNECT);
*this.error = Some(ParseError::TooLarge.into()); *this.error = Some(ParseError::TooLarge.into());
break; break;
} }
Err(e) => { Err(err) => {
if let Some(mut payload) = this.payload.take() { if let Some(mut payload) = this.payload.take() {
payload.set_error(PayloadError::EncodingCorrupted); payload.set_error(PayloadError::EncodingCorrupted);
} }
@ -636,7 +607,7 @@ where
Response::BadRequest().finish().drop_body(), Response::BadRequest().finish().drop_body(),
)); ));
this.flags.insert(Flags::READ_DISCONNECT); this.flags.insert(Flags::READ_DISCONNECT);
*this.error = Some(e.into()); *this.error = Some(err.into());
break; break;
} }
} }
@ -774,7 +745,12 @@ where
// at this point it's not known io is still scheduled to // at this point it's not known io is still scheduled to
// be waked up. so force wake up dispatcher just in case. // be waked up. so force wake up dispatcher just in case.
// TODO: figure out the overhead. // TODO: figure out the overhead.
cx.waker().wake_by_ref(); if this.payload.is_none() {
// When dispatcher has a payload. The responsibility of
// wake up stream would be shift to PayloadSender.
// Therefore no self wake up is needed.
cx.waker().wake_by_ref();
}
return Ok(false); return Ok(false);
} }
@ -839,9 +815,8 @@ where
if inner.flags.contains(Flags::WRITE_DISCONNECT) { if inner.flags.contains(Flags::WRITE_DISCONNECT) {
Poll::Ready(Ok(())) Poll::Ready(Ok(()))
} else { } else {
// flush buffer // flush buffer and wait on block.
inner.as_mut().poll_flush(cx)?; if inner.as_mut().poll_flush(cx)? {
if !inner.write_buf.is_empty() {
Poll::Pending Poll::Pending
} else { } else {
Pin::new(inner.project().io.as_mut().unwrap()) Pin::new(inner.project().io.as_mut().unwrap())

View File

@ -144,104 +144,54 @@ pub(crate) trait MessageType: Sized {
let k = key.as_str().as_bytes(); let k = key.as_str().as_bytes();
let k_len = k.len(); let k_len = k.len();
match value { // TODO: drain?
Value::One(ref val) => { for val in value.iter() {
let v = val.as_ref(); let v = val.as_ref();
let v_len = v.len(); let v_len = v.len();
// key length + value length + colon + space + \r\n // key length + value length + colon + space + \r\n
let len = k_len + v_len + 4; let len = k_len + v_len + 4;
if len > remaining { if len > remaining {
// not enough room in buffer for this header; reserve more space // SAFETY: all the bytes written up to position "pos" are initialized
// the written byte count and pointer advancement are kept in sync
// SAFETY: all the bytes written up to position "pos" are initialized
// the written byte count and pointer advancement are kept in sync
unsafe {
dst.advance_mut(pos);
}
pos = 0;
dst.reserve(len * 2);
remaining = dst.capacity() - dst.len();
// re-assign buf raw pointer since it's possible that the buffer was
// reallocated and/or resized
buf = dst.chunk_mut().as_mut_ptr();
}
// SAFETY: on each write, it is enough to ensure that the advancement of the
// cursor matches the number of bytes written
unsafe { unsafe {
// use upper Camel-Case dst.advance_mut(pos);
if camel_case {
write_camel_case(k, from_raw_parts_mut(buf, k_len))
} else {
write_data(k, buf, k_len)
}
buf = buf.add(k_len);
write_data(b": ", buf, 2);
buf = buf.add(2);
write_data(v, buf, v_len);
buf = buf.add(v_len);
write_data(b"\r\n", buf, 2);
buf = buf.add(2);
} }
pos += len; pos = 0;
remaining -= len; dst.reserve(len * 2);
remaining = dst.capacity() - dst.len();
// re-assign buf raw pointer since it's possible that the buffer was
// reallocated and/or resized
buf = dst.chunk_mut().as_mut_ptr();
} }
Value::Multi(ref vec) => { // SAFETY: on each write, it is enough to ensure that the advancement of
for val in vec { // the cursor matches the number of bytes written
let v = val.as_ref(); unsafe {
let v_len = v.len(); if camel_case {
let len = k_len + v_len + 4; // use Camel-Case headers
write_camel_case(k, from_raw_parts_mut(buf, k_len));
if len > remaining { } else {
// SAFETY: all the bytes written up to position "pos" are initialized write_data(k, buf, k_len);
// the written byte count and pointer advancement are kept in sync
unsafe {
dst.advance_mut(pos);
}
pos = 0;
dst.reserve(len * 2);
remaining = dst.capacity() - dst.len();
// re-assign buf raw pointer since it's possible that the buffer was
// reallocated and/or resized
buf = dst.chunk_mut().as_mut_ptr();
}
// SAFETY: on each write, it is enough to ensure that the advancement of
// the cursor matches the number of bytes written
unsafe {
if camel_case {
write_camel_case(k, from_raw_parts_mut(buf, k_len));
} else {
write_data(k, buf, k_len);
}
buf = buf.add(k_len);
write_data(b": ", buf, 2);
buf = buf.add(2);
write_data(v, buf, v_len);
buf = buf.add(v_len);
write_data(b"\r\n", buf, 2);
buf = buf.add(2);
};
pos += len;
remaining -= len;
} }
}
buf = buf.add(k_len);
write_data(b": ", buf, 2);
buf = buf.add(2);
write_data(v, buf, v_len);
buf = buf.add(v_len);
write_data(b"\r\n", buf, 2);
buf = buf.add(2);
};
pos += len;
remaining -= len;
} }
}); });
@ -579,8 +529,8 @@ mod tests {
); );
} }
#[test] #[actix_rt::test]
fn test_camel_case() { async fn test_camel_case() {
let mut bytes = BytesMut::with_capacity(2048); let mut bytes = BytesMut::with_capacity(2048);
let mut head = RequestHead::default(); let mut head = RequestHead::default();
head.set_camel_case_headers(true); head.set_camel_case_headers(true);
@ -643,8 +593,8 @@ mod tests {
assert!(data.contains("date: date\r\n")); assert!(data.contains("date: date\r\n"));
} }
#[test] #[actix_rt::test]
fn test_extra_headers() { async fn test_extra_headers() {
let mut bytes = BytesMut::with_capacity(2048); let mut bytes = BytesMut::with_capacity(2048);
let mut head = RequestHead::default(); let mut head = RequestHead::default();
@ -677,8 +627,8 @@ mod tests {
assert!(data.contains("date: date\r\n")); assert!(data.contains("date: date\r\n"));
} }
#[test] #[actix_rt::test]
fn test_no_content_length() { async fn test_no_content_length() {
let mut bytes = BytesMut::with_capacity(2048); let mut bytes = BytesMut::with_capacity(2048);
let mut res: Response<()> = let mut res: Response<()> =

View File

@ -1,4 +1,4 @@
//! HTTP/1 implementation //! HTTP/1 protocol implementation.
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
mod client; mod client;

View File

@ -1,4 +1,4 @@
//! HTTP/2 implementation. //! HTTP/2 protocol.
use std::{ use std::{
pin::Pin, pin::Pin,

View File

@ -243,7 +243,7 @@ where
} }
} }
/// `Service` implementation for http/2 transport /// `Service` implementation for HTTP/2 transport
pub struct H2ServiceHandler<T, S, B> pub struct H2ServiceHandler<T, S, B>
where where
S: Service<Request>, S: Service<Request>,

View File

@ -0,0 +1,48 @@
//! Helper trait for types that can be effectively borrowed as a [HeaderValue].
//!
//! [HeaderValue]: crate::http::HeaderValue
use std::{borrow::Cow, str::FromStr};
use http::header::{HeaderName, InvalidHeaderName};
pub trait AsHeaderName: Sealed {}
pub trait Sealed {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName>;
}
impl Sealed for HeaderName {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
Ok(Cow::Borrowed(self))
}
}
impl AsHeaderName for HeaderName {}
impl Sealed for &HeaderName {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
Ok(Cow::Borrowed(*self))
}
}
impl AsHeaderName for &HeaderName {}
impl Sealed for &str {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned)
}
}
impl AsHeaderName for &str {}
impl Sealed for String {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned)
}
}
impl AsHeaderName for String {}
impl Sealed for &String {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned)
}
}
impl AsHeaderName for &String {}

View File

@ -5,7 +5,7 @@ use crate::header::{
self, from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, self, from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate,
IntoHeaderValue, InvalidHeaderValue, Writer, IntoHeaderValue, InvalidHeaderValue, Writer,
}; };
use crate::httpmessage::HttpMessage; use crate::HttpMessage;
/// `If-Range` header, defined in [RFC7233](http://tools.ietf.org/html/rfc7233#section-3.2) /// `If-Range` header, defined in [RFC7233](http://tools.ietf.org/html/rfc7233#section-3.2)
/// ///

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
//! Typed HTTP headers, pre-defined `HeaderName`s, traits for parsing/conversion and other //! Typed HTTP headers, pre-defined `HeaderName`s, traits for parsing and conversion, and other
//! header utility methods. //! header utility methods.
use std::fmt; use std::fmt;
@ -9,8 +9,9 @@ use percent_encoding::{AsciiSet, CONTROLS};
pub use http::header::*; pub use http::header::*;
use crate::error::ParseError; use crate::error::ParseError;
use crate::httpmessage::HttpMessage; use crate::HttpMessage;
mod as_name;
mod into_pair; mod into_pair;
mod into_value; mod into_value;
mod utils; mod utils;
@ -23,6 +24,7 @@ pub use self::common::*;
#[doc(hidden)] #[doc(hidden)]
pub use self::shared::*; pub use self::shared::*;
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;
#[doc(hidden)] #[doc(hidden)]
@ -39,16 +41,14 @@ pub trait Header: IntoHeaderValue {
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>; fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>;
} }
#[doc(hidden)] #[derive(Debug, Default)]
pub(crate) struct Writer { pub(crate) struct Writer {
buf: BytesMut, buf: BytesMut,
} }
impl Writer { impl Writer {
fn new() -> Writer { fn new() -> Writer {
Writer { Writer::default()
buf: BytesMut::new(),
}
} }
fn take(&mut self) -> Bytes { fn take(&mut self) -> Bytes {
@ -71,12 +71,8 @@ impl fmt::Write for Writer {
/// Convert `http::HeaderMap` to our `HeaderMap`. /// Convert `http::HeaderMap` to our `HeaderMap`.
impl From<http::HeaderMap> for HeaderMap { impl From<http::HeaderMap> for HeaderMap {
fn from(map: http::HeaderMap) -> HeaderMap { fn from(mut map: http::HeaderMap) -> HeaderMap {
let mut new_map = HeaderMap::with_capacity(map.capacity()); HeaderMap::from_drain(map.drain())
for (h, v) in map.iter() {
new_map.append(h.clone(), v.clone());
}
new_map
} }
} }

View File

@ -67,6 +67,14 @@ impl Response {
static_resp!(ExpectationFailed, StatusCode::EXPECTATION_FAILED); static_resp!(ExpectationFailed, StatusCode::EXPECTATION_FAILED);
static_resp!(UnprocessableEntity, StatusCode::UNPROCESSABLE_ENTITY); static_resp!(UnprocessableEntity, StatusCode::UNPROCESSABLE_ENTITY);
static_resp!(TooManyRequests, StatusCode::TOO_MANY_REQUESTS); static_resp!(TooManyRequests, StatusCode::TOO_MANY_REQUESTS);
static_resp!(
RequestHeaderFieldsTooLarge,
StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE
);
static_resp!(
UnavailableForLegalReasons,
StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS
);
static_resp!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR); static_resp!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR);
static_resp!(NotImplemented, StatusCode::NOT_IMPLEMENTED); static_resp!(NotImplemented, StatusCode::NOT_IMPLEMENTED);

View File

@ -1,6 +1,6 @@
//! HTTP primitives for the Actix ecosystem. //! HTTP primitives for the Actix ecosystem.
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms, nonstandard_style)]
#![allow( #![allow(
clippy::type_complexity, clippy::type_complexity,
clippy::too_many_arguments, clippy::too_many_arguments,
@ -25,8 +25,8 @@ pub mod encoding;
mod extensions; mod extensions;
mod header; mod header;
mod helpers; mod helpers;
mod httpcodes; mod http_codes;
pub mod httpmessage; mod http_message;
mod message; mod message;
mod payload; mod payload;
mod request; mod request;
@ -45,7 +45,7 @@ pub use self::builder::HttpServiceBuilder;
pub use self::config::{KeepAlive, ServiceConfig}; pub use self::config::{KeepAlive, ServiceConfig};
pub use self::error::{Error, ResponseError, Result}; pub use self::error::{Error, ResponseError, Result};
pub use self::extensions::Extensions; pub use self::extensions::Extensions;
pub use self::httpmessage::HttpMessage; pub use self::http_message::HttpMessage;
pub use self::message::{Message, RequestHead, RequestHeadType, ResponseHead}; pub use self::message::{Message, RequestHead, RequestHeadType, ResponseHead};
pub use self::payload::{Payload, PayloadStream}; pub use self::payload::{Payload, PayloadStream};
pub use self::request::Request; pub use self::request::Request;

View File

@ -13,8 +13,10 @@ use crate::http::{header, Method, StatusCode, Uri, Version};
pub enum ConnectionType { pub enum ConnectionType {
/// Close connection after response /// Close connection after response
Close, Close,
/// Keep connection alive after response /// Keep connection alive after response
KeepAlive, KeepAlive,
/// Connection is upgraded to different type /// Connection is upgraded to different type
Upgrade, Upgrade,
} }

View File

@ -9,9 +9,9 @@ use http::{header, Method, Uri, Version};
use crate::extensions::Extensions; use crate::extensions::Extensions;
use crate::header::HeaderMap; use crate::header::HeaderMap;
use crate::httpmessage::HttpMessage;
use crate::message::{Message, RequestHead}; use crate::message::{Message, RequestHead};
use crate::payload::{Payload, PayloadStream}; use crate::payload::{Payload, PayloadStream};
use crate::HttpMessage;
/// Request /// Request
pub struct Request<P = PayloadStream> { pub struct Request<P = PayloadStream> {
@ -107,7 +107,7 @@ impl<P> Request<P> {
#[inline] #[inline]
#[doc(hidden)] #[doc(hidden)]
/// Mutable reference to a http message part of the request /// Mutable reference to a HTTP message part of the request
pub fn head_mut(&mut self) -> &mut RequestHead { pub fn head_mut(&mut self) -> &mut RequestHead {
&mut *self.head &mut *self.head
} }
@ -158,10 +158,12 @@ impl<P> Request<P> {
self.head().method == Method::CONNECT self.head().method == Method::CONNECT
} }
/// Peer socket address /// Peer socket address.
/// ///
/// Peer address is actual socket address, if proxy is used in front of /// Peer address is the directly connected peer's socket address. If a proxy is used in front of
/// actix http server, then peer address would be address of this proxy. /// the Actix Web server, then it would be address of this proxy.
///
/// Will only return None when called in unit tests.
#[inline] #[inline]
pub fn peer_addr(&self) -> Option<net::SocketAddr> { pub fn peer_addr(&self) -> Option<net::SocketAddr> {
self.head().peer_addr self.head().peer_addr
@ -177,13 +179,17 @@ impl<P> fmt::Debug for Request<P> {
self.method(), self.method(),
self.path() self.path()
)?; )?;
if let Some(q) = self.uri().query().as_ref() { if let Some(q) = self.uri().query().as_ref() {
writeln!(f, " query: ?{:?}", q)?; writeln!(f, " query: ?{:?}", q)?;
} }
writeln!(f, " headers:")?; writeln!(f, " headers:")?;
for (key, val) in self.headers() {
for (key, val) in self.headers().iter() {
writeln!(f, " {:?}: {:?}", key, val)?; writeln!(f, " {:?}: {:?}", key, val)?;
} }
Ok(()) Ok(())
} }
} }

View File

@ -32,13 +32,13 @@ pub struct Response<B = Body> {
} }
impl Response<Body> { impl Response<Body> {
/// Create http response builder with specific status. /// Create HTTP response builder with specific status.
#[inline] #[inline]
pub fn build(status: StatusCode) -> ResponseBuilder { pub fn build(status: StatusCode) -> ResponseBuilder {
ResponseBuilder::new(status) ResponseBuilder::new(status)
} }
/// Create http response builder /// Create HTTP response builder
#[inline] #[inline]
pub fn build_from<T: Into<ResponseBuilder>>(source: T) -> ResponseBuilder { pub fn build_from<T: Into<ResponseBuilder>>(source: T) -> ResponseBuilder {
source.into() source.into()
@ -97,7 +97,7 @@ impl<B> Response<B> {
} }
#[inline] #[inline]
/// Mutable reference to a http message part of the response /// Mutable reference to a HTTP message part of the response
pub fn head_mut(&mut self) -> &mut ResponseHead { pub fn head_mut(&mut self) -> &mut ResponseHead {
&mut *self.head &mut *self.head
} }
@ -363,7 +363,9 @@ impl ResponseBuilder {
{ {
if let Some(parts) = parts(&mut self.head, &self.err) { if let Some(parts) = parts(&mut self.head, &self.err) {
match header.try_into_header_pair() { match header.try_into_header_pair() {
Ok((key, value)) => parts.headers.insert(key, value), Ok((key, value)) => {
parts.headers.insert(key, value);
}
Err(e) => self.err = Some(e.into()), Err(e) => self.err = Some(e.into()),
}; };
} }
@ -752,9 +754,11 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder {
let mut msg = BoxedResponseHead::new(head.status); let mut msg = BoxedResponseHead::new(head.status);
msg.version = head.version; msg.version = head.version;
msg.reason = head.reason; msg.reason = head.reason;
for (k, v) in &head.headers {
for (k, v) in head.headers.iter() {
msg.headers.append(k.clone(), v.clone()); msg.headers.append(k.clone(), v.clone());
} }
msg.no_chunking(!head.chunked()); msg.no_chunking(!head.chunked());
ResponseBuilder { ResponseBuilder {
@ -863,6 +867,7 @@ mod tests {
use super::*; use super::*;
use crate::body::Body; use crate::body::Body;
use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE, SET_COOKIE}; use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE, SET_COOKIE};
use crate::HttpMessage;
#[test] #[test]
fn test_debug() { fn test_debug() {
@ -876,8 +881,6 @@ mod tests {
#[test] #[test]
fn test_response_cookies() { fn test_response_cookies() {
use crate::httpmessage::HttpMessage;
let req = crate::test::TestRequest::default() let req = crate::test::TestRequest::default()
.append_header((COOKIE, "cookie1=value1")) .append_header((COOKIE, "cookie1=value1"))
.append_header((COOKIE, "cookie2=value2")) .append_header((COOKIE, "cookie2=value2"))
@ -893,16 +896,20 @@ mod tests {
.max_age(time::Duration::days(1)) .max_age(time::Duration::days(1))
.finish(), .finish(),
) )
.del_cookie(&cookies[1]) .del_cookie(&cookies[0])
.finish(); .finish();
let mut val: Vec<_> = resp let mut val = resp
.headers() .headers()
.get_all(SET_COOKIE) .get_all(SET_COOKIE)
.map(|v| v.to_str().unwrap().to_owned()) .map(|v| v.to_str().unwrap().to_owned())
.collect(); .collect::<Vec<_>>();
val.sort(); val.sort();
// the .del_cookie call
assert!(val[0].starts_with("cookie1=; Max-Age=0;")); assert!(val[0].starts_with("cookie1=; Max-Age=0;"));
// the .cookie call
assert_eq!( assert_eq!(
val[1], val[1],
"name=value; HttpOnly; Path=/test; Domain=www.rust-lang.org; Max-Age=86400" "name=value; HttpOnly; Path=/test; Domain=www.rust-lang.org; Max-Age=86400"
@ -927,9 +934,9 @@ mod tests {
let mut iter = r.cookies(); let mut iter = r.cookies();
let v = iter.next().unwrap(); let v = iter.next().unwrap();
assert_eq!((v.name(), v.value()), ("cookie3", "val300"));
let v = iter.next().unwrap();
assert_eq!((v.name(), v.value()), ("original", "val100")); assert_eq!((v.name(), v.value()), ("original", "val100"));
let v = iter.next().unwrap();
assert_eq!((v.name(), v.value()), ("cookie3", "val300"));
} }
#[test] #[test]

View File

@ -432,7 +432,7 @@ where
} }
} }
/// `Service` implementation for http transport /// `Service` implementation for HTTP transport
pub struct HttpServiceHandler<T, S, B, X, U> pub struct HttpServiceHandler<T, S, B, X, U>
where where
S: Service<Request>, S: Service<Request>,

View File

@ -8,35 +8,65 @@ pub fn parse_http_date(time: &str) -> Option<PrimitiveDateTime> {
} }
/// Attempt to parse a `time` string as a RFC 1123 formatted date time string. /// Attempt to parse a `time` string as a RFC 1123 formatted date time string.
///
/// Eg: `Fri, 12 Feb 2021 00:14:29 GMT`
fn try_parse_rfc_1123(time: &str) -> Option<PrimitiveDateTime> { fn try_parse_rfc_1123(time: &str) -> Option<PrimitiveDateTime> {
time::parse(time, "%a, %d %b %Y %H:%M:%S").ok() time::parse(time, "%a, %d %b %Y %H:%M:%S").ok()
} }
/// Attempt to parse a `time` string as a RFC 850 formatted date time string. /// Attempt to parse a `time` string as a RFC 850 formatted date time string.
///
/// Eg: `Wednesday, 11-Jan-21 13:37:41 UTC`
fn try_parse_rfc_850(time: &str) -> Option<PrimitiveDateTime> { fn try_parse_rfc_850(time: &str) -> Option<PrimitiveDateTime> {
match PrimitiveDateTime::parse(time, "%A, %d-%b-%y %H:%M:%S") { let dt = PrimitiveDateTime::parse(time, "%A, %d-%b-%y %H:%M:%S").ok()?;
Ok(dt) => {
// If the `time` string contains a two-digit year, then as per RFC 2616 § 19.3,
// we consider the year as part of this century if it's within the next 50 years,
// otherwise we consider as part of the previous century.
let now = OffsetDateTime::now_utc();
let century_start_year = (now.year() / 100) * 100;
let mut expanded_year = century_start_year + dt.year();
if expanded_year > now.year() + 50 { // If the `time` string contains a two-digit year, then as per RFC 2616 § 19.3,
expanded_year -= 100; // we consider the year as part of this century if it's within the next 50 years,
} // otherwise we consider as part of the previous century.
match Date::try_from_ymd(expanded_year, dt.month(), dt.day()) { let now = OffsetDateTime::now_utc();
Ok(date) => Some(PrimitiveDateTime::new(date, dt.time())), let century_start_year = (now.year() / 100) * 100;
Err(_) => None, let mut expanded_year = century_start_year + dt.year();
}
} if expanded_year > now.year() + 50 {
Err(_) => None, expanded_year -= 100;
} }
let date = Date::try_from_ymd(expanded_year, dt.month(), dt.day()).ok()?;
Some(PrimitiveDateTime::new(date, dt.time()))
} }
/// Attempt to parse a `time` string using ANSI C's `asctime` format. /// Attempt to parse a `time` string using ANSI C's `asctime` format.
///
/// Eg: `Wed Feb 13 15:46:11 2013`
fn try_parse_asctime(time: &str) -> Option<PrimitiveDateTime> { fn try_parse_asctime(time: &str) -> Option<PrimitiveDateTime> {
time::parse(time, "%a %b %_d %H:%M:%S %Y").ok() time::parse(time, "%a %b %_d %H:%M:%S %Y").ok()
} }
#[cfg(test)]
mod tests {
use time::{date, time};
use super::*;
#[test]
fn test_rfc_850_year_shift() {
let date = try_parse_rfc_850("Friday, 19-Nov-82 16:14:55 EST").unwrap();
assert_eq!(date, date!(1982 - 11 - 19).with_time(time!(16:14:55)));
let date = try_parse_rfc_850("Wednesday, 11-Jan-62 13:37:41 EST").unwrap();
assert_eq!(date, date!(2062 - 01 - 11).with_time(time!(13:37:41)));
let date = try_parse_rfc_850("Wednesday, 11-Jan-21 13:37:41 EST").unwrap();
assert_eq!(date, date!(2021 - 01 - 11).with_time(time!(13:37:41)));
let date = try_parse_rfc_850("Wednesday, 11-Jan-23 13:37:41 EST").unwrap();
assert_eq!(date, date!(2023 - 01 - 11).with_time(time!(13:37:41)));
let date = try_parse_rfc_850("Wednesday, 11-Jan-99 13:37:41 EST").unwrap();
assert_eq!(date, date!(1999 - 01 - 11).with_time(time!(13:37:41)));
let date = try_parse_rfc_850("Wednesday, 11-Jan-00 13:37:41 EST").unwrap();
assert_eq!(date, date!(2000 - 01 - 11).with_time(time!(13:37:41)));
}
}

View File

@ -54,7 +54,7 @@ pub enum Frame {
Close(Option<CloseReason>), Close(Option<CloseReason>),
} }
/// A `WebSocket` continuation item. /// A WebSocket continuation item.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum Item { pub enum Item {
FirstText(Bytes), FirstText(Bytes),
@ -79,7 +79,7 @@ bitflags! {
} }
impl Codec { impl Codec {
/// Create new websocket frames decoder. /// Create new WebSocket frames decoder.
pub fn new() -> Codec { pub fn new() -> Codec {
Codec { Codec {
max_size: 65_536, max_size: 65_536,

View File

@ -7,7 +7,7 @@ use crate::ws::mask::apply_mask;
use crate::ws::proto::{CloseCode, CloseReason, OpCode}; use crate::ws::proto::{CloseCode, CloseReason, OpCode};
use crate::ws::ProtocolError; use crate::ws::ProtocolError;
/// A struct representing a `WebSocket` frame. /// A struct representing a WebSocket frame.
#[derive(Debug)] #[derive(Debug)]
pub struct Parser; pub struct Parser;
@ -16,7 +16,8 @@ impl Parser {
src: &[u8], src: &[u8],
server: bool, server: bool,
max_size: usize, max_size: usize,
) -> Result<Option<(usize, bool, OpCode, usize, Option<u32>)>, ProtocolError> { ) -> Result<Option<(usize, bool, OpCode, usize, Option<[u8; 4]>)>, ProtocolError>
{
let chunk_len = src.len(); let chunk_len = src.len();
let mut idx = 2; let mut idx = 2;
@ -77,9 +78,10 @@ impl Parser {
return Ok(None); return Ok(None);
} }
let mask = let mask = TryFrom::try_from(&src[idx..idx + 4]).unwrap();
u32::from_le_bytes(TryFrom::try_from(&src[idx..idx + 4]).unwrap());
idx += 4; idx += 4;
Some(mask) Some(mask)
} else { } else {
None None
@ -187,8 +189,8 @@ impl Parser {
}; };
if mask { if mask {
let mask = rand::random::<u32>(); let mask = rand::random::<[u8; 4]>();
dst.put_u32_le(mask); dst.put_slice(mask.as_ref());
dst.put_slice(payload.as_ref()); dst.put_slice(payload.as_ref());
let pos = dst.len() - payload_len; let pos = dst.len() - payload_len;
apply_mask(&mut dst[pos..], mask); apply_mask(&mut dst[pos..], mask);

View File

@ -1,136 +1,57 @@
//! This is code from [Tungstenite project](https://github.com/snapview/tungstenite-rs) //! This is code from [Tungstenite project](https://github.com/snapview/tungstenite-rs)
#![allow(clippy::cast_ptr_alignment)]
use std::ptr::copy_nonoverlapping;
use std::slice;
/// Holds a slice guaranteed to be shorter than 8 bytes. /// Mask/unmask a frame.
struct ShortSlice<'a> {
inner: &'a mut [u8],
}
impl<'a> ShortSlice<'a> {
/// # Safety
/// Given slice must be shorter than 8 bytes.
unsafe fn new(slice: &'a mut [u8]) -> Self {
// Sanity check for debug builds
debug_assert!(slice.len() < 8);
ShortSlice { inner: slice }
}
fn len(&self) -> usize {
self.inner.len()
}
}
/// Faster version of `apply_mask()` which operates on 8-byte blocks.
#[inline] #[inline]
#[allow(clippy::cast_lossless)] pub fn apply_mask(buf: &mut [u8], mask: [u8; 4]) {
pub(crate) fn apply_mask(buf: &mut [u8], mask_u32: u32) { apply_mask_fast32(buf, mask)
// Extend the mask to 64 bits }
let mut mask_u64 = ((mask_u32 as u64) << 32) | (mask_u32 as u64);
// Split the buffer into three segments
let (head, mid, tail) = align_buf(buf);
// Initial unaligned segment /// A safe unoptimized mask application.
let head_len = head.len(); #[inline]
if head_len > 0 { fn apply_mask_fallback(buf: &mut [u8], mask: [u8; 4]) {
xor_short(head, mask_u64); for (i, byte) in buf.iter_mut().enumerate() {
*byte ^= mask[i & 3];
}
}
/// Faster version of `apply_mask()` which operates on 4-byte blocks.
#[inline]
pub fn apply_mask_fast32(buf: &mut [u8], mask: [u8; 4]) {
let mask_u32 = u32::from_ne_bytes(mask);
// SAFETY:
//
// buf is a valid slice borrowed mutably from bytes::BytesMut.
//
// un aligned prefix and suffix would be mask/unmask per byte.
// proper aligned middle slice goes into fast path and operates on 4-byte blocks.
let (mut prefix, words, mut suffix) = unsafe { buf.align_to_mut::<u32>() };
apply_mask_fallback(&mut prefix, mask);
let head = prefix.len() & 3;
let mask_u32 = if head > 0 {
if cfg!(target_endian = "big") { if cfg!(target_endian = "big") {
mask_u64 = mask_u64.rotate_left(8 * head_len as u32); mask_u32.rotate_left(8 * head as u32)
} else { } else {
mask_u64 = mask_u64.rotate_right(8 * head_len as u32); mask_u32.rotate_right(8 * head as u32)
}
}
// Aligned segment
for v in mid {
*v ^= mask_u64;
}
// Final unaligned segment
if tail.len() > 0 {
xor_short(tail, mask_u64);
}
}
// TODO: copy_nonoverlapping here compiles to call memcpy. While it is not so
// inefficient, it could be done better. The compiler does not understand that
// a `ShortSlice` must be smaller than a u64.
#[inline]
#[allow(clippy::needless_pass_by_value)]
fn xor_short(buf: ShortSlice<'_>, mask: u64) {
// SAFETY: we know that a `ShortSlice` fits in a u64
unsafe {
let (ptr, len) = (buf.inner.as_mut_ptr(), buf.len());
let mut b: u64 = 0;
#[allow(trivial_casts)]
copy_nonoverlapping(ptr, &mut b as *mut _ as *mut u8, len);
b ^= mask;
#[allow(trivial_casts)]
copy_nonoverlapping(&b as *const _ as *const u8, ptr, len);
}
}
/// # Safety
/// Caller must ensure the buffer has the correct size and alignment.
#[inline]
unsafe fn cast_slice(buf: &mut [u8]) -> &mut [u64] {
// Assert correct size and alignment in debug builds
debug_assert!(buf.len().trailing_zeros() >= 3);
debug_assert!((buf.as_ptr() as usize).trailing_zeros() >= 3);
slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u64, buf.len() >> 3)
}
/// Splits a slice into three parts:
/// - an unaligned short head
/// - an aligned `u64` slice mid section
/// - an unaligned short tail
#[inline]
fn align_buf(buf: &mut [u8]) -> (ShortSlice<'_>, &mut [u64], ShortSlice<'_>) {
let start_ptr = buf.as_ptr() as usize;
let end_ptr = start_ptr + buf.len();
// Round *up* to next aligned boundary for start
let start_aligned = (start_ptr + 7) & !0x7;
// Round *down* to last aligned boundary for end
let end_aligned = end_ptr & !0x7;
if end_aligned >= start_aligned {
// We have our three segments (head, mid, tail)
let (tmp, tail) = buf.split_at_mut(end_aligned - start_ptr);
let (head, mid) = tmp.split_at_mut(start_aligned - start_ptr);
// SAFETY: we know the middle section is correctly aligned, and the outer
// sections are smaller than 8 bytes
unsafe {
(
ShortSlice::new(head),
cast_slice(mid),
ShortSlice::new(tail),
)
} }
} else { } else {
// We didn't cross even one aligned boundary! mask_u32
};
// SAFETY: The outer sections are smaller than 8 bytes for word in words.iter_mut() {
unsafe { (ShortSlice::new(buf), &mut [], ShortSlice::new(&mut [])) } *word ^= mask_u32;
} }
apply_mask_fallback(&mut suffix, mask_u32.to_ne_bytes());
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::apply_mask; use super::*;
/// A safe unoptimized mask application.
fn apply_mask_fallback(buf: &mut [u8], mask: &[u8; 4]) {
for (i, byte) in buf.iter_mut().enumerate() {
*byte ^= mask[i & 3];
}
}
// legacy test from old apply mask test. kept for now for back compat test.
// TODO: remove it and favor the other test.
#[test] #[test]
fn test_apply_mask() { fn test_apply_mask_legacy() {
let mask = [0x6d, 0xb6, 0xb2, 0x80]; let mask = [0x6d, 0xb6, 0xb2, 0x80];
let mask_u32 = u32::from_le_bytes(mask);
let unmasked = vec![ let unmasked = vec![
0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, 0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17,
@ -140,10 +61,10 @@ mod tests {
// Check masking with proper alignment. // Check masking with proper alignment.
{ {
let mut masked = unmasked.clone(); let mut masked = unmasked.clone();
apply_mask_fallback(&mut masked, &mask); apply_mask_fallback(&mut masked, mask);
let mut masked_fast = unmasked.clone(); let mut masked_fast = unmasked.clone();
apply_mask(&mut masked_fast, mask_u32); apply_mask(&mut masked_fast, mask);
assert_eq!(masked, masked_fast); assert_eq!(masked, masked_fast);
} }
@ -151,12 +72,38 @@ mod tests {
// Check masking without alignment. // Check masking without alignment.
{ {
let mut masked = unmasked.clone(); let mut masked = unmasked.clone();
apply_mask_fallback(&mut masked[1..], &mask); apply_mask_fallback(&mut masked[1..], mask);
let mut masked_fast = unmasked; let mut masked_fast = unmasked;
apply_mask(&mut masked_fast[1..], mask_u32); apply_mask(&mut masked_fast[1..], mask);
assert_eq!(masked, masked_fast); assert_eq!(masked, masked_fast);
} }
} }
#[test]
fn test_apply_mask() {
let mask = [0x6d, 0xb6, 0xb2, 0x80];
let unmasked = vec![
0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17,
0x74, 0xf9, 0x12, 0x03,
];
for data_len in 0..=unmasked.len() {
let unmasked = &unmasked[0..data_len];
// Check masking with different alignment.
for off in 0..=3 {
if unmasked.len() < off {
continue;
}
let mut masked = unmasked.to_vec();
apply_mask_fallback(&mut masked[off..], mask);
let mut masked_fast = unmasked.to_vec();
apply_mask_fast32(&mut masked_fast[off..], mask);
assert_eq!(masked, masked_fast);
}
}
}
} }

View File

@ -1,6 +1,6 @@
//! WebSocket protocol support. //! WebSocket protocol.
//! //!
//! To setup a WebSocket, first do web socket handshake then on success convert `Payload` into a //! To setup a WebSocket, first perform the WebSocket handshake then on success convert `Payload` into a
//! `WsStream` stream and then use `WsWriter` to communicate with the peer. //! `WsStream` stream and then use `WsWriter` to communicate with the peer.
use std::io; use std::io;
@ -76,7 +76,7 @@ pub enum HandshakeError {
#[display(fmt = "Method not allowed.")] #[display(fmt = "Method not allowed.")]
GetMethodRequired, GetMethodRequired,
/// Upgrade header if not set to websocket. /// Upgrade header if not set to WebSocket.
#[display(fmt = "WebSocket upgrade is expected.")] #[display(fmt = "WebSocket upgrade is expected.")]
NoWebsocketUpgrade, NoWebsocketUpgrade,
@ -88,7 +88,7 @@ pub enum HandshakeError {
#[display(fmt = "WebSocket version header is required.")] #[display(fmt = "WebSocket version header is required.")]
NoVersionHeader, NoVersionHeader,
/// Unsupported websocket version. /// Unsupported WebSocket version.
#[display(fmt = "Unsupported version.")] #[display(fmt = "Unsupported version.")]
UnsupportedVersion, UnsupportedVersion,
@ -127,20 +127,20 @@ impl ResponseError for HandshakeError {
} }
} }
/// Verify `WebSocket` handshake request and create handshake response. /// Verify WebSocket handshake request and create handshake response.
pub fn handshake(req: &RequestHead) -> Result<ResponseBuilder, HandshakeError> { pub fn handshake(req: &RequestHead) -> Result<ResponseBuilder, HandshakeError> {
verify_handshake(req)?; verify_handshake(req)?;
Ok(handshake_response(req)) Ok(handshake_response(req))
} }
/// Verify `WebSocket` handshake request. /// Verify WebSocket handshake request.
pub fn verify_handshake(req: &RequestHead) -> Result<(), HandshakeError> { pub fn verify_handshake(req: &RequestHead) -> Result<(), HandshakeError> {
// WebSocket accepts only GET // WebSocket accepts only GET
if req.method != Method::GET { if req.method != Method::GET {
return Err(HandshakeError::GetMethodRequired); return Err(HandshakeError::GetMethodRequired);
} }
// Check for "UPGRADE" to websocket header // Check for "UPGRADE" to WebSocket header
let has_hdr = if let Some(hdr) = req.headers().get(header::UPGRADE) { let has_hdr = if let Some(hdr) = req.headers().get(header::UPGRADE) {
if let Ok(s) = hdr.to_str() { if let Ok(s) = hdr.to_str() {
s.to_ascii_lowercase().contains("websocket") s.to_ascii_lowercase().contains("websocket")
@ -181,7 +181,7 @@ pub fn verify_handshake(req: &RequestHead) -> Result<(), HandshakeError> {
Ok(()) Ok(())
} }
/// Create websocket handshake response /// Create WebSocket handshake response.
/// ///
/// This function returns handshake `Response`, ready to send to peer. /// This function returns handshake `Response`, ready to send to peer.
pub fn handshake_response(req: &RequestHead) -> ResponseBuilder { pub fn handshake_response(req: &RequestHead) -> ResponseBuilder {

View File

@ -74,8 +74,7 @@ impl From<u8> for OpCode {
} }
} }
/// Status code used to indicate why an endpoint is closing the `WebSocket` /// Status code used to indicate why an endpoint is closing the WebSocket connection.
/// connection.
#[derive(Debug, Eq, PartialEq, Clone, Copy)] #[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum CloseCode { pub enum CloseCode {
/// Indicates a normal closure, meaning that the purpose for /// Indicates a normal closure, meaning that the purpose for

View File

@ -7,7 +7,7 @@ use std::io;
use actix_http::error::{ErrorBadRequest, PayloadError}; use actix_http::error::{ErrorBadRequest, PayloadError};
use actix_http::http::header::{self, HeaderName, HeaderValue}; use actix_http::http::header::{self, HeaderName, HeaderValue};
use actix_http::http::{Method, StatusCode, Version}; use actix_http::http::{Method, StatusCode, Version};
use actix_http::httpmessage::HttpMessage; use actix_http::HttpMessage;
use actix_http::{body, Error, HttpService, Request, Response}; use actix_http::{body, Error, 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};

View File

@ -10,7 +10,7 @@ use futures_util::future::{self, err, ok, ready, FutureExt};
use futures_util::stream::{once, StreamExt}; use futures_util::stream::{once, StreamExt};
use regex::Regex; use regex::Regex;
use actix_http::httpmessage::HttpMessage; use actix_http::HttpMessage;
use actix_http::{ use actix_http::{
body, error, http, http::header, Error, HttpService, KeepAlive, Request, Response, body, error, http, http::header, Error, HttpService, KeepAlive, Request, Response,
}; };

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.4.0-beta.2 - 2021-02-10
* No notable changes.
## 0.4.0-beta.1 - 2021-01-07 ## 0.4.0-beta.1 - 2021-01-07
* Fix multipart consuming payload before header checks. [#1513] * Fix multipart consuming payload before header checks. [#1513]
* Update `bytes` to `1.0`. [#1813] * Update `bytes` to `1.0`. [#1813]

View File

@ -1,8 +1,8 @@
[package] [package]
name = "actix-multipart" name = "actix-multipart"
version = "0.4.0-beta.1" version = "0.4.0-beta.2"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Multipart support for actix web framework." description = "Multipart form support for Actix Web"
readme = "README.md" readme = "README.md"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
@ -16,7 +16,7 @@ name = "actix_multipart"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
actix-web = { version = "4.0.0-beta.1", default-features = false } actix-web = { version = "4.0.0-beta.3", default-features = false }
actix-utils = "3.0.0-beta.2" actix-utils = "3.0.0-beta.2"
bytes = "1" bytes = "1"
@ -29,4 +29,4 @@ twoway = "0.2"
[dev-dependencies] [dev-dependencies]
actix-rt = "2" actix-rt = "2"
actix-http = "3.0.0-beta.1" actix-http = "3.0.0-beta.3"

View File

@ -1,8 +1,18 @@
# Multipart support for actix web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-multipart)](https://crates.io/crates/actix-multipart) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) # actix-multipart
## Documentation & community resources > Multipart form support for Actix Web.
* [API Documentation](https://docs.rs/actix-multipart/) [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart)
* [Chat on gitter](https://gitter.im/actix/actix) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.2)](https://docs.rs/actix-multipart/0.4.0-beta.2)
* Cargo package: [actix-multipart](https://crates.io/crates/actix-multipart) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
* Minimum supported Rust version: 1.40 or later ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.2/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.2)
[![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-multipart)
- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum Supported Rust Version (MSRV): 1.46.0

View File

@ -1,4 +1,4 @@
//! Multipart form support for Actix web. //! Multipart form support for Actix Web.
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms)]
#![allow(clippy::borrow_interior_mutable_const)] #![allow(clippy::borrow_interior_mutable_const)]

View File

@ -13,9 +13,7 @@ use futures_util::stream::{LocalBoxStream, Stream, StreamExt};
use actix_utils::task::LocalWaker; use actix_utils::task::LocalWaker;
use actix_web::error::{ParseError, PayloadError}; use actix_web::error::{ParseError, PayloadError};
use actix_web::http::header::{ use actix_web::http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue};
self, ContentDisposition, HeaderMap, HeaderName, HeaderValue,
};
use crate::error::MultipartError; use crate::error::MultipartError;
@ -120,10 +118,7 @@ impl Multipart {
impl Stream for Multipart { impl Stream for Multipart {
type Item = Result<Field, MultipartError>; type Item = Result<Field, MultipartError>;
fn poll_next( fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
if let Some(err) = self.error.take() { if let Some(err) = self.error.take() {
Poll::Ready(Some(Err(err))) Poll::Ready(Some(Err(err)))
} else if self.safety.current() { } else if self.safety.current() {
@ -142,9 +137,7 @@ impl Stream for Multipart {
} }
impl InnerMultipart { impl InnerMultipart {
fn read_headers( fn read_headers(payload: &mut PayloadBuffer) -> Result<Option<HeaderMap>, MultipartError> {
payload: &mut PayloadBuffer,
) -> Result<Option<HeaderMap>, MultipartError> {
match payload.read_until(b"\r\n\r\n")? { match payload.read_until(b"\r\n\r\n")? {
None => { None => {
if payload.eof { if payload.eof {
@ -226,8 +219,7 @@ impl InnerMultipart {
if chunk.len() < boundary.len() { if chunk.len() < boundary.len() {
continue; continue;
} }
if &chunk[..2] == b"--" if &chunk[..2] == b"--" && &chunk[2..chunk.len() - 2] == boundary.as_bytes()
&& &chunk[2..chunk.len() - 2] == boundary.as_bytes()
{ {
break; break;
} else { } else {
@ -273,9 +265,7 @@ impl InnerMultipart {
match field.borrow_mut().poll(safety) { match field.borrow_mut().poll(safety) {
Poll::Pending => return Poll::Pending, Poll::Pending => return Poll::Pending,
Poll::Ready(Some(Ok(_))) => continue, Poll::Ready(Some(Ok(_))) => continue,
Poll::Ready(Some(Err(e))) => { Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
return Poll::Ready(Some(Err(e)))
}
Poll::Ready(None) => true, Poll::Ready(None) => true,
} }
} }
@ -311,10 +301,7 @@ impl InnerMultipart {
} }
// read boundary // read boundary
InnerState::Boundary => { InnerState::Boundary => {
match InnerMultipart::read_boundary( match InnerMultipart::read_boundary(&mut *payload, &self.boundary)? {
&mut *payload,
&self.boundary,
)? {
None => return Poll::Pending, None => return Poll::Pending,
Some(eof) => { Some(eof) => {
if eof { if eof {
@ -418,8 +405,7 @@ impl Field {
pub fn content_disposition(&self) -> Option<ContentDisposition> { pub fn content_disposition(&self) -> Option<ContentDisposition> {
// RFC 7578: 'Each part MUST contain a Content-Disposition header field // RFC 7578: 'Each part MUST contain a Content-Disposition header field
// where the disposition type is "form-data".' // where the disposition type is "form-data".'
if let Some(content_disposition) = self.headers.get(&header::CONTENT_DISPOSITION) if let Some(content_disposition) = self.headers.get(&header::CONTENT_DISPOSITION) {
{
ContentDisposition::from_raw(content_disposition).ok() ContentDisposition::from_raw(content_disposition).ok()
} else { } else {
None None
@ -430,15 +416,10 @@ impl Field {
impl Stream for Field { impl Stream for Field {
type Item = Result<Bytes, MultipartError>; type Item = Result<Bytes, MultipartError>;
fn poll_next( fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
if self.safety.current() { if self.safety.current() {
let mut inner = self.inner.borrow_mut(); let mut inner = self.inner.borrow_mut();
if let Some(mut payload) = if let Some(mut payload) = inner.payload.as_ref().unwrap().get_mut(&self.safety) {
inner.payload.as_ref().unwrap().get_mut(&self.safety)
{
payload.poll_stream(cx)?; payload.poll_stream(cx)?;
} }
inner.poll(&self.safety) inner.poll(&self.safety)
@ -607,8 +588,7 @@ impl InnerField {
return Poll::Ready(None); return Poll::Ready(None);
} }
let result = if let Some(mut payload) = self.payload.as_ref().unwrap().get_mut(s) let result = if let Some(mut payload) = self.payload.as_ref().unwrap().get_mut(s) {
{
if !self.eof { if !self.eof {
let res = if let Some(ref mut len) = self.length { let res = if let Some(ref mut len) = self.length {
InnerField::read_len(&mut *payload, len) InnerField::read_len(&mut *payload, len)
@ -628,7 +608,9 @@ impl InnerField {
Ok(None) => Poll::Pending, Ok(None) => Poll::Pending,
Ok(Some(line)) => { Ok(Some(line)) => {
if line.as_ref() != b"\r\n" { if line.as_ref() != b"\r\n" {
log::warn!("multipart field did not read all the data or it is malformed"); log::warn!(
"multipart field did not read all the data or it is malformed"
);
} }
Poll::Ready(None) Poll::Ready(None)
} }
@ -804,9 +786,7 @@ impl PayloadBuffer {
/// Read bytes until new line delimiter or eof /// Read bytes until new line delimiter or eof
pub fn readline_or_eof(&mut self) -> Result<Option<Bytes>, MultipartError> { pub fn readline_or_eof(&mut self) -> Result<Option<Bytes>, MultipartError> {
match self.readline() { match self.readline() {
Err(MultipartError::Incomplete) if self.eof => { Err(MultipartError::Incomplete) if self.eof => Ok(Some(self.buf.split().freeze())),
Ok(Some(self.buf.split().freeze()))
}
line => line, line => line,
} }
} }
@ -902,10 +882,7 @@ mod tests {
impl Stream for SlowStream { impl Stream for SlowStream {
type Item = Result<Bytes, PayloadError>; type Item = Result<Bytes, PayloadError>;
fn poll_next( fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
let this = self.get_mut(); let this = self.get_mut();
if !this.ready { if !this.ready {
this.ready = true; this.ready = true;

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 4.0.0-beta.2 - 2021-02-10
* No notable changes.
## 4.0.0-beta.1 - 2021-01-07 ## 4.0.0-beta.1 - 2021-01-07
* Update `pin-project` to `1.0`. * Update `pin-project` to `1.0`.
* Update `bytes` to `1.0`. [#1813] * Update `bytes` to `1.0`. [#1813]

View File

@ -1,8 +1,8 @@
[package] [package]
name = "actix-web-actors" name = "actix-web-actors"
version = "4.0.0-beta.1" version = "4.0.0-beta.2"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix actors support for actix web framework." description = "Actix actors support for Actix Web"
readme = "README.md" readme = "README.md"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
@ -18,8 +18,8 @@ path = "src/lib.rs"
[dependencies] [dependencies]
actix = { version = "0.11.0-beta.2", default-features = false } actix = { version = "0.11.0-beta.2", default-features = false }
actix-codec = "0.4.0-beta.1" actix-codec = "0.4.0-beta.1"
actix-http = "3.0.0-beta.1" actix-http = "3.0.0-beta.3"
actix-web = { version = "4.0.0-beta.1", default-features = false } actix-web = { version = "4.0.0-beta.3", default-features = false }
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"

View File

@ -1,8 +1,18 @@
Actix actors support for actix web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web-actors)](https://crates.io/crates/actix-web-actors) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) # actix-web-actors
## Documentation & community resources > Actix actors support for Actix Web.
* [API Documentation](https://docs.rs/actix-web-actors/) [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors)
* [Chat on gitter](https://gitter.im/actix/actix) [![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=0.5.0)](https://docs.rs/actix-web-actors/0.5.0)
* Cargo package: [actix-web-actors](https://crates.io/crates/actix-web-actors) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
* Minimum supported Rust version: 1.40 or later ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-web-actors/0.5.0/status.svg)](https://deps.rs/crate/actix-web-actors/0.5.0)
[![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-web-actors)
- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum supported Rust version: 1.46 or later

View File

@ -3,9 +3,7 @@ use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use actix::dev::{ use actix::dev::{AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, ToEnvelope};
AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, ToEnvelope,
};
use actix::fut::ActorFuture; use actix::fut::ActorFuture;
use actix::{ use actix::{
Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message, SpawnHandle, Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message, SpawnHandle,
@ -15,7 +13,7 @@ use bytes::Bytes;
use futures_core::Stream; use futures_core::Stream;
use tokio::sync::oneshot::Sender; use tokio::sync::oneshot::Sender;
/// Execution context for http actors /// Execution context for HTTP actors
pub struct HttpContext<A> pub struct HttpContext<A>
where where
A: Actor<Context = HttpContext<A>>, A: Actor<Context = HttpContext<A>>,
@ -165,10 +163,7 @@ where
{ {
type Item = Result<Bytes, Error>; type Item = Result<Bytes, Error>;
fn poll_next( fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
if self.fut.alive() { if self.fut.alive() {
let _ = Pin::new(&mut self.fut).poll(cx); let _ = Pin::new(&mut self.fut).poll(cx);
} }
@ -233,10 +228,11 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_default_resource() { async fn test_default_resource() {
let srv = init_service(App::new().service(web::resource("/test").to(|| { let srv =
HttpResponse::Ok().streaming(HttpContext::create(MyActor { count: 0 })) init_service(App::new().service(web::resource("/test").to(|| {
}))) HttpResponse::Ok().streaming(HttpContext::create(MyActor { count: 0 }))
.await; })))
.await;
let req = TestRequest::with_uri("/test").to_request(); let req = TestRequest::with_uri("/test").to_request();
let resp = call_service(&srv, req).await; let resp = call_service(&srv, req).await;

View File

@ -1,4 +1,4 @@
//! Actix actors integration for Actix web framework //! Actix actors support for Actix Web.
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms)]
#![allow(clippy::borrow_interior_mutable_const)] #![allow(clippy::borrow_interior_mutable_const)]

View File

@ -7,13 +7,12 @@ use std::task::{Context, Poll};
use std::{collections::VecDeque, convert::TryFrom}; use std::{collections::VecDeque, convert::TryFrom};
use actix::dev::{ use actix::dev::{
AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, ToEnvelope,
ToEnvelope,
}; };
use actix::fut::ActorFuture; use actix::fut::ActorFuture;
use actix::{ use actix::{
Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message as ActixMessage,
Message as ActixMessage, SpawnHandle, SpawnHandle,
}; };
use actix_codec::{Decoder, Encoder}; use actix_codec::{Decoder, Encoder};
use actix_http::ws::{hash_key, Codec}; use actix_http::ws::{hash_key, Codec};
@ -32,8 +31,7 @@ use tokio::sync::oneshot::Sender;
/// Perform WebSocket handshake and start actor. /// Perform WebSocket handshake and start actor.
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>> A: Actor<Context = WebsocketContext<A>> + StreamHandler<Result<Message, ProtocolError>>,
+ StreamHandler<Result<Message, ProtocolError>>,
T: Stream<Item = Result<Bytes, PayloadError>> + 'static, T: Stream<Item = Result<Bytes, PayloadError>> + 'static,
{ {
let mut res = handshake(req)?; let mut res = handshake(req)?;
@ -50,15 +48,14 @@ where
/// ///
/// If successful, returns a pair where the first item is an address for the /// If successful, returns a pair where the first item is an address for the
/// created actor and the second item is the response that should be returned /// created actor and the second item is the response that should be returned
/// from the websocket request. /// 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,
stream: T, stream: T,
) -> Result<(Addr<A>, HttpResponse), Error> ) -> Result<(Addr<A>, HttpResponse), Error>
where where
A: Actor<Context = WebsocketContext<A>> A: Actor<Context = WebsocketContext<A>> + StreamHandler<Result<Message, ProtocolError>>,
+ StreamHandler<Result<Message, ProtocolError>>,
T: Stream<Item = Result<Bytes, PayloadError>> + 'static, T: Stream<Item = Result<Bytes, PayloadError>> + 'static,
{ {
let mut res = handshake(req)?; let mut res = handshake(req)?;
@ -66,7 +63,7 @@ where
Ok((addr, res.streaming(out_stream))) Ok((addr, res.streaming(out_stream)))
} }
/// 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.
pub fn start_with_protocols<A, T>( pub fn start_with_protocols<A, T>(
@ -76,15 +73,14 @@ pub fn start_with_protocols<A, T>(
stream: T, stream: T,
) -> Result<HttpResponse, Error> ) -> Result<HttpResponse, Error>
where where
A: Actor<Context = WebsocketContext<A>> A: Actor<Context = WebsocketContext<A>> + StreamHandler<Result<Message, ProtocolError>>,
+ StreamHandler<Result<Message, ProtocolError>>,
T: Stream<Item = Result<Bytes, PayloadError>> + 'static, T: Stream<Item = Result<Bytes, PayloadError>> + 'static,
{ {
let mut res = handshake_with_protocols(req, protocols)?; let mut res = handshake_with_protocols(req, protocols)?;
Ok(res.streaming(WebsocketContext::create(actor, stream))) Ok(res.streaming(WebsocketContext::create(actor, stream)))
} }
/// Prepare `WebSocket` handshake response. /// Prepare WebSocket handshake response.
/// ///
/// This function returns handshake `HttpResponse`, ready to send to peer. /// This function returns handshake `HttpResponse`, ready to send to peer.
/// It does not perform any IO. /// It does not perform any IO.
@ -92,7 +88,7 @@ pub fn handshake(req: &HttpRequest) -> Result<HttpResponseBuilder, HandshakeErro
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. /// This function returns handshake `HttpResponse`, ready to send to peer.
/// It does not perform any IO. /// It does not perform any IO.
@ -109,7 +105,7 @@ pub fn handshake_with_protocols(
return Err(HandshakeError::GetMethodRequired); return Err(HandshakeError::GetMethodRequired);
} }
// Check for "UPGRADE" to websocket header // check for "UPGRADE" to WebSocket header
let has_hdr = if let Some(hdr) = req.headers().get(&header::UPGRADE) { let has_hdr = if let Some(hdr) = req.headers().get(&header::UPGRADE) {
if let Ok(s) = hdr.to_str() { if let Ok(s) = hdr.to_str() {
s.to_ascii_lowercase().contains("websocket") s.to_ascii_lowercase().contains("websocket")
@ -301,10 +297,7 @@ where
} }
/// Create a new Websocket context /// Create a new Websocket context
pub fn with_factory<S, F>( pub fn with_factory<S, F>(stream: S, f: F) -> impl Stream<Item = Result<Bytes, Error>>
stream: S,
f: F,
) -> impl Stream<Item = Result<Bytes, Error>>
where where
F: FnOnce(&mut Self) -> A + 'static, F: FnOnce(&mut Self) -> A + 'static,
A: StreamHandler<Result<Message, ProtocolError>>, A: StreamHandler<Result<Message, ProtocolError>>,
@ -423,10 +416,7 @@ where
{ {
type Item = Result<Bytes, Error>; type Item = Result<Bytes, Error>;
fn poll_next( fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
let this = self.get_mut(); let this = self.get_mut();
if this.fut.alive() { if this.fut.alive() {
@ -493,10 +483,7 @@ where
{ {
type Item = Result<Message, ProtocolError>; type Item = Result<Message, ProtocolError>;
fn poll_next( fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
let mut this = self.as_mut().project(); let mut this = self.as_mut().project();
if !*this.closed { if !*this.closed {
@ -512,9 +499,10 @@ where
} }
Poll::Pending => break, Poll::Pending => break,
Poll::Ready(Some(Err(e))) => { Poll::Ready(Some(Err(e))) => {
return Poll::Ready(Some(Err(ProtocolError::Io( return Poll::Ready(Some(Err(ProtocolError::Io(io::Error::new(
io::Error::new(io::ErrorKind::Other, format!("{}", e)), io::ErrorKind::Other,
)))); format!("{}", e),
)))));
} }
} }
} }

View File

@ -11,11 +11,7 @@ impl Actor for Ws {
} }
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for Ws { impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for Ws {
fn handle( fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
&mut self,
msg: Result<ws::Message, ws::ProtocolError>,
ctx: &mut Self::Context,
) {
match msg.unwrap() { match msg.unwrap() {
ws::Message::Ping(msg) => ctx.pong(&msg), ws::Message::Ping(msg) => ctx.pong(&msg),
ws::Message::Text(text) => ctx.text(text), ws::Message::Text(text) => ctx.text(text),
@ -30,9 +26,7 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for Ws {
async fn test_simple() { async fn test_simple() {
let mut srv = test::start(|| { let mut srv = 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::start(Ws, &req, stream) },
ws::start(Ws, &req, stream)
},
)) ))
}); });

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.5.0-beta.1 - 2021-02-10
* Use new call signature for `System::new`.
## 0.4.0 - 2020-09-20 ## 0.4.0 - 2020-09-20
* Added compile success and failure testing. [#1677] * Added compile success and failure testing. [#1677]
* Add `route` macro for supporting multiple HTTP methods guards. [#1674] * Add `route` macro for supporting multiple HTTP methods guards. [#1674]

View File

@ -1,7 +1,7 @@
[package] [package]
name = "actix-web-codegen" name = "actix-web-codegen"
version = "0.4.0" version = "0.5.0-beta.1"
description = "Actix web proc macros" description = "Routing and runtime macros for Actix Web"
readme = "README.md" readme = "README.md"
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web" repository = "https://github.com/actix/actix-web"
@ -20,7 +20,7 @@ proc-macro2 = "1"
[dev-dependencies] [dev-dependencies]
actix-rt = "2" actix-rt = "2"
actix-web = "4.0.0-beta.1" actix-web = "4.0.0-beta.3"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }
trybuild = "1" trybuild = "1"
rustversion = "1" rustversion = "1"

View File

@ -1,22 +1,24 @@
# actix-web-codegen # actix-web-codegen
> Helper and convenience macros for Actix Web > Routing and runtime macros for Actix Web.
[![crates.io](https://meritbadge.herokuapp.com/actix-web-codegen)](https://crates.io/crates/actix-web-codegen) [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen)
[![Documentation](https://docs.rs/actix-web-codegen/badge.svg)](https://docs.rs/actix-web-codegen/0.4.0/actix_web_codegen/) [![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.1)](https://docs.rs/actix-web-codegen/0.5.0-beta.1)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
[![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) <br />
[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.1/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.1)
[![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & Resources ## Documentation & Resources
- [API Documentation](https://docs.rs/actix-web-codegen) - [API Documentation](https://docs.rs/actix-web-codegen)
- [Chat on Gitter](https://gitter.im/actix/actix-web) - [Chat on Gitter](https://gitter.im/actix/actix-web)
- Cargo package: [actix-web-codegen](https://crates.io/crates/actix-web-codegen)
- Minimum supported Rust version: 1.46 or later. - Minimum supported Rust version: 1.46 or later.
## Compile Testing ## Compile Testing
Uses the [`trybuild`] crate. All compile fail tests should include a stderr file generated by `trybuild`. See the [workflow section](https://github.com/dtolnay/trybuild#workflow) of the trybuild docs for info on how to do this. Uses the [`trybuild`] crate. All compile fail tests should include a stderr file generated by `trybuild`. See the [workflow section](https://github.com/dtolnay/trybuild#workflow) of the trybuild docs for info on how to do this.
[`trybuild`]: https://github.com/dtolnay/trybuild [`trybuild`]: https://github.com/dtolnay/trybuild

View File

@ -1,6 +1,6 @@
//! Macros for reducing boilerplate code in Actix Web applications. //! Routing and runtime macros for Actix Web.
//! //!
//! ## Actix Web Re-exports //! # Actix Web Re-exports
//! Actix Web re-exports a version of this crate in it's entirety so you usually don't have to //! Actix Web re-exports a version of this crate in it's entirety so you usually don't have to
//! specify a dependency on this crate explicitly. Sometimes, however, updates are made to this //! specify a dependency on this crate explicitly. Sometimes, however, updates are made to this
//! crate before the actix-web dependency is updated. Therefore, code examples here will show //! crate before the actix-web dependency is updated. Therefore, code examples here will show
@ -10,7 +10,7 @@
//! # Runtime Setup //! # Runtime Setup
//! Used for setting up the actix async runtime. See [macro@main] macro docs. //! Used for setting up the actix async runtime. See [macro@main] macro docs.
//! //!
//! ```rust //! ```
//! #[actix_web_codegen::main] // or `#[actix_web::main]` in Actix Web apps //! #[actix_web_codegen::main] // or `#[actix_web::main]` in Actix Web apps
//! async fn main() { //! async fn main() {
//! async { println!("Hello world"); }.await //! async { println!("Hello world"); }.await
@ -23,7 +23,7 @@
//! //!
//! See docs for: [GET], [POST], [PATCH], [PUT], [DELETE], [HEAD], [CONNECT], [OPTIONS], [TRACE] //! See docs for: [GET], [POST], [PATCH], [PUT], [DELETE], [HEAD], [CONNECT], [OPTIONS], [TRACE]
//! //!
//! ```rust //! ```
//! # use actix_web::HttpResponse; //! # use actix_web::HttpResponse;
//! # use actix_web_codegen::get; //! # use actix_web_codegen::get;
//! #[get("/test")] //! #[get("/test")]
@ -36,7 +36,7 @@
//! Similar to the single method handler macro but takes one or more arguments for the HTTP methods //! Similar to the single method handler macro but takes one or more arguments for the HTTP methods
//! it should respond to. See [macro@route] macro docs. //! it should respond to. See [macro@route] macro docs.
//! //!
//! ```rust //! ```
//! # use actix_web::HttpResponse; //! # use actix_web::HttpResponse;
//! # use actix_web_codegen::route; //! # use actix_web_codegen::route;
//! #[route("/test", method="GET", method="HEAD")] //! #[route("/test", method="GET", method="HEAD")]
@ -159,7 +159,7 @@ method_macro! {
/// # Actix Web Re-export /// # Actix Web Re-export
/// This macro can be applied with `#[actix_web::main]` when used in Actix Web applications. /// This macro can be applied with `#[actix_web::main]` when used in Actix Web applications.
/// ///
/// # Usage /// # Examples
/// ```rust /// ```rust
/// #[actix_web_codegen::main] /// #[actix_web_codegen::main]
/// async fn main() { /// async fn main() {

View File

@ -4,9 +4,7 @@ use std::task::{Context, Poll};
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::http::header::{HeaderName, HeaderValue}; use actix_web::http::header::{HeaderName, HeaderValue};
use actix_web::{http, test, web::Path, App, Error, HttpResponse, Responder}; use actix_web::{http, test, web::Path, App, Error, HttpResponse, Responder};
use actix_web_codegen::{ use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, route, trace};
connect, delete, get, head, options, patch, post, put, route, trace,
};
use futures_util::future::{self, LocalBoxFuture}; use futures_util::future::{self, LocalBoxFuture};
// Make sure that we can name function as 'config' // Make sure that we can name function as 'config'

View File

@ -1,9 +1,13 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.2 - 2021-02-10
### Added ### Added
* `ClientRequest::insert_header` method which allows using typed headers. [#1869] * `ClientRequest::insert_header` method which allows using typed headers. [#1869]
* `ClientRequest::append_header` method which allows using typed headers. [#1869] * `ClientRequest::append_header` method which allows using typed headers. [#1869]
* `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969]
### Changed ### Changed
* Relax default timeout for `Connector` to 5 seconds(original 1 second). [#1905] * Relax default timeout for `Connector` to 5 seconds(original 1 second). [#1905]
@ -16,6 +20,7 @@
[#1869]: https://github.com/actix/actix-web/pull/1869 [#1869]: https://github.com/actix/actix-web/pull/1869
[#1905]: https://github.com/actix/actix-web/pull/1905 [#1905]: https://github.com/actix/actix-web/pull/1905
[#1969]: https://github.com/actix/actix-web/pull/1969
## 3.0.0-beta.1 - 2021-01-07 ## 3.0.0-beta.1 - 2021-01-07

View File

@ -1,6 +1,6 @@
[package] [package]
name = "awc" name = "awc"
version = "3.0.0-beta.1" version = "3.0.0-beta.2"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Async HTTP and WebSocket client library built on the Actix ecosystem" description = "Async HTTP and WebSocket client library built on the Actix ecosystem"
readme = "README.md" readme = "README.md"
@ -36,10 +36,13 @@ rustls = ["tls-rustls", "actix-http/rustls"]
# content-encoding support # content-encoding support
compress = ["actix-http/compress"] compress = ["actix-http/compress"]
# trust-dns as dns resolver
trust-dns = ["actix-http/trust-dns"]
[dependencies] [dependencies]
actix-codec = "0.4.0-beta.1" actix-codec = "0.4.0-beta.1"
actix-service = "2.0.0-beta.4" actix-service = "2.0.0-beta.4"
actix-http = "3.0.0-beta.1" actix-http = "3.0.0-beta.3"
actix-rt = "2" actix-rt = "2"
base64 = "0.13" base64 = "0.13"
@ -57,10 +60,16 @@ serde_urlencoded = "0.7"
tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
tls-rustls = { version = "0.19.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } tls-rustls = { version = "0.19.0", package = "rustls", optional = true, features = ["dangerous_configuration"] }
[target.'cfg(windows)'.dependencies.tls-openssl]
version = "0.10.9"
package = "openssl"
features = ["vendored"]
optional = true
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0-beta.1", features = ["openssl"] } actix-web = { version = "4.0.0-beta.3", features = ["openssl"] }
actix-http = { version = "3.0.0-beta.1", features = ["openssl"] } actix-http = { version = "3.0.0-beta.3", features = ["openssl"] }
actix-http-test = { version = "3.0.0-beta.1", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.2", features = ["openssl"] }
actix-utils = "3.0.0-beta.1" actix-utils = "3.0.0-beta.1"
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-beta.3"
actix-tls = { version = "3.0.0-beta.3", features = ["openssl", "rustls"] } actix-tls = { version = "3.0.0-beta.3", features = ["openssl", "rustls"] }

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

View File

@ -82,8 +82,9 @@ impl ClientBuilder {
self self
} }
/// Maximum supported http major version /// Maximum supported HTTP major version.
/// Supported versions http/1.1, http/2 ///
/// Supported versions are HTTP/1.1 and HTTP/2.
pub fn max_http_version(mut self, val: http::Version) -> Self { pub fn max_http_version(mut self, val: http::Version) -> Self {
self.max_http_version = Some(val); self.max_http_version = Some(val);
self self

View File

@ -82,8 +82,7 @@ where
let connection = fut.await?; let connection = fut.await?;
// send request // send request
let (head, framed) = let (head, framed) = connection.open_tunnel(RequestHeadType::from(head)).await?;
connection.open_tunnel(RequestHeadType::from(head)).await?;
let framed = framed.into_map_io(|io| BoxedSocket(Box::new(Socket(io)))); let framed = framed.into_map_io(|io| BoxedSocket(Box::new(Socket(io))));
Ok((head, framed)) Ok((head, framed))
@ -142,10 +141,7 @@ impl AsyncWrite for BoxedSocket {
Pin::new(self.get_mut().0.as_write()).poll_flush(cx) Pin::new(self.get_mut().0.as_write()).poll_flush(cx)
} }
fn poll_shutdown( fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<io::Result<()>> {
Pin::new(self.get_mut().0.as_write()).poll_shutdown(cx) Pin::new(self.get_mut().0.as_write()).poll_shutdown(cx)
} }
} }

View File

@ -1,7 +1,5 @@
//! Http client errors //! Http client errors
pub use actix_http::client::{ pub use actix_http::client::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};
ConnectError, FreezeRequestError, InvalidUrl, SendRequestError,
};
pub use actix_http::error::PayloadError; pub use actix_http::error::PayloadError;
pub use actix_http::http::Error as HttpError; pub use actix_http::http::Error as HttpError;
pub use actix_http::ws::HandshakeError as WsHandshakeError; pub use actix_http::ws::HandshakeError as WsHandshakeError;

View File

@ -145,7 +145,9 @@ impl FrozenSendBuilder {
{ {
match HeaderName::try_from(key) { match HeaderName::try_from(key) {
Ok(key) => match value.try_into_value() { Ok(key) => match value.try_into_value() {
Ok(value) => self.extra_headers.insert(key, value), Ok(value) => {
self.extra_headers.insert(key, value);
}
Err(e) => self.err = Some(e.into()), Err(e) => self.err = Some(e.into()),
}, },
Err(e) => self.err = Some(e.into()), Err(e) => self.err = Some(e.into()),

View File

@ -134,7 +134,7 @@ use self::connect::{Connect, ConnectorWrapper};
/// ///
/// let res = client.get("http://www.rust-lang.org") // <- Create request builder /// let res = client.get("http://www.rust-lang.org") // <- Create request builder
/// .insert_header(("User-Agent", "Actix-web")) /// .insert_header(("User-Agent", "Actix-web"))
/// .send() // <- Send http request /// .send() // <- Send HTTP request
/// .await; // <- send request and wait for response /// .await; // <- send request and wait for response
/// ///
/// println!("Response: {:?}", res); /// println!("Response: {:?}", res);

View File

@ -11,8 +11,7 @@ use actix_http::body::Body;
use actix_http::cookie::{Cookie, CookieJar}; use actix_http::cookie::{Cookie, CookieJar};
use actix_http::http::header::{self, IntoHeaderPair}; use actix_http::http::header::{self, IntoHeaderPair};
use actix_http::http::{ use actix_http::http::{
uri, ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, uri, ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, Version,
Version,
}; };
use actix_http::{Error, RequestHead}; use actix_http::{Error, RequestHead};
@ -42,10 +41,10 @@ cfg_if::cfg_if! {
/// let response = awc::Client::new() /// let response = awc::Client::new()
/// .get("http://www.rust-lang.org") // <- Create request builder /// .get("http://www.rust-lang.org") // <- Create request builder
/// .insert_header(("User-Agent", "Actix-web")) /// .insert_header(("User-Agent", "Actix-web"))
/// .send() // <- Send http request /// .send() // <- Send HTTP request
/// .await; /// .await;
/// ///
/// response.and_then(|response| { // <- server http response /// response.and_then(|response| { // <- server HTTP response
/// println!("Response: {:?}", response); /// println!("Response: {:?}", response);
/// Ok(()) /// Ok(())
/// }); /// });
@ -159,7 +158,9 @@ impl ClientRequest {
H: IntoHeaderPair, H: IntoHeaderPair,
{ {
match header.try_into_header_pair() { match header.try_into_header_pair() {
Ok((key, value)) => self.head.headers.insert(key, value), Ok((key, value)) => {
self.head.headers.insert(key, value);
}
Err(e) => self.err = Some(e.into()), Err(e) => self.err = Some(e.into()),
}; };
@ -217,7 +218,7 @@ impl ClientRequest {
} }
/// Force close connection instead of returning it back to connections pool. /// Force close connection instead of returning it back to connections pool.
/// This setting affect only http/1 connections. /// This setting affect only HTTP/1 connections.
#[inline] #[inline]
pub fn force_close(mut self) -> Self { pub fn force_close(mut self) -> Self {
self.head.set_connection_type(ConnectionType::Close); self.head.set_connection_type(ConnectionType::Close);
@ -232,7 +233,9 @@ impl ClientRequest {
<HeaderValue as TryFrom<V>>::Error: Into<HttpError>, <HeaderValue as TryFrom<V>>::Error: Into<HttpError>,
{ {
match HeaderValue::try_from(value) { match HeaderValue::try_from(value) {
Ok(value) => self.head.headers.insert(header::CONTENT_TYPE, value), Ok(value) => {
self.head.headers.insert(header::CONTENT_TYPE, value);
}
Err(e) => self.err = Some(e.into()), Err(e) => self.err = Some(e.into()),
} }
self self
@ -516,15 +519,11 @@ impl ClientRequest {
.unwrap_or(true); .unwrap_or(true);
if https { if https {
slf = slf = slf.insert_header_if_none((header::ACCEPT_ENCODING, HTTPS_ENCODING))
slf.insert_header_if_none((header::ACCEPT_ENCODING, HTTPS_ENCODING))
} else { } else {
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
{ {
slf = slf.insert_header_if_none(( slf = slf.insert_header_if_none((header::ACCEPT_ENCODING, "gzip, deflate"))
header::ACCEPT_ENCODING,
"gzip, deflate",
))
} }
}; };
} }

View File

@ -85,8 +85,7 @@ impl<S> HttpMessage for ClientResponse<S> {
if self.extensions().get::<Cookies>().is_none() { if self.extensions().get::<Cookies>().is_none() {
let mut cookies = Vec::new(); let mut cookies = Vec::new();
for hdr in self.headers().get_all(&SET_COOKIE) { for hdr in self.headers().get_all(&SET_COOKIE) {
let s = std::str::from_utf8(hdr.as_bytes()) let s = std::str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?;
.map_err(CookieParseError::from)?;
cookies.push(Cookie::parse_encoded(s)?.into_owned()); cookies.push(Cookie::parse_encoded(s)?.into_owned());
} }
self.extensions_mut().insert(Cookies(cookies)); self.extensions_mut().insert(Cookies(cookies));
@ -185,7 +184,7 @@ impl<S> ClientResponse<S>
where where
S: Stream<Item = Result<Bytes, PayloadError>>, S: Stream<Item = Result<Bytes, PayloadError>>,
{ {
/// Loads http response's body. /// Loads HTTP response's body.
pub fn body(&mut self) -> MessageBody<S> { pub fn body(&mut self) -> MessageBody<S> {
MessageBody::new(self) MessageBody::new(self)
} }
@ -230,7 +229,7 @@ impl<S> fmt::Debug for ClientResponse<S> {
} }
} }
/// Future that resolves to a complete http message body. /// Future that resolves to a complete HTTP message body.
pub struct MessageBody<S> { pub struct MessageBody<S> {
length: Option<usize>, length: Option<usize>,
err: Option<PayloadError>, err: Option<PayloadError>,
@ -393,9 +392,7 @@ where
if let Some(len) = self.length.take() { if let Some(len) = self.length.take() {
if len > self.fut.as_ref().unwrap().limit { if len > self.fut.as_ref().unwrap().limit {
return Poll::Ready(Err(JsonPayloadError::Payload( return Poll::Ready(Err(JsonPayloadError::Payload(PayloadError::Overflow)));
PayloadError::Overflow,
)));
} }
} }
@ -465,8 +462,7 @@ mod tests {
_ => unreachable!("error"), _ => unreachable!("error"),
} }
let mut req = let mut req = TestResponse::with_header(header::CONTENT_LENGTH, "1000000").finish();
TestResponse::with_header(header::CONTENT_LENGTH, "1000000").finish();
match req.body().await.err().unwrap() { match req.body().await.err().unwrap() {
PayloadError::Overflow => {} PayloadError::Overflow => {}
_ => unreachable!("error"), _ => unreachable!("error"),

View File

@ -78,8 +78,7 @@ impl SendClientRequest {
#[cfg(feature = "compress")] #[cfg(feature = "compress")]
impl Future for SendClientRequest { impl Future for SendClientRequest {
type Output = type Output = Result<ClientResponse<Decoder<Payload<PayloadStream>>>, SendRequestError>;
Result<ClientResponse<Decoder<Payload<PayloadStream>>>, SendRequestError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut(); let this = self.get_mut();
@ -95,15 +94,9 @@ impl Future for SendClientRequest {
let res = ready!(send.as_mut().poll(cx)).map(|res| { let res = ready!(send.as_mut().poll(cx)).map(|res| {
res._timeout(delay.take()).map_body(|head, payload| { res._timeout(delay.take()).map_body(|head, payload| {
if *response_decompress { if *response_decompress {
Payload::Stream(Decoder::from_headers( Payload::Stream(Decoder::from_headers(payload, &head.headers))
payload,
&head.headers,
))
} else { } else {
Payload::Stream(Decoder::new( Payload::Stream(Decoder::new(payload, ContentEncoding::Identity))
payload,
ContentEncoding::Identity,
))
} }
}) })
}); });
@ -187,11 +180,11 @@ impl RequestSender {
B: Into<Body>, B: Into<Body>,
{ {
let fut = match self { let fut = match self {
RequestSender::Owned(head) => config.connector.send_request( RequestSender::Owned(head) => {
RequestHeadType::Owned(head), config
body.into(), .connector
addr, .send_request(RequestHeadType::Owned(head), body.into(), addr)
), }
RequestSender::Rc(head, extra_headers) => config.connector.send_request( RequestSender::Rc(head, extra_headers) => config.connector.send_request(
RequestHeadType::Rc(head, extra_headers), RequestHeadType::Rc(head, extra_headers),
body.into(), body.into(),
@ -215,8 +208,7 @@ impl RequestSender {
Err(e) => return Error::from(e).into(), Err(e) => return Error::from(e).into(),
}; };
if let Err(e) = self.set_header_if_none(header::CONTENT_TYPE, "application/json") if let Err(e) = self.set_header_if_none(header::CONTENT_TYPE, "application/json") {
{
return e.into(); return e.into();
} }
@ -243,10 +235,9 @@ impl RequestSender {
}; };
// set content-type // set content-type
if let Err(e) = self.set_header_if_none( if let Err(e) =
header::CONTENT_TYPE, self.set_header_if_none(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
"application/x-www-form-urlencoded", {
) {
return e.into(); return e.into();
} }
@ -290,11 +281,7 @@ impl RequestSender {
self.send_body(addr, response_decompress, timeout, config, Body::Empty) self.send_body(addr, response_decompress, timeout, config, Body::Empty)
} }
fn set_header_if_none<V>( fn set_header_if_none<V>(&mut self, key: HeaderName, value: V) -> Result<(), HttpError>
&mut self,
key: HeaderName,
value: V,
) -> Result<(), HttpError>
where where
V: IntoHeaderValue, V: IntoHeaderValue,
{ {
@ -302,7 +289,9 @@ impl RequestSender {
RequestSender::Owned(head) => { RequestSender::Owned(head) => {
if !head.headers.contains_key(&key) { if !head.headers.contains_key(&key) {
match value.try_into_value() { match value.try_into_value() {
Ok(value) => head.headers.insert(key, value), Ok(value) => {
head.headers.insert(key, value);
}
Err(e) => return Err(e.into()), Err(e) => return Err(e.into()),
} }
} }

View File

@ -40,16 +40,12 @@ pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message};
use crate::connect::BoxedSocket; use crate::connect::BoxedSocket;
use crate::error::{InvalidUrl, SendRequestError, WsClientError}; use crate::error::{InvalidUrl, SendRequestError, WsClientError};
use crate::http::header::{ use crate::http::header::{self, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION};
self, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION, use crate::http::{ConnectionType, Error as HttpError, Method, StatusCode, Uri, Version};
};
use crate::http::{
ConnectionType, Error as HttpError, Method, StatusCode, Uri, Version,
};
use crate::response::ClientResponse; use crate::response::ClientResponse;
use crate::ClientConfig; use crate::ClientConfig;
/// `WebSocket` connection /// WebSocket connection.
pub struct WebsocketsRequest { pub struct WebsocketsRequest {
pub(crate) head: RequestHead, pub(crate) head: RequestHead,
err: Option<HttpError>, err: Option<HttpError>,
@ -63,7 +59,7 @@ pub struct WebsocketsRequest {
} }
impl WebsocketsRequest { impl WebsocketsRequest {
/// Create new websocket connection /// Create new WebSocket connection
pub(crate) fn new<U>(uri: U, config: Rc<ClientConfig>) -> Self pub(crate) fn new<U>(uri: U, config: Rc<ClientConfig>) -> Self
where where
Uri: TryFrom<U>, Uri: TryFrom<U>,
@ -106,7 +102,7 @@ impl WebsocketsRequest {
self self
} }
/// Set supported websocket protocols /// Set supported WebSocket protocols
pub fn protocols<U, V>(mut self, protos: U) -> Self pub fn protocols<U, V>(mut self, protos: U) -> Self
where where
U: IntoIterator<Item = V>, U: IntoIterator<Item = V>,
@ -243,7 +239,7 @@ impl WebsocketsRequest {
self.header(AUTHORIZATION, format!("Bearer {}", token)) self.header(AUTHORIZATION, format!("Bearer {}", token))
} }
/// Complete request construction and connect to a websockets server. /// Complete request construction and connect to a WebSocket server.
pub async fn connect( pub async fn connect(
mut self, mut self,
) -> Result<(ClientResponse, Framed<BoxedSocket, Codec>), WsClientError> { ) -> Result<(ClientResponse, Framed<BoxedSocket, Codec>), WsClientError> {
@ -342,7 +338,7 @@ impl WebsocketsRequest {
return Err(WsClientError::InvalidResponseStatus(head.status)); return Err(WsClientError::InvalidResponseStatus(head.status));
} }
// Check for "UPGRADE" to websocket header // check for "UPGRADE" to WebSocket header
let has_hdr = if let Some(hdr) = head.headers.get(&header::UPGRADE) { let has_hdr = if let Some(hdr) = head.headers.get(&header::UPGRADE) {
if let Ok(s) = hdr.to_str() { if let Ok(s) = hdr.to_str() {
s.to_ascii_lowercase().contains("websocket") s.to_ascii_lowercase().contains("websocket")

View File

@ -51,8 +51,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
#[actix_rt::test] #[actix_rt::test]
async fn test_simple() { async fn test_simple() {
let srv = test::start(|| { let srv = test::start(|| {
App::new() App::new().service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))))
.service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))))
}); });
let request = srv.get("/").insert_header(("x-test", "111")).send(); let request = srv.get("/").insert_header(("x-test", "111")).send();
@ -685,9 +684,8 @@ async fn test_client_streaming_explicit() {
})) }))
}); });
let body = stream::once(async { let body =
Ok::<_, actix_http::Error>(Bytes::from_static(STR.as_bytes())) stream::once(async { Ok::<_, actix_http::Error>(Bytes::from_static(STR.as_bytes())) });
});
let req = srv.post("/").send_stream(Box::pin(body)); let req = srv.post("/").send_stream(Box::pin(body));
let mut res = req.await.unwrap(); let mut res = req.await.unwrap();

View File

@ -64,8 +64,7 @@ async fn test_connection_reuse_h2() {
.and_then( .and_then(
HttpService::build() HttpService::build()
.h2(map_config( .h2(map_config(
App::new() App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))),
.service(web::resource("/").route(web::to(HttpResponse::Ok))),
|_| AppConfig::default(), |_| AppConfig::default(),
)) ))
.rustls(tls_config()) .rustls(tls_config())

View File

@ -48,8 +48,7 @@ async fn test_connection_reuse_h2() {
.and_then( .and_then(
HttpService::build() HttpService::build()
.h2(map_config( .h2(map_config(
App::new() App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))),
.service(web::resource("/").route(web::to(HttpResponse::Ok))),
|_| AppConfig::default(), |_| AppConfig::default(),
)) ))
.openssl(ssl_acceptor()) .openssl(ssl_acceptor())

View File

@ -31,7 +31,7 @@ async fn test_simple() {
.send(h1::Message::Item((res.drop_body(), BodySize::None))) .send(h1::Message::Item((res.drop_body(), BodySize::None)))
.await?; .await?;
// start websocket service // start WebSocket service
let framed = framed.replace_codec(ws::Codec::new()); let framed = framed.replace_codec(ws::Codec::new());
ws::Dispatcher::with(framed, ws_service).await ws::Dispatcher::with(framed, ws_service).await
} }

View File

@ -33,9 +33,8 @@ fn bench_async_burst(c: &mut Criterion) {
let srv = rt.block_on(async { let srv = rt.block_on(async {
test::start(|| { test::start(|| {
App::new().service( App::new()
web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))), .service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))))
)
}) })
}); });
@ -43,24 +42,25 @@ fn bench_async_burst(c: &mut Criterion) {
c.bench_function("get_body_async_burst", move |b| { c.bench_function("get_body_async_burst", move |b| {
b.iter_custom(|iters| { b.iter_custom(|iters| {
let client = rt.block_on(async {
rt.block_on(async { Client::new().get(url.clone()).freeze().unwrap() }); let client = Client::new().get(url.clone()).freeze().unwrap();
let start = std::time::Instant::now();
// benchmark body
let start = std::time::Instant::now();
// benchmark body
let resps = rt.block_on(async move {
let burst = (0..iters).map(|_| client.send()); let burst = (0..iters).map(|_| client.send());
join_all(burst).await let resps = join_all(burst).await;
});
let elapsed = start.elapsed();
// if there are failed requests that might be an issue let elapsed = start.elapsed();
let failed = resps.iter().filter(|r| r.is_err()).count();
if failed > 0 {
eprintln!("failed {} requests (might be bench timeout)", failed);
};
elapsed // if there are failed requests that might be an issue
let failed = resps.iter().filter(|r| r.is_err()).count();
if failed > 0 {
eprintln!("failed {} requests (might be bench timeout)", failed);
};
elapsed
})
}) })
}); });
} }

View File

@ -1,6 +1,7 @@
digraph { digraph {
subgraph cluster_web { subgraph cluster_web {
label="actix/actix-web" label="actix/actix-web"
"awc" "awc"
"actix-web" "actix-web"
"actix-files" "actix-files"
@ -16,7 +17,7 @@ digraph {
"actix-web-actors" -> { "actix" "actix-web" "actix-http" "actix-codec" } "actix-web-actors" -> { "actix" "actix-web" "actix-http" "actix-codec" }
"actix-multipart" -> { "actix-web" "actix-service" "actix-utils" } "actix-multipart" -> { "actix-web" "actix-service" "actix-utils" }
"actix-http" -> { "actix-service" "actix-codec" "actix-tls" "actix-utils" "actix-rt" "threadpool" } "actix-http" -> { "actix-service" "actix-codec" "actix-tls" "actix-utils" "actix-rt" "threadpool" }
"actix-http" -> { "actix" "actix-tls" }[color=blue] // optional "actix-http" -> { "actix-tls" }[color=blue] // optional
"actix-files" -> { "actix-web" } "actix-files" -> { "actix-web" }
"actix-http-test" -> { "actix-service" "actix-codec" "actix-tls" "actix-utils" "actix-rt" "actix-server" "awc" } "actix-http-test" -> { "actix-service" "actix-codec" "actix-tls" "actix-utils" "actix-rt" "actix-server" "awc" }
@ -27,4 +28,8 @@ digraph {
"actix-tls" -> { "actix-service" "actix-codec" "actix-utils" } "actix-tls" -> { "actix-service" "actix-codec" "actix-utils" }
"actix-server" -> { "actix-service" "actix-rt" "actix-codec" "actix-utils" } "actix-server" -> { "actix-service" "actix-rt" "actix-codec" "actix-utils" }
"actix-rt" -> { "macros" "threadpool" } "actix-rt" -> { "macros" "threadpool" }
// actix
"actix" -> { "actix-rt" }
} }

View File

@ -15,7 +15,6 @@ digraph {
"awc" -> { "actix-http" } "awc" -> { "actix-http" }
"actix-web-actors" -> { "actix" "actix-web" "actix-http" } "actix-web-actors" -> { "actix" "actix-web" "actix-http" }
"actix-multipart" -> { "actix-web" } "actix-multipart" -> { "actix-web" }
"actix-http" -> { "actix" }[color=blue] // optional
"actix-files" -> { "actix-web" } "actix-files" -> { "actix-web" }
"actix-http-test" -> { "awc" } "actix-http-test" -> { "awc" }
} }

View File

@ -29,12 +29,8 @@ async fn main() -> std::io::Result<()> {
.service(no_params) .service(no_params)
.service( .service(
web::resource("/resource2/index.html") web::resource("/resource2/index.html")
.wrap( .wrap(middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"))
middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), .default_service(web::route().to(|| HttpResponse::MethodNotAllowed()))
)
.default_service(
web::route().to(|| HttpResponse::MethodNotAllowed()),
)
.route(web::get().to(index_async)), .route(web::get().to(index_async)),
) )
.service(web::resource("/test1.html").to(|| async { "Test\r\n" })) .service(web::resource("/test1.html").to(|| async { "Test\r\n" }))

View File

@ -33,12 +33,8 @@ async fn main() -> std::io::Result<()> {
.service(no_params) .service(no_params)
.service( .service(
web::resource("/resource2/index.html") web::resource("/resource2/index.html")
.wrap( .wrap(middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"))
middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), .default_service(web::route().to(|| HttpResponse::MethodNotAllowed()))
)
.default_service(
web::route().to(|| HttpResponse::MethodNotAllowed()),
)
.route(web::get().to(index_async)), .route(web::get().to(index_async)),
) )
.service(web::resource("/test1.html").to(|| async { "Test\r\n" })) .service(web::resource("/test1.html").to(|| async { "Test\r\n" }))

View File

@ -1,2 +1,2 @@
max_width = 89 max_width = 96
reorder_imports = true reorder_imports = true

View File

@ -8,8 +8,7 @@ use actix_http::body::{Body, MessageBody};
use actix_http::{Extensions, Request}; use actix_http::{Extensions, Request};
use actix_service::boxed::{self, BoxServiceFactory}; use actix_service::boxed::{self, BoxServiceFactory};
use actix_service::{ use actix_service::{
apply, apply_fn_factory, IntoServiceFactory, ServiceFactory, ServiceFactoryExt, apply, apply_fn_factory, IntoServiceFactory, ServiceFactory, ServiceFactoryExt, Transform,
Transform,
}; };
use futures_util::future::FutureExt; use futures_util::future::FutureExt;
@ -72,7 +71,7 @@ where
/// Set application data. Application data could be accessed /// Set application data. Application data could be accessed
/// by using `Data<T>` extractor where `T` is data type. /// by using `Data<T>` extractor where `T` is data type.
/// ///
/// **Note**: http server accepts an application factory rather than /// **Note**: HTTP server accepts an application factory rather than
/// an application instance. Http server constructs an application /// an application instance. Http server constructs an application
/// instance for each thread, thus application data must be constructed /// instance for each thread, thus application data must be constructed
/// multiple times. If you want to share data between different /// multiple times. If you want to share data between different
@ -207,11 +206,11 @@ where
) )
} }
/// Register http service. /// Register HTTP service.
/// ///
/// Http service is any type that implements `HttpServiceFactory` trait. /// Http service is any type that implements `HttpServiceFactory` trait.
/// ///
/// Actix web provides several services implementations: /// Actix Web provides several services implementations:
/// ///
/// * *Resource* is an entry in resource table which corresponds to requested URL. /// * *Resource* is an entry in resource table which corresponds to requested URL.
/// * *Scope* is a set of resources with common root path. /// * *Scope* is a set of resources with common root path.
@ -473,17 +472,13 @@ mod tests {
use crate::http::{header, HeaderValue, Method, StatusCode}; use crate::http::{header, HeaderValue, Method, StatusCode};
use crate::middleware::DefaultHeaders; use crate::middleware::DefaultHeaders;
use crate::service::ServiceRequest; use crate::service::ServiceRequest;
use crate::test::{ use crate::test::{call_service, init_service, read_body, try_init_service, TestRequest};
call_service, init_service, read_body, try_init_service, TestRequest,
};
use crate::{web, HttpRequest, HttpResponse}; use crate::{web, HttpRequest, HttpResponse};
#[actix_rt::test] #[actix_rt::test]
async fn test_default_resource() { async fn test_default_resource() {
let srv = init_service( let srv =
App::new().service(web::resource("/test").to(HttpResponse::Ok)), init_service(App::new().service(web::resource("/test").to(HttpResponse::Ok))).await;
)
.await;
let req = TestRequest::with_uri("/test").to_request(); let req = TestRequest::with_uri("/test").to_request();
let resp = srv.call(req).await.unwrap(); let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
@ -525,20 +520,22 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_data_factory() { async fn test_data_factory() {
let srv = let srv = init_service(
init_service(App::new().data_factory(|| ok::<_, ()>(10usize)).service( App::new()
web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok()), .data_factory(|| ok::<_, ()>(10usize))
)) .service(web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok())),
.await; )
.await;
let req = TestRequest::default().to_request(); let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap(); let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
let srv = let srv = init_service(
init_service(App::new().data_factory(|| ok::<_, ()>(10u32)).service( App::new()
web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok()), .data_factory(|| ok::<_, ()>(10u32))
)) .service(web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok())),
.await; )
.await;
let req = TestRequest::default().to_request(); let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap(); let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
@ -546,23 +543,24 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_data_factory_errors() { async fn test_data_factory_errors() {
let srv = let srv = try_init_service(
try_init_service(App::new().data_factory(|| err::<u32, _>(())).service( App::new()
web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok()), .data_factory(|| err::<u32, _>(()))
)) .service(web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok())),
.await; )
.await;
assert!(srv.is_err()); assert!(srv.is_err());
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_extension() { async fn test_extension() {
let srv = init_service(App::new().app_data(10usize).service( let srv = init_service(App::new().app_data(10usize).service(web::resource("/").to(
web::resource("/").to(|req: HttpRequest| { |req: HttpRequest| {
assert_eq!(*req.app_data::<usize>().unwrap(), 10); assert_eq!(*req.app_data::<usize>().unwrap(), 10);
HttpResponse::Ok() HttpResponse::Ok()
}), },
)) )))
.await; .await;
let req = TestRequest::default().to_request(); let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap(); let resp = srv.call(req).await.unwrap();
@ -617,10 +615,8 @@ mod tests {
let fut = srv.call(req); let fut = srv.call(req);
async move { async move {
let mut res = fut.await?; let mut res = fut.await?;
res.headers_mut().insert( res.headers_mut()
header::CONTENT_TYPE, .insert(header::CONTENT_TYPE, HeaderValue::from_static("0001"));
HeaderValue::from_static("0001"),
);
Ok(res) Ok(res)
} }
}) })
@ -645,10 +641,8 @@ mod tests {
let fut = srv.call(req); let fut = srv.call(req);
async { async {
let mut res = fut.await?; let mut res = fut.await?;
res.headers_mut().insert( res.headers_mut()
header::CONTENT_TYPE, .insert(header::CONTENT_TYPE, HeaderValue::from_static("0001"));
HeaderValue::from_static("0001"),
);
Ok(res) Ok(res)
} }
}), }),
@ -671,9 +665,8 @@ mod tests {
.route( .route(
"/test", "/test",
web::get().to(|req: HttpRequest| { web::get().to(|req: HttpRequest| {
HttpResponse::Ok().body( HttpResponse::Ok()
req.url_for("youtube", &["12345"]).unwrap().to_string(), .body(req.url_for("youtube", &["12345"]).unwrap().to_string())
)
}), }),
), ),
) )

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