diff --git a/.github/workflows/linux.yml b/.github/workflows/ci.yml similarity index 58% rename from .github/workflows/linux.yml rename to .github/workflows/ci.yml index 53f22df63..55af5a380 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/ci.yml @@ -1,24 +1,27 @@ -name: CI (Linux) +name: CI on: pull_request: types: [opened, synchronize, reopened] push: - branches: - - master + branches: [master] jobs: build_and_test: strategy: fail-fast: false 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: - 1.46.0 # MSRV - stable - nightly - name: ${{ matrix.version }} - x86_64-unknown-linux-gnu - runs-on: ubuntu-latest + name: ${{ matrix.target.name }} / ${{ matrix.version }} + runs-on: ${{ matrix.target.os }} steps: - uses: actions/checkout@v2 @@ -26,7 +29,7 @@ jobs: - name: Install ${{ matrix.version }} uses: actions-rs/toolchain@v1 with: - toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu + toolchain: ${{ matrix.version }}-${{ matrix.target.triple }} profile: minimal override: true @@ -37,18 +40,25 @@ jobs: - name: Cache Dependencies uses: Swatinem/rust-cache@v1.0.1 - - name: check build + - name: check minimal uses: actions-rs/cargo@v1 with: 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 uses: actions-rs/cargo@v1 - timeout-minutes: 40 with: 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) uses: actions-rs/cargo@v1 @@ -65,12 +75,18 @@ jobs: args: --package=awc --no-default-features --features=rustls -- --nocapture - 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: | cargo install cargo-tarpaulin --vers "^0.13" cargo tarpaulin --out Xml - 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 with: file: cobertura.xml diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml deleted file mode 100644 index 6b5366faf..000000000 --- a/.github/workflows/macos.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml deleted file mode 100644 index d3de72a61..000000000 --- a/.github/workflows/windows.yml +++ /dev/null @@ -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 diff --git a/CHANGES.md b/CHANGES.md index 9a3b7fe3e..954410b30 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,26 +1,33 @@ # Changes ## 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 * The method `Either, web::Form>::into_inner()` which returns the inner type for whichever variant was created. Also works for `Either, web::Json>`. [#1894] * 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 * Rework `Responder` trait to be sync and returns `Response`/`HttpResponse` directly. - Making it more simple and performant. [#1891] -* `ServiceRequest::into_parts` and `ServiceRequest::from_parts` would not fail. - `ServiceRequest::from_request` would not fail and no payload would be generated [#1893] + Making it simpler and more performant. [#1891] +* `ServiceRequest::into_parts` and `ServiceRequest::from_parts` can no longer fail. [#1893] +* `ServiceRequest::from_request` can no longer fail. [#1893] * 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] -* `App::wrap_fn`, `Resource::wrap_fn` and `Scope::wrap_fn` would give `&Service` in closure - argument [#1905] -* `web::block` accept any closure that has an output bound to `Send` and `'static`. [#1957] - +* `App::wrap_fn`, `Resource::wrap_fn` and `Scope::wrap_fn` provide `&Service` in closure + argument. [#1905] +* `web::block` no longer requires the output is a Result. [#1957] + ### 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 * Public field of `web::Path` has been made private. [#1894] diff --git a/Cargo.toml b/Cargo.toml index 479a89f87..b0302b352 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.1" +version = "4.0.0-beta.3" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" readme = "README.md" @@ -47,10 +47,10 @@ compress = ["actix-http/compress", "awc/compress"] secure-cookies = ["actix-http/secure-cookies"] # 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 = ["tls_rustls", "actix-tls/accept", "actix-tls/rustls", "awc/rustls"] +rustls = ["tls-rustls", "actix-tls/accept", "actix-tls/rustls", "awc/rustls"] [[example]] name = "basic" @@ -82,9 +82,9 @@ actix-service = "2.0.0-beta.4" actix-utils = "3.0.0-beta.2" actix-tls = { version = "3.0.0-beta.3", default-features = false, optional = true } -actix-web-codegen = "0.4.0" -actix-http = "3.0.0-beta.1" -awc = { version = "3.0.0-beta.1", default-features = false } +actix-web-codegen = "0.5.0-beta.1" +actix-http = "3.0.0-beta.3" +awc = { version = "3.0.0-beta.2", default-features = false } ahash = "0.7" bytes = "1" @@ -103,13 +103,18 @@ serde_json = "1.0" serde_urlencoded = "0.7" time = { version = "0.2.23", default-features = false, features = ["std"] } url = "2.1" -tls_openssl = { package = "openssl", version = "0.10.9", optional = true } -tls_rustls = { package = "rustls", version = "0.19.0", optional = true } +tls-openssl = { package = "openssl", version = "0.10.9", optional = true } +tls-rustls = { package = "rustls", version = "0.19.0", optional = true } smallvec = "1.6" +[target.'cfg(windows)'.dependencies.tls-openssl] +version = "0.10.9" +package = "openssl" +features = ["vendored"] +optional = true + [dev-dependencies] actix = { version = "0.11.0-beta.2", default-features = false } -actix-http = { version = "3.0.0-beta.1", features = ["actors"] } rand = "0.8" env_logger = "0.8" serde_derive = "1.0" @@ -117,6 +122,9 @@ brotli2 = "0.3.2" flate2 = "1.0.13" criterion = "0.3" +[profile.dev] +debug = false + [profile.release] lto = true opt-level = 3 diff --git a/README.md b/README.md index 8a703117f..cc7c4cd52 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@
-

Actix web

+

Actix Web

Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust

