diff --git a/.cargo/config.toml b/.cargo/config.toml index 0bab205cd..72f445d8a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,7 +1,9 @@ [alias] -chk = "hack check --workspace --all-features --tests --examples" -lint = "hack --clean-per-run clippy --workspace --tests --examples" +chk = "check --workspace --all-features --tests --examples --bins" +lint = "clippy --workspace --tests --examples" ci-min = "hack check --workspace --no-default-features" ci-min-test = "hack check --workspace --no-default-features --tests --examples" -ci-default = "hack check --workspace" -ci-full = "check --workspace --bins --examples --tests" +ci-default = "check --workspace --bins --tests --examples" +ci-full = "check --workspace --all-features --bins --tests --examples" +ci-test = "test --workspace --all-features --lib --tests --no-fail-fast -- --nocapture" +ci-doctest = "hack test --workspace --all-features --doc --no-fail-fast -- --nocapture" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d8c6d66ca..bfa124ffd 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,15 +1,8 @@ blank_issues_enabled: true contact_links: - - name: GitHub Discussions - url: https://github.com/actix/actix-web/discussions - about: Actix Web Q&A - - name: Gitter chat (actix-web) - url: https://gitter.im/actix/actix-web - about: Actix Web Q&A - - name: Gitter chat (actix) - url: https://gitter.im/actix/actix - about: Actix (actor framework) Q&A - name: Actix Discord url: https://discord.gg/NWpN5mmg3x about: Actix developer discussion and community chat - + - name: GitHub Discussions + url: https://github.com/actix/actix-web/discussions + about: Actix Web Q&A diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 585b3f497..22b92759a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,13 +44,6 @@ jobs: profile: minimal override: true - - name: Install ${{ matrix.version }} - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ matrix.version }}-${{ matrix.target.triple }} - profile: minimal - override: true - - name: Generate Cargo.lock uses: actions-rs/cargo@v1 with: @@ -66,43 +59,33 @@ jobs: - name: check minimal uses: actions-rs/cargo@v1 - with: - command: hack - args: check --workspace --no-default-features + with: { command: ci-min } - name: check minimal + tests uses: actions-rs/cargo@v1 - with: - command: hack - args: check --workspace --no-default-features --tests --examples + with: { command: ci-min-test } + - name: check default + uses: actions-rs/cargo@v1 + with: { command: ci-default } + - name: check full uses: actions-rs/cargo@v1 - with: - command: check - args: --workspace --bins --examples --tests + with: { command: ci-full } - name: tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --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 timeout-minutes: 40 with: - command: test - args: --package=actix-http --no-default-features --features=rustls -- --nocapture + command: ci-test + args: --skip=test_reading_deflate_encoding_large_random_rustls - - name: tests (awc) + - name: doc tests + # due to unknown issue with running doc tests on macOS + if: matrix.target.os == 'ubuntu-latest' uses: actions-rs/cargo@v1 timeout-minutes: 40 - with: - command: test - args: --package=awc --no-default-features --features=rustls -- --nocapture + with: { command: ci-doctest } - name: Generate coverage file if: > @@ -123,5 +106,5 @@ jobs: - name: Clear the cargo caches run: | - cargo install cargo-cache --no-default-features --features ci-autoclean + cargo install cargo-cache --version 0.6.2 --no-default-features --features ci-autoclean cargo-cache diff --git a/.github/workflows/clippy-fmt.yml b/.github/workflows/clippy-fmt.yml index e966fa4ab..957256d32 100644 --- a/.github/workflows/clippy-fmt.yml +++ b/.github/workflows/clippy-fmt.yml @@ -36,4 +36,4 @@ jobs: uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: --workspace --tests --all-features + args: --workspace --all-features --tests diff --git a/.gitignore b/.gitignore index 638a4397a..543403267 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ guide/build/ # Configuration directory generated by CLion .idea + +# Configuration directory generated by VSCode +.vscode diff --git a/CHANGES.md b/CHANGES.md index 4b3f1d7ab..fb3bc56ce 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,23 +1,60 @@ # Changes ## Unreleased - 2021-xx-xx +### Removed +* `FromRequest::Config` was removed. [#2233] + +[#2233]: https://github.com/actix/actix-web/pull/2233 + + +## 4.0.0-beta.8 - 2021-06-26 +### Added +* Add `ServiceRequest::parts_mut`. [#2177] +* Add extractors for `Uri` and `Method`. [#2263] +* Add extractors for `ConnectionInfo` and `PeerAddr`. [#2263] +* Add `Route::service` for using hand-written services as handlers. [#2262] + +### Changed +* Change compression algorithm features flags. [#2250] +* Deprecate `App::data` and `App::data_factory`. [#2271] +* Smarter extraction of `ConnectionInfo` parts. [#2282] + +### Fixed +* Scope and Resource middleware can access data items set on their own layer. [#2288] + +[#2177]: https://github.com/actix/actix-web/pull/2177 +[#2250]: https://github.com/actix/actix-web/pull/2250 +[#2271]: https://github.com/actix/actix-web/pull/2271 +[#2262]: https://github.com/actix/actix-web/pull/2262 +[#2263]: https://github.com/actix/actix-web/pull/2263 +[#2282]: https://github.com/actix/actix-web/pull/2282 +[#2288]: https://github.com/actix/actix-web/pull/2288 + + +## 4.0.0-beta.7 - 2021-06-17 ### Added * `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200] ### Changed +* Adjusted default JSON payload limit to 2MB (from 32kb) and included size and limits in the `JsonPayloadError::Overflow` error variant. [#2162] +[#2162]: (https://github.com/actix/actix-web/pull/2162) * `ServiceResponse::error_response` now uses body type of `Body`. [#2201] * `ServiceResponse::checked_expr` now returns a `Result`. [#2201] * Update `language-tags` to `0.3`. * `ServiceResponse::take_body`. [#2201] * `ServiceResponse::map_body` closure receives and returns `B` instead of `ResponseBody` types. [#2201] +* All error trait bounds in server service builders have changed from `Into` to `Into>`. [#2253] +* All error trait bounds in message body and stream impls changed from `Into` to `Into>`. [#2253] +* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226] +* `middleware::normalize` now will not try to normalize URIs with no valid path [#2246] ### Removed * `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201] -* `FromRequest::Config` was removed. [#2233] [#2200]: https://github.com/actix/actix-web/pull/2200 [#2201]: https://github.com/actix/actix-web/pull/2201 -[#2233]: https://github.com/actix/actix-web/pull/2233 +[#2253]: https://github.com/actix/actix-web/pull/2253 +[#2246]: https://github.com/actix/actix-web/pull/2246 ## 4.0.0-beta.6 - 2021-04-17 diff --git a/Cargo.toml b/Cargo.toml index 91d1212fc..9d4e9562f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.6" +version = "4.0.0-beta.8" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" keywords = ["actix", "http", "web", "framework", "async"] @@ -17,7 +17,7 @@ edition = "2018" [package.metadata.docs.rs] # features that docs.rs will build with -features = ["openssl", "rustls", "compress", "cookies", "secure-cookies"] +features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"] rustdoc-args = ["--cfg", "docsrs"] [lib] @@ -40,10 +40,14 @@ members = [ # resolver = "2" [features] -default = ["compress", "cookies"] +default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] -# content-encoding support -compress = ["actix-http/compress"] +# Brotli algorithm content-encoding support +compress-brotli = ["actix-http/compress-brotli", "__compress"] +# Gzip and deflate algorithms content-encoding support +compress-gzip = ["actix-http/compress-gzip", "__compress"] +# Zstd algorithm content-encoding support +compress-zstd = ["actix-http/compress-zstd", "__compress"] # support for cookies cookies = ["cookie"] @@ -57,9 +61,13 @@ openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] # rustls rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] +# Internal (PRIVATE!) features used to aid testing and cheking feature status. +# Don't rely on these whatsoever. They may disappear at anytime. +__compress = [] + [dependencies] actix-codec = "0.4.0" -actix-macros = "0.2.0" +actix-macros = "0.2.1" actix-router = "0.2.7" actix-rt = "2.2" actix-server = "2.0.0-beta.3" @@ -68,10 +76,11 @@ actix-utils = "3.0.0" actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true } actix-web-codegen = "0.5.0-beta.2" -actix-http = "3.0.0-beta.6" +actix-http = "3.0.0-beta.8" ahash = "0.7" bytes = "1" +cfg-if = "1" cookie = { version = "0.15", features = ["percent-encode"], optional = true } derive_more = "0.99.5" either = "1.5.3" @@ -95,16 +104,16 @@ time = { version = "0.2.23", default-features = false, features = ["std"] } url = "2.1" [dev-dependencies] -actix-test = { version = "0.1.0-beta.2", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.5", features = ["openssl"] } +actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } +awc = { version = "3.0.0-beta.7", features = ["openssl"] } brotli2 = "0.3.2" -criterion = "0.3" +criterion = { version = "0.3", features = ["html_reports"] } env_logger = "0.8" flate2 = "1.0.13" +zstd = "0.7" rand = "0.8" rcgen = "0.8" -serde_derive = "1.0" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.19.0" } @@ -126,15 +135,15 @@ awc = { path = "awc" } [[test]] name = "test_server" -required-features = ["compress", "cookies"] +required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] [[example]] name = "basic" -required-features = ["compress"] +required-features = ["compress-gzip"] [[example]] name = "uds" -required-features = ["compress"] +required-features = ["compress-gzip"] [[example]] name = "on_connect" diff --git a/MIGRATION.md b/MIGRATION.md index c5d08e8c1..c644d95e9 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -12,6 +12,19 @@ * The `type Config` of `FromRequest` was removed. +* Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd). + By default all compression algorithms are enabled. + To select algorithm you want to include with `middleware::Compress` use following flags: + - `compress-brotli` + - `compress-gzip` + - `compress-zstd` + If you have set in your `Cargo.toml` dedicated `actix-web` features and you still want + to have compression enabled. Please change features selection like bellow: + + Before: `"compress"` + After: `"compress-brotli", "compress-gzip", "compress-zstd"` + + ## 3.0.0 * The return type for `ServiceRequest::app_data::()` was changed from returning a `Data` to diff --git a/README.md b/README.md index 60ec57c60..309a18466 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) -[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.5)](https://docs.rs/actix-web/4.0.0-beta.5) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.8)](https://docs.rs/actix-web/4.0.0-beta.8) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.5/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.5) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.8/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.8)
[![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) @@ -25,7 +25,7 @@ * Streaming and pipelining * Keep-alive and slow requests handling * Client/server [WebSockets](https://actix.rs/docs/websockets/) support -* Transparent content compression/decompression (br, gzip, deflate) +* Transparent content compression/decompression (br, gzip, deflate, zstd) * Powerful [request routing](https://actix.rs/docs/url-dispatch/) * Multipart streams * Static assets diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 12b70c135..db047c44c 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,22 +1,34 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.6.0-beta.6 - 2021-06-26 +* Added `Files::path_filter()`. [#2274] +* `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228] + +[#2274]: https://github.com/actix/actix-web/pull/2274 +[#2228]: https://github.com/actix/actix-web/pull/2228 + + +## 0.6.0-beta.5 - 2021-06-17 * `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135] * For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156] * `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225] +* `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257] [#2135]: https://github.com/actix/actix-web/pull/2135 [#2156]: https://github.com/actix/actix-web/pull/2156 [#2225]: https://github.com/actix/actix-web/pull/2225 +[#2257]: https://github.com/actix/actix-web/pull/2257 ## 0.6.0-beta.4 - 2021-04-02 -* No notable changes. - * Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046] [#2046]: https://github.com/actix/actix-web/pull/2046 + ## 0.6.0-beta.3 - 2021-03-09 * No notable changes. diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index b97badd3e..ef288215b 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,13 +1,11 @@ [package] name = "actix-files" -version = "0.6.0-beta.4" +version = "0.6.0-beta.6" authors = ["Nikolay Kim "] description = "Static file serving for Actix Web" -readme = "README.md" keywords = ["actix", "http", "async", "futures"] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-files/" +repository = "https://github.com/actix/actix-web" categories = ["asynchronous", "web-programming::http-server"] license = "MIT OR Apache-2.0" edition = "2018" @@ -17,7 +15,8 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.6", default-features = false } +actix-web = { version = "4.0.0-beta.8", default-features = false } +actix-http = "3.0.0-beta.8" actix-service = "2.0.0" actix-utils = "3.0.0" @@ -34,5 +33,5 @@ percent-encoding = "2.1" [dev-dependencies] actix-rt = "2.2" -actix-web = "4.0.0-beta.6" -actix-test = "0.1.0-beta.2" +actix-web = "4.0.0-beta.8" +actix-test = "0.1.0-beta.3" diff --git a/actix-files/README.md b/actix-files/README.md index 895d5e687..13c301c56 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,17 +3,16 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.4)](https://docs.rs/actix-files/0.6.0-beta.4) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.6)](https://docs.rs/actix-files/0.6.0-beta.6) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.4/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.4) +[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.6/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.6) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) -[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-files/) - [Example Project](https://github.com/actix/examples/tree/master/basics/static_index) -- [Chat on Gitter](https://gitter.im/actix/actix-web) - Minimum supported Rust version: 1.46 or later diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index fc8fd2531..49d81eb03 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -1,9 +1,17 @@ -use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc}; +use std::{ + cell::RefCell, + fmt, io, + path::{Path, PathBuf}, + rc::Rc, +}; use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt}; use actix_utils::future::ok; use actix_web::{ - dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse}, + dev::{ + AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, + ServiceResponse, + }, error::Error, guard::Guard, http::header::DispositionType, @@ -13,7 +21,7 @@ use futures_core::future::LocalBoxFuture; use crate::{ directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService, - MimeOverride, + MimeOverride, PathFilter, }; /// Static files handling service. @@ -36,6 +44,7 @@ pub struct Files { default: Rc>>>, renderer: Rc, mime_override: Option>, + path_filter: Option>, file_flags: named::Flags, use_guards: Option>, guards: Vec>, @@ -60,6 +69,7 @@ impl Clone for Files { file_flags: self.file_flags, path: self.path.clone(), mime_override: self.mime_override.clone(), + path_filter: self.path_filter.clone(), use_guards: self.use_guards.clone(), guards: self.guards.clone(), hidden_files: self.hidden_files, @@ -104,6 +114,7 @@ impl Files { default: Rc::new(RefCell::new(None)), renderer: Rc::new(directory_listing), mime_override: None, + path_filter: None, file_flags: named::Flags::default(), use_guards: None, guards: Vec::new(), @@ -114,6 +125,9 @@ impl Files { /// Show files listing for directories. /// /// By default show files listing is disabled. + /// + /// When used with [`Files::index_file()`], files listing is shown as a fallback + /// when the index file is not found. pub fn show_files_listing(mut self) -> Self { self.show_index = true; self @@ -146,10 +160,45 @@ impl Files { self } + /// Sets path filtering closure. + /// + /// The path provided to the closure is relative to `serve_from` path. + /// You can safely join this path with the `serve_from` path to get the real path. + /// However, the real path may not exist since the filter is called before checking path existence. + /// + /// When a path doesn't pass the filter, [`Files::default_handler`] is called if set, otherwise, + /// `404 Not Found` is returned. + /// + /// # Examples + /// ``` + /// use std::path::Path; + /// use actix_files::Files; + /// + /// // prevent searching subdirectories and following symlinks + /// let files_service = Files::new("/", "./static").path_filter(|path, _| { + /// path.components().count() == 1 + /// && Path::new("./static") + /// .join(path) + /// .symlink_metadata() + /// .map(|m| !m.file_type().is_symlink()) + /// .unwrap_or(false) + /// }); + /// ``` + pub fn path_filter(mut self, f: F) -> Self + where + F: Fn(&Path, &RequestHead) -> bool + 'static, + { + self.path_filter = Some(Rc::new(f)); + self + } + /// Set index file /// - /// Shows specific index file for directory "/" instead of + /// Shows specific index file for directories instead of /// showing files listing. + /// + /// If the index file is not found, files listing is shown as a fallback if + /// [`Files::show_files_listing()`] is set. pub fn index_file>(mut self, index: T) -> Self { self.index = Some(index.into()); self @@ -312,6 +361,7 @@ impl ServiceFactory for Files { default: None, renderer: self.renderer.clone(), mime_override: self.mime_override.clone(), + path_filter: self.path_filter.clone(), file_flags: self.file_flags, guards: self.use_guards.clone(), hidden_files: self.hidden_files, diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index aa5960b5b..1eb091aaf 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -16,11 +16,12 @@ use actix_service::boxed::{BoxService, BoxServiceFactory}; use actix_web::{ - dev::{ServiceRequest, ServiceResponse}, + dev::{RequestHead, ServiceRequest, ServiceResponse}, error::Error, http::header::DispositionType, }; use mime_guess::from_ext; +use std::path::Path; mod chunked; mod directory; @@ -56,6 +57,8 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime { type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType; +type PathFilter = dyn Fn(&Path, &RequestHead) -> bool; + #[cfg(test)] mod tests { use std::{ @@ -279,6 +282,22 @@ mod tests { ); } + #[actix_rt::test] + async fn test_named_file_javascript() { + let file = NamedFile::open("tests/test.js").unwrap(); + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).await.unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "application/javascript" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"test.js\"" + ); + } + #[actix_rt::test] async fn test_named_file_image_attachment() { let cd = ContentDisposition { @@ -856,4 +875,69 @@ mod tests { "inline; filename=\"symlink-test.png\"" ); } + + #[actix_rt::test] + async fn test_index_with_show_files_listing() { + let service = Files::new(".", ".") + .index_file("lib.rs") + .show_files_listing() + .new_service(()) + .await + .unwrap(); + + // Serve the index if exists + let req = TestRequest::default().uri("/src").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/x-rust" + ); + + // Show files listing, otherwise. + let req = TestRequest::default().uri("/tests").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/html; charset=utf-8" + ); + let bytes = test::read_body(resp).await; + assert!(format!("{:?}", bytes).contains("/tests/test.png")); + } + + #[actix_rt::test] + async fn test_path_filter() { + // prevent searching subdirectories + let st = Files::new("/", ".") + .path_filter(|path, _| path.components().count() == 1) + .new_service(()) + .await + .unwrap(); + + let req = TestRequest::with_uri("/Cargo.toml").to_srv_request(); + let resp = test::call_service(&st, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/src/lib.rs").to_srv_request(); + let resp = test::call_service(&st, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[actix_rt::test] + async fn test_default_handler_filter() { + let st = Files::new("/", ".") + .default_handler(|req: ServiceRequest| { + ok(req.into_response(HttpResponse::Ok().body("default content"))) + }) + .path_filter(|path, _| path.extension() == Some("png".as_ref())) + .new_service(()) + .await + .unwrap(); + let req = TestRequest::with_uri("/Cargo.toml").to_srv_request(); + let resp = test::call_service(&st, req).await; + + assert_eq!(resp.status(), StatusCode::OK); + let bytes = test::read_body(resp).await; + assert_eq!(bytes, web::Bytes::from_static(b"default content")); + } } diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 519234f0d..241e78cf0 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -120,6 +120,11 @@ impl NamedFile { let disposition = match ct.type_() { mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, + mime::APPLICATION => match ct.subtype() { + mime::JAVASCRIPT | mime::JSON => DispositionType::Inline, + name if name == "wasm" => DispositionType::Inline, + _ => DispositionType::Attachment, + }, _ => DispositionType::Attachment, }; @@ -213,9 +218,11 @@ impl NamedFile { /// Set the Content-Disposition for serving this file. This allows /// changing the inline/attachment disposition as well as the filename - /// sent to the peer. By default the disposition is `inline` for text, - /// image, and video content types, and `attachment` otherwise, and - /// the filename is taken from the path provided in the `open` method + /// sent to the peer. + /// + /// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and + /// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise, + /// and the filename is taken from the path provided in the `open` method /// after converting it to UTF-8 using. /// [`std::ffi::OsStr::to_string_lossy`] #[inline] @@ -235,6 +242,8 @@ impl NamedFile { } /// Set content encoding for serving this file + /// + /// Must be used with [`actix_web::middleware::Compress`] to take effect. #[inline] pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { self.encoding = Some(enc); @@ -346,8 +355,8 @@ impl NamedFile { } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = (last_modified, req.get_header()) { - let t1: SystemTime = m.clone().into(); - let t2: SystemTime = since.clone().into(); + let t1: SystemTime = (*m).into(); + let t2: SystemTime = (*since).into(); match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { (Ok(t1), Ok(t2)) => t1.as_secs() > t2.as_secs(), @@ -365,8 +374,8 @@ impl NamedFile { } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = (last_modified, req.get_header()) { - let t1: SystemTime = m.clone().into(); - let t2: SystemTime = since.clone().into(); + let t1: SystemTime = (*m).into(); + let t2: SystemTime = (*since).into(); match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { (Ok(t1), Ok(t2)) => t1.as_secs() <= t2.as_secs(), diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index 831115dc6..09122c63e 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -13,7 +13,7 @@ use futures_core::future::LocalBoxFuture; use crate::{ named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile, - PathBufWrap, + PathBufWrap, PathFilter, }; /// Assembled file serving service. @@ -25,6 +25,7 @@ pub struct FilesService { pub(crate) default: Option, pub(crate) renderer: Rc, pub(crate) mime_override: Option>, + pub(crate) path_filter: Option>, pub(crate) file_flags: named::Flags, pub(crate) guards: Option>, pub(crate) hidden_files: bool, @@ -82,6 +83,18 @@ impl Service for FilesService { Err(e) => return Box::pin(ok(req.error_response(e))), }; + if let Some(filter) = &self.path_filter { + if !filter(real_path.as_ref(), req.head()) { + if let Some(ref default) = self.default { + return Box::pin(default.call(req)); + } else { + return Box::pin(ok( + req.into_response(actix_web::HttpResponse::NotFound().finish()) + )); + } + } + } + // full file path let path = self.directory.join(&real_path); if let Err(err) = path.canonicalize() { @@ -102,26 +115,20 @@ impl Service for FilesService { ))); } - if let Some(ref redir_index) = self.index { - let path = path.join(redir_index); - - 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_()); - named_file.content_disposition.disposition = new_disposition; - } - named_file.flags = self.file_flags; - - let (req, _) = req.into_parts(); - let res = named_file.into_response(&req); - Box::pin(ok(ServiceResponse::new(req, res))) - } - Err(err) => self.handle_err(err, req), + let serve_named_file = |req: ServiceRequest, mut named_file: NamedFile| { + if let Some(ref mime_override) = self.mime_override { + let new_disposition = mime_override(&named_file.content_type.type_()); + named_file.content_disposition.disposition = new_disposition; } - } else if self.show_index { - let dir = Directory::new(self.directory.clone(), path); + named_file.flags = self.file_flags; + + let (req, _) = req.into_parts(); + let res = named_file.into_response(&req); + Box::pin(ok(ServiceResponse::new(req, res))) + }; + + let show_index = |req: ServiceRequest| { + let dir = Directory::new(self.directory.clone(), path.clone()); let (req, _) = req.into_parts(); let x = (self.renderer)(&dir, &req); @@ -130,11 +137,19 @@ impl Service for FilesService { Ok(resp) => ok(resp), Err(err) => ok(ServiceResponse::from_err(err, req)), }) - } else { - Box::pin(ok(ServiceResponse::from_err( + }; + + match self.index { + Some(ref index) => match NamedFile::open(path.join(index)) { + Ok(named_file) => serve_named_file(req, named_file), + Err(_) if self.show_index => show_index(req), + Err(err) => self.handle_err(err, req), + }, + None if self.show_index => show_index(req), + _ => Box::pin(ok(ServiceResponse::from_err( FilesError::IsDirectory, req.into_parts().0, - ))) + ))), } } else { match NamedFile::open(path) { diff --git a/actix-files/tests/test.js b/actix-files/tests/test.js new file mode 100644 index 000000000..2ee135561 --- /dev/null +++ b/actix-files/tests/test.js @@ -0,0 +1 @@ +// this file is empty. diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 88b4bd04a..c04b5da49 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -35,7 +35,7 @@ actix-tls = "3.0.0-beta.5" actix-utils = "3.0.0" actix-rt = "2.2" actix-server = "2.0.0-beta.3" -awc = { version = "3.0.0-beta.5", default-features = false } +awc = { version = "3.0.0-beta.7", default-features = false } base64 = "0.13" bytes = "1" @@ -51,5 +51,5 @@ time = { version = "0.2.23", default-features = false, features = ["std"] } tls-openssl = { version = "0.10.9", package = "openssl", optional = true } [dev-dependencies] -actix-web = { version = "4.0.0-beta.6", default-features = false, features = ["cookies"] } -actix-http = "3.0.0-beta.6" +actix-web = { version = "4.0.0-beta.8", default-features = false, features = ["cookies"] } +actix-http = "3.0.0-beta.8" diff --git a/actix-http-test/README.md b/actix-http-test/README.md index b8cf450d4..74260a352 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -4,12 +4,14 @@ [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.4)](https://docs.rs/actix-http-test/3.0.0-beta.4) +[![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-test) +
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.4) -[![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) +[![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http-test) -- [Chat on Gitter](https://gitter.im/actix/actix-web) - Minimum Supported Rust Version (MSRV): 1.46.0 diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index ec7d8ee3b..8ead43718 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,20 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.8 - 2021-06-26 +### Changed +* Change compression algorithm features flags. [#2250] + +### Removed +* `downcast` and `downcast_get_type_id` macros. [#2291] + +[#2291]: https://github.com/actix/actix-web/pull/2291 +[#2250]: https://github.com/actix/actix-web/pull/2250 + + +## 3.0.0-beta.7 - 2021-06-17 ### Added * Alias `body::Body` as `body::AnyBody`. [#2215] * `BoxAnyBody`: a boxed message body with boxed errors. [#2183] @@ -9,15 +23,20 @@ * Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171] * `Response::into_body` that consumes response and returns body type. [#2201] * `impl Default` for `Response`. [#2201] +* Add zstd support for `ContentEncoding`. [#2244] ### Changed * The `MessageBody` trait now has an associated `Error` type. [#2183] +* All error trait bounds in server service builders have changed from `Into` to `Into>`. [#2253] +* All error trait bounds in message body and stream impls changed from `Into` to `Into>`. [#2253] * Places in `Response` where `ResponseBody` was received or returned now simply use `B`. [#2201] * `header` mod is now public. [#2171] * `uri` mod is now public. [#2171] * Update `language-tags` to `0.3`. * Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201] * `ResponseBuilder::message_body` now returns a `Result`. [#2201] +* Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253] +* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226] ### Removed * Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171] @@ -35,6 +54,9 @@ [#2201]: https://github.com/actix/actix-web/pull/2201 [#2205]: https://github.com/actix/actix-web/pull/2205 [#2215]: https://github.com/actix/actix-web/pull/2215 +[#2253]: https://github.com/actix/actix-web/pull/2253 +[#2244]: https://github.com/actix/actix-web/pull/2244 + ## 3.0.0-beta.6 - 2021-04-17 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 1f7df39a6..a12fed4b9 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,13 +1,11 @@ [package] name = "actix-http" -version = "3.0.0-beta.6" +version = "3.0.0-beta.8" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" -readme = "README.md" keywords = ["actix", "http", "framework", "async", "futures"] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-http/" +repository = "https://github.com/actix/actix-web" categories = ["network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket"] @@ -16,7 +14,7 @@ edition = "2018" [package.metadata.docs.rs] # features that docs.rs will build with -features = ["openssl", "rustls", "compress"] +features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"] [lib] name = "actix_http" @@ -32,11 +30,17 @@ openssl = ["actix-tls/openssl"] rustls = ["actix-tls/rustls"] # enable compression support -compress = ["flate2", "brotli2"] +compress-brotli = ["brotli2", "__compress"] +compress-gzip = ["flate2", "__compress"] +compress-zstd = ["zstd", "__compress"] # trust-dns as client dns resolver trust-dns = ["trust-dns-resolver"] +# Internal (PRIVATE!) features used to aid testing and cheking feature status. +# Don't rely on these whatsoever. They may disappear at anytime. +__compress = [] + [dependencies] actix-service = "2.0.0" actix-codec = "0.4.0" @@ -76,6 +80,7 @@ tokio = { version = "1.2", features = ["sync"] } # compression brotli2 = { version="0.3.2", optional = true } flate2 = { version = "1.0.13", optional = true } +zstd = { version = "0.7", optional = true } trust-dns-resolver = { version = "0.20.0", optional = true } @@ -83,13 +88,15 @@ trust-dns-resolver = { version = "0.20.0", optional = true } actix-server = "2.0.0-beta.3" actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] } actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] } -criterion = "0.3" +async-stream = "0.3" +criterion = { version = "0.3", features = ["html_reports"] } env_logger = "0.8" rcgen = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tls-openssl = { version = "0.10", package = "openssl" } tls-rustls = { version = "0.19", package = "rustls" } +webpki = { version = "0.21.0" } [[example]] name = "ws" diff --git a/actix-http/README.md b/actix-http/README.md index 87eb38e5d..de1ef0a9b 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -3,18 +3,17 @@ > 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=3.0.0-beta.6)](https://docs.rs/actix-http/3.0.0-beta.6) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.8)](https://docs.rs/actix-http/3.0.0-beta.8) [![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.6/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.6) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.8/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.8) [![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) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http) -- [Chat on Gitter](https://gitter.im/actix/actix-web) - Minimum Supported Rust Version (MSRV): 1.46.0 ## Example diff --git a/actix-http/benches/uninit-headers.rs b/actix-http/benches/uninit-headers.rs index 83e74171c..53a2528ab 100644 --- a/actix-http/benches/uninit-headers.rs +++ b/actix-http/benches/uninit-headers.rs @@ -78,12 +78,12 @@ impl HeaderIndex { // test cases taken from: // https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs -const REQ_SHORT: &'static [u8] = b"\ +const REQ_SHORT: &[u8] = b"\ GET / HTTP/1.0\r\n\ Host: example.com\r\n\ Cookie: session=60; user_id=1\r\n\r\n"; -const REQ: &'static [u8] = b"\ +const REQ: &[u8] = b"\ GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n\ Host: www.kittyhell.com\r\n\ User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n\ @@ -119,6 +119,8 @@ mod _original { use std::mem::MaybeUninit; pub fn parse_headers(src: &mut BytesMut) -> usize { + #![allow(clippy::uninit_assumed_init)] + let mut headers: [HeaderIndex; MAX_HEADERS] = unsafe { MaybeUninit::uninit().assume_init() }; diff --git a/actix-http/examples/echo.rs b/actix-http/examples/echo.rs index 54a71a106..6cfe3a675 100644 --- a/actix-http/examples/echo.rs +++ b/actix-http/examples/echo.rs @@ -5,14 +5,13 @@ use actix_server::Server; use bytes::BytesMut; use futures_util::StreamExt as _; use http::header::HeaderValue; -use log::info; #[actix_rt::main] async fn main() -> io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); Server::build() - .bind("echo", "127.0.0.1:8080", || { + .bind("echo", ("127.0.0.1", 8080), || { HttpService::build() .client_timeout(1000) .client_disconnect(1000) @@ -22,7 +21,8 @@ async fn main() -> io::Result<()> { body.extend_from_slice(&item?); } - info!("request body: {:?}", body); + log::info!("request body: {:?}", body); + Ok::<_, Error>( Response::build(StatusCode::OK) .insert_header(( diff --git a/actix-http/examples/echo2.rs b/actix-http/examples/echo2.rs index 3974cf20b..db195d65b 100644 --- a/actix-http/examples/echo2.rs +++ b/actix-http/examples/echo2.rs @@ -5,7 +5,6 @@ use actix_http::{Error, HttpService, Request, Response}; use actix_server::Server; use bytes::BytesMut; use futures_util::StreamExt as _; -use log::info; async fn handle_request(mut req: Request) -> Result, Error> { let mut body = BytesMut::new(); @@ -13,7 +12,8 @@ async fn handle_request(mut req: Request) -> Result, Error> { body.extend_from_slice(&item?) } - info!("request body: {:?}", body); + log::info!("request body: {:?}", body); + Ok(Response::build(StatusCode::OK) .insert_header(("x-head", HeaderValue::from_static("dummy value!"))) .body(body)) @@ -24,7 +24,7 @@ async fn main() -> io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); Server::build() - .bind("echo", "127.0.0.1:8080", || { + .bind("echo", ("127.0.0.1", 8080), || { HttpService::build().finish(handle_request).tcp() })? .run() diff --git a/actix-http/examples/hello-world.rs b/actix-http/examples/hello-world.rs index d51de6f4e..9a593c66a 100644 --- a/actix-http/examples/hello-world.rs +++ b/actix-http/examples/hello-world.rs @@ -1,28 +1,28 @@ -use std::io; +use std::{convert::Infallible, io}; use actix_http::{http::StatusCode, HttpService, Response}; use actix_server::Server; -use actix_utils::future; use http::header::HeaderValue; -use log::info; #[actix_rt::main] async fn main() -> io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); Server::build() - .bind("hello-world", "127.0.0.1:8080", || { + .bind("hello-world", ("127.0.0.1", 8080), || { HttpService::build() .client_timeout(1000) .client_disconnect(1000) - .finish(|_req| { - info!("{:?}", _req); + .finish(|req| async move { + log::info!("{:?}", req); + let mut res = Response::build(StatusCode::OK); res.insert_header(( "x-head", HeaderValue::from_static("dummy value!"), )); - future::ok::<_, ()>(res.body("Hello world!")) + + Ok::<_, Infallible>(res.body("Hello world!")) }) .tcp() })? diff --git a/actix-http/examples/streaming-error.rs b/actix-http/examples/streaming-error.rs new file mode 100644 index 000000000..3988cbac2 --- /dev/null +++ b/actix-http/examples/streaming-error.rs @@ -0,0 +1,40 @@ +//! Example showing response body (chunked) stream erroring. +//! +//! Test using `nc` or `curl`. +//! ```sh +//! $ curl -vN 127.0.0.1:8080 +//! $ echo 'GET / HTTP/1.1\n\n' | nc 127.0.0.1 8080 +//! ``` + +use std::{convert::Infallible, io, time::Duration}; + +use actix_http::{body::BodyStream, HttpService, Response}; +use actix_server::Server; +use async_stream::stream; +use bytes::Bytes; + +#[actix_rt::main] +async fn main() -> io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + Server::build() + .bind("streaming-error", ("127.0.0.1", 8080), || { + HttpService::build() + .finish(|req| async move { + log::info!("{:?}", req); + let res = Response::ok(); + + Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! { + yield Ok(Bytes::from("123")); + yield Ok(Bytes::from("456")); + + actix_rt::time::sleep(Duration::from_millis(1000)).await; + + yield Err(io::Error::new(io::ErrorKind::Other, "")); + }))) + }) + .tcp() + })? + .run() + .await +} diff --git a/actix-http/src/body/body.rs b/actix-http/src/body/body.rs index 3fda8ae11..f04837d07 100644 --- a/actix-http/src/body/body.rs +++ b/actix-http/src/body/body.rs @@ -76,7 +76,9 @@ impl MessageBody for AnyBody { // TODO: MSRV 1.51: poll_map_err AnyBody::Message(body) => match ready!(body.as_pin_mut().poll_next(cx)) { - Some(Err(err)) => Poll::Ready(Some(Err(err.into()))), + Some(Err(err)) => { + Poll::Ready(Some(Err(Error::new_body().with_cause(err)))) + } Some(Ok(val)) => Poll::Ready(Some(Ok(val))), None => Poll::Ready(None), }, @@ -162,9 +164,10 @@ impl From for AnyBody { } } -impl From> for AnyBody +impl From> for AnyBody where - S: Stream> + 'static, + S: Stream> + 'static, + E: Into> + 'static, { fn from(s: SizedStream) -> Body { AnyBody::from_message(s) @@ -174,7 +177,7 @@ where impl From> for AnyBody where S: Stream> + 'static, - E: Into + 'static, + E: Into> + 'static, { fn from(s: BodyStream) -> Body { AnyBody::from_message(s) @@ -222,7 +225,7 @@ impl MessageBody for BoxAnyBody { ) -> Poll>> { // TODO: MSRV 1.51: poll_map_err match ready!(self.0.as_mut().poll_next(cx)) { - Some(Err(err)) => Poll::Ready(Some(Err(err.into()))), + Some(Err(err)) => Poll::Ready(Some(Err(Error::new_body().with_cause(err)))), Some(Ok(val)) => Poll::Ready(Some(Ok(val))), None => Poll::Ready(None), } diff --git a/actix-http/src/body/body_stream.rs b/actix-http/src/body/body_stream.rs index ebe872022..f726f4475 100644 --- a/actix-http/src/body/body_stream.rs +++ b/actix-http/src/body/body_stream.rs @@ -1,4 +1,5 @@ use std::{ + error::Error as StdError, pin::Pin, task::{Context, Poll}, }; @@ -7,8 +8,6 @@ use bytes::Bytes; use futures_core::{ready, Stream}; use pin_project_lite::pin_project; -use crate::error::Error; - use super::{BodySize, MessageBody}; pin_project! { @@ -24,7 +23,7 @@ pin_project! { impl BodyStream where S: Stream>, - E: Into, + E: Into> + 'static, { pub fn new(stream: S) -> Self { BodyStream { stream } @@ -34,9 +33,9 @@ where impl MessageBody for BodyStream where S: Stream>, - E: Into, + E: Into> + 'static, { - type Error = Error; + type Error = E; fn size(&self) -> BodySize { BodySize::Stream @@ -56,7 +55,7 @@ where let chunk = match ready!(stream.poll_next(cx)) { Some(Ok(ref bytes)) if bytes.is_empty() => continue, - opt => opt.map(|res| res.map_err(Into::into)), + opt => opt, }; return Poll::Ready(chunk); @@ -66,9 +65,16 @@ where #[cfg(test)] mod tests { - use actix_rt::pin; + use std::{convert::Infallible, time::Duration}; + + use actix_rt::{ + pin, + time::{sleep, Sleep}, + }; use actix_utils::future::poll_fn; - use futures_util::stream; + use derive_more::{Display, Error}; + use futures_core::ready; + use futures_util::{stream, FutureExt as _}; use super::*; use crate::body::to_bytes; @@ -78,7 +84,7 @@ mod tests { let body = BodyStream::new(stream::iter( ["1", "", "2"] .iter() - .map(|&v| Ok(Bytes::from(v)) as Result), + .map(|&v| Ok::<_, Infallible>(Bytes::from(v))), )); pin!(body); @@ -103,9 +109,63 @@ mod tests { let body = BodyStream::new(stream::iter( ["1", "", "2"] .iter() - .map(|&v| Ok(Bytes::from(v)) as Result), + .map(|&v| Ok::<_, Infallible>(Bytes::from(v))), )); assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12"))); } + #[derive(Debug, Display, Error)] + #[display(fmt = "stream error")] + struct StreamErr; + + #[actix_rt::test] + async fn stream_immediate_error() { + let body = BodyStream::new(stream::once(async { Err(StreamErr) })); + assert!(matches!(to_bytes(body).await, Err(StreamErr))); + } + + #[actix_rt::test] + async fn stream_delayed_error() { + let body = + BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)])); + assert!(matches!(to_bytes(body).await, Err(StreamErr))); + + #[pin_project::pin_project(project = TimeDelayStreamProj)] + #[derive(Debug)] + enum TimeDelayStream { + Start, + Sleep(Pin>), + Done, + } + + impl Stream for TimeDelayStream { + type Item = Result; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + match self.as_mut().get_mut() { + TimeDelayStream::Start => { + let sleep = sleep(Duration::from_millis(1)); + self.as_mut().set(TimeDelayStream::Sleep(Box::pin(sleep))); + cx.waker().wake_by_ref(); + Poll::Pending + } + + TimeDelayStream::Sleep(ref mut delay) => { + ready!(delay.poll_unpin(cx)); + self.set(TimeDelayStream::Done); + cx.waker().wake_by_ref(); + Poll::Pending + } + + TimeDelayStream::Done => Poll::Ready(Some(Err(StreamErr))), + } + } + } + + let body = BodyStream::new(TimeDelayStream::Start); + assert!(matches!(to_bytes(body).await, Err(StreamErr))); + } } diff --git a/actix-http/src/body/mod.rs b/actix-http/src/body/mod.rs index 11aff039e..8a08dbd2b 100644 --- a/actix-http/src/body/mod.rs +++ b/actix-http/src/body/mod.rs @@ -191,11 +191,15 @@ mod tests { } #[actix_rt::test] - async fn test_box() { + async fn test_box_and_pin() { let val = Box::new(()); pin!(val); assert_eq!(val.size(), BodySize::Empty); assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none()); + + let mut val = Box::pin(()); + assert_eq!(val.size(), BodySize::Empty); + assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none()); } #[actix_rt::test] diff --git a/actix-http/src/body/sized_stream.rs b/actix-http/src/body/sized_stream.rs index 4af132389..b6ceb32fe 100644 --- a/actix-http/src/body/sized_stream.rs +++ b/actix-http/src/body/sized_stream.rs @@ -1,4 +1,5 @@ use std::{ + error::Error as StdError, pin::Pin, task::{Context, Poll}, }; @@ -7,15 +8,13 @@ use bytes::Bytes; use futures_core::{ready, Stream}; use pin_project_lite::pin_project; -use crate::error::Error; - use super::{BodySize, MessageBody}; pin_project! { /// Known sized streaming response wrapper. /// - /// This body implementation should be used if total size of stream is known. Data get sent as is - /// without using transfer encoding. + /// This body implementation should be used if total size of stream is known. Data is sent as-is + /// without using chunked transfer encoding. pub struct SizedStream { size: u64, #[pin] @@ -23,20 +22,22 @@ pin_project! { } } -impl SizedStream +impl SizedStream where - S: Stream>, + S: Stream>, + E: Into> + 'static, { pub fn new(size: u64, stream: S) -> Self { SizedStream { size, stream } } } -impl MessageBody for SizedStream +impl MessageBody for SizedStream where - S: Stream>, + S: Stream>, + E: Into> + 'static, { - type Error = Error; + type Error = E; fn size(&self) -> BodySize { BodySize::Sized(self.size as u64) @@ -66,6 +67,8 @@ where #[cfg(test)] mod tests { + use std::convert::Infallible; + use actix_rt::pin; use actix_utils::future::poll_fn; use futures_util::stream; @@ -77,7 +80,11 @@ mod tests { async fn skips_empty_chunks() { let body = SizedStream::new( 2, - stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))), + stream::iter( + ["1", "", "2"] + .iter() + .map(|&v| Ok::<_, Infallible>(Bytes::from(v))), + ), ); pin!(body); @@ -103,7 +110,11 @@ mod tests { async fn read_to_bytes() { let body = SizedStream::new( 2, - stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))), + stream::iter( + ["1", "", "2"] + .iter() + .map(|&v| Ok::<_, Infallible>(Bytes::from(v))), + ), ); assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12"))); diff --git a/actix-http/src/builder.rs b/actix-http/src/builder.rs index 660cd9817..4e68dc920 100644 --- a/actix-http/src/builder.rs +++ b/actix-http/src/builder.rs @@ -1,19 +1,16 @@ -use std::marker::PhantomData; -use std::rc::Rc; -use std::{fmt, net}; +use std::{error::Error as StdError, fmt, marker::PhantomData, net, rc::Rc}; use actix_codec::Framed; use actix_service::{IntoServiceFactory, Service, ServiceFactory}; -use crate::body::MessageBody; -use crate::config::{KeepAlive, ServiceConfig}; -use crate::error::Error; -use crate::h1::{Codec, ExpectHandler, H1Service, UpgradeHandler}; -use crate::h2::H2Service; -use crate::request::Request; -use crate::response::Response; -use crate::service::HttpService; -use crate::{ConnectCallback, Extensions}; +use crate::{ + body::{AnyBody, MessageBody}, + config::{KeepAlive, ServiceConfig}, + h1::{self, ExpectHandler, H1Service, UpgradeHandler}, + h2::H2Service, + service::HttpService, + ConnectCallback, Extensions, Request, Response, +}; /// A HTTP service builder /// @@ -34,7 +31,7 @@ pub struct HttpServiceBuilder { impl HttpServiceBuilder where S: ServiceFactory, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, >::Future: 'static, { @@ -57,13 +54,13 @@ where impl HttpServiceBuilder where S: ServiceFactory, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, >::Future: 'static, X: ServiceFactory, - X::Error: Into, + X::Error: Into>, X::InitError: fmt::Debug, - U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, + U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U::Error: fmt::Display, U::InitError: fmt::Debug, { @@ -123,7 +120,7 @@ where where F: IntoServiceFactory, X1: ServiceFactory, - X1::Error: Into, + X1::Error: Into>, X1::InitError: fmt::Debug, { HttpServiceBuilder { @@ -145,8 +142,8 @@ where /// and this service get called with original request and framed object. pub fn upgrade(self, upgrade: F) -> HttpServiceBuilder where - F: IntoServiceFactory)>, - U1: ServiceFactory<(Request, Framed), Config = (), Response = ()>, + F: IntoServiceFactory)>, + U1: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U1::Error: fmt::Display, U1::InitError: fmt::Debug, { @@ -181,7 +178,7 @@ where where B: MessageBody, F: IntoServiceFactory, - S::Error: Into, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, { @@ -203,12 +200,12 @@ where pub fn h2(self, service: F) -> H2Service where F: IntoServiceFactory, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, { let cfg = ServiceConfig::new( self.keep_alive, @@ -226,12 +223,12 @@ where pub fn finish(self, service: F) -> HttpService where F: IntoServiceFactory, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, { let cfg = ServiceConfig::new( self.keep_alive, diff --git a/actix-http/src/client/connector.rs b/actix-http/src/client/connector.rs index 508fe748b..bd46919e8 100644 --- a/actix-http/src/client/connector.rs +++ b/actix-http/src/client/connector.rs @@ -85,7 +85,7 @@ impl Connector<()> { use bytes::{BufMut, BytesMut}; let mut alpn = BytesMut::with_capacity(20); - for proto in protocols.iter() { + for proto in &protocols { alpn.put_u8(proto.len() as u8); alpn.put(proto.as_slice()); } @@ -290,8 +290,7 @@ where let h2 = sock .ssl() .selected_alpn_protocol() - .map(|protos| protos.windows(2).any(|w| w == H2)) - .unwrap_or(false); + .map_or(false, |protos| protos.windows(2).any(|w| w == H2)); if h2 { (Box::new(sock), Protocol::Http2) } else { @@ -325,8 +324,7 @@ where .get_ref() .1 .get_alpn_protocol() - .map(|protos| protos.windows(2).any(|w| w == H2)) - .unwrap_or(false); + .map_or(false, |protos| protos.windows(2).any(|w| w == H2)); if h2 { (Box::new(sock), Protocol::Http2) } else { diff --git a/actix-http/src/client/error.rs b/actix-http/src/client/error.rs index d27363456..34833503b 100644 --- a/actix-http/src/client/error.rs +++ b/actix-http/src/client/error.rs @@ -1,15 +1,16 @@ -use std::io; +use std::{error::Error as StdError, fmt, io}; use derive_more::{Display, From}; #[cfg(feature = "openssl")] use actix_tls::accept::openssl::SslError; -use crate::error::{Error, ParseError, ResponseError}; -use crate::http::{Error as HttpError, StatusCode}; +use crate::error::{Error, ParseError}; +use crate::http::Error as HttpError; /// A set of errors that can occur while connecting to an HTTP host #[derive(Debug, Display, From)] +#[non_exhaustive] pub enum ConnectError { /// SSL feature is not enabled #[display(fmt = "SSL is not supported")] @@ -64,6 +65,7 @@ impl From for ConnectError { } #[derive(Debug, Display, From)] +#[non_exhaustive] pub enum InvalidUrl { #[display(fmt = "Missing URL scheme")] MissingScheme, @@ -82,6 +84,7 @@ impl std::error::Error for InvalidUrl {} /// A set of errors that can occur during request sending and response reading #[derive(Debug, Display, From)] +#[non_exhaustive] pub enum SendRequestError { /// Invalid URL #[display(fmt = "Invalid URL: {}", _0)] @@ -115,25 +118,17 @@ pub enum SendRequestError { /// Error sending request body Body(Error), + + /// Other errors that can occur after submitting a request. + #[display(fmt = "{:?}: {}", _1, _0)] + Custom(Box, Box), } impl std::error::Error for SendRequestError {} -/// Convert `SendRequestError` to a server `Response` -impl ResponseError for SendRequestError { - fn status_code(&self) -> StatusCode { - match *self { - SendRequestError::Connect(ConnectError::Timeout) => { - StatusCode::GATEWAY_TIMEOUT - } - SendRequestError::Connect(_) => StatusCode::BAD_REQUEST, - _ => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - /// A set of errors that can occur during freezing a request #[derive(Debug, Display, From)] +#[non_exhaustive] pub enum FreezeRequestError { /// Invalid URL #[display(fmt = "Invalid URL: {}", _0)] @@ -142,15 +137,20 @@ pub enum FreezeRequestError { /// HTTP error #[display(fmt = "{}", _0)] Http(HttpError), + + /// Other errors that can occur after submitting a request. + #[display(fmt = "{:?}: {}", _1, _0)] + Custom(Box, Box), } impl std::error::Error for FreezeRequestError {} impl From for SendRequestError { - fn from(e: FreezeRequestError) -> Self { - match e { - FreezeRequestError::Url(e) => e.into(), - FreezeRequestError::Http(e) => e.into(), + fn from(err: FreezeRequestError) -> Self { + match err { + FreezeRequestError::Url(err) => err.into(), + FreezeRequestError::Http(err) => err.into(), + FreezeRequestError::Custom(err, msg) => SendRequestError::Custom(err, msg), } } } diff --git a/actix-http/src/client/h2proto.rs b/actix-http/src/client/h2proto.rs index cf423ef12..b9d5f96bd 100644 --- a/actix-http/src/client/h2proto.rs +++ b/actix-http/src/client/h2proto.rs @@ -168,14 +168,13 @@ where if let Err(e) = send.send_data(bytes, false) { return Err(e.into()); - } else { - if !b.is_empty() { - send.reserve_capacity(b.len()); - } else { - buf = None; - } - continue; } + if !b.is_empty() { + send.reserve_capacity(b.len()); + } else { + buf = None; + } + continue; } Some(Err(e)) => return Err(e.into()), } diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index 9a2293e92..0e01e8748 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -152,8 +152,8 @@ impl ServiceConfig { } } - #[inline] /// Return keep-alive timer delay is configured. + #[inline] pub fn keep_alive_timer(&self) -> Option { self.keep_alive().map(|ka| sleep_until(self.now() + ka)) } @@ -365,11 +365,11 @@ mod tests { let clone3 = service.clone(); drop(clone1); - assert_eq!(false, notify_on_drop::is_dropped()); + assert!(!notify_on_drop::is_dropped()); drop(clone2); - assert_eq!(false, notify_on_drop::is_dropped()); + assert!(!notify_on_drop::is_dropped()); drop(clone3); - assert_eq!(false, notify_on_drop::is_dropped()); + assert!(!notify_on_drop::is_dropped()); drop(service); assert!(notify_on_drop::is_dropped()); diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index f0abae865..d3e304836 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -8,11 +8,18 @@ use std::{ }; use actix_rt::task::{spawn_blocking, JoinHandle}; -use brotli2::write::BrotliDecoder; use bytes::Bytes; -use flate2::write::{GzDecoder, ZlibDecoder}; use futures_core::{ready, Stream}; +#[cfg(feature = "compress-brotli")] +use brotli2::write::BrotliDecoder; + +#[cfg(feature = "compress-gzip")] +use flate2::write::{GzDecoder, ZlibDecoder}; + +#[cfg(feature = "compress-zstd")] +use zstd::stream::write::Decoder as ZstdDecoder; + use crate::{ encoding::Writer, error::{BlockingError, PayloadError}, @@ -36,15 +43,25 @@ where #[inline] pub fn new(stream: S, encoding: ContentEncoding) -> Decoder { let decoder = match encoding { + #[cfg(feature = "compress-brotli")] ContentEncoding::Br => Some(ContentDecoder::Br(Box::new( BrotliDecoder::new(Writer::new()), ))), + #[cfg(feature = "compress-gzip")] ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( ZlibDecoder::new(Writer::new()), ))), + #[cfg(feature = "compress-gzip")] ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new( GzDecoder::new(Writer::new()), ))), + #[cfg(feature = "compress-zstd")] + ContentEncoding::Zstd => Some(ContentDecoder::Zstd(Box::new( + ZstdDecoder::new(Writer::new()).expect( + "Failed to create zstd decoder. This is a bug. \ + Please report it to the actix-web repository.", + ), + ))), _ => None, }; @@ -141,14 +158,22 @@ where } enum ContentDecoder { + #[cfg(feature = "compress-gzip")] Deflate(Box>), + #[cfg(feature = "compress-gzip")] Gzip(Box>), + #[cfg(feature = "compress-brotli")] Br(Box>), + // We need explicit 'static lifetime here because ZstdDecoder need lifetime + // argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static` + #[cfg(feature = "compress-zstd")] + Zstd(Box>), } impl ContentDecoder { fn feed_eof(&mut self) -> io::Result> { match self { + #[cfg(feature = "compress-brotli")] ContentDecoder::Br(ref mut decoder) => match decoder.flush() { Ok(()) => { let b = decoder.get_mut().take(); @@ -162,6 +187,7 @@ impl ContentDecoder { Err(e) => Err(e), }, + #[cfg(feature = "compress-gzip")] ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() { Ok(_) => { let b = decoder.get_mut().take(); @@ -175,6 +201,7 @@ impl ContentDecoder { Err(e) => Err(e), }, + #[cfg(feature = "compress-gzip")] ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() { Ok(_) => { let b = decoder.get_mut().take(); @@ -186,11 +213,25 @@ impl ContentDecoder { } Err(e) => Err(e), }, + + #[cfg(feature = "compress-zstd")] + ContentDecoder::Zstd(ref mut decoder) => match decoder.flush() { + Ok(_) => { + let b = decoder.get_mut().take(); + if !b.is_empty() { + Ok(Some(b)) + } else { + Ok(None) + } + } + Err(e) => Err(e), + }, } } fn feed_data(&mut self, data: Bytes) -> io::Result> { match self { + #[cfg(feature = "compress-brotli")] ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; @@ -205,6 +246,7 @@ impl ContentDecoder { Err(e) => Err(e), }, + #[cfg(feature = "compress-gzip")] ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; @@ -219,6 +261,7 @@ impl ContentDecoder { Err(e) => Err(e), }, + #[cfg(feature = "compress-gzip")] ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; @@ -232,6 +275,21 @@ impl ContentDecoder { } Err(e) => Err(e), }, + + #[cfg(feature = "compress-zstd")] + ContentDecoder::Zstd(ref mut decoder) => match decoder.write_all(&data) { + Ok(_) => { + decoder.flush()?; + + let b = decoder.get_mut().take(); + if !b.is_empty() { + Ok(Some(b)) + } else { + Ok(None) + } + } + Err(e) => Err(e), + }, } } } diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index b8bc8b68d..1e69990a0 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -9,13 +9,20 @@ use std::{ }; use actix_rt::task::{spawn_blocking, JoinHandle}; -use brotli2::write::BrotliEncoder; use bytes::Bytes; use derive_more::Display; -use flate2::write::{GzEncoder, ZlibEncoder}; use futures_core::ready; use pin_project::pin_project; +#[cfg(feature = "compress-brotli")] +use brotli2::write::BrotliEncoder; + +#[cfg(feature = "compress-gzip")] +use flate2::write::{GzEncoder, ZlibEncoder}; + +#[cfg(feature = "compress-zstd")] +use zstd::stream::write::Encoder as ZstdEncoder; + use crate::{ body::{Body, BodySize, BoxAnyBody, MessageBody, ResponseBody}, http::{ @@ -132,9 +139,7 @@ where }, EncoderBodyProj::BoxedStream(ref mut b) => { match ready!(b.as_pin_mut().poll_next(cx)) { - Some(Err(err)) => { - Poll::Ready(Some(Err(EncoderError::Boxed(err.into())))) - } + Some(Err(err)) => Poll::Ready(Some(Err(EncoderError::Boxed(err)))), Some(Ok(val)) => Poll::Ready(Some(Ok(val))), None => Poll::Ready(None), } @@ -234,25 +239,40 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { } enum ContentEncoder { + #[cfg(feature = "compress-gzip")] Deflate(ZlibEncoder), + #[cfg(feature = "compress-gzip")] Gzip(GzEncoder), + #[cfg(feature = "compress-brotli")] Br(BrotliEncoder), + // We need explicit 'static lifetime here because ZstdEncoder need lifetime + // argument, and we use `spawn_blocking` in `Encoder::poll_next` that require `FnOnce() -> R + Send + 'static` + #[cfg(feature = "compress-zstd")] + Zstd(ZstdEncoder<'static, Writer>), } impl ContentEncoder { fn encoder(encoding: ContentEncoding) -> Option { match encoding { + #[cfg(feature = "compress-gzip")] ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new( Writer::new(), flate2::Compression::fast(), ))), + #[cfg(feature = "compress-gzip")] ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new( Writer::new(), flate2::Compression::fast(), ))), + #[cfg(feature = "compress-brotli")] ContentEncoding::Br => { Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) } + #[cfg(feature = "compress-zstd")] + ContentEncoding::Zstd => { + let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?; + Some(ContentEncoder::Zstd(encoder)) + } _ => None, } } @@ -260,31 +280,45 @@ impl ContentEncoder { #[inline] pub(crate) fn take(&mut self) -> Bytes { match *self { + #[cfg(feature = "compress-brotli")] ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), + #[cfg(feature = "compress-gzip")] ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), + #[cfg(feature = "compress-gzip")] ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), + #[cfg(feature = "compress-zstd")] + ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(), } } fn finish(self) -> Result { match self { + #[cfg(feature = "compress-brotli")] ContentEncoder::Br(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, + #[cfg(feature = "compress-gzip")] ContentEncoder::Gzip(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, + #[cfg(feature = "compress-gzip")] ContentEncoder::Deflate(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, + #[cfg(feature = "compress-zstd")] + ContentEncoder::Zstd(encoder) => match encoder.finish() { + Ok(writer) => Ok(writer.buf.freeze()), + Err(err) => Err(err), + }, } } fn write(&mut self, data: &[u8]) -> Result<(), io::Error> { match *self { + #[cfg(feature = "compress-brotli")] ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), Err(err) => { @@ -292,6 +326,7 @@ impl ContentEncoder { Err(err) } }, + #[cfg(feature = "compress-gzip")] ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), Err(err) => { @@ -299,6 +334,7 @@ impl ContentEncoder { Err(err) } }, + #[cfg(feature = "compress-gzip")] ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), Err(err) => { @@ -306,6 +342,14 @@ impl ContentEncoder { Err(err) } }, + #[cfg(feature = "compress-zstd")] + ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) { + Ok(_) => Ok(()), + Err(err) => { + trace!("Error decoding ztsd encoding: {}", err); + Err(err) + } + }, } } } @@ -317,7 +361,7 @@ pub enum EncoderError { Body(E), #[display(fmt = "boxed")] - Boxed(Error), + Boxed(Box), #[display(fmt = "blocking")] Blocking(BlockingError), @@ -326,19 +370,19 @@ pub enum EncoderError { Io(io::Error), } -impl StdError for EncoderError { +impl StdError for EncoderError { fn source(&self) -> Option<&(dyn StdError + 'static)> { - None - } -} - -impl> From> for Error { - fn from(err: EncoderError) -> Self { - match err { - EncoderError::Body(err) => err.into(), - EncoderError::Boxed(err) => err, - EncoderError::Blocking(err) => err.into(), - EncoderError::Io(err) => err.into(), + match self { + EncoderError::Body(err) => Some(err), + EncoderError::Boxed(err) => Some(&**err), + EncoderError::Blocking(err) => Some(err), + EncoderError::Io(err) => Some(err), } } } + +impl From> for crate::Error { + fn from(err: EncoderError) -> Self { + crate::Error::new_encoder().with_cause(err) + } +} diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 92efd572d..6c3d692c3 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -1,174 +1,155 @@ //! Error and Result module -use std::{ - error::Error as StdError, - fmt, - io::{self, Write as _}, - str::Utf8Error, - string::FromUtf8Error, -}; +use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Error}; -use bytes::BytesMut; use derive_more::{Display, Error, From}; -use http::{header, uri::InvalidUri, StatusCode}; -use serde::de::value::Error as DeError; +use http::{uri::InvalidUri, StatusCode}; -use crate::{body::Body, helpers::Writer, Response}; +use crate::{ + body::{AnyBody, Body}, + ws, Response, +}; pub use http::Error as HttpError; -/// General purpose actix web error. -/// -/// An actix web error is used to carry errors from `std::error` -/// through actix in a convenient way. It can be created through -/// converting errors with `into()`. -/// -/// Whenever it is created from an external object a response error is created -/// for it that can be used to create an HTTP response from it this means that -/// if you have access to an actix `Error` you can always get a -/// `ResponseError` reference from it. pub struct Error { - cause: Box, + inner: Box, +} + +pub(crate) struct ErrorInner { + #[allow(dead_code)] + kind: Kind, + cause: Option>, } impl Error { - /// Returns the reference to the underlying `ResponseError`. - pub fn as_response_error(&self) -> &dyn ResponseError { - self.cause.as_ref() + fn new(kind: Kind) -> Self { + Self { + inner: Box::new(ErrorInner { kind, cause: None }), + } } - /// Similar to `as_response_error` but downcasts. - pub fn as_error(&self) -> Option<&T> { - ::downcast_ref(self.cause.as_ref()) + pub(crate) fn new_http() -> Self { + Self::new(Kind::Http) + } + + pub(crate) fn new_parse() -> Self { + Self::new(Kind::Parse) + } + + pub(crate) fn new_payload() -> Self { + Self::new(Kind::Payload) + } + + pub(crate) fn new_body() -> Self { + Self::new(Kind::Body) + } + + pub(crate) fn new_send_response() -> Self { + Self::new(Kind::SendResponse) + } + + // TODO: remove allow + #[allow(dead_code)] + pub(crate) fn new_io() -> Self { + Self::new(Kind::Io) + } + + pub(crate) fn new_encoder() -> Self { + Self::new(Kind::Encoder) + } + + pub(crate) fn new_ws() -> Self { + Self::new(Kind::Ws) + } + + pub(crate) fn with_cause(mut self, cause: impl Into>) -> Self { + self.inner.cause = Some(cause.into()); + self } } -/// Errors that can generate responses. -pub trait ResponseError: fmt::Debug + fmt::Display { - /// Returns appropriate status code for error. - /// - /// A 500 Internal Server Error is used by default. If [error_response](Self::error_response) is - /// also implemented and does not call `self.status_code()`, then this will not be used. - fn status_code(&self) -> StatusCode { - StatusCode::INTERNAL_SERVER_ERROR - } +impl From for Response { + fn from(err: Error) -> Self { + let status_code = match err.inner.kind { + Kind::Parse => StatusCode::BAD_REQUEST, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }; - /// Creates full response for error. - /// - /// By default, the generated response uses a 500 Internal Server Error status code, a - /// `Content-Type` of `text/plain`, and the body is set to `Self`'s `Display` impl. - fn error_response(&self) -> Response { - let mut resp = Response::new(self.status_code()); - let mut buf = BytesMut::new(); - let _ = write!(Writer(&mut buf), "{}", self); - resp.headers_mut().insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("text/plain; charset=utf-8"), - ); - resp.set_body(Body::from(buf)) + Response::new(status_code).set_body(Body::from(err.to_string())) } - - downcast_get_type_id!(); } -downcast!(ResponseError); +#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)] +pub enum Kind { + #[display(fmt = "error processing HTTP")] + Http, -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.cause, f) - } + #[display(fmt = "error parsing HTTP message")] + Parse, + + #[display(fmt = "request payload read error")] + Payload, + + #[display(fmt = "response body write error")] + Body, + + #[display(fmt = "send response error")] + SendResponse, + + #[display(fmt = "error in WebSocket process")] + Ws, + + #[display(fmt = "connection error")] + Io, + + #[display(fmt = "encoder error")] + Encoder, } impl fmt::Debug for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", &self.cause) + // TODO: more detail + f.write_str("actix_http::Error") } } -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - None +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.inner.cause.as_ref() { + Some(err) => write!(f, "{}: {}", &self.inner.kind, err), + None => write!(f, "{}", &self.inner.kind), + } } } -impl From<()> for Error { - fn from(_: ()) -> Self { - Error::from(UnitError) +impl StdError for Error { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + self.inner.cause.as_ref().map(Box::as_ref) } } impl From for Error { - fn from(_: std::convert::Infallible) -> Self { - // hint that an error that will never happen - unreachable!() + fn from(err: std::convert::Infallible) -> Self { + match err {} } } -/// Convert `Error` to a `Response` instance -impl From for Response { - fn from(err: Error) -> Self { - Response::from_error(err) +impl From for Error { + fn from(err: ws::ProtocolError) -> Self { + Self::new_ws().with_cause(err) } } -/// `Error` for any error that implements `ResponseError` -impl From for Error { - fn from(err: T) -> Error { - Error { - cause: Box::new(err), - } +impl From for Error { + fn from(err: HttpError) -> Self { + Self::new_http().with_cause(err) } } -#[derive(Debug, Display, Error)] -#[display(fmt = "Unknown Error")] -struct UnitError; - -impl ResponseError for Box {} - -/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`UnitError`]. -impl ResponseError for UnitError {} - -/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`actix_tls::accept::openssl::SslError`]. -#[cfg(feature = "openssl")] -impl ResponseError for actix_tls::accept::openssl::SslError {} - -/// Returns [`StatusCode::BAD_REQUEST`] for [`DeError`]. -impl ResponseError for DeError { - fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST - } -} - -/// Returns [`StatusCode::BAD_REQUEST`] for [`Utf8Error`]. -impl ResponseError for Utf8Error { - fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST - } -} - -/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`HttpError`]. -impl ResponseError for HttpError {} - -/// Inspects the underlying [`io::ErrorKind`] and returns an appropriate status code. -/// -/// If the error is [`io::ErrorKind::NotFound`], [`StatusCode::NOT_FOUND`] is returned. If the -/// error is [`io::ErrorKind::PermissionDenied`], [`StatusCode::FORBIDDEN`] is returned. Otherwise, -/// [`StatusCode::INTERNAL_SERVER_ERROR`] is returned. -impl ResponseError for io::Error { - fn status_code(&self) -> StatusCode { - match self.kind() { - io::ErrorKind::NotFound => StatusCode::NOT_FOUND, - io::ErrorKind::PermissionDenied => StatusCode::FORBIDDEN, - _ => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -/// Returns [`StatusCode::BAD_REQUEST`] for [`header::InvalidHeaderValue`]. -impl ResponseError for header::InvalidHeaderValue { - fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST +impl From for Error { + fn from(err: ws::HandshakeError) -> Self { + Self::new_ws().with_cause(err) } } @@ -218,13 +199,6 @@ pub enum ParseError { Utf8(Utf8Error), } -/// Return `BadRequest` for `ParseError` -impl ResponseError for ParseError { - fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST - } -} - impl From for ParseError { fn from(err: io::Error) -> ParseError { ParseError::Io(err) @@ -263,14 +237,23 @@ impl From for ParseError { } } +impl From for Error { + fn from(err: ParseError) -> Self { + Self::new_parse().with_cause(err) + } +} + +impl From for Response { + fn from(err: ParseError) -> Self { + Error::from(err).into() + } +} + /// A set of errors that can occur running blocking tasks in thread pool. #[derive(Debug, Display, Error)] #[display(fmt = "Blocking thread pool is gone")] pub struct BlockingError; -/// `InternalServerError` for `BlockingError` -impl ResponseError for BlockingError {} - /// A set of errors that can occur during payload parsing. #[derive(Debug, Display)] #[non_exhaustive] @@ -344,16 +327,9 @@ impl From for PayloadError { } } -/// `PayloadError` returns two possible results: -/// -/// - `Overflow` returns `PayloadTooLarge` -/// - Other errors returns `BadRequest` -impl ResponseError for PayloadError { - fn status_code(&self) -> StatusCode { - match *self { - PayloadError::Overflow => StatusCode::PAYLOAD_TOO_LARGE, - _ => StatusCode::BAD_REQUEST, - } +impl From for Error { + fn from(err: PayloadError) -> Self { + Self::new_payload().with_cause(err) } } @@ -362,13 +338,19 @@ impl ResponseError for PayloadError { #[non_exhaustive] pub enum DispatchError { /// Service error - Service(Error), + // FIXME: display and error type + #[display(fmt = "Service Error")] + Service(#[error(not(source))] Response), + + /// Body error + // FIXME: display and error type + #[display(fmt = "Body Error")] + Body(#[error(not(source))] Box), /// Upgrade service error Upgrade, - /// An `io::Error` that occurred while trying to read or write to a network - /// stream. + /// An `io::Error` that occurred while trying to read or write to a network stream. #[display(fmt = "IO error: {}", _0)] Io(io::Error), @@ -434,12 +416,6 @@ mod content_type_test_impls { } } -impl ResponseError for ContentTypeError { - fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST - } -} - #[cfg(test)] mod tests { use super::*; @@ -448,42 +424,36 @@ mod tests { #[test] fn test_into_response() { - let resp: Response = ParseError::Incomplete.error_response(); + let resp: Response = ParseError::Incomplete.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into(); - let resp: Response = err.error_response(); + let resp: Response = Error::new_http().with_cause(err).into(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } #[test] fn test_as_response() { let orig = io::Error::new(io::ErrorKind::Other, "other"); - let e: Error = ParseError::Io(orig).into(); - assert_eq!(format!("{}", e.as_response_error()), "IO error: other"); - } - - #[test] - fn test_error_cause() { - let orig = io::Error::new(io::ErrorKind::Other, "other"); - let desc = orig.to_string(); - let e = Error::from(orig); - assert_eq!(format!("{}", e.as_response_error()), desc); + let err: Error = ParseError::Io(orig).into(); + assert_eq!( + format!("{}", err), + "error parsing HTTP message: IO error: other" + ); } #[test] fn test_error_display() { let orig = io::Error::new(io::ErrorKind::Other, "other"); - let desc = orig.to_string(); - let e = Error::from(orig); - assert_eq!(format!("{}", e), desc); + let err = Error::new_io().with_cause(orig); + assert_eq!("connection error: other", err.to_string()); } #[test] fn test_error_http_response() { let orig = io::Error::new(io::ErrorKind::Other, "other"); - let e = Error::from(orig); - let resp: Response = e.into(); + let err = Error::new_io().with_cause(orig); + let resp: Response = err.into(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } @@ -535,14 +505,4 @@ mod tests { from!(httparse::Error::TooManyHeaders => ParseError::TooLarge); from!(httparse::Error::Version => ParseError::Version); } - - #[test] - fn test_error_casting() { - let err = PayloadError::Overflow; - let resp_err: &dyn ResponseError = &err; - let err = resp_err.downcast_ref::().unwrap(); - assert_eq!(err.to_string(), "Payload reached size limit."); - let not_err = resp_err.downcast_ref::(); - assert!(not_err.is_none()); - } } diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index 8aba9f623..f240710c2 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -102,7 +102,7 @@ pub(crate) trait MessageType: Sized { } // transfer-encoding header::TRANSFER_ENCODING => { - if let Ok(s) = value.to_str().map(|s| s.trim()) { + if let Ok(s) = value.to_str().map(str::trim) { chunked = s.eq_ignore_ascii_case("chunked"); } else { return Err(ParseError::Header); @@ -110,7 +110,7 @@ pub(crate) trait MessageType: Sized { } // connection keep-alive state header::CONNECTION => { - ka = if let Ok(conn) = value.to_str().map(|conn| conn.trim()) { + ka = if let Ok(conn) = value.to_str().map(str::trim) { if conn.eq_ignore_ascii_case("keep-alive") { Some(ConnectionType::KeepAlive) } else if conn.eq_ignore_ascii_case("close") { @@ -125,7 +125,7 @@ pub(crate) trait MessageType: Sized { }; } header::UPGRADE => { - if let Ok(val) = value.to_str().map(|val| val.trim()) { + if let Ok(val) = value.to_str().map(str::trim) { if val.eq_ignore_ascii_case("websocket") { has_upgrade_websocket = true; } diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index c81d0b3bc..deb25763c 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -1,5 +1,6 @@ use std::{ collections::VecDeque, + error::Error as StdError, fmt, future::Future, io, mem, net, @@ -17,19 +18,19 @@ use futures_core::ready; use log::{error, trace}; use pin_project::pin_project; -use crate::body::{Body, BodySize, MessageBody}; -use crate::config::ServiceConfig; -use crate::error::{DispatchError, Error}; -use crate::error::{ParseError, PayloadError}; -use crate::http::StatusCode; -use crate::request::Request; -use crate::response::Response; -use crate::service::HttpFlow; -use crate::OnConnectData; +use crate::{ + body::{AnyBody, BodySize, MessageBody}, + config::ServiceConfig, + error::{DispatchError, ParseError, PayloadError}, + service::HttpFlow, + OnConnectData, Request, Response, StatusCode, +}; -use super::codec::Codec; -use super::payload::{Payload, PayloadSender, PayloadStatus}; -use super::{Message, MessageType}; +use super::{ + codec::Codec, + payload::{Payload, PayloadSender, PayloadStatus}, + Message, MessageType, +}; const LW_BUFFER_SIZE: usize = 1024; const HW_BUFFER_SIZE: usize = 1024 * 8; @@ -50,13 +51,13 @@ bitflags! { pub struct Dispatcher where S: Service, - S::Error: Into, + S::Error: Into>, B: MessageBody, - B::Error: Into, + B::Error: Into>, X: Service, - X::Error: Into, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -72,13 +73,13 @@ where enum DispatcherState where S: Service, - S::Error: Into, + S::Error: Into>, B: MessageBody, - B::Error: Into, + B::Error: Into>, X: Service, - X::Error: Into, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -91,13 +92,13 @@ where struct InnerDispatcher where S: Service, - S::Error: Into, + S::Error: Into>, B: MessageBody, - B::Error: Into, + B::Error: Into>, X: Service, - X::Error: Into, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -136,13 +137,13 @@ where X: Service, B: MessageBody, - B::Error: Into, + B::Error: Into>, { None, ExpectCall(#[pin] X::Future), ServiceCall(#[pin] S::Future), SendPayload(#[pin] B), - SendErrorPayload(#[pin] Body), + SendErrorPayload(#[pin] AnyBody), } impl State @@ -152,7 +153,7 @@ where X: Service, B: MessageBody, - B::Error: Into, + B::Error: Into>, { fn is_empty(&self) -> bool { matches!(self, State::None) @@ -170,14 +171,14 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into, + S::Error: Into>, S::Response: Into>, B: MessageBody, - B::Error: Into, + B::Error: Into>, X: Service, - X::Error: Into, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -231,14 +232,14 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into, + S::Error: Into>, S::Response: Into>, B: MessageBody, - B::Error: Into, + B::Error: Into>, X: Service, - X::Error: Into, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -334,7 +335,7 @@ where fn send_error_response( mut self: Pin<&mut Self>, message: Response<()>, - body: Body, + body: AnyBody, ) -> Result<(), DispatchError> { let size = self.as_mut().send_response_inner(message, &body)?; let state = match size { @@ -379,7 +380,7 @@ where // send_response would update InnerDispatcher state to SendPayload or // None(If response body is empty). // continue loop to poll it. - self.as_mut().send_error_response(res, Body::Empty)?; + self.as_mut().send_error_response(res, AnyBody::Empty)?; } // return with upgrade request and poll it exclusively. @@ -399,7 +400,7 @@ where // send service call error as response Poll::Ready(Err(err)) => { - let res = Response::from_error(err); + let res: Response = err.into(); let (res, body) = res.replace_body(()); self.as_mut().send_error_response(res, body)?; } @@ -438,7 +439,7 @@ where } Poll::Ready(Some(Err(err))) => { - return Err(DispatchError::Service(err.into())) + return Err(DispatchError::Body(err.into())) } Poll::Pending => return Ok(PollResponse::DoNothing), @@ -473,7 +474,7 @@ where } Poll::Ready(Some(Err(err))) => { - return Err(DispatchError::Service(err)) + return Err(DispatchError::Service(err.into())) } Poll::Pending => return Ok(PollResponse::DoNothing), @@ -496,7 +497,7 @@ where // send expect error as response Poll::Ready(Err(err)) => { - let res = Response::from_error(err); + let res: Response = err.into(); let (res, body) = res.replace_body(()); self.as_mut().send_error_response(res, body)?; } @@ -514,14 +515,13 @@ where cx: &mut Context<'_>, ) -> Result<(), DispatchError> { // Handle `EXPECT: 100-Continue` header + let mut this = self.as_mut().project(); if req.head().expect() { // set dispatcher state so the future is pinned. - let mut this = self.as_mut().project(); let task = this.flow.expect.call(req); this.state.set(State::ExpectCall(task)); } else { // the same as above. - let mut this = self.as_mut().project(); let task = this.flow.service.call(req); this.state.set(State::ServiceCall(task)); }; @@ -546,7 +546,7 @@ where // to notify the dispatcher a new state is set and the outer loop // should be continue. Poll::Ready(Err(err)) => { - let res = Response::from_error(err); + let res: Response = err.into(); let (res, body) = res.replace_body(()); return self.send_error_response(res, body); } @@ -566,7 +566,7 @@ where Poll::Pending => Ok(()), // see the comment on ExpectCall state branch's Ready(Err(err)). Poll::Ready(Err(err)) => { - let res = Response::from_error(err); + let res: Response = err.into(); let (res, body) = res.replace_body(()); self.send_error_response(res, body) } @@ -772,7 +772,7 @@ where trace!("Slow request timeout"); let _ = self.as_mut().send_error_response( Response::with_body(StatusCode::REQUEST_TIMEOUT, ()), - Body::Empty, + AnyBody::Empty, ); this = self.project(); this.flags.insert(Flags::STARTED | Flags::SHUTDOWN); @@ -909,14 +909,14 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into, + S::Error: Into>, S::Response: Into>, B: MessageBody, - B::Error: Into, + B::Error: Into>, X: Service, - X::Error: Into, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -1067,16 +1067,17 @@ mod tests { } } - fn ok_service() -> impl Service, Error = Error> { + fn ok_service() -> impl Service, Error = Error> + { fn_service(|_req: Request| ready(Ok::<_, Error>(Response::ok()))) } fn echo_path_service( - ) -> impl Service, Error = Error> { + ) -> impl Service, Error = Error> { fn_service(|req: Request| { let path = req.path().as_bytes(); ready(Ok::<_, Error>( - Response::ok().set_body(Body::from_slice(path)), + Response::ok().set_body(AnyBody::from_slice(path)), )) }) } diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index eaabcb687..254981123 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -6,14 +6,15 @@ use std::{cmp, io}; use bytes::{BufMut, BytesMut}; -use crate::body::BodySize; -use crate::config::ServiceConfig; -use crate::header::{map::Value, HeaderName}; -use crate::helpers; -use crate::http::header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; -use crate::http::{HeaderMap, StatusCode, Version}; -use crate::message::{ConnectionType, RequestHeadType}; -use crate::response::Response; +use crate::{ + body::BodySize, + config::ServiceConfig, + header::{map::Value, HeaderMap, HeaderName}, + header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}, + helpers, + message::{ConnectionType, RequestHeadType}, + Response, StatusCode, Version, +}; const AVERAGE_HEADER_SIZE: usize = 30; @@ -287,7 +288,7 @@ impl MessageType for RequestHeadType { let head = self.as_ref(); dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE); write!( - helpers::Writer(dst), + helpers::MutWriter(dst), "{} {} {}", head.method, head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"), @@ -420,7 +421,7 @@ impl TransferEncoding { *eof = true; buf.extend_from_slice(b"0\r\n\r\n"); } else { - writeln!(helpers::Writer(buf), "{:X}\r", msg.len()) + writeln!(helpers::MutWriter(buf), "{:X}\r", msg.len()) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; buf.reserve(msg.len() + 2); diff --git a/actix-http/src/h1/payload.rs b/actix-http/src/h1/payload.rs index e72493fa2..cc771f28a 100644 --- a/actix-http/src/h1/payload.rs +++ b/actix-http/src/h1/payload.rs @@ -186,8 +186,7 @@ impl Inner { if self .task .as_ref() - .map(|w| !cx.waker().will_wake(w)) - .unwrap_or(true) + .map_or(true, |w| !cx.waker().will_wake(w)) { self.task = Some(cx.waker().clone()); } @@ -199,8 +198,7 @@ impl Inner { if self .io_task .as_ref() - .map(|w| !cx.waker().will_wake(w)) - .unwrap_or(true) + .map_or(true, |w| !cx.waker().will_wake(w)) { self.io_task = Some(cx.waker().clone()); } diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index 1ab85cbf3..dbad8cfac 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -1,7 +1,11 @@ -use std::marker::PhantomData; -use std::rc::Rc; -use std::task::{Context, Poll}; -use std::{fmt, net}; +use std::{ + error::Error as StdError, + fmt, + marker::PhantomData, + net, + rc::Rc, + task::{Context, Poll}, +}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_rt::net::TcpStream; @@ -11,17 +15,15 @@ use actix_service::{ use actix_utils::future::ready; use futures_core::future::LocalBoxFuture; -use crate::body::MessageBody; -use crate::config::ServiceConfig; -use crate::error::{DispatchError, Error}; -use crate::request::Request; -use crate::response::Response; -use crate::service::HttpServiceHandler; -use crate::{ConnectCallback, OnConnectData}; +use crate::{ + body::{AnyBody, MessageBody}, + config::ServiceConfig, + error::DispatchError, + service::HttpServiceHandler, + ConnectCallback, OnConnectData, Request, Response, +}; -use super::codec::Codec; -use super::dispatcher::Dispatcher; -use super::{ExpectHandler, UpgradeHandler}; +use super::{codec::Codec, dispatcher::Dispatcher, ExpectHandler, UpgradeHandler}; /// `ServiceFactory` implementation for HTTP1 transport pub struct H1Service { @@ -36,7 +38,7 @@ pub struct H1Service { impl H1Service where S: ServiceFactory, - S::Error: Into, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, B: MessageBody, @@ -61,21 +63,21 @@ impl H1Service where S: ServiceFactory, S::Future: 'static, - S::Error: Into, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, B: MessageBody, - B::Error: Into, + B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U::Future: 'static, - U::Error: fmt::Display + Into, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create simple tcp stream service @@ -110,16 +112,16 @@ mod openssl { where S: ServiceFactory, S::Future: 'static, - S::Error: Into, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, B: MessageBody, - B::Error: Into, + B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory< @@ -128,7 +130,7 @@ mod openssl { Response = (), >, U::Future: 'static, - U::Error: fmt::Display + Into, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create openssl based service @@ -170,16 +172,16 @@ mod rustls { where S: ServiceFactory, S::Future: 'static, - S::Error: Into, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, B: MessageBody, - B::Error: Into, + B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory< @@ -188,7 +190,7 @@ mod rustls { Response = (), >, U::Future: 'static, - U::Error: fmt::Display + Into, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create rustls based service @@ -217,7 +219,7 @@ mod rustls { impl H1Service where S: ServiceFactory, - S::Error: Into, + S::Error: Into>, S::Response: Into>, S::InitError: fmt::Debug, B: MessageBody, @@ -225,7 +227,7 @@ where pub fn expect(self, expect: X1) -> H1Service where X1: ServiceFactory, - X1::Error: Into, + X1::Error: Into>, X1::InitError: fmt::Debug, { H1Service { @@ -268,21 +270,21 @@ where S: ServiceFactory, S::Future: 'static, - S::Error: Into, + S::Error: Into>, S::Response: Into>, S::InitError: fmt::Debug, B: MessageBody, - B::Error: Into, + B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U::Future: 'static, - U::Error: fmt::Display + Into, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { type Response = (); @@ -338,17 +340,17 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into, + S::Error: Into>, S::Response: Into>, B: MessageBody, - B::Error: Into, + B::Error: Into>, X: Service, - X::Error: Into, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display + Into, + U::Error: fmt::Display + Into>, { type Response = (); type Error = DispatchError; diff --git a/actix-http/src/h1/utils.rs b/actix-http/src/h1/utils.rs index 90e44daa4..523e652fd 100644 --- a/actix-http/src/h1/utils.rs +++ b/actix-http/src/h1/utils.rs @@ -81,7 +81,9 @@ where let _ = this.body.take(); } let framed = this.framed.as_mut().as_pin_mut().unwrap(); - framed.write(Message::Chunk(item))?; + framed.write(Message::Chunk(item)).map_err(|err| { + Error::new_send_response().with_cause(err) + })?; } Poll::Pending => body_ready = false, } @@ -92,7 +94,10 @@ where // flush write buffer if !framed.is_write_buf_empty() { - match framed.flush(cx)? { + match framed + .flush(cx) + .map_err(|err| Error::new_send_response().with_cause(err))? + { Poll::Ready(_) => { if body_ready { continue; @@ -106,7 +111,9 @@ where // send response if let Some(res) = this.res.take() { - framed.write(res)?; + framed + .write(res) + .map_err(|err| Error::new_send_response().with_cause(err))?; continue; } diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index baff20e51..ea149b1e0 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -1,5 +1,6 @@ use std::{ cmp, + error::Error as StdError, future::Future, marker::PhantomData, net, @@ -18,15 +19,12 @@ use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCOD use log::{error, trace}; use pin_project_lite::pin_project; -use crate::body::{BodySize, MessageBody}; -use crate::config::ServiceConfig; -use crate::error::Error; -use crate::message::ResponseHead; -use crate::payload::Payload; -use crate::request::Request; -use crate::response::Response; -use crate::service::HttpFlow; -use crate::OnConnectData; +use crate::{ + body::{AnyBody, BodySize, MessageBody}, + config::ServiceConfig, + service::HttpFlow, + OnConnectData, Payload, Request, Response, ResponseHead, +}; const CHUNK_SIZE: usize = 16_384; @@ -66,12 +64,12 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into, + S::Error: Into>, S::Future: 'static, S::Response: Into>, B: MessageBody, - B::Error: Into, + B::Error: Into>, { type Output = Result<(), crate::error::DispatchError>; @@ -106,7 +104,7 @@ where let res = match fut.await { Ok(res) => handle_response(res.into(), tx, config).await, Err(err) => { - let res = Response::from_error(err.into()); + let res: Response = err.into(); handle_response(res, tx, config).await } }; @@ -133,7 +131,7 @@ where enum DispatchError { SendResponse(h2::Error), SendData(h2::Error), - ResponseBody(Error), + ResponseBody(Box), } async fn handle_response( @@ -143,7 +141,7 @@ async fn handle_response( ) -> Result<(), DispatchError> where B: MessageBody, - B::Error: Into, + B::Error: Into>, { let (res, body) = res.replace_body(()); diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index a75abef7d..09e24045b 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -1,8 +1,12 @@ -use std::future::Future; -use std::marker::PhantomData; -use std::pin::Pin; -use std::task::{Context, Poll}; -use std::{net, rc::Rc}; +use std::{ + error::Error as StdError, + future::Future, + marker::PhantomData, + net, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; use actix_codec::{AsyncRead, AsyncWrite}; use actix_rt::net::TcpStream; @@ -13,16 +17,16 @@ use actix_service::{ use actix_utils::future::ready; use bytes::Bytes; use futures_core::{future::LocalBoxFuture, ready}; -use h2::server::{handshake, Handshake}; +use h2::server::{handshake as h2_handshake, Handshake as H2Handshake}; use log::error; -use crate::body::MessageBody; -use crate::config::ServiceConfig; -use crate::error::{DispatchError, Error}; -use crate::request::Request; -use crate::response::Response; -use crate::service::HttpFlow; -use crate::{ConnectCallback, OnConnectData}; +use crate::{ + body::{AnyBody, MessageBody}, + config::ServiceConfig, + error::DispatchError, + service::HttpFlow, + ConnectCallback, OnConnectData, Request, Response, +}; use super::dispatcher::Dispatcher; @@ -37,12 +41,12 @@ pub struct H2Service { impl H2Service where S: ServiceFactory, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, { /// Create new `H2Service` instance with config. pub(crate) fn with_config>( @@ -68,12 +72,12 @@ impl H2Service where S: ServiceFactory, S::Future: 'static, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, { /// Create plain TCP based service pub fn tcp( @@ -107,12 +111,12 @@ mod openssl { where S: ServiceFactory, S::Future: 'static, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, { /// Create OpenSSL based service pub fn openssl( @@ -153,12 +157,12 @@ mod rustls { where S: ServiceFactory, S::Future: 'static, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, { /// Create Rustls based service pub fn rustls( @@ -171,7 +175,8 @@ mod rustls { Error = TlsError, InitError = S::InitError, > { - let protos = vec!["h2".to_string().into()]; + let mut protos = vec![b"h2".to_vec()]; + protos.extend_from_slice(&config.alpn_protocols); config.set_protocols(&protos); Acceptor::new(config) @@ -196,12 +201,12 @@ where S: ServiceFactory, S::Future: 'static, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, { type Response = (); type Error = DispatchError; @@ -236,7 +241,7 @@ where impl H2ServiceHandler where S: Service, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, @@ -259,11 +264,11 @@ impl Service<(T, Option)> for H2ServiceHandler, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, { type Response = (); type Error = DispatchError; @@ -287,7 +292,7 @@ where Some(self.cfg.clone()), addr, on_connect_data, - handshake(io), + h2_handshake(io), ), } } @@ -304,7 +309,7 @@ where Option, Option, OnConnectData, - Handshake, + H2Handshake, ), } @@ -312,7 +317,7 @@ pub struct H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, @@ -324,11 +329,11 @@ impl Future for H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody, - B::Error: Into, + B::Error: Into>, { type Output = Result<(), DispatchError>; diff --git a/actix-http/src/header/as_name.rs b/actix-http/src/header/as_name.rs index af81ff7f2..5ce321566 100644 --- a/actix-http/src/header/as_name.rs +++ b/actix-http/src/header/as_name.rs @@ -8,40 +8,42 @@ use http::header::{HeaderName, InvalidHeaderName}; pub trait AsHeaderName: Sealed {} +pub struct Seal; + pub trait Sealed { - fn try_as_name(&self) -> Result, InvalidHeaderName>; + fn try_as_name(&self, seal: Seal) -> Result, InvalidHeaderName>; } impl Sealed for HeaderName { - fn try_as_name(&self) -> Result, InvalidHeaderName> { + fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { Ok(Cow::Borrowed(self)) } } impl AsHeaderName for HeaderName {} impl Sealed for &HeaderName { - fn try_as_name(&self) -> Result, InvalidHeaderName> { + fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { Ok(Cow::Borrowed(*self)) } } impl AsHeaderName for &HeaderName {} impl Sealed for &str { - fn try_as_name(&self) -> Result, InvalidHeaderName> { + fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { HeaderName::from_str(self).map(Cow::Owned) } } impl AsHeaderName for &str {} impl Sealed for String { - fn try_as_name(&self) -> Result, InvalidHeaderName> { + fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { HeaderName::from_str(self).map(Cow::Owned) } } impl AsHeaderName for String {} impl Sealed for &String { - fn try_as_name(&self) -> Result, InvalidHeaderName> { + fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { HeaderName::from_str(self).map(Cow::Owned) } } diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index 106e44edb..634d9282f 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -213,7 +213,7 @@ impl HeaderMap { } fn get_value(&self, key: impl AsHeaderName) -> Option<&Value> { - match key.try_as_name().ok()? { + match key.try_as_name(super::as_name::Seal).ok()? { Cow::Borrowed(name) => self.inner.get(name), Cow::Owned(name) => self.inner.get(&name), } @@ -249,7 +249,7 @@ impl HeaderMap { /// 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()) + self.get_value(key).map(Value::first) } /// Returns a mutable reference to the _first_ value associated a header name. @@ -279,9 +279,9 @@ impl HeaderMap { /// 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()), + match key.try_as_name(super::as_name::Seal).ok()? { + Cow::Borrowed(name) => self.inner.get_mut(name).map(Value::first_mut), + Cow::Owned(name) => self.inner.get_mut(&name).map(Value::first_mut), } } @@ -327,7 +327,7 @@ impl HeaderMap { /// assert!(map.contains_key(header::ACCEPT)); /// ``` pub fn contains_key(&self, key: impl AsHeaderName) -> bool { - match key.try_as_name() { + match key.try_as_name(super::as_name::Seal) { Ok(Cow::Borrowed(name)) => self.inner.contains_key(name), Ok(Cow::Owned(name)) => self.inner.contains_key(&name), Err(_) => false, @@ -410,7 +410,7 @@ impl HeaderMap { /// /// assert!(map.is_empty()); pub fn remove(&mut self, key: impl AsHeaderName) -> Removed { - let value = match key.try_as_name() { + let value = match key.try_as_name(super::as_name::Seal) { Ok(Cow::Borrowed(name)) => self.inner.remove(name), Ok(Cow::Owned(name)) => self.inner.remove(&name), Err(_) => None, diff --git a/actix-http/src/header/shared/content_encoding.rs b/actix-http/src/header/shared/content_encoding.rs index b93d66101..b9c1d2795 100644 --- a/actix-http/src/header/shared/content_encoding.rs +++ b/actix-http/src/header/shared/content_encoding.rs @@ -23,6 +23,9 @@ pub enum ContentEncoding { /// Gzip algorithm. Gzip, + // Zstd algorithm. + Zstd, + /// Indicates the identity function (i.e. no compression, nor modification). Identity, } @@ -41,6 +44,7 @@ impl ContentEncoding { ContentEncoding::Br => "br", ContentEncoding::Gzip => "gzip", ContentEncoding::Deflate => "deflate", + ContentEncoding::Zstd => "zstd", ContentEncoding::Identity | ContentEncoding::Auto => "identity", } } @@ -53,6 +57,7 @@ impl ContentEncoding { ContentEncoding::Gzip => 1.0, ContentEncoding::Deflate => 0.9, ContentEncoding::Identity | ContentEncoding::Auto => 0.1, + ContentEncoding::Zstd => 0.0, } } } @@ -81,6 +86,8 @@ impl From<&str> for ContentEncoding { ContentEncoding::Gzip } else if val.eq_ignore_ascii_case("deflate") { ContentEncoding::Deflate + } else if val.eq_ignore_ascii_case("zstd") { + ContentEncoding::Zstd } else { ContentEncoding::default() } diff --git a/actix-http/src/helpers.rs b/actix-http/src/helpers.rs index 34bb989f9..cba94d9b8 100644 --- a/actix-http/src/helpers.rs +++ b/actix-http/src/helpers.rs @@ -27,7 +27,9 @@ pub(crate) fn write_status_line(version: Version, n: u16, buf: &mut B buf.put_u8(b' '); } -/// NOTE: bytes object has to contain enough space +/// Write out content length header. +/// +/// Buffer must to contain enough space or be implicitly extendable. pub fn write_content_length(n: u64, buf: &mut B) { if n == 0 { buf.put_slice(b"\r\ncontent-length: 0\r\n"); @@ -41,11 +43,15 @@ pub fn write_content_length(n: u64, buf: &mut B) { buf.put_slice(b"\r\n"); } -// TODO: bench why this is needed vs Buf::writer -/// An `io` writer for a `BufMut` that should only be used once and on an empty buffer. -pub(crate) struct Writer<'a, B>(pub &'a mut B); +/// An `io::Write`r that only requires mutable reference and assumes that there is space available +/// in the buffer for every write operation or that it can be extended implicitly (like +/// `bytes::BytesMut`, for example). +/// +/// This is slightly faster (~10%) than `bytes::buf::Writer` in such cases because it does not +/// perform a remaining length check before writing. +pub(crate) struct MutWriter<'a, B>(pub(crate) &'a mut B); -impl<'a, B> io::Write for Writer<'a, B> +impl<'a, B> io::Write for MutWriter<'a, B> where B: BufMut, { diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 7c2c3b4e3..d22e1ee44 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -1,12 +1,14 @@ //! HTTP primitives for the Actix ecosystem. //! //! ## Crate Features -//! | Feature | Functionality | -//! | ---------------- | ----------------------------------------------------- | -//! | `openssl` | TLS support via [OpenSSL]. | -//! | `rustls` | TLS support via [rustls]. | -//! | `compress` | Payload compression support. (Deflate, Gzip & Brotli) | -//! | `trust-dns` | Use [trust-dns] as the client DNS resolver. | +//! | Feature | Functionality | +//! | ------------------- | ------------------------------------------- | +//! | `openssl` | TLS support via [OpenSSL]. | +//! | `rustls` | TLS support via [rustls]. | +//! | `compress-brotli` | Payload compression support: Brotli. | +//! | `compress-gzip` | Payload compression support: Deflate, Gzip. | +//! | `compress-zstd` | Payload compression support: Zstd. | +//! | `trust-dns` | Use [trust-dns] as the client DNS resolver. | //! //! [OpenSSL]: https://crates.io/crates/openssl //! [rustls]: https://crates.io/crates/rustls @@ -25,14 +27,12 @@ #[macro_use] extern crate log; -#[macro_use] -mod macros; - pub mod body; mod builder; pub mod client; mod config; -#[cfg(feature = "compress")] + +#[cfg(feature = "__compress")] pub mod encoding; mod extensions; pub mod header; @@ -54,7 +54,7 @@ pub mod ws; pub use self::builder::HttpServiceBuilder; pub use self::config::{KeepAlive, ServiceConfig}; -pub use self::error::{Error, ResponseError}; +pub use self::error::Error; pub use self::extensions::Extensions; pub use self::header::ContentEncoding; pub use self::http_message::HttpMessage; diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index 0a3f3a915..e85d686b7 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -152,15 +152,16 @@ impl RequestHead { /// Connection upgrade status pub fn upgrade(&self) -> bool { - if let Some(hdr) = self.headers().get(header::CONNECTION) { - if let Ok(s) = hdr.to_str() { - s.to_ascii_lowercase().contains("upgrade") - } else { - false - } - } else { - false - } + self.headers() + .get(header::CONNECTION) + .map(|hdr| { + if let Ok(s) = hdr.to_str() { + s.to_ascii_lowercase().contains("upgrade") + } else { + false + } + }) + .unwrap_or(false) } #[inline] @@ -308,13 +309,11 @@ impl ResponseHead { /// Get custom reason for the response #[inline] pub fn reason(&self) -> &str { - if let Some(reason) = self.reason { - reason - } else { + self.reason.unwrap_or_else(|| { self.status .canonical_reason() .unwrap_or("") - } + }) } #[inline] @@ -356,7 +355,7 @@ pub struct Message { impl Message { /// Get new message from the pool of objects pub fn new() -> Self { - T::with_pool(|p| p.get_message()) + T::with_pool(MessagePool::get_message) } } diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index bcfa65732..2aa38c153 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -8,7 +8,7 @@ use std::{ use bytes::{Bytes, BytesMut}; use crate::{ - body::{Body, MessageBody}, + body::{AnyBody, MessageBody}, error::Error, extensions::Extensions, http::{HeaderMap, StatusCode}, @@ -22,13 +22,13 @@ pub struct Response { pub(crate) body: B, } -impl Response { +impl Response { /// Constructs a new response with default body. #[inline] - pub fn new(status: StatusCode) -> Response { + pub fn new(status: StatusCode) -> Self { Response { head: BoxedResponseHead::new(status), - body: Body::Empty, + body: AnyBody::Empty, } } @@ -43,40 +43,29 @@ impl Response { /// Constructs a new response with status 200 OK. #[inline] - pub fn ok() -> Response { + pub fn ok() -> Self { Response::new(StatusCode::OK) } /// Constructs a new response with status 400 Bad Request. #[inline] - pub fn bad_request() -> Response { + pub fn bad_request() -> Self { Response::new(StatusCode::BAD_REQUEST) } /// Constructs a new response with status 404 Not Found. #[inline] - pub fn not_found() -> Response { + pub fn not_found() -> Self { Response::new(StatusCode::NOT_FOUND) } /// Constructs a new response with status 500 Internal Server Error. #[inline] - pub fn internal_server_error() -> Response { + pub fn internal_server_error() -> Self { Response::new(StatusCode::INTERNAL_SERVER_ERROR) } // end shortcuts - - /// Constructs a new response from an error. - #[inline] - pub fn from_error(error: impl Into) -> Response { - let error = error.into(); - let resp = error.as_response_error().error_response(); - if resp.head.status == StatusCode::INTERNAL_SERVER_ERROR { - debug!("Internal Server Error: {:?}", error); - } - resp - } } impl Response { @@ -209,7 +198,6 @@ impl Response { impl fmt::Debug for Response where B: MessageBody, - B::Error: Into, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let res = writeln!( @@ -235,7 +223,9 @@ impl Default for Response { } } -impl>, E: Into> From> for Response { +impl>, E: Into> From> + for Response +{ fn from(res: Result) -> Self { match res { Ok(val) => val.into(), @@ -244,13 +234,19 @@ impl>, E: Into> From> for Response for Response { +impl From for Response { fn from(mut builder: ResponseBuilder) -> Self { builder.finish() } } -impl From<&'static str> for Response { +impl From for Response { + fn from(val: std::convert::Infallible) -> Self { + match val {} + } +} + +impl From<&'static str> for Response { fn from(val: &'static str) -> Self { Response::build(StatusCode::OK) .content_type(mime::TEXT_PLAIN_UTF_8) @@ -258,7 +254,7 @@ impl From<&'static str> for Response { } } -impl From<&'static [u8]> for Response { +impl From<&'static [u8]> for Response { fn from(val: &'static [u8]) -> Self { Response::build(StatusCode::OK) .content_type(mime::APPLICATION_OCTET_STREAM) @@ -266,7 +262,7 @@ impl From<&'static [u8]> for Response { } } -impl From for Response { +impl From for Response { fn from(val: String) -> Self { Response::build(StatusCode::OK) .content_type(mime::TEXT_PLAIN_UTF_8) @@ -274,7 +270,7 @@ impl From for Response { } } -impl<'a> From<&'a String> for Response { +impl<'a> From<&'a String> for Response { fn from(val: &'a String) -> Self { Response::build(StatusCode::OK) .content_type(mime::TEXT_PLAIN_UTF_8) @@ -282,7 +278,7 @@ impl<'a> From<&'a String> for Response { } } -impl From for Response { +impl From for Response { fn from(val: Bytes) -> Self { Response::build(StatusCode::OK) .content_type(mime::APPLICATION_OCTET_STREAM) @@ -290,7 +286,7 @@ impl From for Response { } } -impl From for Response { +impl From for Response { fn from(val: BytesMut) -> Self { Response::build(StatusCode::OK) .content_type(mime::APPLICATION_OCTET_STREAM) @@ -301,7 +297,6 @@ impl From for Response { #[cfg(test)] mod tests { use super::*; - use crate::body::Body; use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; #[test] @@ -316,7 +311,7 @@ mod tests { #[test] fn test_into_response() { - let resp: Response = "test".into(); + let resp: Response = "test".into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), @@ -325,7 +320,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().get_ref(), b"test"); - let resp: Response = b"test".as_ref().into(); + let resp: Response = b"test".as_ref().into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), @@ -334,7 +329,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().get_ref(), b"test"); - let resp: Response = "test".to_owned().into(); + let resp: Response = "test".to_owned().into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), @@ -343,7 +338,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().get_ref(), b"test"); - let resp: Response = (&"test".to_owned()).into(); + let resp: Response = (&"test".to_owned()).into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), @@ -353,7 +348,7 @@ mod tests { assert_eq!(resp.body().get_ref(), b"test"); let b = Bytes::from_static(b"test"); - let resp: Response = b.into(); + let resp: Response = b.into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), @@ -363,7 +358,7 @@ mod tests { assert_eq!(resp.body().get_ref(), b"test"); let b = Bytes::from_static(b"test"); - let resp: Response = b.into(); + let resp: Response = b.into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), @@ -373,7 +368,7 @@ mod tests { assert_eq!(resp.body().get_ref(), b"test"); let b = BytesMut::from("test"); - let resp: Response = b.into(); + let resp: Response = b.into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), diff --git a/actix-http/src/response_builder.rs b/actix-http/src/response_builder.rs index df9079d70..e46d9a28c 100644 --- a/actix-http/src/response_builder.rs +++ b/actix-http/src/response_builder.rs @@ -2,6 +2,7 @@ use std::{ cell::{Ref, RefMut}, + error::Error as StdError, fmt, future::Future, pin::Pin, @@ -13,7 +14,7 @@ use bytes::Bytes; use futures_core::Stream; use crate::{ - body::{Body, BodyStream}, + body::{AnyBody, BodyStream}, error::{Error, HttpError}, header::{self, IntoHeaderPair, IntoHeaderValue}, message::{BoxedResponseHead, ConnectionType, ResponseHead}, @@ -235,9 +236,9 @@ impl ResponseBuilder { /// /// This `ResponseBuilder` will be left in a useless state. #[inline] - pub fn body>(&mut self, body: B) -> Response { + pub fn body>(&mut self, body: B) -> Response { self.message_body(body.into()) - .unwrap_or_else(Response::from_error) + .unwrap_or_else(Response::from) } /// Generate response with a body. @@ -245,7 +246,7 @@ impl ResponseBuilder { /// This `ResponseBuilder` will be left in a useless state. pub fn message_body(&mut self, body: B) -> Result, Error> { if let Some(err) = self.err.take() { - return Err(err.into()); + return Err(Error::new_http().with_cause(err)); } let head = self.head.take().expect("cannot reuse response builder"); @@ -256,20 +257,20 @@ impl ResponseBuilder { /// /// This `ResponseBuilder` will be left in a useless state. #[inline] - pub fn streaming(&mut self, stream: S) -> Response + pub fn streaming(&mut self, stream: S) -> Response where - S: Stream> + Unpin + 'static, - E: Into + 'static, + S: Stream> + 'static, + E: Into> + 'static, { - self.body(Body::from_message(BodyStream::new(stream))) + self.body(AnyBody::from_message(BodyStream::new(stream))) } /// Generate response with an empty body. /// /// This `ResponseBuilder` will be left in a useless state. #[inline] - pub fn finish(&mut self) -> Response { - self.body(Body::Empty) + pub fn finish(&mut self) -> Response { + self.body(AnyBody::Empty) } /// Create an owned `ResponseBuilder`, leaving the original in a useless state. @@ -327,7 +328,7 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder { } impl Future for ResponseBuilder { - type Output = Result, Error>; + type Output = Result, Error>; fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll { Poll::Ready(Ok(self.finish())) diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index d25a67a19..afe47bf2d 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -1,4 +1,5 @@ use std::{ + error::Error as StdError, fmt, future::Future, marker::PhantomData, @@ -8,6 +9,7 @@ use std::{ task::{Context, Poll}, }; +use ::h2::server::{handshake as h2_handshake, Handshake as H2Handshake}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_rt::net::TcpStream; use actix_service::{ @@ -15,16 +17,15 @@ use actix_service::{ }; use bytes::Bytes; use futures_core::{future::LocalBoxFuture, ready}; -use h2::server::{handshake, Handshake}; use pin_project::pin_project; -use crate::body::MessageBody; -use crate::builder::HttpServiceBuilder; -use crate::config::{KeepAlive, ServiceConfig}; -use crate::error::{DispatchError, Error}; -use crate::request::Request; -use crate::response::Response; -use crate::{h1, h2::Dispatcher, ConnectCallback, OnConnectData, Protocol}; +use crate::{ + body::{AnyBody, MessageBody}, + builder::HttpServiceBuilder, + config::{KeepAlive, ServiceConfig}, + error::DispatchError, + h1, h2, ConnectCallback, OnConnectData, Protocol, Request, Response, +}; /// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol. pub struct HttpService { @@ -39,7 +40,7 @@ pub struct HttpService { impl HttpService where S: ServiceFactory, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, @@ -54,12 +55,12 @@ where impl HttpService where S: ServiceFactory, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, { /// Create new `HttpService` instance. pub fn new>(service: F) -> Self { @@ -94,7 +95,7 @@ where impl HttpService where S: ServiceFactory, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, @@ -108,7 +109,7 @@ where pub fn expect(self, expect: X1) -> HttpService where X1: ServiceFactory, - X1::Error: Into, + X1::Error: Into>, X1::InitError: fmt::Debug, { HttpService { @@ -152,17 +153,17 @@ impl HttpService where S: ServiceFactory, S::Future: 'static, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory< @@ -171,7 +172,7 @@ where Response = (), >, U::Future: 'static, - U::Error: fmt::Display + Into, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create simple tcp stream service @@ -204,17 +205,17 @@ mod openssl { where S: ServiceFactory, S::Future: 'static, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory< @@ -223,7 +224,7 @@ mod openssl { Response = (), >, U::Future: 'static, - U::Error: fmt::Display + Into, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create openssl based service @@ -272,17 +273,17 @@ mod rustls { where S: ServiceFactory, S::Future: 'static, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory< @@ -291,7 +292,7 @@ mod rustls { Response = (), >, U::Future: 'static, - U::Error: fmt::Display + Into, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create rustls based service @@ -305,7 +306,8 @@ mod rustls { Error = TlsError, InitError = (), > { - let protos = vec!["h2".to_string().into(), "http/1.1".to_string().into()]; + let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + protos.extend_from_slice(&config.alpn_protocols); config.set_protocols(&protos); Acceptor::new(config) @@ -337,22 +339,22 @@ where S: ServiceFactory, S::Future: 'static, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U::Future: 'static, - U::Error: fmt::Display + Into, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { type Response = (); @@ -415,11 +417,11 @@ where impl HttpServiceHandler where S: Service, - S::Error: Into, + S::Error: Into>, X: Service, - X::Error: Into, + X::Error: Into>, U: Service<(Request, Framed)>, - U::Error: Into, + U::Error: Into>, { pub(super) fn new( cfg: ServiceConfig, @@ -436,7 +438,10 @@ where } } - pub(super) fn _poll_ready(&self, cx: &mut Context<'_>) -> Poll> { + pub(super) fn _poll_ready( + &self, + cx: &mut Context<'_>, + ) -> Poll>> { ready!(self.flow.expect.poll_ready(cx).map_err(Into::into))?; ready!(self.flow.service.poll_ready(cx).map_err(Into::into))?; @@ -472,18 +477,18 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, X: Service, - X::Error: Into, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display + Into, + U::Error: fmt::Display + Into>, { type Response = (); type Error = DispatchError; @@ -506,7 +511,7 @@ where match proto { Protocol::Http2 => HttpServiceHandlerResponse { state: State::H2Handshake(Some(( - handshake(io), + h2_handshake(io), self.cfg.clone(), self.flow.clone(), on_connect_data, @@ -536,22 +541,22 @@ where S: Service, S::Future: 'static, - S::Error: Into, + S::Error: Into>, B: MessageBody, - B::Error: Into, + B::Error: Into>, X: Service, - X::Error: Into, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, { H1(#[pin] h1::Dispatcher), - H2(#[pin] Dispatcher), + H2(#[pin] h2::Dispatcher), H2Handshake( Option<( - Handshake, + H2Handshake, ServiceConfig, Rc>, OnConnectData, @@ -566,15 +571,15 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody, - B::Error: Into, + B::Error: Into>, X: Service, - X::Error: Into, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -588,15 +593,15 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, X: Service, - X::Error: Into, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -612,13 +617,15 @@ where Ok(conn) => { let (_, cfg, srv, on_connect_data, peer_addr) = data.take().unwrap(); - self.as_mut().project().state.set(State::H2(Dispatcher::new( - srv, - conn, - on_connect_data, - cfg, - peer_addr, - ))); + self.as_mut().project().state.set(State::H2( + h2::Dispatcher::new( + srv, + conn, + on_connect_data, + cfg, + peer_addr, + ), + )); self.poll(cx) } Err(err) => { diff --git a/actix-http/src/ws/dispatcher.rs b/actix-http/src/ws/dispatcher.rs index 576851139..f49cbe5d4 100644 --- a/actix-http/src/ws/dispatcher.rs +++ b/actix-http/src/ws/dispatcher.rs @@ -72,7 +72,7 @@ mod inner { use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed}; - use crate::ResponseError; + use crate::{body::AnyBody, Response}; /// Framed transport errors pub enum DispatcherError @@ -136,13 +136,16 @@ mod inner { } } - impl ResponseError for DispatcherError + impl From> for Response where E: fmt::Debug + fmt::Display, U: Encoder + Decoder, >::Error: fmt::Debug, ::Error: fmt::Debug, { + fn from(err: DispatcherError) -> Self { + Response::internal_server_error().set_body(AnyBody::from(err.to_string())) + } } /// Message type wrapper for signalling end of message stream. diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index 22df2b4ff..7df924cf5 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -9,8 +9,8 @@ use derive_more::{Display, Error, From}; use http::{header, Method, StatusCode}; use crate::{ - body::Body, error::ResponseError, header::HeaderValue, message::RequestHead, - response::Response, ResponseBuilder, + body::AnyBody, header::HeaderValue, message::RequestHead, response::Response, + ResponseBuilder, }; mod codec; @@ -25,7 +25,7 @@ pub use self::frame::Parser; pub use self::proto::{hash_key, CloseCode, CloseReason, OpCode}; /// WebSocket protocol errors. -#[derive(Debug, Display, From, Error)] +#[derive(Debug, Display, Error, From)] pub enum ProtocolError { /// Received an unmasked frame from client. #[display(fmt = "Received an unmasked frame from client.")] @@ -68,10 +68,8 @@ pub enum ProtocolError { Io(io::Error), } -impl ResponseError for ProtocolError {} - /// WebSocket handshake errors -#[derive(PartialEq, Debug, Display)] +#[derive(Debug, PartialEq, Display, Error)] pub enum HandshakeError { /// Only get method is allowed. #[display(fmt = "Method not allowed.")] @@ -98,44 +96,55 @@ pub enum HandshakeError { BadWebsocketKey, } -impl ResponseError for HandshakeError { - fn error_response(&self) -> Response { - match self { +impl From<&HandshakeError> for Response { + fn from(err: &HandshakeError) -> Self { + match err { HandshakeError::GetMethodRequired => { - Response::build(StatusCode::METHOD_NOT_ALLOWED) - .insert_header((header::ALLOW, "GET")) - .finish() + let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED); + res.headers_mut() + .insert(header::ALLOW, HeaderValue::from_static("GET")); + res } HandshakeError::NoWebsocketUpgrade => { - Response::build(StatusCode::BAD_REQUEST) - .reason("No WebSocket Upgrade header found") - .finish() + let mut res = Response::bad_request(); + res.head_mut().reason = Some("No WebSocket Upgrade header found"); + res } HandshakeError::NoConnectionUpgrade => { - Response::build(StatusCode::BAD_REQUEST) - .reason("No Connection upgrade") - .finish() + let mut res = Response::bad_request(); + res.head_mut().reason = Some("No Connection upgrade"); + res } - HandshakeError::NoVersionHeader => Response::build(StatusCode::BAD_REQUEST) - .reason("WebSocket version header is required") - .finish(), + HandshakeError::NoVersionHeader => { + let mut res = Response::bad_request(); + res.head_mut().reason = Some("WebSocket version header is required"); + res + } HandshakeError::UnsupportedVersion => { - Response::build(StatusCode::BAD_REQUEST) - .reason("Unsupported WebSocket version") - .finish() + let mut res = Response::bad_request(); + res.head_mut().reason = Some("Unsupported WebSocket version"); + res } - HandshakeError::BadWebsocketKey => Response::build(StatusCode::BAD_REQUEST) - .reason("Handshake error") - .finish(), + HandshakeError::BadWebsocketKey => { + let mut res = Response::bad_request(); + res.head_mut().reason = Some("Handshake error"); + res + } } } } +impl From for Response { + fn from(err: HandshakeError) -> Self { + (&err).into() + } +} + /// Verify WebSocket handshake request and create handshake response. pub fn handshake(req: &RequestHead) -> Result { verify_handshake(req)?; @@ -213,7 +222,7 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder { #[cfg(test)] mod tests { use super::*; - use crate::test::TestRequest; + use crate::{body::AnyBody, test::TestRequest}; use http::{header, Method}; #[test] @@ -327,18 +336,18 @@ mod tests { } #[test] - fn test_wserror_http_response() { - let resp = HandshakeError::GetMethodRequired.error_response(); + fn test_ws_error_http_response() { + let resp: Response = HandshakeError::GetMethodRequired.into(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - let resp = HandshakeError::NoWebsocketUpgrade.error_response(); + let resp: Response = HandshakeError::NoWebsocketUpgrade.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp = HandshakeError::NoConnectionUpgrade.error_response(); + let resp: Response = HandshakeError::NoConnectionUpgrade.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp = HandshakeError::NoVersionHeader.error_response(); + let resp: Response = HandshakeError::NoVersionHeader.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp = HandshakeError::UnsupportedVersion.error_response(); + let resp: Response = HandshakeError::UnsupportedVersion.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp = HandshakeError::BadWebsocketKey.error_response(); + let resp: Response = HandshakeError::BadWebsocketKey.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } } diff --git a/actix-http/tests/test_client.rs b/actix-http/tests/test_client.rs index 4bd7dbe14..414266d81 100644 --- a/actix-http/tests/test_client.rs +++ b/actix-http/tests/test_client.rs @@ -1,5 +1,7 @@ +use std::convert::Infallible; + use actix_http::{ - http, http::StatusCode, HttpMessage, HttpService, Request, Response, ResponseError, + body::AnyBody, http, http::StatusCode, HttpMessage, HttpService, Request, Response, }; use actix_http_test::test_server; use actix_service::ServiceFactoryExt; @@ -34,7 +36,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ async fn test_h1_v2() { let srv = test_server(move || { HttpService::build() - .finish(|_| future::ok::<_, ()>(Response::ok().set_body(STR))) + .finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR))) .tcp() }) .await; @@ -62,7 +64,7 @@ async fn test_h1_v2() { async fn test_connection_close() { let srv = test_server(move || { HttpService::build() - .finish(|_| future::ok::<_, ()>(Response::ok().set_body(STR))) + .finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR))) .tcp() .map(|_| ()) }) @@ -76,11 +78,11 @@ async fn test_connection_close() { async fn test_with_query_parameter() { let srv = test_server(move || { HttpService::build() - .finish(|req: Request| { + .finish(|req: Request| async move { if req.uri().query().unwrap().contains("qp=") { - future::ok::<_, ()>(Response::ok()) + Ok::<_, Infallible>(Response::ok()) } else { - future::ok::<_, ()>(Response::bad_request()) + Ok(Response::bad_request()) } }) .tcp() @@ -97,9 +99,9 @@ async fn test_with_query_parameter() { #[display(fmt = "expect failed")] struct ExpectFailed; -impl ResponseError for ExpectFailed { - fn status_code(&self) -> StatusCode { - StatusCode::EXPECTATION_FAILED +impl From for Response { + fn from(_: ExpectFailed) -> Self { + Response::new(StatusCode::EXPECTATION_FAILED) } } @@ -123,7 +125,7 @@ async fn test_h1_expect() { let str = std::str::from_utf8(&buf).unwrap(); assert_eq!(str, "expect body"); - Ok::<_, ()>(Response::ok()) + Ok::<_, Infallible>(Response::ok()) }) .tcp() }) diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index d3a3bea3b..a58d0cc70 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -2,16 +2,16 @@ extern crate tls_openssl as openssl; -use std::io; +use std::{convert::Infallible, io}; use actix_http::{ - body::{Body, SizedStream}, + body::{AnyBody, Body, SizedStream}, error::PayloadError, http::{ header::{self, HeaderName, HeaderValue}, Method, StatusCode, Version, }, - Error, HttpMessage, HttpService, Request, Response, ResponseError, + Error, HttpMessage, HttpService, Request, Response, }; use actix_http_test::test_server; use actix_service::{fn_service, ServiceFactoryExt}; @@ -136,7 +136,7 @@ async fn test_h2_content_length() { StatusCode::OK, StatusCode::NOT_FOUND, ]; - ok::<_, ()>(Response::new(statuses[idx])) + ok::<_, Infallible>(Response::new(statuses[idx])) }) .openssl(tls_config()) .map_err(|_| ()) @@ -206,7 +206,7 @@ async fn test_h2_headers() { TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", )); } - ok::<_, ()>(builder.body(data.clone())) + ok::<_, Infallible>(builder.body(data.clone())) }) .openssl(tls_config()) .map_err(|_| ()) @@ -246,7 +246,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ async fn test_h2_body2() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| ok::<_, ()>(Response::ok().set_body(STR))) + .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .openssl(tls_config()) .map_err(|_| ()) }) @@ -264,7 +264,7 @@ async fn test_h2_body2() { async fn test_h2_head_empty() { let mut srv = test_server(move || { HttpService::build() - .finish(|_| ok::<_, ()>(Response::ok().set_body(STR))) + .finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .openssl(tls_config()) .map_err(|_| ()) }) @@ -288,7 +288,7 @@ async fn test_h2_head_empty() { async fn test_h2_head_binary() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| ok::<_, ()>(Response::ok().set_body(STR))) + .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .openssl(tls_config()) .map_err(|_| ()) }) @@ -311,7 +311,7 @@ async fn test_h2_head_binary() { async fn test_h2_head_binary2() { let srv = test_server(move || { HttpService::build() - .h2(|_| ok::<_, ()>(Response::ok().set_body(STR))) + .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .openssl(tls_config()) .map_err(|_| ()) }) @@ -330,9 +330,12 @@ async fn test_h2_head_binary2() { async fn test_h2_body_length() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| { - let body = once(ok(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>( + .h2(|_| async { + let body = once(async { + Ok::<_, Infallible>(Bytes::from_static(STR.as_ref())) + }); + + Ok::<_, Infallible>( Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), ) }) @@ -355,7 +358,7 @@ async fn test_h2_body_chunked_explicit() { HttpService::build() .h2(|_| { let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>( + ok::<_, Infallible>( Response::build(StatusCode::OK) .insert_header((header::TRANSFER_ENCODING, "chunked")) .streaming(body), @@ -383,7 +386,7 @@ async fn test_h2_response_http_error_handling() { HttpService::build() .h2(fn_service(|_| { let broken_header = Bytes::from_static(b"\0\0\0"); - ok::<_, ()>( + ok::<_, Infallible>( Response::build(StatusCode::OK) .insert_header((header::CONTENT_TYPE, broken_header)) .body(STR), @@ -399,16 +402,19 @@ async fn test_h2_response_http_error_handling() { // read response let bytes = srv.load_body(response).await.unwrap(); - assert_eq!(bytes, Bytes::from_static(b"failed to parse header value")); + assert_eq!( + bytes, + Bytes::from_static(b"error processing HTTP: failed to parse header value") + ); } #[derive(Debug, Display, Error)] #[display(fmt = "error")] struct BadRequest; -impl ResponseError for BadRequest { - fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST +impl From for Response { + fn from(err: BadRequest) -> Self { + Response::build(StatusCode::BAD_REQUEST).body(err.to_string()) } } @@ -439,7 +445,7 @@ async fn test_h2_on_connect() { }) .h2(|req: Request| { assert!(req.extensions().contains::()); - ok::<_, ()>(Response::ok()) + ok::<_, Infallible>(Response::ok()) }) .openssl(tls_config()) .map_err(|_| ()) diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 2382d1ad3..cb7c77ad6 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -2,14 +2,21 @@ extern crate tls_rustls as rustls; +use std::{ + convert::Infallible, + io::{self, BufReader, Write}, + net::{SocketAddr, TcpStream as StdTcpStream}, + sync::Arc, +}; + use actix_http::{ - body::{Body, SizedStream}, + body::{AnyBody, Body, SizedStream}, error::PayloadError, http::{ header::{self, HeaderName, HeaderValue}, Method, StatusCode, Version, }, - Error, HttpService, Request, Response, ResponseError, + Error, HttpService, Request, Response, }; use actix_http_test::test_server; use actix_service::{fn_factory_with_config, fn_service}; @@ -20,10 +27,9 @@ use futures_core::Stream; use futures_util::stream::{once, StreamExt as _}; use rustls::{ internal::pemfile::{certs, pkcs8_private_keys}, - NoClientAuth, ServerConfig as RustlsServerConfig, + NoClientAuth, ServerConfig as RustlsServerConfig, Session, }; - -use std::io::{self, BufReader}; +use webpki::DNSNameRef; async fn load_body(mut stream: S) -> Result where @@ -52,6 +58,25 @@ fn tls_config() -> RustlsServerConfig { config } +pub fn get_negotiated_alpn_protocol( + addr: SocketAddr, + client_alpn_protocol: &[u8], +) -> Option> { + let mut config = rustls::ClientConfig::new(); + config.alpn_protocols.push(client_alpn_protocol.to_vec()); + let mut sess = rustls::ClientSession::new( + &Arc::new(config), + DNSNameRef::try_from_ascii_str("localhost").unwrap(), + ); + let mut sock = StdTcpStream::connect(addr).unwrap(); + let mut stream = rustls::Stream::new(&mut sess, &mut sock); + // The handshake will fails because the client will not be able to verify the server + // certificate, but it doesn't matter here as we are just interested in the negotiated ALPN + // protocol + let _ = stream.flush(); + sess.get_alpn_protocol().map(|proto| proto.to_vec()) +} + #[actix_rt::test] async fn test_h1() -> io::Result<()> { let srv = test_server(move || { @@ -149,7 +174,7 @@ async fn test_h2_content_length() { StatusCode::OK, StatusCode::NOT_FOUND, ]; - ok::<_, ()>(Response::new(statuses[indx])) + ok::<_, Infallible>(Response::new(statuses[indx])) }) .rustls(tls_config()) }) @@ -218,7 +243,7 @@ async fn test_h2_headers() { TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", )); } - ok::<_, ()>(config.body(data.clone())) + ok::<_, Infallible>(config.body(data.clone())) }) .rustls(tls_config()) }).await; @@ -257,7 +282,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ async fn test_h2_body2() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| ok::<_, ()>(Response::ok().set_body(STR))) + .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .rustls(tls_config()) }) .await; @@ -274,7 +299,7 @@ async fn test_h2_body2() { async fn test_h2_head_empty() { let mut srv = test_server(move || { HttpService::build() - .finish(|_| ok::<_, ()>(Response::ok().set_body(STR))) + .finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .rustls(tls_config()) }) .await; @@ -300,7 +325,7 @@ async fn test_h2_head_empty() { async fn test_h2_head_binary() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| ok::<_, ()>(Response::ok().set_body(STR))) + .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .rustls(tls_config()) }) .await; @@ -325,7 +350,7 @@ async fn test_h2_head_binary() { async fn test_h2_head_binary2() { let srv = test_server(move || { HttpService::build() - .h2(|_| ok::<_, ()>(Response::ok().set_body(STR))) + .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .rustls(tls_config()) }) .await; @@ -347,8 +372,8 @@ async fn test_h2_body_length() { let mut srv = test_server(move || { HttpService::build() .h2(|_| { - let body = once(ok(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>( + let body = once(ok::<_, Infallible>(Bytes::from_static(STR.as_ref()))); + ok::<_, Infallible>( Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), ) }) @@ -370,7 +395,7 @@ async fn test_h2_body_chunked_explicit() { HttpService::build() .h2(|_| { let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>( + ok::<_, Infallible>( Response::build(StatusCode::OK) .insert_header((header::TRANSFER_ENCODING, "chunked")) .streaming(body), @@ -396,9 +421,9 @@ async fn test_h2_response_http_error_handling() { let mut srv = test_server(move || { HttpService::build() .h2(fn_factory_with_config(|_: ()| { - ok::<_, ()>(fn_service(|_| { + ok::<_, Infallible>(fn_service(|_| { let broken_header = Bytes::from_static(b"\0\0\0"); - ok::<_, ()>( + ok::<_, Infallible>( Response::build(StatusCode::OK) .insert_header((http::header::CONTENT_TYPE, broken_header)) .body(STR), @@ -414,16 +439,19 @@ async fn test_h2_response_http_error_handling() { // read response let bytes = srv.load_body(response).await.unwrap(); - assert_eq!(bytes, Bytes::from_static(b"failed to parse header value")); + assert_eq!( + bytes, + Bytes::from_static(b"error processing HTTP: failed to parse header value") + ); } #[derive(Debug, Display, Error)] #[display(fmt = "error")] struct BadRequest; -impl ResponseError for BadRequest { - fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST +impl From for Response { + fn from(_: BadRequest) -> Self { + Response::bad_request().set_body(AnyBody::from("error")) } } @@ -460,3 +488,85 @@ async fn test_h1_service_error() { let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from_static(b"error")); } + +const H2_ALPN_PROTOCOL: &[u8] = b"h2"; +const HTTP1_1_ALPN_PROTOCOL: &[u8] = b"http/1.1"; +const CUSTOM_ALPN_PROTOCOL: &[u8] = b"custom"; + +#[actix_rt::test] +async fn test_alpn_h1() -> io::Result<()> { + let srv = test_server(move || { + let mut config = tls_config(); + config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec()); + HttpService::build() + .h1(|_| ok::<_, Error>(Response::ok())) + .rustls(config) + }) + .await; + + assert_eq!( + get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL), + Some(CUSTOM_ALPN_PROTOCOL.to_vec()) + ); + + let response = srv.sget("/").send().await.unwrap(); + assert!(response.status().is_success()); + + Ok(()) +} + +#[actix_rt::test] +async fn test_alpn_h2() -> io::Result<()> { + let srv = test_server(move || { + let mut config = tls_config(); + config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec()); + HttpService::build() + .h2(|_| ok::<_, Error>(Response::ok())) + .rustls(config) + }) + .await; + + assert_eq!( + get_negotiated_alpn_protocol(srv.addr(), H2_ALPN_PROTOCOL), + Some(H2_ALPN_PROTOCOL.to_vec()) + ); + assert_eq!( + get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL), + Some(CUSTOM_ALPN_PROTOCOL.to_vec()) + ); + + let response = srv.sget("/").send().await.unwrap(); + assert!(response.status().is_success()); + + Ok(()) +} + +#[actix_rt::test] +async fn test_alpn_h2_1() -> io::Result<()> { + let srv = test_server(move || { + let mut config = tls_config(); + config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec()); + HttpService::build() + .finish(|_| ok::<_, Error>(Response::ok())) + .rustls(config) + }) + .await; + + assert_eq!( + get_negotiated_alpn_protocol(srv.addr(), H2_ALPN_PROTOCOL), + Some(H2_ALPN_PROTOCOL.to_vec()) + ); + assert_eq!( + get_negotiated_alpn_protocol(srv.addr(), HTTP1_1_ALPN_PROTOCOL), + Some(HTTP1_1_ALPN_PROTOCOL.to_vec()) + ); + assert_eq!( + get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL), + Some(CUSTOM_ALPN_PROTOCOL.to_vec()) + ); + + let response = srv.sget("/").send().await.unwrap(); + assert!(response.status().is_success()); + + Ok(()) +} diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index abfda249c..1e6d0b637 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -1,21 +1,25 @@ -use std::io::{Read, Write}; -use std::time::Duration; -use std::{net, thread}; +use std::{ + convert::Infallible, + io::{Read, Write}, + net, thread, + time::Duration, +}; use actix_http::{ - body::{Body, SizedStream}, - http::{self, header, StatusCode}, - Error, HttpService, KeepAlive, Request, Response, + body::{AnyBody, Body, SizedStream}, + header, http, Error, HttpMessage, HttpService, KeepAlive, Request, Response, + StatusCode, }; -use actix_http::{HttpMessage, ResponseError}; use actix_http_test::test_server; use actix_rt::time::sleep; use actix_service::fn_service; use actix_utils::future::{err, ok, ready}; use bytes::Bytes; use derive_more::{Display, Error}; -use futures_util::stream::{once, StreamExt as _}; -use futures_util::FutureExt as _; +use futures_util::{ + stream::{once, StreamExt as _}, + FutureExt as _, +}; use regex::Regex; #[actix_rt::test] @@ -27,7 +31,7 @@ async fn test_h1() { .client_disconnect(1000) .h1(|req: Request| { assert!(req.peer_addr().is_some()); - ok::<_, ()>(Response::ok()) + ok::<_, Infallible>(Response::ok()) }) .tcp() }) @@ -47,7 +51,7 @@ async fn test_h1_2() { .finish(|req: Request| { assert!(req.peer_addr().is_some()); assert_eq!(req.version(), http::Version::HTTP_11); - ok::<_, ()>(Response::ok()) + ok::<_, Infallible>(Response::ok()) }) .tcp() }) @@ -61,9 +65,9 @@ async fn test_h1_2() { #[display(fmt = "expect failed")] struct ExpectFailed; -impl ResponseError for ExpectFailed { - fn status_code(&self) -> StatusCode { - StatusCode::PRECONDITION_FAILED +impl From for Response { + fn from(_: ExpectFailed) -> Self { + Response::new(StatusCode::EXPECTATION_FAILED) } } @@ -78,7 +82,7 @@ async fn test_expect_continue() { err(ExpectFailed) } })) - .finish(|_| ok::<_, ()>(Response::ok())) + .finish(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -87,7 +91,7 @@ async fn test_expect_continue() { let _ = stream.write_all(b"GET /test HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); let mut data = String::new(); let _ = stream.read_to_string(&mut data); - assert!(data.starts_with("HTTP/1.1 412 Precondition Failed\r\ncontent-length")); + assert!(data.starts_with("HTTP/1.1 417 Expectation Failed\r\ncontent-length")); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream.write_all(b"GET /test?yes= HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); @@ -109,7 +113,7 @@ async fn test_expect_continue_h1() { } }) })) - .h1(fn_service(|_| ok::<_, ()>(Response::ok()))) + .h1(fn_service(|_| ok::<_, Infallible>(Response::ok()))) .tcp() }) .await; @@ -118,7 +122,7 @@ async fn test_expect_continue_h1() { let _ = stream.write_all(b"GET /test HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); let mut data = String::new(); let _ = stream.read_to_string(&mut data); - assert!(data.starts_with("HTTP/1.1 412 Precondition Failed\r\ncontent-length")); + assert!(data.starts_with("HTTP/1.1 417 Expectation Failed\r\ncontent-length")); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream.write_all(b"GET /test?yes= HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); @@ -190,7 +194,7 @@ async fn test_slow_request() { let srv = test_server(|| { HttpService::build() .client_timeout(100) - .finish(|_| ok::<_, ()>(Response::ok())) + .finish(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -206,7 +210,7 @@ async fn test_slow_request() { async fn test_http1_malformed_request() { let srv = test_server(|| { HttpService::build() - .h1(|_| ok::<_, ()>(Response::ok())) + .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -222,7 +226,7 @@ async fn test_http1_malformed_request() { async fn test_http1_keepalive() { let srv = test_server(|| { HttpService::build() - .h1(|_| ok::<_, ()>(Response::ok())) + .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -244,7 +248,7 @@ async fn test_http1_keepalive_timeout() { let srv = test_server(|| { HttpService::build() .keep_alive(1) - .h1(|_| ok::<_, ()>(Response::ok())) + .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -265,7 +269,7 @@ async fn test_http1_keepalive_timeout() { async fn test_http1_keepalive_close() { let srv = test_server(|| { HttpService::build() - .h1(|_| ok::<_, ()>(Response::ok())) + .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -286,7 +290,7 @@ async fn test_http1_keepalive_close() { async fn test_http10_keepalive_default_close() { let srv = test_server(|| { HttpService::build() - .h1(|_| ok::<_, ()>(Response::ok())) + .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -306,7 +310,7 @@ async fn test_http10_keepalive_default_close() { async fn test_http10_keepalive() { let srv = test_server(|| { HttpService::build() - .h1(|_| ok::<_, ()>(Response::ok())) + .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -334,7 +338,7 @@ async fn test_http1_keepalive_disabled() { let srv = test_server(|| { HttpService::build() .keep_alive(KeepAlive::Disabled) - .h1(|_| ok::<_, ()>(Response::ok())) + .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -369,7 +373,7 @@ async fn test_content_length() { StatusCode::OK, StatusCode::NOT_FOUND, ]; - ok::<_, ()>(Response::new(statuses[indx])) + ok::<_, Infallible>(Response::new(statuses[indx])) }) .tcp() }) @@ -424,7 +428,7 @@ async fn test_h1_headers() { TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", )); } - ok::<_, ()>(builder.body(data.clone())) + ok::<_, Infallible>(builder.body(data.clone())) }).tcp() }).await; @@ -462,7 +466,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ async fn test_h1_body() { let mut srv = test_server(|| { HttpService::build() - .h1(|_| ok::<_, ()>(Response::ok().set_body(STR))) + .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .tcp() }) .await; @@ -479,7 +483,7 @@ async fn test_h1_body() { async fn test_h1_head_empty() { let mut srv = test_server(|| { HttpService::build() - .h1(|_| ok::<_, ()>(Response::ok().set_body(STR))) + .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .tcp() }) .await; @@ -504,7 +508,7 @@ async fn test_h1_head_empty() { async fn test_h1_head_binary() { let mut srv = test_server(|| { HttpService::build() - .h1(|_| ok::<_, ()>(Response::ok().set_body(STR))) + .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .tcp() }) .await; @@ -529,7 +533,7 @@ async fn test_h1_head_binary() { async fn test_h1_head_binary2() { let srv = test_server(|| { HttpService::build() - .h1(|_| ok::<_, ()>(Response::ok().set_body(STR))) + .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .tcp() }) .await; @@ -551,8 +555,8 @@ async fn test_h1_body_length() { let mut srv = test_server(|| { HttpService::build() .h1(|_| { - let body = once(ok(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>( + let body = once(ok::<_, Infallible>(Bytes::from_static(STR.as_ref()))); + ok::<_, Infallible>( Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), ) }) @@ -574,7 +578,7 @@ async fn test_h1_body_chunked_explicit() { HttpService::build() .h1(|_| { let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>( + ok::<_, Infallible>( Response::build(StatusCode::OK) .insert_header((header::TRANSFER_ENCODING, "chunked")) .streaming(body), @@ -609,7 +613,7 @@ async fn test_h1_body_chunked_implicit() { HttpService::build() .h1(|_| { let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>(Response::build(StatusCode::OK).streaming(body)) + ok::<_, Infallible>(Response::build(StatusCode::OK).streaming(body)) }) .tcp() }) @@ -638,7 +642,7 @@ async fn test_h1_response_http_error_handling() { HttpService::build() .h1(fn_service(|_| { let broken_header = Bytes::from_static(b"\0\0\0"); - ok::<_, ()>( + ok::<_, Infallible>( Response::build(StatusCode::OK) .insert_header((http::header::CONTENT_TYPE, broken_header)) .body(STR), @@ -653,16 +657,19 @@ async fn test_h1_response_http_error_handling() { // read response let bytes = srv.load_body(response).await.unwrap(); - assert_eq!(bytes, Bytes::from_static(b"failed to parse header value")); + assert_eq!( + bytes, + Bytes::from_static(b"error processing HTTP: failed to parse header value") + ); } #[derive(Debug, Display, Error)] #[display(fmt = "error")] struct BadRequest; -impl ResponseError for BadRequest { - fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST +impl From for Response { + fn from(_: BadRequest) -> Self { + Response::bad_request().set_body(AnyBody::from("error")) } } @@ -692,7 +699,7 @@ async fn test_h1_on_connect() { }) .h1(|req: Request| { assert!(req.extensions().contains::()); - ok::<_, ()>(Response::ok()) + ok::<_, Infallible>(Response::ok()) }) .tcp() }) diff --git a/actix-http/tests/test_ws.rs b/actix-http/tests/test_ws.rs index b17d4211f..6d0de2316 100644 --- a/actix-http/tests/test_ws.rs +++ b/actix-http/tests/test_ws.rs @@ -1,11 +1,12 @@ use std::{ cell::Cell, + convert::Infallible, task::{Context, Poll}, }; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_http::{ - body::BodySize, + body::{AnyBody, BodySize}, h1, ws::{self, CloseCode, Frame, Item, Message}, Error, HttpService, Request, Response, @@ -13,6 +14,7 @@ use actix_http::{ use actix_http_test::test_server; use actix_service::{fn_factory, Service}; use bytes::Bytes; +use derive_more::{Display, Error, From}; use futures_core::future::LocalBoxFuture; use futures_util::{SinkExt as _, StreamExt as _}; @@ -33,12 +35,39 @@ impl WsService { } } +#[derive(Debug, Display, Error, From)] +enum WsServiceError { + #[display(fmt = "http error")] + Http(actix_http::Error), + + #[display(fmt = "ws handshake error")] + Ws(actix_http::ws::HandshakeError), + + #[display(fmt = "io error")] + Io(std::io::Error), + + #[display(fmt = "dispatcher error")] + Dispatcher, +} + +impl From for Response { + fn from(err: WsServiceError) -> Self { + match err { + WsServiceError::Http(err) => err.into(), + WsServiceError::Ws(err) => err.into(), + WsServiceError::Io(_err) => unreachable!(), + WsServiceError::Dispatcher => Response::internal_server_error() + .set_body(AnyBody::from(format!("{}", err))), + } + } +} + impl Service<(Request, Framed)> for WsService where T: AsyncRead + AsyncWrite + Unpin + 'static, { type Response = (); - type Error = Error; + type Error = WsServiceError; type Future = LocalBoxFuture<'static, Result>; fn poll_ready(&self, _: &mut Context<'_>) -> Poll> { @@ -56,7 +85,9 @@ where let framed = framed.replace_codec(ws::Codec::new()); - ws::Dispatcher::with(framed, service).await?; + ws::Dispatcher::with(framed, service) + .await + .map_err(|_| WsServiceError::Dispatcher)?; Ok(()) }) @@ -72,7 +103,7 @@ async fn service(msg: Frame) -> Result { Frame::Binary(bin) => Message::Binary(bin), Frame::Continuation(item) => Message::Continuation(item), Frame::Close(reason) => Message::Close(reason), - _ => return Err(Error::from(ws::ProtocolError::BadOpCode)), + _ => return Err(ws::ProtocolError::BadOpCode.into()), }; Ok(msg) @@ -82,8 +113,10 @@ async fn service(msg: Frame) -> Result { async fn test_simple() { let mut srv = test_server(|| { HttpService::build() - .upgrade(fn_factory(|| async { Ok::<_, ()>(WsService::new()) })) - .finish(|_| async { Ok::<_, ()>(Response::not_found()) }) + .upgrade(fn_factory(|| async { + Ok::<_, Infallible>(WsService::new()) + })) + .finish(|_| async { Ok::<_, Infallible>(Response::not_found()) }) .tcp() }) .await; diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index cd50305cb..0b6affa3c 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 0.4.0-beta.5 - 2021-06-17 +* No notable changes. + + ## 0.4.0-beta.4 - 2021-04-02 * No notable changes. diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index fd9a8d529..5103407ca 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.4.0-beta.4" +version = "0.4.0-beta.5" authors = ["Nikolay Kim "] description = "Multipart form support for Actix Web" readme = "README.md" @@ -16,7 +16,7 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.6", default-features = false } +actix-web = { version = "4.0.0-beta.8", default-features = false } actix-utils = "3.0.0" bytes = "1" @@ -31,6 +31,6 @@ twoway = "0.2" [dev-dependencies] actix-rt = "2.2" -actix-http = "3.0.0-beta.6" +actix-http = "3.0.0-beta.8" tokio = { version = "1", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-multipart/README.md b/actix-multipart/README.md index 8a4279a62..78855b815 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -3,15 +3,15 @@ > Multipart form support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) -[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.4)](https://docs.rs/actix-multipart/0.4.0-beta.4) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.5)](https://docs.rs/actix-multipart/0.4.0-beta.5) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![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.4/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.4) +[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.5/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.5) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## 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-test/CHANGES.md b/actix-test/CHANGES.md index 2276fe745..fa554ba2e 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 0.1.0-beta.3 - 2021-06-20 +* No significant changes from `0.1.0-beta.2`. + + ## 0.1.0-beta.2 - 2021-04-17 * No significant changes from `0.1.0-beta.1`. diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 23f3650cd..b732cf744 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-test" -version = "0.1.0-beta.2" +version = "0.1.0-beta.3" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -20,13 +20,13 @@ openssl = ["tls-openssl", "actix-http/openssl"] [dependencies] actix-codec = "0.4.0" -actix-http = "3.0.0-beta.6" +actix-http = "3.0.0-beta.8" actix-http-test = { version = "3.0.0-beta.4", features = [] } actix-service = "2.0.0" actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.6", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.8", default-features = false, features = ["cookies"] } actix-rt = "2.1" -awc = { version = "3.0.0-beta.5", default-features = false, features = ["cookies"] } +awc = { version = "3.0.0-beta.7", default-features = false, features = ["cookies"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false, features = [] } diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs index 5d85c2687..c863af44a 100644 --- a/actix-test/src/lib.rs +++ b/actix-test/src/lib.rs @@ -31,7 +31,7 @@ extern crate tls_openssl as openssl; #[cfg(feature = "rustls")] extern crate tls_rustls as rustls; -use std::{fmt, net, sync::mpsc, thread, time}; +use std::{error::Error as StdError, fmt, net, sync::mpsc, thread, time}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; pub use actix_http::test::TestBuffer; @@ -39,7 +39,7 @@ use actix_http::{ http::{HeaderMap, Method}, ws, HttpService, Request, Response, }; -use actix_service::{map_config, IntoServiceFactory, ServiceFactory}; +use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _}; use actix_web::{ dev::{AppConfig, MessageBody, Server, Service}, rt, web, Error, @@ -86,7 +86,7 @@ where S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, { start_with(TestServerConfig::default(), factory) } @@ -126,7 +126,7 @@ where S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, { let (tx, rx) = mpsc::channel(); @@ -153,25 +153,40 @@ where HttpVer::Http1 => builder.listen("test", tcp, move || { let app_cfg = AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + HttpService::build() .client_timeout(timeout) - .h1(map_config(factory(), move |_| app_cfg.clone())) + .h1(map_config(fac, move |_| app_cfg.clone())) .tcp() }), HttpVer::Http2 => builder.listen("test", tcp, move || { let app_cfg = AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + HttpService::build() .client_timeout(timeout) - .h2(map_config(factory(), move |_| app_cfg.clone())) + .h2(map_config(fac, move |_| app_cfg.clone())) .tcp() }), HttpVer::Both => builder.listen("test", tcp, move || { let app_cfg = AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + HttpService::build() .client_timeout(timeout) - .finish(map_config(factory(), move |_| app_cfg.clone())) + .finish(map_config(fac, move |_| app_cfg.clone())) .tcp() }), }, @@ -180,25 +195,40 @@ where HttpVer::Http1 => builder.listen("test", tcp, move || { let app_cfg = AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + HttpService::build() .client_timeout(timeout) - .h1(map_config(factory(), move |_| app_cfg.clone())) + .h1(map_config(fac, move |_| app_cfg.clone())) .openssl(acceptor.clone()) }), HttpVer::Http2 => builder.listen("test", tcp, move || { let app_cfg = AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + HttpService::build() .client_timeout(timeout) - .h2(map_config(factory(), move |_| app_cfg.clone())) + .h2(map_config(fac, move |_| app_cfg.clone())) .openssl(acceptor.clone()) }), HttpVer::Both => builder.listen("test", tcp, move || { let app_cfg = AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + HttpService::build() .client_timeout(timeout) - .finish(map_config(factory(), move |_| app_cfg.clone())) + .finish(map_config(fac, move |_| app_cfg.clone())) .openssl(acceptor.clone()) }), }, @@ -207,25 +237,40 @@ where HttpVer::Http1 => builder.listen("test", tcp, move || { let app_cfg = AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + HttpService::build() .client_timeout(timeout) - .h1(map_config(factory(), move |_| app_cfg.clone())) + .h1(map_config(fac, move |_| app_cfg.clone())) .rustls(config.clone()) }), HttpVer::Http2 => builder.listen("test", tcp, move || { let app_cfg = AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + HttpService::build() .client_timeout(timeout) - .h2(map_config(factory(), move |_| app_cfg.clone())) + .h2(map_config(fac, move |_| app_cfg.clone())) .rustls(config.clone()) }), HttpVer::Both => builder.listen("test", tcp, move || { let app_cfg = AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + HttpService::build() .client_timeout(timeout) - .finish(map_config(factory(), move |_| app_cfg.clone())) + .finish(map_config(fac, move |_| app_cfg.clone())) .rustls(config.clone()) }), }, diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index cd76b201e..bf642ef95 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -3,6 +3,16 @@ ## Unreleased - 2021-xx-xx +## 4.0.0-beta.6 - 2021-06-26 +* Update `actix` to `0.12`. [#2277] + +[#2277]: https://github.com/actix/actix-web/pull/2277 + + +## 4.0.0-beta.5 - 2021-06-17 +* No notable changes. + + ## 4.0.0-beta.4 - 2021-04-02 * No notable changes. diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index b653dd92a..fcb5195b8 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,13 +1,11 @@ [package] name = "actix-web-actors" -version = "4.0.0-beta.4" +version = "4.0.0-beta.6" authors = ["Nikolay Kim "] description = "Actix actors support for Actix Web" -readme = "README.md" keywords = ["actix", "http", "web", "framework", "async"] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-web-actors/" +repository = "https://github.com/actix/actix-web" license = "MIT OR Apache-2.0" edition = "2018" @@ -16,10 +14,10 @@ name = "actix_web_actors" path = "src/lib.rs" [dependencies] -actix = { version = "0.11.0-beta.3", default-features = false } +actix = { version = "0.12.0", default-features = false } actix-codec = "0.4.0" -actix-http = "3.0.0-beta.6" -actix-web = { version = "4.0.0-beta.6", default-features = false } +actix-http = "3.0.0-beta.8" +actix-web = { version = "4.0.0-beta.8", default-features = false } bytes = "1" bytestring = "1" @@ -29,8 +27,8 @@ tokio = { version = "1", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.2" +actix-test = "0.1.0-beta.3" -awc = { version = "3.0.0-beta.5", default-features = false } +awc = { version = "3.0.0-beta.7", default-features = false } env_logger = "0.8" futures-util = { version = "0.3.7", default-features = false } diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index 2dc779f10..5f8f78bde 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -3,16 +3,15 @@ > Actix actors support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) -[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.4)](https://docs.rs/actix-web-actors/4.0.0-beta.4) +[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.6)](https://docs.rs/actix-web-actors/4.0.0-beta.6) [![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/4.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.4) +[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.6/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.6) [![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) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## 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/ws.rs b/actix-web-actors/src/ws.rs index 8c575206d..f0a53d4e0 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -22,10 +22,11 @@ use actix_http::{ http::HeaderValue, ws::{hash_key, Codec}, }; -use actix_web::error::{Error, PayloadError}; -use actix_web::http::{header, Method, StatusCode}; -use actix_web::HttpResponseBuilder; -use actix_web::{HttpRequest, HttpResponse}; +use actix_web::{ + error::{Error, PayloadError}, + http::{header, Method, StatusCode}, + HttpRequest, HttpResponse, HttpResponseBuilder, +}; use bytes::{Bytes, BytesMut}; use bytestring::ByteString; use futures_core::Stream; diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index f2c9b44b5..a8a901f72 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.3 - 2021-06-17 +* No notable changes. + + ## 0.5.0-beta.2 - 2021-03-09 * Preserve doc comments when using route macros. [#2022] * Add `name` attribute to `route` macro. [#1934] diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index db4f8430c..4d0fd5e26 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-codegen" -version = "0.5.0-beta.2" +version = "0.5.0-beta.3" description = "Routing and runtime macros for Actix Web" readme = "README.md" homepage = "https://actix.rs" @@ -20,9 +20,9 @@ proc-macro2 = "1" [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.2" +actix-test = "0.1.0-beta.3" actix-utils = "3.0.0" -actix-web = "4.0.0-beta.6" +actix-web = "4.0.0-beta.8" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } trybuild = "1" diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index 9552d4b56..96e4cb51f 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -3,18 +3,17 @@ > Routing and runtime macros for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) -[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.2)](https://docs.rs/actix-web-codegen/0.5.0-beta.2) +[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.3)](https://docs.rs/actix-web-codegen/0.5.0-beta.3) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
-[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.2/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.2) +[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3) [![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen) -[![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) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-codegen) -- [Chat on Gitter](https://gitter.im/actix/actix-web) - Minimum supported Rust version: 1.46 or later. ## Compile Testing diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 336345014..2237f422c 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -171,27 +171,10 @@ method_macro! { #[proc_macro_attribute] pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { use quote::quote; - - let mut input = syn::parse_macro_input!(item as syn::ItemFn); - let attrs = &input.attrs; - let vis = &input.vis; - let sig = &mut input.sig; - let body = &input.block; - - if sig.asyncness.is_none() { - return syn::Error::new_spanned(sig.fn_token, "only async fn is supported") - .to_compile_error() - .into(); - } - - sig.asyncness = None; - + let input = syn::parse_macro_input!(item as syn::ItemFn); (quote! { - #(#attrs)* - #vis #sig { - actix_web::rt::System::new() - .block_on(async move { #body }) - } + #[actix_web::rt::main(system = "::actix_web::rt::System")] + #input }) .into() } diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index ac0b7cea1..747042527 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -6,7 +6,7 @@ use std::convert::TryFrom; use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{format_ident, quote, ToTokens, TokenStreamExt}; -use syn::{parse_macro_input, AttributeArgs, Ident, NestedMeta}; +use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, NestedMeta}; enum ResourceType { Async, @@ -227,8 +227,7 @@ impl Route { format!( r#"invalid service definition, expected #[{}("")]"#, method - .map(|it| it.as_str()) - .unwrap_or("route") + .map_or("route", |it| it.as_str()) .to_ascii_lowercase() ), )); @@ -298,7 +297,7 @@ impl ToTokens for Route { } = self; let resource_name = resource_name .as_ref() - .map_or_else(|| name.to_string(), |n| n.value()); + .map_or_else(|| name.to_string(), LitStr::value); let method_guards = { let mut others = methods.iter(); // unwrapping since length is checked to be at least one diff --git a/awc/CHANGES.md b/awc/CHANGES.md index b2e0ff78d..16132be1c 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -3,6 +3,17 @@ ## Unreleased - 2021-xx-xx +## 3.0.0-beta.7 - 2021-06-26 +### Changed +* Change compression algorithm features flags. [#2250] + +[#2250]: https://github.com/actix/actix-web/pull/2250 + + +## 3.0.0-beta.6 - 2021-06-17 +* No significant changes since 3.0.0-beta.5. + + ## 3.0.0-beta.5 - 2021-04-17 ### Removed * Deprecated methods on `ClientRequest`: `if_true`, `if_some`. [#2148] diff --git a/awc/Cargo.toml b/awc/Cargo.toml index c8a184513..016d3b48b 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.5" +version = "3.0.0-beta.7" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -24,10 +24,10 @@ path = "src/lib.rs" [package.metadata.docs.rs] # features that docs.rs will build with -features = ["openssl", "rustls", "compress", "cookies"] +features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies"] [features] -default = ["compress", "cookies"] +default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] # openssl openssl = ["tls-openssl", "actix-http/openssl"] @@ -35,8 +35,12 @@ openssl = ["tls-openssl", "actix-http/openssl"] # rustls rustls = ["tls-rustls", "actix-http/rustls"] -# content-encoding support -compress = ["actix-http/compress"] +# Brotli algorithm content-encoding support +compress-brotli = ["actix-http/compress-brotli", "__compress"] +# Gzip and deflate algorithms content-encoding support +compress-gzip = ["actix-http/compress-gzip", "__compress"] +# Zstd algorithm content-encoding support +compress-zstd = ["actix-http/compress-zstd", "__compress"] # cookie parsing and cookie jar cookies = ["cookie"] @@ -44,14 +48,19 @@ cookies = ["cookie"] # trust-dns as dns resolver trust-dns = ["actix-http/trust-dns"] +# Internal (PRIVATE!) features used to aid testing and cheking feature status. +# Don't rely on these whatsoever. They may disappear at anytime. +__compress = [] + [dependencies] actix-codec = "0.4.0" actix-service = "2.0.0" -actix-http = "3.0.0-beta.6" +actix-http = "3.0.0-beta.8" actix-rt = { version = "2.1", default-features = false } base64 = "0.13" bytes = "1" +cfg-if = "1" cookie = { version = "0.15", features = ["percent-encode"], optional = true } derive_more = "0.99.5" futures-core = { version = "0.3.7", default-features = false } @@ -68,13 +77,13 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tls-rustls = { version = "0.19.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } [dev-dependencies] -actix-web = { version = "4.0.0-beta.6", features = ["openssl"] } -actix-http = { version = "3.0.0-beta.6", features = ["openssl"] } +actix-web = { version = "4.0.0-beta.8", features = ["openssl"] } +actix-http = { version = "3.0.0-beta.8", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] } actix-utils = "3.0.0" actix-server = "2.0.0-beta.3" actix-tls = { version = "3.0.0-beta.5", features = ["openssl", "rustls"] } -actix-test = { version = "0.1.0-beta.2", features = ["openssl", "rustls"] } +actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } brotli2 = "0.3.2" env_logger = "0.8" diff --git a/awc/README.md b/awc/README.md index a836a2497..dd08c6e10 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,16 +3,15 @@ > 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=3.0.0-beta.4)](https://docs.rs/awc/3.0.0-beta.4) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.7)](https://docs.rs/awc/3.0.0-beta.7) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.4/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.4) -[![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) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.7/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.7) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/awc) - [Example Project](https://github.com/actix/examples/tree/HEAD/security/awc_https) -- [Chat on Gitter](https://gitter.im/actix/actix-web) - Minimum Supported Rust Version (MSRV): 1.46.0 ## Example diff --git a/awc/examples/client.rs b/awc/examples/client.rs index b9574590d..653cb226f 100644 --- a/awc/examples/client.rs +++ b/awc/examples/client.rs @@ -1,18 +1,20 @@ -use actix_http::Error; +use std::error::Error as StdError; #[actix_web::main] -async fn main() -> Result<(), Error> { - std::env::set_var("RUST_LOG", "actix_http=trace"); +async fn main() -> Result<(), Box> { + std::env::set_var("RUST_LOG", "client=trace,awc=trace,actix_http=trace"); env_logger::init(); let client = awc::Client::new(); // Create request builder, configure request and send - let mut response = client + let request = client .get("https://www.rust-lang.org/") - .append_header(("User-Agent", "Actix-web")) - .send() - .await?; + .append_header(("User-Agent", "Actix-web")); + + println!("Request: {:?}", request); + + let mut response = request.send().await?; // server http response println!("Response: {:?}", response); diff --git a/awc/src/error.rs b/awc/src/error.rs index b715f6213..c83c5ebbf 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -6,7 +6,6 @@ pub use actix_http::http::Error as HttpError; pub use actix_http::ws::HandshakeError as WsHandshakeError; pub use actix_http::ws::ProtocolError as WsProtocolError; -use actix_http::ResponseError; use serde_json::error::Error as JsonError; use actix_http::http::{header::HeaderValue, StatusCode}; @@ -77,6 +76,3 @@ pub enum JsonPayloadError { } impl std::error::Error for JsonPayloadError {} - -/// Return `InternalServerError` for `JsonPayloadError` -impl ResponseError for JsonPayloadError {} diff --git a/awc/src/frozen.rs b/awc/src/frozen.rs index 5fe8edb19..cb8c0f1bf 100644 --- a/awc/src/frozen.rs +++ b/awc/src/frozen.rs @@ -1,21 +1,21 @@ -use std::convert::TryFrom; -use std::net; -use std::rc::Rc; -use std::time::Duration; +use std::{convert::TryFrom, error::Error as StdError, net, rc::Rc, time::Duration}; use bytes::Bytes; use futures_core::Stream; use serde::Serialize; -use actix_http::body::Body; -use actix_http::http::header::IntoHeaderValue; -use actix_http::http::{Error as HttpError, HeaderMap, HeaderName, Method, Uri}; -use actix_http::{Error, RequestHead}; +use actix_http::{ + body::Body, + http::{header::IntoHeaderValue, Error as HttpError, HeaderMap, HeaderName, Method, Uri}, + RequestHead, +}; -use crate::sender::{RequestSender, SendClientRequest}; -use crate::ClientConfig; +use crate::{ + sender::{RequestSender, SendClientRequest}, + ClientConfig, +}; -/// `FrozenClientRequest` struct represents clonable client request. +/// `FrozenClientRequest` struct represents cloneable client request. /// It could be used to send same request multiple times. #[derive(Clone)] pub struct FrozenClientRequest { @@ -82,7 +82,7 @@ impl FrozenClientRequest { pub fn send_stream(&self, stream: S) -> SendClientRequest where S: Stream> + Unpin + 'static, - E: Into + 'static, + E: Into> + 'static, { RequestSender::Rc(self.head.clone(), None).send_stream( self.addr, @@ -207,7 +207,7 @@ impl FrozenSendBuilder { pub fn send_stream(self, stream: S) -> SendClientRequest where S: Stream> + Unpin + 'static, - E: Into + 'static, + E: Into> + 'static, { if let Some(e) = self.err { return e.into(); diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 562d6ee7f..c0290ddcf 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -1,7 +1,6 @@ //! `awc` is a HTTP and WebSocket client library built on the Actix ecosystem. //! -//! ## Making a GET request -//! +//! # Making a GET request //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), awc::error::SendRequestError> { @@ -16,10 +15,8 @@ //! # } //! ``` //! -//! ## Making POST requests -//! -//! ### Raw body contents -//! +//! # Making POST requests +//! ## Raw body contents //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), awc::error::SendRequestError> { @@ -31,8 +28,7 @@ //! # } //! ``` //! -//! ### Forms -//! +//! ## Forms //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), awc::error::SendRequestError> { @@ -46,8 +42,7 @@ //! # } //! ``` //! -//! ### JSON -//! +//! ## JSON //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), awc::error::SendRequestError> { @@ -64,8 +59,24 @@ //! # } //! ``` //! -//! ## WebSocket support +//! # Response Compression +//! All [official][iana-encodings] and common content encoding codecs are supported, optionally. //! +//! The `Accept-Encoding` header will automatically be populated with enabled codecs and added to +//! outgoing requests, allowing servers to select their `Content-Encoding` accordingly. +//! +//! Feature flags enable these codecs according to the table below. By default, all `compress-*` +//! features are enabled. +//! +//! | Feature | Codecs | +//! | ----------------- | ------------- | +//! | `compress-brotli` | brotli | +//! | `compress-gzip` | gzip, deflate | +//! | `compress-zstd` | zstd | +//! +//! [iana-encodings]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding +//! +//! # WebSocket support //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), Box> { @@ -128,8 +139,10 @@ pub use self::sender::SendClientRequest; /// An asynchronous HTTP and WebSocket client. /// -/// ## Examples +/// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU +/// and memory usage. /// +/// # Examples /// ``` /// use awc::Client; /// @@ -137,10 +150,10 @@ pub use self::sender::SendClientRequest; /// async fn main() { /// let mut client = Client::default(); /// -/// let res = client.get("http://www.rust-lang.org") // <- Create request builder -/// .insert_header(("User-Agent", "Actix-web")) -/// .send() // <- Send HTTP request -/// .await; // <- send request and wait for response +/// let res = client.get("http://www.rust-lang.org") +/// .insert_header(("User-Agent", "my-app/1.2")) +/// .send() +/// .await; /// /// println!("Response: {:?}", res); /// } diff --git a/awc/src/request.rs b/awc/src/request.rs index 483524102..812c76318 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -1,30 +1,26 @@ -use std::convert::TryFrom; -use std::rc::Rc; -use std::time::Duration; -use std::{fmt, net}; +use std::{convert::TryFrom, error::Error as StdError, fmt, net, rc::Rc, time::Duration}; use bytes::Bytes; use futures_core::Stream; use serde::Serialize; -use actix_http::body::Body; -use actix_http::http::header::{self, IntoHeaderPair}; -use actix_http::http::{ - uri, ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, Version, +use actix_http::{ + body::Body, + http::{ + header::{self, IntoHeaderPair}, + ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, Version, + }, + RequestHead, }; -use actix_http::{Error, RequestHead}; #[cfg(feature = "cookies")] use crate::cookie::{Cookie, CookieJar}; -use crate::error::{FreezeRequestError, InvalidUrl}; -use crate::frozen::FrozenClientRequest; -use crate::sender::{PrepForSendingError, RequestSender, SendClientRequest}; -use crate::ClientConfig; - -#[cfg(feature = "compress")] -const HTTPS_ENCODING: &str = "br, gzip, deflate"; -#[cfg(not(feature = "compress"))] -const HTTPS_ENCODING: &str = "br"; +use crate::{ + error::{FreezeRequestError, InvalidUrl}, + frozen::FrozenClientRequest, + sender::{PrepForSendingError, RequestSender, SendClientRequest}, + ClientConfig, +}; /// An HTTP Client request builder /// @@ -408,7 +404,7 @@ impl ClientRequest { pub fn send_stream(self, stream: S) -> SendClientRequest where S: Stream> + Unpin + 'static, - E: Into + 'static, + E: Into> + 'static, { let slf = match self.prep_for_sending() { Ok(slf) => slf, @@ -479,22 +475,44 @@ impl ClientRequest { let mut slf = self; + // Set Accept-Encoding HTTP header depending on enabled feature. + // If decompress is not ask, then we are not able to find which encoding is + // supported, so we cannot guess Accept-Encoding HTTP header. if slf.response_decompress { - let https = slf - .head - .uri - .scheme() - .map(|s| s == &uri::Scheme::HTTPS) - .unwrap_or(true); + // Set Accept-Encoding with compression algorithm awc is built with. + #[allow(clippy::vec_init_then_push)] + #[cfg(feature = "__compress")] + let accept_encoding = { + let mut encoding = vec![]; - if https { - slf = slf.insert_header_if_none((header::ACCEPT_ENCODING, HTTPS_ENCODING)); - } else { - #[cfg(feature = "compress")] + #[cfg(feature = "compress-brotli")] { - slf = slf.insert_header_if_none((header::ACCEPT_ENCODING, "gzip, deflate")); + encoding.push("br"); } + + #[cfg(feature = "compress-gzip")] + { + encoding.push("gzip"); + encoding.push("deflate"); + } + + #[cfg(feature = "compress-zstd")] + encoding.push("zstd"); + + assert!( + !encoding.is_empty(), + "encoding can not be empty unless __compress feature has been explicitly enabled" + ); + + encoding.join(", ") }; + + // Otherwise tell the server, we do not support any compression algorithm. + // So we clearly indicate that we do want identity encoding. + #[cfg(not(feature = "__compress"))] + let accept_encoding = "identity"; + + slf = slf.insert_header_if_none((header::ACCEPT_ENCODING, accept_encoding)); } Ok(slf) diff --git a/awc/src/sender.rs b/awc/src/sender.rs index 0e63be221..c0639606e 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -1,6 +1,7 @@ use std::{ + error::Error as StdError, future::Future, - io, net, + net, pin::Pin, rc::Rc, task::{Context, Poll}, @@ -21,25 +22,33 @@ use derive_more::From; use futures_core::Stream; use serde::Serialize; -#[cfg(feature = "compress")] +#[cfg(feature = "__compress")] use actix_http::{encoding::Decoder, http::header::ContentEncoding, Payload, PayloadStream}; -use crate::connect::{ConnectRequest, ConnectResponse}; -use crate::error::{FreezeRequestError, InvalidUrl, SendRequestError}; -use crate::response::ClientResponse; -use crate::ClientConfig; +use crate::{ + error::{FreezeRequestError, InvalidUrl, SendRequestError}, + ClientConfig, ClientResponse, ConnectRequest, ConnectResponse, +}; #[derive(Debug, From)] pub(crate) enum PrepForSendingError { Url(InvalidUrl), Http(HttpError), + Json(serde_json::Error), + Form(serde_urlencoded::ser::Error), } impl From for FreezeRequestError { fn from(err: PrepForSendingError) -> FreezeRequestError { match err { - PrepForSendingError::Url(e) => FreezeRequestError::Url(e), - PrepForSendingError::Http(e) => FreezeRequestError::Http(e), + PrepForSendingError::Url(err) => FreezeRequestError::Url(err), + PrepForSendingError::Http(err) => FreezeRequestError::Http(err), + PrepForSendingError::Json(err) => { + FreezeRequestError::Custom(Box::new(err), Box::new("json serialization error")) + } + PrepForSendingError::Form(err) => { + FreezeRequestError::Custom(Box::new(err), Box::new("form serialization error")) + } } } } @@ -49,6 +58,12 @@ impl From for SendRequestError { match err { PrepForSendingError::Url(e) => SendRequestError::Url(e), PrepForSendingError::Http(e) => SendRequestError::Http(e), + PrepForSendingError::Json(err) => { + SendRequestError::Custom(Box::new(err), Box::new("json serialization error")) + } + PrepForSendingError::Form(err) => { + SendRequestError::Custom(Box::new(err), Box::new("form serialization error")) + } } } } @@ -76,7 +91,7 @@ impl SendClientRequest { } } -#[cfg(feature = "compress")] +#[cfg(feature = "__compress")] impl Future for SendClientRequest { type Output = Result>>, SendRequestError>; @@ -116,7 +131,7 @@ impl Future for SendClientRequest { } } -#[cfg(not(feature = "compress"))] +#[cfg(not(feature = "__compress"))] impl Future for SendClientRequest { type Output = Result; @@ -209,8 +224,7 @@ impl RequestSender { ) -> SendClientRequest { let body = match serde_json::to_string(value) { Ok(body) => body, - // TODO: own error type - Err(e) => return Error::from(io::Error::new(io::ErrorKind::Other, e)).into(), + Err(err) => return PrepForSendingError::Json(err).into(), }; if let Err(e) = self.set_header_if_none(header::CONTENT_TYPE, "application/json") { @@ -236,8 +250,7 @@ impl RequestSender { ) -> SendClientRequest { let body = match serde_urlencoded::to_string(value) { Ok(body) => body, - // TODO: own error type - Err(e) => return Error::from(io::Error::new(io::ErrorKind::Other, e)).into(), + Err(err) => return PrepForSendingError::Form(err).into(), }; // set content-type @@ -266,7 +279,7 @@ impl RequestSender { ) -> SendClientRequest where S: Stream> + Unpin + 'static, - E: Into + 'static, + E: Into> + 'static, { self.send_body( addr, diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 34b71f052..2fe36399c 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -517,7 +517,7 @@ mod tests { "test-origin" ); assert_eq!(req.max_size, 100); - assert_eq!(req.server_mode, true); + assert!(req.server_mode); assert_eq!(req.protocols, Some("v1,v2".to_string())); assert_eq!( req.head.headers.get(header::CONTENT_TYPE).unwrap(), diff --git a/benches/responder.rs b/benches/responder.rs index 0dfc8cd18..5d0b98d5f 100644 --- a/benches/responder.rs +++ b/benches/responder.rs @@ -1,6 +1,5 @@ use std::{future::Future, time::Instant}; -use actix_http::Response; use actix_utils::future::{ready, Ready}; use actix_web::http::StatusCode; use actix_web::test::TestRequest; @@ -24,11 +23,11 @@ struct StringResponder(String); impl FutureResponder for StringResponder { type Error = Error; - type Future = Ready>; + type Future = Ready>; fn future_respond_to(self, _: &HttpRequest) -> Self::Future { // this is default builder for string response in both new and old responder trait. - ready(Ok(Response::build(StatusCode::OK) + ready(Ok(HttpResponse::build(StatusCode::OK) .content_type("text/plain; charset=utf-8") .body(self.0))) } @@ -37,7 +36,7 @@ impl FutureResponder for StringResponder { impl FutureResponder for OptionResponder where T: FutureResponder, - T::Future: Future>, + T::Future: Future>, { type Error = Error; type Future = Either>>; @@ -52,7 +51,7 @@ where impl Responder for StringResponder { fn respond_to(self, _: &HttpRequest) -> HttpResponse { - Response::build(StatusCode::OK) + HttpResponse::build(StatusCode::OK) .content_type("text/plain; charset=utf-8") .body(self.0) } @@ -62,7 +61,7 @@ impl Responder for OptionResponder { fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self.0 { Some(t) => t.respond_to(req), - None => Response::from_error(error::ErrorInternalServerError("err")), + None => HttpResponse::from_error(error::ErrorInternalServerError("err")), } } } diff --git a/benches/service.rs b/benches/service.rs index 30708477d..87e51f170 100644 --- a/benches/service.rs +++ b/benches/service.rs @@ -51,9 +51,8 @@ where fut.await.unwrap(); } }); - let elapsed = start.elapsed(); // check that at least first request succeeded - elapsed + start.elapsed() }) }); } @@ -93,9 +92,8 @@ fn async_web_service(c: &mut Criterion) { fut.await.unwrap(); } }); - let elapsed = start.elapsed(); // check that at least first request succeeded - elapsed + start.elapsed() }) }); } diff --git a/src/app.rs b/src/app.rs index 357d45eeb..5cff20568 100644 --- a/src/app.rs +++ b/src/app.rs @@ -43,13 +43,14 @@ impl App { /// Create application builder. Application can be configured with a builder-like pattern. #[allow(clippy::new_without_default)] pub fn new() -> Self { - let fref = Rc::new(RefCell::new(None)); + let factory_ref = Rc::new(RefCell::new(None)); + App { - endpoint: AppEntry::new(fref.clone()), + endpoint: AppEntry::new(factory_ref.clone()), data_factories: Vec::new(), services: Vec::new(), default: None, - factory_ref: fref, + factory_ref, external: Vec::new(), extensions: Extensions::new(), _phantom: PhantomData, @@ -68,43 +69,83 @@ where InitError = (), >, { - /// Set application data. Application data could be accessed - /// by using `Data` extractor where `T` is data type. + /// Set application (root level) data. /// - /// **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 shared object should be used, e.g. `Arc`. Internally `Data` type - /// uses `Arc` so data could be created outside of app factory and clones could - /// be stored via `App::app_data()` method. + /// Application data stored with `App::app_data()` method is available through the + /// [`HttpRequest::app_data`](crate::HttpRequest::app_data) method at runtime. + /// + /// # [`Data`] + /// Any [`Data`] type added here can utilize it's extractor implementation in handlers. + /// Types not wrapped in `Data` cannot use this extractor. See [its docs](Data) for more + /// about its usage and patterns. /// /// ``` /// use std::cell::Cell; - /// use actix_web::{web, App, HttpResponse, Responder}; + /// use actix_web::{web, App, HttpRequest, HttpResponse, Responder}; /// /// struct MyData { - /// counter: Cell, + /// count: std::cell::Cell, /// } /// - /// async fn index(data: web::Data) -> impl Responder { - /// data.counter.set(data.counter.get() + 1); - /// HttpResponse::Ok() + /// async fn handler(req: HttpRequest, counter: web::Data) -> impl Responder { + /// // note this cannot use the Data extractor because it was not added with it + /// let incr = *req.app_data::().unwrap(); + /// assert_eq!(incr, 3); + /// + /// // update counter using other value from app data + /// counter.count.set(counter.count.get() + incr); + /// + /// HttpResponse::Ok().body(counter.count.get().to_string()) /// } /// - /// let app = App::new() - /// .data(MyData{ counter: Cell::new(0) }) - /// .service( - /// web::resource("/index.html").route( - /// web::get().to(index))); + /// let app = App::new().service( + /// web::resource("/") + /// .app_data(3usize) + /// .app_data(web::Data::new(MyData { count: Default::default() })) + /// .route(web::get().to(handler)) + /// ); /// ``` + /// + /// # Shared Mutable State + /// [`HttpServer::new`](crate::HttpServer::new) accepts an application factory rather than an + /// application instance; the factory closure is called on each worker thread independently. + /// Therefore, if you want to share a data object between different workers, a shareable object + /// needs to be created first, outside the `HttpServer::new` closure and cloned into it. + /// [`Data`] is an example of such a sharable object. + /// + /// ```ignore + /// let counter = web::Data::new(AppStateWithCounter { + /// counter: Mutex::new(0), + /// }); + /// + /// HttpServer::new(move || { + /// // move counter object into the closure and clone for each worker + /// + /// App::new() + /// .app_data(counter.clone()) + /// .route("/", web::get().to(handler)) + /// }) + /// ``` + pub fn app_data(mut self, ext: U) -> Self { + self.extensions.insert(ext); + self + } + + /// Add application (root) data after wrapping in `Data`. + /// + /// Deprecated in favor of [`app_data`](Self::app_data). + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] pub fn data(self, data: U) -> Self { self.app_data(Data::new(data)) } - /// Set application data factory. This function is - /// similar to `.data()` but it accepts data factory. Data object get - /// constructed asynchronously during application initialization. + /// Add application data factory. This function is similar to `.data()` but it accepts a + /// "data factory". Data values are constructed asynchronously during application + /// initialization, before the server starts accepting requests. + #[deprecated( + since = "4.0.0", + note = "Construct data value before starting server and use `.app_data(Data::new(val))` instead." + )] pub fn data_factory(mut self, data: F) -> Self where F: Fn() -> Out + 'static, @@ -133,18 +174,6 @@ where self } - /// Set application level arbitrary data item. - /// - /// Application data stored with `App::app_data()` method is available - /// via `HttpRequest::app_data()` method at runtime. - /// - /// This method could be used for storing `Data` as well, in that case - /// data could be accessed by using `Data` extractor. - pub fn app_data(mut self, ext: U) -> Self { - self.extensions.insert(ext); - self - } - /// Run external configuration as part of the application building /// process /// @@ -518,6 +547,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::CREATED); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_data_factory() { let srv = init_service( @@ -541,6 +572,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_data_factory_errors() { let srv = try_init_service( diff --git a/src/app_service.rs b/src/app_service.rs index ca6f36202..3c1b78474 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -1,22 +1,22 @@ -use std::cell::RefCell; -use std::rc::Rc; +use std::{cell::RefCell, mem, rc::Rc}; use actix_http::{Extensions, Request}; use actix_router::{Path, ResourceDef, Router, Url}; -use actix_service::boxed::{self, BoxService, BoxServiceFactory}; -use actix_service::{fn_service, Service, ServiceFactory}; +use actix_service::{ + boxed::{self, BoxService, BoxServiceFactory}, + fn_service, Service, ServiceFactory, +}; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; -use crate::data::FnDataFactory; -use crate::error::Error; -use crate::guard::Guard; -use crate::request::{HttpRequest, HttpRequestPool}; -use crate::rmap::ResourceMap; -use crate::service::{AppServiceFactory, ServiceRequest, ServiceResponse}; use crate::{ config::{AppConfig, AppService}, - HttpResponse, + data::FnDataFactory, + guard::Guard, + request::{HttpRequest, HttpRequestPool}, + rmap::ResourceMap, + service::{AppServiceFactory, ServiceRequest, ServiceResponse}, + Error, HttpResponse, }; type Guards = Vec>; @@ -75,7 +75,7 @@ where let mut config = AppService::new(config, default.clone()); // register services - std::mem::take(&mut *self.services.borrow_mut()) + mem::take(&mut *self.services.borrow_mut()) .into_iter() .for_each(|mut srv| srv.register(&mut config)); @@ -98,7 +98,7 @@ where }); // external resources - for mut rdef in std::mem::take(&mut *self.external.borrow_mut()) { + for mut rdef in mem::take(&mut *self.external.borrow_mut()) { rmap.add(&mut rdef, None); } @@ -131,9 +131,9 @@ where let service = endpoint_fut.await?; // populate app data container from (async) data factories. - async_data_factories.iter().for_each(|factory| { + for factory in &async_data_factories { factory.create(&mut app_data); - }); + } Ok(AppInitService { service, @@ -144,7 +144,9 @@ where } } -/// Service that takes a [`Request`] and delegates to a service that take a [`ServiceRequest`]. +/// The [`Service`] that is passed to `actix-http`'s server builder. +/// +/// Wraps a service receiving a [`ServiceRequest`] into one receiving a [`Request`]. pub struct AppInitService where T: Service, Error = Error>, @@ -275,6 +277,7 @@ impl ServiceFactory for AppRoutingFactory { } } +/// The Actix Web router default entry point. pub struct AppRouting { router: Router, default: HttpService, @@ -349,6 +352,8 @@ mod tests { } } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_drop_data() { let data = Arc::new(AtomicBool::new(false)); diff --git a/src/config.rs b/src/config.rs index 4bd76f2b7..b072ace16 100644 --- a/src/config.rs +++ b/src/config.rs @@ -62,6 +62,8 @@ impl AppService { (self.config, self.services) } + /// Clones inner config and default service, returning new `AppService` with empty service list + /// marked as non-root. pub(crate) fn clone_config(&self) -> Self { AppService { config: self.config.clone(), @@ -71,12 +73,12 @@ impl AppService { } } - /// Service configuration + /// Returns reference to configuration. pub fn config(&self) -> &AppConfig { &self.config } - /// Default resource + /// Returns default handler factory. pub fn default_service(&self) -> Rc { self.default.clone() } @@ -92,9 +94,9 @@ impl AppService { F: IntoServiceFactory, S: ServiceFactory< ServiceRequest, - Config = (), Response = ServiceResponse, Error = Error, + Config = (), InitError = (), > + 'static, { @@ -116,6 +118,7 @@ impl AppConfig { AppConfig { secure, host, addr } } + /// Needed in actix-test crate. Semver exempt. #[doc(hidden)] pub fn __priv_test_new(secure: bool, host: String, addr: SocketAddr) -> Self { AppConfig::new(secure, host, addr) @@ -141,6 +144,11 @@ impl AppConfig { pub fn local_addr(&self) -> SocketAddr { self.addr } + + #[cfg(test)] + pub(crate) fn set_host(&mut self, host: &str) { + self.host = host.to_owned(); + } } impl Default for AppConfig { @@ -191,6 +199,7 @@ impl ServiceConfig { /// Add shared app data item. /// /// Counterpart to [`App::data()`](crate::App::data). + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] pub fn data(&mut self, data: U) -> &mut Self { self.app_data(Data::new(data)); self @@ -256,6 +265,8 @@ mod tests { use crate::test::{call_service, init_service, read_body, TestRequest}; use crate::{web, App, HttpRequest, HttpResponse}; + // allow deprecated `ServiceConfig::data` + #[allow(deprecated)] #[actix_rt::test] async fn test_data() { let cfg = |cfg: &mut ServiceConfig| { diff --git a/src/data.rs b/src/data.rs index 7bb0758c6..9d4fe0840 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,12 +1,13 @@ use std::{any::type_name, ops::Deref, sync::Arc}; -use actix_http::{error::Error, Extensions}; +use actix_http::Extensions; use actix_utils::future::{err, ok, Ready}; use futures_core::future::LocalBoxFuture; use serde::Serialize; use crate::{ dev::Payload, error::ErrorInternalServerError, extract::FromRequest, request::HttpRequest, + Error, }; /// Data factory. @@ -35,6 +36,11 @@ pub(crate) type FnDataFactory = /// If route data is not set for a handler, using `Data` extractor would cause *Internal /// Server Error* response. /// +// TODO: document `dyn T` functionality through converting an Arc +// TODO: note equivalence of req.app_data> and Data extractor +// TODO: note that data must be inserted using Data in order to extract it +/// +/// # Examples /// ``` /// use std::sync::Mutex; /// use actix_web::{web, App, HttpResponse, Responder}; @@ -152,6 +158,8 @@ mod tests { web, App, HttpResponse, }; + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_data_extractor() { let srv = init_service(App::new().data("TEST".to_string()).service( @@ -219,6 +227,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_route_data_extractor() { let srv = init_service( @@ -248,6 +258,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_override_data() { let srv = diff --git a/src/dev.rs b/src/dev.rs new file mode 100644 index 000000000..a656604e3 --- /dev/null +++ b/src/dev.rs @@ -0,0 +1,102 @@ +//! Lower level `actix-web` types. +//! +//! Most users will not have to interact with the types in this module, +//! but it is useful as a glob import for those writing middleware, developing libraries, +//! or interacting with the service API directly: +//! +//! ``` +//! # #![allow(unused_imports)] +//! use actix_web::dev::*; +//! ``` + +pub use crate::config::{AppConfig, AppService}; +#[doc(hidden)] +pub use crate::handler::Handler; +pub use crate::info::{ConnectionInfo, PeerAddr}; +pub use crate::rmap::ResourceMap; +pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService}; + +pub use crate::types::form::UrlEncoded; +pub use crate::types::json::JsonBody; +pub use crate::types::readlines::Readlines; + +pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, ResponseBody, SizedStream}; + +#[cfg(feature = "__compress")] +pub use actix_http::encoding::Decoder as Decompress; +pub use actix_http::ResponseBuilder as BaseHttpResponseBuilder; +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::{ + always_ready, fn_factory, fn_service, forward_ready, Service, Transform, +}; + +pub(crate) fn insert_slash(mut patterns: Vec) -> Vec { + for path in &mut patterns { + if !path.is_empty() && !path.starts_with('/') { + path.insert(0, '/'); + }; + } + patterns +} + +use crate::http::header::ContentEncoding; +use actix_http::{Response, ResponseBuilder}; + +struct Enc(ContentEncoding); + +/// Helper trait that allows to set specific encoding for response. +pub trait BodyEncoding { + /// Get content encoding + fn get_encoding(&self) -> Option; + + /// Set content encoding + /// + /// Must be used with [`crate::middleware::Compress`] to take effect. + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self; +} + +impl BodyEncoding for ResponseBuilder { + fn get_encoding(&self) -> Option { + self.extensions().get::().map(|enc| enc.0) + } + + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(Enc(encoding)); + self + } +} + +impl BodyEncoding for Response { + fn get_encoding(&self) -> Option { + self.extensions().get::().map(|enc| enc.0) + } + + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(Enc(encoding)); + self + } +} + +impl BodyEncoding for crate::HttpResponseBuilder { + fn get_encoding(&self) -> Option { + self.extensions().get::().map(|enc| enc.0) + } + + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(Enc(encoding)); + self + } +} + +impl BodyEncoding for crate::HttpResponse { + fn get_encoding(&self) -> Option { + self.extensions().get::().map(|enc| enc.0) + } + + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(Enc(encoding)); + self + } +} diff --git a/src/error/error.rs b/src/error/error.rs new file mode 100644 index 000000000..add290867 --- /dev/null +++ b/src/error/error.rs @@ -0,0 +1,76 @@ +use std::{error::Error as StdError, fmt}; + +use actix_http::{body::AnyBody, Response}; + +use crate::{HttpResponse, ResponseError}; + +/// General purpose actix web error. +/// +/// An actix web error is used to carry errors from `std::error` +/// through actix in a convenient way. It can be created through +/// converting errors with `into()`. +/// +/// Whenever it is created from an external object a response error is created +/// for it that can be used to create an HTTP response from it this means that +/// if you have access to an actix `Error` you can always get a +/// `ResponseError` reference from it. +pub struct Error { + cause: Box, +} + +impl Error { + /// Returns the reference to the underlying `ResponseError`. + pub fn as_response_error(&self) -> &dyn ResponseError { + self.cause.as_ref() + } + + /// Similar to `as_response_error` but downcasts. + pub fn as_error(&self) -> Option<&T> { + ::downcast_ref(self.cause.as_ref()) + } + + /// Shortcut for creating an `HttpResponse`. + pub fn error_response(&self) -> HttpResponse { + self.cause.error_response() + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.cause, f) + } +} + +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", &self.cause) + } +} + +impl StdError for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + // TODO: populate if replacement for Box is found + None + } +} + +impl From for Error { + fn from(val: std::convert::Infallible) -> Self { + match val {} + } +} + +/// `Error` for any error that implements `ResponseError` +impl From for Error { + fn from(err: T) -> Error { + Error { + cause: Box::new(err), + } + } +} + +impl From for Response { + fn from(err: Error) -> Response { + err.error_response().into() + } +} diff --git a/src/error/internal.rs b/src/error/internal.rs index 23b7dc31e..1d9ca904e 100644 --- a/src/error/internal.rs +++ b/src/error/internal.rs @@ -1,9 +1,9 @@ use std::{cell::RefCell, fmt, io::Write as _}; -use actix_http::{body::Body, header, Response, StatusCode}; +use actix_http::{body::Body, header, StatusCode}; use bytes::{BufMut as _, BytesMut}; -use crate::{Error, HttpResponse, ResponseError}; +use crate::{Error, HttpRequest, HttpResponse, Responder, ResponseError}; /// Wraps errors to alter the generated response status code. /// @@ -77,10 +77,10 @@ where } } - fn error_response(&self) -> Response { + fn error_response(&self) -> HttpResponse { match self.status { InternalErrorType::Status(status) => { - let mut res = Response::new(status); + let mut res = HttpResponse::new(status); let mut buf = BytesMut::new().writer(); let _ = write!(buf, "{}", self); @@ -88,20 +88,29 @@ where header::CONTENT_TYPE, header::HeaderValue::from_static("text/plain; charset=utf-8"), ); - res.set_body(Body::from(buf.into_inner())).into() + res.set_body(Body::from(buf.into_inner())) } InternalErrorType::Response(ref resp) => { if let Some(resp) = resp.borrow_mut().take() { - resp.into() + resp } else { - Response::new(StatusCode::INTERNAL_SERVER_ERROR) + HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR) } } } } } +impl Responder for InternalError +where + T: fmt::Debug + fmt::Display + 'static, +{ + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + HttpResponse::from_error(self) + } +} + macro_rules! error_helper { ($name:ident, $status:ident) => { paste::paste! { @@ -171,134 +180,134 @@ error_helper!( #[cfg(test)] mod tests { - use actix_http::{error::ParseError, Response}; + use actix_http::error::ParseError; use super::*; #[test] fn test_internal_error() { let err = InternalError::from_response(ParseError::Method, HttpResponse::Ok().finish()); - let resp: Response = err.error_response(); + let resp: HttpResponse = err.error_response(); assert_eq!(resp.status(), StatusCode::OK); } #[test] fn test_error_helpers() { - let res: Response = ErrorBadRequest("err").into(); + let res: HttpResponse = ErrorBadRequest("err").into(); assert_eq!(res.status(), StatusCode::BAD_REQUEST); - let res: Response = ErrorUnauthorized("err").into(); + let res: HttpResponse = ErrorUnauthorized("err").into(); assert_eq!(res.status(), StatusCode::UNAUTHORIZED); - let res: Response = ErrorPaymentRequired("err").into(); + let res: HttpResponse = ErrorPaymentRequired("err").into(); assert_eq!(res.status(), StatusCode::PAYMENT_REQUIRED); - let res: Response = ErrorForbidden("err").into(); + let res: HttpResponse = ErrorForbidden("err").into(); assert_eq!(res.status(), StatusCode::FORBIDDEN); - let res: Response = ErrorNotFound("err").into(); + let res: HttpResponse = ErrorNotFound("err").into(); assert_eq!(res.status(), StatusCode::NOT_FOUND); - let res: Response = ErrorMethodNotAllowed("err").into(); + let res: HttpResponse = ErrorMethodNotAllowed("err").into(); assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED); - let res: Response = ErrorNotAcceptable("err").into(); + let res: HttpResponse = ErrorNotAcceptable("err").into(); assert_eq!(res.status(), StatusCode::NOT_ACCEPTABLE); - let res: Response = ErrorProxyAuthenticationRequired("err").into(); + let res: HttpResponse = ErrorProxyAuthenticationRequired("err").into(); assert_eq!(res.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED); - let res: Response = ErrorRequestTimeout("err").into(); + let res: HttpResponse = ErrorRequestTimeout("err").into(); assert_eq!(res.status(), StatusCode::REQUEST_TIMEOUT); - let res: Response = ErrorConflict("err").into(); + let res: HttpResponse = ErrorConflict("err").into(); assert_eq!(res.status(), StatusCode::CONFLICT); - let res: Response = ErrorGone("err").into(); + let res: HttpResponse = ErrorGone("err").into(); assert_eq!(res.status(), StatusCode::GONE); - let res: Response = ErrorLengthRequired("err").into(); + let res: HttpResponse = ErrorLengthRequired("err").into(); assert_eq!(res.status(), StatusCode::LENGTH_REQUIRED); - let res: Response = ErrorPreconditionFailed("err").into(); + let res: HttpResponse = ErrorPreconditionFailed("err").into(); assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED); - let res: Response = ErrorPayloadTooLarge("err").into(); + let res: HttpResponse = ErrorPayloadTooLarge("err").into(); assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE); - let res: Response = ErrorUriTooLong("err").into(); + let res: HttpResponse = ErrorUriTooLong("err").into(); assert_eq!(res.status(), StatusCode::URI_TOO_LONG); - let res: Response = ErrorUnsupportedMediaType("err").into(); + let res: HttpResponse = ErrorUnsupportedMediaType("err").into(); assert_eq!(res.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); - let res: Response = ErrorRangeNotSatisfiable("err").into(); + let res: HttpResponse = ErrorRangeNotSatisfiable("err").into(); assert_eq!(res.status(), StatusCode::RANGE_NOT_SATISFIABLE); - let res: Response = ErrorExpectationFailed("err").into(); + let res: HttpResponse = ErrorExpectationFailed("err").into(); assert_eq!(res.status(), StatusCode::EXPECTATION_FAILED); - let res: Response = ErrorImATeapot("err").into(); + let res: HttpResponse = ErrorImATeapot("err").into(); assert_eq!(res.status(), StatusCode::IM_A_TEAPOT); - let res: Response = ErrorMisdirectedRequest("err").into(); + let res: HttpResponse = ErrorMisdirectedRequest("err").into(); assert_eq!(res.status(), StatusCode::MISDIRECTED_REQUEST); - let res: Response = ErrorUnprocessableEntity("err").into(); + let res: HttpResponse = ErrorUnprocessableEntity("err").into(); assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); - let res: Response = ErrorLocked("err").into(); + let res: HttpResponse = ErrorLocked("err").into(); assert_eq!(res.status(), StatusCode::LOCKED); - let res: Response = ErrorFailedDependency("err").into(); + let res: HttpResponse = ErrorFailedDependency("err").into(); assert_eq!(res.status(), StatusCode::FAILED_DEPENDENCY); - let res: Response = ErrorUpgradeRequired("err").into(); + let res: HttpResponse = ErrorUpgradeRequired("err").into(); assert_eq!(res.status(), StatusCode::UPGRADE_REQUIRED); - let res: Response = ErrorPreconditionRequired("err").into(); + let res: HttpResponse = ErrorPreconditionRequired("err").into(); assert_eq!(res.status(), StatusCode::PRECONDITION_REQUIRED); - let res: Response = ErrorTooManyRequests("err").into(); + let res: HttpResponse = ErrorTooManyRequests("err").into(); assert_eq!(res.status(), StatusCode::TOO_MANY_REQUESTS); - let res: Response = ErrorRequestHeaderFieldsTooLarge("err").into(); + let res: HttpResponse = ErrorRequestHeaderFieldsTooLarge("err").into(); assert_eq!(res.status(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE); - let res: Response = ErrorUnavailableForLegalReasons("err").into(); + let res: HttpResponse = ErrorUnavailableForLegalReasons("err").into(); assert_eq!(res.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS); - let res: Response = ErrorInternalServerError("err").into(); + let res: HttpResponse = ErrorInternalServerError("err").into(); assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); - let res: Response = ErrorNotImplemented("err").into(); + let res: HttpResponse = ErrorNotImplemented("err").into(); assert_eq!(res.status(), StatusCode::NOT_IMPLEMENTED); - let res: Response = ErrorBadGateway("err").into(); + let res: HttpResponse = ErrorBadGateway("err").into(); assert_eq!(res.status(), StatusCode::BAD_GATEWAY); - let res: Response = ErrorServiceUnavailable("err").into(); + let res: HttpResponse = ErrorServiceUnavailable("err").into(); assert_eq!(res.status(), StatusCode::SERVICE_UNAVAILABLE); - let res: Response = ErrorGatewayTimeout("err").into(); + let res: HttpResponse = ErrorGatewayTimeout("err").into(); assert_eq!(res.status(), StatusCode::GATEWAY_TIMEOUT); - let res: Response = ErrorHttpVersionNotSupported("err").into(); + let res: HttpResponse = ErrorHttpVersionNotSupported("err").into(); assert_eq!(res.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED); - let res: Response = ErrorVariantAlsoNegotiates("err").into(); + let res: HttpResponse = ErrorVariantAlsoNegotiates("err").into(); assert_eq!(res.status(), StatusCode::VARIANT_ALSO_NEGOTIATES); - let res: Response = ErrorInsufficientStorage("err").into(); + let res: HttpResponse = ErrorInsufficientStorage("err").into(); assert_eq!(res.status(), StatusCode::INSUFFICIENT_STORAGE); - let res: Response = ErrorLoopDetected("err").into(); + let res: HttpResponse = ErrorLoopDetected("err").into(); assert_eq!(res.status(), StatusCode::LOOP_DETECTED); - let res: Response = ErrorNotExtended("err").into(); + let res: HttpResponse = ErrorNotExtended("err").into(); assert_eq!(res.status(), StatusCode::NOT_EXTENDED); - let res: Response = ErrorNetworkAuthenticationRequired("err").into(); + let res: HttpResponse = ErrorNetworkAuthenticationRequired("err").into(); assert_eq!(res.status(), StatusCode::NETWORK_AUTHENTICATION_REQUIRED); } } diff --git a/actix-http/src/macros.rs b/src/error/macros.rs similarity index 89% rename from actix-http/src/macros.rs rename to src/error/macros.rs index be8e63d6e..38650c5e8 100644 --- a/actix-http/src/macros.rs +++ b/src/error/macros.rs @@ -1,5 +1,3 @@ -#[macro_export] -#[doc(hidden)] macro_rules! downcast_get_type_id { () => { /// A helper method to get the type ID of the type @@ -20,10 +18,8 @@ macro_rules! downcast_get_type_id { /// safe code cannot obtain a `PrivateHelper` instance by /// delegating to an existing implementation of `__private_get_type_id__` #[doc(hidden)] - fn __private_get_type_id__( - &self, - _: PrivateHelper, - ) -> (std::any::TypeId, PrivateHelper) + #[allow(dead_code)] + fn __private_get_type_id__(&self, _: PrivateHelper) -> (std::any::TypeId, PrivateHelper) where Self: 'static, { @@ -32,19 +28,19 @@ macro_rules! downcast_get_type_id { }; } -//Generate implementation for dyn $name -#[doc(hidden)] -#[macro_export] -macro_rules! downcast { +// Generate implementation for dyn $name +macro_rules! downcast_dyn { ($name:ident) => { /// A struct with a private constructor, for use with /// `__private_get_type_id__`. Its single field is private, /// ensuring that it can only be constructed from this module #[doc(hidden)] + #[allow(dead_code)] pub struct PrivateHelper(()); impl dyn $name + 'static { /// Downcasts generic body to a specific type. + #[allow(dead_code)] pub fn downcast_ref(&self) -> Option<&T> { if self.__private_get_type_id__(PrivateHelper(())).0 == std::any::TypeId::of::() @@ -61,6 +57,7 @@ macro_rules! downcast { } /// Downcasts a generic body to a mutable specific type. + #[allow(dead_code)] pub fn downcast_mut(&mut self) -> Option<&mut T> { if self.__private_get_type_id__(PrivateHelper(())).0 == std::any::TypeId::of::() @@ -70,9 +67,7 @@ macro_rules! downcast { // it requires returning a private type. We can therefore // rely on the returned `TypeId`, which ensures that this // case is correct. - unsafe { - Some(&mut *(self as *const dyn $name as *const T as *mut T)) - } + unsafe { Some(&mut *(self as *const dyn $name as *const T as *mut T)) } } else { None } @@ -81,6 +76,8 @@ macro_rules! downcast { }; } +pub(crate) use {downcast_dyn, downcast_get_type_id}; + #[cfg(test)] mod tests { #![allow(clippy::upper_case_acronyms)] @@ -89,7 +86,7 @@ mod tests { downcast_get_type_id!(); } - downcast!(MB); + downcast_dyn!(MB); impl MB for String {} impl MB for () {} diff --git a/src/error/mod.rs b/src/error/mod.rs index 7be9f501b..3ccd5bba6 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -9,14 +9,21 @@ use url::ParseError as UrlParseError; use crate::http::StatusCode; +#[allow(clippy::module_inception)] +mod error; mod internal; +mod macros; +mod response_error; +pub use self::error::Error; pub use self::internal::*; +pub use self::response_error::ResponseError; +pub(crate) use macros::{downcast_dyn, downcast_get_type_id}; /// A convenience [`Result`](std::result::Result) for Actix Web operations. /// /// This type alias is generally used to avoid writing out `actix_http::Error` directly. -pub type Result = std::result::Result; +pub type Result = std::result::Result; /// Errors which can occur when attempting to generate resource uri. #[derive(Debug, PartialEq, Display, Error, From)] @@ -93,9 +100,17 @@ impl ResponseError for UrlencodedError { #[derive(Debug, Display, Error)] #[non_exhaustive] pub enum JsonPayloadError { - /// Payload size is bigger than allowed. (default: 32kB) - #[display(fmt = "Json payload size is bigger than allowed")] - Overflow, + /// Payload size is bigger than allowed & content length header set. (default: 2MB) + #[display( + fmt = "JSON payload ({} bytes) is larger than allowed (limit: {} bytes).", + length, + limit + )] + OverflowKnownLength { length: usize, limit: usize }, + + /// Payload size is bigger than allowed but no content length header set. (default: 2MB) + #[display(fmt = "JSON payload has exceeded limit ({} bytes).", limit)] + Overflow { limit: usize }, /// Content type error #[display(fmt = "Content type error")] @@ -123,7 +138,11 @@ impl From for JsonPayloadError { impl ResponseError for JsonPayloadError { fn status_code(&self) -> StatusCode { match self { - Self::Overflow => StatusCode::PAYLOAD_TOO_LARGE, + Self::OverflowKnownLength { + length: _, + limit: _, + } => StatusCode::PAYLOAD_TOO_LARGE, + Self::Overflow { limit: _ } => StatusCode::PAYLOAD_TOO_LARGE, Self::Serialize(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::Payload(err) => err.status_code(), _ => StatusCode::BAD_REQUEST, @@ -208,7 +227,13 @@ mod tests { #[test] fn test_json_payload_error() { - let resp = JsonPayloadError::Overflow.error_response(); + let resp = JsonPayloadError::OverflowKnownLength { + length: 0, + limit: 0, + } + .error_response(); + assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); + let resp = JsonPayloadError::Overflow { limit: 0 }.error_response(); assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); let resp = JsonPayloadError::ContentType.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); diff --git a/src/error/response_error.rs b/src/error/response_error.rs new file mode 100644 index 000000000..c3c543419 --- /dev/null +++ b/src/error/response_error.rs @@ -0,0 +1,148 @@ +//! `ResponseError` trait and foreign impls. + +use std::{ + error::Error as StdError, + fmt, + io::{self, Write as _}, +}; + +use actix_http::{body::AnyBody, header, Response, StatusCode}; +use bytes::BytesMut; + +use crate::error::{downcast_dyn, downcast_get_type_id}; +use crate::{helpers, HttpResponse}; + +/// Errors that can generate responses. +// TODO: add std::error::Error bound when replacement for Box is found +pub trait ResponseError: fmt::Debug + fmt::Display { + /// Returns appropriate status code for error. + /// + /// A 500 Internal Server Error is used by default. If [error_response](Self::error_response) is + /// also implemented and does not call `self.status_code()`, then this will not be used. + fn status_code(&self) -> StatusCode { + StatusCode::INTERNAL_SERVER_ERROR + } + + /// Creates full response for error. + /// + /// By default, the generated response uses a 500 Internal Server Error status code, a + /// `Content-Type` of `text/plain`, and the body is set to `Self`'s `Display` impl. + fn error_response(&self) -> HttpResponse { + let mut res = HttpResponse::new(self.status_code()); + + let mut buf = BytesMut::new(); + let _ = write!(helpers::MutWriter(&mut buf), "{}", self); + + res.headers_mut().insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("text/plain; charset=utf-8"), + ); + + res.set_body(AnyBody::from(buf)) + } + + downcast_get_type_id!(); +} + +downcast_dyn!(ResponseError); + +impl ResponseError for Box {} + +#[cfg(feature = "openssl")] +impl ResponseError for actix_tls::accept::openssl::SslError {} + +impl ResponseError for serde::de::value::Error { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } +} + +impl ResponseError for serde_json::Error {} + +impl ResponseError for serde_urlencoded::ser::Error {} + +impl ResponseError for std::str::Utf8Error { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } +} + +impl ResponseError for std::io::Error { + fn status_code(&self) -> StatusCode { + // TODO: decide if these errors should consider not found or permission errors + match self.kind() { + io::ErrorKind::NotFound => StatusCode::NOT_FOUND, + io::ErrorKind::PermissionDenied => StatusCode::FORBIDDEN, + _ => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl ResponseError for actix_http::error::HttpError {} + +impl ResponseError for actix_http::Error { + fn status_code(&self) -> StatusCode { + // TODO: map error kinds to status code better + StatusCode::INTERNAL_SERVER_ERROR + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::new(self.status_code()).set_body(self.to_string().into()) + } +} + +impl ResponseError for actix_http::header::InvalidHeaderValue { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } +} + +impl ResponseError for actix_http::error::ParseError { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } +} + +impl ResponseError for actix_http::error::BlockingError {} + +impl ResponseError for actix_http::error::PayloadError { + fn status_code(&self) -> StatusCode { + match *self { + actix_http::error::PayloadError::Overflow => StatusCode::PAYLOAD_TOO_LARGE, + _ => StatusCode::BAD_REQUEST, + } + } +} + +impl ResponseError for actix_http::ws::ProtocolError {} + +impl ResponseError for actix_http::error::ContentTypeError { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } +} + +impl ResponseError for actix_http::ws::HandshakeError { + fn error_response(&self) -> HttpResponse { + Response::from(self).into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_casting() { + use actix_http::error::{ContentTypeError, PayloadError}; + + let err = PayloadError::Overflow; + let resp_err: &dyn ResponseError = &err; + + let err = resp_err.downcast_ref::().unwrap(); + assert_eq!(err.to_string(), "Payload reached size limit."); + + let not_err = resp_err.downcast_ref::(); + assert!(not_err.is_none()); + } +} diff --git a/src/extract.rs b/src/extract.rs index 82746708c..8cd603338 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -1,12 +1,14 @@ //! Request extractors use std::{ + convert::Infallible, future::Future, pin::Pin, task::{Context, Poll}, }; -use actix_utils::future::{ready, Ready}; +use actix_http::http::{Method, Uri}; +use actix_utils::future::{ok, Ready}; use futures_core::ready; use crate::{dev::Payload, Error, HttpRequest}; @@ -68,13 +70,12 @@ pub trait FromRequest: Sized { /// /// If the FromRequest for T fails, return None rather than returning an error response /// -/// ## Example -/// +/// # Examples /// ``` /// use actix_web::{web, dev, App, Error, HttpRequest, FromRequest}; /// use actix_web::error::ErrorBadRequest; /// use futures_util::future::{ok, err, Ready}; -/// use serde_derive::Deserialize; +/// use serde::Deserialize; /// use rand; /// /// #[derive(Debug, Deserialize)] @@ -158,13 +159,12 @@ where /// /// If the `FromRequest` for T fails, inject Err into handler rather than returning an error response /// -/// ## Example -/// +/// # Examples /// ``` /// use actix_web::{web, dev, App, Result, Error, HttpRequest, FromRequest}; /// use actix_web::error::ErrorBadRequest; /// use futures_util::future::{ok, err, Ready}; -/// use serde_derive::Deserialize; +/// use serde::Deserialize; /// use rand; /// /// #[derive(Debug, Deserialize)] @@ -235,13 +235,57 @@ where } } +/// Extract the request's URI. +/// +/// # Examples +/// ``` +/// use actix_web::{http::Uri, web, App, Responder}; +/// +/// async fn handler(uri: Uri) -> impl Responder { +/// format!("Requested path: {}", uri.path()) +/// } +/// +/// let app = App::new().default_service(web::to(handler)); +/// ``` +impl FromRequest for Uri { + type Error = Infallible; + type Future = Ready>; + type Config = (); + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ok(req.uri().clone()) + } +} + +/// Extract the request's method. +/// +/// # Examples +/// ``` +/// use actix_web::{http::Method, web, App, Responder}; +/// +/// async fn handler(method: Method) -> impl Responder { +/// format!("Request method: {}", method) +/// } +/// +/// let app = App::new().default_service(web::to(handler)); +/// ``` +impl FromRequest for Method { + type Error = Infallible; + type Future = Ready>; + type Config = (); + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ok(req.method().clone()) + } +} + #[doc(hidden)] impl FromRequest for () { - type Error = Error; - type Future = Ready>; + type Error = Infallible; + type Future = Ready>; fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future { - ready(Ok(())) + ok(()) } } @@ -347,7 +391,7 @@ mod m { mod tests { use actix_http::http::header; use bytes::Bytes; - use serde_derive::Deserialize; + use serde::Deserialize; use super::*; use crate::test::TestRequest; @@ -428,4 +472,18 @@ mod tests { .unwrap(); assert!(r.is_err()); } + + #[actix_rt::test] + async fn test_uri() { + let req = TestRequest::default().uri("/foo/bar").to_http_request(); + let uri = Uri::extract(&req).await.unwrap(); + assert_eq!(uri.path(), "/foo/bar"); + } + + #[actix_rt::test] + async fn test_method() { + let req = TestRequest::default().method(Method::GET).to_http_request(); + let method = Method::extract(&req).await.unwrap(); + assert_eq!(method, Method::GET); + } } diff --git a/src/handler.rs b/src/handler.rs index 822dcafdd..bc91ce41b 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -3,18 +3,14 @@ use std::marker::PhantomData; use std::pin::Pin; use std::task::{Context, Poll}; -use actix_http::Error; use actix_service::{Service, ServiceFactory}; use actix_utils::future::{ready, Ready}; use futures_core::ready; use pin_project::pin_project; use crate::{ - extract::FromRequest, - request::HttpRequest, - responder::Responder, - response::HttpResponse, service::{ServiceRequest, ServiceResponse}, + Error, FromRequest, HttpRequest, HttpResponse, Responder, }; /// A request handler is an async function that accepts zero or more parameters that can be diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 000000000..1d2679fce --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,25 @@ +use std::io; + +use bytes::BufMut; + +/// An `io::Write`r that only requires mutable reference and assumes that there is space available +/// in the buffer for every write operation or that it can be extended implicitly (like +/// `bytes::BytesMut`, for example). +/// +/// This is slightly faster (~10%) than `bytes::buf::Writer` in such cases because it does not +/// perform a remaining length check before writing. +pub(crate) struct MutWriter<'a, B>(pub(crate) &'a mut B); + +impl<'a, B> io::Write for MutWriter<'a, B> +where + B: BufMut, +{ + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.put_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} diff --git a/src/http/header/accept.rs b/src/http/header/accept.rs index 1b6a963da..75366dfae 100644 --- a/src/http/header/accept.rs +++ b/src/http/header/accept.rs @@ -5,7 +5,7 @@ use mime::Mime; use super::{qitem, QualityItem}; use crate::http::header; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2) /// /// The `Accept` header field can be used by user agents to specify @@ -81,14 +81,14 @@ crate::__define_common_header! { test_accept { // Tests from the RFC - crate::__common_header_test!( + crate::http::header::common_header_test!( test1, vec![b"audio/*; q=0.2, audio/basic"], Some(Accept(vec![ QualityItem::new("audio/*".parse().unwrap(), q(200)), qitem("audio/basic".parse().unwrap()), ]))); - crate::__common_header_test!( + crate::http::header::common_header_test!( test2, vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"], Some(Accept(vec![ @@ -100,13 +100,13 @@ crate::__define_common_header! { qitem("text/x-c".parse().unwrap()), ]))); // Custom tests - crate::__common_header_test!( + crate::http::header::common_header_test!( test3, vec![b"text/plain; charset=utf-8"], Some(Accept(vec![ qitem(mime::TEXT_PLAIN_UTF_8), ]))); - crate::__common_header_test!( + crate::http::header::common_header_test!( test4, vec![b"text/plain; charset=utf-8; q=0.5"], Some(Accept(vec![ diff --git a/src/http/header/accept_charset.rs b/src/http/header/accept_charset.rs index 2c6a0b9f6..bb7d86516 100644 --- a/src/http/header/accept_charset.rs +++ b/src/http/header/accept_charset.rs @@ -1,6 +1,6 @@ use super::{Charset, QualityItem, ACCEPT_CHARSET}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Accept-Charset` header, defined in /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.3) /// @@ -57,6 +57,6 @@ crate::__define_common_header! { test_accept_charset { // Test case from RFC - crate::__common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); + crate::http::header::common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); } } diff --git a/src/http/header/accept_encoding.rs b/src/http/header/accept_encoding.rs index 734a435b3..cfd29bf77 100644 --- a/src/http/header/accept_encoding.rs +++ b/src/http/header/accept_encoding.rs @@ -64,12 +64,12 @@ header! { test_accept_encoding { // From the RFC - crate::__common_header_test!(test1, vec![b"compress, gzip"]); - crate::__common_header_test!(test2, vec![b""], Some(AcceptEncoding(vec![]))); - crate::__common_header_test!(test3, vec![b"*"]); + crate::http::header::common_header_test!(test1, vec![b"compress, gzip"]); + crate::http::header::common_header_test!(test2, vec![b""], Some(AcceptEncoding(vec![]))); + crate::http::header::common_header_test!(test3, vec![b"*"]); // Note: Removed quality 1 from gzip - crate::__common_header_test!(test4, vec![b"compress;q=0.5, gzip"]); + crate::http::header::common_header_test!(test4, vec![b"compress;q=0.5, gzip"]); // Note: Removed quality 1 from gzip - crate::__common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); + crate::http::header::common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); } } diff --git a/src/http/header/accept_language.rs b/src/http/header/accept_language.rs index 034946d4d..1552f6578 100644 --- a/src/http/header/accept_language.rs +++ b/src/http/header/accept_language.rs @@ -2,7 +2,7 @@ use language_tags::LanguageTag; use super::{QualityItem, ACCEPT_LANGUAGE}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Accept-Language` header, defined in /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.5) /// @@ -53,9 +53,9 @@ crate::__define_common_header! { test_accept_language { // From the RFC - crate::__common_header_test!(test1, vec![b"da, en-gb;q=0.8, en;q=0.7"]); + crate::http::header::common_header_test!(test1, vec![b"da, en-gb;q=0.8, en;q=0.7"]); // Own test - crate::__common_header_test!( + crate::http::header::common_header_test!( test2, vec![b"en-US, en; q=0.5, fr"], Some(AcceptLanguage(vec![ qitem("en-US".parse().unwrap()), diff --git a/src/http/header/allow.rs b/src/http/header/allow.rs index 15a627b8f..946f70e0a 100644 --- a/src/http/header/allow.rs +++ b/src/http/header/allow.rs @@ -1,7 +1,7 @@ use crate::http::header; use actix_http::http::Method; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1) /// /// The `Allow` header field lists the set of methods advertised as @@ -49,12 +49,12 @@ crate::__define_common_header! { test_allow { // From the RFC - crate::__common_header_test!( + crate::http::header::common_header_test!( test1, vec![b"GET, HEAD, PUT"], Some(HeaderField(vec![Method::GET, Method::HEAD, Method::PUT]))); // Own tests - crate::__common_header_test!( + crate::http::header::common_header_test!( test2, vec![b"OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH"], Some(HeaderField(vec![ @@ -67,7 +67,7 @@ crate::__define_common_header! { Method::TRACE, Method::CONNECT, Method::PATCH]))); - crate::__common_header_test!( + crate::http::header::common_header_test!( test3, vec![b""], Some(HeaderField(Vec::::new()))); diff --git a/src/http/header/cache_control.rs b/src/http/header/cache_control.rs index 620c576ae..05903e3a3 100644 --- a/src/http/header/cache_control.rs +++ b/src/http/header/cache_control.rs @@ -49,9 +49,9 @@ use crate::http::header; #[derive(PartialEq, Clone, Debug)] pub struct CacheControl(pub Vec); -crate::__common_header_deref!(CacheControl => Vec); +crate::http::header::common_header_deref!(CacheControl => Vec); -// TODO: this could just be the __define_common_header! macro +// TODO: this could just be the crate::http::header::common_header! macro impl Header for CacheControl { fn name() -> header::HeaderName { header::CACHE_CONTROL diff --git a/src/http/header/content_disposition.rs b/src/http/header/content_disposition.rs index 71c610157..9f67baffb 100644 --- a/src/http/header/content_disposition.rs +++ b/src/http/header/content_disposition.rs @@ -410,41 +410,33 @@ impl ContentDisposition { /// Return the value of *name* if exists. pub fn get_name(&self) -> Option<&str> { - self.parameters.iter().filter_map(|p| p.as_name()).next() + self.parameters.iter().find_map(DispositionParam::as_name) } /// Return the value of *filename* if exists. pub fn get_filename(&self) -> Option<&str> { self.parameters .iter() - .filter_map(|p| p.as_filename()) - .next() + .find_map(DispositionParam::as_filename) } /// Return the value of *filename\** if exists. pub fn get_filename_ext(&self) -> Option<&ExtendedValue> { self.parameters .iter() - .filter_map(|p| p.as_filename_ext()) - .next() + .find_map(DispositionParam::as_filename_ext) } /// Return the value of the parameter which the `name` matches. pub fn get_unknown(&self, name: impl AsRef) -> Option<&str> { let name = name.as_ref(); - self.parameters - .iter() - .filter_map(|p| p.as_unknown(name)) - .next() + self.parameters.iter().find_map(|p| p.as_unknown(name)) } /// Return the value of the extended parameter which the `name` matches. pub fn get_unknown_ext(&self, name: impl AsRef) -> Option<&ExtendedValue> { let name = name.as_ref(); - self.parameters - .iter() - .filter_map(|p| p.as_unknown_ext(name)) - .next() + self.parameters.iter().find_map(|p| p.as_unknown_ext(name)) } } diff --git a/src/http/header/content_language.rs b/src/http/header/content_language.rs index c2469edd1..604ada83c 100644 --- a/src/http/header/content_language.rs +++ b/src/http/header/content_language.rs @@ -1,7 +1,7 @@ use super::{QualityItem, CONTENT_LANGUAGE}; use language_tags::LanguageTag; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Content-Language` header, defined in /// [RFC7231](https://tools.ietf.org/html/rfc7231#section-3.1.3.2) /// @@ -50,7 +50,7 @@ crate::__define_common_header! { (ContentLanguage, CONTENT_LANGUAGE) => (QualityItem)+ test_content_language { - crate::__common_header_test!(test1, vec![b"da"]); - crate::__common_header_test!(test2, vec![b"mi, en"]); + crate::http::header::common_header_test!(test1, vec![b"da"]); + crate::http::header::common_header_test!(test2, vec![b"mi, en"]); } } diff --git a/src/http/header/content_range.rs b/src/http/header/content_range.rs index ba0d51742..3bdead2c0 100644 --- a/src/http/header/content_range.rs +++ b/src/http/header/content_range.rs @@ -4,65 +4,65 @@ use std::str::FromStr; use super::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer, CONTENT_RANGE}; use crate::error::ParseError; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Content-Range` header, defined in /// [RFC7233](http://tools.ietf.org/html/rfc7233#section-4.2) (ContentRange, CONTENT_RANGE) => [ContentRangeSpec] test_content_range { - crate::__common_header_test!(test_bytes, + crate::http::header::common_header_test!(test_bytes, vec![b"bytes 0-499/500"], Some(ContentRange(ContentRangeSpec::Bytes { range: Some((0, 499)), instance_length: Some(500) }))); - crate::__common_header_test!(test_bytes_unknown_len, + crate::http::header::common_header_test!(test_bytes_unknown_len, vec![b"bytes 0-499/*"], Some(ContentRange(ContentRangeSpec::Bytes { range: Some((0, 499)), instance_length: None }))); - crate::__common_header_test!(test_bytes_unknown_range, + crate::http::header::common_header_test!(test_bytes_unknown_range, vec![b"bytes */500"], Some(ContentRange(ContentRangeSpec::Bytes { range: None, instance_length: Some(500) }))); - crate::__common_header_test!(test_unregistered, + crate::http::header::common_header_test!(test_unregistered, vec![b"seconds 1-2"], Some(ContentRange(ContentRangeSpec::Unregistered { unit: "seconds".to_owned(), resp: "1-2".to_owned() }))); - crate::__common_header_test!(test_no_len, + crate::http::header::common_header_test!(test_no_len, vec![b"bytes 0-499"], None::); - crate::__common_header_test!(test_only_unit, + crate::http::header::common_header_test!(test_only_unit, vec![b"bytes"], None::); - crate::__common_header_test!(test_end_less_than_start, + crate::http::header::common_header_test!(test_end_less_than_start, vec![b"bytes 499-0/500"], None::); - crate::__common_header_test!(test_blank, + crate::http::header::common_header_test!(test_blank, vec![b""], None::); - crate::__common_header_test!(test_bytes_many_spaces, + crate::http::header::common_header_test!(test_bytes_many_spaces, vec![b"bytes 1-2/500 3"], None::); - crate::__common_header_test!(test_bytes_many_slashes, + crate::http::header::common_header_test!(test_bytes_many_slashes, vec![b"bytes 1-2/500/600"], None::); - crate::__common_header_test!(test_bytes_many_dashes, + crate::http::header::common_header_test!(test_bytes_many_dashes, vec![b"bytes 1-2-3/500"], None::); diff --git a/src/http/header/content_type.rs b/src/http/header/content_type.rs index 65cb2a986..e1c419c22 100644 --- a/src/http/header/content_type.rs +++ b/src/http/header/content_type.rs @@ -1,7 +1,7 @@ use super::CONTENT_TYPE; use mime::Mime; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Content-Type` header, defined in /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-3.1.1.5) /// @@ -52,7 +52,7 @@ crate::__define_common_header! { (ContentType, CONTENT_TYPE) => [Mime] test_content_type { - crate::__common_header_test!( + crate::http::header::common_header_test!( test1, vec![b"text/html"], Some(HeaderField(mime::TEXT_HTML))); diff --git a/src/http/header/date.rs b/src/http/header/date.rs index 982a1455c..4d1717886 100644 --- a/src/http/header/date.rs +++ b/src/http/header/date.rs @@ -1,7 +1,7 @@ use super::{HttpDate, DATE}; use std::time::SystemTime; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Date` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.1.1.2) /// /// The `Date` header field represents the date and time at which the @@ -32,7 +32,7 @@ crate::__define_common_header! { (Date, DATE) => [HttpDate] test_date { - crate::__common_header_test!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]); + crate::http::header::common_header_test!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]); } } diff --git a/src/http/header/encoding.rs b/src/http/header/encoding.rs index aa49dea45..ce31c100f 100644 --- a/src/http/header/encoding.rs +++ b/src/http/header/encoding.rs @@ -1,7 +1,7 @@ use std::{fmt, str}; pub use self::Encoding::{ - Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, + Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, Zstd, }; /// A value to represent an encoding used in `Transfer-Encoding` @@ -22,6 +22,8 @@ pub enum Encoding { Identity, /// The `trailers` encoding. Trailers, + /// The `zstd` encoding. + Zstd, /// Some other encoding that is less common, can be any String. EncodingExt(String), } @@ -36,6 +38,7 @@ impl fmt::Display for Encoding { Compress => "compress", Identity => "identity", Trailers => "trailers", + Zstd => "zstd", EncodingExt(ref s) => s.as_ref(), }) } @@ -52,6 +55,7 @@ impl str::FromStr for Encoding { "compress" => Ok(Compress), "identity" => Ok(Identity), "trailers" => Ok(Trailers), + "zstd" => Ok(Zstd), _ => Ok(EncodingExt(s.to_owned())), } } diff --git a/src/http/header/etag.rs b/src/http/header/etag.rs index b121fe26f..aded72665 100644 --- a/src/http/header/etag.rs +++ b/src/http/header/etag.rs @@ -1,6 +1,6 @@ use super::{EntityTag, ETAG}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `ETag` header, defined in [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.3) /// /// The `ETag` header field in a response provides the current entity-tag @@ -50,50 +50,50 @@ crate::__define_common_header! { test_etag { // From the RFC - crate::__common_header_test!(test1, + crate::http::header::common_header_test!(test1, vec![b"\"xyzzy\""], Some(ETag(EntityTag::new(false, "xyzzy".to_owned())))); - crate::__common_header_test!(test2, + crate::http::header::common_header_test!(test2, vec![b"W/\"xyzzy\""], Some(ETag(EntityTag::new(true, "xyzzy".to_owned())))); - crate::__common_header_test!(test3, + crate::http::header::common_header_test!(test3, vec![b"\"\""], Some(ETag(EntityTag::new(false, "".to_owned())))); // Own tests - crate::__common_header_test!(test4, + crate::http::header::common_header_test!(test4, vec![b"\"foobar\""], Some(ETag(EntityTag::new(false, "foobar".to_owned())))); - crate::__common_header_test!(test5, + crate::http::header::common_header_test!(test5, vec![b"\"\""], Some(ETag(EntityTag::new(false, "".to_owned())))); - crate::__common_header_test!(test6, + crate::http::header::common_header_test!(test6, vec![b"W/\"weak-etag\""], Some(ETag(EntityTag::new(true, "weak-etag".to_owned())))); - crate::__common_header_test!(test7, + crate::http::header::common_header_test!(test7, vec![b"W/\"\x65\x62\""], Some(ETag(EntityTag::new(true, "\u{0065}\u{0062}".to_owned())))); - crate::__common_header_test!(test8, + crate::http::header::common_header_test!(test8, vec![b"W/\"\""], Some(ETag(EntityTag::new(true, "".to_owned())))); - crate::__common_header_test!(test9, + crate::http::header::common_header_test!(test9, vec![b"no-dquotes"], None::); - crate::__common_header_test!(test10, + crate::http::header::common_header_test!(test10, vec![b"w/\"the-first-w-is-case-sensitive\""], None::); - crate::__common_header_test!(test11, + crate::http::header::common_header_test!(test11, vec![b""], None::); - crate::__common_header_test!(test12, + crate::http::header::common_header_test!(test12, vec![b"\"unmatched-dquotes1"], None::); - crate::__common_header_test!(test13, + crate::http::header::common_header_test!(test13, vec![b"unmatched-dquotes2\""], None::); - crate::__common_header_test!(test14, + crate::http::header::common_header_test!(test14, vec![b"matched-\"dquotes\""], None::); - crate::__common_header_test!(test15, + crate::http::header::common_header_test!(test15, vec![b"\""], None::); } diff --git a/src/http/header/expires.rs b/src/http/header/expires.rs index 759e7d280..e810fe267 100644 --- a/src/http/header/expires.rs +++ b/src/http/header/expires.rs @@ -1,6 +1,6 @@ use super::{HttpDate, EXPIRES}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Expires` header, defined in [RFC7234](http://tools.ietf.org/html/rfc7234#section-5.3) /// /// The `Expires` header field gives the date/time after which the @@ -36,6 +36,6 @@ crate::__define_common_header! { test_expires { // Test case from RFC - crate::__common_header_test!(test1, vec![b"Thu, 01 Dec 1994 16:00:00 GMT"]); + crate::http::header::common_header_test!(test1, vec![b"Thu, 01 Dec 1994 16:00:00 GMT"]); } } diff --git a/src/http/header/if_match.rs b/src/http/header/if_match.rs index d4402715d..87a94a809 100644 --- a/src/http/header/if_match.rs +++ b/src/http/header/if_match.rs @@ -1,6 +1,6 @@ use super::{EntityTag, IF_MATCH}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `If-Match` header, defined in /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.1) /// @@ -53,18 +53,18 @@ crate::__define_common_header! { (IfMatch, IF_MATCH) => {Any / (EntityTag)+} test_if_match { - crate::__common_header_test!( + crate::http::header::common_header_test!( test1, vec![b"\"xyzzy\""], Some(HeaderField::Items( vec![EntityTag::new(false, "xyzzy".to_owned())]))); - crate::__common_header_test!( + crate::http::header::common_header_test!( test2, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""], Some(HeaderField::Items( vec![EntityTag::new(false, "xyzzy".to_owned()), EntityTag::new(false, "r2d2xxxx".to_owned()), EntityTag::new(false, "c3piozzzz".to_owned())]))); - crate::__common_header_test!(test3, vec![b"*"], Some(IfMatch::Any)); + crate::http::header::common_header_test!(test3, vec![b"*"], Some(IfMatch::Any)); } } diff --git a/src/http/header/if_modified_since.rs b/src/http/header/if_modified_since.rs index ba393032d..254003523 100644 --- a/src/http/header/if_modified_since.rs +++ b/src/http/header/if_modified_since.rs @@ -1,6 +1,6 @@ use super::{HttpDate, IF_MODIFIED_SINCE}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `If-Modified-Since` header, defined in /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.3) /// @@ -36,6 +36,6 @@ crate::__define_common_header! { test_if_modified_since { // Test case from RFC - crate::__common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); } } diff --git a/src/http/header/if_none_match.rs b/src/http/header/if_none_match.rs index f16b196cc..e1422bd36 100644 --- a/src/http/header/if_none_match.rs +++ b/src/http/header/if_none_match.rs @@ -1,6 +1,6 @@ use super::{EntityTag, IF_NONE_MATCH}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `If-None-Match` header, defined in /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.2) /// @@ -55,11 +55,11 @@ crate::__define_common_header! { (IfNoneMatch, IF_NONE_MATCH) => {Any / (EntityTag)+} test_if_none_match { - crate::__common_header_test!(test1, vec![b"\"xyzzy\""]); - crate::__common_header_test!(test2, vec![b"W/\"xyzzy\""]); - crate::__common_header_test!(test3, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]); - crate::__common_header_test!(test4, vec![b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]); - crate::__common_header_test!(test5, vec![b"*"]); + crate::http::header::common_header_test!(test1, vec![b"\"xyzzy\""]); + crate::http::header::common_header_test!(test2, vec![b"W/\"xyzzy\""]); + crate::http::header::common_header_test!(test3, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]); + crate::http::header::common_header_test!(test4, vec![b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]); + crate::http::header::common_header_test!(test5, vec![b"*"]); } } diff --git a/src/http/header/if_range.rs b/src/http/header/if_range.rs index 9612405e8..cf69e7269 100644 --- a/src/http/header/if_range.rs +++ b/src/http/header/if_range.rs @@ -113,7 +113,7 @@ mod test_if_range { use crate::http::header::*; use std::str; - crate::__common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); - crate::__common_header_test!(test2, vec![b"\"abc\""]); - crate::__common_header_test!(test3, vec![b"this-is-invalid"], None::); + crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + crate::http::header::common_header_test!(test2, vec![b"\"abc\""]); + crate::http::header::common_header_test!(test3, vec![b"this-is-invalid"], None::); } diff --git a/src/http/header/if_unmodified_since.rs b/src/http/header/if_unmodified_since.rs index 26b16b513..1cc7b304e 100644 --- a/src/http/header/if_unmodified_since.rs +++ b/src/http/header/if_unmodified_since.rs @@ -1,6 +1,6 @@ use super::{HttpDate, IF_UNMODIFIED_SINCE}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `If-Unmodified-Since` header, defined in /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.4) /// @@ -37,6 +37,6 @@ crate::__define_common_header! { test_if_unmodified_since { // Test case from RFC - crate::__common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); } } diff --git a/src/http/header/last_modified.rs b/src/http/header/last_modified.rs index 0de2fc06b..c43bf3ac9 100644 --- a/src/http/header/last_modified.rs +++ b/src/http/header/last_modified.rs @@ -1,6 +1,6 @@ use super::{HttpDate, LAST_MODIFIED}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Last-Modified` header, defined in /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.2) /// @@ -36,6 +36,6 @@ crate::__define_common_header! { test_last_modified { // Test case from RFC - crate::__common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); } } diff --git a/src/http/header/macros.rs b/src/http/header/macros.rs index 1718a8663..419d4fb6e 100644 --- a/src/http/header/macros.rs +++ b/src/http/header/macros.rs @@ -1,6 +1,4 @@ -#[doc(hidden)] -#[macro_export] -macro_rules! __common_header_deref { +macro_rules! common_header_deref { ($from:ty => $to:ty) => { impl ::std::ops::Deref for $from { type Target = $to; @@ -20,9 +18,7 @@ macro_rules! __common_header_deref { }; } -#[doc(hidden)] -#[macro_export] -macro_rules! __common_header_test_module { +macro_rules! common_header_test_module { ($id:ident, $tm:ident{$($tf:item)*}) => { #[allow(unused_imports)] #[cfg(test)] @@ -37,9 +33,8 @@ macro_rules! __common_header_test_module { } } -#[doc(hidden)] -#[macro_export] -macro_rules! __common_header_test { +#[cfg(test)] +macro_rules! common_header_test { ($id:ident, $raw:expr) => { #[test] fn $id() { @@ -99,9 +94,7 @@ macro_rules! __common_header_test { }; } -#[doc(hidden)] -#[macro_export] -macro_rules! __define_common_header { +macro_rules! common_header { // $a:meta: Attributes associated with the header item (usually docs) // $id:ident: Identifier of the header // $n:expr: Lowercase name of the header @@ -112,7 +105,7 @@ macro_rules! __define_common_header { $(#[$a])* #[derive(Clone, Debug, PartialEq)] pub struct $id(pub Vec<$item>); - crate::__common_header_deref!($id => Vec<$item>); + crate::http::header::common_header_deref!($id => Vec<$item>); impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { @@ -148,7 +141,7 @@ macro_rules! __define_common_header { $(#[$a])* #[derive(Clone, Debug, PartialEq)] pub struct $id(pub Vec<$item>); - crate::__common_header_deref!($id => Vec<$item>); + crate::http::header::common_header_deref!($id => Vec<$item>); impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { @@ -184,7 +177,7 @@ macro_rules! __define_common_header { $(#[$a])* #[derive(Clone, Debug, PartialEq)] pub struct $id(pub $value); - crate::__common_header_deref!($id => $value); + crate::http::header::common_header_deref!($id => $value); impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { @@ -267,34 +260,39 @@ macro_rules! __define_common_header { // optional test module ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)* $tm:ident{$($tf:item)*}) => { - crate::__define_common_header! { + crate::http::header::common_header! { $(#[$a])* ($id, $name) => ($item)* } - crate::__common_header_test_module! { $id, $tm { $($tf)* }} + crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} }; ($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)+ $tm:ident{$($tf:item)*}) => { - crate::__define_common_header! { + crate::http::header::common_header! { $(#[$a])* ($id, $n) => ($item)+ } - crate::__common_header_test_module! { $id, $tm { $($tf)* }} + crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} }; ($(#[$a:meta])*($id:ident, $name:expr) => [$item:ty] $tm:ident{$($tf:item)*}) => { - crate::__define_common_header! { + crate::http::header::common_header! { $(#[$a])* ($id, $name) => [$item] } - crate::__common_header_test_module! { $id, $tm { $($tf)* }} + crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} }; ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+} $tm:ident{$($tf:item)*}) => { - crate::__define_common_header! { + crate::http::header::common_header! { $(#[$a])* ($id, $name) => {Any / ($item)+} } - crate::__common_header_test_module! { $id, $tm { $($tf)* }} + crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} }; } + +pub(crate) use {common_header, common_header_deref, common_header_test_module}; + +#[cfg(test)] +pub(crate) use common_header_test; diff --git a/src/http/header/mod.rs b/src/http/header/mod.rs index 0e5651a77..79ba5772b 100644 --- a/src/http/header/mod.rs +++ b/src/http/header/mod.rs @@ -84,4 +84,8 @@ mod if_none_match; mod if_range; mod if_unmodified_since; mod last_modified; + mod macros; +#[cfg(test)] +pub(crate) use macros::common_header_test; +pub(crate) use macros::{common_header, common_header_deref, common_header_test_module}; diff --git a/src/info.rs b/src/info.rs index c9ddf6ec4..1f8263add 100644 --- a/src/info.rs +++ b/src/info.rs @@ -1,13 +1,68 @@ -use std::cell::Ref; +use std::{cell::Ref, convert::Infallible, net::SocketAddr}; -use crate::dev::{AppConfig, RequestHead}; -use crate::http::header::{self, HeaderName}; +use actix_utils::future::{err, ok, Ready}; +use derive_more::{Display, Error}; +use once_cell::sync::Lazy; -const X_FORWARDED_FOR: &[u8] = b"x-forwarded-for"; -const X_FORWARDED_HOST: &[u8] = b"x-forwarded-host"; -const X_FORWARDED_PROTO: &[u8] = b"x-forwarded-proto"; +use crate::{ + dev::{AppConfig, Payload, RequestHead}, + http::{ + header::{self, HeaderName}, + uri::{Authority, Scheme}, + }, + FromRequest, HttpRequest, ResponseError, +}; -/// `HttpRequest` connection information +static X_FORWARDED_FOR: Lazy = + Lazy::new(|| HeaderName::from_static("x-forwarded-for")); +static X_FORWARDED_HOST: Lazy = + Lazy::new(|| HeaderName::from_static("x-forwarded-host")); +static X_FORWARDED_PROTO: Lazy = + Lazy::new(|| HeaderName::from_static("x-forwarded-proto")); + +/// Trim whitespace then any quote marks. +fn unquote(val: &str) -> &str { + val.trim().trim_start_matches('"').trim_end_matches('"') +} + +/// Extracts and trims first value for given header name. +fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option<&'a str> { + let hdr = req.headers.get(name)?.to_str().ok()?; + let val = hdr.split(',').next()?.trim(); + Some(val) +} + +/// HTTP connection information. +/// +/// `ConnectionInfo` implements `FromRequest` and can be extracted in handlers. +/// +/// # Examples +/// ``` +/// # use actix_web::{HttpResponse, Responder}; +/// use actix_web::dev::ConnectionInfo; +/// +/// async fn handler(conn: ConnectionInfo) -> impl Responder { +/// match conn.host() { +/// "actix.rs" => HttpResponse::Ok().body("Welcome!"), +/// "admin.actix.rs" => HttpResponse::Ok().body("Admin portal."), +/// _ => HttpResponse::NotFound().finish() +/// } +/// } +/// # let _svc = actix_web::web::to(handler); +/// ``` +/// +/// # Implementation Notes +/// Parses `Forwarded` header information according to [RFC 7239][rfc7239] but does not try to +/// interpret the values for each property. As such, the getter methods on `ConnectionInfo` return +/// strings instead of IP addresses or other types to acknowledge that they may be +/// [obfuscated][rfc7239-63] or [unknown][rfc7239-62]. +/// +/// If the older, related headers are also present (eg. `X-Forwarded-For`), then `Forwarded` +/// is preferred. +/// +/// [rfc7239]: https://datatracker.ietf.org/doc/html/rfc7239 +/// [rfc7239-62]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.2 +/// [rfc7239-63]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.3 #[derive(Debug, Clone, Default)] pub struct ConnectionInfo { scheme: String, @@ -25,105 +80,75 @@ impl ConnectionInfo { Ref::map(req.extensions(), |e| e.get().unwrap()) } - #[allow(clippy::cognitive_complexity, clippy::borrow_interior_mutable_const)] fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo { let mut host = None; let mut scheme = None; let mut realip_remote_addr = None; - // load forwarded header - for hdr in req.headers.get_all(&header::FORWARDED) { - if let Ok(val) = hdr.to_str() { - for pair in val.split(';') { - for el in pair.split(',') { - let mut items = el.trim().splitn(2, '='); - if let Some(name) = items.next() { - if let Some(val) = items.next() { - match &name.to_lowercase() as &str { - "for" => { - if realip_remote_addr.is_none() { - realip_remote_addr = Some(val.trim()); - } - } - "proto" => { - if scheme.is_none() { - scheme = Some(val.trim()); - } - } - "host" => { - if host.is_none() { - host = Some(val.trim()); - } - } - _ => {} - } - } - } - } + for (name, val) in req + .headers + .get_all(&header::FORWARDED) + .into_iter() + .filter_map(|hdr| hdr.to_str().ok()) + // "for=1.2.3.4, for=5.6.7.8; scheme=https" + .flat_map(|val| val.split(';')) + // ["for=1.2.3.4, for=5.6.7.8", " scheme=https"] + .flat_map(|vals| vals.split(',')) + // ["for=1.2.3.4", " for=5.6.7.8", " scheme=https"] + .flat_map(|pair| { + let mut items = pair.trim().splitn(2, '='); + Some((items.next()?, items.next()?)) + }) + { + // [(name , val ), ... ] + // [("for", "1.2.3.4"), ("for", "5.6.7.8"), ("scheme", "https")] + + // taking the first value for each property is correct because spec states that first + // "for" value is client and rest are proxies; multiple values other properties have + // no defined semantics + // + // > In a chain of proxy servers where this is fully utilized, the first + // > "for" parameter will disclose the client where the request was first + // > made, followed by any subsequent proxy identifiers. + // --- https://datatracker.ietf.org/doc/html/rfc7239#section-5.2 + + match name.trim().to_lowercase().as_str() { + "for" => realip_remote_addr.get_or_insert_with(|| unquote(val)), + "proto" => scheme.get_or_insert_with(|| unquote(val)), + "host" => host.get_or_insert_with(|| unquote(val)), + "by" => { + // TODO: implement https://datatracker.ietf.org/doc/html/rfc7239#section-5.1 + continue; } - } + _ => continue, + }; } - // scheme - if scheme.is_none() { - if let Some(h) = req - .headers - .get(&HeaderName::from_lowercase(X_FORWARDED_PROTO).unwrap()) - { - if let Ok(h) = h.to_str() { - scheme = h.split(',').next().map(|v| v.trim()); - } - } - if scheme.is_none() { - scheme = req.uri.scheme().map(|a| a.as_str()); - if scheme.is_none() && cfg.secure() { - scheme = Some("https") - } - } - } + let scheme = scheme + .or_else(|| first_header_value(req, &*X_FORWARDED_PROTO)) + .or_else(|| req.uri.scheme().map(Scheme::as_str)) + .or_else(|| Some("https").filter(|_| cfg.secure())) + .unwrap_or("http") + .to_owned(); - // host - if host.is_none() { - if let Some(h) = req - .headers - .get(&HeaderName::from_lowercase(X_FORWARDED_HOST).unwrap()) - { - if let Ok(h) = h.to_str() { - host = h.split(',').next().map(|v| v.trim()); - } - } - if host.is_none() { - if let Some(h) = req.headers.get(&header::HOST) { - host = h.to_str().ok(); - } - if host.is_none() { - host = req.uri.authority().map(|a| a.as_str()); - if host.is_none() { - host = Some(cfg.host()); - } - } - } - } + let host = host + .or_else(|| first_header_value(req, &*X_FORWARDED_HOST)) + .or_else(|| req.headers.get(&header::HOST)?.to_str().ok()) + .or_else(|| req.uri.authority().map(Authority::as_str)) + .unwrap_or(cfg.host()) + .to_owned(); - // get remote_addraddr from socketaddr - let remote_addr = req.peer_addr.map(|addr| format!("{}", addr)); + let realip_remote_addr = realip_remote_addr + .or_else(|| first_header_value(req, &*X_FORWARDED_FOR)) + .map(str::to_owned); - if realip_remote_addr.is_none() { - if let Some(h) = req - .headers - .get(&HeaderName::from_lowercase(X_FORWARDED_FOR).unwrap()) - { - if let Ok(h) = h.to_str() { - realip_remote_addr = h.split(',').next().map(|v| v.trim()); - } - } - } + let remote_addr = req.peer_addr.map(|addr| addr.to_string()); ConnectionInfo { remote_addr, - scheme: scheme.unwrap_or("http").to_owned(), - host: host.unwrap_or("localhost").to_owned(), - realip_remote_addr: realip_remote_addr.map(|s| s.to_owned()), + scheme, + host, + realip_remote_addr, } } @@ -152,19 +177,16 @@ impl ConnectionInfo { &self.host } - /// remote_addr address of the request. + /// Remote address of the connection. /// - /// Get remote_addr address from socket address + /// Get remote_addr address from socket address. pub fn remote_addr(&self) -> Option<&str> { - if let Some(ref remote_addr) = self.remote_addr { - Some(remote_addr) - } else { - None - } + self.remote_addr.as_deref() } - /// Real ip remote addr of client initiated HTTP request. + + /// Real IP (remote address) of client that initiated request. /// - /// The addr is resolved through the following headers, in this order: + /// The address is resolved through the following headers, in this order: /// /// - Forwarded /// - X-Forwarded-For @@ -173,16 +195,72 @@ impl ConnectionInfo { /// # Security /// Do not use this function for security purposes, unless you can ensure the Forwarded and /// X-Forwarded-For headers cannot be spoofed by the client. If you want the client's socket - /// address explicitly, use - /// [`HttpRequest::peer_addr()`](super::web::HttpRequest::peer_addr()) instead. + /// address explicitly, use [`HttpRequest::peer_addr()`][peer_addr] instead. + /// + /// [peer_addr]: crate::web::HttpRequest::peer_addr() #[inline] pub fn realip_remote_addr(&self) -> Option<&str> { - if let Some(ref r) = self.realip_remote_addr { - Some(r) - } else if let Some(ref remote_addr) = self.remote_addr { - Some(remote_addr) - } else { - None + self.realip_remote_addr + .as_deref() + .or_else(|| self.remote_addr.as_deref()) + } +} + +impl FromRequest for ConnectionInfo { + type Error = Infallible; + type Future = Ready>; + type Config = (); + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ok(req.connection_info().clone()) + } +} + +/// Extractor for peer's socket address. +/// +/// Also see [`HttpRequest::peer_addr`]. +/// +/// # Examples +/// ``` +/// # use actix_web::Responder; +/// use actix_web::dev::PeerAddr; +/// +/// async fn handler(peer_addr: PeerAddr) -> impl Responder { +/// let socket_addr = peer_addr.0; +/// socket_addr.to_string() +/// } +/// # let _svc = actix_web::web::to(handler); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Display)] +#[display(fmt = "{}", _0)] +pub struct PeerAddr(pub SocketAddr); + +impl PeerAddr { + /// Unwrap into inner `SocketAddr` value. + pub fn into_inner(self) -> SocketAddr { + self.0 + } +} + +#[derive(Debug, Display, Error)] +#[non_exhaustive] +#[display(fmt = "Missing peer address")] +pub struct MissingPeerAddr; + +impl ResponseError for MissingPeerAddr {} + +impl FromRequest for PeerAddr { + type Error = MissingPeerAddr; + type Future = Ready>; + type Config = (); + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + match req.peer_addr() { + Some(addr) => ok(PeerAddr(addr)), + None => { + log::error!("Missing peer address."); + err(MissingPeerAddr) + } } } } @@ -192,13 +270,60 @@ mod tests { use super::*; use crate::test::TestRequest; + const X_FORWARDED_FOR: &str = "x-forwarded-for"; + const X_FORWARDED_HOST: &str = "x-forwarded-host"; + const X_FORWARDED_PROTO: &str = "x-forwarded-proto"; + #[test] - fn test_forwarded() { + fn info_default() { let req = TestRequest::default().to_http_request(); let info = req.connection_info(); assert_eq!(info.scheme(), "http"); assert_eq!(info.host(), "localhost:8080"); + } + #[test] + fn host_header() { + let req = TestRequest::default() + .insert_header((header::HOST, "rust-lang.org")) + .to_http_request(); + + let info = req.connection_info(); + assert_eq!(info.scheme(), "http"); + assert_eq!(info.host(), "rust-lang.org"); + assert_eq!(info.realip_remote_addr(), None); + } + + #[test] + fn x_forwarded_for_header() { + let req = TestRequest::default() + .insert_header((X_FORWARDED_FOR, "192.0.2.60")) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); + } + + #[test] + fn x_forwarded_host_header() { + let req = TestRequest::default() + .insert_header((X_FORWARDED_HOST, "192.0.2.60")) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.host(), "192.0.2.60"); + assert_eq!(info.realip_remote_addr(), None); + } + + #[test] + fn x_forwarded_proto_header() { + let req = TestRequest::default() + .insert_header((X_FORWARDED_PROTO, "https")) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.scheme(), "https"); + } + + #[test] + fn forwarded_header() { let req = TestRequest::default() .insert_header(( header::FORWARDED, @@ -212,31 +337,118 @@ mod tests { assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); let req = TestRequest::default() - .insert_header((header::HOST, "rust-lang.org")) + .insert_header(( + header::FORWARDED, + "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org", + )) .to_http_request(); let info = req.connection_info(); - assert_eq!(info.scheme(), "http"); + assert_eq!(info.scheme(), "https"); assert_eq!(info.host(), "rust-lang.org"); - assert_eq!(info.realip_remote_addr(), None); + assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); + } + #[test] + fn forwarded_case_sensitivity() { let req = TestRequest::default() - .insert_header((X_FORWARDED_FOR, "192.0.2.60")) + .insert_header((header::FORWARDED, "For=192.0.2.60")) .to_http_request(); let info = req.connection_info(); assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); + } + #[test] + fn forwarded_weird_whitespace() { let req = TestRequest::default() - .insert_header((X_FORWARDED_HOST, "192.0.2.60")) + .insert_header((header::FORWARDED, "for= 1.2.3.4; proto= https")) .to_http_request(); let info = req.connection_info(); - assert_eq!(info.host(), "192.0.2.60"); - assert_eq!(info.realip_remote_addr(), None); + assert_eq!(info.realip_remote_addr(), Some("1.2.3.4")); + assert_eq!(info.scheme(), "https"); let req = TestRequest::default() - .insert_header((X_FORWARDED_PROTO, "https")) + .insert_header((header::FORWARDED, " for = 1.2.3.4 ")) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.realip_remote_addr(), Some("1.2.3.4")); + } + + #[test] + fn forwarded_for_quoted() { + let req = TestRequest::default() + .insert_header((header::FORWARDED, r#"for="192.0.2.60:8080""#)) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.realip_remote_addr(), Some("192.0.2.60:8080")); + } + + #[test] + fn forwarded_for_ipv6() { + let req = TestRequest::default() + .insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]:4711""#)) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.realip_remote_addr(), Some("[2001:db8:cafe::17]:4711")); + } + + #[test] + fn forwarded_for_multiple() { + let req = TestRequest::default() + .insert_header((header::FORWARDED, "for=192.0.2.60, for=198.51.100.17")) + .to_http_request(); + let info = req.connection_info(); + // takes the first value + assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); + } + + #[test] + fn scheme_from_uri() { + let req = TestRequest::get() + .uri("https://actix.rs/test") .to_http_request(); let info = req.connection_info(); assert_eq!(info.scheme(), "https"); } + + #[test] + fn host_from_uri() { + let req = TestRequest::get() + .uri("https://actix.rs/test") + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.host(), "actix.rs"); + } + + #[test] + fn host_from_server_hostname() { + let mut req = TestRequest::get(); + req.set_server_hostname("actix.rs"); + let req = req.to_http_request(); + + let info = req.connection_info(); + assert_eq!(info.host(), "actix.rs"); + } + + #[actix_rt::test] + async fn conn_info_extract() { + let req = TestRequest::default() + .uri("https://actix.rs/test") + .to_http_request(); + let conn_info = ConnectionInfo::extract(&req).await.unwrap(); + assert_eq!(conn_info.scheme(), "https"); + assert_eq!(conn_info.host(), "actix.rs"); + } + + #[actix_rt::test] + async fn peer_addr_extract() { + let addr = "127.0.0.1:8080".parse().unwrap(); + let req = TestRequest::default().peer_addr(addr).to_http_request(); + let peer_addr = PeerAddr::extract(&req).await.unwrap(); + assert_eq!(peer_addr, PeerAddr(addr)); + + let req = TestRequest::default().to_http_request(); + let res = PeerAddr::extract(&req).await; + assert!(res.is_err()); + } } diff --git a/src/lib.rs b/src/lib.rs index 96e6ecbf8..714c759cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,6 @@ //! Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust. //! -//! ## Example -//! +//! # Examples //! ```no_run //! use actix_web::{get, web, App, HttpServer, Responder}; //! @@ -20,13 +19,12 @@ //! } //! ``` //! -//! ## Documentation & Community Resources -//! +//! # Documentation & Community Resources //! In addition to this API documentation, several other resources are available: //! //! * [Website & User Guide](https://actix.rs/) //! * [Examples Repository](https://github.com/actix/examples) -//! * [Community Chat on Gitter](https://gitter.im/actix/actix-web) +//! * [Community Chat on Discord](https://discord.gg/NWpN5mmg3x) //! //! To get started navigating the API docs, you may consider looking at the following pages first: //! @@ -43,13 +41,12 @@ //! structs represent HTTP requests and responses and expose methods for creating, inspecting, //! and otherwise utilizing them. //! -//! ## Features -//! +//! # Features //! * Supports *HTTP/1.x* and *HTTP/2* //! * Streaming and pipelining //! * Keep-alive and slow requests handling //! * Client/server [WebSockets](https://actix.rs/docs/websockets/) support -//! * Transparent content compression/decompression (br, gzip, deflate) +//! * Transparent content compression/decompression (br, gzip, deflate, zstd) //! * Powerful [request routing](https://actix.rs/docs/url-dispatch/) //! * Multipart streams //! * Static assets @@ -58,10 +55,11 @@ //! * Includes an async [HTTP client](https://docs.rs/awc/) //! * Runs on stable Rust 1.46+ //! -//! ## Crate Features -//! -//! * `compress` - content encoding compression support (enabled by default) +//! # Crate Features //! * `cookies` - cookies support (enabled by default) +//! * `compress-brotli` - brotli content encoding compression support (enabled by default) +//! * `compress-gzip` - gzip and deflate content encoding compression support (enabled by default) +//! * `compress-zstd` - zstd content encoding compression support (enabled by default) //! * `openssl` - HTTPS support via `openssl` crate, supports `HTTP/2` //! * `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2` //! * `secure-cookies` - secure cookies support @@ -75,10 +73,12 @@ mod app; mod app_service; mod config; mod data; +pub mod dev; pub mod error; mod extract; pub mod guard; mod handler; +mod helpers; pub mod http; mod info; pub mod middleware; @@ -97,7 +97,7 @@ pub(crate) mod types; pub mod web; pub use actix_http::Response as BaseHttpResponse; -pub use actix_http::{body, Error, HttpMessage, ResponseError}; +pub use actix_http::{body, HttpMessage}; #[doc(inline)] pub use actix_rt as rt; pub use actix_web_codegen::*; @@ -105,7 +105,7 @@ pub use actix_web_codegen::*; pub use cookie; pub use crate::app::App; -pub use crate::error::Result; +pub use crate::error::{Error, ResponseError, Result}; pub use crate::extract::FromRequest; pub use crate::request::HttpRequest; pub use crate::resource::Resource; @@ -116,102 +116,3 @@ pub use crate::scope::Scope; pub use crate::server::HttpServer; // TODO: is exposing the error directly really needed pub use crate::types::{Either, EitherExtractError}; - -pub mod dev { - //! The `actix-web` prelude for library developers - //! - //! The purpose of this module is to alleviate imports of many common actix - //! traits by adding a glob import to the top of actix heavy modules: - //! - //! ``` - //! # #![allow(unused_imports)] - //! use actix_web::dev::*; - //! ``` - - pub use crate::config::{AppConfig, AppService}; - #[doc(hidden)] - 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::types::form::UrlEncoded; - pub use crate::types::json::JsonBody; - pub use crate::types::readlines::Readlines; - - pub use actix_http::body::{Body, BodySize, MessageBody, ResponseBody, SizedStream}; - #[cfg(feature = "compress")] - pub use actix_http::encoding::Decoder as Decompress; - pub use actix_http::ResponseBuilder as BaseHttpResponseBuilder; - 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::{always_ready, forward_ready, Service, Transform}; - - pub(crate) fn insert_slash(mut patterns: Vec) -> Vec { - for path in &mut patterns { - if !path.is_empty() && !path.starts_with('/') { - path.insert(0, '/'); - }; - } - patterns - } - - use crate::http::header::ContentEncoding; - use actix_http::{Response, ResponseBuilder}; - - struct Enc(ContentEncoding); - - /// Helper trait that allows to set specific encoding for response. - pub trait BodyEncoding { - /// Get content encoding - fn get_encoding(&self) -> Option; - - /// Set content encoding - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self; - } - - impl BodyEncoding for ResponseBuilder { - fn get_encoding(&self) -> Option { - self.extensions().get::().map(|enc| enc.0) - } - - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(Enc(encoding)); - self - } - } - - impl BodyEncoding for Response { - fn get_encoding(&self) -> Option { - self.extensions().get::().map(|enc| enc.0) - } - - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(Enc(encoding)); - self - } - } - - impl BodyEncoding for crate::HttpResponseBuilder { - fn get_encoding(&self) -> Option { - self.extensions().get::().map(|enc| enc.0) - } - - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(Enc(encoding)); - self - } - } - - impl BodyEncoding for crate::HttpResponse { - fn get_encoding(&self) -> Option { - self.extensions().get::().map(|enc| enc.0) - } - - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(Enc(encoding)); - self - } - } -} diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs index 4f2f2a504..0a6256fe2 100644 --- a/src/middleware/compat.rs +++ b/src/middleware/compat.rs @@ -50,7 +50,7 @@ where T: Transform, T::Future: 'static, T::Response: MapServiceResponseBody, - Error: From, + T::Error: Into, { type Response = ServiceResponse; type Error = Error; @@ -75,7 +75,7 @@ impl Service for CompatMiddleware where S: Service, S::Response: MapServiceResponseBody, - Error: From, + S::Error: Into, { type Response = ServiceResponse; type Error = Error; @@ -99,12 +99,16 @@ impl Future for CompatMiddlewareFuture where Fut: Future>, T: MapServiceResponseBody, - Error: From, + E: Into, { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let res = ready!(self.project().fut.poll(cx))?; + let res = match ready!(self.project().fut.poll(cx)) { + Ok(res) => res, + Err(err) => return Poll::Ready(Err(err.into())), + }; + Poll::Ready(Ok(res.map_body())) } } @@ -140,7 +144,7 @@ mod tests { use crate::{web, App, HttpResponse}; #[actix_rt::test] - #[cfg(all(feature = "cookies", feature = "compress"))] + #[cfg(all(feature = "cookies", feature = "__compress"))] async fn test_scope_middleware() { use crate::middleware::Compress; @@ -163,7 +167,7 @@ mod tests { } #[actix_rt::test] - #[cfg(all(feature = "cookies", feature = "compress"))] + #[cfg(all(feature = "cookies", feature = "__compress"))] async fn test_resource_scope_middleware() { use crate::middleware::Compress; diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index f8514c7cc..a9128bc47 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -13,7 +13,6 @@ use actix_http::{ body::{MessageBody, ResponseBody}, encoding::Encoder, http::header::{ContentEncoding, ACCEPT_ENCODING}, - Error, }; use actix_service::{Service, Transform}; use actix_utils::future::{ok, Ready}; @@ -23,6 +22,7 @@ use pin_project::pin_project; use crate::{ dev::BodyEncoding, service::{ServiceRequest, ServiceResponse}, + Error, }; /// Middleware for compressing response payloads. @@ -200,8 +200,7 @@ impl AcceptEncoding { let mut encodings = raw .replace(' ', "") .split(',') - .map(|l| AcceptEncoding::new(l)) - .flatten() + .filter_map(|l| AcceptEncoding::new(l)) .collect::>(); encodings.sort(); diff --git a/src/middleware/err_handlers.rs b/src/middleware/err_handlers.rs index 88834f8ce..75cc819bc 100644 --- a/src/middleware/err_handlers.rs +++ b/src/middleware/err_handlers.rs @@ -13,8 +13,8 @@ use futures_core::{future::LocalBoxFuture, ready}; use crate::{ dev::{ServiceRequest, ServiceResponse}, - error::{Error, Result}, http::StatusCode, + Error, Result, }; /// Return type for [`ErrorHandlers`] custom handlers. diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index e24782f07..96a361fcf 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -14,7 +14,8 @@ pub use self::err_handlers::{ErrorHandlerResponse, ErrorHandlers}; pub use self::logger::Logger; pub use self::normalize::{NormalizePath, TrailingSlash}; -#[cfg(feature = "compress")] +#[cfg(feature = "__compress")] mod compress; -#[cfg(feature = "compress")] + +#[cfg(feature = "__compress")] pub use self::compress::Compress; diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index ec6c2a344..219af1c6a 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -137,58 +137,63 @@ where let original_path = head.uri.path(); - // Either adds a string to the end (duplicates will be removed anyways) or trims all slashes from the end - let path = match self.trailing_slash_behavior { - TrailingSlash::Always => original_path.to_string() + "/", - TrailingSlash::MergeOnly => original_path.to_string(), - TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(), - }; - - // normalize multiple /'s to one / - let path = self.merge_slash.replace_all(&path, "/"); - - // Ensure root paths are still resolvable. If resulting path is blank after previous step - // it means the path was one or more slashes. Reduce to single slash. - let path = if path.is_empty() { "/" } else { path.as_ref() }; - - // Check whether the path has been changed - // - // This check was previously implemented as string length comparison - // - // That approach fails when a trailing slash is added, - // and a duplicate slash is removed, - // since the length of the strings remains the same - // - // For example, the path "/v1//s" will be normalized to "/v1/s/" - // Both of the paths have the same length, - // so the change can not be deduced from the length comparison - if path != original_path { - let mut parts = head.uri.clone().into_parts(); - let query = parts.path_and_query.as_ref().and_then(|pq| pq.query()); - - let path = if let Some(q) = query { - Bytes::from(format!("{}?{}", path, q)) - } else { - Bytes::copy_from_slice(path.as_bytes()) + // An empty path here means that the URI has no valid path. We skip normalization in this + // case, because adding a path can make the URI invalid + if !original_path.is_empty() { + // Either adds a string to the end (duplicates will be removed anyways) or trims all + // slashes from the end + let path = match self.trailing_slash_behavior { + TrailingSlash::Always => format!("{}/", original_path), + TrailingSlash::MergeOnly => original_path.to_string(), + TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(), }; - parts.path_and_query = Some(PathAndQuery::from_maybe_shared(path).unwrap()); - let uri = Uri::from_parts(parts).unwrap(); - req.match_info_mut().get_mut().update(&uri); - req.head_mut().uri = uri; + // normalize multiple /'s to one / + let path = self.merge_slash.replace_all(&path, "/"); + + // Ensure root paths are still resolvable. If resulting path is blank after previous + // step it means the path was one or more slashes. Reduce to single slash. + let path = if path.is_empty() { "/" } else { path.as_ref() }; + + // Check whether the path has been changed + // + // This check was previously implemented as string length comparison + // + // That approach fails when a trailing slash is added, + // and a duplicate slash is removed, + // since the length of the strings remains the same + // + // For example, the path "/v1//s" will be normalized to "/v1/s/" + // Both of the paths have the same length, + // so the change can not be deduced from the length comparison + if path != original_path { + let mut parts = head.uri.clone().into_parts(); + let query = parts.path_and_query.as_ref().and_then(|pq| pq.query()); + + let path = match query { + Some(q) => Bytes::from(format!("{}?{}", path, q)), + None => Bytes::copy_from_slice(path.as_bytes()), + }; + parts.path_and_query = Some(PathAndQuery::from_maybe_shared(path).unwrap()); + + let uri = Uri::from_parts(parts).unwrap(); + req.match_info_mut().get_mut().update(&uri); + req.head_mut().uri = uri; + } } - self.service.call(req) } } #[cfg(test)] mod tests { + use actix_http::StatusCode; use actix_service::IntoService; use super::*; use crate::{ dev::ServiceRequest, + guard::fn_guard, test::{call_service, init_service, TestRequest}, web, App, HttpResponse, }; @@ -199,37 +204,34 @@ mod tests { App::new() .wrap(NormalizePath::default()) .service(web::resource("/").to(HttpResponse::Ok)) - .service(web::resource("/v1/something").to(HttpResponse::Ok)), + .service(web::resource("/v1/something").to(HttpResponse::Ok)) + .service( + web::resource("/v2/something") + .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .to(HttpResponse::Ok), + ), ) .await; - let req = TestRequest::with_uri("/").to_request(); - let res = call_service(&app, req).await; - assert!(res.status().is_success()); + let test_uris = vec![ + "/", + "/?query=test", + "///", + "/v1//something", + "/v1//something////", + "//v1/something", + "//v1//////something", + "/v2//something?query=test", + "/v2//something////?query=test", + "//v2/something?query=test", + "//v2//////something?query=test", + ]; - let req = TestRequest::with_uri("/?query=test").to_request(); - let res = call_service(&app, req).await; - assert!(res.status().is_success()); - - let req = TestRequest::with_uri("///").to_request(); - let res = call_service(&app, req).await; - assert!(res.status().is_success()); - - let req = TestRequest::with_uri("/v1//something////").to_request(); - let res = call_service(&app, req).await; - assert!(res.status().is_success()); - - let req2 = TestRequest::with_uri("//v1/something").to_request(); - let res2 = call_service(&app, req2).await; - assert!(res2.status().is_success()); - - let req3 = TestRequest::with_uri("//v1//////something").to_request(); - let res3 = call_service(&app, req3).await; - assert!(res3.status().is_success()); - - let req4 = TestRequest::with_uri("/v1//something").to_request(); - let res4 = call_service(&app, req4).await; - assert!(res4.status().is_success()); + for uri in test_uris { + let req = TestRequest::with_uri(uri).to_request(); + let res = call_service(&app, req).await; + assert!(res.status().is_success(), "Failed uri: {}", uri); + } } #[actix_rt::test] @@ -238,38 +240,114 @@ mod tests { App::new() .wrap(NormalizePath(TrailingSlash::Trim)) .service(web::resource("/").to(HttpResponse::Ok)) - .service(web::resource("/v1/something").to(HttpResponse::Ok)), + .service(web::resource("/v1/something").to(HttpResponse::Ok)) + .service( + web::resource("/v2/something") + .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .to(HttpResponse::Ok), + ), ) .await; - // root paths should still work - let req = TestRequest::with_uri("/").to_request(); - let res = call_service(&app, req).await; - assert!(res.status().is_success()); + let test_uris = vec![ + "/", + "///", + "/v1/something", + "/v1/something/", + "/v1/something////", + "//v1//something", + "//v1//something//", + "/v2/something?query=test", + "/v2/something/?query=test", + "/v2/something////?query=test", + "//v2//something?query=test", + "//v2//something//?query=test", + ]; - let req = TestRequest::with_uri("/?query=test").to_request(); - let res = call_service(&app, req).await; - assert!(res.status().is_success()); + for uri in test_uris { + let req = TestRequest::with_uri(uri).to_request(); + let res = call_service(&app, req).await; + assert!(res.status().is_success(), "Failed uri: {}", uri); + } + } - let req = TestRequest::with_uri("///").to_request(); - let res = call_service(&app, req).await; - assert!(res.status().is_success()); + #[actix_rt::test] + async fn trim_root_trailing_slashes_with_query() { + let app = init_service( + App::new().wrap(NormalizePath(TrailingSlash::Trim)).service( + web::resource("/") + .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .to(HttpResponse::Ok), + ), + ) + .await; - let req = TestRequest::with_uri("/v1/something////").to_request(); - let res = call_service(&app, req).await; - assert!(res.status().is_success()); + let test_uris = vec!["/?query=test", "//?query=test", "///?query=test"]; - let req2 = TestRequest::with_uri("/v1/something/").to_request(); - let res2 = call_service(&app, req2).await; - assert!(res2.status().is_success()); + for uri in test_uris { + let req = TestRequest::with_uri(uri).to_request(); + let res = call_service(&app, req).await; + assert!(res.status().is_success(), "Failed uri: {}", uri); + } + } - let req3 = TestRequest::with_uri("//v1//something//").to_request(); - let res3 = call_service(&app, req3).await; - assert!(res3.status().is_success()); + #[actix_rt::test] + async fn ensure_trailing_slash() { + let app = init_service( + App::new() + .wrap(NormalizePath(TrailingSlash::Always)) + .service(web::resource("/").to(HttpResponse::Ok)) + .service(web::resource("/v1/something/").to(HttpResponse::Ok)) + .service( + web::resource("/v2/something/") + .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .to(HttpResponse::Ok), + ), + ) + .await; - let req4 = TestRequest::with_uri("//v1//something").to_request(); - let res4 = call_service(&app, req4).await; - assert!(res4.status().is_success()); + let test_uris = vec![ + "/", + "///", + "/v1/something", + "/v1/something/", + "/v1/something////", + "//v1//something", + "//v1//something//", + "/v2/something?query=test", + "/v2/something/?query=test", + "/v2/something////?query=test", + "//v2//something?query=test", + "//v2//something//?query=test", + ]; + + for uri in test_uris { + let req = TestRequest::with_uri(uri).to_request(); + let res = call_service(&app, req).await; + assert!(res.status().is_success(), "Failed uri: {}", uri); + } + } + + #[actix_rt::test] + async fn ensure_root_trailing_slash_with_query() { + let app = init_service( + App::new() + .wrap(NormalizePath(TrailingSlash::Always)) + .service( + web::resource("/") + .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .to(HttpResponse::Ok), + ), + ) + .await; + + let test_uris = vec!["/?query=test", "//?query=test", "///?query=test"]; + + for uri in test_uris { + let req = TestRequest::with_uri(uri).to_request(); + let res = call_service(&app, req).await; + assert!(res.status().is_success(), "Failed uri: {}", uri); + } } #[actix_rt::test] @@ -279,7 +357,12 @@ mod tests { .wrap(NormalizePath(TrailingSlash::MergeOnly)) .service(web::resource("/").to(HttpResponse::Ok)) .service(web::resource("/v1/something").to(HttpResponse::Ok)) - .service(web::resource("/v1/").to(HttpResponse::Ok)), + .service(web::resource("/v1/").to(HttpResponse::Ok)) + .service( + web::resource("/v2/something") + .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .to(HttpResponse::Ok), + ), ) .await; @@ -295,15 +378,35 @@ mod tests { ("/v1////", true), ("//v1//", true), ("///v1", false), + ("/v2/something?query=test", true), + ("/v2/something/?query=test", false), + ("/v2/something//?query=test", false), + ("//v2//something?query=test", true), ]; - for (path, success) in tests { - let req = TestRequest::with_uri(path).to_request(); + for (uri, success) in tests { + let req = TestRequest::with_uri(uri).to_request(); let res = call_service(&app, req).await; - assert_eq!(res.status().is_success(), success); + assert_eq!(res.status().is_success(), success, "Failed uri: {}", uri); } } + #[actix_rt::test] + async fn no_path() { + let app = init_service( + App::new() + .wrap(NormalizePath::default()) + .service(web::resource("/").to(HttpResponse::Ok)), + ) + .await; + + // This URI will be interpreted as an authority form, i.e. there is no path nor scheme + // (https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3) + let req = TestRequest::with_uri("eh").to_request(); + let res = call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + } + #[actix_rt::test] async fn test_in_place_normalization() { let srv = |req: ServiceRequest| { @@ -316,21 +419,18 @@ mod tests { .await .unwrap(); - let req = TestRequest::with_uri("/v1//something////").to_srv_request(); - let res = normalize.call(req).await.unwrap(); - assert!(res.status().is_success()); + let test_uris = vec![ + "/v1//something////", + "///v1/something", + "//v1///something", + "/v1//something", + ]; - let req2 = TestRequest::with_uri("///v1/something").to_srv_request(); - let res2 = normalize.call(req2).await.unwrap(); - assert!(res2.status().is_success()); - - let req3 = TestRequest::with_uri("//v1///something").to_srv_request(); - let res3 = normalize.call(req3).await.unwrap(); - assert!(res3.status().is_success()); - - let req4 = TestRequest::with_uri("/v1//something").to_srv_request(); - let res4 = normalize.call(req4).await.unwrap(); - assert!(res4.status().is_success()); + for uri in test_uris { + let req = TestRequest::with_uri(uri).to_srv_request(); + let res = normalize.call(req).await.unwrap(); + assert!(res.status().is_success(), "Failed uri: {}", uri); + } } #[actix_rt::test] diff --git a/src/request.rs b/src/request.rs index c218388eb..803556e9a 100644 --- a/src/request.rs +++ b/src/request.rs @@ -7,7 +7,7 @@ use std::{ use actix_http::{ http::{HeaderMap, Method, Uri, Version}, - Error, Extensions, HttpMessage, Message, Payload, RequestHead, + Extensions, HttpMessage, Message, Payload, RequestHead, }; use actix_router::{Path, Url}; use actix_utils::future::{ok, Ready}; @@ -17,7 +17,7 @@ use smallvec::SmallVec; use crate::{ app_service::AppInitServiceState, config::AppConfig, error::UrlGenerationError, - extract::FromRequest, info::ConnectionInfo, rmap::ResourceMap, + info::ConnectionInfo, rmap::ResourceMap, Error, FromRequest, }; #[cfg(feature = "cookies")] @@ -60,18 +60,6 @@ impl HttpRequest { }), } } - - #[doc(hidden)] - pub fn __priv_test_new( - path: Path, - head: Message, - rmap: Rc, - config: AppConfig, - app_data: Rc, - ) -> HttpRequest { - let app_state = AppInitServiceState::new(rmap, config); - Self::new(path, head, app_state, app_data) - } } impl HttpRequest { @@ -123,11 +111,7 @@ impl HttpRequest { /// E.g., id=10 #[inline] pub fn query_string(&self) -> &str { - if let Some(query) = self.uri().query().as_ref() { - query - } else { - "" - } + self.uri().query().unwrap_or_default() } /// Get a reference to the Path parameters. @@ -356,11 +340,10 @@ impl Drop for HttpRequest { /// It is possible to get `HttpRequest` as an extractor handler parameter /// -/// ## Example -/// +/// # Examples /// ``` /// use actix_web::{web, App, HttpRequest}; -/// use serde_derive::Deserialize; +/// use serde::Deserialize; /// /// /// extract `Thing` from request /// async fn index(req: HttpRequest) -> String { @@ -723,6 +706,8 @@ mod tests { assert_eq!(body, Bytes::from_static(b"1")); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_extensions_dropped() { struct Tracker { diff --git a/src/request_data.rs b/src/request_data.rs index 6558cbb37..575dc1eb3 100644 --- a/src/request_data.rs +++ b/src/request_data.rs @@ -1,9 +1,8 @@ use std::{any::type_name, ops::Deref}; -use actix_http::error::Error; use actix_utils::future::{err, ok, Ready}; -use crate::{dev::Payload, error::ErrorInternalServerError, FromRequest, HttpRequest}; +use crate::{dev::Payload, error::ErrorInternalServerError, Error, FromRequest, HttpRequest}; /// Request-local data extractor. /// diff --git a/src/resource.rs b/src/resource.rs index e90ad2dd9..4e609f31a 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -3,7 +3,7 @@ use std::fmt; use std::future::Future; use std::rc::Rc; -use actix_http::{Error, Extensions}; +use actix_http::Extensions; use actix_router::IntoPattern; use actix_service::boxed::{self, BoxService, BoxServiceFactory}; use actix_service::{ @@ -13,14 +13,16 @@ use actix_service::{ use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; -use crate::dev::{insert_slash, AppService, HttpServiceFactory, ResourceDef}; -use crate::extract::FromRequest; -use crate::guard::Guard; -use crate::handler::Handler; -use crate::responder::Responder; -use crate::route::{Route, RouteService}; -use crate::service::{ServiceRequest, ServiceResponse}; -use crate::{data::Data, HttpResponse}; +use crate::{ + data::Data, + dev::{insert_slash, AppService, HttpServiceFactory, ResourceDef}, + guard::Guard, + handler::Handler, + responder::Responder, + route::{Route, RouteService}, + service::{ServiceRequest, ServiceResponse}, + Error, FromRequest, HttpResponse, +}; type HttpService = BoxService; type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; @@ -167,38 +169,38 @@ where self } - /// Provide resource specific data. This method allows to add extractor - /// configuration or specific state available via `Data` extractor. - /// Provided data is available for all routes registered for the current resource. - /// Resource data overrides data registered by `App::data()` method. - /// - /// ``` - /// use actix_web::{web, App}; - /// - /// /// extract text data from request - /// async fn index(body: String) -> String { - /// format!("Body {}!", body) - /// } - /// - /// fn main() { - /// let app = App::new().service( - /// web::resource("/index.html") - /// // limit size of the payload - /// .data(web::PayloadConfig::new(4096)) - /// .route( - /// web::get() - /// // register handler - /// .to(index) - /// )); - /// } - /// ``` - pub fn data(self, data: U) -> Self { - self.app_data(Data::new(data)) - } - /// Add resource data. /// - /// Data of different types from parent contexts will still be accessible. + /// Data of different types from parent contexts will still be accessible. Any `Data` types + /// set here can be extracted in handlers using the `Data` extractor. + /// + /// # Examples + /// ``` + /// use std::cell::Cell; + /// use actix_web::{web, App, HttpRequest, HttpResponse, Responder}; + /// + /// struct MyData { + /// count: std::cell::Cell, + /// } + /// + /// async fn handler(req: HttpRequest, counter: web::Data) -> impl Responder { + /// // note this cannot use the Data extractor because it was not added with it + /// let incr = *req.app_data::().unwrap(); + /// assert_eq!(incr, 3); + /// + /// // update counter using other value from app data + /// counter.count.set(counter.count.get() + incr); + /// + /// HttpResponse::Ok().body(counter.count.get().to_string()) + /// } + /// + /// let app = App::new().service( + /// web::resource("/") + /// .app_data(3usize) + /// .app_data(web::Data::new(MyData { count: Default::default() })) + /// .route(web::get().to(handler)) + /// ); + /// ``` pub fn app_data(mut self, data: U) -> Self { self.app_data .get_or_insert_with(Extensions::new) @@ -207,6 +209,14 @@ where self } + /// Add resource data after wrapping in `Data`. + /// + /// Deprecated in favor of [`app_data`](Self::app_data). + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] + pub fn data(self, data: U) -> Self { + self.app_data(Data::new(data)) + } + /// Register a new route and add handler. This route matches all requests. /// /// ``` @@ -222,7 +232,6 @@ where /// This is shortcut for: /// /// ``` - /// # extern crate actix_web; /// # use actix_web::*; /// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } /// App::new().service(web::resource("/").route(web::route().to(index))); @@ -391,34 +400,28 @@ where *rdef.name_mut() = name.clone(); } - config.register_service(rdef, guards, self, None) - } -} - -impl IntoServiceFactory for Resource -where - T: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - InitError = (), - >, -{ - fn into_factory(self) -> T { *self.factory_ref.borrow_mut() = Some(ResourceFactory { routes: self.routes, - app_data: self.app_data.map(Rc::new), default: self.default, }); - self.endpoint + let resource_data = self.app_data.map(Rc::new); + + // wraps endpoint service (including middleware) call and injects app data for this scope + let endpoint = apply_fn_factory(self.endpoint, move |mut req: ServiceRequest, srv| { + if let Some(ref data) = resource_data { + req.add_data_container(Rc::clone(data)); + } + + srv.call(req) + }); + + config.register_service(rdef, guards, endpoint, None) } } pub struct ResourceFactory { routes: Vec, - app_data: Option>, default: HttpNewService, } @@ -437,8 +440,6 @@ impl ServiceFactory for ResourceFactory { // construct route service factory futures let factory_fut = join_all(self.routes.iter().map(|route| route.new_service(()))); - let app_data = self.app_data.clone(); - Box::pin(async move { let default = default_fut.await?; let routes = factory_fut @@ -446,18 +447,13 @@ impl ServiceFactory for ResourceFactory { .into_iter() .collect::, _>>()?; - Ok(ResourceService { - routes, - app_data, - default, - }) + Ok(ResourceService { routes, default }) }) } } pub struct ResourceService { routes: Vec, - app_data: Option>, default: HttpService, } @@ -469,20 +465,12 @@ impl Service for ResourceService { actix_service::always_ready!(); fn call(&self, mut req: ServiceRequest) -> Self::Future { - for route in self.routes.iter() { + for route in &self.routes { if route.check(&mut req) { - if let Some(ref app_data) = self.app_data { - req.add_data_container(app_data.clone()); - } - return route.call(req); } } - if let Some(ref app_data) = self.app_data { - req.add_data_container(app_data.clone()); - } - self.default.call(req) } } @@ -519,11 +507,14 @@ mod tests { use actix_service::Service; use actix_utils::future::ok; - use crate::http::{header, HeaderValue, Method, StatusCode}; - use crate::middleware::DefaultHeaders; - use crate::service::ServiceRequest; - use crate::test::{call_service, init_service, TestRequest}; - use crate::{guard, web, App, Error, HttpResponse}; + use crate::{ + guard, + http::{header, HeaderValue, Method, StatusCode}, + middleware::DefaultHeaders, + service::{ServiceRequest, ServiceResponse}, + test::{call_service, init_service, TestRequest}, + web, App, Error, HttpMessage, HttpResponse, + }; #[actix_rt::test] async fn test_middleware() { @@ -690,6 +681,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::NO_CONTENT); } + // allow deprecated `{App, Resource}::data` + #[allow(deprecated)] #[actix_rt::test] async fn test_data() { let srv = init_service( @@ -722,6 +715,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); } + // allow deprecated `{App, Resource}::data` + #[allow(deprecated)] #[actix_rt::test] async fn test_data_default_service() { let srv = init_service( @@ -740,4 +735,39 @@ mod tests { let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } + + #[actix_rt::test] + async fn test_middleware_app_data() { + let srv = init_service( + App::new().service( + web::resource("test") + .app_data(1usize) + .wrap_fn(|req, srv| { + assert_eq!(req.app_data::(), Some(&1usize)); + req.extensions_mut().insert(1usize); + srv.call(req) + }) + .route(web::get().to(HttpResponse::Ok)) + .default_service(|req: ServiceRequest| async move { + let (req, _) = req.into_parts(); + + assert_eq!(req.extensions().get::(), Some(&1)); + + Ok(ServiceResponse::new( + req, + HttpResponse::BadRequest().finish(), + )) + }), + ), + ) + .await; + + let req = TestRequest::get().uri("/test").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::post().uri("/test").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } } diff --git a/src/responder.rs b/src/responder.rs index 8bf8d9ea0..c5852a501 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, fmt}; +use std::borrow::Cow; use actix_http::{ body::Body, @@ -6,7 +6,7 @@ use actix_http::{ }; use bytes::{Bytes, BytesMut}; -use crate::{error::InternalError, Error, HttpRequest, HttpResponse, HttpResponseBuilder}; +use crate::{Error, HttpRequest, HttpResponse, HttpResponseBuilder}; /// Trait implemented by types that can be converted to an HTTP response. /// @@ -226,15 +226,6 @@ impl Responder for CustomResponder { } } -impl Responder for InternalError -where - T: fmt::Debug + fmt::Display + 'static, -{ - fn respond_to(self, _: &HttpRequest) -> HttpResponse { - HttpResponse::from_error(self.into()) - } -} - #[cfg(test)] pub(crate) mod tests { use actix_service::Service; diff --git a/src/response/builder.rs b/src/response/builder.rs index b9a10c56b..56d30d9d0 100644 --- a/src/response/builder.rs +++ b/src/response/builder.rs @@ -1,13 +1,14 @@ use std::{ cell::{Ref, RefMut}, convert::TryInto, + error::Error as StdError, future::Future, pin::Pin, task::{Context, Poll}, }; use actix_http::{ - body::{Body, BodyStream}, + body::{AnyBody, BodyStream}, http::{ header::{self, HeaderName, IntoHeaderPair, IntoHeaderValue}, ConnectionType, Error as HttpError, StatusCode, @@ -32,7 +33,7 @@ use crate::{ /// /// This type can be used to construct an instance of `Response` through a builder-like pattern. pub struct HttpResponseBuilder { - res: Option>, + res: Option>, err: Option, #[cfg(feature = "cookies")] cookies: Option, @@ -310,7 +311,7 @@ impl HttpResponseBuilder { /// /// `HttpResponseBuilder` can not be used after this call. #[inline] - pub fn body>(&mut self, body: B) -> HttpResponse { + pub fn body>(&mut self, body: B) -> HttpResponse { match self.message_body(body.into()) { Ok(res) => res, Err(err) => HttpResponse::from_error(err), @@ -354,9 +355,9 @@ impl HttpResponseBuilder { pub fn streaming(&mut self, stream: S) -> HttpResponse where S: Stream> + Unpin + 'static, - E: Into + 'static, + E: Into> + 'static, { - self.body(Body::from_message(BodyStream::new(stream))) + self.body(AnyBody::from_message(BodyStream::new(stream))) } /// Set a json body and generate `Response` @@ -375,9 +376,9 @@ impl HttpResponseBuilder { self.insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)); } - self.body(Body::from(body)) + self.body(AnyBody::from(body)) } - Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err).into()), + Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)), } } @@ -386,7 +387,7 @@ impl HttpResponseBuilder { /// `HttpResponseBuilder` can not be used after this call. #[inline] pub fn finish(&mut self) -> HttpResponse { - self.body(Body::Empty) + self.body(AnyBody::Empty) } /// This method construct new `HttpResponseBuilder` @@ -405,7 +406,7 @@ impl HttpResponseBuilder { return None; } - self.res.as_mut().map(|res| res.head_mut()) + self.res.as_mut().map(Response::head_mut) } } @@ -415,7 +416,7 @@ impl From for HttpResponse { } } -impl From for Response { +impl From for Response { fn from(mut builder: HttpResponseBuilder) -> Self { builder.finish().into() } diff --git a/src/response/response.rs b/src/response/response.rs index 194e2dff8..9a3bb2874 100644 --- a/src/response/response.rs +++ b/src/response/response.rs @@ -8,7 +8,7 @@ use std::{ }; use actix_http::{ - body::{Body, MessageBody}, + body::{AnyBody, Body, MessageBody}, http::{header::HeaderMap, StatusCode}, Extensions, Response, ResponseHead, }; @@ -25,12 +25,12 @@ use { use crate::{error::Error, HttpResponseBuilder}; /// An HTTP Response -pub struct HttpResponse { +pub struct HttpResponse { res: Response, pub(crate) error: Option, } -impl HttpResponse { +impl HttpResponse { /// Create HTTP response builder with specific status. #[inline] pub fn build(status: StatusCode) -> HttpResponseBuilder { @@ -48,13 +48,11 @@ impl HttpResponse { /// Create an error response. #[inline] - pub fn from_error(error: Error) -> Self { - let res = error.as_response_error().error_response(); - - Self { - res, - error: Some(error), - } + pub fn from_error(error: impl Into) -> Self { + let error = error.into(); + let mut response = error.as_response_error().error_response(); + response.error = Some(error); + response } } @@ -238,7 +236,6 @@ impl HttpResponse { impl fmt::Debug for HttpResponse where B: MessageBody, - B::Error: Into, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("HttpResponse") diff --git a/src/route.rs b/src/route.rs index 0a297b456..d85b940bd 100644 --- a/src/route.rs +++ b/src/route.rs @@ -2,19 +2,19 @@ use std::{future::Future, rc::Rc}; -use actix_http::{http::Method, Error}; +use actix_http::http::Method; use actix_service::{ boxed::{self, BoxService, BoxServiceFactory}, - Service, ServiceFactory, + Service, ServiceFactory, ServiceFactoryExt, }; use futures_core::future::LocalBoxFuture; -use crate::extract::FromRequest; -use crate::guard::{self, Guard}; -use crate::handler::{Handler, HandlerService}; -use crate::responder::Responder; -use crate::service::{ServiceRequest, ServiceResponse}; -use crate::HttpResponse; +use crate::{ + guard::{self, Guard}, + handler::{Handler, HandlerService}, + service::{ServiceRequest, ServiceResponse}, + Error, FromRequest, HttpResponse, Responder, +}; /// Resource route definition /// @@ -128,9 +128,10 @@ impl Route { /// Set handler function, use request extractors for parameters. /// + /// # Examples /// ``` /// use actix_web::{web, http, App}; - /// use serde_derive::Deserialize; + /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// struct Info { @@ -154,7 +155,7 @@ impl Route { /// /// ``` /// # use std::collections::HashMap; - /// # use serde_derive::Deserialize; + /// # use serde::Deserialize; /// use actix_web::{web, App}; /// /// #[derive(Deserialize)] @@ -184,17 +185,67 @@ impl Route { self.service = boxed::factory(HandlerService::new(handler)); self } + + /// Set raw service to be constructed and called as the request handler. + /// + /// # Examples + /// ``` + /// # use std::convert::Infallible; + /// # use futures_util::future::LocalBoxFuture; + /// # use actix_web::{*, dev::*, http::header}; + /// struct HelloWorld; + /// + /// impl Service for HelloWorld { + /// type Response = ServiceResponse; + /// type Error = Infallible; + /// type Future = LocalBoxFuture<'static, Result>; + /// + /// always_ready!(); + /// + /// fn call(&self, req: ServiceRequest) -> Self::Future { + /// let (req, _) = req.into_parts(); + /// + /// let res = HttpResponse::Ok() + /// .insert_header(header::ContentType::plaintext()) + /// .body("Hello world!"); + /// + /// Box::pin(async move { Ok(ServiceResponse::new(req, res)) }) + /// } + /// } + /// + /// App::new().route( + /// "/", + /// web::get().service(fn_factory(|| async { Ok(HelloWorld) })), + /// ); + /// ``` + pub fn service(mut self, service_factory: S) -> Self + where + S: ServiceFactory< + ServiceRequest, + Response = ServiceResponse, + Error = E, + InitError = (), + Config = (), + > + 'static, + E: Into + 'static, + { + self.service = boxed::factory(service_factory.map_err(Into::into)); + self + } } #[cfg(test)] mod tests { - use std::time::Duration; + use std::{convert::Infallible, time::Duration}; use actix_rt::time::sleep; use bytes::Bytes; - use serde_derive::Serialize; + use futures_core::future::LocalBoxFuture; + use serde::Serialize; - use crate::http::{Method, StatusCode}; + use crate::dev::{always_ready, fn_factory, fn_service, Service}; + use crate::http::{header, Method, StatusCode}; + use crate::service::{ServiceRequest, ServiceResponse}; use crate::test::{call_service, init_service, read_body, TestRequest}; use crate::{error, web, App, HttpResponse}; @@ -215,7 +266,7 @@ mod tests { })) .route(web::post().to(|| async { sleep(Duration::from_millis(100)).await; - Ok::<_, ()>(HttpResponse::Created()) + Ok::<_, Infallible>(HttpResponse::Created()) })) .route(web::delete().to(|| async { sleep(Duration::from_millis(100)).await; @@ -268,4 +319,65 @@ mod tests { let body = read_body(resp).await; assert_eq!(body, Bytes::from_static(b"{\"name\":\"test\"}")); } + + #[actix_rt::test] + async fn test_service_handler() { + struct HelloWorld; + + impl Service for HelloWorld { + type Response = ServiceResponse; + type Error = crate::Error; + type Future = LocalBoxFuture<'static, Result>; + + always_ready!(); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let (req, _) = req.into_parts(); + + let res = HttpResponse::Ok() + .insert_header(header::ContentType::plaintext()) + .body("Hello world!"); + + Box::pin(async move { Ok(ServiceResponse::new(req, res)) }) + } + } + + let srv = init_service( + App::new() + .route( + "/hello", + web::get().service(fn_factory(|| async { Ok(HelloWorld) })), + ) + .route( + "/bye", + web::get().service(fn_factory(|| async { + Ok::<_, ()>(fn_service(|req: ServiceRequest| async { + let (req, _) = req.into_parts(); + + let res = HttpResponse::Ok() + .insert_header(header::ContentType::plaintext()) + .body("Goodbye, and thanks for all the fish!"); + + Ok::<_, Infallible>(ServiceResponse::new(req, res)) + })) + })), + ), + ) + .await; + + let req = TestRequest::get().uri("/hello").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + let body = read_body(resp).await; + assert_eq!(body, Bytes::from_static(b"Hello world!")); + + let req = TestRequest::get().uri("/bye").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + let body = read_body(resp).await; + assert_eq!( + body, + Bytes::from_static(b"Goodbye, and thanks for all the fish!") + ); + } } diff --git a/src/scope.rs b/src/scope.rs index 412c01d95..aa546c422 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,28 +1,23 @@ -use std::cell::RefCell; -use std::fmt; -use std::future::Future; -use std::rc::Rc; +use std::{cell::RefCell, fmt, future::Future, mem, rc::Rc}; 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, + boxed::{self, BoxService, BoxServiceFactory}, + IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt, Transform, }; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; -use crate::config::ServiceConfig; -use crate::data::Data; -use crate::dev::{AppService, HttpServiceFactory}; -use crate::error::Error; -use crate::guard::Guard; -use crate::resource::Resource; -use crate::rmap::ResourceMap; -use crate::route::Route; -use crate::service::{ - AppServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse, +use crate::{ + config::ServiceConfig, + data::Data, + dev::{AppService, HttpServiceFactory}, + guard::Guard, + rmap::ResourceMap, + service::{AppServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse}, + Error, Resource, Route, }; type Guards = Vec>; @@ -71,16 +66,17 @@ pub struct Scope { impl Scope { /// Create a new scope pub fn new(path: &str) -> Scope { - let fref = Rc::new(RefCell::new(None)); + let factory_ref = Rc::new(RefCell::new(None)); + Scope { - endpoint: ScopeEndpoint::new(fref.clone()), + endpoint: ScopeEndpoint::new(Rc::clone(&factory_ref)), rdef: path.to_string(), app_data: None, guards: Vec::new(), services: Vec::new(), default: None, external: Vec::new(), - factory_ref: fref, + factory_ref, } } } @@ -120,39 +116,38 @@ where self } - /// Set or override application data. Application data could be accessed - /// by using `Data` extractor where `T` is data type. - /// - /// ``` - /// use std::cell::Cell; - /// use actix_web::{web, App, HttpResponse, Responder}; - /// - /// struct MyData { - /// counter: Cell, - /// } - /// - /// async fn index(data: web::Data) -> impl Responder { - /// data.counter.set(data.counter.get() + 1); - /// HttpResponse::Ok() - /// } - /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app") - /// .data(MyData{ counter: Cell::new(0) }) - /// .service( - /// web::resource("/index.html").route( - /// web::get().to(index))) - /// ); - /// } - /// ``` - pub fn data(self, data: U) -> Self { - self.app_data(Data::new(data)) - } - /// Add scope data. /// - /// Data of different types from parent contexts will still be accessible. + /// Data of different types from parent contexts will still be accessible. Any `Data` types + /// set here can be extracted in handlers using the `Data` extractor. + /// + /// # Examples + /// ``` + /// use std::cell::Cell; + /// use actix_web::{web, App, HttpRequest, HttpResponse, Responder}; + /// + /// struct MyData { + /// count: std::cell::Cell, + /// } + /// + /// async fn handler(req: HttpRequest, counter: web::Data) -> impl Responder { + /// // note this cannot use the Data extractor because it was not added with it + /// let incr = *req.app_data::().unwrap(); + /// assert_eq!(incr, 3); + /// + /// // update counter using other value from app data + /// counter.count.set(counter.count.get() + incr); + /// + /// HttpResponse::Ok().body(counter.count.get().to_string()) + /// } + /// + /// let app = App::new().service( + /// web::scope("/app") + /// .app_data(3usize) + /// .app_data(web::Data::new(MyData { count: Default::default() })) + /// .route("/", web::get().to(handler)) + /// ); + /// ``` pub fn app_data(mut self, data: U) -> Self { self.app_data .get_or_insert_with(Extensions::new) @@ -161,15 +156,20 @@ where self } - /// Run external configuration as part of the scope building - /// process + /// Add scope data after wrapping in `Data`. /// - /// This function is useful for moving parts of configuration to a - /// different module or even library. For example, - /// some of the resource's configuration could be moved to different module. + /// Deprecated in favor of [`app_data`](Self::app_data). + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] + pub fn data(self, data: U) -> Self { + self.app_data(Data::new(data)) + } + + /// Run external configuration as part of the scope building process. + /// + /// This function is useful for moving parts of configuration to a different module or library. + /// For example, some of the resource's configuration could be moved to different module. /// /// ``` - /// # extern crate actix_web; /// use actix_web::{web, middleware, App, HttpResponse}; /// /// // this function could be located in different module @@ -190,18 +190,21 @@ where /// .route("/index.html", web::get().to(|| HttpResponse::Ok())); /// } /// ``` - pub fn configure(mut self, f: F) -> Self + pub fn configure(mut self, cfg_fn: F) -> Self where F: FnOnce(&mut ServiceConfig), { let mut cfg = ServiceConfig::new(); - f(&mut cfg); + cfg_fn(&mut cfg); + self.services.extend(cfg.services); self.external.extend(cfg.external); + // TODO: add Extensions::is_empty check and conditionally insert data self.app_data .get_or_insert_with(Extensions::new) .extend(cfg.app_data); + self } @@ -418,13 +421,12 @@ where let mut rmap = ResourceMap::new(ResourceDef::root_prefix(&self.rdef)); // external resources - for mut rdef in std::mem::take(&mut self.external) { + for mut rdef in mem::take(&mut self.external) { rmap.add(&mut rdef, None); } // complete scope pipeline creation *self.factory_ref.borrow_mut() = Some(ScopeFactory { - app_data: self.app_data.take().map(Rc::new), default, services: cfg .into_services() @@ -446,18 +448,28 @@ where Some(self.guards) }; + let scope_data = self.app_data.map(Rc::new); + + // wraps endpoint service (including middleware) call and injects app data for this scope + let endpoint = apply_fn_factory(self.endpoint, move |mut req: ServiceRequest, srv| { + if let Some(ref data) = scope_data { + req.add_data_container(Rc::clone(data)); + } + + srv.call(req) + }); + // register final service config.register_service( ResourceDef::root_prefix(&self.rdef), guards, - self.endpoint, + endpoint, Some(Rc::new(rmap)), ) } } pub struct ScopeFactory { - app_data: Option>, services: Rc<[(ResourceDef, HttpNewService, RefCell>)]>, default: Rc, } @@ -485,8 +497,6 @@ impl ServiceFactory for ScopeFactory { } })); - let app_data = self.app_data.clone(); - Box::pin(async move { let default = default_fut.await?; @@ -502,17 +512,12 @@ impl ServiceFactory for ScopeFactory { }) .finish(); - Ok(ScopeService { - app_data, - router, - default, - }) + Ok(ScopeService { router, default }) }) } } pub struct ScopeService { - app_data: Option>, router: Router>>, default: HttpService, } @@ -536,10 +541,6 @@ impl Service for ScopeService { true }); - if let Some(ref app_data) = self.app_data { - req.add_data_container(app_data.clone()); - } - if let Some((srv, _info)) = res { srv.call(req) } else { @@ -578,12 +579,15 @@ mod tests { use actix_utils::future::ok; use bytes::Bytes; - use crate::dev::Body; - use crate::http::{header, HeaderValue, Method, StatusCode}; - use crate::middleware::DefaultHeaders; - use crate::service::ServiceRequest; - use crate::test::{call_service, init_service, read_body, TestRequest}; - use crate::{guard, web, App, HttpRequest, HttpResponse}; + use crate::{ + dev::Body, + guard, + http::{header, HeaderValue, Method, StatusCode}, + middleware::DefaultHeaders, + service::{ServiceRequest, ServiceResponse}, + test::{call_service, init_service, read_body, TestRequest}, + web, App, HttpMessage, HttpRequest, HttpResponse, + }; #[actix_rt::test] async fn test_scope() { @@ -915,10 +919,7 @@ mod tests { async fn test_default_resource_propagation() { let srv = init_service( App::new() - .service( - web::scope("/app1") - .default_service(web::resource("").to(HttpResponse::BadRequest)), - ) + .service(web::scope("/app1").default_service(web::to(HttpResponse::BadRequest))) .service(web::scope("/app2")) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::MethodNotAllowed())) @@ -990,6 +991,43 @@ mod tests { ); } + #[actix_rt::test] + async fn test_middleware_app_data() { + let srv = init_service( + App::new().service( + web::scope("app") + .app_data(1usize) + .wrap_fn(|req, srv| { + assert_eq!(req.app_data::(), Some(&1usize)); + req.extensions_mut().insert(1usize); + srv.call(req) + }) + .route("/test", web::get().to(HttpResponse::Ok)) + .default_service(|req: ServiceRequest| async move { + let (req, _) = req.into_parts(); + + assert_eq!(req.extensions().get::(), Some(&1)); + + Ok(ServiceResponse::new( + req, + HttpResponse::BadRequest().finish(), + )) + }), + ), + ) + .await; + + let req = TestRequest::with_uri("/app/test").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/default").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } + + // allow deprecated {App, Scope}::data + #[allow(deprecated)] #[actix_rt::test] async fn test_override_data() { let srv = init_service(App::new().data(1usize).service( @@ -1008,6 +1046,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); } + // allow deprecated `{App, Scope}::data` + #[allow(deprecated)] #[actix_rt::test] async fn test_override_data_default_service() { let srv = init_service(App::new().data(1usize).service( diff --git a/src/server.rs b/src/server.rs index 44ae6f880..804b3e367 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,23 +1,25 @@ use std::{ any::Any, - cmp, fmt, io, + cmp, + error::Error as StdError, + fmt, io, marker::PhantomData, net, sync::{Arc, Mutex}, }; -use actix_http::{ - body::MessageBody, Error, Extensions, HttpService, KeepAlive, Request, Response, -}; +use actix_http::{body::MessageBody, Extensions, HttpService, KeepAlive, Request, Response}; use actix_server::{Server, ServerBuilder}; -use actix_service::{map_config, IntoServiceFactory, Service, ServiceFactory}; +use actix_service::{ + map_config, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _, +}; #[cfg(feature = "openssl")] use actix_tls::accept::openssl::{AlpnError, SslAcceptor, SslAcceptorBuilder}; #[cfg(feature = "rustls")] use actix_tls::accept::rustls::ServerConfig as RustlsServerConfig; -use crate::config::AppConfig; +use crate::{config::AppConfig, Error}; struct Socket { scheme: &'static str, @@ -81,7 +83,7 @@ where S::Service: 'static, // S::Service: 'static, B: MessageBody + 'static, - B::Error: Into, + B::Error: Into>, { /// Create new HTTP server with application factory pub fn new(factory: F) -> Self { @@ -290,18 +292,22 @@ where let c = cfg.lock().unwrap(); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); - let svc = HttpService::build() + let mut 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 + if let Some(handler) = on_connect_fn.clone() { + svc = svc.on_connect_ext(move |io: &_, ext: _| { + (handler)(io as &dyn Any, ext) + }) }; - svc.finish(map_config(factory(), move |_| { + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + svc.finish(map_config(fac, move |_| { AppConfig::new(false, host.clone(), addr) })) .tcp() @@ -356,7 +362,11 @@ where svc }; - svc.finish(map_config(factory(), move |_| { + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + svc.finish(map_config(fac, move |_| { AppConfig::new(true, host.clone(), addr) })) .openssl(acceptor.clone()) @@ -368,7 +378,7 @@ where #[cfg(feature = "rustls")] /// Use listener for accepting incoming tls connection requests /// - /// This method sets alpn protocols to "h2" and "http/1.1" + /// This method prepends alpn protocols "h2" and "http/1.1" to configured ones pub fn listen_rustls( self, lst: net::TcpListener, @@ -410,7 +420,11 @@ where svc }; - svc.finish(map_config(factory(), move |_| { + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + svc.finish(map_config(fac, move |_| { AppConfig::new(true, host.clone(), addr) })) .rustls(config.clone()) @@ -447,17 +461,15 @@ where } } - if !success { - if let Some(e) = err.take() { - Err(e) - } else { - Err(io::Error::new( - io::ErrorKind::Other, - "Can not bind to address.", - )) - } - } else { + if success { Ok(sockets) + } else if let Some(e) = err.take() { + Err(e) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + "Can not bind to address.", + )) } } @@ -482,7 +494,7 @@ where #[cfg(feature = "rustls")] /// Start listening for incoming tls connections. /// - /// This method sets alpn protocols to "h2" and "http/1.1" + /// This method prepends alpn protocols "h2" and "http/1.1" to configured ones pub fn bind_rustls( mut self, addr: A, @@ -523,17 +535,20 @@ where ); fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then({ - let svc = HttpService::build() + let mut 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 - }; + if let Some(handler) = on_connect_fn.clone() { + svc = svc + .on_connect_ext(move |io: &_, ext: _| (&*handler)(io as &dyn Any, ext)); + } - svc.finish(map_config(factory(), move |_| config.clone())) + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + svc.finish(map_config(fac, move |_| config.clone())) }) })?; Ok(self) @@ -568,14 +583,20 @@ where c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)), socket_addr, ); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + fn_service(|io: UnixStream| async { 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())), + .finish(map_config(fac, move |_| config.clone())), ) }, )?; + Ok(self) } } diff --git a/src/service.rs b/src/service.rs index b7f244797..c1bffac49 100644 --- a/src/service.rs +++ b/src/service.rs @@ -2,24 +2,23 @@ use std::cell::{Ref, RefMut}; use std::rc::Rc; use std::{fmt, net}; -use actix_http::body::{Body, MessageBody}; -use actix_http::http::{HeaderMap, Method, StatusCode, Uri, Version}; use actix_http::{ - Error, Extensions, HttpMessage, Payload, PayloadStream, RequestHead, Response, ResponseHead, + body::{AnyBody, MessageBody}, + http::{HeaderMap, Method, StatusCode, Uri, Version}, + Extensions, HttpMessage, Payload, PayloadStream, RequestHead, Response, ResponseHead, }; use actix_router::{IntoPattern, Path, Resource, ResourceDef, Url}; use actix_service::{IntoServiceFactory, ServiceFactory}; #[cfg(feature = "cookies")] use cookie::{Cookie, ParseError as CookieParseError}; -use crate::dev::insert_slash; -use crate::guard::Guard; -use crate::info::ConnectionInfo; -use crate::request::HttpRequest; -use crate::rmap::ResourceMap; use crate::{ config::{AppConfig, AppService}, - HttpResponse, + dev::insert_slash, + guard::Guard, + info::ConnectionInfo, + rmap::ResourceMap, + Error, HttpRequest, HttpResponse, }; pub trait HttpServiceFactory { @@ -74,18 +73,18 @@ impl ServiceRequest { Self { req, payload } } - /// Construct service request. - #[doc(hidden)] - pub fn __priv_test_new(req: HttpRequest, payload: Payload) -> Self { - Self::new(req, payload) - } - /// Deconstruct request into parts #[inline] pub fn into_parts(self) -> (HttpRequest, Payload) { (self.req, self.payload) } + /// Get mutable access to inner `HttpRequest` and `Payload` + #[inline] + pub fn parts_mut(&mut self) -> (&mut HttpRequest, &mut Payload) { + (&mut self.req, &mut self.payload) + } + /// Construct request from parts. pub fn from_parts(req: HttpRequest, payload: Payload) -> Self { Self { req, payload } @@ -168,11 +167,7 @@ impl ServiceRequest { /// E.g., id=10 #[inline] pub fn query_string(&self) -> &str { - if let Some(query) = self.uri().query().as_ref() { - query - } else { - "" - } + self.uri().query().unwrap_or_default() } /// Peer socket address. @@ -330,15 +325,15 @@ impl fmt::Debug for ServiceRequest { } } -pub struct ServiceResponse { +pub struct ServiceResponse { request: HttpRequest, response: HttpResponse, } -impl ServiceResponse { +impl ServiceResponse { /// Create service response from the error pub fn from_err>(err: E, request: HttpRequest) -> Self { - let response = HttpResponse::from_error(err.into()); + let response = HttpResponse::from_error(err); ServiceResponse { request, response } } } @@ -655,6 +650,8 @@ mod tests { assert_eq!(resp.status(), http::StatusCode::NOT_FOUND); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_service_data() { let srv = diff --git a/src/test.rs b/src/test.rs index de97dc8aa..634826d19 100644 --- a/src/test.rs +++ b/src/test.rs @@ -613,6 +613,11 @@ impl TestRequest { let req = self.to_request(); call_service(app, req).await } + + #[cfg(test)] + pub fn set_server_hostname(&mut self, host: &str) { + self.config.set_host(host) + } } #[cfg(test)] @@ -839,6 +844,8 @@ mod tests { assert!(res.status().is_success()); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_server_data() { async fn handler(data: web::Data) -> impl Responder { diff --git a/src/types/form.rs b/src/types/form.rs index d999b35d8..b618dc616 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -1,6 +1,7 @@ //! For URL encoded form helper documentation, see [`Form`]. use std::{ + borrow::Cow, fmt, future::Future, ops, @@ -16,7 +17,7 @@ use futures_core::{future::LocalBoxFuture, ready}; use futures_util::{FutureExt as _, StreamExt as _}; use serde::{de::DeserializeOwned, Serialize}; -#[cfg(feature = "compress")] +#[cfg(feature = "__compress")] use crate::dev::Decompress; use crate::{ error::UrlencodedError, extract::FromRequest, http::header::CONTENT_LENGTH, web, Error, @@ -80,6 +81,10 @@ use crate::{ /// }) /// } /// ``` +/// +/// # Panics +/// URL encoded forms consist of unordered `key=value` pairs, therefore they cannot be decoded into +/// any type which depends upon data ordering (eg. tuples). Trying to do so will result in a panic. #[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct Form(pub T); @@ -180,7 +185,7 @@ impl Responder for Form { Ok(body) => HttpResponse::Ok() .content_type(mime::APPLICATION_WWW_FORM_URLENCODED) .body(body), - Err(err) => HttpResponse::from_error(UrlencodedError::Serialize(err).into()), + Err(err) => HttpResponse::from_error(UrlencodedError::Serialize(err)), } } } @@ -259,9 +264,9 @@ impl Default for FormConfig { /// - content type is not `application/x-www-form-urlencoded` /// - content length is greater than [limit](UrlEncoded::limit()) pub struct UrlEncoded { - #[cfg(feature = "compress")] + #[cfg(feature = "__compress")] stream: Option>, - #[cfg(not(feature = "compress"))] + #[cfg(not(feature = "__compress"))] stream: Option, limit: usize, @@ -297,10 +302,15 @@ impl UrlEncoded { } }; - #[cfg(feature = "compress")] - let payload = Decompress::from_headers(payload.take(), req.headers()); - #[cfg(not(feature = "compress"))] - let payload = payload.take(); + let payload = { + cfg_if::cfg_if! { + if #[cfg(feature = "__compress")] { + Decompress::from_headers(payload.take(), req.headers()) + } else { + payload.take() + } + } + }; UrlEncoded { encoding, @@ -379,7 +389,7 @@ where } else { let body = encoding .decode_without_bom_handling_and_without_replacement(&body) - .map(|s| s.into_owned()) + .map(Cow::into_owned) .ok_or(UrlencodedError::Encoding)?; serde_urlencoded::from_str::(&body).map_err(UrlencodedError::Parse) diff --git a/src/types/json.rs b/src/types/json.rs index 5b2bda5fd..2bf953183 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -16,7 +16,7 @@ use serde::{de::DeserializeOwned, Serialize}; use actix_http::Payload; -#[cfg(feature = "compress")] +#[cfg(feature = "__compress")] use crate::dev::Decompress; use crate::{ error::{Error, JsonPayloadError}, @@ -127,7 +127,7 @@ impl Responder for Json { Ok(body) => HttpResponse::Ok() .content_type(mime::APPLICATION_JSON) .body(body), - Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err).into()), + Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)), } } } @@ -239,7 +239,7 @@ pub struct JsonConfig { } impl JsonConfig { - /// Set maximum accepted payload size. By default this limit is 32kB. + /// Set maximum accepted payload size. By default this limit is 2MB. pub fn limit(mut self, limit: usize) -> Self { self.limit = limit; self @@ -272,9 +272,11 @@ impl JsonConfig { } } +const DEFAULT_LIMIT: usize = 2_097_152; // 2 mb + /// Allow shared refs used as default. const DEFAULT_CONFIG: JsonConfig = JsonConfig { - limit: 32_768, // 2^15 bytes, (~32kB) + limit: DEFAULT_LIMIT, err_handler: None, content_type: None, }; @@ -297,9 +299,9 @@ pub enum JsonBody { Body { limit: usize, length: Option, - #[cfg(feature = "compress")] + #[cfg(feature = "__compress")] payload: Decompress, - #[cfg(not(feature = "compress"))] + #[cfg(not(feature = "__compress"))] payload: Payload, buf: BytesMut, _res: PhantomData, @@ -342,13 +344,18 @@ where // As the internal usage always call JsonBody::limit after JsonBody::new. // And limit check to return an error variant of JsonBody happens there. - #[cfg(feature = "compress")] - let payload = Decompress::from_headers(payload.take(), req.headers()); - #[cfg(not(feature = "compress"))] - let payload = payload.take(); + let payload = { + cfg_if::cfg_if! { + if #[cfg(feature = "__compress")] { + Decompress::from_headers(payload.take(), req.headers()) + } else { + payload.take() + } + } + }; JsonBody::Body { - limit: 32_768, + limit: DEFAULT_LIMIT, length, payload, buf: BytesMut::with_capacity(8192), @@ -356,7 +363,7 @@ where } } - /// Set maximum accepted payload size. The default limit is 32kB. + /// Set maximum accepted payload size. The default limit is 2MB. pub fn limit(self, limit: usize) -> Self { match self { JsonBody::Body { @@ -367,7 +374,10 @@ where } => { if let Some(len) = length { if len > limit { - return JsonBody::Error(Some(JsonPayloadError::Overflow)); + return JsonBody::Error(Some(JsonPayloadError::OverflowKnownLength { + length: len, + limit, + })); } } @@ -404,8 +414,11 @@ where match res { Some(chunk) => { let chunk = chunk?; - if (buf.len() + chunk.len()) > *limit { - return Poll::Ready(Err(JsonPayloadError::Overflow)); + let buf_len = buf.len() + chunk.len(); + if buf_len > *limit { + return Poll::Ready(Err(JsonPayloadError::Overflow { + limit: *limit, + })); } else { buf.extend_from_slice(&chunk); } @@ -444,7 +457,12 @@ mod tests { fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { match err { - JsonPayloadError::Overflow => matches!(other, JsonPayloadError::Overflow), + JsonPayloadError::Overflow { .. } => { + matches!(other, JsonPayloadError::Overflow { .. }) + } + JsonPayloadError::OverflowKnownLength { .. } => { + matches!(other, JsonPayloadError::OverflowKnownLength { .. }) + } JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), _ => false, } @@ -486,7 +504,7 @@ mod tests { }; let resp = HttpResponse::BadRequest().body(serde_json::to_string(&msg).unwrap()); - InternalError::from_response(err, resp.into()).into() + InternalError::from_response(err, resp).into() })) .to_http_parts(); @@ -537,7 +555,7 @@ mod tests { let s = Json::::from_request(&req, &mut pl).await; assert!(format!("{}", s.err().unwrap()) - .contains("Json payload size is bigger than allowed")); + .contains("JSON payload (16 bytes) is larger than allowed (limit: 10 bytes).")); let (req, mut pl) = TestRequest::default() .insert_header(( @@ -588,7 +606,30 @@ mod tests { let json = JsonBody::::new(&req, &mut pl, None) .limit(100) .await; - assert!(json_eq(json.err().unwrap(), JsonPayloadError::Overflow)); + assert!(json_eq( + json.err().unwrap(), + JsonPayloadError::OverflowKnownLength { + length: 10000, + limit: 100 + } + )); + + let (req, mut pl) = TestRequest::default() + .insert_header(( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + )) + .set_payload(Bytes::from_static(&[0u8; 1000])) + .to_http_parts(); + + let json = JsonBody::::new(&req, &mut pl, None) + .limit(100) + .await; + + assert!(json_eq( + json.err().unwrap(), + JsonPayloadError::Overflow { limit: 100 } + )); let (req, mut pl) = TestRequest::default() .insert_header(( @@ -685,6 +726,7 @@ mod tests { assert!(s.is_err()); let err_str = s.err().unwrap().to_string(); - assert!(err_str.contains("Json payload size is bigger than allowed")); + assert!(err_str + .contains("JSON payload (16 bytes) is larger than allowed (limit: 10 bytes).")); } } diff --git a/src/types/path.rs b/src/types/path.rs index a47801b3f..ebccf323e 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -2,14 +2,13 @@ use std::{fmt, ops, sync::Arc}; -use actix_http::error::Error; use actix_router::PathDeserializer; use actix_utils::future::{ready, Ready}; use serde::de; use crate::{ dev::Payload, - error::{ErrorNotFound, PathError}, + error::{Error, ErrorNotFound, PathError}, FromRequest, HttpRequest, }; @@ -103,8 +102,7 @@ where fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let error_handler = req .app_data::() - .map(|c| c.ehandler.clone()) - .unwrap_or(None); + .and_then(|c| c.ehandler.clone()); ready( de::Deserialize::deserialize(PathDeserializer::new(req.match_info())) @@ -295,11 +293,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(), - ) - .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 7af4db0b1..46ad96beb 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -1,6 +1,7 @@ //! Basic binary and string payload extractors. use std::{ + borrow::Cow, future::Future, pin::Pin, str, @@ -186,7 +187,7 @@ fn bytes_to_string(body: Bytes, encoding: &'static Encoding) -> Result, - #[cfg(feature = "compress")] + #[cfg(feature = "__compress")] stream: dev::Decompress, - #[cfg(not(feature = "compress"))] + #[cfg(not(feature = "__compress"))] stream: dev::Payload, buf: BytesMut, err: Option, @@ -309,10 +310,15 @@ impl HttpMessageBody { } } - #[cfg(feature = "compress")] - let stream = dev::Decompress::from_headers(payload.take(), req.headers()); - #[cfg(not(feature = "compress"))] - let stream = payload.take(); + let stream = { + cfg_if::cfg_if! { + if #[cfg(feature = "__compress")] { + dev::Decompress::from_headers(payload.take(), req.headers()) + } else { + payload.take() + } + } + }; HttpMessageBody { stream, @@ -390,6 +396,8 @@ mod tests { assert!(cfg.check_mimetype(&req).is_ok()); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_config_recall_locations() { async fn bytes_handler(_: Bytes) -> impl Responder { diff --git a/src/types/query.rs b/src/types/query.rs index 698c4d02f..ca300139a 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -118,8 +118,7 @@ where fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let error_handler = req .app_data::() - .map(|c| c.err_handler.clone()) - .unwrap_or(None); + .and_then(|c| c.err_handler.clone()); serde_urlencoded::from_str::(req.query_string()) .map(|val| ok(Query(val))) @@ -266,7 +265,7 @@ mod tests { let req = TestRequest::with_uri("/name/user1/") .app_data(QueryConfig::default().error_handler(|e, _| { let resp = HttpResponse::UnprocessableEntity().finish(); - InternalError::from_response(e, resp.into()).into() + InternalError::from_response(e, resp).into() })) .to_srv_request(); diff --git a/src/web.rs b/src/web.rs index 8662848a4..40ac46275 100644 --- a/src/web.rs +++ b/src/web.rs @@ -43,7 +43,6 @@ pub use crate::types::*; /// the exposed `Params` object: /// /// ``` -/// # extern crate actix_web; /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( diff --git a/tests/test_error_propagation.rs b/tests/test_error_propagation.rs new file mode 100644 index 000000000..3e7320920 --- /dev/null +++ b/tests/test_error_propagation.rs @@ -0,0 +1,99 @@ +use std::sync::Arc; + +use actix_utils::future::{ok, Ready}; +use actix_web::{ + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + get, + test::{call_service, init_service, TestRequest}, + ResponseError, +}; +use futures_core::future::LocalBoxFuture; +use futures_util::lock::Mutex; + +#[derive(Debug, Clone)] +pub struct MyError; + +impl ResponseError for MyError {} + +impl std::fmt::Display for MyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "A custom error") + } +} + +#[get("/test")] +async fn test() -> Result { + return Err(MyError.into()); +} + +#[derive(Clone)] +pub struct SpyMiddleware(Arc>>); + +impl Transform for SpyMiddleware +where + S: Service, Error = actix_web::Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + type Transform = Middleware; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ok(Middleware { + was_error: self.0.clone(), + service, + }) + } +} + +#[doc(hidden)] +pub struct Middleware { + was_error: Arc>>, + service: S, +} + +impl Service for Middleware +where + S: Service, Error = actix_web::Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + type Future = LocalBoxFuture<'static, Result>; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let lock = self.was_error.clone(); + let response_future = self.service.call(req); + Box::pin(async move { + let response = response_future.await; + if let Ok(success) = &response { + *lock.lock().await = Some(success.response().error().is_some()); + } + response + }) + } +} + +#[actix_rt::test] +async fn error_cause_should_be_propagated_to_middlewares() { + let lock = Arc::new(Mutex::new(None)); + let spy_middleware = SpyMiddleware(lock.clone()); + + let app = init_service( + actix_web::App::new() + .wrap(spy_middleware.clone()) + .service(test), + ) + .await; + + call_service(&app, TestRequest::with_uri("/test").to_request()).await; + + let was_error_captured = lock.lock().await.unwrap(); + assert!(was_error_captured); +} diff --git a/tests/test_server.rs b/tests/test_server.rs index c341aa0ce..afea39dd9 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -29,6 +29,7 @@ use openssl::{ x509::X509, }; use rand::{distributions::Alphanumeric, Rng}; +use zstd::stream::{read::Decoder as ZstdDecoder, write::Encoder as ZstdEncoder}; use actix_web::dev::BodyEncoding; use actix_web::middleware::{Compress, NormalizePath, TrailingSlash}; @@ -476,6 +477,125 @@ async fn test_body_brotli() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); } +#[actix_rt::test] +async fn test_body_zstd() { + let srv = actix_test::start_with(actix_test::config().h1(), || { + App::new() + .wrap(Compress::new(ContentEncoding::Zstd)) + .service(web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR)))) + }); + + // client request + let mut response = srv + .get("/") + .append_header((ACCEPT_ENCODING, "zstd")) + .no_decompress() + .send() + .await + .unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = response.body().await.unwrap(); + + // decode + let mut e = ZstdDecoder::new(&bytes[..]).unwrap(); + let mut dec = Vec::new(); + e.read_to_end(&mut dec).unwrap(); + assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); +} + +#[actix_rt::test] +async fn test_body_zstd_streaming() { + let srv = actix_test::start_with(actix_test::config().h1(), || { + App::new() + .wrap(Compress::new(ContentEncoding::Zstd)) + .service(web::resource("/").route(web::to(move || { + HttpResponse::Ok() + .streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) + }))) + }); + + // client request + let mut response = srv + .get("/") + .append_header((ACCEPT_ENCODING, "zstd")) + .no_decompress() + .send() + .await + .unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = response.body().await.unwrap(); + + // decode + let mut e = ZstdDecoder::new(&bytes[..]).unwrap(); + let mut dec = Vec::new(); + e.read_to_end(&mut dec).unwrap(); + assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); +} + +#[actix_rt::test] +async fn test_zstd_encoding() { + let srv = actix_test::start_with(actix_test::config().h1(), || { + App::new().service( + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + ) + }); + + let mut e = ZstdEncoder::new(Vec::new(), 5).unwrap(); + e.write_all(STR.as_ref()).unwrap(); + let enc = e.finish().unwrap(); + + // client request + let request = srv + .post("/") + .append_header((CONTENT_ENCODING, "zstd")) + .send_body(enc.clone()); + let mut response = request.await.unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = response.body().await.unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +} + +#[actix_rt::test] +async fn test_zstd_encoding_large() { + let data = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(320_000) + .map(char::from) + .collect::(); + + let srv = actix_test::start_with(actix_test::config().h1(), || { + App::new().service( + web::resource("/") + .app_data(web::PayloadConfig::new(320_000)) + .route(web::to(move |body: Bytes| { + HttpResponse::Ok().streaming(TestBody::new(body, 10240)) + })), + ) + }); + + let mut e = ZstdEncoder::new(Vec::new(), 5).unwrap(); + e.write_all(data.as_ref()).unwrap(); + let enc = e.finish().unwrap(); + + // client request + let request = srv + .post("/") + .append_header((CONTENT_ENCODING, "zstd")) + .send_body(enc.clone()); + let mut response = request.await.unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = response.body().limit(320_000).await.unwrap(); + assert_eq!(bytes, Bytes::from(data)); +} + #[actix_rt::test] async fn test_encoding() { let srv = actix_test::start_with(actix_test::config().h1(), || { @@ -759,7 +879,7 @@ async fn test_brotli_encoding_large_openssl() { assert_eq!(bytes, Bytes::from(data)); } -#[cfg(all(feature = "rustls", feature = "openssl"))] +#[cfg(feature = "rustls")] mod plus_rustls { use std::io::BufReader; @@ -908,6 +1028,8 @@ async fn test_normalize() { assert!(response.status().is_success()); } +// allow deprecated App::data +#[allow(deprecated)] #[actix_rt::test] async fn test_data_drop() { use std::sync::{