[![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) -![License](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) +![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.2/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.2)
[![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) @@ -32,7 +32,6 @@ * SSL support using OpenSSL or Rustls * 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) -* Supports [Actix actor framework](https://github.com/actix/actix) * Runs on stable Rust 1.46+ ## Documentation diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 8e5566b61..6e2a241ac 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,12 +1,16 @@ # Changes ## 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] * Replace `v_htmlescape` with `askama_escape`. [#1953] [#1887]: https://github.com/actix/actix-web/pull/1887 [#1953]: https://github.com/actix/actix-web/pull/1953 + ## 0.6.0-beta.1 - 2021-01-07 * `HttpRange::parse` now has its own error type. * Update `bytes` to `1.0`. [#1813] diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index b4317596c..45fa18a69 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.0-beta.1" +version = "0.6.0-beta.2" authors = ["Nikolay Kim "] description = "Static file serving for Actix Web" readme = "README.md" @@ -17,7 +17,7 @@ name = "actix_files" path = "src/lib.rs" [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" askama_escape = "0.10" @@ -33,4 +33,4 @@ percent-encoding = "2.1" [dev-dependencies] actix-rt = "2" -actix-web = "4.0.0-beta.1" +actix-web = "4.0.0-beta.3" diff --git a/actix-files/src/chunked.rs b/actix-files/src/chunked.rs index 2a62b1d26..f639848c9 100644 --- a/actix-files/src/chunked.rs +++ b/actix-files/src/chunked.rs @@ -49,10 +49,7 @@ impl fmt::Debug for ChunkedReadFile { impl Stream for ChunkedReadFile { type Item = Result; - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.as_mut().get_mut(); match this.state { ChunkedReadFileState::File(ref mut file) => { @@ -68,16 +65,13 @@ impl Stream for ChunkedReadFile { .expect("ChunkedReadFile polled after completion"); let fut = spawn_blocking(move || { - let max_bytes = - cmp::min(size.saturating_sub(counter), 65_536) as usize; + let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; let mut buf = Vec::with_capacity(max_bytes); file.seek(io::SeekFrom::Start(offset))?; - let n_bytes = file - .by_ref() - .take(max_bytes as u64) - .read_to_end(&mut buf)?; + let n_bytes = + file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; if n_bytes == 0 { return Err(io::ErrorKind::UnexpectedEof.into()); diff --git a/actix-files/src/directory.rs b/actix-files/src/directory.rs index 1103dd6a7..80e0c98d0 100644 --- a/actix-files/src/directory.rs +++ b/actix-files/src/directory.rs @@ -66,9 +66,7 @@ pub(crate) fn directory_listing( if dir.is_visible(&entry) { let entry = entry.unwrap(); let p = match entry.path().strip_prefix(&dir.path) { - Ok(p) if cfg!(windows) => { - base.join(p).to_string_lossy().replace("\\", "/") - } + Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace("\\", "/"), Ok(p) => base.join(p).to_string_lossy().into_owned(), Err(_) => continue, }; diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index 0cf0a91ba..6f8b28bbf 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -2,9 +2,7 @@ use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc}; use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt}; use actix_web::{ - dev::{ - AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse, - }, + dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse}, error::Error, guard::Guard, http::header::DispositionType, @@ -13,8 +11,8 @@ use actix_web::{ use futures_util::future::{ok, FutureExt, LocalBoxFuture}; use crate::{ - directory_listing, named, Directory, DirectoryRenderer, FilesService, - HttpNewService, MimeOverride, + directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService, + MimeOverride, }; /// Static files handling service. @@ -129,8 +127,8 @@ impl Files { /// Set custom directory renderer pub fn files_listing_renderer(mut self, f: F) -> Self where - for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result - + 'static, + for<'r, 's> F: + Fn(&'r Directory, &'s HttpRequest) -> Result + 'static, { self.renderer = Rc::new(f); self diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index b2f6384a8..04dd9f07f 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -98,8 +98,7 @@ mod tests { #[actix_rt::test] async fn test_if_modified_since_without_if_none_match() { let file = NamedFile::open("Cargo.toml").unwrap(); - let since = - header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); let req = TestRequest::default() .insert_header((header::IF_MODIFIED_SINCE, since)) @@ -123,8 +122,7 @@ mod tests { #[actix_rt::test] async fn test_if_modified_since_with_if_none_match() { let file = NamedFile::open("Cargo.toml").unwrap(); - let since = - header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); let req = TestRequest::default() .insert_header((header::IF_NONE_MATCH, "miss_etag")) @@ -212,8 +210,7 @@ mod tests { #[actix_rt::test] async fn test_named_file_non_ascii_file_name() { let mut file = - NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml") - .unwrap(); + NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml").unwrap(); { file.file(); let _f: &File = &file; @@ -605,10 +602,9 @@ mod tests { #[actix_rt::test] async fn test_static_files() { - let srv = test::init_service( - App::new().service(Files::new("/", ".").show_files_listing()), - ) - .await; + let srv = + test::init_service(App::new().service(Files::new("/", ".").show_files_listing())) + .await; let req = TestRequest::with_uri("/missing").to_request(); let resp = test::call_service(&srv, req).await; @@ -620,10 +616,9 @@ mod tests { let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); - let srv = test::init_service( - App::new().service(Files::new("/", ".").show_files_listing()), - ) - .await; + let srv = + test::init_service(App::new().service(Files::new("/", ".").show_files_listing())) + .await; let req = TestRequest::with_uri("/tests").to_request(); let resp = test::call_service(&srv, req).await; assert_eq!( diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 6fa3f7c6c..a688b2e6c 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -11,8 +11,7 @@ use actix_web::{ dev::{BodyEncoding, SizedStream}, http::{ header::{ - self, Charset, ContentDisposition, DispositionParam, DispositionType, - ExtendedValue, + self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue, }, ContentEncoding, StatusCode, }, @@ -395,18 +394,10 @@ impl NamedFile { resp.encoding(ContentEncoding::Identity); resp.insert_header(( header::CONTENT_RANGE, - format!( - "bytes {}-{}/{}", - offset, - offset + length - 1, - self.md.len() - ), + format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()), )); } else { - resp.insert_header(( - header::CONTENT_RANGE, - format!("bytes */{}", length), - )); + resp.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length))); return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish(); }; } else { diff --git a/actix-files/src/range.rs b/actix-files/src/range.rs index e420ce414..6718980cb 100644 --- a/actix-files/src/range.rs +++ b/actix-files/src/range.rs @@ -46,8 +46,7 @@ impl HttpRange { if start_str.is_empty() { // If no start is specified, end specifies the // range start relative to the end of the file. - let mut length: i64 = - end_str.parse().map_err(|_| ParseRangeErr(()))?; + let mut length: i64 = end_str.parse().map_err(|_| ParseRangeErr(()))?; if length > size_sig { length = size_sig; @@ -72,8 +71,7 @@ impl HttpRange { // If no end is specified, range extends to end of the file. size_sig - start } else { - let mut end: i64 = - end_str.parse().map_err(|_| ParseRangeErr(()))?; + let mut end: i64 = end_str.parse().map_err(|_| ParseRangeErr(()))?; if start > end { return Err(ParseRangeErr(())); diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index a9822f486..14eea6ebc 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -11,8 +11,8 @@ use actix_web::{ use futures_util::future::{ok, Either, LocalBoxFuture, Ready}; use crate::{ - named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, - NamedFile, PathBufWrap, + named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile, + PathBufWrap, }; /// Assembled file serving service. @@ -138,8 +138,7 @@ impl Service for FilesService { match NamedFile::open(path) { Ok(mut named_file) => { if let Some(ref mime_override) = self.mime_override { - let new_disposition = - mime_override(&named_file.content_type.type_()); + let new_disposition = mime_override(&named_file.content_type.type_()); named_file.content_disposition.disposition = new_disposition; } named_file.flags = self.file_flags; diff --git a/actix-files/tests/encoding.rs b/actix-files/tests/encoding.rs index 6cfa3a7f7..d21d4f8fd 100644 --- a/actix-files/tests/encoding.rs +++ b/actix-files/tests/encoding.rs @@ -23,10 +23,9 @@ async fn test_utf8_file_contents() { ); // prefer UTF-8 encoding - let srv = test::init_service( - App::new().service(Files::new("/", "./tests").prefer_utf8(true)), - ) - .await; + let srv = + test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(true))) + .await; let req = TestRequest::with_uri("/utf8.txt").to_request(); let res = test::call_service(&srv, req).await; diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index ed6d87a60..2f47d700d 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 3.0.0-beta.2 - 2021-02-10 +* No notable changes. + + ## 3.0.0-beta.1 - 2021-01-07 * Update `bytes` to `1.0`. [#1813] diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 25fd74a10..6dcf73637 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http-test" -version = "3.0.0-beta.1" +version = "3.0.0-beta.2" authors = ["Nikolay Kim "] description = "Various helpers for Actix applications to use during testing" readme = "README.md" @@ -35,7 +35,7 @@ actix-tls = "3.0.0-beta.3" actix-utils = "3.0.0-beta.2" actix-rt = "2" actix-server = "2.0.0-beta.3" -awc = "3.0.0-beta.1" +awc = "3.0.0-beta.2" base64 = "0.13" bytes = "1" @@ -50,6 +50,12 @@ serde_urlencoded = "0.7" time = { version = "0.2.23", default-features = false, features = ["std"] } 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] -actix-web = "4.0.0-beta.1" -actix-http = "3.0.0-beta.1" +actix-web = "4.0.0-beta.3" +actix-http = "3.0.0-beta.3" diff --git a/actix-http-test/README.md b/actix-http-test/README.md index bca9a7976..66f15979d 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -4,7 +4,7 @@ [![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) -![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) [![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) diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index fcf8b3476..df5774998 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -120,8 +120,7 @@ pub async fn test_server_with_addr>( /// Get first available unused address pub fn unused_addr() -> net::SocketAddr { let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); - let socket = - Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap(); + let socket = Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap(); socket.bind(&addr.into()).unwrap(); socket.set_reuse_address(true).unwrap(); 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 { if uri.starts_with('/') { format!("https://localhost:{}{}", self.addr.port(), uri) @@ -164,7 +163,7 @@ impl TestServer { self.client.get(self.url(path.as_ref()).as_str()) } - /// Create https `GET` request + /// Create HTTPS `GET` request pub fn sget>(&self, path: S) -> ClientRequest { 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()) } - /// Create https `POST` request + /// Create HTTPS `POST` request pub fn spost>(&self, path: S) -> ClientRequest { 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()) } - /// Create https `HEAD` request + /// Create HTTPS `HEAD` request pub fn shead>(&self, path: S) -> ClientRequest { 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()) } - /// Create https `PUT` request + /// Create HTTPS `PUT` request pub fn sput>(&self, path: S) -> ClientRequest { 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()) } - /// Create https `PATCH` request + /// Create HTTPS `PATCH` request pub fn spatch>(&self, path: S) -> ClientRequest { 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()) } - /// Create https `DELETE` request + /// Create HTTPS `DELETE` request pub fn sdelete>(&self, path: S) -> ClientRequest { 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()) } - /// Create https `OPTIONS` request + /// Create HTTPS `OPTIONS` request pub fn soptions>(&self, path: S) -> ClientRequest { self.client.options(self.surl(path.as_ref()).as_str()) } - /// Connect to test http server + /// Connect to test HTTP server pub fn request>(&self, method: Method, path: S) -> ClientRequest { self.client.request(method, path.as_ref()) } @@ -244,26 +243,24 @@ impl TestServer { 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( &mut self, path: &str, - ) -> Result, awc::error::WsClientError> - { + ) -> Result, awc::error::WsClientError> { let url = self.url(path); let connect = self.client.ws(url).connect(); connect.await.map(|(_, framed)| framed) } - /// Connect to a websocket server + /// Connect to a WebSocket server. pub async fn ws( &mut self, - ) -> Result, awc::error::WsClientError> - { + ) -> Result, awc::error::WsClientError> { self.ws_at("/").await } - /// Stop http server + /// Stop HTTP server fn stop(&mut self) { self.system.stop(); } diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 52980fb5d..b781fe50e 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,12 +1,23 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.3 - 2021-02-10 +* No notable changes. + + +## 3.0.0-beta.2 - 2021-02-10 ### Added * `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::append_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] +* `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 * `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 `TryInto` trait. [#1894] * `Extensions::insert` returns Option of replaced item. [#1904] -* Remove `HttpResponseBuilder::json2()` and make `HttpResponseBuilder::json()` take a value by - reference. [#1903] -* `client::error::ConnectError` Resolver variant contains `Box` type [#1905] +* Remove `HttpResponseBuilder::json2()`. [#1903] +* Enable `HttpResponseBuilder::json()` to receive data by value and reference. [#1903] +* `client::error::ConnectError` Resolver variant contains `Box` type. [#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 * `ResponseBuilder::set`; use `ResponseBuilder::insert_header`. [#1869] @@ -26,6 +41,11 @@ * `ResponseBuilder::header`; use `ResponseBuilder::append_header`. [#1869] * `TestRequest::with_hdr`; 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 [#1894]: https://github.com/actix/actix-web/pull/1894 @@ -34,6 +54,8 @@ [#1905]: https://github.com/actix/actix-web/pull/1905 [#1912]: https://github.com/actix/actix-web/pull/1912 [#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 @@ -109,15 +131,14 @@ * Update actix-connect and actix-tls dependencies. -## [2.0.0-beta.3] - 2020-08-14 - +## 2.0.0-beta.3 - 2020-08-14 ### Fixed * Memory leak of `client::pool::ConnectorPoolSupport`. [#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 * Potential UB in h1 decoder using uninitialized memory. [#1614] @@ -128,10 +149,8 @@ [#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 - * Migrate cookie handling to `cookie` crate. [#1558] * Update `sha-1` to 0.9. [#1586] * Fix leak in client pool. [#1580] @@ -141,33 +160,30 @@ [#1586]: https://github.com/actix/actix-web/pull/1586 [#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 - * 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 `u64` instead of a `usize`. * Update `base64` dependency to 0.12 ### Fixed - * Support parsing of `SameSite=None` [#1503] [#1439]: https://github.com/actix/actix-web/pull/1439 [#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 - * Correct spelling of ConnectError::Unresolved [#1487] * Fix a mistake in the encoding of websocket continuation messages wherein Item::FirstText and Item::FirstBinary are each encoded as the other. ### Changed - * Implement `std::error::Error` for our custom errors [#1422] * Remove `failure` support for `ResponseError` since that crate will be deprecated in the near future. @@ -175,338 +191,247 @@ [#1422]: https://github.com/actix/actix-web/pull/1422 [#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 - * 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 respectively - 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] - +* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB + respectively 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 allowing to set max_http_version to limit HTTP version to be used. [#1394] [#1394]: https://github.com/actix/actix-web/pull/1394 [#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 - * Update the `time` dependency to 0.2.7. * 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. ### Fixed - * Allow `SameSite=None` cookies to be sent in a response. -## [1.0.1] - 2019-12-20 +## 1.0.1 - 2019-12-20 ### Fixed - * Poll upgrade service's readiness from HTTP service handlers - * Replace brotli with brotli2 #1224 -## [1.0.0] - 2019-12-13 +## 1.0.0 - 2019-12-13 ### Added - * Add websockets continuation frame support ### Changed - * Replace `flate2-xxx` features with `compress` -## [1.0.0-alpha.5] - 2019-12-09 +## 1.0.0-alpha.5 - 2019-12-09 ### Fixed - * Check `Upgrade` service readiness before calling it - -* Fix buffer remaining capacity calcualtion +* Fix buffer remaining capacity calculation ### Changed - * 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 - * Add impl ResponseBuilder for Error ### Changed - * Use rust based brotli compression library -## [1.0.0-alpha.3] - 2019-12-07 - +## 1.0.0-alpha.3 - 2019-12-07 ### Changed - * Migrate to tokio 0.2 - * Migrate to `std::future` -## [0.2.11] - 2019-11-06 - +## 0.2.11 - 2019-11-06 ### Added - * 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 `actix_files::NamedFile` to be more compatible. (#1151) - +* Add an additional `filename*` param in the `Content-Disposition` header of + `actix_files::NamedFile` to be more compatible. (#1151) * Allow to use `std::convert::Infallible` as `actix_http::error::Error` ### 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 - -* Add support for sending HTTP requests with `Rc` in addition to sending HTTP requests with `RequestHead` +* Add support for sending HTTP requests with `Rc` in addition to sending HTTP requests + with `RequestHead` ### Fixed - * h2 will use error response #1080 - * 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 - * Dropped the `byteorder`-dependency in favor of `stdlib`-implementation - * Update percent-encoding to 2.1 - * Update serde_urlencoded to 0.6.1 ### Fixed - * 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 - * Add `rustls` support - * Add `Clone` impl for `HeaderMap` ### Fixed - * awc client panic #1016 - -* Invalid response with compression middleware enabled, but compression-related features disabled #997 +* Invalid response with compression middleware enabled, but compression-related features + disabled #997 -## [0.2.7] - 2019-07-18 - +## 0.2.7 - 2019-07-18 ### Added - * Add support for downcasting response errors #986 -## [0.2.6] - 2019-07-17 - +## 0.2.6 - 2019-07-17 ### Changed - * Replace `ClonableService` with local copy - * Upgrade `rand` dependency version to 0.7 -## [0.2.5] - 2019-06-28 - +## 0.2.5 - 2019-06-28 ### Added - * Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946 ### Changed - * Use `encoding_rs` crate instead of unmaintained `encoding` crate - * Add `Copy` and `Clone` impls for `ws::Codec` -## [0.2.4] - 2019-06-16 - +## 0.2.4 - 2019-06-16 ### Fixed - * Do not compress NoContent (204) responses #918 -## [0.2.3] - 2019-06-02 - +## 0.2.3 - 2019-06-02 ### Added - * Debug impl for ResponseBuilder - * From SizedStream and BodyStream for Body ### Changed - * SizedStream uses u64 -## [0.2.2] - 2019-05-29 - +## 0.2.2 - 2019-05-29 ### Fixed - * Parse incoming stream before closing stream on disconnect #868 -## [0.2.1] - 2019-05-25 - +## 0.2.1 - 2019-05-25 ### Fixed - * Handle socket read disconnect -## [0.2.0] - 2019-05-12 - +## 0.2.0 - 2019-05-12 ### Changed - * Update actix-service to 0.4 - * Expect and upgrade services accept `ServerConfig` config. ### Deleted - * `OneRequest` service -## [0.1.5] - 2019-05-04 - +## 0.1.5 - 2019-05-04 ### Fixed - * Clean up response extensions in response pool #817 -## [0.1.4] - 2019-04-24 - +## 0.1.4 - 2019-04-24 ### Added - * Allow to render h1 request headers in `Camel-Case` ### Fixed - * Read until eof for http/1.0 responses #771 -## [0.1.3] - 2019-04-23 - +## 0.1.3 - 2019-04-23 ### Fixed - * Fix http client pool management - * Fix http client wait queue management #794 -## [0.1.2] - 2019-04-23 - +## 0.1.2 - 2019-04-23 ### Fixed - * Fix BorrowMutError panic in client connector #793 -## [0.1.1] - 2019-04-19 - +## 0.1.1 - 2019-04-19 ### Changed - * Cookie::max_age() accepts value in seconds - * Cookie::max_age_time() accepts value in time::Duration - * Allow to specify server address for client connector -## [0.1.0] - 2019-04-16 - +## 0.1.0 - 2019-04-16 ### Added - * Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr` ### Changed - * `actix_http::encoding` always available - * use trust-dns-resolver 0.11.0 -## [0.1.0-alpha.5] - 2019-04-12 - +## 0.1.0-alpha.5 - 2019-04-12 ### Added - * Allow to use custom service for upgrade requests - * Added `h1::SendResponse` future. ### Changed - * MessageBody::length() renamed to MessageBody::size() for consistency - * 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 - * Allow to use custom `Expect` handler - * Add minimal `std::error::Error` impl for `Error` ### Changed - * Export IntoHeaderValue - * Render error and return as response body - -* Use thread pool for response body comression +* Use thread pool for response body compression ### Deleted - * Removed PayloadBuffer -## [0.1.0-alpha.3] - 2019-04-02 - +## 0.1.0-alpha.3 - 2019-04-02 ### Added - * Warn when an unsealed private cookie isn't valid UTF-8 ### Fixed - * Rust 1.31.0 compatibility - * Preallocate read buffer for h1 codec - * Detect socket disconnection during protocol selection -## [0.1.0-alpha.2] - 2019-03-29 - +## 0.1.0-alpha.2 - 2019-03-29 ### Added - * Added ws::Message::Nop, no-op websockets message ### Changed - -* Do not use thread pool for decomression if chunk size is smaller than 2048. +* Do not use thread pool for decompression 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 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 22a54f569..2c4775898 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.0.0-beta.1" +version = "3.0.0-beta.3" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" readme = "README.md" @@ -15,7 +15,7 @@ license = "MIT OR Apache-2.0" edition = "2018" [package.metadata.docs.rs] -features = ["openssl", "rustls", "compress", "secure-cookies", "actors"] +features = ["openssl", "rustls", "compress", "secure-cookies"] [lib] name = "actix_http" @@ -36,8 +36,8 @@ compress = ["flate2", "brotli2"] # support for secure cookies secure-cookies = ["cookie/secure"] -# support for actix Actor messages -actors = ["actix"] +# trust-dns as client dns resolver +trust-dns = ["trust-dns-resolver"] [dependencies] 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-rt = "2" actix-tls = "3.0.0-beta.2" -actix = { version = "0.11.0-beta.2", default-features = false, optional = true } base64 = "0.13" bitflags = "1.2" @@ -53,7 +52,6 @@ bytes = "1" bytestring = "1" cookie = { version = "0.14.1", features = ["percent-encode"] } derive_more = "0.99.5" -either = "1.5.3" encoding_rs = "0.8" futures-channel = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } @@ -84,9 +82,11 @@ time = { version = "0.2.23", default-features = false, features = ["std"] } brotli2 = { version="0.3.2", optional = true } flate2 = { version = "1.0.13", optional = true } +trust-dns-resolver = { version = "0.20.0", optional = true } + [dev-dependencies] 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"] } criterion = "0.3" env_logger = "0.8" @@ -94,6 +94,11 @@ serde_derive = "1.0" tls-openssl = { version = "0.10", package = "openssl" } tls-rustls = { version = "0.19", package = "rustls" } +[target.'cfg(windows)'.dev-dependencies.tls-openssl] +version = "0.10.9" +package = "openssl" +features = ["vendored"] + [[bench]] name = "write-camel-case" harness = false diff --git a/actix-http/README.md b/actix-http/README.md index 9dfb85e24..881fbc8c5 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -3,10 +3,13 @@ > HTTP primitives for the Actix ecosystem. [![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) -![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-http) -[![Dependency Status](https://deps.rs/crate/actix-http/2.2.0/status.svg)](https://deps.rs/crate/actix-http/2.2.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) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.3)](https://docs.rs/actix-http/3.0.0-beta.3) +[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) +![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg) +
+[![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 diff --git a/actix-http/src/body.rs b/actix-http/src/body.rs index d5d1989d4..0dbe93a4a 100644 --- a/actix-http/src/body.rs +++ b/actix-http/src/body.rs @@ -1,6 +1,10 @@ -use std::pin::Pin; -use std::task::{Context, Poll}; -use std::{fmt, mem}; +//! Traits and structures to aid consuming and writing HTTP payloads. + +use std::{ + fmt, mem, + pin::Pin, + task::{Context, Poll}, +}; use bytes::{Bytes, BytesMut}; use futures_core::{ready, Stream}; @@ -8,8 +12,8 @@ use pin_project::pin_project; use crate::error::Error; +/// Body size hint. #[derive(Debug, PartialEq, Copy, Clone)] -/// Body size hint pub enum BodySize { None, 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 { fn size(&self) -> BodySize; @@ -80,7 +84,7 @@ impl ResponseBody { impl ResponseBody { pub fn take_body(&mut self) -> ResponseBody { - std::mem::replace(self, ResponseBody::Other(Body::None)) + mem::replace(self, ResponseBody::Other(Body::None)) } } @@ -127,7 +131,7 @@ impl Stream for ResponseBody { } } -/// Represents various types of http message body. +/// Represents various types of HTTP message body. pub enum Body { /// Empty response. `Content-Length` header is not set. None, diff --git a/actix-http/src/client/connector.rs b/actix-http/src/client/connector.rs index 425ee0f70..3bf424d49 100644 --- a/actix-http/src/client/connector.rs +++ b/actix-http/src/client/connector.rs @@ -6,7 +6,7 @@ use actix_codec::{AsyncRead, AsyncWrite}; use actix_rt::net::TcpStream; use actix_service::{apply_fn, Service, ServiceExt}; 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 http::Uri; @@ -19,7 +19,6 @@ use super::Connect; #[cfg(feature = "openssl")] use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector; - #[cfg(feature = "rustls")] use actix_tls::connect::ssl::rustls::ClientConfig; #[cfg(feature = "rustls")] @@ -35,7 +34,8 @@ enum SslConnector { #[cfg(not(any(feature = "openssl", feature = "rustls")))] type SslConnector = (); -/// Manages http client network connectivity +/// Manages HTTP client network connectivity. +/// /// The `Connector` type uses a builder-like combinator pattern for service /// construction that finishes by calling the `.finish()` method. /// @@ -70,7 +70,7 @@ impl Connector<(), ()> { > { Connector { 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(), _phantom: PhantomData, } @@ -161,8 +161,9 @@ where self } - /// Maximum supported http major version - /// Supported versions http/1.1, http/2 + /// Maximum supported HTTP major version. + /// + /// Supported versions are HTTP/1.1 and HTTP/2. pub fn max_http_version(mut self, val: http::Version) -> Self { let versions = match val { 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, Box>> + { + 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> = 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 + } + } + }) + } +} diff --git a/actix-http/src/client/error.rs b/actix-http/src/client/error.rs index 8d609f546..7768462b8 100644 --- a/actix-http/src/client/error.rs +++ b/actix-http/src/client/error.rs @@ -65,13 +65,16 @@ impl From for ConnectError { #[derive(Debug, Display, From)] pub enum InvalidUrl { - #[display(fmt = "Missing url scheme")] + #[display(fmt = "Missing URL scheme")] MissingScheme, - #[display(fmt = "Unknown url scheme")] + + #[display(fmt = "Unknown URL scheme")] UnknownScheme, + #[display(fmt = "Missing host name")] MissingHost, - #[display(fmt = "Url parse error: {}", _0)] + + #[display(fmt = "URL parse error: {}", _0)] HttpError(http::Error), } @@ -83,25 +86,33 @@ pub enum SendRequestError { /// Invalid URL #[display(fmt = "Invalid URL: {}", _0)] Url(InvalidUrl), + /// Failed to connect to host #[display(fmt = "Failed to connect to host: {}", _0)] Connect(ConnectError), + /// Error sending request Send(io::Error), + /// Error parsing response Response(ParseError), + /// Http error #[display(fmt = "{}", _0)] Http(HttpError), + /// Http2 error #[display(fmt = "{}", _0)] H2(h2::Error), + /// Response took too long #[display(fmt = "Timeout while waiting for response")] 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")] TunnelNotSupported, + /// Error sending request body Body(Error), } @@ -127,7 +138,8 @@ pub enum FreezeRequestError { /// Invalid URL #[display(fmt = "Invalid URL: {}", _0)] Url(InvalidUrl), - /// Http error + + /// HTTP error #[display(fmt = "{}", _0)] Http(HttpError), } diff --git a/actix-http/src/client/h1proto.rs b/actix-http/src/client/h1proto.rs index 24f4207e8..92c3c0e1b 100644 --- a/actix-http/src/client/h1proto.rs +++ b/actix-http/src/client/h1proto.rs @@ -48,11 +48,11 @@ where match wrt.get_mut().split().freeze().try_into_value() { Ok(value) => match head { RequestHeadType::Owned(ref mut head) => { - head.headers.insert(HOST, value) + head.headers.insert(HOST, value); } RequestHeadType::Rc(_, ref mut extra_headers) => { 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), diff --git a/actix-http/src/client/mod.rs b/actix-http/src/client/mod.rs index dd1e9b25a..9c7f632ea 100644 --- a/actix-http/src/client/mod.rs +++ b/actix-http/src/client/mod.rs @@ -1,4 +1,5 @@ -//! Http client api +//! HTTP client. + use http::Uri; mod config; diff --git a/actix-http/src/clinu/mod.rs b/actix-http/src/clinu/mod.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index f178db274..9f84b8694 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -4,9 +4,11 @@ use std::rc::Rc; use std::time::Duration; 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 futures_util::{future, FutureExt}; use time::OffsetDateTime; /// "Sun, 06 Nov 1994 08:49:37 GMT".len() @@ -49,7 +51,7 @@ struct Inner { ka_enabled: bool, secure: bool, local_addr: Option, - timer: DateService, + date_service: DateService, } impl Clone for ServiceConfig { @@ -91,41 +93,41 @@ impl ServiceConfig { client_disconnect, secure, local_addr, - timer: DateService::new(), + date_service: DateService::new(), })) } + /// Returns true if connection is secure (HTTPS) #[inline] - /// Returns true if connection is secure(https) pub fn secure(&self) -> bool { self.0.secure } - #[inline] /// Returns the local address that this server is bound to. + #[inline] pub fn local_addr(&self) -> Option { self.0.local_addr } - #[inline] /// Keep alive duration if configured. + #[inline] pub fn keep_alive(&self) -> Option { self.0.keep_alive } - #[inline] /// Return state of connection keep-alive functionality + #[inline] pub fn keep_alive_enabled(&self) -> bool { self.0.ka_enabled } - #[inline] /// Client timeout for first request. + #[inline] pub fn client_timer(&self) -> Option { let delay_time = self.0.client_timeout; if delay_time != 0 { Some(sleep_until( - self.0.timer.now() + Duration::from_millis(delay_time), + self.0.date_service.now() + Duration::from_millis(delay_time), )) } else { None @@ -136,7 +138,7 @@ impl ServiceConfig { pub fn client_timer_expire(&self) -> Option { let delay = self.0.client_timeout; if delay != 0 { - Some(self.0.timer.now() + Duration::from_millis(delay)) + Some(self.0.date_service.now() + Duration::from_millis(delay)) } else { None } @@ -146,7 +148,7 @@ impl ServiceConfig { pub fn client_disconnect_timer(&self) -> Option { let delay = self.0.client_disconnect; if delay != 0 { - Some(self.0.timer.now() + Duration::from_millis(delay)) + Some(self.0.date_service.now() + Duration::from_millis(delay)) } else { None } @@ -156,7 +158,7 @@ impl ServiceConfig { /// Return keep-alive timer delay is configured. pub fn keep_alive_timer(&self) -> Option { 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 { None } @@ -165,7 +167,7 @@ impl ServiceConfig { /// Keep-alive expire time pub fn keep_alive_expire(&self) -> Option { if let Some(ka) = self.0.keep_alive { - Some(self.0.timer.now() + ka) + Some(self.0.date_service.now() + ka) } else { None } @@ -173,7 +175,7 @@ impl ServiceConfig { #[inline] pub(crate) fn now(&self) -> Instant { - self.0.timer.now() + self.0.date_service.now() } #[doc(hidden)] @@ -181,7 +183,7 @@ impl ServiceConfig { let mut buf: [u8; 39] = [0; 39]; buf[..6].copy_from_slice(b"date: "); self.0 - .timer + .date_service .set_date(|date| buf[6..35].copy_from_slice(&date.bytes)); buf[35..].copy_from_slice(b"\r\n\r\n"); dst.extend_from_slice(&buf); @@ -189,7 +191,7 @@ impl ServiceConfig { pub(crate) fn set_date_header(&self, dst: &mut BytesMut) { self.0 - .timer + .date_service .set_date(|date| dst.extend_from_slice(&date.bytes)); } } @@ -230,57 +232,103 @@ impl fmt::Write for Date { } } -#[derive(Clone)] -struct DateService(Rc); - -struct DateServiceInner { - current: Cell>, +/// Service for update Date and Instant periodically at 500 millis interval. +struct DateService { + current: Rc>, + handle: JoinHandle<()>, } -impl DateServiceInner { - fn new() -> Self { - DateServiceInner { - current: Cell::new(None), - } - } - - fn reset(&self) { - self.current.take(); - } - - fn update(&self) { - let now = Instant::now(); - let date = Date::new(); - self.current.set(Some((date, now))); +impl Drop for DateService { + fn drop(&mut self) { + // stop the timer update async task on drop. + self.handle.abort(); } } impl DateService { 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(¤t); + // 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) { - if self.0.current.get().is_none() { - self.0.update(); + let mut interval = interval(Duration::from_millis(500)); + loop { + let now = interval.tick().await; + let date = Date::new(); + current_clone.set((date, now)); + } + }); - // periodic date update - let s = self.clone(); - actix_rt::spawn(sleep(Duration::from_millis(500)).then(move |_| { - s.0.reset(); - future::ready(()) - })); - } + DateService { current, handle } } fn now(&self) -> Instant { - self.check_date(); - self.0.current.get().unwrap().1 + self.current.get().1 } fn set_date(&self, mut f: F) { - self.check_date(); - f(&self.0.current.get().unwrap().0); + f(&self.current.get().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> = 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 { use super::*; - // Test modifying the date from within the closure - // passed to `set_date` - #[test] - fn test_evil_date() { - let service = DateService::new(); - // Make sure that `check_date` doesn't try to spawn a task - service.0.update(); - service.set_date(|_| service.0.reset()); + use actix_rt::task::yield_now; + + #[actix_rt::test] + async fn test_date_service_update() { + let settings = ServiceConfig::new(KeepAlive::Os, 0, 0, false, None); + + yield_now().await; + + 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] diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index 2cf2f6e03..f0abae865 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -1,7 +1,11 @@ -use std::future::Future; -use std::io::{self, Write}; -use std::pin::Pin; -use std::task::{Context, Poll}; +//! Stream decoders. + +use std::{ + future::Future, + io::{self, Write as _}, + pin::Pin, + task::{Context, Poll}, +}; use actix_rt::task::{spawn_blocking, JoinHandle}; use brotli2::write::BrotliDecoder; @@ -9,11 +13,13 @@ use bytes::Bytes; use flate2::write::{GzDecoder, ZlibDecoder}; use futures_core::{ready, Stream}; -use super::Writer; -use crate::error::{BlockingError, PayloadError}; -use crate::http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING}; +use crate::{ + encoding::Writer, + 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 { decoder: Option, @@ -41,6 +47,7 @@ where ))), _ => None, }; + Decoder { decoder, stream, @@ -53,15 +60,11 @@ where #[inline] pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder { // check content-encoding - let encoding = if let Some(enc) = headers.get(&CONTENT_ENCODING) { - if let Ok(enc) = enc.to_str() { - ContentEncoding::from(enc) - } else { - ContentEncoding::Identity - } - } else { - ContentEncoding::Identity - }; + let encoding = headers + .get(&CONTENT_ENCODING) + .and_then(|val| val.to_str().ok()) + .map(ContentEncoding::from) + .unwrap_or(ContentEncoding::Identity); Self::new(stream, encoding) } @@ -81,8 +84,10 @@ where if let Some(ref mut fut) = self.fut { let (chunk, decoder) = ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??; + self.decoder = Some(decoder); self.fut.take(); + if let Some(chunk) = chunk { return Poll::Ready(Some(Ok(chunk))); } @@ -92,13 +97,15 @@ where return Poll::Ready(None); } - match Pin::new(&mut self.stream).poll_next(cx) { - Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))), - Poll::Ready(Some(Ok(chunk))) => { + match ready!(Pin::new(&mut self.stream).poll_next(cx)) { + Some(Err(err)) => return Poll::Ready(Some(Err(err))), + + Some(Ok(chunk)) => { 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)?; self.decoder = Some(decoder); + if let Some(chunk) = chunk { return Poll::Ready(Some(Ok(chunk))); } @@ -108,13 +115,16 @@ where Ok((chunk, decoder)) })); } + continue; } else { return Poll::Ready(Some(Ok(chunk))); } } - Poll::Ready(None) => { + + None => { self.eof = true; + return if let Some(mut decoder) = self.decoder.take() { match decoder.feed_eof() { Ok(Some(res)) => Poll::Ready(Some(Ok(res))), @@ -125,10 +135,8 @@ where Poll::Ready(None) }; } - Poll::Pending => break, } } - Poll::Pending } } @@ -144,6 +152,7 @@ impl ContentDecoder { ContentDecoder::Br(ref mut decoder) => match decoder.flush() { Ok(()) => { let b = decoder.get_mut().take(); + if !b.is_empty() { Ok(Some(b)) } else { @@ -152,9 +161,11 @@ impl ContentDecoder { } Err(e) => Err(e), }, + ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() { Ok(_) => { let b = decoder.get_mut().take(); + if !b.is_empty() { Ok(Some(b)) } else { @@ -163,6 +174,7 @@ impl ContentDecoder { } Err(e) => Err(e), }, + ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() { Ok(_) => { let b = decoder.get_mut().take(); @@ -183,6 +195,7 @@ impl ContentDecoder { Ok(_) => { decoder.flush()?; let b = decoder.get_mut().take(); + if !b.is_empty() { Ok(Some(b)) } else { @@ -191,10 +204,12 @@ impl ContentDecoder { } Err(e) => Err(e), }, + ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; let b = decoder.get_mut().take(); + if !b.is_empty() { Ok(Some(b)) } else { @@ -203,9 +218,11 @@ impl ContentDecoder { } Err(e) => Err(e), }, + ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; + let b = decoder.get_mut().take(); if !b.is_empty() { Ok(Some(b)) diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 1d4a8e933..366ecb8c4 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -1,8 +1,11 @@ -//! Stream encoder -use std::future::Future; -use std::io::{self, Write}; -use std::pin::Pin; -use std::task::{Context, Poll}; +//! Stream encoders. + +use std::{ + future::Future, + io::{self, Write as _}, + pin::Pin, + task::{Context, Poll}, +}; use actix_rt::task::{spawn_blocking, JoinHandle}; use brotli2::write::BrotliEncoder; @@ -11,15 +14,19 @@ use flate2::write::{GzEncoder, ZlibEncoder}; use futures_core::ready; use pin_project::pin_project; -use crate::body::{Body, BodySize, MessageBody, ResponseBody}; -use crate::http::header::{ContentEncoding, CONTENT_ENCODING}; -use crate::http::{HeaderValue, StatusCode}; -use crate::{Error, ResponseHead}; +use crate::{ + body::{Body, BodySize, MessageBody, ResponseBody}, + http::{ + header::{ContentEncoding, CONTENT_ENCODING}, + HeaderValue, StatusCode, + }, + Error, ResponseHead, +}; use super::Writer; use crate::error::BlockingError; -const INPLACE: usize = 1024; +const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024; #[pin_project] pub struct Encoder { @@ -138,23 +145,28 @@ impl MessageBody for Encoder { if let Some(ref mut fut) = this.fut { let mut encoder = ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??; + let chunk = encoder.take(); *this.encoder = Some(encoder); this.fut.take(); + if !chunk.is_empty() { 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 { - 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 chunk.len() < INPLACE { + if chunk.len() < MAX_CHUNK_SIZE_ENCODE_IN_PLACE { encoder.write(&chunk)?; let chunk = encoder.take(); *this.encoder = Some(encoder); + if !chunk.is_empty() { return Poll::Ready(Some(Ok(chunk))); } @@ -168,7 +180,8 @@ impl MessageBody for Encoder { return Poll::Ready(Some(Ok(chunk))); } } - Poll::Ready(None) => { + + None => { if let Some(encoder) = this.encoder.take() { let chunk = encoder.finish()?; if chunk.is_empty() { @@ -181,7 +194,6 @@ impl MessageBody for Encoder { return Poll::Ready(None); } } - val => return val, } } } diff --git a/actix-http/src/encoding/mod.rs b/actix-http/src/encoding/mod.rs index 9eaf4104e..cb271c638 100644 --- a/actix-http/src/encoding/mod.rs +++ b/actix-http/src/encoding/mod.rs @@ -1,4 +1,5 @@ -//! Content-Encoding support +//! Content-Encoding support. + use std::io; use bytes::{Bytes, BytesMut}; diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 7beedc3ba..5eb3c157a 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -38,7 +38,7 @@ pub type Result = result::Result; /// converting errors with `into()`. /// /// Whenever it is created from an external object a response error is created -/// for it that can be used to create an http response from it this means that +/// for it that can be used to create an HTTP response from it this means that /// if you have access to an actix `Error` you can always get a /// `ResponseError` reference from it. pub struct Error { @@ -404,7 +404,7 @@ impl ResponseError for crate::cookie::ParseError { } #[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 { /// Service error Service(Error), @@ -968,12 +968,6 @@ where 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)] mod tests { use super::*; diff --git a/actix-http/src/h1/codec.rs b/actix-http/src/h1/codec.rs index d5035df26..634ca25e8 100644 --- a/actix-http/src/h1/codec.rs +++ b/actix-http/src/h1/codec.rs @@ -199,10 +199,10 @@ mod tests { use http::Method; use super::*; - use crate::httpmessage::HttpMessage; + use crate::HttpMessage; - #[test] - fn test_http_request_chunked_payload_and_next_message() { + #[actix_rt::test] + async fn test_http_request_chunked_payload_and_next_message() { let mut codec = Codec::default(); let mut buf = BytesMut::from( diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index 9da958563..93a4b13d2 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -224,7 +224,7 @@ impl MessageType for Request { let decoder = match length { PayloadLength::Payload(pl) => pl, PayloadLength::UpgradeWebSocket => { - // upgrade(websocket) + // upgrade (WebSocket) PayloadType::Stream(PayloadDecoder::eof()) } PayloadLength::None => { @@ -652,7 +652,7 @@ mod tests { use super::*; use crate::error::ParseError; use crate::http::header::{HeaderName, SET_COOKIE}; - use crate::httpmessage::HttpMessage; + use crate::HttpMessage; impl PayloadType { fn unwrap(self) -> PayloadDecoder { @@ -830,8 +830,8 @@ mod tests { .get_all(SET_COOKIE) .map(|v| v.to_str().unwrap().to_owned()) .collect(); - assert_eq!(val[1], "c1=cookie1"); - assert_eq!(val[0], "c2=cookie2"); + assert_eq!(val[0], "c1=cookie1"); + assert_eq!(val[1], "c2=cookie2"); } #[test] diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index f8fc95921..2e729b78d 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -37,15 +37,14 @@ bitflags! { pub struct Flags: u8 { const STARTED = 0b0000_0001; const KEEPALIVE = 0b0000_0010; - const POLLED = 0b0000_0100; - const SHUTDOWN = 0b0000_1000; - const READ_DISCONNECT = 0b0001_0000; - const WRITE_DISCONNECT = 0b0010_0000; - const UPGRADE = 0b0100_0000; + const SHUTDOWN = 0b0000_0100; + const READ_DISCONNECT = 0b0000_1000; + const WRITE_DISCONNECT = 0b0001_0000; + const UPGRADE = 0b0010_0000; } } -#[pin_project::pin_project] +#[pin_project] /// Dispatcher for HTTP/1.1 protocol pub struct Dispatcher where @@ -139,27 +138,14 @@ where fn is_empty(&self) -> bool { matches!(self, State::None) } - - fn is_call(&self) -> bool { - matches!(self, State::ServiceCall(_)) - } } + enum PollResponse { Upgrade(Request), DoNothing, 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 Dispatcher where T: AsyncRead + AsyncWrite + Unpin, @@ -174,62 +160,35 @@ where { /// Create HTTP/1 dispatcher. pub(crate) fn new( - stream: T, - config: ServiceConfig, - services: Rc>, - on_connect_data: OnConnectData, - peer_addr: Option, - ) -> 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, - codec: Codec, config: ServiceConfig, - read_buf: BytesMut, - timeout: Option, - services: Rc>, + flow: Rc>, on_connect_data: OnConnectData, peer_addr: Option, ) -> Self { - let keepalive = config.keep_alive_enabled(); - let flags = if keepalive { + let flags = if config.keep_alive_enabled() { Flags::KEEPALIVE } else { Flags::empty() }; // keep-alive timer - let (ka_expire, ka_timer) = if let Some(delay) = timeout { - (delay.deadline(), Some(delay)) - } else if let Some(delay) = config.keep_alive_timer() { - (delay.deadline(), Some(delay)) - } else { - (config.now(), None) + let (ka_expire, ka_timer) = match config.keep_alive_timer() { + Some(delay) => (delay.deadline(), Some(delay)), + None => (config.now(), None), }; Dispatcher { inner: DispatcherState::Normal(InnerDispatcher { + read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), payload: None, state: State::None, error: None, messages: VecDeque::new(), io: Some(io), - codec, - read_buf, - flow: services, + codec: Codec::new(config), + flow, on_connect_data, flags, peer_addr, @@ -286,15 +245,12 @@ where self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Result { - let len = self.write_buf.len(); - if len == 0 { - return Ok(false); - } - let InnerDispatcherProj { io, write_buf, .. } = self.project(); let mut io = Pin::new(io.as_mut().unwrap()); + let len = write_buf.len(); let mut written = 0; + while written < len { match io.as_mut().poll_write(cx, &write_buf[written..]) { Poll::Ready(Ok(0)) => { @@ -312,11 +268,13 @@ where } } - // SAFETY: setting length to 0 is safe - // skips one length check vs truncate - unsafe { write_buf.set_len(0) } + // everything has written to io. clear buffer. + write_buf.clear(); - Ok(false) + // flush the io and check if get blocked. + let blocked = io.poll_flush(cx)?.is_pending(); + + Ok(blocked) } fn send_response( @@ -324,9 +282,10 @@ where message: Response<()>, body: ResponseBody, ) -> Result<(), DispatchError> { + let size = body.size(); let mut this = self.project(); this.codec - .encode(Message::Item((message, body.size())), &mut this.write_buf) + .encode(Message::Item((message, size)), &mut this.write_buf) .map_err(|err| { if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::Incomplete(None)); @@ -335,7 +294,7 @@ where })?; this.flags.set(Flags::KEEPALIVE, this.codec.keepalive()); - match body.size() { + match size { BodySize::None | BodySize::Empty => this.state.set(State::None), _ => this.state.set(State::SendPayload(body)), }; @@ -352,109 +311,121 @@ where mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Result { - loop { + 'res: loop { let mut this = self.as_mut().project(); - // state is not changed on Poll::Pending. - // other variant and conditions always trigger a state change(or an error). - let state_change = match this.state.project() { + match this.state.as_mut().project() { + // no future is in InnerDispatcher state. pop next message. StateProj::None => match this.messages.pop_front() { + // handle request message. Some(DispatcherMessage::Item(req)) => { - self.as_mut().handle_request(req, cx)?; - true + // Handle `EXPECT: 100-Continue` header + 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)) => { + // send_response would update InnerDispatcher state to SendPayload or + // None(If response body is empty). + // continue loop to poll it. self.as_mut() .send_response(res, ResponseBody::Other(Body::Empty))?; - true } + + // return with upgrade request and poll it exclusively. Some(DispatcherMessage::Upgrade(req)) => { return Ok(PollResponse::Upgrade(req)); } - None => false, - }, - StateProj::ExpectCall(fut) => match fut.poll(cx) { - 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, + + // all messages are dealt with. + None => return Ok(PollResponse::DoNothing), }, StateProj::ServiceCall(fut) => match fut.poll(cx) { + // service call resolved. send response. Poll::Ready(Ok(res)) => { let (res, body) = res.into().replace_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(()); self.as_mut().send_response(res, body.into_body())?; - true } - Poll::Pending => false, - }, - StateProj::SendPayload(mut stream) => { - loop { - if this.write_buf.len() < super::payload::MAX_BUFFER_SIZE { - match stream.as_mut().poll_next(cx) { - Poll::Ready(Some(Ok(item))) => { - this.codec.encode( - Message::Chunk(Some(item)), - &mut this.write_buf, - )?; - 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); + + // service call pending and could be waiting for more chunk messages. + // (pipeline message limit and/or payload can_read limit) + Poll::Pending => { + // no new message is decoded and no new payload is feed. + // nothing to do except waiting for new incoming data from client. + if !self.as_mut().poll_request(cx)? { + return Ok(PollResponse::DoNothing); } - break; + // otherwise keep loop. } - continue; - } - }; + }, - // state is changed and continue when the state is not Empty - if state_change { - if !self.state.is_empty() { - continue; - } - } else { - // if read-backpressure is enabled and we consumed some data. - // we may read more data and retry - if self.state.is_call() { - if self.as_mut().poll_request(cx)? { - continue; + StateProj::SendPayload(mut stream) => { + // keep populate writer buffer until buffer size limit hit, + // get blocked or finished. + while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE { + match stream.as_mut().poll_next(cx) { + Poll::Ready(Some(Ok(item))) => { + this.codec.encode( + Message::Chunk(Some(item)), + &mut this.write_buf, + )?; + } + + Poll::Ready(None) => { + this.codec + .encode(Message::Chunk(None), &mut this.write_buf)?; + // payload stream finished. + // set state to None and handle next message + this.state.set(State::None); + continue 'res; + } + + Poll::Ready(Some(Err(err))) => { + return Err(DispatchError::Service(err)) + } + + Poll::Pending => return Ok(PollResponse::DoNothing), + } } - } else if !self.messages.is_empty() { - continue; + // buffer is beyond max size. + // 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( @@ -494,9 +465,9 @@ where // future is error. send response and return a result. On success // to notify the dispatcher a new state is set and the outer loop // should be continue. - Poll::Ready(Err(e)) => { - let e = e.into(); - let res: Response = e.into(); + Poll::Ready(Err(err)) => { + let err = err.into(); + let res: Response = err.into(); let (res, body) = res.replace_body(()); return self.send_response(res, body.into_body()); } @@ -514,9 +485,9 @@ where } // see the comment on ExpectCall state branch's Pending. Poll::Pending => Ok(()), - // see the comment on ExpectCall state branch's Ready(Err(e)). - Poll::Ready(Err(e)) => { - let res: Response = e.into().into(); + // see the comment on ExpectCall state branch's Ready(Err(err)). + Poll::Ready(Err(err)) => { + let res: Response = err.into().into(); let (res, body) = res.replace_body(()); self.send_response(res, body.into_body()) } @@ -608,25 +579,25 @@ where // decode is partial and buffer is not full yet. // break and wait for more read. Ok(None) => break, - Err(ParseError::Io(e)) => { + Err(ParseError::Io(err)) => { self.as_mut().client_disconnected(); this = self.as_mut().project(); - *this.error = Some(DispatchError::Io(e)); + *this.error = Some(DispatchError::Io(err)); break; } Err(ParseError::TooLarge) => { if let Some(mut payload) = this.payload.take() { 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( - Response::PayloadTooLarge().finish().drop_body(), + Response::RequestHeaderFieldsTooLarge().finish().drop_body(), )); this.flags.insert(Flags::READ_DISCONNECT); *this.error = Some(ParseError::TooLarge.into()); break; } - Err(e) => { + Err(err) => { if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::EncodingCorrupted); } @@ -636,7 +607,7 @@ where Response::BadRequest().finish().drop_body(), )); this.flags.insert(Flags::READ_DISCONNECT); - *this.error = Some(e.into()); + *this.error = Some(err.into()); break; } } @@ -774,7 +745,12 @@ where // at this point it's not known io is still scheduled to // be waked up. so force wake up dispatcher just in case. // TODO: figure out the overhead. - 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); } @@ -839,9 +815,8 @@ where if inner.flags.contains(Flags::WRITE_DISCONNECT) { Poll::Ready(Ok(())) } else { - // flush buffer - inner.as_mut().poll_flush(cx)?; - if !inner.write_buf.is_empty() { + // flush buffer and wait on block. + if inner.as_mut().poll_flush(cx)? { Poll::Pending } else { Pin::new(inner.project().io.as_mut().unwrap()) diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index bd8287b26..69e69de42 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -144,104 +144,54 @@ pub(crate) trait MessageType: Sized { let k = key.as_str().as_bytes(); let k_len = k.len(); - match value { - Value::One(ref val) => { - let v = val.as_ref(); - let v_len = v.len(); + // TODO: drain? + for val in value.iter() { + let v = val.as_ref(); + let v_len = v.len(); - // key length + value length + colon + space + \r\n - let len = k_len + v_len + 4; + // key length + value length + colon + space + \r\n + let len = k_len + v_len + 4; - 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 - 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 + if len > remaining { + // SAFETY: all the bytes written up to position "pos" are initialized + // the written byte count and pointer advancement are kept in sync unsafe { - // use upper Camel-Case - 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); + dst.advance_mut(pos); } - pos += len; - remaining -= len; + 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(); } - Value::Multi(ref vec) => { - for val in vec { - let v = val.as_ref(); - let v_len = v.len(); - let len = k_len + v_len + 4; - - if len > remaining { - // 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 { - 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; + // 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 { + // use Camel-Case headers + 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; } }); @@ -579,8 +529,8 @@ mod tests { ); } - #[test] - fn test_camel_case() { + #[actix_rt::test] + async fn test_camel_case() { let mut bytes = BytesMut::with_capacity(2048); let mut head = RequestHead::default(); head.set_camel_case_headers(true); @@ -643,8 +593,8 @@ mod tests { assert!(data.contains("date: date\r\n")); } - #[test] - fn test_extra_headers() { + #[actix_rt::test] + async fn test_extra_headers() { let mut bytes = BytesMut::with_capacity(2048); let mut head = RequestHead::default(); @@ -677,8 +627,8 @@ mod tests { assert!(data.contains("date: date\r\n")); } - #[test] - fn test_no_content_length() { + #[actix_rt::test] + async fn test_no_content_length() { let mut bytes = BytesMut::with_capacity(2048); let mut res: Response<()> = diff --git a/actix-http/src/h1/mod.rs b/actix-http/src/h1/mod.rs index 3d5dea5d6..7e6df6ceb 100644 --- a/actix-http/src/h1/mod.rs +++ b/actix-http/src/h1/mod.rs @@ -1,4 +1,4 @@ -//! HTTP/1 implementation +//! HTTP/1 protocol implementation. use bytes::{Bytes, BytesMut}; mod client; diff --git a/actix-http/src/h2/mod.rs b/actix-http/src/h2/mod.rs index c05ee609d..7eff44ac1 100644 --- a/actix-http/src/h2/mod.rs +++ b/actix-http/src/h2/mod.rs @@ -1,4 +1,4 @@ -//! HTTP/2 implementation. +//! HTTP/2 protocol. use std::{ pin::Pin, diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 36c76b17c..e00c8d968 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -243,7 +243,7 @@ where } } -/// `Service` implementation for http/2 transport +/// `Service` implementation for HTTP/2 transport pub struct H2ServiceHandler where S: Service, diff --git a/actix-http/src/header/as_name.rs b/actix-http/src/header/as_name.rs new file mode 100644 index 000000000..af81ff7f2 --- /dev/null +++ b/actix-http/src/header/as_name.rs @@ -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, InvalidHeaderName>; +} + +impl Sealed for HeaderName { + fn try_as_name(&self) -> Result, InvalidHeaderName> { + Ok(Cow::Borrowed(self)) + } +} +impl AsHeaderName for HeaderName {} + +impl Sealed for &HeaderName { + fn try_as_name(&self) -> Result, InvalidHeaderName> { + Ok(Cow::Borrowed(*self)) + } +} +impl AsHeaderName for &HeaderName {} + +impl Sealed for &str { + fn try_as_name(&self) -> Result, InvalidHeaderName> { + HeaderName::from_str(self).map(Cow::Owned) + } +} +impl AsHeaderName for &str {} + +impl Sealed for String { + fn try_as_name(&self) -> Result, InvalidHeaderName> { + HeaderName::from_str(self).map(Cow::Owned) + } +} +impl AsHeaderName for String {} + +impl Sealed for &String { + fn try_as_name(&self) -> Result, InvalidHeaderName> { + HeaderName::from_str(self).map(Cow::Owned) + } +} +impl AsHeaderName for &String {} diff --git a/actix-http/src/header/common/if_range.rs b/actix-http/src/header/common/if_range.rs index 1513b7a44..0a5749505 100644 --- a/actix-http/src/header/common/if_range.rs +++ b/actix-http/src/header/common/if_range.rs @@ -5,7 +5,7 @@ use crate::header::{ self, from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, 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) /// diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index 8f20f3e6f..106e44edb 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -1,332 +1,563 @@ -use std::{ - collections::hash_map::{self, Entry}, - convert::TryFrom, -}; +//! A multi-value [`HeaderMap`] and its iterators. + +use std::{borrow::Cow, collections::hash_map, ops}; use ahash::AHashMap; -use either::Either; use http::header::{HeaderName, HeaderValue}; use smallvec::{smallvec, SmallVec}; +use crate::header::AsHeaderName; + /// A multi-map of HTTP headers. /// -/// `HeaderMap` is a "multi-map" of [`HeaderName`] to one or more values. +/// `HeaderMap` is a "multi-map" of [`HeaderName`] to one or more [`HeaderValue`]s. +/// +/// # Examples +/// ``` +/// use actix_http::http::{header, HeaderMap, HeaderValue}; +/// +/// let mut map = HeaderMap::new(); +/// +/// map.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/plain")); +/// map.insert(header::ORIGIN, HeaderValue::from_static("example.com")); +/// +/// assert!(map.contains_key(header::CONTENT_TYPE)); +/// assert!(map.contains_key(header::ORIGIN)); +/// +/// let mut removed = map.remove(header::ORIGIN); +/// assert_eq!(removed.next().unwrap(), "example.com"); +/// +/// assert!(!map.contains_key(header::ORIGIN)); +/// ``` #[derive(Debug, Clone, Default)] pub struct HeaderMap { pub(crate) inner: AHashMap, } +/// A bespoke non-empty list for HeaderMap values. #[derive(Debug, Clone)] -pub(crate) enum Value { - One(HeaderValue), - Multi(SmallVec<[HeaderValue; 4]>), +pub(crate) struct Value { + inner: SmallVec<[HeaderValue; 4]>, } impl Value { - fn first(&self) -> &HeaderValue { - match self { - Value::One(ref val) => val, - Value::Multi(ref val) => &val[0], + fn one(val: HeaderValue) -> Self { + Self { + inner: smallvec![val], } } + fn first(&self) -> &HeaderValue { + &self.inner[0] + } + fn first_mut(&mut self) -> &mut HeaderValue { - match self { - Value::One(ref mut val) => val, - Value::Multi(ref mut val) => &mut val[0], - } + &mut self.inner[0] } - fn append(&mut self, val: HeaderValue) { - match self { - Value::One(_) => { - let data = std::mem::replace(self, Value::Multi(smallvec![val])); - match data { - Value::One(val) => self.append(val), - Value::Multi(_) => unreachable!(), - } - } - Value::Multi(ref mut vec) => vec.push(val), - } + fn append(&mut self, new_val: HeaderValue) { + self.inner.push(new_val) + } +} + +impl ops::Deref for Value { + type Target = SmallVec<[HeaderValue; 4]>; + + fn deref(&self) -> &Self::Target { + &self.inner } } impl HeaderMap { /// Create an empty `HeaderMap`. /// - /// The map will be created without any capacity. This function will not - /// allocate. + /// The map will be created without any capacity; this function will not allocate. + /// + /// # Examples + /// ``` + /// # use actix_http::http::HeaderMap; + /// let map = HeaderMap::new(); + /// + /// assert!(map.is_empty()); + /// assert_eq!(0, map.capacity()); + /// ``` pub fn new() -> Self { - HeaderMap { - inner: AHashMap::default(), - } + HeaderMap::default() } /// Create an empty `HeaderMap` with the specified capacity. /// - /// The returned map will allocate internal storage in order to hold about - /// `capacity` elements without reallocating. However, this is a "best - /// effort" as there are usage patterns that could cause additional - /// allocations before `capacity` headers are stored in the map. + /// The map will be able to hold at least `capacity` elements without needing to reallocate. + /// If `capacity` is 0, the map will be created without allocating. /// - /// More capacity than requested may be allocated. - pub fn with_capacity(capacity: usize) -> HeaderMap { + /// # Examples + /// ``` + /// # use actix_http::http::HeaderMap; + /// let map = HeaderMap::with_capacity(16); + /// + /// assert!(map.is_empty()); + /// assert!(map.capacity() >= 16); + /// ``` + pub fn with_capacity(capacity: usize) -> Self { HeaderMap { - inner: AHashMap::with_capacity_and_hasher(capacity, Default::default()), + inner: AHashMap::with_capacity(capacity), } } - /// Returns the number of keys stored in the map. + /// Create new `HeaderMap` from a `http::HeaderMap`-like drain. + pub(crate) fn from_drain(mut drain: I) -> Self + where + I: Iterator, HeaderValue)>, + { + let (first_name, first_value) = match drain.next() { + None => return HeaderMap::new(), + Some((name, val)) => { + let name = name.expect("drained first item had no name"); + (name, val) + } + }; + + let (lb, ub) = drain.size_hint(); + let capacity = ub.unwrap_or(lb); + + let mut map = HeaderMap::with_capacity(capacity); + map.append(first_name.clone(), first_value); + + let (map, _) = + drain.fold((map, first_name), |(mut map, prev_name), (name, value)| { + let name = name.unwrap_or(prev_name); + map.append(name.clone(), value); + (map, name) + }); + + map + } + + /// Returns the number of values stored in the map. /// - /// This number could be be less than or equal to actual headers stored in - /// the map. + /// See also: [`len_keys`](Self::len_keys). + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// assert_eq!(map.len(), 0); + /// + /// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); + /// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// assert_eq!(map.len(), 2); + /// + /// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2")); + /// assert_eq!(map.len(), 3); + /// ``` pub fn len(&self) -> usize { + self.inner + .iter() + .fold(0, |acc, (_, values)| acc + values.len()) + } + + /// Returns the number of _keys_ stored in the map. + /// + /// The number of values stored will be at least this number. See also: [`Self::len`]. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// assert_eq!(map.len_keys(), 0); + /// + /// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); + /// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// assert_eq!(map.len_keys(), 2); + /// + /// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2")); + /// assert_eq!(map.len_keys(), 2); + /// ``` + pub fn len_keys(&self) -> usize { self.inner.len() } /// Returns true if the map contains no elements. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// assert!(map.is_empty()); + /// + /// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); + /// assert!(!map.is_empty()); + /// ``` pub fn is_empty(&self) -> bool { self.inner.len() == 0 } - /// Clears the map, removing all key-value pairs. Keeps the allocated memory - /// for reuse. + /// Clears the map, removing all name-value pairs. + /// + /// Keeps the allocated memory for reuse. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); + /// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// assert_eq!(map.len(), 2); + /// + /// map.clear(); + /// assert!(map.is_empty()); + /// ``` pub fn clear(&mut self) { self.inner.clear(); } - /// Returns the number of headers the map can hold without reallocating. + fn get_value(&self, key: impl AsHeaderName) -> Option<&Value> { + match key.try_as_name().ok()? { + Cow::Borrowed(name) => self.inner.get(name), + Cow::Owned(name) => self.inner.get(&name), + } + } + + /// Returns a reference to the _first_ value associated with a header name. /// - /// This number is an approximation as certain usage patterns could cause - /// additional allocations before the returned capacity is filled. + /// Returns `None` if there is no value associated with the key. + /// + /// Even when multiple values are associated with the key, the "first" one is returned but is + /// not guaranteed to be chosen with any particular order; though, the returned item will be + /// consistent for each call to `get` if the map has not changed. + /// + /// See also: [`get_all`](Self::get_all). + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// + /// let cookie = map.get(header::SET_COOKIE).unwrap(); + /// assert_eq!(cookie, "one=1"); + /// + /// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2")); + /// assert_eq!(map.get(header::SET_COOKIE).unwrap(), "one=1"); + /// + /// assert_eq!(map.get(header::SET_COOKIE), map.get("set-cookie")); + /// assert_eq!(map.get(header::SET_COOKIE), map.get("Set-Cookie")); + /// + /// assert!(map.get(header::HOST).is_none()); + /// assert!(map.get("INVALID HEADER NAME").is_none()); + /// ``` + pub fn get(&self, key: impl AsHeaderName) -> Option<&HeaderValue> { + self.get_value(key).map(|val| val.first()) + } + + /// Returns a mutable reference to the _first_ value associated a header name. + /// + /// Returns `None` if there is no value associated with the key. + /// + /// Even when multiple values are associated with the key, the "first" one is returned but is + /// not guaranteed to be chosen with any particular order; though, the returned item will be + /// consistent for each call to `get_mut` if the map has not changed. + /// + /// See also: [`get_all`](Self::get_all). + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// + /// let mut cookie = map.get_mut(header::SET_COOKIE).unwrap(); + /// assert_eq!(cookie, "one=1"); + /// + /// *cookie = HeaderValue::from_static("three=3"); + /// assert_eq!(map.get(header::SET_COOKIE).unwrap(), "three=3"); + /// + /// assert!(map.get(header::HOST).is_none()); + /// assert!(map.get("INVALID HEADER NAME").is_none()); + /// ``` + pub fn get_mut(&mut self, key: impl AsHeaderName) -> Option<&mut HeaderValue> { + match key.try_as_name().ok()? { + Cow::Borrowed(name) => self.inner.get_mut(name).map(|v| v.first_mut()), + Cow::Owned(name) => self.inner.get_mut(&name).map(|v| v.first_mut()), + } + } + + /// Returns an iterator over all values associated with a header name. + /// + /// The returned iterator does not incur any allocations and will yield no items if there are no + /// values associated with the key. Iteration order is **not** guaranteed to be the same as + /// insertion order. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// let mut none_iter = map.get_all(header::ORIGIN); + /// assert!(none_iter.next().is_none()); + /// + /// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2")); + /// + /// let mut set_cookies_iter = map.get_all(header::SET_COOKIE); + /// assert_eq!(set_cookies_iter.next().unwrap(), "one=1"); + /// assert_eq!(set_cookies_iter.next().unwrap(), "two=2"); + /// assert!(set_cookies_iter.next().is_none()); + /// ``` + pub fn get_all(&self, key: impl AsHeaderName) -> GetAll<'_> { + GetAll::new(self.get_value(key)) + } + + // TODO: get_all_mut ? + + /// Returns `true` if the map contains a value for the specified key. + /// + /// Invalid header names will simply return false. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// assert!(!map.contains_key(header::ACCEPT)); + /// + /// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); + /// assert!(map.contains_key(header::ACCEPT)); + /// ``` + pub fn contains_key(&self, key: impl AsHeaderName) -> bool { + match key.try_as_name() { + Ok(Cow::Borrowed(name)) => self.inner.contains_key(name), + Ok(Cow::Owned(name)) => self.inner.contains_key(&name), + Err(_) => false, + } + } + + /// Inserts a name-value pair into the map. + /// + /// If the map already contained this key, the new value is associated with the key and all + /// previous values are removed and returned as a `Removed` iterator. The key is not updated; + /// this matters for types that can be `==` without being identical. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); + /// assert!(map.contains_key(header::ACCEPT)); + /// assert_eq!(map.len(), 1); + /// + /// let mut removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/csv")); + /// assert_eq!(removed.next().unwrap(), "text/plain"); + /// assert!(removed.next().is_none()); + /// + /// assert_eq!(map.len(), 1); + /// ``` + pub fn insert(&mut self, key: HeaderName, val: HeaderValue) -> Removed { + let value = self.inner.insert(key, Value::one(val)); + Removed::new(value) + } + + /// Inserts a name-value pair into the map. + /// + /// If the map already contained this key, the new value is added to the list of values + /// currently associated with the key. The key is not updated; this matters for types that can + /// be `==` without being identical. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// map.append(header::HOST, HeaderValue::from_static("example.com")); + /// assert_eq!(map.len(), 1); + /// + /// map.append(header::ACCEPT, HeaderValue::from_static("text/csv")); + /// assert_eq!(map.len(), 2); + /// + /// map.append(header::ACCEPT, HeaderValue::from_static("text/html")); + /// assert_eq!(map.len(), 3); + /// ``` + pub fn append(&mut self, key: HeaderName, value: HeaderValue) { + match self.inner.entry(key) { + hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().append(value); + } + hash_map::Entry::Vacant(entry) => { + entry.insert(Value::one(value)); + } + }; + } + + /// Removes all headers for a particular header name from the map. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// map.append(header::SET_COOKIE, HeaderValue::from_static("one=2")); + /// + /// assert_eq!(map.len(), 2); + /// + /// let mut removed = map.remove(header::SET_COOKIE); + /// assert_eq!(removed.next().unwrap(), "one=1"); + /// assert_eq!(removed.next().unwrap(), "one=2"); + /// assert!(removed.next().is_none()); + /// + /// assert!(map.is_empty()); + pub fn remove(&mut self, key: impl AsHeaderName) -> Removed { + let value = match key.try_as_name() { + Ok(Cow::Borrowed(name)) => self.inner.remove(name), + Ok(Cow::Owned(name)) => self.inner.remove(&name), + Err(_) => None, + }; + + Removed::new(value) + } + + /// Returns the number of single-value headers the map can hold without needing to reallocate. + /// + /// Since this is a multi-value map, the actual capacity is much larger when considering + /// each header name can be associated with an arbitrary number of values. The effect is that + /// the size of `len` may be greater than `capacity` since it counts all the values. + /// Conversely, [`len_keys`](Self::len_keys) will never be larger than capacity. + /// + /// # Examples + /// ``` + /// # use actix_http::http::HeaderMap; + /// let map = HeaderMap::with_capacity(16); + /// + /// assert!(map.is_empty()); + /// assert!(map.capacity() >= 16); + /// ``` pub fn capacity(&self) -> usize { self.inner.capacity() } - /// Reserves capacity for at least `additional` more headers to be inserted - /// into the `HeaderMap`. + /// Reserves capacity for at least `additional` more headers to be inserted in the map. /// - /// The header map may reserve more space to avoid frequent reallocations. - /// Like with `with_capacity`, this will be a "best effort" to avoid - /// allocations until `additional` more headers are inserted. Certain usage - /// patterns could cause additional allocations before the number is - /// reached. + /// The header map may reserve more space to avoid frequent reallocations. Additional capacity + /// only considers single-value headers. + /// + /// # Panics + /// Panics if the new allocation size overflows usize. + /// + /// # Examples + /// ``` + /// # use actix_http::http::HeaderMap; + /// let mut map = HeaderMap::with_capacity(2); + /// assert!(map.capacity() >= 2); + /// + /// map.reserve(100); + /// assert!(map.capacity() >= 102); + /// + /// assert!(map.is_empty()); + /// ``` pub fn reserve(&mut self, additional: usize) { self.inner.reserve(additional) } - /// Returns a reference to the value associated with the key. + /// An iterator over all name-value pairs. /// - /// If there are multiple values associated with the key, then the first one - /// is returned. Use `get_all` to get all values associated with a given - /// key. Returns `None` if there are no values associated with the key. - pub fn get(&self, name: N) -> Option<&HeaderValue> { - self.get2(name).map(|v| v.first()) - } - - fn get2(&self, name: N) -> Option<&Value> { - match name.as_name() { - Either::Left(name) => self.inner.get(name), - Either::Right(s) => { - if let Ok(name) = HeaderName::try_from(s) { - self.inner.get(&name) - } else { - None - } - } - } - } - - /// Returns an iterator of all values associated with a key. + /// Names will be yielded for each associated value. So, if a key has 3 associated values, it + /// will be yielded 3 times. The iteration order should be considered arbitrary. /// - /// The returned view does not incur any allocations and allows iterating the values associated - /// with the key. Returns `None` if there are no values associated with the key. Iteration order - /// is not guaranteed to be the same as insertion order. - pub fn get_all(&self, name: N) -> GetAll<'_> { - GetAll { - idx: 0, - item: self.get2(name), - } - } - - /// Returns a mutable reference to the value associated with the key. + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); /// - /// If there are multiple values associated with the key, then the first one - /// is returned. Use `entry` to get all values associated with a given - /// key. Returns `None` if there are no values associated with the key. - pub fn get_mut(&mut self, name: N) -> Option<&mut HeaderValue> { - match name.as_name() { - Either::Left(name) => self.inner.get_mut(name).map(|v| v.first_mut()), - Either::Right(s) => { - if let Ok(name) = HeaderName::try_from(s) { - self.inner.get_mut(&name).map(|v| v.first_mut()) - } else { - None - } - } - } - } - - /// Returns true if the map contains a value for the specified key. - pub fn contains_key(&self, key: N) -> bool { - match key.as_name() { - Either::Left(name) => self.inner.contains_key(name), - Either::Right(s) => { - if let Ok(name) = HeaderName::try_from(s) { - self.inner.contains_key(&name) - } else { - false - } - } - } - } - - /// An iterator visiting all key-value pairs. + /// let mut iter = map.iter(); + /// assert!(iter.next().is_none()); /// - /// The iteration order is arbitrary, but consistent across platforms for - /// the same crate version. Each key will be yielded once per associated - /// value. So, if a key has 3 associated values, it will be yielded 3 times. + /// map.append(header::HOST, HeaderValue::from_static("duck.com")); + /// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2")); + /// + /// let mut iter = map.iter(); + /// assert!(iter.next().is_some()); + /// assert!(iter.next().is_some()); + /// assert!(iter.next().is_some()); + /// assert!(iter.next().is_none()); + /// + /// let pairs = map.iter().collect::>(); + /// assert!(pairs.contains(&(&header::HOST, &HeaderValue::from_static("duck.com")))); + /// assert!(pairs.contains(&(&header::SET_COOKIE, &HeaderValue::from_static("one=1")))); + /// assert!(pairs.contains(&(&header::SET_COOKIE, &HeaderValue::from_static("two=2")))); + /// ``` pub fn iter(&self) -> Iter<'_> { Iter::new(self.inner.iter()) } - /// An iterator visiting all keys. + /// An iterator over all contained header names. /// - /// The iteration order is arbitrary, but consistent across platforms for - /// the same crate version. Each key will be yielded only once even if it - /// has multiple associated values. + /// Each name will only be yielded once even if it has multiple associated values. The iteration + /// order should be considered arbitrary. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// let mut iter = map.keys(); + /// assert!(iter.next().is_none()); + /// + /// map.append(header::HOST, HeaderValue::from_static("duck.com")); + /// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2")); + /// + /// let keys = map.keys().cloned().collect::>(); + /// assert_eq!(keys.len(), 2); + /// assert!(keys.contains(&header::HOST)); + /// assert!(keys.contains(&header::SET_COOKIE)); + /// ``` pub fn keys(&self) -> Keys<'_> { Keys(self.inner.keys()) } - /// Inserts a key-value pair into the map. + /// Clears the map, returning all name-value sets as an iterator. /// - /// If the map did not previously have this key present, then `None` is - /// returned. + /// Header names will only be yielded for the first value in each set. All items that are + /// yielded without a name and after an item with a name are associated with that same name. + /// The first item will always contain a name. /// - /// If the map did have this key present, the new value is associated with - /// the key and all previous values are removed. **Note** that only a single - /// one of the previous values is returned. If there are multiple values - /// that have been previously associated with the key, then the first one is - /// returned. See `insert_mult` on `OccupiedEntry` for an API that returns - /// all values. + /// Keeps the allocated memory for reuse. + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); /// - /// The key is not updated, though; this matters for types that can be `==` - /// without being identical. - pub fn insert(&mut self, key: HeaderName, val: HeaderValue) { - let _ = self.inner.insert(key, Value::One(val)); - } - - /// Inserts a key-value pair into the map. + /// let mut iter = map.drain(); + /// assert!(iter.next().is_none()); + /// drop(iter); /// - /// If the map did not previously have this key present, then `false` is - /// returned. + /// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2")); /// - /// If the map did have this key present, the new value is pushed to the end - /// of the list of values currently associated with the key. The key is not - /// updated, though; this matters for types that can be `==` without being - /// identical. - pub fn append(&mut self, key: HeaderName, value: HeaderValue) { - match self.inner.entry(key) { - Entry::Occupied(mut entry) => entry.get_mut().append(value), - Entry::Vacant(entry) => { - entry.insert(Value::One(value)); - } - } - } - - /// Removes all headers for a particular header name from the map. - pub fn remove(&mut self, key: N) { - match key.as_name() { - Either::Left(name) => { - let _ = self.inner.remove(name); - } - Either::Right(s) => { - if let Ok(name) = HeaderName::try_from(s) { - let _ = self.inner.remove(&name); - } - } - } + /// let mut iter = map.drain(); + /// assert_eq!(iter.next().unwrap(), (Some(header::SET_COOKIE), HeaderValue::from_static("one=1"))); + /// assert_eq!(iter.next().unwrap(), (None, HeaderValue::from_static("two=2"))); + /// drop(iter); + /// + /// assert!(map.is_empty()); + /// ``` + pub fn drain(&mut self) -> Drain<'_> { + Drain::new(self.inner.drain()) } } -#[doc(hidden)] -pub trait AsName { - fn as_name(&self) -> Either<&HeaderName, &str>; -} - -impl AsName for HeaderName { - fn as_name(&self) -> Either<&HeaderName, &str> { - Either::Left(self) - } -} - -impl<'a> AsName for &'a HeaderName { - fn as_name(&self) -> Either<&HeaderName, &str> { - Either::Left(self) - } -} - -impl<'a> AsName for &'a str { - fn as_name(&self) -> Either<&HeaderName, &str> { - Either::Right(self) - } -} - -impl AsName for String { - fn as_name(&self) -> Either<&HeaderName, &str> { - Either::Right(self.as_str()) - } -} - -impl<'a> AsName for &'a String { - fn as_name(&self) -> Either<&HeaderName, &str> { - Either::Right(self.as_str()) - } -} - -/// Iterator for all values in a `HeaderMap` with the same name. -pub struct GetAll<'a> { - idx: usize, - item: Option<&'a Value>, -} - -impl<'a> Iterator for GetAll<'a> { - type Item = &'a HeaderValue; +/// Note that this implementation will clone a [HeaderName] for each value. +impl IntoIterator for HeaderMap { + type Item = (HeaderName, HeaderValue); + type IntoIter = IntoIter; #[inline] - fn next(&mut self) -> Option<&'a HeaderValue> { - if let Some(ref val) = self.item { - match val { - Value::One(ref val) => { - self.item.take(); - Some(val) - } - Value::Multi(ref vec) => { - if self.idx < vec.len() { - let item = Some(&vec[self.idx]); - self.idx += 1; - item - } else { - self.item.take(); - None - } - } - } - } else { - None - } - } -} - -pub struct Keys<'a>(hash_map::Keys<'a, HeaderName, Value>); - -impl<'a> Iterator for Keys<'a> { - type Item = &'a HeaderName; - - #[inline] - fn next(&mut self) -> Option<&'a HeaderName> { - self.0.next() + fn into_iter(self) -> Self::IntoIter { + IntoIter::new(self.inner.into_iter()) } } @@ -334,23 +565,116 @@ impl<'a> IntoIterator for &'a HeaderMap { type Item = (&'a HeaderName, &'a HeaderValue); type IntoIter = Iter<'a>; + #[inline] fn into_iter(self) -> Self::IntoIter { - self.iter() + Iter::new(self.inner.iter()) } } -pub struct Iter<'a> { +/// Iterator for all values with the same header name. +/// +/// See [`HeaderMap::get_all`]. +#[derive(Debug)] +pub struct GetAll<'a> { idx: usize, - current: Option<(&'a HeaderName, &'a SmallVec<[HeaderValue; 4]>)>, - iter: hash_map::Iter<'a, HeaderName, Value>, + value: Option<&'a Value>, +} + +impl<'a> GetAll<'a> { + fn new(value: Option<&'a Value>) -> Self { + Self { idx: 0, value } + } +} + +impl<'a> Iterator for GetAll<'a> { + type Item = &'a HeaderValue; + + fn next(&mut self) -> Option { + let val = self.value?; + + match val.get(self.idx) { + Some(val) => { + self.idx += 1; + Some(val) + } + None => { + // current index is none; remove value to fast-path future next calls + self.value = None; + None + } + } + } + + fn size_hint(&self) -> (usize, Option) { + match self.value { + Some(val) => (val.len(), Some(val.len())), + None => (0, Some(0)), + } + } +} + +/// Iterator for owned [`HeaderValue`]s with the same associated [`HeaderName`] returned from methods +/// on [`HeaderMap`] that remove or replace items. +#[derive(Debug)] +pub struct Removed { + inner: Option>, +} + +impl<'a> Removed { + fn new(value: Option) -> Self { + let inner = value.map(|value| value.inner.into_iter()); + Self { inner } + } +} + +impl Iterator for Removed { + type Item = HeaderValue; + + #[inline] + fn next(&mut self) -> Option { + self.inner.as_mut()?.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + match self.inner { + Some(ref iter) => iter.size_hint(), + None => (0, None), + } + } +} + +/// Iterator over all [`HeaderName`]s in the map. +#[derive(Debug)] +pub struct Keys<'a>(hash_map::Keys<'a, HeaderName, Value>); + +impl<'a> Iterator for Keys<'a> { + type Item = &'a HeaderName; + + #[inline] + fn next(&mut self) -> Option { + self.0.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } +} + +#[derive(Debug)] +pub struct Iter<'a> { + inner: hash_map::Iter<'a, HeaderName, Value>, + multi_inner: Option<(&'a HeaderName, &'a SmallVec<[HeaderValue; 4]>)>, + multi_idx: usize, } impl<'a> Iter<'a> { fn new(iter: hash_map::Iter<'a, HeaderName, Value>) -> Self { Self { - iter, - idx: 0, - current: None, + inner: iter, + multi_idx: 0, + multi_inner: None, } } } @@ -358,28 +682,272 @@ impl<'a> Iter<'a> { impl<'a> Iterator for Iter<'a> { type Item = (&'a HeaderName, &'a HeaderValue); - #[inline] - fn next(&mut self) -> Option<(&'a HeaderName, &'a HeaderValue)> { - if let Some(ref mut item) = self.current { - if self.idx < item.1.len() { - let item = (item.0, &item.1[self.idx]); - self.idx += 1; - return Some(item); - } else { - self.idx = 0; - self.current.take(); - } - } - if let Some(item) = self.iter.next() { - match item.1 { - Value::One(ref value) => Some((item.0, value)), - Value::Multi(ref vec) => { - self.current = Some((item.0, vec)); - self.next() + fn next(&mut self) -> Option { + // handle in-progress multi value lists first + if let Some((ref name, ref mut vals)) = self.multi_inner { + match vals.get(self.multi_idx) { + Some(val) => { + self.multi_idx += 1; + return Some((name, val)); + } + None => { + // no more items in value list; reset state + self.multi_idx = 0; + self.multi_inner = None; } } - } else { - None + } + + let (name, value) = self.inner.next()?; + + // set up new inner iter and recurse into it + self.multi_inner = Some((name, &value.inner)); + self.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + // take inner lower bound + // make no attempt at an upper bound + (self.inner.size_hint().0, None) + } +} + +/// Iterator over drained name-value pairs. +/// +/// Iterator items are `(Option, HeaderValue)` to avoid cloning. +#[derive(Debug)] +pub struct Drain<'a> { + inner: hash_map::Drain<'a, HeaderName, Value>, + multi_inner: Option<(Option, SmallVec<[HeaderValue; 4]>)>, + multi_idx: usize, +} + +impl<'a> Drain<'a> { + fn new(iter: hash_map::Drain<'a, HeaderName, Value>) -> Self { + Self { + inner: iter, + multi_inner: None, + multi_idx: 0, } } } + +impl<'a> Iterator for Drain<'a> { + type Item = (Option, HeaderValue); + + fn next(&mut self) -> Option { + // handle in-progress multi value iterators first + if let Some((ref mut name, ref mut vals)) = self.multi_inner { + if !vals.is_empty() { + // OPTIMIZE: array removals + return Some((name.take(), vals.remove(0))); + } else { + // no more items in value iterator; reset state + self.multi_inner = None; + self.multi_idx = 0; + } + } + + let (name, value) = self.inner.next()?; + + // set up new inner iter and recurse into it + self.multi_inner = Some((Some(name), value.inner)); + self.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + // take inner lower bound + // make no attempt at an upper bound + (self.inner.size_hint().0, None) + } +} + +/// Iterator over owned name-value pairs. +/// +/// Implementation necessarily clones header names for each value. +#[derive(Debug)] +pub struct IntoIter { + inner: hash_map::IntoIter, + multi_inner: Option<(HeaderName, smallvec::IntoIter<[HeaderValue; 4]>)>, +} + +impl IntoIter { + fn new(inner: hash_map::IntoIter) -> Self { + Self { + inner, + multi_inner: None, + } + } +} + +impl Iterator for IntoIter { + type Item = (HeaderName, HeaderValue); + + fn next(&mut self) -> Option { + // handle in-progress multi value iterators first + if let Some((ref name, ref mut vals)) = self.multi_inner { + match vals.next() { + Some(val) => { + return Some((name.clone(), val)); + } + None => { + // no more items in value iterator; reset state + self.multi_inner = None; + } + } + } + + let (name, value) = self.inner.next()?; + + // set up new inner iter and recurse into it + self.multi_inner = Some((name, value.inner.into_iter())); + self.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + // take inner lower bound + // make no attempt at an upper bound + (self.inner.size_hint().0, None) + } +} + +#[cfg(test)] +mod tests { + use http::header; + + use super::*; + + #[test] + fn create() { + let map = HeaderMap::new(); + assert_eq!(map.len(), 0); + assert_eq!(map.capacity(), 0); + + let map = HeaderMap::with_capacity(16); + assert_eq!(map.len(), 0); + assert!(map.capacity() >= 16); + } + + #[test] + fn insert() { + let mut map = HeaderMap::new(); + + map.insert(header::LOCATION, HeaderValue::from_static("/test")); + assert_eq!(map.len(), 1); + } + + #[test] + fn contains() { + let mut map = HeaderMap::new(); + assert!(!map.contains_key(header::LOCATION)); + + map.insert(header::LOCATION, HeaderValue::from_static("/test")); + assert!(map.contains_key(header::LOCATION)); + assert!(map.contains_key("Location")); + assert!(map.contains_key("Location".to_owned())); + assert!(map.contains_key("location")); + } + + #[test] + fn entries_iter() { + let mut map = HeaderMap::new(); + + map.append(header::HOST, HeaderValue::from_static("duck.com")); + map.append(header::COOKIE, HeaderValue::from_static("one=1")); + map.append(header::COOKIE, HeaderValue::from_static("two=2")); + + let mut iter = map.iter(); + assert!(iter.next().is_some()); + assert!(iter.next().is_some()); + assert!(iter.next().is_some()); + assert!(iter.next().is_none()); + + let pairs = map.iter().collect::>(); + assert!(pairs.contains(&(&header::HOST, &HeaderValue::from_static("duck.com")))); + assert!(pairs.contains(&(&header::COOKIE, &HeaderValue::from_static("one=1")))); + assert!(pairs.contains(&(&header::COOKIE, &HeaderValue::from_static("two=2")))); + } + + #[test] + fn drain_iter() { + let mut map = HeaderMap::new(); + + map.append(header::COOKIE, HeaderValue::from_static("one=1")); + map.append(header::COOKIE, HeaderValue::from_static("two=2")); + + let mut vals = vec![]; + let mut iter = map.drain(); + + let (name, val) = iter.next().unwrap(); + assert_eq!(name, Some(header::COOKIE)); + vals.push(val); + + let (name, val) = iter.next().unwrap(); + assert!(name.is_none()); + vals.push(val); + + assert!(vals.contains(&HeaderValue::from_static("one=1"))); + assert!(vals.contains(&HeaderValue::from_static("two=2"))); + + assert!(iter.next().is_none()); + drop(iter); + + assert!(map.is_empty()); + } + + #[test] + fn entries_into_iter() { + let mut map = HeaderMap::new(); + + map.append(header::HOST, HeaderValue::from_static("duck.com")); + map.append(header::COOKIE, HeaderValue::from_static("one=1")); + map.append(header::COOKIE, HeaderValue::from_static("two=2")); + + let mut iter = map.into_iter(); + assert!(iter.next().is_some()); + assert!(iter.next().is_some()); + assert!(iter.next().is_some()); + assert!(iter.next().is_none()); + } + + #[test] + fn iter_and_into_iter_same_order() { + let mut map = HeaderMap::new(); + + map.append(header::HOST, HeaderValue::from_static("duck.com")); + map.append(header::COOKIE, HeaderValue::from_static("one=1")); + map.append(header::COOKIE, HeaderValue::from_static("two=2")); + + let mut iter = map.iter(); + let mut into_iter = map.clone().into_iter(); + + assert_eq!(iter.next().map(owned_pair), into_iter.next()); + assert_eq!(iter.next().map(owned_pair), into_iter.next()); + assert_eq!(iter.next().map(owned_pair), into_iter.next()); + assert_eq!(iter.next().map(owned_pair), into_iter.next()); + } + + #[test] + fn get_all_and_remove_same_order() { + let mut map = HeaderMap::new(); + + map.append(header::COOKIE, HeaderValue::from_static("one=1")); + map.append(header::COOKIE, HeaderValue::from_static("two=2")); + + let mut vals = map.get_all(header::COOKIE); + let mut removed = map.clone().remove(header::COOKIE); + + assert_eq!(vals.next(), removed.next().as_ref()); + assert_eq!(vals.next(), removed.next().as_ref()); + assert_eq!(vals.next(), removed.next().as_ref()); + } + + fn owned_pair<'a>( + (name, val): (&'a HeaderName, &'a HeaderValue), + ) -> (HeaderName, HeaderValue) { + (name.clone(), val.clone()) + } +} diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index dc97bf5ff..1100a959d 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -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. use std::fmt; @@ -9,8 +9,9 @@ use percent_encoding::{AsciiSet, CONTROLS}; pub use http::header::*; use crate::error::ParseError; -use crate::httpmessage::HttpMessage; +use crate::HttpMessage; +mod as_name; mod into_pair; mod into_value; mod utils; @@ -23,6 +24,7 @@ pub use self::common::*; #[doc(hidden)] pub use self::shared::*; +pub use self::as_name::AsHeaderName; pub use self::into_pair::IntoHeaderPair; pub use self::into_value::IntoHeaderValue; #[doc(hidden)] @@ -39,16 +41,14 @@ pub trait Header: IntoHeaderValue { fn parse(msg: &T) -> Result; } -#[doc(hidden)] +#[derive(Debug, Default)] pub(crate) struct Writer { buf: BytesMut, } impl Writer { fn new() -> Writer { - Writer { - buf: BytesMut::new(), - } + Writer::default() } fn take(&mut self) -> Bytes { @@ -71,12 +71,8 @@ impl fmt::Write for Writer { /// Convert `http::HeaderMap` to our `HeaderMap`. impl From for HeaderMap { - fn from(map: http::HeaderMap) -> HeaderMap { - let mut new_map = HeaderMap::with_capacity(map.capacity()); - for (h, v) in map.iter() { - new_map.append(h.clone(), v.clone()); - } - new_map + fn from(mut map: http::HeaderMap) -> HeaderMap { + HeaderMap::from_drain(map.drain()) } } diff --git a/actix-http/src/httpcodes.rs b/actix-http/src/http_codes.rs similarity index 94% rename from actix-http/src/httpcodes.rs rename to actix-http/src/http_codes.rs index f421d8e23..688a08be5 100644 --- a/actix-http/src/httpcodes.rs +++ b/actix-http/src/http_codes.rs @@ -67,6 +67,14 @@ impl Response { static_resp!(ExpectationFailed, StatusCode::EXPECTATION_FAILED); static_resp!(UnprocessableEntity, StatusCode::UNPROCESSABLE_ENTITY); 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!(NotImplemented, StatusCode::NOT_IMPLEMENTED); diff --git a/actix-http/src/httpmessage.rs b/actix-http/src/http_message.rs similarity index 100% rename from actix-http/src/httpmessage.rs rename to actix-http/src/http_message.rs diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index cc99130eb..a55aaadbf 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -1,6 +1,6 @@ //! HTTP primitives for the Actix ecosystem. -#![deny(rust_2018_idioms)] +#![deny(rust_2018_idioms, nonstandard_style)] #![allow( clippy::type_complexity, clippy::too_many_arguments, @@ -25,8 +25,8 @@ pub mod encoding; mod extensions; mod header; mod helpers; -mod httpcodes; -pub mod httpmessage; +mod http_codes; +mod http_message; mod message; mod payload; mod request; @@ -45,7 +45,7 @@ pub use self::builder::HttpServiceBuilder; pub use self::config::{KeepAlive, ServiceConfig}; pub use self::error::{Error, ResponseError, Result}; 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::payload::{Payload, PayloadStream}; pub use self::request::Request; diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index 736f35ee1..6438ccba0 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -13,8 +13,10 @@ use crate::http::{header, Method, StatusCode, Uri, Version}; pub enum ConnectionType { /// Close connection after response Close, + /// Keep connection alive after response KeepAlive, + /// Connection is upgraded to different type Upgrade, } diff --git a/actix-http/src/request.rs b/actix-http/src/request.rs index 1031d7dce..197ec11c6 100644 --- a/actix-http/src/request.rs +++ b/actix-http/src/request.rs @@ -9,9 +9,9 @@ use http::{header, Method, Uri, Version}; use crate::extensions::Extensions; use crate::header::HeaderMap; -use crate::httpmessage::HttpMessage; use crate::message::{Message, RequestHead}; use crate::payload::{Payload, PayloadStream}; +use crate::HttpMessage; /// Request pub struct Request

{ @@ -107,7 +107,7 @@ impl

Request

{ #[inline] #[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 { &mut *self.head } @@ -158,10 +158,12 @@ impl

Request

{ self.head().method == Method::CONNECT } - /// Peer socket address + /// Peer socket address. /// - /// Peer address is actual socket address, if proxy is used in front of - /// actix http server, then peer address would be address of this proxy. + /// Peer address is the directly connected peer's socket address. If a proxy is used in front of + /// the Actix Web server, then it would be address of this proxy. + /// + /// Will only return None when called in unit tests. #[inline] pub fn peer_addr(&self) -> Option { self.head().peer_addr @@ -177,13 +179,17 @@ impl

fmt::Debug for Request

{ self.method(), self.path() )?; + if let Some(q) = self.uri().query().as_ref() { writeln!(f, " query: ?{:?}", q)?; } + writeln!(f, " headers:")?; - for (key, val) in self.headers() { + + for (key, val) in self.headers().iter() { writeln!(f, " {:?}: {:?}", key, val)?; } + Ok(()) } } diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index 110514e05..763243a63 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -32,13 +32,13 @@ pub struct Response { } impl Response { - /// Create http response builder with specific status. + /// Create HTTP response builder with specific status. #[inline] pub fn build(status: StatusCode) -> ResponseBuilder { ResponseBuilder::new(status) } - /// Create http response builder + /// Create HTTP response builder #[inline] pub fn build_from>(source: T) -> ResponseBuilder { source.into() @@ -97,7 +97,7 @@ impl Response { } #[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 { &mut *self.head } @@ -363,7 +363,9 @@ impl ResponseBuilder { { if let Some(parts) = parts(&mut self.head, &self.err) { 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()), }; } @@ -752,9 +754,11 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder { let mut msg = BoxedResponseHead::new(head.status); msg.version = head.version; 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.no_chunking(!head.chunked()); ResponseBuilder { @@ -863,6 +867,7 @@ mod tests { use super::*; use crate::body::Body; use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE, SET_COOKIE}; + use crate::HttpMessage; #[test] fn test_debug() { @@ -876,8 +881,6 @@ mod tests { #[test] fn test_response_cookies() { - use crate::httpmessage::HttpMessage; - let req = crate::test::TestRequest::default() .append_header((COOKIE, "cookie1=value1")) .append_header((COOKIE, "cookie2=value2")) @@ -893,16 +896,20 @@ mod tests { .max_age(time::Duration::days(1)) .finish(), ) - .del_cookie(&cookies[1]) + .del_cookie(&cookies[0]) .finish(); - let mut val: Vec<_> = resp + let mut val = resp .headers() .get_all(SET_COOKIE) .map(|v| v.to_str().unwrap().to_owned()) - .collect(); + .collect::>(); val.sort(); + + // the .del_cookie call assert!(val[0].starts_with("cookie1=; Max-Age=0;")); + + // the .cookie call assert_eq!( val[1], "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 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")); + let v = iter.next().unwrap(); + assert_eq!((v.name(), v.value()), ("cookie3", "val300")); } #[test] diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index f0121db97..fee26dcc3 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -432,7 +432,7 @@ where } } -/// `Service` implementation for http transport +/// `Service` implementation for HTTP transport pub struct HttpServiceHandler where S: Service, diff --git a/actix-http/src/time_parser.rs b/actix-http/src/time_parser.rs index 0d06a5867..46bf73037 100644 --- a/actix-http/src/time_parser.rs +++ b/actix-http/src/time_parser.rs @@ -8,35 +8,65 @@ pub fn parse_http_date(time: &str) -> Option { } /// 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 { 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. +/// +/// Eg: `Wednesday, 11-Jan-21 13:37:41 UTC` fn try_parse_rfc_850(time: &str) -> Option { - match PrimitiveDateTime::parse(time, "%A, %d-%b-%y %H:%M:%S") { - 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(); + let dt = PrimitiveDateTime::parse(time, "%A, %d-%b-%y %H:%M:%S").ok()?; - if expanded_year > now.year() + 50 { - expanded_year -= 100; - } + // 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. - match Date::try_from_ymd(expanded_year, dt.month(), dt.day()) { - Ok(date) => Some(PrimitiveDateTime::new(date, dt.time())), - Err(_) => None, - } - } - Err(_) => None, + 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 { + 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. +/// +/// Eg: `Wed Feb 13 15:46:11 2013` fn try_parse_asctime(time: &str) -> Option { 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))); + } +} diff --git a/actix-http/src/ws/codec.rs b/actix-http/src/ws/codec.rs index d01e8dab9..54d850854 100644 --- a/actix-http/src/ws/codec.rs +++ b/actix-http/src/ws/codec.rs @@ -54,7 +54,7 @@ pub enum Frame { Close(Option), } -/// A `WebSocket` continuation item. +/// A WebSocket continuation item. #[derive(Debug, PartialEq)] pub enum Item { FirstText(Bytes), @@ -79,7 +79,7 @@ bitflags! { } impl Codec { - /// Create new websocket frames decoder. + /// Create new WebSocket frames decoder. pub fn new() -> Codec { Codec { max_size: 65_536, diff --git a/actix-http/src/ws/frame.rs b/actix-http/src/ws/frame.rs index 8c49524e6..46edf5d85 100644 --- a/actix-http/src/ws/frame.rs +++ b/actix-http/src/ws/frame.rs @@ -7,7 +7,7 @@ use crate::ws::mask::apply_mask; use crate::ws::proto::{CloseCode, CloseReason, OpCode}; use crate::ws::ProtocolError; -/// A struct representing a `WebSocket` frame. +/// A struct representing a WebSocket frame. #[derive(Debug)] pub struct Parser; @@ -16,7 +16,8 @@ impl Parser { src: &[u8], server: bool, max_size: usize, - ) -> Result)>, ProtocolError> { + ) -> Result)>, ProtocolError> + { let chunk_len = src.len(); let mut idx = 2; @@ -77,9 +78,10 @@ impl Parser { return Ok(None); } - let mask = - u32::from_le_bytes(TryFrom::try_from(&src[idx..idx + 4]).unwrap()); + let mask = TryFrom::try_from(&src[idx..idx + 4]).unwrap(); + idx += 4; + Some(mask) } else { None @@ -187,8 +189,8 @@ impl Parser { }; if mask { - let mask = rand::random::(); - dst.put_u32_le(mask); + let mask = rand::random::<[u8; 4]>(); + dst.put_slice(mask.as_ref()); dst.put_slice(payload.as_ref()); let pos = dst.len() - payload_len; apply_mask(&mut dst[pos..], mask); diff --git a/actix-http/src/ws/mask.rs b/actix-http/src/ws/mask.rs index 79e015f79..276ca4a85 100644 --- a/actix-http/src/ws/mask.rs +++ b/actix-http/src/ws/mask.rs @@ -1,136 +1,57 @@ //! 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. -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. +/// Mask/unmask a frame. #[inline] -#[allow(clippy::cast_lossless)] -pub(crate) fn apply_mask(buf: &mut [u8], mask_u32: u32) { - // 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); +pub fn apply_mask(buf: &mut [u8], mask: [u8; 4]) { + apply_mask_fast32(buf, mask) +} - // Initial unaligned segment - let head_len = head.len(); - if head_len > 0 { - xor_short(head, mask_u64); +/// A safe unoptimized mask application. +#[inline] +fn apply_mask_fallback(buf: &mut [u8], mask: [u8; 4]) { + 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::() }; + apply_mask_fallback(&mut prefix, mask); + let head = prefix.len() & 3; + let mask_u32 = if head > 0 { 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 { - mask_u64 = mask_u64.rotate_right(8 * head_len 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), - ) + mask_u32.rotate_right(8 * head as u32) } } else { - // We didn't cross even one aligned boundary! - - // SAFETY: The outer sections are smaller than 8 bytes - unsafe { (ShortSlice::new(buf), &mut [], ShortSlice::new(&mut [])) } + mask_u32 + }; + for word in words.iter_mut() { + *word ^= mask_u32; } + apply_mask_fallback(&mut suffix, mask_u32.to_ne_bytes()); } #[cfg(test)] mod tests { - use super::apply_mask; - - /// 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]; - } - } + use super::*; + // legacy test from old apply mask test. kept for now for back compat test. + // TODO: remove it and favor the other test. #[test] - fn test_apply_mask() { + fn test_apply_mask_legacy() { let mask = [0x6d, 0xb6, 0xb2, 0x80]; - let mask_u32 = u32::from_le_bytes(mask); let unmasked = vec![ 0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, @@ -140,10 +61,10 @@ mod tests { // Check masking with proper alignment. { let mut masked = unmasked.clone(); - apply_mask_fallback(&mut masked, &mask); + apply_mask_fallback(&mut masked, mask); let mut masked_fast = unmasked.clone(); - apply_mask(&mut masked_fast, mask_u32); + apply_mask(&mut masked_fast, mask); assert_eq!(masked, masked_fast); } @@ -151,12 +72,38 @@ mod tests { // Check masking without alignment. { let mut masked = unmasked.clone(); - apply_mask_fallback(&mut masked[1..], &mask); + apply_mask_fallback(&mut masked[1..], mask); 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); } } + + #[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); + } + } + } } diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index dad2646c1..0490163d5 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -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. use std::io; @@ -76,7 +76,7 @@ pub enum HandshakeError { #[display(fmt = "Method not allowed.")] GetMethodRequired, - /// Upgrade header if not set to websocket. + /// Upgrade header if not set to WebSocket. #[display(fmt = "WebSocket upgrade is expected.")] NoWebsocketUpgrade, @@ -88,7 +88,7 @@ pub enum HandshakeError { #[display(fmt = "WebSocket version header is required.")] NoVersionHeader, - /// Unsupported websocket version. + /// Unsupported WebSocket version. #[display(fmt = "Unsupported version.")] 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 { verify_handshake(req)?; Ok(handshake_response(req)) } -/// Verify `WebSocket` handshake request. +/// Verify WebSocket handshake request. pub fn verify_handshake(req: &RequestHead) -> Result<(), HandshakeError> { // WebSocket accepts only GET if req.method != Method::GET { 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) { if let Ok(s) = hdr.to_str() { s.to_ascii_lowercase().contains("websocket") @@ -181,7 +181,7 @@ pub fn verify_handshake(req: &RequestHead) -> Result<(), HandshakeError> { Ok(()) } -/// Create websocket handshake response +/// Create WebSocket handshake response. /// /// This function returns handshake `Response`, ready to send to peer. pub fn handshake_response(req: &RequestHead) -> ResponseBuilder { diff --git a/actix-http/src/ws/proto.rs b/actix-http/src/ws/proto.rs index 93468d232..1e8bf7af3 100644 --- a/actix-http/src/ws/proto.rs +++ b/actix-http/src/ws/proto.rs @@ -74,8 +74,7 @@ impl From for OpCode { } } -/// Status code used to indicate why an endpoint is closing the `WebSocket` -/// connection. +/// Status code used to indicate why an endpoint is closing the WebSocket connection. #[derive(Debug, Eq, PartialEq, Clone, Copy)] pub enum CloseCode { /// Indicates a normal closure, meaning that the purpose for diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index 73bbe6208..188516f83 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -7,7 +7,7 @@ use std::io; use actix_http::error::{ErrorBadRequest, PayloadError}; use actix_http::http::header::{self, HeaderName, HeaderValue}; 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_test::test_server; use actix_service::{fn_service, ServiceFactoryExt}; diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index b4ef74406..910fa81f2 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -10,7 +10,7 @@ use futures_util::future::{self, err, ok, ready, FutureExt}; use futures_util::stream::{once, StreamExt}; use regex::Regex; -use actix_http::httpmessage::HttpMessage; +use actix_http::HttpMessage; use actix_http::{ body, error, http, http::header, Error, HttpService, KeepAlive, Request, Response, }; diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index e1fe9c4af..2142ebf4b 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 0.4.0-beta.2 - 2021-02-10 +* No notable changes. + + ## 0.4.0-beta.1 - 2021-01-07 * Fix multipart consuming payload before header checks. [#1513] * Update `bytes` to `1.0`. [#1813] diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index beecd2e9d..67b2698bd 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "actix-multipart" -version = "0.4.0-beta.1" +version = "0.4.0-beta.2" authors = ["Nikolay Kim "] -description = "Multipart support for actix web framework." +description = "Multipart form support for Actix Web" readme = "README.md" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" @@ -16,7 +16,7 @@ name = "actix_multipart" path = "src/lib.rs" [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" bytes = "1" @@ -29,4 +29,4 @@ twoway = "0.2" [dev-dependencies] actix-rt = "2" -actix-http = "3.0.0-beta.1" +actix-http = "3.0.0-beta.3" diff --git a/actix-multipart/README.md b/actix-multipart/README.md index edb2e0020..defcf7828 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -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/) -* [Chat on gitter](https://gitter.im/actix/actix) -* Cargo package: [actix-multipart](https://crates.io/crates/actix-multipart) -* Minimum supported Rust version: 1.40 or later +[![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.2)](https://docs.rs/actix-multipart/0.4.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) +![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg) +
+[![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 diff --git a/actix-multipart/src/lib.rs b/actix-multipart/src/lib.rs index 46dd0ee9b..38a24e28f 100644 --- a/actix-multipart/src/lib.rs +++ b/actix-multipart/src/lib.rs @@ -1,4 +1,4 @@ -//! Multipart form support for Actix web. +//! Multipart form support for Actix Web. #![deny(rust_2018_idioms)] #![allow(clippy::borrow_interior_mutable_const)] diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index 2cde5e8cf..8cd1c8e0c 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -13,9 +13,7 @@ use futures_util::stream::{LocalBoxStream, Stream, StreamExt}; use actix_utils::task::LocalWaker; use actix_web::error::{ParseError, PayloadError}; -use actix_web::http::header::{ - self, ContentDisposition, HeaderMap, HeaderName, HeaderValue, -}; +use actix_web::http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}; use crate::error::MultipartError; @@ -120,10 +118,7 @@ impl Multipart { impl Stream for Multipart { type Item = Result; - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { if let Some(err) = self.error.take() { Poll::Ready(Some(Err(err))) } else if self.safety.current() { @@ -142,9 +137,7 @@ impl Stream for Multipart { } impl InnerMultipart { - fn read_headers( - payload: &mut PayloadBuffer, - ) -> Result, MultipartError> { + fn read_headers(payload: &mut PayloadBuffer) -> Result, MultipartError> { match payload.read_until(b"\r\n\r\n")? { None => { if payload.eof { @@ -226,8 +219,7 @@ impl InnerMultipart { if chunk.len() < boundary.len() { continue; } - if &chunk[..2] == b"--" - && &chunk[2..chunk.len() - 2] == boundary.as_bytes() + if &chunk[..2] == b"--" && &chunk[2..chunk.len() - 2] == boundary.as_bytes() { break; } else { @@ -273,9 +265,7 @@ impl InnerMultipart { match field.borrow_mut().poll(safety) { Poll::Pending => return Poll::Pending, Poll::Ready(Some(Ok(_))) => continue, - Poll::Ready(Some(Err(e))) => { - return Poll::Ready(Some(Err(e))) - } + Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))), Poll::Ready(None) => true, } } @@ -311,10 +301,7 @@ impl InnerMultipart { } // read boundary InnerState::Boundary => { - match InnerMultipart::read_boundary( - &mut *payload, - &self.boundary, - )? { + match InnerMultipart::read_boundary(&mut *payload, &self.boundary)? { None => return Poll::Pending, Some(eof) => { if eof { @@ -418,8 +405,7 @@ impl Field { pub fn content_disposition(&self) -> Option { // RFC 7578: 'Each part MUST contain a Content-Disposition header field // 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() } else { None @@ -430,15 +416,10 @@ impl Field { impl Stream for Field { type Item = Result; - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { if self.safety.current() { let mut inner = self.inner.borrow_mut(); - if let Some(mut payload) = - inner.payload.as_ref().unwrap().get_mut(&self.safety) - { + if let Some(mut payload) = inner.payload.as_ref().unwrap().get_mut(&self.safety) { payload.poll_stream(cx)?; } inner.poll(&self.safety) @@ -607,8 +588,7 @@ impl InnerField { 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 { let res = if let Some(ref mut len) = self.length { InnerField::read_len(&mut *payload, len) @@ -628,7 +608,9 @@ impl InnerField { Ok(None) => Poll::Pending, Ok(Some(line)) => { 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) } @@ -804,9 +786,7 @@ impl PayloadBuffer { /// Read bytes until new line delimiter or eof pub fn readline_or_eof(&mut self) -> Result, MultipartError> { match self.readline() { - Err(MultipartError::Incomplete) if self.eof => { - Ok(Some(self.buf.split().freeze())) - } + Err(MultipartError::Incomplete) if self.eof => Ok(Some(self.buf.split().freeze())), line => line, } } @@ -902,10 +882,7 @@ mod tests { impl Stream for SlowStream { type Item = Result; - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); if !this.ready { this.ready = true; diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index f75c6805f..acd9ceada 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 4.0.0-beta.2 - 2021-02-10 +* No notable changes. + + ## 4.0.0-beta.1 - 2021-01-07 * Update `pin-project` to `1.0`. * Update `bytes` to `1.0`. [#1813] diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 522279fd0..698ff9420 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "actix-web-actors" -version = "4.0.0-beta.1" +version = "4.0.0-beta.2" authors = ["Nikolay Kim "] -description = "Actix actors support for actix web framework." +description = "Actix actors support for Actix Web" readme = "README.md" keywords = ["actix", "http", "web", "framework", "async"] homepage = "https://actix.rs" @@ -18,8 +18,8 @@ path = "src/lib.rs" [dependencies] actix = { version = "0.11.0-beta.2", default-features = false } actix-codec = "0.4.0-beta.1" -actix-http = "3.0.0-beta.1" -actix-web = { version = "4.0.0-beta.1", default-features = false } +actix-http = "3.0.0-beta.3" +actix-web = { version = "4.0.0-beta.3", default-features = false } bytes = "1" bytestring = "1" diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index fb8c3a621..c9b588153 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -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/) -* [Chat on gitter](https://gitter.im/actix/actix) -* Cargo package: [actix-web-actors](https://crates.io/crates/actix-web-actors) -* Minimum supported Rust version: 1.40 or later +[![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) +[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=0.5.0)](https://docs.rs/actix-web-actors/0.5.0) +[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) +![License](https://img.shields.io/crates/l/actix-web-actors.svg) +
+[![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 diff --git a/actix-web-actors/src/context.rs b/actix-web-actors/src/context.rs index afe17cd21..bd6635cba 100644 --- a/actix-web-actors/src/context.rs +++ b/actix-web-actors/src/context.rs @@ -3,9 +3,7 @@ use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; -use actix::dev::{ - AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, ToEnvelope, -}; +use actix::dev::{AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, ToEnvelope}; use actix::fut::ActorFuture; use actix::{ Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message, SpawnHandle, @@ -15,7 +13,7 @@ use bytes::Bytes; use futures_core::Stream; use tokio::sync::oneshot::Sender; -/// Execution context for http actors +/// Execution context for HTTP actors pub struct HttpContext where A: Actor>, @@ -165,10 +163,7 @@ where { type Item = Result; - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { if self.fut.alive() { let _ = Pin::new(&mut self.fut).poll(cx); } @@ -233,10 +228,11 @@ mod tests { #[actix_rt::test] async fn test_default_resource() { - let srv = init_service(App::new().service(web::resource("/test").to(|| { - HttpResponse::Ok().streaming(HttpContext::create(MyActor { count: 0 })) - }))) - .await; + let srv = + init_service(App::new().service(web::resource("/test").to(|| { + HttpResponse::Ok().streaming(HttpContext::create(MyActor { count: 0 })) + }))) + .await; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs index 0421f05fb..7a4823d91 100644 --- a/actix-web-actors/src/lib.rs +++ b/actix-web-actors/src/lib.rs @@ -1,4 +1,4 @@ -//! Actix actors integration for Actix web framework +//! Actix actors support for Actix Web. #![deny(rust_2018_idioms)] #![allow(clippy::borrow_interior_mutable_const)] diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index 10113665b..1ab4cfce5 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -7,13 +7,12 @@ use std::task::{Context, Poll}; use std::{collections::VecDeque, convert::TryFrom}; use actix::dev::{ - AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, - ToEnvelope, + AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, ToEnvelope, }; use actix::fut::ActorFuture; use actix::{ - Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, - Message as ActixMessage, SpawnHandle, + Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message as ActixMessage, + SpawnHandle, }; use actix_codec::{Decoder, Encoder}; use actix_http::ws::{hash_key, Codec}; @@ -32,8 +31,7 @@ use tokio::sync::oneshot::Sender; /// Perform WebSocket handshake and start actor. pub fn start(actor: A, req: &HttpRequest, stream: T) -> Result where - A: Actor> - + StreamHandler>, + A: Actor> + StreamHandler>, T: Stream> + 'static, { let mut res = handshake(req)?; @@ -50,15 +48,14 @@ where /// /// 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 -/// from the websocket request. +/// from the WebSocket request. pub fn start_with_addr( actor: A, req: &HttpRequest, stream: T, ) -> Result<(Addr, HttpResponse), Error> where - A: Actor> - + StreamHandler>, + A: Actor> + StreamHandler>, T: Stream> + 'static, { let mut res = handshake(req)?; @@ -66,7 +63,7 @@ where 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. pub fn start_with_protocols( @@ -76,15 +73,14 @@ pub fn start_with_protocols( stream: T, ) -> Result where - A: Actor> - + StreamHandler>, + A: Actor> + StreamHandler>, T: Stream> + 'static, { let mut res = handshake_with_protocols(req, protocols)?; 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. /// It does not perform any IO. @@ -92,7 +88,7 @@ pub fn handshake(req: &HttpRequest) -> Result( - stream: S, - f: F, - ) -> impl Stream> + pub fn with_factory(stream: S, f: F) -> impl Stream> where F: FnOnce(&mut Self) -> A + 'static, A: StreamHandler>, @@ -423,10 +416,7 @@ where { type Item = Result; - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); if this.fut.alive() { @@ -493,10 +483,7 @@ where { type Item = Result; - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let mut this = self.as_mut().project(); if !*this.closed { @@ -512,9 +499,10 @@ where } Poll::Pending => break, Poll::Ready(Some(Err(e))) => { - return Poll::Ready(Some(Err(ProtocolError::Io( - io::Error::new(io::ErrorKind::Other, format!("{}", e)), - )))); + return Poll::Ready(Some(Err(ProtocolError::Io(io::Error::new( + io::ErrorKind::Other, + format!("{}", e), + ))))); } } } diff --git a/actix-web-actors/tests/test_ws.rs b/actix-web-actors/tests/test_ws.rs index 7fd59a4a7..912480ae4 100644 --- a/actix-web-actors/tests/test_ws.rs +++ b/actix-web-actors/tests/test_ws.rs @@ -11,11 +11,7 @@ impl Actor for Ws { } impl StreamHandler> for Ws { - fn handle( - &mut self, - msg: Result, - ctx: &mut Self::Context, - ) { + fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { match msg.unwrap() { ws::Message::Ping(msg) => ctx.pong(&msg), ws::Message::Text(text) => ctx.text(text), @@ -30,9 +26,7 @@ impl StreamHandler> for Ws { async fn test_simple() { let mut srv = test::start(|| { App::new().service(web::resource("/").to( - |req: HttpRequest, stream: web::Payload| async move { - ws::start(Ws, &req, stream) - }, + |req: HttpRequest, stream: web::Payload| async move { ws::start(Ws, &req, stream) }, )) }); diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index a7675d9dd..2ce728aad 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -3,6 +3,10 @@ ## 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 * Added compile success and failure testing. [#1677] * Add `route` macro for supporting multiple HTTP methods guards. [#1674] diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 04bd10421..886d9ac3e 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "actix-web-codegen" -version = "0.4.0" -description = "Actix web proc macros" +version = "0.5.0-beta.1" +description = "Routing and runtime macros for Actix Web" readme = "README.md" homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web" @@ -20,7 +20,7 @@ proc-macro2 = "1" [dev-dependencies] 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 } trybuild = "1" rustversion = "1" diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index 887502075..5820bb443 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -1,22 +1,24 @@ # 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) -[![Documentation](https://docs.rs/actix-web-codegen/badge.svg)](https://docs.rs/actix-web-codegen/0.4.0/actix_web_codegen/) +[![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) +[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.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) -[![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) +![License](https://img.shields.io/crates/l/actix-web-codegen.svg) +
+[![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) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-codegen) - [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. ## 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. [`trybuild`]: https://github.com/dtolnay/trybuild diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index ede1e0005..670d82ce9 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -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 //! 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 @@ -10,7 +10,7 @@ //! # Runtime Setup //! 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 //! async fn main() { //! async { println!("Hello world"); }.await @@ -23,7 +23,7 @@ //! //! See docs for: [GET], [POST], [PATCH], [PUT], [DELETE], [HEAD], [CONNECT], [OPTIONS], [TRACE] //! -//! ```rust +//! ``` //! # use actix_web::HttpResponse; //! # use actix_web_codegen::get; //! #[get("/test")] @@ -36,7 +36,7 @@ //! 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. //! -//! ```rust +//! ``` //! # use actix_web::HttpResponse; //! # use actix_web_codegen::route; //! #[route("/test", method="GET", method="HEAD")] @@ -159,7 +159,7 @@ method_macro! { /// # Actix Web Re-export /// This macro can be applied with `#[actix_web::main]` when used in Actix Web applications. /// -/// # Usage +/// # Examples /// ```rust /// #[actix_web_codegen::main] /// async fn main() { diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 252be1b95..34cdad649 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -4,9 +4,7 @@ use std::task::{Context, Poll}; use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; use actix_web::http::header::{HeaderName, HeaderValue}; use actix_web::{http, test, web::Path, App, Error, HttpResponse, Responder}; -use actix_web_codegen::{ - connect, delete, get, head, options, patch, post, put, route, trace, -}; +use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, route, trace}; use futures_util::future::{self, LocalBoxFuture}; // Make sure that we can name function as 'config' diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 16ec7ad1a..20e8af6df 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,9 +1,13 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.2 - 2021-02-10 ### Added * `ClientRequest::insert_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 * 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 [#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 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 7a9130780..615a76e09 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.1" +version = "3.0.0-beta.2" authors = ["Nikolay Kim "] description = "Async HTTP and WebSocket client library built on the Actix ecosystem" readme = "README.md" @@ -36,10 +36,13 @@ rustls = ["tls-rustls", "actix-http/rustls"] # content-encoding support compress = ["actix-http/compress"] +# trust-dns as dns resolver +trust-dns = ["actix-http/trust-dns"] + [dependencies] actix-codec = "0.4.0-beta.1" actix-service = "2.0.0-beta.4" -actix-http = "3.0.0-beta.1" +actix-http = "3.0.0-beta.3" actix-rt = "2" base64 = "0.13" @@ -57,10 +60,16 @@ serde_urlencoded = "0.7" tls-openssl = { version = "0.10.9", package = "openssl", optional = true } 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] -actix-web = { version = "4.0.0-beta.1", features = ["openssl"] } -actix-http = { version = "3.0.0-beta.1", features = ["openssl"] } -actix-http-test = { version = "3.0.0-beta.1", features = ["openssl"] } +actix-web = { version = "4.0.0-beta.3", features = ["openssl"] } +actix-http = { version = "3.0.0-beta.3", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.2", features = ["openssl"] } actix-utils = "3.0.0-beta.1" actix-server = "2.0.0-beta.3" actix-tls = { version = "3.0.0-beta.3", features = ["openssl", "rustls"] } diff --git a/awc/README.md b/awc/README.md index 3d18a07c5..043ae6a41 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,9 +3,9 @@ > Async HTTP and WebSocket client library. [![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) -![Apache 2.0 or MIT 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) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.2)](https://docs.rs/awc/3.0.0-beta.2) +![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) +[![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) ## Documentation & Resources diff --git a/awc/src/builder.rs b/awc/src/builder.rs index 39eb24c39..94ffb8a71 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -82,8 +82,9 @@ impl ClientBuilder { self } - /// Maximum supported http major version - /// Supported versions http/1.1, http/2 + /// Maximum supported HTTP major version. + /// + /// Supported versions are HTTP/1.1 and HTTP/2. pub fn max_http_version(mut self, val: http::Version) -> Self { self.max_http_version = Some(val); self diff --git a/awc/src/connect.rs b/awc/src/connect.rs index 5450550a7..a9b8f9f83 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -82,8 +82,7 @@ where let connection = fut.await?; // send request - let (head, framed) = - connection.open_tunnel(RequestHeadType::from(head)).await?; + let (head, framed) = connection.open_tunnel(RequestHeadType::from(head)).await?; let framed = framed.into_map_io(|io| BoxedSocket(Box::new(Socket(io)))); Ok((head, framed)) @@ -142,10 +141,7 @@ impl AsyncWrite for BoxedSocket { Pin::new(self.get_mut().0.as_write()).poll_flush(cx) } - fn poll_shutdown( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(self.get_mut().0.as_write()).poll_shutdown(cx) } } diff --git a/awc/src/error.rs b/awc/src/error.rs index d008166d9..c60339f76 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -1,7 +1,5 @@ //! Http client errors -pub use actix_http::client::{ - ConnectError, FreezeRequestError, InvalidUrl, SendRequestError, -}; +pub use actix_http::client::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; pub use actix_http::error::PayloadError; pub use actix_http::http::Error as HttpError; pub use actix_http::ws::HandshakeError as WsHandshakeError; diff --git a/awc/src/frozen.rs b/awc/src/frozen.rs index 878f404c6..46b4063a0 100644 --- a/awc/src/frozen.rs +++ b/awc/src/frozen.rs @@ -145,7 +145,9 @@ impl FrozenSendBuilder { { match HeaderName::try_from(key) { 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()), diff --git a/awc/src/lib.rs b/awc/src/lib.rs index a92125b5a..bd52f7ab1 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -134,7 +134,7 @@ use self::connect::{Connect, ConnectorWrapper}; /// /// let res = client.get("http://www.rust-lang.org") // <- Create request builder /// .insert_header(("User-Agent", "Actix-web")) -/// .send() // <- Send http request +/// .send() // <- Send HTTP request /// .await; // <- send request and wait for response /// /// println!("Response: {:?}", res); diff --git a/awc/src/request.rs b/awc/src/request.rs index b9a333b18..db8196c5b 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -11,8 +11,7 @@ use actix_http::body::Body; use actix_http::cookie::{Cookie, CookieJar}; use actix_http::http::header::{self, IntoHeaderPair}; use actix_http::http::{ - uri, ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, - Version, + uri, ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, Version, }; use actix_http::{Error, RequestHead}; @@ -42,10 +41,10 @@ cfg_if::cfg_if! { /// let response = awc::Client::new() /// .get("http://www.rust-lang.org") // <- Create request builder /// .insert_header(("User-Agent", "Actix-web")) -/// .send() // <- Send http request +/// .send() // <- Send HTTP request /// .await; /// -/// response.and_then(|response| { // <- server http response +/// response.and_then(|response| { // <- server HTTP response /// println!("Response: {:?}", response); /// Ok(()) /// }); @@ -159,7 +158,9 @@ impl ClientRequest { H: IntoHeaderPair, { 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()), }; @@ -217,7 +218,7 @@ impl ClientRequest { } /// 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] pub fn force_close(mut self) -> Self { self.head.set_connection_type(ConnectionType::Close); @@ -232,7 +233,9 @@ impl ClientRequest { >::Error: Into, { 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()), } self @@ -516,15 +519,11 @@ impl ClientRequest { .unwrap_or(true); if https { - slf = - slf.insert_header_if_none((header::ACCEPT_ENCODING, HTTPS_ENCODING)) + slf = slf.insert_header_if_none((header::ACCEPT_ENCODING, HTTPS_ENCODING)) } else { #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] { - slf = slf.insert_header_if_none(( - header::ACCEPT_ENCODING, - "gzip, deflate", - )) + slf = slf.insert_header_if_none((header::ACCEPT_ENCODING, "gzip, deflate")) } }; } diff --git a/awc/src/response.rs b/awc/src/response.rs index 0a6bbcd52..bbb47ceba 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -85,8 +85,7 @@ impl HttpMessage for ClientResponse { if self.extensions().get::().is_none() { let mut cookies = Vec::new(); for hdr in self.headers().get_all(&SET_COOKIE) { - let s = std::str::from_utf8(hdr.as_bytes()) - .map_err(CookieParseError::from)?; + let s = std::str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; cookies.push(Cookie::parse_encoded(s)?.into_owned()); } self.extensions_mut().insert(Cookies(cookies)); @@ -185,7 +184,7 @@ impl ClientResponse where S: Stream>, { - /// Loads http response's body. + /// Loads HTTP response's body. pub fn body(&mut self) -> MessageBody { MessageBody::new(self) } @@ -230,7 +229,7 @@ impl fmt::Debug for ClientResponse { } } -/// Future that resolves to a complete http message body. +/// Future that resolves to a complete HTTP message body. pub struct MessageBody { length: Option, err: Option, @@ -393,9 +392,7 @@ where if let Some(len) = self.length.take() { if len > self.fut.as_ref().unwrap().limit { - return Poll::Ready(Err(JsonPayloadError::Payload( - PayloadError::Overflow, - ))); + return Poll::Ready(Err(JsonPayloadError::Payload(PayloadError::Overflow))); } } @@ -465,8 +462,7 @@ mod tests { _ => unreachable!("error"), } - let mut req = - TestResponse::with_header(header::CONTENT_LENGTH, "1000000").finish(); + let mut req = TestResponse::with_header(header::CONTENT_LENGTH, "1000000").finish(); match req.body().await.err().unwrap() { PayloadError::Overflow => {} _ => unreachable!("error"), diff --git a/awc/src/sender.rs b/awc/src/sender.rs index f8d74292d..039f245f6 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -78,8 +78,7 @@ impl SendClientRequest { #[cfg(feature = "compress")] impl Future for SendClientRequest { - type Output = - Result>>, SendRequestError>; + type Output = Result>>, SendRequestError>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); @@ -95,15 +94,9 @@ impl Future for SendClientRequest { let res = ready!(send.as_mut().poll(cx)).map(|res| { res._timeout(delay.take()).map_body(|head, payload| { if *response_decompress { - Payload::Stream(Decoder::from_headers( - payload, - &head.headers, - )) + Payload::Stream(Decoder::from_headers(payload, &head.headers)) } else { - Payload::Stream(Decoder::new( - payload, - ContentEncoding::Identity, - )) + Payload::Stream(Decoder::new(payload, ContentEncoding::Identity)) } }) }); @@ -187,11 +180,11 @@ impl RequestSender { B: Into, { let fut = match self { - RequestSender::Owned(head) => config.connector.send_request( - RequestHeadType::Owned(head), - body.into(), - addr, - ), + RequestSender::Owned(head) => { + config + .connector + .send_request(RequestHeadType::Owned(head), body.into(), addr) + } RequestSender::Rc(head, extra_headers) => config.connector.send_request( RequestHeadType::Rc(head, extra_headers), body.into(), @@ -215,8 +208,7 @@ impl RequestSender { 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(); } @@ -243,10 +235,9 @@ impl RequestSender { }; // set content-type - if let Err(e) = self.set_header_if_none( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ) { + if let Err(e) = + self.set_header_if_none(header::CONTENT_TYPE, "application/x-www-form-urlencoded") + { return e.into(); } @@ -290,11 +281,7 @@ impl RequestSender { self.send_body(addr, response_decompress, timeout, config, Body::Empty) } - fn set_header_if_none( - &mut self, - key: HeaderName, - value: V, - ) -> Result<(), HttpError> + fn set_header_if_none(&mut self, key: HeaderName, value: V) -> Result<(), HttpError> where V: IntoHeaderValue, { @@ -302,7 +289,9 @@ impl RequestSender { RequestSender::Owned(head) => { if !head.headers.contains_key(&key) { 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()), } } diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 7c795226b..8db002836 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -40,16 +40,12 @@ pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message}; use crate::connect::BoxedSocket; use crate::error::{InvalidUrl, SendRequestError, WsClientError}; -use crate::http::header::{ - self, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION, -}; -use crate::http::{ - ConnectionType, Error as HttpError, Method, StatusCode, Uri, Version, -}; +use crate::http::header::{self, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION}; +use crate::http::{ConnectionType, Error as HttpError, Method, StatusCode, Uri, Version}; use crate::response::ClientResponse; use crate::ClientConfig; -/// `WebSocket` connection +/// WebSocket connection. pub struct WebsocketsRequest { pub(crate) head: RequestHead, err: Option, @@ -63,7 +59,7 @@ pub struct WebsocketsRequest { } impl WebsocketsRequest { - /// Create new websocket connection + /// Create new WebSocket connection pub(crate) fn new(uri: U, config: Rc) -> Self where Uri: TryFrom, @@ -106,7 +102,7 @@ impl WebsocketsRequest { self } - /// Set supported websocket protocols + /// Set supported WebSocket protocols pub fn protocols(mut self, protos: U) -> Self where U: IntoIterator, @@ -243,7 +239,7 @@ impl WebsocketsRequest { 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( mut self, ) -> Result<(ClientResponse, Framed), WsClientError> { @@ -342,7 +338,7 @@ impl WebsocketsRequest { 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) { if let Ok(s) = hdr.to_str() { s.to_ascii_lowercase().contains("websocket") diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index 5c8d07f4d..e094fa377 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -51,8 +51,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ #[actix_rt::test] async fn test_simple() { let srv = test::start(|| { - App::new() - .service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR)))) + App::new().service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR)))) }); 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 { - Ok::<_, actix_http::Error>(Bytes::from_static(STR.as_bytes())) - }); + let body = + stream::once(async { Ok::<_, actix_http::Error>(Bytes::from_static(STR.as_bytes())) }); let req = srv.post("/").send_stream(Box::pin(body)); let mut res = req.await.unwrap(); diff --git a/awc/tests/test_rustls_client.rs b/awc/tests/test_rustls_client.rs index b0f3b71be..a928715a8 100644 --- a/awc/tests/test_rustls_client.rs +++ b/awc/tests/test_rustls_client.rs @@ -64,8 +64,7 @@ async fn test_connection_reuse_h2() { .and_then( HttpService::build() .h2(map_config( - App::new() - .service(web::resource("/").route(web::to(HttpResponse::Ok))), + App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))), |_| AppConfig::default(), )) .rustls(tls_config()) diff --git a/awc/tests/test_ssl_client.rs b/awc/tests/test_ssl_client.rs index 064824d59..b93c729e5 100644 --- a/awc/tests/test_ssl_client.rs +++ b/awc/tests/test_ssl_client.rs @@ -48,8 +48,7 @@ async fn test_connection_reuse_h2() { .and_then( HttpService::build() .h2(map_config( - App::new() - .service(web::resource("/").route(web::to(HttpResponse::Ok))), + App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))), |_| AppConfig::default(), )) .openssl(ssl_acceptor()) diff --git a/awc/tests/test_ws.rs b/awc/tests/test_ws.rs index 8eb912dac..1b3f780dc 100644 --- a/awc/tests/test_ws.rs +++ b/awc/tests/test_ws.rs @@ -31,7 +31,7 @@ async fn test_simple() { .send(h1::Message::Item((res.drop_body(), BodySize::None))) .await?; - // start websocket service + // start WebSocket service let framed = framed.replace_codec(ws::Codec::new()); ws::Dispatcher::with(framed, ws_service).await } diff --git a/benches/server.rs b/benches/server.rs index ce79f077d..9dd540a73 100644 --- a/benches/server.rs +++ b/benches/server.rs @@ -33,9 +33,8 @@ fn bench_async_burst(c: &mut Criterion) { let srv = rt.block_on(async { test::start(|| { - App::new().service( - web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))), - ) + App::new() + .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| { b.iter_custom(|iters| { - let client = - rt.block_on(async { Client::new().get(url.clone()).freeze().unwrap() }); + rt.block_on(async { + 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()); - join_all(burst).await - }); - let elapsed = start.elapsed(); + let resps = join_all(burst).await; - // 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); - }; + let elapsed = start.elapsed(); - 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 + }) }) }); } diff --git a/docs/graphs/web-focus.dot b/docs/graphs/web-focus.dot index 55a82bb41..ec0f7a946 100644 --- a/docs/graphs/web-focus.dot +++ b/docs/graphs/web-focus.dot @@ -1,6 +1,7 @@ digraph { subgraph cluster_web { label="actix/actix-web" + "awc" "actix-web" "actix-files" @@ -16,7 +17,7 @@ digraph { "actix-web-actors" -> { "actix" "actix-web" "actix-http" "actix-codec" } "actix-multipart" -> { "actix-web" "actix-service" "actix-utils" } "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-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-server" -> { "actix-service" "actix-rt" "actix-codec" "actix-utils" } "actix-rt" -> { "macros" "threadpool" } + + // actix + + "actix" -> { "actix-rt" } } diff --git a/docs/graphs/web-only.dot b/docs/graphs/web-only.dot index 9e1bb2805..6f8292a3a 100644 --- a/docs/graphs/web-only.dot +++ b/docs/graphs/web-only.dot @@ -15,7 +15,6 @@ digraph { "awc" -> { "actix-http" } "actix-web-actors" -> { "actix" "actix-web" "actix-http" } "actix-multipart" -> { "actix-web" } - "actix-http" -> { "actix" }[color=blue] // optional "actix-files" -> { "actix-web" } "actix-http-test" -> { "awc" } } diff --git a/examples/basic.rs b/examples/basic.rs index e8ad5fcdb..99eef3ee1 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -29,12 +29,8 @@ async fn main() -> std::io::Result<()> { .service(no_params) .service( web::resource("/resource2/index.html") - .wrap( - middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), - ) - .default_service( - web::route().to(|| HttpResponse::MethodNotAllowed()), - ) + .wrap(middleware::DefaultHeaders::new().header("X-Version-R2", "0.3")) + .default_service(web::route().to(|| HttpResponse::MethodNotAllowed())) .route(web::get().to(index_async)), ) .service(web::resource("/test1.html").to(|| async { "Test\r\n" })) diff --git a/examples/uds.rs b/examples/uds.rs index c0a5d76a6..096781984 100644 --- a/examples/uds.rs +++ b/examples/uds.rs @@ -33,12 +33,8 @@ async fn main() -> std::io::Result<()> { .service(no_params) .service( web::resource("/resource2/index.html") - .wrap( - middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), - ) - .default_service( - web::route().to(|| HttpResponse::MethodNotAllowed()), - ) + .wrap(middleware::DefaultHeaders::new().header("X-Version-R2", "0.3")) + .default_service(web::route().to(|| HttpResponse::MethodNotAllowed())) .route(web::get().to(index_async)), ) .service(web::resource("/test1.html").to(|| async { "Test\r\n" })) diff --git a/rustfmt.toml b/rustfmt.toml index 94bd11d51..973e002c0 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,2 +1,2 @@ -max_width = 89 +max_width = 96 reorder_imports = true diff --git a/src/app.rs b/src/app.rs index 1660c4b94..7a26a3a89 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,8 +8,7 @@ use actix_http::body::{Body, MessageBody}; use actix_http::{Extensions, Request}; use actix_service::boxed::{self, BoxServiceFactory}; use actix_service::{ - apply, apply_fn_factory, IntoServiceFactory, ServiceFactory, ServiceFactoryExt, - Transform, + apply, apply_fn_factory, IntoServiceFactory, ServiceFactory, ServiceFactoryExt, Transform, }; use futures_util::future::FutureExt; @@ -72,7 +71,7 @@ where /// Set application data. Application data could be accessed /// by using `Data` 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 /// instance for each thread, thus application data must be constructed /// 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. /// - /// Actix web provides several services implementations: + /// Actix Web provides several services implementations: /// /// * *Resource* is an entry in resource table which corresponds to requested URL. /// * *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::middleware::DefaultHeaders; use crate::service::ServiceRequest; - use crate::test::{ - call_service, init_service, read_body, try_init_service, TestRequest, - }; + use crate::test::{call_service, init_service, read_body, try_init_service, TestRequest}; use crate::{web, HttpRequest, HttpResponse}; #[actix_rt::test] async fn test_default_resource() { - let srv = init_service( - App::new().service(web::resource("/test").to(HttpResponse::Ok)), - ) - .await; + let srv = + init_service(App::new().service(web::resource("/test").to(HttpResponse::Ok))).await; let req = TestRequest::with_uri("/test").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); @@ -525,20 +520,22 @@ mod tests { #[actix_rt::test] async fn test_data_factory() { - let srv = - init_service(App::new().data_factory(|| ok::<_, ()>(10usize)).service( - web::resource("/").to(|_: web::Data| HttpResponse::Ok()), - )) - .await; + let srv = init_service( + App::new() + .data_factory(|| ok::<_, ()>(10usize)) + .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), + ) + .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let srv = - init_service(App::new().data_factory(|| ok::<_, ()>(10u32)).service( - web::resource("/").to(|_: web::Data| HttpResponse::Ok()), - )) - .await; + let srv = init_service( + App::new() + .data_factory(|| ok::<_, ()>(10u32)) + .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), + ) + .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); @@ -546,23 +543,24 @@ mod tests { #[actix_rt::test] async fn test_data_factory_errors() { - let srv = - try_init_service(App::new().data_factory(|| err::(())).service( - web::resource("/").to(|_: web::Data| HttpResponse::Ok()), - )) - .await; + let srv = try_init_service( + App::new() + .data_factory(|| err::(())) + .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), + ) + .await; assert!(srv.is_err()); } #[actix_rt::test] async fn test_extension() { - let srv = init_service(App::new().app_data(10usize).service( - web::resource("/").to(|req: HttpRequest| { + let srv = init_service(App::new().app_data(10usize).service(web::resource("/").to( + |req: HttpRequest| { assert_eq!(*req.app_data::().unwrap(), 10); HttpResponse::Ok() - }), - )) + }, + ))) .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); @@ -617,10 +615,8 @@ mod tests { let fut = srv.call(req); async move { let mut res = fut.await?; - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static("0001"), - ); + res.headers_mut() + .insert(header::CONTENT_TYPE, HeaderValue::from_static("0001")); Ok(res) } }) @@ -645,10 +641,8 @@ mod tests { let fut = srv.call(req); async { let mut res = fut.await?; - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static("0001"), - ); + res.headers_mut() + .insert(header::CONTENT_TYPE, HeaderValue::from_static("0001")); Ok(res) } }), @@ -671,9 +665,8 @@ mod tests { .route( "/test", web::get().to(|req: HttpRequest| { - HttpResponse::Ok().body( - req.url_for("youtube", &["12345"]).unwrap().to_string(), - ) + HttpResponse::Ok() + .body(req.url_for("youtube", &["12345"]).unwrap().to_string()) }), ), ) diff --git a/src/app_service.rs b/src/app_service.rs index a38f1d652..9b4ae3354 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -241,16 +241,15 @@ impl ServiceFactory for AppRoutingFactory { fn new_service(&self, _: ()) -> Self::Future { // construct all services factory future with it's resource def and guards. - let factory_fut = - join_all(self.services.iter().map(|(path, factory, guards)| { - let path = path.clone(); - let guards = guards.borrow_mut().take(); - let factory_fut = factory.new_service(()); - async move { - let service = factory_fut.await?; - Ok((path, guards, service)) - } - })); + let factory_fut = join_all(self.services.iter().map(|(path, factory, guards)| { + let path = path.clone(); + let guards = guards.borrow_mut().take(); + let factory_fut = factory.new_service(()); + async move { + let service = factory_fut.await?; + Ok((path, guards, service)) + } + })); // construct default service factory future let default_fut = self.default.new_service(()); diff --git a/src/config.rs b/src/config.rs index 24afca295..bd9a25c6f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,8 +17,7 @@ use crate::service::{ }; type Guards = Vec>; -type HttpNewService = - boxed::BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; +type HttpNewService = boxed::BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; /// Application configuration pub struct AppService { @@ -99,12 +98,8 @@ impl AppService { InitError = (), > + 'static, { - self.services.push(( - rdef, - boxed::factory(factory.into_factory()), - guards, - nested, - )); + self.services + .push((rdef, boxed::factory(factory.into_factory()), guards, nested)); } } @@ -263,12 +258,12 @@ mod tests { cfg.app_data(15u8); }; - let srv = init_service(App::new().configure(cfg).service( - web::resource("/").to(|_: web::Data, req: HttpRequest| { + let srv = init_service(App::new().configure(cfg).service(web::resource("/").to( + |_: web::Data, req: HttpRequest| { assert_eq!(*req.app_data::().unwrap(), 15u8); HttpResponse::Ok() - }), - )) + }, + ))) .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); @@ -312,17 +307,13 @@ mod tests { let srv = init_service( App::new() .configure(|cfg| { - cfg.external_resource( - "youtube", - "https://youtube.com/watch/{video_id}", - ); + cfg.external_resource("youtube", "https://youtube.com/watch/{video_id}"); }) .route( "/test", web::get().to(|req: HttpRequest| { - HttpResponse::Ok().body( - req.url_for("youtube", &["12345"]).unwrap().to_string(), - ) + HttpResponse::Ok() + .body(req.url_for("youtube", &["12345"]).unwrap().to_string()) }), ), ) @@ -337,10 +328,8 @@ mod tests { #[actix_rt::test] async fn test_service() { let srv = init_service(App::new().configure(|cfg| { - cfg.service( - web::resource("/test").route(web::get().to(HttpResponse::Created)), - ) - .route("/index.html", web::get().to(HttpResponse::Ok)); + cfg.service(web::resource("/test").route(web::get().to(HttpResponse::Created))) + .route("/index.html", web::get().to(HttpResponse::Ok)); })) .await; diff --git a/src/data.rs b/src/data.rs index 12a1f5cf8..133248212 100644 --- a/src/data.rs +++ b/src/data.rs @@ -27,7 +27,7 @@ pub(crate) type FnDataFactory = /// /// Application data can be accessed by using `Data` extractor where `T` is data type. /// -/// **Note**: http server accepts an application factory rather than an application instance. HTTP +/// **Note**: HTTP server accepts an application factory rather than an application instance. HTTP /// server constructs an application instance for each thread, thus application data must be /// constructed multiple times. If you want to share data between different threads, a shareable /// object should be used, e.g. `Send + Sync`. Application data does not need to be `Send` @@ -156,11 +156,12 @@ mod tests { let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let srv = - init_service(App::new().data(10u32).service( - web::resource("/").to(|_: web::Data| HttpResponse::Ok()), - )) - .await; + let srv = init_service( + App::new() + .data(10u32) + .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), + ) + .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); @@ -186,21 +187,23 @@ mod tests { #[actix_rt::test] async fn test_app_data_extractor() { - let srv = - init_service(App::new().app_data(Data::new(10usize)).service( - web::resource("/").to(|_: web::Data| HttpResponse::Ok()), - )) - .await; + let srv = init_service( + App::new() + .app_data(Data::new(10usize)) + .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), + ) + .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let srv = - init_service(App::new().app_data(Data::new(10u32)).service( - web::resource("/").to(|_: web::Data| HttpResponse::Ok()), - )) - .await; + let srv = init_service( + App::new() + .app_data(Data::new(10u32)) + .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), + ) + .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); @@ -237,15 +240,16 @@ mod tests { #[actix_rt::test] async fn test_override_data() { - let srv = init_service(App::new().data(1usize).service( - web::resource("/").data(10usize).route(web::get().to( - |data: web::Data| { - assert_eq!(**data, 10); - HttpResponse::Ok() - }, - )), - )) - .await; + let srv = + init_service(App::new().data(1usize).service( + web::resource("/").data(10usize).route(web::get().to( + |data: web::Data| { + assert_eq!(**data, 10); + HttpResponse::Ok() + }, + )), + )) + .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); diff --git a/src/error.rs b/src/error.rs index c0d6f8af9..1d7c781d8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -92,9 +92,7 @@ impl std::error::Error for JsonPayloadError {} impl ResponseError for JsonPayloadError { fn error_response(&self) -> HttpResponse { match *self { - JsonPayloadError::Overflow => { - HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) - } + JsonPayloadError::Overflow => HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE), _ => HttpResponse::new(StatusCode::BAD_REQUEST), } } diff --git a/src/guard.rs b/src/guard.rs index ba0cbea85..5d0de58c2 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -176,7 +176,7 @@ impl Guard for NotGuard { } } -/// Http method guard +/// HTTP method guard. #[doc(hidden)] pub struct MethodGuard(http::Method); @@ -186,52 +186,52 @@ impl Guard for MethodGuard { } } -/// Guard to match *GET* http method +/// Guard to match *GET* HTTP method. pub fn Get() -> MethodGuard { MethodGuard(http::Method::GET) } -/// Predicate to match *POST* http method +/// Predicate to match *POST* HTTP method. pub fn Post() -> MethodGuard { MethodGuard(http::Method::POST) } -/// Predicate to match *PUT* http method +/// Predicate to match *PUT* HTTP method. pub fn Put() -> MethodGuard { MethodGuard(http::Method::PUT) } -/// Predicate to match *DELETE* http method +/// Predicate to match *DELETE* HTTP method. pub fn Delete() -> MethodGuard { MethodGuard(http::Method::DELETE) } -/// Predicate to match *HEAD* http method +/// Predicate to match *HEAD* HTTP method. pub fn Head() -> MethodGuard { MethodGuard(http::Method::HEAD) } -/// Predicate to match *OPTIONS* http method +/// Predicate to match *OPTIONS* HTTP method. pub fn Options() -> MethodGuard { MethodGuard(http::Method::OPTIONS) } -/// Predicate to match *CONNECT* http method +/// Predicate to match *CONNECT* HTTP method. pub fn Connect() -> MethodGuard { MethodGuard(http::Method::CONNECT) } -/// Predicate to match *PATCH* http method +/// Predicate to match *PATCH* HTTP method. pub fn Patch() -> MethodGuard { MethodGuard(http::Method::PATCH) } -/// Predicate to match *TRACE* http method +/// Predicate to match *TRACE* HTTP method. pub fn Trace() -> MethodGuard { MethodGuard(http::Method::TRACE) } -/// Predicate to match specified http method +/// Predicate to match specified HTTP method. pub fn Method(method: http::Method) -> MethodGuard { MethodGuard(method) } diff --git a/src/lib.rs b/src/lib.rs index 46fbc2937..39cfaf197 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,7 +30,7 @@ //! //! To get started navigating the API docs, you may consider looking at the following pages first: //! -//! * [App]: This struct represents an Actix web application and is used to +//! * [App]: This struct represents an Actix Web application and is used to //! configure routes and other common application settings. //! //! * [HttpServer]: This struct represents an HTTP server instance and is @@ -56,7 +56,6 @@ //! * SSL support using OpenSSL or Rustls //! * 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) -//! * Supports [Actix actor framework](https://github.com/actix/actix) //! * Runs on stable Rust 1.46+ //! //! ## Crate Features @@ -131,9 +130,7 @@ pub mod dev { pub use crate::handler::Handler; pub use crate::info::ConnectionInfo; pub use crate::rmap::ResourceMap; - pub use crate::service::{ - HttpServiceFactory, ServiceRequest, ServiceResponse, WebService, - }; + pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService}; pub use crate::types::form::UrlEncoded; pub use crate::types::json::JsonBody; @@ -143,9 +140,7 @@ pub mod dev { #[cfg(feature = "compress")] pub use actix_http::encoding::Decoder as Decompress; pub use actix_http::ResponseBuilder as HttpResponseBuilder; - pub use actix_http::{ - Extensions, Payload, PayloadStream, RequestHead, ResponseHead, - }; + pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, ResponseHead}; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_server::Server; pub use actix_service::{Service, Transform}; @@ -203,29 +198,3 @@ pub mod dev { } } } - -pub mod client { - //! Actix web async HTTP client. - //! - //! ```rust - //! use actix_web::client::Client; - //! - //! #[actix_web::main] - //! async fn main() { - //! let mut client = Client::default(); - //! - //! // Create request builder and send request - //! let response = client.get("http://www.rust-lang.org") - //! .insert_header(("User-Agent", "actix-web/3.0")) - //! .send() // <- Send request - //! .await; // <- Wait for response - //! - //! println!("Response: {:?}", response); - //! } - //! ``` - - pub use awc::error::*; - pub use awc::{ - test, Client, ClientBuilder, ClientRequest, ClientResponse, Connector, - }; -} diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs index 2df535280..97742e92c 100644 --- a/src/middleware/compat.rs +++ b/src/middleware/compat.rs @@ -15,7 +15,7 @@ use crate::{error::Error, service::ServiceResponse}; /// Middleware for enabling any middleware to be used in [`Resource::wrap`](crate::Resource::wrap), /// [`Scope::wrap`](crate::Scope::wrap) and [`Condition`](super::Condition). /// -/// # Usage +/// # Examples /// ```rust /// use actix_web::middleware::{Logger, Compat}; /// use actix_web::{App, web}; @@ -143,9 +143,7 @@ mod tests { web::scope("app") .wrap(Compat::new(logger)) .wrap(Compat::new(compress)) - .service( - web::resource("/test").route(web::get().to(HttpResponse::Ok)), - ), + .service(web::resource("/test").route(web::get().to(HttpResponse::Ok))), ), ) .await; diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 7a45e4c8d..698ba768e 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -30,7 +30,7 @@ use crate::{ /// Use `BodyEncoding` trait for overriding response compression. To disable compression set /// encoding to `ContentEncoding::Identity`. /// -/// # Usage +/// # Examples /// ```rust /// use actix_web::{web, middleware, App, HttpResponse}; /// diff --git a/src/middleware/condition.rs b/src/middleware/condition.rs index 9263292b9..f0c344062 100644 --- a/src/middleware/condition.rs +++ b/src/middleware/condition.rs @@ -11,7 +11,7 @@ use futures_util::future::{Either, FutureExt, LocalBoxFuture}; /// control such middlewares like `Logger` or `Compress` directly. See the [`Compat`](super::Compat) /// middleware for a workaround. /// -/// # Usage +/// # Examples /// ```rust /// use actix_web::middleware::{Condition, NormalizePath}; /// use actix_web::App; @@ -119,15 +119,13 @@ mod tests { ok(req.into_response(HttpResponse::InternalServerError().finish())) }; - let mw = - ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); + let mw = ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); let mw = Condition::new(true, mw) .new_transform(srv.into_service()) .await .unwrap(); - let resp = - test::call_service(&mw, TestRequest::default().to_srv_request()).await; + let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); } @@ -137,16 +135,14 @@ mod tests { ok(req.into_response(HttpResponse::InternalServerError().finish())) }; - let mw = - ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); + let mw = ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); let mw = Condition::new(false, mw) .new_transform(srv.into_service()) .await .unwrap(); - let resp = - test::call_service(&mw, TestRequest::default().to_srv_request()).await; + let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE), None); } } diff --git a/src/middleware/default_headers.rs b/src/middleware/default_headers.rs index a9a50ec2c..a36cc2f29 100644 --- a/src/middleware/default_headers.rs +++ b/src/middleware/default_headers.rs @@ -28,7 +28,7 @@ use crate::{ /// /// Headers with the same key that are already set in a response will *not* be overwritten. /// -/// # Usage +/// # Examples /// ```rust /// use actix_web::{web, http, middleware, App, HttpResponse}; /// @@ -229,8 +229,7 @@ mod tests { #[actix_rt::test] async fn test_content_type() { - let srv = - |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish())); + let srv = |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish())); let mw = DefaultHeaders::new() .add_content_type() .new_transform(srv.into_service()) diff --git a/src/middleware/err_handlers.rs b/src/middleware/err_handlers.rs index b55959460..4673ed4ce 100644 --- a/src/middleware/err_handlers.rs +++ b/src/middleware/err_handlers.rs @@ -33,7 +33,7 @@ type ErrorHandler = dyn Fn(ServiceResponse) -> Result Self { let inner = Rc::get_mut(&mut self.0).unwrap(); - let ft = inner.format.0.iter_mut().find(|ft| { - matches!(ft, FormatText::CustomRequest(unit_label, _) if label == unit_label) - }); + let ft = inner.format.0.iter_mut().find( + |ft| matches!(ft, FormatText::CustomRequest(unit_label, _) if label == unit_label), + ); if let Some(FormatText::CustomRequest(_, request_fn)) = ft { // replace into None or previously registered fn using same label @@ -363,8 +363,7 @@ impl Format { /// Returns `None` if the format string syntax is incorrect. pub fn new(s: &str) -> Format { log::trace!("Access log format: {}", s); - let fmt = - Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|xi)|[atPrUsbTD]?)").unwrap(); + let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|xi)|[atPrUsbTD]?)").unwrap(); let mut idx = 0; let mut results = Vec::new(); @@ -385,12 +384,12 @@ impl Format { unreachable!() } } - "i" => FormatText::RequestHeader( - HeaderName::try_from(key.as_str()).unwrap(), - ), - "o" => FormatText::ResponseHeader( - HeaderName::try_from(key.as_str()).unwrap(), - ), + "i" => { + FormatText::RequestHeader(HeaderName::try_from(key.as_str()).unwrap()) + } + "o" => { + FormatText::ResponseHeader(HeaderName::try_from(key.as_str()).unwrap()) + } "e" => FormatText::EnvironHeader(key.as_str().to_owned()), "xi" => FormatText::CustomRequest(key.as_str().to_owned(), None), _ => unreachable!(), @@ -533,9 +532,7 @@ impl FormatText { }; } FormatText::UrlPath => *self = FormatText::Str(req.path().to_string()), - FormatText::RequestTime => { - *self = FormatText::Str(now.format("%Y-%m-%dT%H:%M:%S")) - } + FormatText::RequestTime => *self = FormatText::Str(now.format("%Y-%m-%dT%H:%M:%S")), FormatText::RequestHeader(ref name) => { let s = if let Some(val) = req.headers().get(name) { if let Ok(s) = val.to_str() { @@ -557,8 +554,7 @@ impl FormatText { *self = s; } FormatText::RealIPRemoteAddr => { - let s = if let Some(remote) = req.connection_info().realip_remote_addr() - { + let s = if let Some(remote) = req.connection_info().realip_remote_addr() { FormatText::Str(remote.to_string()) } else { FormatText::Str("-".to_string()) @@ -629,8 +625,8 @@ mod tests { .finish(), )) }; - let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test") - .exclude_regex("\\w"); + let logger = + Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test").exclude_regex("\\w"); let srv = logger.new_transform(srv.into_service()).await.unwrap(); @@ -743,9 +739,7 @@ mod tests { let req = TestRequest::default() .insert_header(( header::FORWARDED, - header::HeaderValue::from_static( - "for=192.0.2.60;proto=http;by=203.0.113.43", - ), + header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"), )) .to_srv_request(); diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index 8519f041a..ea21a7215 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -53,7 +53,7 @@ impl Default for TrailingSlash { /// trailing slashes or else they will be inaccessible (or vice versa when using the /// `TrailingSlash::Always` behavior), as shown in the example tests below. /// -/// # Usage +/// # Examples /// ```rust /// use actix_web::{web, middleware, App}; /// diff --git a/src/request.rs b/src/request.rs index a563518e0..d62fca300 100644 --- a/src/request.rs +++ b/src/request.rs @@ -62,7 +62,7 @@ impl HttpRequest { } /// This method returns mutable reference to the request head. - /// panics if multiple references of http request exists. + /// panics if multiple references of HTTP request exists. #[inline] pub(crate) fn head_mut(&mut self) -> &mut RequestHead { &mut Rc::get_mut(&mut self.inner).unwrap().head @@ -175,11 +175,7 @@ impl HttpRequest { /// ); /// } /// ``` - pub fn url_for( - &self, - name: &str, - elements: U, - ) -> Result + pub fn url_for(&self, name: &str, elements: U) -> Result where U: IntoIterator, I: AsRef, @@ -202,12 +198,14 @@ impl HttpRequest { &self.app_state().rmap() } - /// Peer socket address + /// Peer socket address. /// - /// Peer address is actual socket address, if proxy is used in front of - /// actix http server, then peer address would be address of this proxy. + /// Peer address is the directly connected peer's socket address. If a proxy is used in front of + /// the Actix Web server, then it would be address of this proxy. /// /// To get client connection information `.connection_info()` should be used. + /// + /// Will only return None when called in unit tests. #[inline] pub fn peer_addr(&self) -> Option { self.head().peer_addr @@ -445,10 +443,10 @@ mod tests { { let cookies = req.cookies().unwrap(); assert_eq!(cookies.len(), 2); - assert_eq!(cookies[0].name(), "cookie2"); - assert_eq!(cookies[0].value(), "value2"); - assert_eq!(cookies[1].name(), "cookie1"); - assert_eq!(cookies[1].value(), "value1"); + assert_eq!(cookies[0].name(), "cookie1"); + assert_eq!(cookies[0].value(), "value1"); + assert_eq!(cookies[1].name(), "cookie2"); + assert_eq!(cookies[1].value(), "value2"); } let cookie = req.cookie("cookie1"); @@ -575,30 +573,30 @@ mod tests { #[actix_rt::test] async fn test_data() { - let srv = init_service(App::new().app_data(10usize).service( - web::resource("/").to(|req: HttpRequest| { + let srv = init_service(App::new().app_data(10usize).service(web::resource("/").to( + |req: HttpRequest| { if req.app_data::().is_some() { HttpResponse::Ok() } else { HttpResponse::BadRequest() } - }), - )) + }, + ))) .await; let req = TestRequest::default().to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); - let srv = init_service(App::new().app_data(10u32).service( - web::resource("/").to(|req: HttpRequest| { + let srv = init_service(App::new().app_data(10u32).service(web::resource("/").to( + |req: HttpRequest| { if req.app_data::().is_some() { HttpResponse::Ok() } else { HttpResponse::BadRequest() } - }), - )) + }, + ))) .await; let req = TestRequest::default().to_request(); @@ -685,14 +683,14 @@ mod tests { let tracker = Rc::new(RefCell::new(Tracker { dropped: false })); { let tracker2 = Rc::clone(&tracker); - let srv = init_service(App::new().data(10u32).service( - web::resource("/").to(move |req: HttpRequest| { + let srv = init_service(App::new().data(10u32).service(web::resource("/").to( + move |req: HttpRequest| { req.extensions_mut().insert(Foo { tracker: Rc::clone(&tracker2), }); HttpResponse::Ok() - }), - )) + }, + ))) .await; let req = TestRequest::default().to_request(); diff --git a/src/resource.rs b/src/resource.rs index 188e6fa43..944beeefa 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -358,9 +358,10 @@ where U::InitError: fmt::Debug, { // create and configure default resource - self.default = boxed::factory(f.into_factory().map_init_err(|e| { - log::error!("Can not construct default service: {:?}", e) - })); + self.default = boxed::factory( + f.into_factory() + .map_init_err(|e| log::error!("Can not construct default service: {:?}", e)), + ); self } @@ -437,8 +438,7 @@ impl ServiceFactory for ResourceFactory { let default_fut = self.default.new_service(()); // construct route service factory futures - let factory_fut = - join_all(self.routes.iter().map(|route| route.new_service(()))); + let factory_fut = join_all(self.routes.iter().map(|route| route.new_service(()))); let app_data = self.app_data.clone(); @@ -530,19 +530,18 @@ mod tests { #[actix_rt::test] async fn test_middleware() { - let srv = - init_service( - App::new().service( - web::resource("/test") - .name("test") - .wrap(DefaultHeaders::new().header( - header::CONTENT_TYPE, - HeaderValue::from_static("0001"), - )) - .route(web::get().to(HttpResponse::Ok)), - ), - ) - .await; + let srv = init_service( + App::new().service( + web::resource("/test") + .name("test") + .wrap( + DefaultHeaders::new() + .header(header::CONTENT_TYPE, HeaderValue::from_static("0001")), + ) + .route(web::get().to(HttpResponse::Ok)), + ), + ) + .await; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); @@ -584,12 +583,11 @@ mod tests { #[actix_rt::test] async fn test_to() { - let srv = - init_service(App::new().service(web::resource("/test").to(|| async { - sleep(Duration::from_millis(100)).await; - Ok::<_, Error>(HttpResponse::Ok()) - }))) - .await; + let srv = init_service(App::new().service(web::resource("/test").to(|| async { + sleep(Duration::from_millis(100)).await; + Ok::<_, Error>(HttpResponse::Ok()) + }))) + .await; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); diff --git a/src/responder.rs b/src/responder.rs index dcad45e0f..92945cdaa 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -262,9 +262,7 @@ pub(crate) mod tests { async fn test_option_responder() { let srv = init_service( App::new() - .service( - web::resource("/none").to(|| async { Option::<&'static str>::None }), - ) + .service(web::resource("/none").to(|| async { Option::<&'static str>::None })) .service(web::resource("/some").to(|| async { Some("some") })), ) .await; @@ -364,8 +362,7 @@ pub(crate) mod tests { ); // InternalError - let resp = - error::InternalError::new("err", StatusCode::BAD_REQUEST).respond_to(&req); + let resp = error::InternalError::new("err", StatusCode::BAD_REQUEST).respond_to(&req); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } @@ -382,9 +379,8 @@ pub(crate) mod tests { HeaderValue::from_static("text/plain; charset=utf-8") ); - let res = - Err::(error::InternalError::new("err", StatusCode::BAD_REQUEST)) - .respond_to(&req); + let res = Err::(error::InternalError::new("err", StatusCode::BAD_REQUEST)) + .respond_to(&req); assert_eq!(res.status(), StatusCode::BAD_REQUEST); } diff --git a/src/route.rs b/src/route.rs index c5e297411..b6b2482cd 100644 --- a/src/route.rs +++ b/src/route.rs @@ -238,12 +238,7 @@ where impl RouteNewService where - T: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - >, + T: ServiceFactory, T::Future: 'static, T::Service: 'static, >::Future: 'static, @@ -255,12 +250,7 @@ where impl ServiceFactory for RouteNewService where - T: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - >, + T: ServiceFactory, T::Future: 'static, T::Service: 'static, >::Future: 'static, diff --git a/src/scope.rs b/src/scope.rs index d17acd843..dd02501b0 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -8,8 +8,8 @@ use actix_http::Extensions; use actix_router::{ResourceDef, Router}; use actix_service::boxed::{self, BoxService, BoxServiceFactory}; use actix_service::{ - apply, apply_fn_factory, IntoServiceFactory, Service, ServiceFactory, - ServiceFactoryExt, Transform, + apply, apply_fn_factory, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt, + Transform, }; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; @@ -206,11 +206,11 @@ where self } - /// Register http service. + /// Register HTTP service. /// /// This is similar to `App's` service registration. /// - /// Actix web provides several services implementations: + /// Actix Web provides several services implementations: /// /// * *Resource* is an entry in resource table which corresponds to requested URL. /// * *Scope* is a set of resources with common root path. @@ -476,16 +476,15 @@ impl ServiceFactory for ScopeFactory { let default_fut = self.default.new_service(()); // construct all services factory future with it's resource def and guards. - let factory_fut = - join_all(self.services.iter().map(|(path, factory, guards)| { - let path = path.clone(); - let guards = guards.borrow_mut().take(); - let factory_fut = factory.new_service(()); - async move { - let service = factory_fut.await?; - Ok((path, guards, service)) - } - })); + let factory_fut = join_all(self.services.iter().map(|(path, factory, guards)| { + let path = path.clone(); + let guards = guards.borrow_mut().take(); + let factory_fut = factory.new_service(()); + async move { + let service = factory_fut.await?; + Ok((path, guards, service)) + } + })); let app_data = self.app_data.clone(); @@ -589,10 +588,11 @@ mod tests { #[actix_rt::test] async fn test_scope() { - let srv = init_service(App::new().service( - web::scope("/app").service(web::resource("/path1").to(HttpResponse::Ok)), - )) - .await; + let srv = + init_service(App::new().service( + web::scope("/app").service(web::resource("/path1").to(HttpResponse::Ok)), + )) + .await; let req = TestRequest::with_uri("/app/path1").to_request(); let resp = srv.call(req).await.unwrap(); @@ -621,9 +621,10 @@ mod tests { #[actix_rt::test] async fn test_scope_root2() { - let srv = init_service(App::new().service( - web::scope("/app/").service(web::resource("").to(HttpResponse::Ok)), - )) + let srv = init_service( + App::new() + .service(web::scope("/app/").service(web::resource("").to(HttpResponse::Ok))), + ) .await; let req = TestRequest::with_uri("/app").to_request(); @@ -637,9 +638,10 @@ mod tests { #[actix_rt::test] async fn test_scope_root3() { - let srv = init_service(App::new().service( - web::scope("/app/").service(web::resource("/").to(HttpResponse::Ok)), - )) + let srv = init_service( + App::new() + .service(web::scope("/app/").service(web::resource("/").to(HttpResponse::Ok))), + ) .await; let req = TestRequest::with_uri("/app").to_request(); @@ -737,8 +739,7 @@ mod tests { async fn test_scope_variable_segment() { let srv = init_service(App::new().service(web::scope("/ab-{project}").service( web::resource("/path1").to(|r: HttpRequest| { - HttpResponse::Ok() - .body(format!("project: {}", &r.match_info()["project"])) + HttpResponse::Ok().body(format!("project: {}", &r.match_info()["project"])) }), ))) .await; @@ -945,14 +946,10 @@ mod tests { App::new().service( web::scope("app") .wrap( - DefaultHeaders::new().header( - header::CONTENT_TYPE, - HeaderValue::from_static("0001"), - ), + DefaultHeaders::new() + .header(header::CONTENT_TYPE, HeaderValue::from_static("0001")), ) - .service( - web::resource("/test").route(web::get().to(HttpResponse::Ok)), - ), + .service(web::resource("/test").route(web::get().to(HttpResponse::Ok))), ), ) .await; @@ -975,10 +972,8 @@ mod tests { let fut = srv.call(req); async move { let mut res = fut.await?; - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static("0001"), - ); + res.headers_mut() + .insert(header::CONTENT_TYPE, HeaderValue::from_static("0001")); Ok(res) } }) @@ -1083,9 +1078,8 @@ mod tests { s.route( "/", web::get().to(|req: HttpRequest| { - HttpResponse::Ok().body( - req.url_for("youtube", &["xxxxxx"]).unwrap().to_string(), - ) + HttpResponse::Ok() + .body(req.url_for("youtube", &["xxxxxx"]).unwrap().to_string()) }), ); })); diff --git a/src/server.rs b/src/server.rs index 59d589439..d69d6570d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -40,7 +40,7 @@ struct Config { /// An HTTP Server. /// -/// Create new http server with application factory. +/// Create new HTTP server with application factory. /// /// ```rust,no_run /// use actix_web::{web, App, HttpResponse, HttpServer}; @@ -86,7 +86,7 @@ where S::Service: 'static, B: MessageBody + 'static, { - /// Create new http server with application factory + /// Create new HTTP server with application factory pub fn new(factory: F) -> Self { HttpServer { factory, @@ -131,8 +131,7 @@ where /// Set number of workers to start. /// - /// By default http server uses number of available logical cpu as threads - /// count. + /// By default, server uses number of available logical CPU as thread count. pub fn workers(mut self, num: usize) -> Self { self.builder = self.builder.workers(num); self @@ -257,7 +256,7 @@ where /// Get addresses of bound sockets and the scheme for it. /// /// This is useful when the server is bound from different sources - /// with some sockets listening on http and some listening on https + /// with some sockets listening on HTTP and some listening on HTTPS /// and the user should be presented with an enumeration of which /// socket requires which protocol. pub fn addrs_with_scheme(&self) -> Vec<(net::SocketAddr, &str)> { @@ -278,32 +277,28 @@ where }); let on_connect_fn = self.on_connect_fn.clone(); - self.builder = self.builder.listen( - format!("actix-web-service-{}", addr), - lst, - move || { - let c = cfg.lock().unwrap(); - let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); + self.builder = + self.builder + .listen(format!("actix-web-service-{}", addr), lst, move || { + let c = cfg.lock().unwrap(); + let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); - let svc = HttpService::build() - .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout) - .local_addr(addr); + let svc = HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout) + .local_addr(addr); - let svc = if let Some(handler) = on_connect_fn.clone() { - svc.on_connect_ext(move |io: &_, ext: _| { - (handler)(io as &dyn Any, ext) - }) - } else { - svc - }; + let svc = if let Some(handler) = on_connect_fn.clone() { + svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) + } else { + svc + }; - svc.finish(map_config(factory(), move |_| { - AppConfig::new(false, addr, host.clone()) - })) - .tcp() - }, - )?; + svc.finish(map_config(factory(), move |_| { + AppConfig::new(false, addr, host.clone()) + })) + .tcp() + })?; Ok(self) } @@ -335,32 +330,30 @@ where let on_connect_fn = self.on_connect_fn.clone(); - self.builder = self.builder.listen( - format!("actix-web-service-{}", addr), - lst, - move || { - let c = cfg.lock().unwrap(); - let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); + self.builder = + self.builder + .listen(format!("actix-web-service-{}", addr), lst, move || { + let c = cfg.lock().unwrap(); + let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); - let svc = HttpService::build() - .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout) - .client_disconnect(c.client_shutdown); + let svc = HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout) + .client_disconnect(c.client_shutdown); - let svc = if let Some(handler) = on_connect_fn.clone() { - svc.on_connect_ext(move |io: &_, ext: _| { - (&*handler)(io as &dyn Any, ext) - }) - } else { - svc - }; + let svc = if let Some(handler) = on_connect_fn.clone() { + svc.on_connect_ext(move |io: &_, ext: _| { + (&*handler)(io as &dyn Any, ext) + }) + } else { + svc + }; - svc.finish(map_config(factory(), move |_| { - AppConfig::new(true, addr, host.clone()) - })) - .openssl(acceptor.clone()) - }, - )?; + svc.finish(map_config(factory(), move |_| { + AppConfig::new(true, addr, host.clone()) + })) + .openssl(acceptor.clone()) + })?; Ok(self) } @@ -392,32 +385,28 @@ where let on_connect_fn = self.on_connect_fn.clone(); - self.builder = self.builder.listen( - format!("actix-web-service-{}", addr), - lst, - move || { - let c = cfg.lock().unwrap(); - let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); + self.builder = + self.builder + .listen(format!("actix-web-service-{}", addr), lst, move || { + let c = cfg.lock().unwrap(); + let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); - let svc = HttpService::build() - .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout) - .client_disconnect(c.client_shutdown); + let svc = HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout) + .client_disconnect(c.client_shutdown); - let svc = if let Some(handler) = on_connect_fn.clone() { - svc.on_connect_ext(move |io: &_, ext: _| { - (handler)(io as &dyn Any, ext) - }) - } else { - svc - }; + let svc = if let Some(handler) = on_connect_fn.clone() { + svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) + } else { + svc + }; - svc.finish(map_config(factory(), move |_| { - AppConfig::new(true, addr, host.clone()) - })) - .rustls(config.clone()) - }, - )?; + svc.finish(map_config(factory(), move |_| { + AppConfig::new(true, addr, host.clone()) + })) + .rustls(config.clone()) + })?; Ok(self) } @@ -434,10 +423,7 @@ where Ok(self) } - fn bind2( - &self, - addr: A, - ) -> io::Result> { + fn bind2(&self, addr: A) -> io::Result> { let mut err = None; let mut success = false; let mut sockets = Vec::new(); @@ -470,11 +456,7 @@ where /// Start listening for incoming tls connections. /// /// This method sets alpn protocols to "h2" and "http/1.1" - pub fn bind_openssl
( - mut self, - addr: A, - builder: SslAcceptorBuilder, - ) -> io::Result + pub fn bind_openssl(mut self, addr: A, builder: SslAcceptorBuilder) -> io::Result where A: net::ToSocketAddrs, { @@ -506,18 +488,13 @@ where #[cfg(unix)] /// Start listening for unix domain (UDS) connections on existing listener. - pub fn listen_uds( - mut self, - lst: std::os::unix::net::UnixListener, - ) -> io::Result { + pub fn listen_uds(mut self, lst: std::os::unix::net::UnixListener) -> io::Result { use actix_rt::net::UnixStream; let cfg = self.config.clone(); let factory = self.factory.clone(); - let socket_addr = net::SocketAddr::new( - net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), - 8080, - ); + let socket_addr = + net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080); self.sockets.push(Socket { scheme: "http", addr: socket_addr, @@ -534,23 +511,19 @@ where c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)), ); - pipeline_factory(|io: UnixStream| ok((io, Protocol::Http1, None))).and_then( - { - let svc = HttpService::build() - .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout); + pipeline_factory(|io: UnixStream| ok((io, Protocol::Http1, None))).and_then({ + let svc = HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout); - let svc = if let Some(handler) = on_connect_fn.clone() { - svc.on_connect_ext(move |io: &_, ext: _| { - (&*handler)(io as &dyn Any, ext) - }) - } else { - svc - }; + let svc = if let Some(handler) = on_connect_fn.clone() { + svc.on_connect_ext(move |io: &_, ext: _| (&*handler)(io as &dyn Any, ext)) + } else { + svc + }; - svc.finish(map_config(factory(), move |_| config.clone())) - }, - ) + svc.finish(map_config(factory(), move |_| config.clone())) + }) })?; Ok(self) } @@ -565,10 +538,8 @@ where let cfg = self.config.clone(); let factory = self.factory.clone(); - let socket_addr = net::SocketAddr::new( - net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), - 8080, - ); + let socket_addr = + net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080); self.sockets.push(Socket { scheme: "http", addr: socket_addr, @@ -584,13 +555,12 @@ where socket_addr, c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)), ); - pipeline_factory(|io: UnixStream| ok((io, Protocol::Http1, None))) - .and_then( - HttpService::build() - .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout) - .finish(map_config(factory(), move |_| config.clone())), - ) + pipeline_factory(|io: UnixStream| ok((io, Protocol::Http1, None))).and_then( + HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout) + .finish(map_config(factory(), move |_| config.clone())), + ) }, )?; Ok(self) @@ -610,7 +580,7 @@ where { /// Start listening for incoming connections. /// - /// This method starts number of http workers in separate threads. + /// This method starts number of HTTP workers in separate threads. /// For each address this method starts separate thread which does /// `accept()` in a loop. /// @@ -634,10 +604,7 @@ where } } -fn create_tcp_listener( - addr: net::SocketAddr, - backlog: u32, -) -> io::Result { +fn create_tcp_listener(addr: net::SocketAddr, backlog: u32) -> io::Result { use socket2::{Domain, Protocol, Socket, Type}; let domain = match addr { net::SocketAddr::V4(_) => Domain::ipv4(), diff --git a/src/service.rs b/src/service.rs index db0ec602a..fcbe61a02 100644 --- a/src/service.rs +++ b/src/service.rs @@ -5,8 +5,7 @@ use std::{fmt, net}; use actix_http::body::{Body, MessageBody, ResponseBody}; use actix_http::http::{HeaderMap, Method, StatusCode, Uri, Version}; use actix_http::{ - Error, Extensions, HttpMessage, Payload, PayloadStream, RequestHead, Response, - ResponseHead, + Error, Extensions, HttpMessage, Payload, PayloadStream, RequestHead, Response, ResponseHead, }; use actix_router::{IntoPattern, Path, Resource, ResourceDef, Url}; use actix_service::{IntoServiceFactory, ServiceFactory}; @@ -164,12 +163,14 @@ impl ServiceRequest { } } - /// Peer socket address + /// Peer socket address. /// - /// Peer address is actual socket address, if proxy is used in front of - /// actix http server, then peer address would be address of this proxy. + /// Peer address is the directly connected peer's socket address. If a proxy is used in front of + /// the Actix Web server, then it would be address of this proxy. /// /// To get client connection information `ConnectionInfo` should be used. + /// + /// Will only return None when called in unit tests. #[inline] pub fn peer_addr(&self) -> Option { self.head().peer_addr @@ -633,20 +634,18 @@ mod tests { #[actix_rt::test] async fn test_service_data() { - let srv = init_service( - App::new() - .data(42u32) - .service(web::service("/test").name("test").finish( - |req: ServiceRequest| { - assert_eq!( - req.app_data::>().unwrap().as_ref(), - &42 - ); - ok(req.into_response(HttpResponse::Ok().finish())) - }, - )), - ) - .await; + let srv = + init_service( + App::new() + .data(42u32) + .service(web::service("/test").name("test").finish( + |req: ServiceRequest| { + assert_eq!(req.app_data::>().unwrap().as_ref(), &42); + ok(req.into_response(HttpResponse::Ok().finish())) + }, + )), + ) + .await; let req = TestRequest::with_uri("/test").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); diff --git a/src/test.rs b/src/test.rs index 62c329c91..d576838de 100644 --- a/src/test.rs +++ b/src/test.rs @@ -12,9 +12,7 @@ use actix_http::test::TestRequest as HttpTestRequest; use actix_http::{cookie::Cookie, ws, Extensions, HttpService, Request}; use actix_router::{Path, ResourceDef, Url}; use actix_rt::{time::sleep, System}; -use actix_service::{ - map_config, IntoService, IntoServiceFactory, Service, ServiceFactory, -}; +use actix_service::{map_config, IntoService, IntoServiceFactory, Service, ServiceFactory}; use awc::error::PayloadError; use awc::{Client, ClientRequest, ClientResponse, Connector}; use bytes::{Bytes, BytesMut}; @@ -78,12 +76,7 @@ pub async fn init_service( ) -> impl Service, Error = E> where R: IntoServiceFactory, - S: ServiceFactory< - Request, - Config = AppConfig, - Response = ServiceResponse, - Error = E, - >, + S: ServiceFactory, Error = E>, S::InitError: std::fmt::Debug, { try_init_service(app) @@ -97,12 +90,7 @@ pub(crate) async fn try_init_service( ) -> Result, Error = E>, S::InitError> where R: IntoServiceFactory, - S: ServiceFactory< - Request, - Config = AppConfig, - Response = ServiceResponse, - Error = E, - >, + S: ServiceFactory, Error = E>, S::InitError: std::fmt::Debug, { let srv = app.into_factory(); @@ -264,9 +252,8 @@ where { let body = read_body(res).await; - serde_json::from_slice(&body).unwrap_or_else(|e| { - panic!("read_response_json failed during deserialization: {}", e) - }) + serde_json::from_slice(&body) + .unwrap_or_else(|e| panic!("read_response_json failed during deserialization: {}", e)) } pub async fn load_stream(mut stream: S) -> Result @@ -487,8 +474,7 @@ impl TestRequest { /// Serialize `data` to JSON and set it as the request payload. The `Content-Type` header is /// set to `application/json`. pub fn set_json(mut self, data: &T) -> Self { - let bytes = - serde_json::to_string(data).expect("Failed to serialize test data to json"); + let bytes = serde_json::to_string(data).expect("Failed to serialize test data to json"); self.req.set_payload(bytes); self.req.insert_header(ContentType::json()); self @@ -528,8 +514,7 @@ impl TestRequest { head.peer_addr = self.peer_addr; self.path.get_mut().update(&head.uri); - let app_state = - AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); + let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); ServiceRequest::new( HttpRequest::new(self.path, head, app_state, Rc::new(self.app_data)), @@ -548,8 +533,7 @@ impl TestRequest { head.peer_addr = self.peer_addr; self.path.get_mut().update(&head.uri); - let app_state = - AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); + let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); HttpRequest::new(self.path, head, app_state, Rc::new(self.app_data)) } @@ -560,8 +544,7 @@ impl TestRequest { head.peer_addr = self.peer_addr; self.path.get_mut().update(&head.uri); - let app_state = - AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); + let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); let req = HttpRequest::new(self.path, head, app_state, Rc::new(self.app_data)); @@ -678,24 +661,21 @@ where let srv = match cfg.stream { StreamType::Tcp => match cfg.tp { HttpVer::Http1 => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(false, local_addr, format!("{}", local_addr)); + let cfg = AppConfig::new(false, local_addr, format!("{}", local_addr)); HttpService::build() .client_timeout(ctimeout) .h1(map_config(factory(), move |_| cfg.clone())) .tcp() }), HttpVer::Http2 => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(false, local_addr, format!("{}", local_addr)); + let cfg = AppConfig::new(false, local_addr, format!("{}", local_addr)); HttpService::build() .client_timeout(ctimeout) .h2(map_config(factory(), move |_| cfg.clone())) .tcp() }), HttpVer::Both => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(false, local_addr, format!("{}", local_addr)); + let cfg = AppConfig::new(false, local_addr, format!("{}", local_addr)); HttpService::build() .client_timeout(ctimeout) .finish(map_config(factory(), move |_| cfg.clone())) @@ -705,24 +685,21 @@ where #[cfg(feature = "openssl")] StreamType::Openssl(acceptor) => match cfg.tp { HttpVer::Http1 => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(true, local_addr, format!("{}", local_addr)); + let cfg = AppConfig::new(true, local_addr, format!("{}", local_addr)); HttpService::build() .client_timeout(ctimeout) .h1(map_config(factory(), move |_| cfg.clone())) .openssl(acceptor.clone()) }), HttpVer::Http2 => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(true, local_addr, format!("{}", local_addr)); + let cfg = AppConfig::new(true, local_addr, format!("{}", local_addr)); HttpService::build() .client_timeout(ctimeout) .h2(map_config(factory(), move |_| cfg.clone())) .openssl(acceptor.clone()) }), HttpVer::Both => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(true, local_addr, format!("{}", local_addr)); + let cfg = AppConfig::new(true, local_addr, format!("{}", local_addr)); HttpService::build() .client_timeout(ctimeout) .finish(map_config(factory(), move |_| cfg.clone())) @@ -732,24 +709,21 @@ where #[cfg(feature = "rustls")] StreamType::Rustls(config) => match cfg.tp { HttpVer::Http1 => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(true, local_addr, format!("{}", local_addr)); + let cfg = AppConfig::new(true, local_addr, format!("{}", local_addr)); HttpService::build() .client_timeout(ctimeout) .h1(map_config(factory(), move |_| cfg.clone())) .rustls(config.clone()) }), HttpVer::Http2 => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(true, local_addr, format!("{}", local_addr)); + let cfg = AppConfig::new(true, local_addr, format!("{}", local_addr)); HttpService::build() .client_timeout(ctimeout) .h2(map_config(factory(), move |_| cfg.clone())) .rustls(config.clone()) }), HttpVer::Both => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(true, local_addr, format!("{}", local_addr)); + let cfg = AppConfig::new(true, local_addr, format!("{}", local_addr)); HttpService::build() .client_timeout(ctimeout) .finish(map_config(factory(), move |_| cfg.clone())) @@ -851,13 +825,13 @@ impl TestServerConfig { } } - /// Start http/1.1 server only + /// Start HTTP/1.1 server only pub fn h1(mut self) -> Self { self.tp = HttpVer::Http1; self } - /// Start http/2 server only + /// Start HTTP/2 server only pub fn h2(mut self) -> Self { self.tp = HttpVer::Http2; self @@ -887,8 +861,7 @@ impl TestServerConfig { /// Get first available unused address pub fn unused_addr() -> net::SocketAddr { let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); - let socket = - Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap(); + let socket = Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap(); socket.bind(&addr.into()).unwrap(); socket.set_reuse_address(true).unwrap(); let tcp = socket.into_tcp_listener(); @@ -956,7 +929,7 @@ impl TestServer { self.client.options(self.url(path.as_ref()).as_str()) } - /// Connect to test http server + /// Connect to test HTTP server pub fn request>(&self, method: Method, path: S) -> ClientRequest { self.client.request(method, path.as_ref()) } @@ -971,26 +944,24 @@ impl TestServer { 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( &mut self, path: &str, - ) -> Result, awc::error::WsClientError> - { + ) -> Result, awc::error::WsClientError> { let url = self.url(path); let connect = self.client.ws(url).connect(); connect.await.map(|(_, framed)| framed) } - /// Connect to a websocket server + /// Connect to a WebSocket server. pub async fn ws( &mut self, - ) -> Result, awc::error::WsClientError> - { + ) -> Result, awc::error::WsClientError> { self.ws_at("/").await } - /// Gracefully stop http server + /// Gracefully stop HTTP server pub async fn stop(self) { self.server.stop(true).await; self.system.stop(); @@ -1006,7 +977,7 @@ impl Drop for TestServer { #[cfg(test)] mod tests { - use actix_http::httpmessage::HttpMessage; + use actix_http::HttpMessage; use serde::{Deserialize, Serialize}; use std::time::SystemTime; @@ -1218,10 +1189,9 @@ mod tests { } } - let app = init_service( - App::new().service(web::resource("/index.html").to(async_with_block)), - ) - .await; + let app = + init_service(App::new().service(web::resource("/index.html").to(async_with_block))) + .await; let req = TestRequest::post().uri("/index.html").to_request(); let res = app.call(req).await.unwrap(); @@ -1246,55 +1216,4 @@ mod tests { let res = app.call(req).await.unwrap(); assert!(res.status().is_success()); } - - #[actix_rt::test] - async fn test_actor() { - use crate::Error; - use actix::prelude::*; - - struct MyActor; - - impl Actor for MyActor { - type Context = Context; - } - - struct Num(usize); - - impl Message for Num { - type Result = usize; - } - - impl Handler for MyActor { - type Result = usize; - - fn handle(&mut self, msg: Num, _: &mut Self::Context) -> Self::Result { - msg.0 - } - } - - let addr = MyActor.start(); - - async fn actor_handler( - addr: Data>, - ) -> Result { - // `?` operator tests "actors" feature flag on actix-http - let res = addr.send(Num(1)).await?; - - if res == 1 { - Ok(HttpResponse::Ok()) - } else { - Ok(HttpResponse::BadRequest()) - } - } - - let srv = App::new() - .data(addr.clone()) - .service(web::resource("/").to(actor_handler)); - - let app = init_service(srv).await; - - let req = TestRequest::post().uri("/").to_request(); - let res = app.call(req).await.unwrap(); - assert!(res.status().is_success()); - } } diff --git a/src/types/either.rs b/src/types/either.rs index d72a14fd0..bbab48dec 100644 --- a/src/types/either.rs +++ b/src/types/either.rs @@ -276,13 +276,12 @@ mod tests { .set_payload(Bytes::from_static(b"!@$%^&*()")) .to_http_parts(); - let payload = - Either::, Json>, Bytes>::from_request( - &req, &mut pl, - ) - .await - .unwrap() - .unwrap_right(); + let payload = Either::, Json>, Bytes>::from_request( + &req, &mut pl, + ) + .await + .unwrap() + .unwrap_right(); assert_eq!(&payload.as_ref(), &b"!@$%^&*()"); } @@ -294,15 +293,14 @@ mod tests { }) .to_http_parts(); - let form = - Either::, Json>, Bytes>::from_request( - &req, &mut pl, - ) - .await - .unwrap() - .unwrap_left() - .unwrap_right() - .into_inner(); + let form = Either::, Json>, Bytes>::from_request( + &req, &mut pl, + ) + .await + .unwrap() + .unwrap_left() + .unwrap_right() + .into_inner(); assert_eq!(&form.hello, "world"); } } diff --git a/src/types/form.rs b/src/types/form.rs index 96e09ee1c..0b5c3c1b4 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -21,8 +21,8 @@ use serde::{de::DeserializeOwned, Serialize}; #[cfg(feature = "compress")] use crate::dev::Decompress; use crate::{ - error::UrlencodedError, extract::FromRequest, http::header::CONTENT_LENGTH, web, - Error, HttpMessage, HttpRequest, HttpResponse, Responder, + error::UrlencodedError, extract::FromRequest, http::header::CONTENT_LENGTH, web, Error, + HttpMessage, HttpRequest, HttpResponse, Responder, }; /// URL encoded payload extractor and responder. @@ -342,16 +342,14 @@ where } if encoding == UTF_8 { - serde_urlencoded::from_bytes::(&body) - .map_err(|_| UrlencodedError::Parse) + serde_urlencoded::from_bytes::(&body).map_err(|_| UrlencodedError::Parse) } else { let body = encoding .decode_without_bom_handling_and_without_replacement(&body) .map(|s| s.into_owned()) .ok_or(UrlencodedError::Parse)?; - serde_urlencoded::from_str::(&body) - .map_err(|_| UrlencodedError::Parse) + serde_urlencoded::from_str::(&body).map_err(|_| UrlencodedError::Parse) } } .boxed_local(), diff --git a/src/types/json.rs b/src/types/json.rs index 41618b409..28960402a 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -197,7 +197,7 @@ where /// `Json` extractor configuration. /// -/// # Usage +/// # Examples /// ``` /// use actix_web::{error, post, web, App, FromRequest, HttpResponse}; /// use serde::Deserialize; @@ -482,8 +482,8 @@ mod tests { let msg = MyObject { name: "invalid request".to_string(), }; - let resp = HttpResponse::BadRequest() - .body(serde_json::to_string(&msg).unwrap()); + let resp = + HttpResponse::BadRequest().body(serde_json::to_string(&msg).unwrap()); InternalError::from_response(err, resp).into() })) .to_http_parts(); diff --git a/src/types/path.rs b/src/types/path.rs index 9ee5106d0..4ab124d53 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -13,7 +13,7 @@ use crate::{dev::Payload, error::PathError, FromRequest, HttpRequest}; /// /// Use [`PathConfig`] to configure extraction process. /// -/// # Usage +/// # Examples /// ``` /// use actix_web::{get, web}; /// @@ -235,11 +235,9 @@ mod tests { assert_eq!(res.1, "user1"); let (Path(a), Path(b)) = - <(Path<(String, String)>, Path<(String, String)>)>::from_request( - &req, &mut pl, - ) - .await - .unwrap(); + <(Path<(String, String)>, Path<(String, String)>)>::from_request(&req, &mut pl) + .await + .unwrap(); assert_eq!(a.0, "name"); assert_eq!(a.1, "user1"); assert_eq!(b.0, "name"); @@ -300,11 +298,8 @@ mod tests { async fn test_custom_err_handler() { let (req, mut pl) = TestRequest::with_uri("/name/user1/") .app_data(PathConfig::default().error_handler(|err, _| { - error::InternalError::from_response( - err, - HttpResponse::Conflict().finish(), - ) - .into() + error::InternalError::from_response(err, HttpResponse::Conflict().finish()) + .into() })) .to_http_parts(); diff --git a/src/types/payload.rs b/src/types/payload.rs index 724034522..781347b84 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -23,7 +23,7 @@ use crate::{dev, http::header, web, Error, FromRequest, HttpMessage, HttpRequest /// /// See [`PayloadConfig`] for important notes when using this advanced extractor. /// -/// # Usage +/// # Examples /// ``` /// use std::future::Future; /// use futures_util::stream::{Stream, StreamExt}; @@ -55,10 +55,7 @@ impl Stream for Payload { type Item = Result; #[inline] - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.0).poll_next(cx) } } @@ -81,7 +78,7 @@ impl FromRequest for Payload { /// /// Use [`PayloadConfig`] to configure extraction process. /// -/// # Usage +/// # Examples /// ``` /// use actix_web::{post, web}; /// @@ -118,7 +115,7 @@ impl FromRequest for Bytes { /// [**PayloadConfig**](PayloadConfig) allows to configure /// extraction process. /// -/// # Usage +/// # Examples /// ``` /// use actix_web::{post, web, FromRequest}; /// @@ -396,9 +393,7 @@ mod tests { App::new() .service( web::resource("/bytes-app-data") - .app_data( - PayloadConfig::default().mimetype(mime::APPLICATION_JSON), - ) + .app_data(PayloadConfig::default().mimetype(mime::APPLICATION_JSON)) .route(web::get().to(bytes_handler)), ) .service( @@ -408,9 +403,7 @@ mod tests { ) .service( web::resource("/string-app-data") - .app_data( - PayloadConfig::default().mimetype(mime::APPLICATION_JSON), - ) + .app_data(PayloadConfig::default().mimetype(mime::APPLICATION_JSON)) .route(web::get().to(string_handler)), ) .service( diff --git a/src/types/query.rs b/src/types/query.rs index 2cfc18c36..691a4792b 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -18,7 +18,7 @@ use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequ /// A query string consists of unordered `key=value` pairs, therefore it cannot be decoded into any /// type which depends upon data ordering (eg. tuples). Trying to do so will result in a panic. /// -/// # Usage +/// # Examples /// ``` /// use actix_web::{get, web}; /// use serde::Deserialize; @@ -138,7 +138,7 @@ where /// Query extractor configuration. /// -/// # Usage +/// # Examples /// ``` /// use actix_web::{error, get, web, App, FromRequest, HttpResponse}; /// use serde::Deserialize; @@ -167,8 +167,7 @@ where /// ``` #[derive(Clone)] pub struct QueryConfig { - err_handler: - Option Error + Send + Sync>>, + err_handler: Option Error + Send + Sync>>, } impl QueryConfig { diff --git a/src/types/readlines.rs b/src/types/readlines.rs index 01aab64ab..b8bdcc504 100644 --- a/src/types/readlines.rs +++ b/src/types/readlines.rs @@ -74,10 +74,7 @@ where { type Item = Result; - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); if let Some(err) = this.err.take() { diff --git a/src/web.rs b/src/web.rs index 3b4475b63..1cef37109 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,4 +1,5 @@ //! Essentials helper functions and types for application registration. + use actix_http::http::Method; use actix_router::IntoPattern; use std::future::Future; diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs index 07c171674..34d57008d 100644 --- a/tests/test_httpserver.rs +++ b/tests/test_httpserver.rs @@ -23,8 +23,7 @@ async fn test_start() { sys.block_on(async { let srv = HttpServer::new(|| { App::new().service( - web::resource("/") - .route(web::to(|| HttpResponse::Ok().body("test"))), + web::resource("/").route(web::to(|| HttpResponse::Ok().body("test"))), ) }) .workers(1) diff --git a/tests/test_server.rs b/tests/test_server.rs index c626474cf..c2caa9eb2 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -11,8 +11,7 @@ use std::{ }; use actix_http::http::header::{ - ContentEncoding, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, - TRANSFER_ENCODING, + ContentEncoding, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING, }; use brotli2::write::{BrotliDecoder, BrotliEncoder}; use bytes::Bytes; @@ -69,14 +68,10 @@ impl TestBody { impl futures_core::stream::Stream for TestBody { type Item = Result; - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { ready!(Pin::new(&mut self.delay).poll(cx)); - self.delay = - Box::pin(actix_rt::time::sleep(std::time::Duration::from_millis(10))); + self.delay = Box::pin(actix_rt::time::sleep(std::time::Duration::from_millis(10))); let chunk_size = std::cmp::min(self.chunk_size, self.data.len()); let chunk = self.data.split_to(chunk_size); if chunk.is_empty() { @@ -90,8 +85,7 @@ impl futures_core::stream::Stream for TestBody { #[actix_rt::test] async fn test_body() { let srv = test::start(|| { - App::new() - .service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR)))) + App::new().service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR)))) }); let mut response = srv.get("/").send().await.unwrap(); @@ -328,12 +322,12 @@ async fn test_body_chunked_implicit() { #[actix_rt::test] async fn test_body_br_streaming() { let srv = test::start_with(test::config().h1(), || { - App::new().wrap(Compress::new(ContentEncoding::Br)).service( - web::resource("/").route(web::to(move || { + App::new() + .wrap(Compress::new(ContentEncoding::Br)) + .service(web::resource("/").route(web::to(move || { HttpResponse::Ok() .streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) - })), - ) + }))) }); let mut response = srv @@ -361,8 +355,7 @@ async fn test_body_br_streaming() { async fn test_head_binary() { let srv = test::start_with(test::config().h1(), || { App::new().service( - web::resource("/") - .route(web::head().to(move || HttpResponse::Ok().body(STR))), + web::resource("/").route(web::head().to(move || HttpResponse::Ok().body(STR))), ) }); @@ -403,9 +396,7 @@ async fn test_body_deflate() { let srv = test::start_with(test::config().h1(), || { App::new() .wrap(Compress::new(ContentEncoding::Deflate)) - .service( - web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR))), - ) + .service(web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR)))) }); // client request @@ -430,9 +421,9 @@ async fn test_body_deflate() { #[actix_rt::test] async fn test_body_brotli() { let srv = test::start_with(test::config().h1(), || { - App::new().wrap(Compress::new(ContentEncoding::Br)).service( - web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR))), - ) + App::new() + .wrap(Compress::new(ContentEncoding::Br)) + .service(web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR)))) }); // client request @@ -459,8 +450,7 @@ async fn test_body_brotli() { async fn test_encoding() { let srv = test::start_with(test::config().h1(), || { App::new().wrap(Compress::default()).service( - web::resource("/") - .route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), ) }); @@ -485,8 +475,7 @@ async fn test_encoding() { async fn test_gzip_encoding() { let srv = test::start_with(test::config().h1(), || { App::new().service( - web::resource("/") - .route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), ) }); @@ -512,8 +501,7 @@ async fn test_gzip_encoding_large() { let data = STR.repeat(10); let srv = test::start_with(test::config().h1(), || { App::new().service( - web::resource("/") - .route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), ) }); @@ -544,8 +532,7 @@ async fn test_reading_gzip_encoding_large_random() { let srv = test::start_with(test::config().h1(), || { App::new().service( - web::resource("/") - .route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), ) }); @@ -571,8 +558,7 @@ async fn test_reading_gzip_encoding_large_random() { async fn test_reading_deflate_encoding() { let srv = test::start_with(test::config().h1(), || { App::new().service( - web::resource("/") - .route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), ) }); @@ -598,8 +584,7 @@ async fn test_reading_deflate_encoding_large() { let data = STR.repeat(10); let srv = test::start_with(test::config().h1(), || { App::new().service( - web::resource("/") - .route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), ) }); @@ -630,8 +615,7 @@ async fn test_reading_deflate_encoding_large_random() { let srv = test::start_with(test::config().h1(), || { App::new().service( - web::resource("/") - .route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), ) }); @@ -657,8 +641,7 @@ async fn test_reading_deflate_encoding_large_random() { async fn test_brotli_encoding() { let srv = test::start_with(test::config().h1(), || { App::new().service( - web::resource("/") - .route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), ) }); @@ -883,9 +866,7 @@ async fn test_normalize() { let srv = test::start_with(test::config().h1(), || { App::new() .wrap(NormalizePath::new(TrailingSlash::Trim)) - .service( - web::resource("/one").route(web::to(|| HttpResponse::Ok().finish())), - ) + .service(web::resource("/one").route(web::to(|| HttpResponse::Ok().finish()))) }); let response = srv.get("/one/").send().await.unwrap();