diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml new file mode 100644 index 000000000..548ec21b7 --- /dev/null +++ b/.github/workflows/ci-master.yml @@ -0,0 +1,66 @@ +name: CI (master only) + +on: + push: + branches: [master] + +jobs: + ci_feature_powerset_check: + name: Verify Feature Combinations + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.2.0 + + - name: Install cargo-hack + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-hack + + - name: check feature combinations + uses: actions-rs/cargo@v1 + with: { command: ci-check-all-feature-powerset } + + - name: check feature combinations + uses: actions-rs/cargo@v1 + with: { command: ci-check-all-feature-powerset-linux } + + coverage: + name: coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.2.0 + + - name: Generate coverage file + run: | + cargo install cargo-tarpaulin --vers "^0.13" + cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose + - name: Upload to Codecov + uses: codecov/codecov-action@v1 + with: { file: cobertura.xml } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9b98a7b8..fe464bf27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,68 +96,6 @@ jobs: cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean cargo-cache - ci_feature_powerset_check: - name: Verify Feature Combinations - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Install stable - uses: actions-rs/toolchain@v1 - with: - toolchain: stable-x86_64-unknown-linux-gnu - profile: minimal - override: true - - - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: { command: generate-lockfile } - - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.2.0 - - - name: Install cargo-hack - uses: actions-rs/cargo@v1 - with: - command: install - args: cargo-hack - - - name: check feature combinations - uses: actions-rs/cargo@v1 - with: { command: ci-check-all-feature-powerset } - - - name: check feature combinations - uses: actions-rs/cargo@v1 - with: { command: ci-check-all-feature-powerset-linux } - - coverage: - name: coverage - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Install stable - uses: actions-rs/toolchain@v1 - with: - toolchain: stable-x86_64-unknown-linux-gnu - profile: minimal - override: true - - - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: { command: generate-lockfile } - - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.2.0 - - - name: Generate coverage file - if: github.ref == 'refs/heads/master' - run: | - cargo install cargo-tarpaulin --vers "^0.13" - cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose - - name: Upload to Codecov - if: github.ref == 'refs/heads/master' - uses: codecov/codecov-action@v1 - with: { file: cobertura.xml } - rustdoc: name: doc tests runs-on: ubuntu-latest diff --git a/CHANGES.md b/CHANGES.md index 8e030819f..b6d3b103d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,32 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +- `guard::GuardContext` for use with the `Guard` trait. [#2552] +- `ServiceRequest::guard_ctx` for obtaining a guard context. [#2552] + +### Changed +- `Guard` trait now receives a `&GuardContext`. [#2552] +- `guard::fn_guard` functions now receives a `&GuardContext`. [#2552] +- Some guards now return `impl Guard` and their concrete types are made private: `guard::{Header}` and all the method guards. [#2552] +- The `Not` guard is now generic over the type of guard it wraps. [#2552] + +### Fixed +- Rename `ConnectionInfo::{remote_addr => peer_addr}`, deprecating the old name. [#2554] +- `ConnectionInfo::peer_addr` will not return the port number. [#2554] +- `ConnectionInfo::realip_remote_addr` will not return the port number if sourcing the IP from the peer's socket address. [#2554] + +[#2552]: https://github.com/actix/actix-web/pull/2552 +[#2554]: https://github.com/actix/actix-web/pull/2554 + + +## 4.0.0-beta.16 - 2021-12-27 +### Changed +- No longer require `Scope` service body type to be boxed. [#2523] +- No longer require `Resource` service body type to be boxed. [#2526] + +[#2523]: https://github.com/actix/actix-web/pull/2523 +[#2526]: https://github.com/actix/actix-web/pull/2526 ## 4.0.0-beta.15 - 2021-12-17 diff --git a/Cargo.toml b/Cargo.toml index 02bef3af6..b6ef184e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.15" +version = "4.0.0-beta.16" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" keywords = ["actix", "http", "web", "framework", "async"] @@ -72,12 +72,12 @@ experimental-io-uring = ["actix-server/io-uring"] actix-codec = "0.4.1" actix-macros = "0.2.3" actix-rt = "2.3" -actix-server = "2.0.0-rc.1" +actix-server = "2.0.0-rc.2" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true } +actix-tls = { version = "3.0.0", default-features = false, optional = true } -actix-http = "3.0.0-beta.16" +actix-http = "3.0.0-beta.17" actix-router = "0.5.0-beta.3" actix-web-codegen = "0.5.0-beta.6" @@ -89,7 +89,7 @@ derive_more = "0.99.5" encoding_rs = "0.8" futures-core = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false } -itoa = "0.4" +itoa = "1" language-tags = "0.3" once_cell = "1.5" log = "0.4" @@ -106,8 +106,8 @@ time = { version = "0.3", default-features = false, features = ["formatting"] } url = "2.1" [dev-dependencies] -actix-test = { version = "0.1.0-beta.9", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.14", features = ["openssl"] } +actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } +awc = { version = "3.0.0-beta.15", features = ["openssl"] } brotli2 = "0.3.2" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/README.md b/README.md index a9afbf386..f9d388f8b 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.15)](https://docs.rs/actix-web/4.0.0-beta.15) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.16)](https://docs.rs/actix-web/4.0.0-beta.16) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.15/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.15) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.16/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.16)
[![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) diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index ef8eba0fc..af6dcb415 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 0.6.0-beta.11 - 2021-12-27 +* No significant changes since `0.6.0-beta.10`. + + ## 0.6.0-beta.10 - 2021-12-11 - No significant changes since `0.6.0-beta.9`. diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index c74a8e9a6..bbd4fee22 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.0-beta.10" +version = "0.6.0-beta.11" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -22,10 +22,10 @@ path = "src/lib.rs" experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"] [dependencies] -actix-http = "3.0.0-beta.16" +actix-http = "3.0.0-beta.17" actix-service = "2" actix-utils = "3" -actix-web = { version = "4.0.0-beta.15", default-features = false } +actix-web = { version = "4.0.0-beta.16", default-features = false } askama_escape = "0.10" bitflags = "1" @@ -43,5 +43,5 @@ tokio-uring = { version = "0.1", optional = true } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.9" -actix-web = "4.0.0-beta.15" +actix-test = "0.1.0-beta.10" +actix-web = "4.0.0-beta.16" diff --git a/actix-files/README.md b/actix-files/README.md index d686e255c..db5c94d1e 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,11 +3,11 @@ > 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.10)](https://docs.rs/actix-files/0.6.0-beta.10) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.11)](https://docs.rs/actix-files/0.6.0-beta.11) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.10/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.10) +[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.11/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.11) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index f6e1c2e11..057dbe5a3 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -1,8 +1,8 @@ use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc}; -use actix_service::Service; use actix_web::{ - dev::{ServiceRequest, ServiceResponse}, + body::BoxBody, + dev::{Service, ServiceRequest, ServiceResponse}, error::Error, guard::Guard, http::{header, Method}, @@ -94,7 +94,7 @@ impl fmt::Debug for FilesService { } impl Service for FilesService { - type Response = ServiceResponse; + type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; @@ -103,7 +103,7 @@ impl Service for FilesService { fn call(&self, req: ServiceRequest) -> Self::Future { let is_method_valid = if let Some(guard) = &self.guards { // execute user defined guards - (**guard).check(req.head()) + (**guard).check(&req.guard_ctx()) } else { // default behavior matches!(*req.method(), Method::HEAD | Method::GET) diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 4e86e20e8..8c6a63b72 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -3,6 +3,12 @@ ## Unreleased - 2021-xx-xx +## 3.0.0-beta.10 - 2021-12-27 +- Update `actix-server` to `2.0.0-rc.2`. [#2550] + +[#2550]: https://github.com/actix/actix-web/pull/2550 + + ## 3.0.0-beta.9 - 2021-12-11 - No significant changes since `3.0.0-beta.8`. diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 0c205fc2a..5c58978ea 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http-test" -version = "3.0.0-beta.9" +version = "3.0.0-beta.10" authors = ["Nikolay Kim "] description = "Various helpers for Actix applications to use during testing" keywords = ["http", "web", "framework", "async", "futures"] @@ -31,11 +31,11 @@ openssl = ["tls-openssl", "awc/openssl"] [dependencies] actix-service = "2.0.0" actix-codec = "0.4.1" -actix-tls = "3.0.0-rc.1" +actix-tls = "3.0.0" actix-utils = "3.0.0" actix-rt = "2.2" -actix-server = "2.0.0-rc.1" -awc = { version = "3.0.0-beta.14", default-features = false } +actix-server = "2.0.0-rc.2" +awc = { version = "3.0.0-beta.15", default-features = false } base64 = "0.13" bytes = "1" @@ -48,8 +48,8 @@ serde_json = "1.0" slab = "0.4" serde_urlencoded = "0.7" tls-openssl = { version = "0.10.9", package = "openssl", optional = true } -tokio = { version = "1.2", features = ["sync"] } +tokio = { version = "1.8.4", features = ["sync"] } [dev-dependencies] -actix-web = { version = "4.0.0-beta.15", default-features = false, features = ["cookies"] } -actix-http = "3.0.0-beta.16" +actix-web = { version = "4.0.0-beta.16", default-features = false, features = ["cookies"] } +actix-http = "3.0.0-beta.17" diff --git a/actix-http-test/README.md b/actix-http-test/README.md index a0a8f1cd8..589c54c23 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -3,11 +3,11 @@ > Various helpers for Actix applications to use during testing. [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) -[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.9)](https://docs.rs/actix-http-test/3.0.0-beta.9) +[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.10)](https://docs.rs/actix-http-test/3.0.0-beta.10) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
-[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.9) +[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.10) [![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) diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index 03239ece6..8636ef9c4 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -12,7 +12,7 @@ use std::{net, thread, time::Duration}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_rt::{net::TcpStream, System}; -use actix_server::{Server, ServiceFactory}; +use actix_server::{Server, ServerServiceFactory}; use awc::{ error::PayloadError, http::header::HeaderMap, ws, Client, ClientRequest, ClientResponse, Connector, @@ -51,13 +51,13 @@ use tokio::sync::mpsc; /// assert!(response.status().is_success()); /// } /// ``` -pub async fn test_server>(factory: F) -> TestServer { +pub async fn test_server>(factory: F) -> TestServer { let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); test_server_with_addr(tcp, factory).await } /// Start [`test server`](test_server()) on an existing address binding. -pub async fn test_server_with_addr>( +pub async fn test_server_with_addr>( tcp: net::TcpListener, factory: F, ) -> TestServer { diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index c7a236a5f..91f41dd07 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -2,11 +2,25 @@ ## Unreleased - 2021-xx-xx ### Changes -- `HeaderMap::get_all` now returns a `std::slice::Iter`. [#2527] - `impl Eq` for `header::ContentEncoding`. [#2501] [#2501]: https://github.com/actix/actix-web/pull/2501 + + +## 3.0.0-beta.17 - 2021-12-27 +### Changes +- `HeaderMap::get_all` now returns a `std::slice::Iter`. [#2527] +- `Payload` inner fields are now named. [#2545] +- `impl Stream` for `Payload` no longer requires the `Stream` variant be `Unpin`. [#2545] +- `impl Future` for `h1::SendResponse` no longer requires the body type be `Unpin`. [#2545] +- `impl Stream` for `encoding::Decoder` no longer requires the stream type be `Unpin`. [#2545] +- Rename `PayloadStream` to `BoxedPayloadStream`. [#2545] + +### Removed +- `h1::Payload::readany`. [#2545] + [#2527]: https://github.com/actix/actix-web/pull/2527 +[#2545]: https://github.com/actix/actix-web/pull/2545 ## 3.0.0-beta.16 - 2021-12-17 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 9f93bf6d2..9e587890b 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.0.0-beta.16" +version = "3.0.0-beta.17" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" keywords = ["actix", "http", "framework", "async", "futures"] @@ -55,12 +55,11 @@ bytestring = "1" derive_more = "0.99.5" encoding_rs = "0.8" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } -futures-task = { version = "0.3.7", default-features = false, features = ["alloc"] } h2 = "0.3.9" http = "0.2.5" httparse = "1.5.1" httpdate = "1.0.1" -itoa = "0.4" +itoa = "1" language-tags = "0.3" local-channel = "0.1" log = "0.4" @@ -68,11 +67,11 @@ mime = "0.3" percent-encoding = "2.1" pin-project-lite = "0.2" rand = "0.8" -sha-1 = "0.9" +sha-1 = "0.10" smallvec = "1.6.1" # tls -actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true } +actix-tls = { version = "3.0.0", default-features = false, optional = true } # compression brotli2 = { version="0.3.2", optional = true } @@ -80,10 +79,10 @@ flate2 = { version = "1.0.13", optional = true } zstd = { version = "0.9", optional = true } [dev-dependencies] -actix-http-test = { version = "3.0.0-beta.9", features = ["openssl"] } -actix-server = "2.0.0-rc.1" -actix-tls = { version = "3.0.0-rc.1", features = ["openssl"] } -actix-web = "4.0.0-beta.15" +actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] } +actix-server = "2.0.0-rc.2" +actix-tls = { version = "3.0.0", features = ["openssl"] } +actix-web = "4.0.0-beta.16" async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } @@ -97,7 +96,7 @@ serde_json = "1.0" static_assertions = "1" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } -tokio = { version = "1.2", features = ["net", "rt", "macros"] } +tokio = { version = "1.8.4", features = ["net", "rt", "macros"] } [[example]] name = "ws" diff --git a/actix-http/README.md b/actix-http/README.md index 05edffd2c..223e18ceb 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -3,11 +3,11 @@ > 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.16)](https://docs.rs/actix-http/3.0.0-beta.16) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.17)](https://docs.rs/actix-http/3.0.0-beta.17) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
-[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.16/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.16) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.17/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.17) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index a46e330c9..0f519637a 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -28,11 +28,14 @@ use crate::{ const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049; -pub struct Decoder { - decoder: Option, - stream: S, - eof: bool, - fut: Option, ContentDecoder), io::Error>>>, +pin_project_lite::pin_project! { + pub struct Decoder { + decoder: Option, + #[pin] + stream: S, + eof: bool, + fut: Option, ContentDecoder), io::Error>>>, + } } impl Decoder @@ -89,42 +92,44 @@ where impl Stream for Decoder where - S: Stream> + Unpin, + S: Stream>, { type Item = Result; - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.project(); + loop { - if let Some(ref mut fut) = self.fut { + if let Some(ref mut fut) = this.fut { let (chunk, decoder) = ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??; - self.decoder = Some(decoder); - self.fut.take(); + *this.decoder = Some(decoder); + this.fut.take(); if let Some(chunk) = chunk { return Poll::Ready(Some(Ok(chunk))); } } - if self.eof { + if *this.eof { return Poll::Ready(None); } - match ready!(Pin::new(&mut self.stream).poll_next(cx)) { + match ready!(this.stream.as_mut().poll_next(cx)) { Some(Err(err)) => return Poll::Ready(Some(Err(err))), Some(Ok(chunk)) => { - if let Some(mut decoder) = self.decoder.take() { + if let Some(mut decoder) = this.decoder.take() { if chunk.len() < MAX_CHUNK_SIZE_DECODE_IN_PLACE { let chunk = decoder.feed_data(chunk)?; - self.decoder = Some(decoder); + *this.decoder = Some(decoder); if let Some(chunk) = chunk { return Poll::Ready(Some(Ok(chunk))); } } else { - self.fut = Some(spawn_blocking(move || { + *this.fut = Some(spawn_blocking(move || { let chunk = decoder.feed_data(chunk)?; Ok((chunk, decoder)) })); @@ -137,9 +142,9 @@ where } None => { - self.eof = true; + *this.eof = true; - return if let Some(mut decoder) = self.decoder.take() { + return if let Some(mut decoder) = this.decoder.take() { match decoder.feed_eof() { Ok(Some(res)) => Poll::Ready(Some(Ok(res))), Ok(None) => Poll::Ready(None), diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 5c0cb64af..13055f08a 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -646,10 +646,11 @@ where Payload is attached to Request and passed to Service::call where the state can be collected and consumed. */ - let (ps, pl) = Payload::create(false); - let (req1, _) = req.replace_payload(crate::Payload::H1(pl)); + let (sender, payload) = Payload::create(false); + let (req1, _) = + req.replace_payload(crate::Payload::H1 { payload }); req = req1; - *this.payload = Some(ps); + *this.payload = Some(sender); } // Request has no payload. diff --git a/actix-http/src/h1/payload.rs b/actix-http/src/h1/payload.rs index f912e0ba3..4d031c15a 100644 --- a/actix-http/src/h1/payload.rs +++ b/actix-http/src/h1/payload.rs @@ -1,9 +1,12 @@ //! Payload stream -use std::cell::RefCell; -use std::collections::VecDeque; -use std::pin::Pin; -use std::rc::{Rc, Weak}; -use std::task::{Context, Poll, Waker}; + +use std::{ + cell::RefCell, + collections::VecDeque, + pin::Pin, + rc::{Rc, Weak}, + task::{Context, Poll, Waker}, +}; use bytes::Bytes; use futures_core::Stream; @@ -22,39 +25,32 @@ pub enum PayloadStatus { /// Buffered stream of bytes chunks /// -/// Payload stores chunks in a vector. First chunk can be received with -/// `.readany()` method. Payload stream is not thread safe. Payload does not -/// notify current task when new data is available. +/// Payload stores chunks in a vector. First chunk can be received with `poll_next`. Payload does +/// not notify current task when new data is available. /// -/// Payload stream can be used as `Response` body stream. +/// Payload can be used as `Response` body stream. #[derive(Debug)] pub struct Payload { inner: Rc>, } impl Payload { - /// Create payload stream. + /// Creates a payload stream. /// - /// This method construct two objects responsible for bytes stream - /// generation. - /// - /// * `PayloadSender` - *Sender* side of the stream - /// - /// * `Payload` - *Receiver* side of the stream + /// This method construct two objects responsible for bytes stream generation: + /// - `PayloadSender` - *Sender* side of the stream + /// - `Payload` - *Receiver* side of the stream pub fn create(eof: bool) -> (PayloadSender, Payload) { let shared = Rc::new(RefCell::new(Inner::new(eof))); ( - PayloadSender { - inner: Rc::downgrade(&shared), - }, + PayloadSender::new(Rc::downgrade(&shared)), Payload { inner: shared }, ) } - /// Create empty payload - #[doc(hidden)] - pub fn empty() -> Payload { + /// Creates an empty payload. + pub(crate) fn empty() -> Payload { Payload { inner: Rc::new(RefCell::new(Inner::new(true))), } @@ -77,14 +73,6 @@ impl Payload { pub fn unread_data(&mut self, data: Bytes) { self.inner.borrow_mut().unread_data(data); } - - #[inline] - pub fn readany( - &mut self, - cx: &mut Context<'_>, - ) -> Poll>> { - self.inner.borrow_mut().readany(cx) - } } impl Stream for Payload { @@ -94,7 +82,7 @@ impl Stream for Payload { self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll>> { - self.inner.borrow_mut().readany(cx) + Pin::new(&mut *self.inner.borrow_mut()).poll_next(cx) } } @@ -104,6 +92,10 @@ pub struct PayloadSender { } impl PayloadSender { + fn new(inner: Weak>) -> Self { + Self { inner } + } + #[inline] pub fn set_error(&mut self, err: PayloadError) { if let Some(shared) = self.inner.upgrade() { @@ -227,7 +219,10 @@ impl Inner { self.len } - fn readany(&mut self, cx: &mut Context<'_>) -> Poll>> { + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { if let Some(data) = self.items.pop_front() { self.len -= data.len(); self.need_read = self.len < MAX_BUFFER_SIZE; @@ -257,8 +252,18 @@ impl Inner { #[cfg(test)] mod tests { - use super::*; + use std::panic::{RefUnwindSafe, UnwindSafe}; + use actix_utils::future::poll_fn; + use static_assertions::{assert_impl_all, assert_not_impl_any}; + + use super::*; + + assert_impl_all!(Payload: Unpin); + assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe); + + assert_impl_all!(Inner: Unpin, Send, Sync); + assert_not_impl_any!(Inner: UnwindSafe, RefUnwindSafe); #[actix_rt::test] async fn test_unread_data() { @@ -270,7 +275,10 @@ mod tests { assert_eq!( Bytes::from("data"), - poll_fn(|cx| payload.readany(cx)).await.unwrap().unwrap() + poll_fn(|cx| Pin::new(&mut payload).poll_next(cx)) + .await + .unwrap() + .unwrap() ); } } diff --git a/actix-http/src/h1/utils.rs b/actix-http/src/h1/utils.rs index 131c7f1ed..5c11b1dab 100644 --- a/actix-http/src/h1/utils.rs +++ b/actix-http/src/h1/utils.rs @@ -45,7 +45,7 @@ where impl Future for SendResponse where T: AsyncRead + AsyncWrite + Unpin, - B: MessageBody + Unpin, + B: MessageBody, B::Error: Into, { type Output = Result, Error>; @@ -81,7 +81,7 @@ where // body is done when item is None body_done = item.is_none(); if body_done { - let _ = this.body.take(); + this.body.set(None); } let framed = this.framed.as_mut().as_pin_mut().unwrap(); framed diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 8fbefe6de..a90eb3466 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -108,8 +108,8 @@ where match Pin::new(&mut this.connection).poll_accept(cx)? { Poll::Ready(Some((req, tx))) => { let (parts, body) = req.into_parts(); - let pl = crate::h2::Payload::new(body); - let pl = Payload::H2(pl); + let payload = crate::h2::Payload::new(body); + let pl = Payload::H2 { payload }; let mut req = Request::with_payload(pl); let head = req.head_mut(); @@ -288,9 +288,11 @@ fn prepare_response( let _ = match size { BodySize::None | BodySize::Stream => None, - BodySize::Sized(0) => res - .headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), + BodySize::Sized(0) => { + #[allow(clippy::declare_interior_mutable_const)] + const HV_ZERO: HeaderValue = HeaderValue::from_static("0"); + res.headers_mut().insert(CONTENT_LENGTH, HV_ZERO) + } BodySize::Sized(len) => { let mut buf = itoa::Buffer::new(); diff --git a/actix-http/src/h2/mod.rs b/actix-http/src/h2/mod.rs index cbcb6d0fc..47d51b420 100644 --- a/actix-http/src/h2/mod.rs +++ b/actix-http/src/h2/mod.rs @@ -98,3 +98,14 @@ where } } } + +#[cfg(test)] +mod tests { + use std::panic::{RefUnwindSafe, UnwindSafe}; + + use static_assertions::assert_impl_all; + + use super::*; + + assert_impl_all!(Payload: Unpin, Send, Sync, UnwindSafe, RefUnwindSafe); +} diff --git a/actix-http/src/header/shared/content_encoding.rs b/actix-http/src/header/shared/content_encoding.rs index f3cb3d4d8..394136b4d 100644 --- a/actix-http/src/header/shared/content_encoding.rs +++ b/actix-http/src/header/shared/content_encoding.rs @@ -45,13 +45,13 @@ pub enum ContentEncoding { impl ContentEncoding { /// Is the content compressed? #[inline] - pub fn is_compression(self) -> bool { + pub const fn is_compression(self) -> bool { matches!(self, ContentEncoding::Identity | ContentEncoding::Auto) } /// Convert content encoding to string. #[inline] - pub fn as_str(self) -> &'static str { + pub const fn as_str(self) -> &'static str { match self { ContentEncoding::Br => "br", ContentEncoding::Gzip => "gzip", diff --git a/actix-http/src/header/shared/quality.rs b/actix-http/src/header/shared/quality.rs index 5321c754d..c2f08edc2 100644 --- a/actix-http/src/header/shared/quality.rs +++ b/actix-http/src/header/shared/quality.rs @@ -87,7 +87,7 @@ impl fmt::Display for Quality { // 0 is already handled so it's not possible to have a trailing 0 in this range // we can just write the integer - itoa::fmt(f, x) + itoa_fmt(f, x) } else if x < 100 { // x in is range 10–99 @@ -95,21 +95,21 @@ impl fmt::Display for Quality { if x % 10 == 0 { // trailing 0, divide by 10 and write - itoa::fmt(f, x / 10) + itoa_fmt(f, x / 10) } else { - itoa::fmt(f, x) + itoa_fmt(f, x) } } else { // x is in range 100–999 if x % 100 == 0 { // two trailing 0s, divide by 100 and write - itoa::fmt(f, x / 100) + itoa_fmt(f, x / 100) } else if x % 10 == 0 { // one trailing 0, divide by 10 and write - itoa::fmt(f, x / 10) + itoa_fmt(f, x / 10) } else { - itoa::fmt(f, x) + itoa_fmt(f, x) } } } @@ -117,6 +117,12 @@ impl fmt::Display for Quality { } } +/// Write integer to a `fmt::Write`. +pub fn itoa_fmt(mut wr: W, value: V) -> fmt::Result { + let mut buf = itoa::Buffer::new(); + wr.write_str(buf.format(value)) +} + #[derive(Debug, Clone, Display, Error)] #[display(fmt = "quality out of bounds")] #[non_exhaustive] diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 2b7bc730b..f2b415790 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -58,7 +58,8 @@ pub use self::header::ContentEncoding; pub use self::http_message::HttpMessage; pub use self::message::ConnectionType; pub use self::message::Message; -pub use self::payload::{Payload, PayloadStream}; +#[allow(deprecated)] +pub use self::payload::{BoxedPayloadStream, Payload, PayloadStream}; pub use self::requests::{Request, RequestHead, RequestHeadType}; pub use self::responses::{Response, ResponseBuilder, ResponseHead}; pub use self::service::HttpService; diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index 34213f68a..ecd08fbb3 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell, ops, rc::Rc}; use bitflags::bitflags; @@ -49,7 +49,7 @@ impl Message { } } -impl std::ops::Deref for Message { +impl ops::Deref for Message { type Target = T; fn deref(&self) -> &Self::Target { @@ -57,7 +57,7 @@ impl std::ops::Deref for Message { } } -impl std::ops::DerefMut for Message { +impl ops::DerefMut for Message { fn deref_mut(&mut self) -> &mut Self::Target { Rc::get_mut(&mut self.head).expect("Multiple copies exist") } diff --git a/actix-http/src/payload.rs b/actix-http/src/payload.rs index 69840e7c1..c9f338c7d 100644 --- a/actix-http/src/payload.rs +++ b/actix-http/src/payload.rs @@ -1,70 +1,89 @@ use std::{ + mem, pin::Pin, task::{Context, Poll}, }; use bytes::Bytes; use futures_core::Stream; -use h2::RecvStream; use crate::error::PayloadError; -// TODO: rename to boxed payload -/// A boxed payload. -pub type PayloadStream = Pin>>>; +/// A boxed payload stream. +pub type BoxedPayloadStream = Pin>>>; -/// A streaming payload. -pub enum Payload { - None, - H1(crate::h1::Payload), - H2(crate::h2::Payload), - Stream(S), +#[deprecated(since = "4.0.0", note = "Renamed to `BoxedPayloadStream`.")] +pub type PayloadStream = BoxedPayloadStream; + +pin_project_lite::pin_project! { + /// A streaming payload. + #[project = PayloadProj] + pub enum Payload { + None, + H1 { payload: crate::h1::Payload }, + H2 { payload: crate::h2::Payload }, + Stream { #[pin] payload: S }, + } } impl From for Payload { - fn from(v: crate::h1::Payload) -> Self { - Payload::H1(v) + fn from(payload: crate::h1::Payload) -> Self { + Payload::H1 { payload } } } impl From for Payload { - fn from(v: crate::h2::Payload) -> Self { - Payload::H2(v) + fn from(payload: crate::h2::Payload) -> Self { + Payload::H2 { payload } } } -impl From for Payload { - fn from(v: RecvStream) -> Self { - Payload::H2(crate::h2::Payload::new(v)) +impl From for Payload { + fn from(stream: h2::RecvStream) -> Self { + Payload::H2 { + payload: crate::h2::Payload::new(stream), + } } } -impl From for Payload { - fn from(pl: PayloadStream) -> Self { - Payload::Stream(pl) +impl From for Payload { + fn from(payload: BoxedPayloadStream) -> Self { + Payload::Stream { payload } } } impl Payload { /// Takes current payload and replaces it with `None` value pub fn take(&mut self) -> Payload { - std::mem::replace(self, Payload::None) + mem::replace(self, Payload::None) } } impl Stream for Payload where - S: Stream> + Unpin, + S: Stream>, { type Item = Result; #[inline] fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.get_mut() { - Payload::None => Poll::Ready(None), - Payload::H1(ref mut pl) => pl.readany(cx), - Payload::H2(ref mut pl) => Pin::new(pl).poll_next(cx), - Payload::Stream(ref mut pl) => Pin::new(pl).poll_next(cx), + match self.project() { + PayloadProj::None => Poll::Ready(None), + PayloadProj::H1 { payload } => Pin::new(payload).poll_next(cx), + PayloadProj::H2 { payload } => Pin::new(payload).poll_next(cx), + PayloadProj::Stream { payload } => payload.poll_next(cx), } } } + +#[cfg(test)] +mod tests { + use std::panic::{RefUnwindSafe, UnwindSafe}; + + use static_assertions::{assert_impl_all, assert_not_impl_any}; + + use super::*; + + assert_impl_all!(Payload: Unpin); + assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe); +} diff --git a/actix-http/src/requests/request.rs b/actix-http/src/requests/request.rs index 0254a8f11..4eaaba8e1 100644 --- a/actix-http/src/requests/request.rs +++ b/actix-http/src/requests/request.rs @@ -10,11 +10,12 @@ use std::{ use http::{header, Method, Uri, Version}; use crate::{ - header::HeaderMap, Extensions, HttpMessage, Message, Payload, PayloadStream, RequestHead, + header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, Message, Payload, + RequestHead, }; /// An HTTP request. -pub struct Request

{ +pub struct Request

{ pub(crate) payload: Payload

, pub(crate) head: Message, pub(crate) conn_data: Option>, @@ -46,7 +47,7 @@ impl

HttpMessage for Request

{ } } -impl From> for Request { +impl From> for Request { fn from(head: Message) -> Self { Request { head, @@ -57,10 +58,10 @@ impl From> for Request { } } -impl Request { +impl Request { /// Create new Request instance #[allow(clippy::new_without_default)] - pub fn new() -> Request { + pub fn new() -> Request { Request { head: Message::new(), payload: Payload::None, diff --git a/actix-http/src/test.rs b/actix-http/src/test.rs index ea80345fe..1f76498ef 100644 --- a/actix-http/src/test.rs +++ b/actix-http/src/test.rs @@ -120,7 +120,7 @@ impl TestRequest { } /// Set request payload. - pub fn set_payload>(&mut self, data: B) -> &mut Self { + pub fn set_payload(&mut self, data: impl Into) -> &mut Self { let mut payload = crate::h1::Payload::empty(); payload.unread_data(data.into()); parts(&mut self.0).payload = Some(payload.into()); diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index 6491da149..568d801a2 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -99,8 +99,9 @@ impl From for Response { match err { HandshakeError::GetMethodRequired => { let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED); - res.headers_mut() - .insert(header::ALLOW, HeaderValue::from_static("GET")); + #[allow(clippy::declare_interior_mutable_const)] + const HV_GET: HeaderValue = HeaderValue::from_static("GET"); + res.headers_mut().insert(header::ALLOW, HV_GET); res } diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 42ff0dba1..51fefae72 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -7,6 +7,7 @@ use std::{ io::{self, BufReader, Write}, net::{SocketAddr, TcpStream as StdTcpStream}, sync::Arc, + task::Poll, }; use actix_http::{ @@ -16,25 +17,37 @@ use actix_http::{ Error, HttpService, Method, Request, Response, StatusCode, Version, }; use actix_http_test::test_server; +use actix_rt::pin; use actix_service::{fn_factory_with_config, fn_service}; use actix_tls::connect::rustls::webpki_roots_cert_store; -use actix_utils::future::{err, ok}; +use actix_utils::future::{err, ok, poll_fn}; use bytes::{Bytes, BytesMut}; use derive_more::{Display, Error}; -use futures_core::Stream; -use futures_util::stream::{once, StreamExt as _}; +use futures_core::{ready, Stream}; +use futures_util::stream::once; use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig, ServerName}; use rustls_pemfile::{certs, pkcs8_private_keys}; -async fn load_body(mut stream: S) -> Result +async fn load_body(stream: S) -> Result where - S: Stream> + Unpin, + S: Stream>, { - let mut body = BytesMut::new(); - while let Some(item) = stream.next().await { - body.extend_from_slice(&item?) - } - Ok(body) + let mut buf = BytesMut::new(); + + pin!(stream); + + poll_fn(|cx| loop { + let body = stream.as_mut(); + + match ready!(body.poll_next(cx)) { + Some(Ok(bytes)) => buf.extend_from_slice(&*bytes), + None => return Poll::Ready(Ok(())), + Some(Err(err)) => return Poll::Ready(Err(err)), + } + }) + .await?; + + Ok(buf) } fn tls_config() -> RustlsServerConfig { diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index e58c3ee24..a9a1e8784 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 0.4.0-beta.11 - 2021-12-27 +* No significant changes since `0.4.0-beta.10`. + + ## 0.4.0-beta.10 - 2021-12-11 - No significant changes since `0.4.0-beta.9`. diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index c7145e542..de13133a1 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.4.0-beta.10" +version = "0.4.0-beta.11" authors = ["Nikolay Kim "] description = "Multipart form support for Actix Web" keywords = ["http", "web", "framework", "async", "futures"] @@ -15,7 +15,7 @@ path = "src/lib.rs" [dependencies] actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.15", default-features = false } +actix-web = { version = "4.0.0-beta.16", default-features = false } bytes = "1" derive_more = "0.99.5" @@ -28,7 +28,7 @@ twoway = "0.2" [dev-dependencies] actix-rt = "2.2" -actix-http = "3.0.0-beta.16" +actix-http = "3.0.0-beta.17" futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } -tokio = { version = "1", features = ["sync"] } +tokio = { version = "1.8.4", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-multipart/README.md b/actix-multipart/README.md index 647796557..a9ee325ba 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -3,11 +3,11 @@ > 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.10)](https://docs.rs/actix-multipart/0.4.0-beta.10) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.11)](https://docs.rs/actix-multipart/0.4.0-beta.11) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.10/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.10) +[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.11/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.11) [![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) diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index 8eabcee10..239f7f905 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -1233,7 +1233,7 @@ mod tests { // and should not consume the payload match payload { - actix_web::dev::Payload::H1(_) => {} //expected + actix_web::dev::Payload::H1 { .. } => {} //expected _ => unreachable!(), } } diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml index afd39dfd3..c63448bc7 100644 --- a/actix-router/Cargo.toml +++ b/actix-router/Cargo.toml @@ -21,7 +21,7 @@ default = ["http"] [dependencies] bytestring = ">=0.1.5, <2" -firestorm = "0.4" +firestorm = "0.5" http = { version = "0.2.3", optional = true } log = "0.4" regex = "1.5" @@ -29,7 +29,7 @@ serde = "1" [dev-dependencies] criterion = { version = "0.3", features = ["html_reports"] } -firestorm = { version = "0.4", features = ["enable_system_time"] } +firestorm = { version = "0.5", features = ["enable_system_time"] } http = "0.2.5" serde = { version = "1", features = ["derive"] } diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index fa77b1e7b..f1eb9caf5 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -29,26 +29,25 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// /// /// # Pattern Format and Matching Behavior -/// /// Resource pattern is defined as a string of zero or more _segments_ where each segment is /// preceded by a slash `/`. /// -/// This means that pattern string __must__ either be empty or begin with a slash (`/`). -/// This also implies that a trailing slash in pattern defines an empty segment. -/// For example, the pattern `"/user/"` has two segments: `["user", ""]` +/// This means that pattern string __must__ either be empty or begin with a slash (`/`). This also +/// implies that a trailing slash in pattern defines an empty segment. For example, the pattern +/// `"/user/"` has two segments: `["user", ""]` /// -/// A key point to underhand is that `ResourceDef` matches segments, not strings. -/// It matches segments individually. -/// For example, the pattern `/user/` is not considered a prefix for the path `/user/123/456`, -/// because the second segment doesn't match: `["user", ""]` vs `["user", "123", "456"]`. +/// A key point to understand is that `ResourceDef` matches segments, not strings. Segments are +/// matched individually. For example, the pattern `/user/` is not considered a prefix for the path +/// `/user/123/456`, because the second segment doesn't match: `["user", ""]` +/// vs `["user", "123", "456"]`. /// /// This definition is consistent with the definition of absolute URL path in -/// [RFC 3986 (section 3.3)](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3) +/// [RFC 3986 §3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3) /// /// /// # Static Resources -/// A static resource is the most basic type of definition. Pass a pattern to -/// [new][Self::new]. Conforming paths must match the pattern exactly. +/// A static resource is the most basic type of definition. Pass a pattern to [new][Self::new]. +/// Conforming paths must match the pattern exactly. /// /// ## Examples /// ``` @@ -63,7 +62,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(!resource.is_match("/search")); /// ``` /// -/// /// # Dynamic Segments /// Also known as "path parameters". Resources can define sections of a pattern that be extracted /// from a conforming path, if it conforms to (one of) the resource pattern(s). @@ -102,15 +100,15 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert_eq!(path.get("id").unwrap(), "123"); /// ``` /// -/// /// # Prefix Resources /// A prefix resource is defined as pattern that can match just the start of a path, up to a /// segment boundary. /// /// Prefix patterns with a trailing slash may have an unexpected, though correct, behavior. -/// They define and therefore require an empty segment in order to match. Examples are given below. +/// They define and therefore require an empty segment in order to match. It is easier to understand +/// this behavior after reading the [matching behavior section]. Examples are given below. /// -/// Empty pattern matches any path as a prefix. +/// The empty pattern (`""`), as a prefix, matches any path. /// /// Prefix resources can contain dynamic segments. /// @@ -130,7 +128,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(!resource.is_match("/user/123")); /// ``` /// -/// /// # Custom Regex Segments /// Dynamic segments can be customised to only match a specific regular expression. It can be /// helpful to do this if resource definitions would otherwise conflict and cause one to @@ -158,7 +155,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(!resource.is_match("/user/abc")); /// ``` /// -/// /// # Tail Segments /// As a shortcut to defining a custom regex for matching _all_ remaining characters (not just those /// up until a `/` character), there is a special pattern to match (and capture) the remaining @@ -179,7 +175,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert_eq!(path.get("tail").unwrap(), "main/LICENSE"); /// ``` /// -/// /// # Multi-Pattern Resources /// For resources that can map to multiple distinct paths, it may be suitable to use /// multi-pattern resources by passing an array/vec to [`new`][Self::new]. They will be combined @@ -198,7 +193,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(resource.is_match("/index")); /// ``` /// -/// /// # Trailing Slashes /// It should be noted that this library takes no steps to normalize intra-path or trailing slashes. /// As such, all resource definitions implicitly expect a pre-processing step to normalize paths if @@ -212,6 +206,8 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(!ResourceDef::new("/root/").is_match("/root")); /// assert!(!ResourceDef::prefix("/root/").is_match("/root")); /// ``` +/// +/// [matching behavior section]: #pattern-format-and-matching-behavior #[derive(Clone, Debug)] pub struct ResourceDef { id: u16, @@ -279,7 +275,7 @@ impl ResourceDef { /// ``` pub fn new(paths: T) -> Self { profile_method!(new); - Self::new2(paths, false) + Self::construct(paths, false) } /// Constructs a new resource definition using a pattern that performs prefix matching. @@ -292,7 +288,7 @@ impl ResourceDef { /// resource definition with a tail segment; use [`new`][Self::new] in this case. /// /// # Panics - /// Panics if path regex pattern is malformed. + /// Panics if path pattern is malformed. /// /// # Examples /// ``` @@ -307,14 +303,14 @@ impl ResourceDef { /// ``` pub fn prefix(paths: T) -> Self { profile_method!(prefix); - ResourceDef::new2(paths, true) + ResourceDef::construct(paths, true) } /// Constructs a new resource definition using a string pattern that performs prefix matching, - /// inserting a `/` to beginning of the pattern if absent and pattern is not empty. + /// ensuring a leading `/` if pattern is not empty. /// /// # Panics - /// Panics if path regex pattern is malformed. + /// Panics if path pattern is malformed. /// /// # Examples /// ``` @@ -515,8 +511,8 @@ impl ResourceDef { .collect::>(); match patterns.len() { - 1 => ResourceDef::new2(&patterns[0], other.is_prefix()), - _ => ResourceDef::new2(patterns, other.is_prefix()), + 1 => ResourceDef::construct(&patterns[0], other.is_prefix()), + _ => ResourceDef::construct(patterns, other.is_prefix()), } } @@ -881,8 +877,8 @@ impl ResourceDef { } } - fn new2(paths: T, is_prefix: bool) -> Self { - profile_method!(new2); + fn construct(paths: T, is_prefix: bool) -> Self { + profile_method!(construct); let patterns = paths.patterns(); let (pat_type, segments) = match &patterns { @@ -1814,7 +1810,7 @@ mod tests { #[test] #[should_panic] - fn prefix_plus_tail_match_is_allowed() { + fn prefix_plus_tail_match_disallowed() { ResourceDef::prefix("/user/{id}*"); } } diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index e3deeb3f4..2de0a69d6 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 0.1.0-beta.10 - 2021-12-27 +* No significant changes since `0.1.0-beta.9`. + + ## 0.1.0-beta.9 - 2021-12-17 - Re-export `actix_http::body::to_bytes`. [#2518] - Update `actix_web::test` re-exports. [#2518] diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 7957b3a9c..c7177a38c 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-test" -version = "0.1.0-beta.9" +version = "0.1.0-beta.10" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] actix-codec = "0.4.1" -actix-http = "3.0.0-beta.16" -actix-http-test = "3.0.0-beta.9" +actix-http = "3.0.0-beta.17" +actix-http-test = "3.0.0-beta.10" actix-rt = "2.1" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.15", default-features = false, features = ["cookies"] } -awc = { version = "3.0.0-beta.14", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.16", default-features = false, features = ["cookies"] } +awc = { version = "3.0.0-beta.15", 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 = [] } @@ -45,4 +45,4 @@ serde_json = "1" serde_urlencoded = "0.7" tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-rustls = { package = "rustls", version = "0.20.0", optional = true } -tokio = { version = "1.2", features = ["sync"] } +tokio = { version = "1.8.4", features = ["sync"] } diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 6abfe2c61..2fbbe7444 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 4.0.0-beta.9 - 2021-12-27 +* No significant changes since `4.0.0-beta.8`. + + ## 4.0.0-beta.8 - 2021-12-11 - Add `ws:WsResponseBuilder` for building WebSocket session response. [#1920] - Deprecate `ws::{start_with_addr, start_with_protocols}`. [#1920] diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 3f213f378..3b792093a 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-actors" -version = "4.0.0-beta.8" +version = "4.0.0-beta.9" authors = ["Nikolay Kim "] description = "Actix actors support for Actix Web" keywords = ["actix", "http", "web", "framework", "async"] @@ -16,19 +16,19 @@ path = "src/lib.rs" [dependencies] actix = { version = "0.12.0", default-features = false } actix-codec = "0.4.1" -actix-http = "3.0.0-beta.16" -actix-web = { version = "4.0.0-beta.15", default-features = false } +actix-http = "3.0.0-beta.17" +actix-web = { version = "4.0.0-beta.16", default-features = false } bytes = "1" bytestring = "1" futures-core = { version = "0.3.7", default-features = false } pin-project-lite = "0.2" -tokio = { version = "1", features = ["sync"] } +tokio = { version = "1.8.4", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.9" -awc = { version = "3.0.0-beta.14", default-features = false } +actix-test = "0.1.0-beta.10" +awc = { version = "3.0.0-beta.15", default-features = false } env_logger = "0.9" futures-util = { version = "0.3.7", default-features = false } diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index 954f8273b..232c81eac 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -3,11 +3,11 @@ > 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.8)](https://docs.rs/actix-web-actors/4.0.0-beta.8) +[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.9)](https://docs.rs/actix-web-actors/4.0.0-beta.9) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.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.8/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.8) +[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.9) [![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 8d42137e7..5250baa90 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -23,9 +23,9 @@ actix-router = "0.5.0-beta.3" [dev-dependencies] actix-macros = "0.2.3" actix-rt = "2.2" -actix-test = "0.1.0-beta.9" +actix-test = "0.1.0-beta.10" actix-utils = "3.0.0" -actix-web = "4.0.0-beta.15" +actix-web = "4.0.0-beta.16" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } trybuild = "1" diff --git a/awc/CHANGES.md b/awc/CHANGES.md index b5144b7a2..06e94292a 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,10 +1,24 @@ # Changes ## Unreleased - 2021-xx-xx +- `*::send_json` and `*::send_form` methods now receive `impl Serialize`. [#2553] +- `FrozenClientRequest::extra_header` now uses receives an `impl TryIntoHeaderPair`. [#2553] +- Remove unnecessary `Unpin` bounds on `*::send_stream`. [#2553] + +[#2553]: https://github.com/actix/actix-web/pull/2553 + + +## 3.0.0-beta.15 - 2021-12-27 - Rename `Connector::{ssl => openssl}`. [#2503] - Improve `Client` instantiation efficiency when using `openssl` by only building connectors once. [#2503] +- `ClientRequest::send_body` now takes an `impl MessageBody`. [#2546] +- Rename `MessageBody => ResponseBody` to avoid conflicts with `MessageBody` trait. [#2546] +- `impl Future` for `ResponseBody` no longer requires the body type be `Unpin`. [#2546] +- `impl Future` for `JsonBody` no longer requires the body type be `Unpin`. [#2546] +- `impl Stream` for `ClientResponse` no longer requires the body type be `Unpin`. [#2546] [#2503]: https://github.com/actix/actix-web/pull/2503 +[#2546]: https://github.com/actix/actix-web/pull/2546 ## 3.0.0-beta.14 - 2021-12-17 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index f9a541c7e..8777ffa74 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.14" +version = "3.0.0-beta.15" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -60,9 +60,9 @@ dangerous-h2c = [] [dependencies] actix-codec = "0.4.1" actix-service = "2.0.0" -actix-http = "3.0.0-beta.16" +actix-http = "3.0.0-beta.17" actix-rt = { version = "2.1", default-features = false } -actix-tls = { version = "3.0.0-rc.2", features = ["connect", "uri"] } +actix-tls = { version = "3.0.0", features = ["connect", "uri"] } actix-utils = "3.0.0" ahash = "0.7" @@ -74,7 +74,7 @@ futures-core = { version = "0.3.7", default-features = false, features = ["alloc futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] } h2 = "0.3.9" http = "0.2.5" -itoa = "0.4" +itoa = "1" log =" 0.4" mime = "0.3" percent-encoding = "2.1" @@ -83,7 +83,7 @@ rand = "0.8" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7" -tokio = { version = "1", features = ["sync"] } +tokio = { version = "1.8.4", features = ["sync"] } cookie = { version = "0.15", features = ["percent-encode"], optional = true } @@ -93,13 +93,13 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] -actix-http = { version = "3.0.0-beta.16", features = ["openssl"] } -actix-http-test = { version = "3.0.0-beta.9", features = ["openssl"] } -actix-server = "2.0.0-rc.1" -actix-test = { version = "0.1.0-beta.9", features = ["openssl", "rustls"] } -actix-tls = { version = "3.0.0-rc.1", features = ["openssl", "rustls"] } +actix-http = { version = "3.0.0-beta.17", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] } +actix-server = "2.0.0-rc.2" +actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } +actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] } actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.15", features = ["openssl"] } +actix-web = { version = "4.0.0-beta.16", features = ["openssl"] } brotli2 = "0.3.2" env_logger = "0.9" diff --git a/awc/README.md b/awc/README.md index 3fbdd903a..582ecb18f 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,9 +3,9 @@ > Async HTTP and WebSocket client library. [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.14)](https://docs.rs/awc/3.0.0-beta.14) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.15)](https://docs.rs/awc/3.0.0-beta.15) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.14/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.14) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.15/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.15) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources diff --git a/awc/src/any_body.rs b/awc/src/any_body.rs index 2ffeb5074..d09a943ab 100644 --- a/awc/src/any_body.rs +++ b/awc/src/any_body.rs @@ -1,17 +1,13 @@ use std::{ - borrow::Cow, fmt, mem, pin::Pin, task::{Context, Poll}, }; -use bytes::{Bytes, BytesMut}; -use futures_core::Stream; +use bytes::Bytes; use pin_project_lite::pin_project; -use actix_http::body::{BodySize, BodyStream, BoxBody, MessageBody, SizedStream}; - -use crate::BoxError; +use actix_http::body::{BodySize, BoxBody, MessageBody}; pin_project! { /// Represents various types of HTTP message body. @@ -77,10 +73,27 @@ impl AnyBody where B: MessageBody + 'static, { + /// Converts a [`MessageBody`] type into the best possible representation. + /// + /// Checks size for `None` and tries to convert to `Bytes`. Otherwise, uses the `Body` variant. + pub fn from_message_body(body: B) -> Self + where + B: MessageBody, + { + if matches!(body.size(), BodySize::None) { + return Self::None; + } + + match body.try_into_bytes() { + Ok(body) => Self::Bytes { body }, + Err(body) => Self::new(body), + } + } + pub fn into_boxed(self) -> AnyBody { match self { Self::None => AnyBody::None, - Self::Bytes { body: bytes } => AnyBody::Bytes { body: bytes }, + Self::Bytes { body } => AnyBody::Bytes { body }, Self::Body { body } => AnyBody::new_boxed(body), } } @@ -143,91 +156,6 @@ impl fmt::Debug for AnyBody { } } -impl From<&'static str> for AnyBody { - fn from(string: &'static str) -> Self { - Self::Bytes { - body: Bytes::from_static(string.as_ref()), - } - } -} - -impl From<&'static [u8]> for AnyBody { - fn from(bytes: &'static [u8]) -> Self { - Self::Bytes { - body: Bytes::from_static(bytes), - } - } -} - -impl From> for AnyBody { - fn from(vec: Vec) -> Self { - Self::Bytes { - body: Bytes::from(vec), - } - } -} - -impl From for AnyBody { - fn from(string: String) -> Self { - Self::Bytes { - body: Bytes::from(string), - } - } -} - -impl From<&'_ String> for AnyBody { - fn from(string: &String) -> Self { - Self::Bytes { - body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string)), - } - } -} - -impl From> for AnyBody { - fn from(string: Cow<'_, str>) -> Self { - match string { - Cow::Owned(s) => Self::from(s), - Cow::Borrowed(s) => Self::Bytes { - body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)), - }, - } - } -} - -impl From for AnyBody { - fn from(bytes: Bytes) -> Self { - Self::Bytes { body: bytes } - } -} - -impl From for AnyBody { - fn from(bytes: BytesMut) -> Self { - Self::Bytes { - body: bytes.freeze(), - } - } -} - -impl From> for AnyBody -where - S: Stream> + 'static, - E: Into + 'static, -{ - fn from(stream: SizedStream) -> Self { - AnyBody::new_boxed(stream) - } -} - -impl From> for AnyBody -where - S: Stream> + 'static, - E: Into + 'static, -{ - fn from(stream: BodyStream) -> Self { - AnyBody::new_boxed(stream) - } -} - #[cfg(test)] mod tests { use std::marker::PhantomPinned; diff --git a/awc/src/builder.rs b/awc/src/builder.rs index 30f203bb8..16a4e9cb5 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -9,11 +9,13 @@ use actix_rt::net::{ActixStream, TcpStream}; use actix_service::{boxed, Service}; use crate::{ - client::{ConnectInfo, Connector, ConnectorService, TcpConnectError, TcpConnection}, + client::{ + ClientConfig, ConnectInfo, Connector, ConnectorService, TcpConnectError, TcpConnection, + }, connect::DefaultConnector, error::SendRequestError, middleware::{NestTransform, Redirect, Transform}, - Client, ClientConfig, ConnectRequest, ConnectResponse, + Client, ConnectRequest, ConnectResponse, }; /// An HTTP Client builder diff --git a/awc/src/client/connection.rs b/awc/src/client/connection.rs index 0e1f0bfec..456f119aa 100644 --- a/awc/src/client/connection.rs +++ b/awc/src/client/connection.rs @@ -267,7 +267,9 @@ where Connection::Tls(ConnectionType::H2(conn)) => { h2proto::send_request(conn, head.into(), body).await } - _ => unreachable!("Plain Tcp connection can be used only in Http1 protocol"), + _ => { + unreachable!("Plain TCP connection can be used only with HTTP/1.1 protocol") + } } }) } diff --git a/awc/src/client/h1proto.rs b/awc/src/client/h1proto.rs index 1028a2178..cf716db72 100644 --- a/awc/src/client/h1proto.rs +++ b/awc/src/client/h1proto.rs @@ -13,16 +13,17 @@ use actix_http::{ Payload, RequestHeadType, ResponseHead, StatusCode, }; use actix_utils::future::poll_fn; -use bytes::buf::BufMut; -use bytes::{Bytes, BytesMut}; +use bytes::{buf::BufMut, Bytes, BytesMut}; use futures_core::{ready, Stream}; use futures_util::SinkExt as _; use pin_project_lite::pin_project; use crate::BoxError; -use super::connection::{ConnectionIo, H1Connection}; -use super::error::{ConnectError, SendRequestError}; +use super::{ + connection::{ConnectionIo, H1Connection}, + error::{ConnectError, SendRequestError}, +}; pub(crate) async fn send_request( io: H1Connection, @@ -123,7 +124,12 @@ where Ok((head, Payload::None)) } - _ => Ok((head, Payload::Stream(Box::pin(PlStream::new(framed))))), + _ => Ok(( + head, + Payload::Stream { + payload: Box::pin(PlStream::new(framed)), + }, + )), } } diff --git a/awc/src/client/h2proto.rs b/awc/src/client/h2proto.rs index 9ced5776b..709896ddd 100644 --- a/awc/src/client/h2proto.rs +++ b/awc/src/client/h2proto.rs @@ -52,9 +52,11 @@ where let _ = match length { BodySize::None => None, - BodySize::Sized(0) => req - .headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), + BodySize::Sized(0) => { + #[allow(clippy::declare_interior_mutable_const)] + const HV_ZERO: HeaderValue = HeaderValue::from_static("0"); + req.headers_mut().insert(CONTENT_LENGTH, HV_ZERO) + } BodySize::Sized(len) => { let mut buf = itoa::Buffer::new(); diff --git a/awc/src/client/mod.rs b/awc/src/client/mod.rs index 0d5c899bc..d5854d83e 100644 --- a/awc/src/client/mod.rs +++ b/awc/src/client/mod.rs @@ -1,6 +1,15 @@ //! HTTP client. -use http::Uri; +use std::{convert::TryFrom, rc::Rc, time::Duration}; + +use actix_http::{error::HttpError, header::HeaderMap, Method, RequestHead, Uri}; +use actix_rt::net::TcpStream; +use actix_service::Service; +pub use actix_tls::connect::{ + ConnectError as TcpConnectError, ConnectInfo, Connection as TcpConnection, +}; + +use crate::{ws, BoxConnectorService, ClientBuilder, ClientRequest}; mod config; mod connection; @@ -10,10 +19,6 @@ mod h1proto; mod h2proto; mod pool; -pub use actix_tls::connect::{ - ConnectError as TcpConnectError, ConnectInfo, Connection as TcpConnection, -}; - pub use self::connection::{Connection, ConnectionIo}; pub use self::connector::{Connector, ConnectorService}; pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; @@ -23,3 +28,176 @@ pub struct Connect { pub uri: Uri, pub addr: Option, } + +/// An asynchronous HTTP and WebSocket client. +/// +/// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU +/// and memory usage. +/// +/// # Examples +/// ``` +/// use awc::Client; +/// +/// #[actix_rt::main] +/// async fn main() { +/// let mut client = Client::default(); +/// +/// let res = client.get("http://www.rust-lang.org") +/// .insert_header(("User-Agent", "my-app/1.2")) +/// .send() +/// .await; +/// +/// println!("Response: {:?}", res); +/// } +/// ``` +#[derive(Clone)] +pub struct Client(pub(crate) ClientConfig); + +#[derive(Clone)] +pub(crate) struct ClientConfig { + pub(crate) connector: BoxConnectorService, + pub(crate) default_headers: Rc, + pub(crate) timeout: Option, +} + +impl Default for Client { + fn default() -> Self { + ClientBuilder::new().finish() + } +} + +impl Client { + /// Create new client instance with default settings. + pub fn new() -> Client { + Client::default() + } + + /// Create `Client` builder. + /// This function is equivalent of `ClientBuilder::new()`. + pub fn builder() -> ClientBuilder< + impl Service< + ConnectInfo, + Response = TcpConnection, + Error = TcpConnectError, + > + Clone, + > { + ClientBuilder::new() + } + + /// Construct HTTP request. + pub fn request(&self, method: Method, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + let mut req = ClientRequest::new(method, url, self.0.clone()); + + for header in self.0.default_headers.iter() { + // header map is empty + // TODO: probably append instead + req = req.insert_header_if_none(header); + } + req + } + + /// Create `ClientRequest` from `RequestHead` + /// + /// It is useful for proxy requests. This implementation + /// copies all headers and the method. + pub fn request_from(&self, url: U, head: &RequestHead) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + let mut req = self.request(head.method.clone(), url); + for header in head.headers.iter() { + req = req.insert_header_if_none(header); + } + req + } + + /// Construct HTTP *GET* request. + pub fn get(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::GET, url) + } + + /// Construct HTTP *HEAD* request. + pub fn head(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::HEAD, url) + } + + /// Construct HTTP *PUT* request. + pub fn put(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::PUT, url) + } + + /// Construct HTTP *POST* request. + pub fn post(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::POST, url) + } + + /// Construct HTTP *PATCH* request. + pub fn patch(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::PATCH, url) + } + + /// Construct HTTP *DELETE* request. + pub fn delete(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::DELETE, url) + } + + /// Construct HTTP *OPTIONS* request. + pub fn options(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::OPTIONS, url) + } + + /// Initialize a WebSocket connection. + /// Returns a WebSocket connection builder. + pub fn ws(&self, url: U) -> ws::WebsocketsRequest + where + Uri: TryFrom, + >::Error: Into, + { + let mut req = ws::WebsocketsRequest::new(url, self.0.clone()); + for (key, value) in self.0.default_headers.iter() { + req.head.headers.insert(key.clone(), value.clone()); + } + req + } + + /// Get default HeaderMap of Client. + /// + /// Returns Some(&mut HeaderMap) when Client object is unique + /// (No other clone of client exists at the same time). + pub fn headers(&mut self) -> Option<&mut HeaderMap> { + Rc::get_mut(&mut self.0.default_headers) + } +} diff --git a/awc/src/connect.rs b/awc/src/connect.rs index 19870b069..f93014a67 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -16,7 +16,7 @@ use crate::{ client::{ Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError, }, - response::ClientResponse, + ClientResponse, }; pub type BoxConnectorService = Rc< diff --git a/awc/src/error.rs b/awc/src/error.rs index c1d855053..aa9dc4d99 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -1,5 +1,6 @@ //! HTTP client errors +// TODO: figure out how best to expose http::Error vs actix_http::Error pub use actix_http::{ error::{HttpError, PayloadError}, header::HeaderValue, diff --git a/awc/src/frozen.rs b/awc/src/frozen.rs index cd93a1d60..4023bd1c8 100644 --- a/awc/src/frozen.rs +++ b/awc/src/frozen.rs @@ -1,22 +1,24 @@ -use std::{convert::TryFrom, net, rc::Rc, time::Duration}; +use std::{net, rc::Rc, time::Duration}; use bytes::Bytes; use futures_core::Stream; use serde::Serialize; use actix_http::{ + body::MessageBody, error::HttpError, - header::{HeaderMap, HeaderName, TryIntoHeaderValue}, + header::{HeaderMap, TryIntoHeaderPair}, Method, RequestHead, Uri, }; use crate::{ - any_body::AnyBody, + client::ClientConfig, sender::{RequestSender, SendClientRequest}, - BoxError, ClientConfig, + BoxError, }; /// `FrozenClientRequest` struct represents cloneable client request. +/// /// It could be used to send same request multiple times. #[derive(Clone)] pub struct FrozenClientRequest { @@ -46,7 +48,7 @@ impl FrozenClientRequest { /// Send a body. pub fn send_body(&self, body: B) -> SendClientRequest where - B: Into, + B: MessageBody + 'static, { RequestSender::Rc(self.head.clone(), None).send_body( self.addr, @@ -82,7 +84,7 @@ impl FrozenClientRequest { /// Send a streaming body. pub fn send_stream(&self, stream: S) -> SendClientRequest where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, E: Into + 'static, { RequestSender::Rc(self.head.clone(), None).send_stream( @@ -104,20 +106,14 @@ impl FrozenClientRequest { ) } - /// Create a `FrozenSendBuilder` with extra headers + /// Clones this `FrozenClientRequest`, returning a new one with extra headers added. pub fn extra_headers(&self, extra_headers: HeaderMap) -> FrozenSendBuilder { FrozenSendBuilder::new(self.clone(), extra_headers) } - /// Create a `FrozenSendBuilder` with an extra header - pub fn extra_header(&self, key: K, value: V) -> FrozenSendBuilder - where - HeaderName: TryFrom, - >::Error: Into, - V: TryIntoHeaderValue, - { - self.extra_headers(HeaderMap::new()) - .extra_header(key, value) + /// Clones this `FrozenClientRequest`, returning a new one with the extra header added. + pub fn extra_header(&self, header: impl TryIntoHeaderPair) -> FrozenSendBuilder { + self.extra_headers(HeaderMap::new()).extra_header(header) } } @@ -138,29 +134,20 @@ impl FrozenSendBuilder { } /// Insert a header, it overrides existing header in `FrozenClientRequest`. - pub fn extra_header(mut self, key: K, value: V) -> Self - where - HeaderName: TryFrom, - >::Error: Into, - V: TryIntoHeaderValue, - { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into_value() { - Ok(value) => { - self.extra_headers.insert(key, value); - } - Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), + pub fn extra_header(mut self, header: impl TryIntoHeaderPair) -> Self { + match header.try_into_pair() { + Ok((key, value)) => { + self.extra_headers.insert(key, value); + } + + Err(err) => self.err = Some(err.into()), } + self } /// Complete request construction and send a body. - pub fn send_body(self, body: B) -> SendClientRequest - where - B: Into, - { + pub fn send_body(self, body: impl MessageBody + 'static) -> SendClientRequest { if let Some(e) = self.err { return e.into(); } @@ -175,9 +162,9 @@ impl FrozenSendBuilder { } /// Complete request construction and send a json body. - pub fn send_json(self, value: &T) -> SendClientRequest { - if let Some(e) = self.err { - return e.into(); + pub fn send_json(self, value: impl Serialize) -> SendClientRequest { + if let Some(err) = self.err { + return err.into(); } RequestSender::Rc(self.req.head, Some(self.extra_headers)).send_json( @@ -190,7 +177,7 @@ impl FrozenSendBuilder { } /// Complete request construction and send an urlencoded body. - pub fn send_form(self, value: &T) -> SendClientRequest { + pub fn send_form(self, value: impl Serialize) -> SendClientRequest { if let Some(e) = self.err { return e.into(); } @@ -207,7 +194,7 @@ impl FrozenSendBuilder { /// Complete request construction and send a streaming body. pub fn send_stream(self, stream: S) -> SendClientRequest where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, E: Into + 'static, { if let Some(e) = self.err { diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 00c559406..970ca2d92 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -105,6 +105,11 @@ #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] +pub use actix_http::body; + +#[cfg(feature = "cookies")] +pub use cookie; + mod any_body; mod builder; mod client; @@ -113,203 +118,27 @@ pub mod error; mod frozen; pub mod middleware; mod request; -mod response; +mod responses; mod sender; pub mod test; pub mod ws; -// TODO: hmmmmmm -pub use actix_http as http; -#[cfg(feature = "cookies")] -pub use cookie; +pub mod http { + //! Various HTTP related types. + + // TODO: figure out how best to expose http::Error vs actix_http::Error + pub use actix_http::{ + header, uri, ConnectionType, Error, Method, StatusCode, Uri, Version, + }; +} pub use self::builder::ClientBuilder; -pub use self::client::Connector; +pub use self::client::{Client, Connector}; pub use self::connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse}; pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder}; pub use self::request::ClientRequest; -pub use self::response::{ClientResponse, JsonBody, MessageBody}; +#[allow(deprecated)] +pub use self::responses::{ClientResponse, JsonBody, MessageBody, ResponseBody}; pub use self::sender::SendClientRequest; -use std::{convert::TryFrom, rc::Rc, time::Duration}; - -use actix_http::{error::HttpError, header::HeaderMap, Method, RequestHead, Uri}; -use actix_rt::net::TcpStream; -use actix_service::Service; - -use self::client::{ConnectInfo, TcpConnectError, TcpConnection}; - pub(crate) type BoxError = Box; - -/// An asynchronous HTTP and WebSocket client. -/// -/// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU -/// and memory usage. -/// -/// # Examples -/// ``` -/// use awc::Client; -/// -/// #[actix_rt::main] -/// async fn main() { -/// let mut client = Client::default(); -/// -/// let res = client.get("http://www.rust-lang.org") -/// .insert_header(("User-Agent", "my-app/1.2")) -/// .send() -/// .await; -/// -/// println!("Response: {:?}", res); -/// } -/// ``` -#[derive(Clone)] -pub struct Client(ClientConfig); - -#[derive(Clone)] -pub(crate) struct ClientConfig { - pub(crate) connector: BoxConnectorService, - pub(crate) default_headers: Rc, - pub(crate) timeout: Option, -} - -impl Default for Client { - fn default() -> Self { - ClientBuilder::new().finish() - } -} - -impl Client { - /// Create new client instance with default settings. - pub fn new() -> Client { - Client::default() - } - - /// Create `Client` builder. - /// This function is equivalent of `ClientBuilder::new()`. - pub fn builder() -> ClientBuilder< - impl Service< - ConnectInfo, - Response = TcpConnection, - Error = TcpConnectError, - > + Clone, - > { - ClientBuilder::new() - } - - /// Construct HTTP request. - pub fn request(&self, method: Method, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - let mut req = ClientRequest::new(method, url, self.0.clone()); - - for header in self.0.default_headers.iter() { - // header map is empty - // TODO: probably append instead - req = req.insert_header_if_none(header); - } - req - } - - /// Create `ClientRequest` from `RequestHead` - /// - /// It is useful for proxy requests. This implementation - /// copies all headers and the method. - pub fn request_from(&self, url: U, head: &RequestHead) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - let mut req = self.request(head.method.clone(), url); - for header in head.headers.iter() { - req = req.insert_header_if_none(header); - } - req - } - - /// Construct HTTP *GET* request. - pub fn get(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::GET, url) - } - - /// Construct HTTP *HEAD* request. - pub fn head(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::HEAD, url) - } - - /// Construct HTTP *PUT* request. - pub fn put(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::PUT, url) - } - - /// Construct HTTP *POST* request. - pub fn post(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::POST, url) - } - - /// Construct HTTP *PATCH* request. - pub fn patch(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::PATCH, url) - } - - /// Construct HTTP *DELETE* request. - pub fn delete(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::DELETE, url) - } - - /// Construct HTTP *OPTIONS* request. - pub fn options(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::OPTIONS, url) - } - - /// Initialize a WebSocket connection. - /// Returns a WebSocket connection builder. - pub fn ws(&self, url: U) -> ws::WebsocketsRequest - where - Uri: TryFrom, - >::Error: Into, - { - let mut req = ws::WebsocketsRequest::new(url, self.0.clone()); - for (key, value) in self.0.default_headers.iter() { - req.head.headers.insert(key.clone(), value.clone()); - } - req - } - - /// Get default HeaderMap of Client. - /// - /// Returns Some(&mut HeaderMap) when Client object is unique - /// (No other clone of client exists at the same time). - pub fn headers(&mut self) -> Option<&mut HeaderMap> { - Rc::get_mut(&mut self.0.default_headers) - } -} diff --git a/awc/src/request.rs b/awc/src/request.rs index 9e37b2755..8bcf1ee01 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -5,17 +5,18 @@ use futures_core::Stream; use serde::Serialize; use actix_http::{ + body::MessageBody, error::HttpError, header::{self, HeaderMap, HeaderValue, TryIntoHeaderPair}, ConnectionType, Method, RequestHead, Uri, Version, }; use crate::{ - any_body::AnyBody, + client::ClientConfig, error::{FreezeRequestError, InvalidUrl}, frozen::FrozenClientRequest, sender::{PrepForSendingError, RequestSender, SendClientRequest}, - BoxError, ClientConfig, + BoxError, }; #[cfg(feature = "cookies")] @@ -26,20 +27,20 @@ use crate::cookie::{Cookie, CookieJar}; /// This type can be used to construct an instance of `ClientRequest` through a /// builder-like pattern. /// -/// ``` -/// #[actix_rt::main] -/// async fn main() { -/// let response = awc::Client::new() -/// .get("http://www.rust-lang.org") // <- Create request builder -/// .insert_header(("User-Agent", "Actix-web")) -/// .send() // <- Send HTTP request -/// .await; +/// ```no_run +/// # #[actix_rt::main] +/// # async fn main() { +/// let response = awc::Client::new() +/// .get("http://www.rust-lang.org") // <- Create request builder +/// .insert_header(("User-Agent", "Actix-web")) +/// .send() // <- Send HTTP request +/// .await; /// -/// response.and_then(|response| { // <- server HTTP response -/// println!("Response: {:?}", response); -/// Ok(()) -/// }); -/// } +/// response.and_then(|response| { // <- server HTTP response +/// println!("Response: {:?}", response); +/// Ok(()) +/// }); +/// # } /// ``` pub struct ClientRequest { pub(crate) head: RequestHead, @@ -174,17 +175,13 @@ impl ClientRequest { /// Append a header, keeping any that were set with an equivalent field name. /// - /// ``` - /// # #[actix_rt::main] - /// # async fn main() { - /// # use awc::Client; - /// use awc::http::header::CONTENT_TYPE; + /// ```no_run + /// use awc::{http::header, Client}; /// /// Client::new() /// .get("http://www.rust-lang.org") /// .insert_header(("X-TEST", "value")) - /// .insert_header((CONTENT_TYPE, mime::APPLICATION_JSON)); - /// # } + /// .insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)); /// ``` pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self { match header.try_into_pair() { @@ -252,23 +249,18 @@ impl ClientRequest { /// Set a cookie /// - /// ``` - /// #[actix_rt::main] - /// async fn main() { - /// let resp = awc::Client::new().get("https://www.rust-lang.org") - /// .cookie( - /// awc::cookie::Cookie::build("name", "value") - /// .domain("www.rust-lang.org") - /// .path("/") - /// .secure(true) - /// .http_only(true) - /// .finish(), - /// ) - /// .send() - /// .await; + /// ```no_run + /// use awc::{cookie::Cookie, Client}; /// - /// println!("Response: {:?}", resp); - /// } + /// # #[actix_rt::main] + /// # async fn main() { + /// let res = Client::new().get("https://httpbin.org/cookies") + /// .cookie(Cookie::new("name", "value")) + /// .send() + /// .await; + /// + /// println!("Response: {:?}", res); + /// # } /// ``` #[cfg(feature = "cookies")] pub fn cookie(mut self, cookie: Cookie<'_>) -> Self { @@ -340,7 +332,7 @@ impl ClientRequest { /// Complete request construction and send body. pub fn send_body(self, body: B) -> SendClientRequest where - B: Into, + B: MessageBody + 'static, { let slf = match self.prep_for_sending() { Ok(slf) => slf, @@ -393,7 +385,7 @@ impl ClientRequest { /// Set an streaming body and generate `ClientRequest`. pub fn send_stream(self, stream: S) -> SendClientRequest where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, E: Into + 'static, { let slf = match self.prep_for_sending() { diff --git a/awc/src/response.rs b/awc/src/response.rs deleted file mode 100644 index fefebd0a0..000000000 --- a/awc/src/response.rs +++ /dev/null @@ -1,556 +0,0 @@ -use std::{ - cell::{Ref, RefMut}, - fmt, - future::Future, - io, - marker::PhantomData, - pin::Pin, - task::{Context, Poll}, - time::{Duration, Instant}, -}; - -use actix_http::{ - error::PayloadError, header, header::HeaderMap, Extensions, HttpMessage, Payload, - PayloadStream, ResponseHead, StatusCode, Version, -}; -use actix_rt::time::{sleep, Sleep}; -use bytes::{Bytes, BytesMut}; -use futures_core::{ready, Stream}; -use serde::de::DeserializeOwned; - -#[cfg(feature = "cookies")] -use crate::cookie::{Cookie, ParseError as CookieParseError}; -use crate::error::JsonPayloadError; - -/// Client Response -pub struct ClientResponse { - pub(crate) head: ResponseHead, - pub(crate) payload: Payload, - pub(crate) timeout: ResponseTimeout, -} - -/// helper enum with reusable sleep passed from `SendClientResponse`. -/// See `ClientResponse::_timeout` for reason. -pub(crate) enum ResponseTimeout { - Disabled(Option>>), - Enabled(Pin>), -} - -impl Default for ResponseTimeout { - fn default() -> Self { - Self::Disabled(None) - } -} - -impl ResponseTimeout { - fn poll_timeout(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> { - match *self { - Self::Enabled(ref mut timeout) => { - if timeout.as_mut().poll(cx).is_ready() { - Err(PayloadError::Io(io::Error::new( - io::ErrorKind::TimedOut, - "Response Payload IO timed out", - ))) - } else { - Ok(()) - } - } - Self::Disabled(_) => Ok(()), - } - } -} - -impl HttpMessage for ClientResponse { - type Stream = S; - - fn headers(&self) -> &HeaderMap { - &self.head.headers - } - - fn take_payload(&mut self) -> Payload { - std::mem::replace(&mut self.payload, Payload::None) - } - - fn extensions(&self) -> Ref<'_, Extensions> { - self.head.extensions() - } - - fn extensions_mut(&self) -> RefMut<'_, Extensions> { - self.head.extensions_mut() - } -} - -impl ClientResponse { - /// Create new Request instance - pub(crate) fn new(head: ResponseHead, payload: Payload) -> Self { - ClientResponse { - head, - payload, - timeout: ResponseTimeout::default(), - } - } - - #[inline] - pub(crate) fn head(&self) -> &ResponseHead { - &self.head - } - - /// Read the Request Version. - #[inline] - pub fn version(&self) -> Version { - self.head().version - } - - /// Get the status from the server. - #[inline] - pub fn status(&self) -> StatusCode { - self.head().status - } - - #[inline] - /// Returns request's headers. - pub fn headers(&self) -> &HeaderMap { - &self.head().headers - } - - /// Set a body and return previous body value - pub fn map_body(mut self, f: F) -> ClientResponse - where - F: FnOnce(&mut ResponseHead, Payload) -> Payload, - { - let payload = f(&mut self.head, self.payload); - - ClientResponse { - payload, - head: self.head, - timeout: self.timeout, - } - } - - /// Set a timeout duration for [`ClientResponse`](self::ClientResponse). - /// - /// This duration covers the duration of processing the response body stream - /// and would end it as timeout error when deadline met. - /// - /// Disabled by default. - pub fn timeout(self, dur: Duration) -> Self { - let timeout = match self.timeout { - ResponseTimeout::Disabled(Some(mut timeout)) - | ResponseTimeout::Enabled(mut timeout) => match Instant::now().checked_add(dur) { - Some(deadline) => { - timeout.as_mut().reset(deadline.into()); - ResponseTimeout::Enabled(timeout) - } - None => ResponseTimeout::Enabled(Box::pin(sleep(dur))), - }, - _ => ResponseTimeout::Enabled(Box::pin(sleep(dur))), - }; - - Self { - payload: self.payload, - head: self.head, - timeout, - } - } - - /// This method does not enable timeout. It's used to pass the boxed `Sleep` from - /// `SendClientRequest` and reuse it's heap allocation together with it's slot in - /// timer wheel. - pub(crate) fn _timeout(mut self, timeout: Option>>) -> Self { - self.timeout = ResponseTimeout::Disabled(timeout); - self - } - - /// Load request cookies. - #[cfg(feature = "cookies")] - pub fn cookies(&self) -> Result>>, CookieParseError> { - struct Cookies(Vec>); - - if self.extensions().get::().is_none() { - let mut cookies = Vec::new(); - for hdr in self.headers().get_all(&header::SET_COOKIE) { - let s = std::str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; - cookies.push(Cookie::parse_encoded(s)?.into_owned()); - } - self.extensions_mut().insert(Cookies(cookies)); - } - Ok(Ref::map(self.extensions(), |ext| { - &ext.get::().unwrap().0 - })) - } - - /// Return request cookie. - #[cfg(feature = "cookies")] - pub fn cookie(&self, name: &str) -> Option> { - if let Ok(cookies) = self.cookies() { - for cookie in cookies.iter() { - if cookie.name() == name { - return Some(cookie.to_owned()); - } - } - } - None - } -} - -impl ClientResponse -where - S: Stream>, -{ - /// Loads HTTP response's body. - pub fn body(&mut self) -> MessageBody { - MessageBody::new(self) - } - - /// Loads and parse `application/json` encoded body. - /// Return `JsonBody` future. It resolves to a `T` value. - /// - /// Returns error: - /// - /// * content type is not `application/json` - /// * content length is greater than 256k - pub fn json(&mut self) -> JsonBody { - JsonBody::new(self) - } -} - -impl Stream for ClientResponse -where - S: Stream> + Unpin, -{ - type Item = Result; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.get_mut(); - this.timeout.poll_timeout(cx)?; - - Pin::new(&mut this.payload).poll_next(cx) - } -} - -impl fmt::Debug for ClientResponse { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status(),)?; - writeln!(f, " headers:")?; - for (key, val) in self.headers().iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -const DEFAULT_BODY_LIMIT: usize = 2 * 1024 * 1024; - -/// Future that resolves to a complete HTTP message body. -pub struct MessageBody { - length: Option, - timeout: ResponseTimeout, - body: Result, Option>, -} - -impl MessageBody -where - S: Stream>, -{ - /// Create `MessageBody` for request. - pub fn new(res: &mut ClientResponse) -> MessageBody { - let length = match res.headers().get(&header::CONTENT_LENGTH) { - Some(value) => { - let len = value.to_str().ok().and_then(|s| s.parse::().ok()); - - match len { - None => return Self::err(PayloadError::UnknownLength), - len => len, - } - } - None => None, - }; - - MessageBody { - length, - timeout: std::mem::take(&mut res.timeout), - body: Ok(ReadBody::new(res.take_payload(), DEFAULT_BODY_LIMIT)), - } - } - - /// Change max size of payload. By default max size is 2048kB - pub fn limit(mut self, limit: usize) -> Self { - if let Ok(ref mut body) = self.body { - body.limit = limit; - } - self - } - - fn err(e: PayloadError) -> Self { - MessageBody { - length: None, - timeout: ResponseTimeout::default(), - body: Err(Some(e)), - } - } -} - -impl Future for MessageBody -where - S: Stream> + Unpin, -{ - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.get_mut(); - - match this.body { - Err(ref mut err) => Poll::Ready(Err(err.take().unwrap())), - Ok(ref mut body) => { - if let Some(len) = this.length.take() { - if len > body.limit { - return Poll::Ready(Err(PayloadError::Overflow)); - } - } - - this.timeout.poll_timeout(cx)?; - - Pin::new(body).poll(cx) - } - } - } -} - -/// Response's payload json parser, it resolves to a deserialized `T` value. -/// -/// Returns error: -/// -/// * content type is not `application/json` -/// * content length is greater than 64k -pub struct JsonBody { - length: Option, - err: Option, - timeout: ResponseTimeout, - fut: Option>, - _phantom: PhantomData, -} - -impl JsonBody -where - S: Stream>, - U: DeserializeOwned, -{ - /// Create `JsonBody` for request. - pub fn new(res: &mut ClientResponse) -> Self { - // check content-type - let json = if let Ok(Some(mime)) = res.mime_type() { - mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) - } else { - false - }; - if !json { - return JsonBody { - length: None, - fut: None, - timeout: ResponseTimeout::default(), - err: Some(JsonPayloadError::ContentType), - _phantom: PhantomData, - }; - } - - let mut len = None; - - if let Some(l) = res.headers().get(&header::CONTENT_LENGTH) { - if let Ok(s) = l.to_str() { - if let Ok(l) = s.parse::() { - len = Some(l) - } - } - } - - JsonBody { - length: len, - err: None, - timeout: std::mem::take(&mut res.timeout), - fut: Some(ReadBody::new(res.take_payload(), 65536)), - _phantom: PhantomData, - } - } - - /// Change max size of payload. By default max size is 64kB - pub fn limit(mut self, limit: usize) -> Self { - if let Some(ref mut fut) = self.fut { - fut.limit = limit; - } - self - } -} - -impl Unpin for JsonBody -where - T: Stream> + Unpin, - U: DeserializeOwned, -{ -} - -impl Future for JsonBody -where - T: Stream> + Unpin, - U: DeserializeOwned, -{ - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - if let Some(err) = self.err.take() { - return Poll::Ready(Err(err)); - } - - if let Some(len) = self.length.take() { - if len > self.fut.as_ref().unwrap().limit { - return Poll::Ready(Err(JsonPayloadError::Payload(PayloadError::Overflow))); - } - } - - self.timeout - .poll_timeout(cx) - .map_err(JsonPayloadError::Payload)?; - - let body = ready!(Pin::new(&mut self.get_mut().fut.as_mut().unwrap()).poll(cx))?; - Poll::Ready(serde_json::from_slice::(&body).map_err(JsonPayloadError::from)) - } -} - -struct ReadBody { - stream: Payload, - buf: BytesMut, - limit: usize, -} - -impl ReadBody { - fn new(stream: Payload, limit: usize) -> Self { - Self { - stream, - buf: BytesMut::new(), - limit, - } - } -} - -impl Future for ReadBody -where - S: Stream> + Unpin, -{ - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.get_mut(); - - while let Some(chunk) = ready!(Pin::new(&mut this.stream).poll_next(cx)?) { - if (this.buf.len() + chunk.len()) > this.limit { - return Poll::Ready(Err(PayloadError::Overflow)); - } - this.buf.extend_from_slice(&chunk); - } - - Poll::Ready(Ok(this.buf.split().freeze())) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde::{Deserialize, Serialize}; - - use crate::{http::header, test::TestResponse}; - - #[actix_rt::test] - async fn test_body() { - let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "xxxx")).finish(); - match req.body().await.err().unwrap() { - PayloadError::UnknownLength => {} - _ => unreachable!("error"), - } - - let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "10000000")).finish(); - match req.body().await.err().unwrap() { - PayloadError::Overflow => {} - _ => unreachable!("error"), - } - - let mut req = TestResponse::default() - .set_payload(Bytes::from_static(b"test")) - .finish(); - assert_eq!(req.body().await.ok().unwrap(), Bytes::from_static(b"test")); - - let mut req = TestResponse::default() - .set_payload(Bytes::from_static(b"11111111111111")) - .finish(); - match req.body().limit(5).await.err().unwrap() { - PayloadError::Overflow => {} - _ => unreachable!("error"), - } - } - - #[derive(Serialize, Deserialize, PartialEq, Debug)] - struct MyObject { - name: String, - } - - fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { - match err { - JsonPayloadError::Payload(PayloadError::Overflow) => { - matches!(other, JsonPayloadError::Payload(PayloadError::Overflow)) - } - JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), - _ => false, - } - } - - #[actix_rt::test] - async fn test_json_body() { - let mut req = TestResponse::default().finish(); - let json = JsonBody::<_, MyObject>::new(&mut req).await; - assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); - - let mut req = TestResponse::default() - .insert_header(( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/text"), - )) - .finish(); - let json = JsonBody::<_, MyObject>::new(&mut req).await; - assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); - - let mut req = TestResponse::default() - .insert_header(( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - )) - .insert_header(( - header::CONTENT_LENGTH, - header::HeaderValue::from_static("10000"), - )) - .finish(); - - let json = JsonBody::<_, MyObject>::new(&mut req).limit(100).await; - assert!(json_eq( - json.err().unwrap(), - JsonPayloadError::Payload(PayloadError::Overflow) - )); - - let mut req = TestResponse::default() - .insert_header(( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - )) - .insert_header(( - header::CONTENT_LENGTH, - header::HeaderValue::from_static("16"), - )) - .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) - .finish(); - - let json = JsonBody::<_, MyObject>::new(&mut req).await; - assert_eq!( - json.ok().unwrap(), - MyObject { - name: "test".to_owned() - } - ); - } -} diff --git a/awc/src/responses/json_body.rs b/awc/src/responses/json_body.rs new file mode 100644 index 000000000..3912324b6 --- /dev/null +++ b/awc/src/responses/json_body.rs @@ -0,0 +1,192 @@ +use std::{ + future::Future, + marker::PhantomData, + mem, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_http::{error::PayloadError, header, HttpMessage}; +use bytes::Bytes; +use futures_core::{ready, Stream}; +use pin_project_lite::pin_project; +use serde::de::DeserializeOwned; + +use super::{read_body::ReadBody, ResponseTimeout, DEFAULT_BODY_LIMIT}; +use crate::{error::JsonPayloadError, ClientResponse}; + +pin_project! { + /// A `Future` that reads a body stream, parses JSON, resolving to a deserialized `T`. + /// + /// # Errors + /// `Future` implementation returns error if: + /// - content type is not `application/json`; + /// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB). + pub struct JsonBody { + #[pin] + body: Option>, + length: Option, + timeout: ResponseTimeout, + err: Option, + _phantom: PhantomData, + } +} + +impl JsonBody +where + S: Stream>, + T: DeserializeOwned, +{ + /// Creates a JSON body stream reader from a response by taking its payload. + pub fn new(res: &mut ClientResponse) -> Self { + // check content-type + let json = if let Ok(Some(mime)) = res.mime_type() { + mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) + } else { + false + }; + + if !json { + return JsonBody { + length: None, + body: None, + timeout: ResponseTimeout::default(), + err: Some(JsonPayloadError::ContentType), + _phantom: PhantomData, + }; + } + + let length = res + .headers() + .get(&header::CONTENT_LENGTH) + .and_then(|len_hdr| len_hdr.to_str().ok()) + .and_then(|len_str| len_str.parse::().ok()); + + JsonBody { + body: Some(ReadBody::new(res.take_payload(), DEFAULT_BODY_LIMIT)), + length, + timeout: mem::take(&mut res.timeout), + err: None, + _phantom: PhantomData, + } + } + + /// Change max size of payload. Default limit is 2 MiB. + pub fn limit(mut self, limit: usize) -> Self { + if let Some(ref mut fut) = self.body { + fut.limit = limit; + } + + self + } +} + +impl Future for JsonBody +where + S: Stream>, + T: DeserializeOwned, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + + if let Some(err) = this.err.take() { + return Poll::Ready(Err(err)); + } + + if let Some(len) = this.length.take() { + let body = Option::as_ref(&this.body).unwrap(); + if len > body.limit { + return Poll::Ready(Err(JsonPayloadError::Payload(PayloadError::Overflow))); + } + } + + this.timeout + .poll_timeout(cx) + .map_err(JsonPayloadError::Payload)?; + + let body = ready!(this.body.as_pin_mut().unwrap().poll(cx))?; + Poll::Ready(serde_json::from_slice::(&body).map_err(JsonPayloadError::from)) + } +} + +#[cfg(test)] +mod tests { + use actix_http::BoxedPayloadStream; + use serde::{Deserialize, Serialize}; + use static_assertions::assert_impl_all; + + use super::*; + use crate::{http::header, test::TestResponse}; + + assert_impl_all!(JsonBody: Unpin); + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct MyObject { + name: String, + } + + fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { + match err { + JsonPayloadError::Payload(PayloadError::Overflow) => { + matches!(other, JsonPayloadError::Payload(PayloadError::Overflow)) + } + JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), + _ => false, + } + } + + #[actix_rt::test] + async fn read_json_body() { + let mut req = TestResponse::default().finish(); + let json = JsonBody::<_, MyObject>::new(&mut req).await; + assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); + + let mut req = TestResponse::default() + .insert_header(( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/text"), + )) + .finish(); + let json = JsonBody::<_, MyObject>::new(&mut req).await; + assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); + + let mut req = TestResponse::default() + .insert_header(( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + )) + .insert_header(( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("10000"), + )) + .finish(); + + let json = JsonBody::<_, MyObject>::new(&mut req).limit(100).await; + assert!(json_eq( + json.err().unwrap(), + JsonPayloadError::Payload(PayloadError::Overflow) + )); + + let mut req = TestResponse::default() + .insert_header(( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + )) + .insert_header(( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + )) + .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .finish(); + + let json = JsonBody::<_, MyObject>::new(&mut req).await; + assert_eq!( + json.ok().unwrap(), + MyObject { + name: "test".to_owned() + } + ); + } +} diff --git a/awc/src/responses/mod.rs b/awc/src/responses/mod.rs new file mode 100644 index 000000000..588ce014c --- /dev/null +++ b/awc/src/responses/mod.rs @@ -0,0 +1,49 @@ +use std::{future::Future, io, pin::Pin, task::Context}; + +use actix_http::error::PayloadError; +use actix_rt::time::Sleep; + +mod json_body; +mod read_body; +mod response; +mod response_body; + +pub use self::json_body::JsonBody; +pub use self::response::ClientResponse; +#[allow(deprecated)] +pub use self::response_body::{MessageBody, ResponseBody}; + +/// Default body size limit: 2 MiB +const DEFAULT_BODY_LIMIT: usize = 2 * 1024 * 1024; + +/// Helper enum with reusable sleep passed from `SendClientResponse`. +/// +/// See [`ClientResponse::_timeout`] for reason. +pub(crate) enum ResponseTimeout { + Disabled(Option>>), + Enabled(Pin>), +} + +impl Default for ResponseTimeout { + fn default() -> Self { + Self::Disabled(None) + } +} + +impl ResponseTimeout { + fn poll_timeout(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> { + match *self { + Self::Enabled(ref mut timeout) => { + if timeout.as_mut().poll(cx).is_ready() { + Err(PayloadError::Io(io::Error::new( + io::ErrorKind::TimedOut, + "Response Payload IO timed out", + ))) + } else { + Ok(()) + } + } + Self::Disabled(_) => Ok(()), + } + } +} diff --git a/awc/src/responses/read_body.rs b/awc/src/responses/read_body.rs new file mode 100644 index 000000000..a32bbb984 --- /dev/null +++ b/awc/src/responses/read_body.rs @@ -0,0 +1,61 @@ +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_http::{error::PayloadError, Payload}; +use bytes::{Bytes, BytesMut}; +use futures_core::{ready, Stream}; +use pin_project_lite::pin_project; + +pin_project! { + pub(crate) struct ReadBody { + #[pin] + pub(crate) stream: Payload, + pub(crate) buf: BytesMut, + pub(crate) limit: usize, + } +} + +impl ReadBody { + pub(crate) fn new(stream: Payload, limit: usize) -> Self { + Self { + stream, + buf: BytesMut::new(), + limit, + } + } +} + +impl Future for ReadBody +where + S: Stream>, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + + while let Some(chunk) = ready!(this.stream.as_mut().poll_next(cx)?) { + if (this.buf.len() + chunk.len()) > *this.limit { + return Poll::Ready(Err(PayloadError::Overflow)); + } + + this.buf.extend_from_slice(&chunk); + } + + Poll::Ready(Ok(this.buf.split().freeze())) + } +} + +#[cfg(test)] +mod tests { + use static_assertions::assert_impl_all; + + use super::*; + use crate::any_body::AnyBody; + + assert_impl_all!(ReadBody<()>: Unpin); + assert_impl_all!(ReadBody: Unpin); +} diff --git a/awc/src/responses/response.rs b/awc/src/responses/response.rs new file mode 100644 index 000000000..0197265f1 --- /dev/null +++ b/awc/src/responses/response.rs @@ -0,0 +1,258 @@ +use std::{ + cell::{Ref, RefMut}, + fmt, mem, + pin::Pin, + task::{Context, Poll}, + time::{Duration, Instant}, +}; + +use actix_http::{ + error::PayloadError, header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, + Payload, ResponseHead, StatusCode, Version, +}; +use actix_rt::time::{sleep, Sleep}; +use bytes::Bytes; +use futures_core::Stream; +use pin_project_lite::pin_project; +use serde::de::DeserializeOwned; + +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, ParseError as CookieParseError}; + +use super::{JsonBody, ResponseBody, ResponseTimeout}; + +pin_project! { + /// Client Response + pub struct ClientResponse { + pub(crate) head: ResponseHead, + #[pin] + pub(crate) payload: Payload, + pub(crate) timeout: ResponseTimeout, + } +} + +impl ClientResponse { + /// Create new Request instance + pub(crate) fn new(head: ResponseHead, payload: Payload) -> Self { + ClientResponse { + head, + payload, + timeout: ResponseTimeout::default(), + } + } + + #[inline] + pub(crate) fn head(&self) -> &ResponseHead { + &self.head + } + + /// Read the Request Version. + #[inline] + pub fn version(&self) -> Version { + self.head().version + } + + /// Get the status from the server. + #[inline] + pub fn status(&self) -> StatusCode { + self.head().status + } + + #[inline] + /// Returns request's headers. + pub fn headers(&self) -> &HeaderMap { + &self.head().headers + } + + /// Set a body and return previous body value + pub fn map_body(mut self, f: F) -> ClientResponse + where + F: FnOnce(&mut ResponseHead, Payload) -> Payload, + { + let payload = f(&mut self.head, self.payload); + + ClientResponse { + payload, + head: self.head, + timeout: self.timeout, + } + } + + /// Set a timeout duration for [`ClientResponse`](self::ClientResponse). + /// + /// This duration covers the duration of processing the response body stream + /// and would end it as timeout error when deadline met. + /// + /// Disabled by default. + pub fn timeout(self, dur: Duration) -> Self { + let timeout = match self.timeout { + ResponseTimeout::Disabled(Some(mut timeout)) + | ResponseTimeout::Enabled(mut timeout) => match Instant::now().checked_add(dur) { + Some(deadline) => { + timeout.as_mut().reset(deadline.into()); + ResponseTimeout::Enabled(timeout) + } + None => ResponseTimeout::Enabled(Box::pin(sleep(dur))), + }, + _ => ResponseTimeout::Enabled(Box::pin(sleep(dur))), + }; + + Self { + payload: self.payload, + head: self.head, + timeout, + } + } + + /// This method does not enable timeout. It's used to pass the boxed `Sleep` from + /// `SendClientRequest` and reuse it's heap allocation together with it's slot in + /// timer wheel. + pub(crate) fn _timeout(mut self, timeout: Option>>) -> Self { + self.timeout = ResponseTimeout::Disabled(timeout); + self + } + + /// Load request cookies. + #[cfg(feature = "cookies")] + pub fn cookies(&self) -> Result>>, CookieParseError> { + struct Cookies(Vec>); + + if self.extensions().get::().is_none() { + let mut cookies = Vec::new(); + for hdr in self.headers().get_all(&actix_http::header::SET_COOKIE) { + let s = std::str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; + cookies.push(Cookie::parse_encoded(s)?.into_owned()); + } + self.extensions_mut().insert(Cookies(cookies)); + } + + Ok(Ref::map(self.extensions(), |ext| { + &ext.get::().unwrap().0 + })) + } + + /// Return request cookie. + #[cfg(feature = "cookies")] + pub fn cookie(&self, name: &str) -> Option> { + if let Ok(cookies) = self.cookies() { + for cookie in cookies.iter() { + if cookie.name() == name { + return Some(cookie.to_owned()); + } + } + } + None + } +} + +impl ClientResponse +where + S: Stream>, +{ + /// Returns a [`Future`] that consumes the body stream and resolves to [`Bytes`]. + /// + /// # Errors + /// `Future` implementation returns error if: + /// - content type is not `application/json` + /// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB) + /// + /// # Examples + /// ```no_run + /// # use awc::Client; + /// # use bytes::Bytes; + /// # #[actix_rt::main] + /// # async fn async_ctx() -> Result<(), Box> { + /// let client = Client::default(); + /// let mut res = client.get("https://httpbin.org/robots.txt").send().await?; + /// let body: Bytes = res.body().await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Future`]: std::future::Future + pub fn body(&mut self) -> ResponseBody { + ResponseBody::new(self) + } + + /// Returns a [`Future`] consumes the body stream, parses JSON, and resolves to a deserialized + /// `T` value. + /// + /// # Errors + /// Future returns error if: + /// - content type is not `application/json`; + /// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB). + /// + /// # Examples + /// ```no_run + /// # use awc::Client; + /// # #[actix_rt::main] + /// # async fn async_ctx() -> Result<(), Box> { + /// let client = Client::default(); + /// let mut res = client.get("https://httpbin.org/json").send().await?; + /// let val = res.json::().await?; + /// assert!(val.is_object()); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Future`]: std::future::Future + pub fn json(&mut self) -> JsonBody { + JsonBody::new(self) + } +} + +impl fmt::Debug for ClientResponse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status(),)?; + writeln!(f, " headers:")?; + for (key, val) in self.headers().iter() { + writeln!(f, " {:?}: {:?}", key, val)?; + } + Ok(()) + } +} + +impl HttpMessage for ClientResponse { + type Stream = S; + + fn headers(&self) -> &HeaderMap { + &self.head.headers + } + + fn take_payload(&mut self) -> Payload { + mem::replace(&mut self.payload, Payload::None) + } + + fn extensions(&self) -> Ref<'_, Extensions> { + self.head.extensions() + } + + fn extensions_mut(&self) -> RefMut<'_, Extensions> { + self.head.extensions_mut() + } +} + +impl Stream for ClientResponse +where + S: Stream> + Unpin, +{ + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + this.timeout.poll_timeout(cx)?; + this.payload.poll_next(cx) + } +} + +#[cfg(test)] +mod tests { + use static_assertions::assert_impl_all; + + use super::*; + use crate::any_body::AnyBody; + + assert_impl_all!(ClientResponse: Unpin); + assert_impl_all!(ClientResponse<()>: Unpin); + assert_impl_all!(ClientResponse: Unpin); +} diff --git a/awc/src/responses/response_body.rs b/awc/src/responses/response_body.rs new file mode 100644 index 000000000..8d9d1274a --- /dev/null +++ b/awc/src/responses/response_body.rs @@ -0,0 +1,144 @@ +use std::{ + future::Future, + mem, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_http::{error::PayloadError, header, HttpMessage}; +use bytes::Bytes; +use futures_core::Stream; +use pin_project_lite::pin_project; + +use super::{read_body::ReadBody, ResponseTimeout, DEFAULT_BODY_LIMIT}; +use crate::ClientResponse; + +pin_project! { + /// A `Future` that reads a body stream, resolving as [`Bytes`]. + /// + /// # Errors + /// `Future` implementation returns error if: + /// - content type is not `application/json`; + /// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB). + pub struct ResponseBody { + #[pin] + body: Option>, + length: Option, + timeout: ResponseTimeout, + err: Option, + } +} + +#[deprecated(since = "3.0.0", note = "Renamed to `ResponseBody`.")] +pub type MessageBody = ResponseBody; + +impl ResponseBody +where + S: Stream>, +{ + /// Creates a body stream reader from a response by taking its payload. + pub fn new(res: &mut ClientResponse) -> ResponseBody { + let length = match res.headers().get(&header::CONTENT_LENGTH) { + Some(value) => { + let len = value.to_str().ok().and_then(|s| s.parse::().ok()); + + match len { + None => return Self::err(PayloadError::UnknownLength), + len => len, + } + } + None => None, + }; + + ResponseBody { + body: Some(ReadBody::new(res.take_payload(), DEFAULT_BODY_LIMIT)), + length, + timeout: mem::take(&mut res.timeout), + err: None, + } + } + + /// Change max size limit of payload. + /// + /// The default limit is 2 MiB. + pub fn limit(mut self, limit: usize) -> Self { + if let Some(ref mut body) = self.body { + body.limit = limit; + } + + self + } + + fn err(err: PayloadError) -> Self { + ResponseBody { + body: None, + length: None, + timeout: ResponseTimeout::default(), + err: Some(err), + } + } +} + +impl Future for ResponseBody +where + S: Stream>, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + + if let Some(err) = this.err.take() { + return Poll::Ready(Err(err)); + } + + if let Some(len) = this.length.take() { + let body = Option::as_ref(&this.body).unwrap(); + if len > body.limit { + return Poll::Ready(Err(PayloadError::Overflow)); + } + } + + this.timeout.poll_timeout(cx)?; + + this.body.as_pin_mut().unwrap().poll(cx) + } +} + +#[cfg(test)] +mod tests { + use static_assertions::assert_impl_all; + + use super::*; + use crate::{http::header, test::TestResponse}; + + assert_impl_all!(ResponseBody<()>: Unpin); + + #[actix_rt::test] + async fn read_body() { + let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "xxxx")).finish(); + match req.body().await.err().unwrap() { + PayloadError::UnknownLength => {} + _ => unreachable!("error"), + } + + let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "10000000")).finish(); + match req.body().await.err().unwrap() { + PayloadError::Overflow => {} + _ => unreachable!("error"), + } + + let mut req = TestResponse::default() + .set_payload(Bytes::from_static(b"test")) + .finish(); + assert_eq!(req.body().await.ok().unwrap(), Bytes::from_static(b"test")); + + let mut req = TestResponse::default() + .set_payload(Bytes::from_static(b"11111111111111")) + .finish(); + match req.body().limit(5).await.err().unwrap() { + PayloadError::Overflow => {} + _ => unreachable!("error"), + } + } +} diff --git a/awc/src/sender.rs b/awc/src/sender.rs index f83a70a9b..cd30e571d 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -8,7 +8,7 @@ use std::{ }; use actix_http::{ - body::BodyStream, + body::{BodyStream, MessageBody}, error::HttpError, header::{self, HeaderMap, HeaderName, TryIntoHeaderValue}, RequestHead, RequestHeadType, @@ -20,12 +20,13 @@ use futures_core::Stream; use serde::Serialize; #[cfg(feature = "__compress")] -use actix_http::{encoding::Decoder, header::ContentEncoding, Payload, PayloadStream}; +use actix_http::{encoding::Decoder, header::ContentEncoding, Payload}; use crate::{ any_body::AnyBody, + client::ClientConfig, error::{FreezeRequestError, InvalidUrl, SendRequestError}, - BoxError, ClientConfig, ClientResponse, ConnectRequest, ConnectResponse, + BoxError, ClientResponse, ConnectRequest, ConnectResponse, }; #[derive(Debug, From)] @@ -91,7 +92,7 @@ impl SendClientRequest { #[cfg(feature = "__compress")] impl Future for SendClientRequest { - type Output = Result>>, SendRequestError>; + type Output = Result>, SendRequestError>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); @@ -108,12 +109,13 @@ impl Future for SendClientRequest { res.into_client_response()._timeout(delay.take()).map_body( |head, payload| { if *response_decompress { - Payload::Stream(Decoder::from_headers(payload, &head.headers)) + Payload::Stream { + payload: Decoder::from_headers(payload, &head.headers), + } } else { - Payload::Stream(Decoder::new( - payload, - ContentEncoding::Identity, - )) + Payload::Stream { + payload: Decoder::new(payload, ContentEncoding::Identity), + } } }, ) @@ -179,24 +181,23 @@ pub(crate) enum RequestSender { } impl RequestSender { - pub(crate) fn send_body( + pub(crate) fn send_body( self, addr: Option, response_decompress: bool, timeout: Option, config: &ClientConfig, - body: B, - ) -> SendClientRequest - where - B: Into, - { + body: impl MessageBody + 'static, + ) -> SendClientRequest { let req = match self { - RequestSender::Owned(head) => { - ConnectRequest::Client(RequestHeadType::Owned(head), body.into(), addr) - } + RequestSender::Owned(head) => ConnectRequest::Client( + RequestHeadType::Owned(head), + AnyBody::from_message_body(body).into_boxed(), + addr, + ), RequestSender::Rc(head, extra_headers) => ConnectRequest::Client( RequestHeadType::Rc(head, extra_headers), - body.into(), + AnyBody::from_message_body(body).into_boxed(), addr, ), }; @@ -206,15 +207,15 @@ impl RequestSender { SendClientRequest::new(fut, response_decompress, timeout.or(config.timeout)) } - pub(crate) fn send_json( + pub(crate) fn send_json( mut self, addr: Option, response_decompress: bool, timeout: Option, config: &ClientConfig, - value: &T, + value: impl Serialize, ) -> SendClientRequest { - let body = match serde_json::to_string(value) { + let body = match serde_json::to_string(&value) { Ok(body) => body, Err(err) => return PrepForSendingError::Json(err).into(), }; @@ -223,24 +224,16 @@ impl RequestSender { return e.into(); } - self.send_body( - addr, - response_decompress, - timeout, - config, - AnyBody::Bytes { - body: Bytes::from(body), - }, - ) + self.send_body(addr, response_decompress, timeout, config, body) } - pub(crate) fn send_form( + pub(crate) fn send_form( mut self, addr: Option, response_decompress: bool, timeout: Option, config: &ClientConfig, - value: &T, + value: impl Serialize, ) -> SendClientRequest { let body = match serde_urlencoded::to_string(value) { Ok(body) => body, @@ -248,21 +241,13 @@ impl RequestSender { }; // set content-type - if let Err(e) = + if let Err(err) = self.set_header_if_none(header::CONTENT_TYPE, "application/x-www-form-urlencoded") { - return e.into(); + return err.into(); } - self.send_body( - addr, - response_decompress, - timeout, - config, - AnyBody::Bytes { - body: Bytes::from(body), - }, - ) + self.send_body(addr, response_decompress, timeout, config, body) } pub(crate) fn send_stream( @@ -274,7 +259,7 @@ impl RequestSender { stream: S, ) -> SendClientRequest where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, E: Into + 'static, { self.send_body( @@ -282,7 +267,7 @@ impl RequestSender { response_decompress, timeout, config, - AnyBody::new_boxed(BodyStream::new(stream)), + BodyStream::new(stream), ) } @@ -293,7 +278,7 @@ impl RequestSender { timeout: Option, config: &ClientConfig, ) -> SendClientRequest { - self.send_body(addr, response_decompress, timeout, config, AnyBody::empty()) + self.send_body(addr, response_decompress, timeout, config, ()) } fn set_header_if_none(&mut self, key: HeaderName, value: V) -> Result<(), HttpError> diff --git a/awc/src/test.rs b/awc/src/test.rs index 1b41efc93..96ae1f0a1 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -65,7 +65,7 @@ impl TestResponse { /// Set response's payload pub fn set_payload>(mut self, data: B) -> Self { - let mut payload = h1::Payload::empty(); + let (_, mut payload) = h1::Payload::create(true); payload.unread_data(data.into()); self.payload = Some(payload.into()); self @@ -90,7 +90,8 @@ impl TestResponse { if let Some(pl) = self.payload { ClientResponse::new(head, pl) } else { - ClientResponse::new(head, h1::Payload::empty().into()) + let (_, payload) = h1::Payload::create(true); + ClientResponse::new(head, payload.into()) } } } diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 06d54aadb..f3ee02d43 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -31,19 +31,19 @@ use std::{convert::TryFrom, fmt, net::SocketAddr, str}; use actix_codec::Framed; use actix_http::{ws, Payload, RequestHead}; use actix_rt::time::timeout; -use actix_service::Service; +use actix_service::Service as _; pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message}; use crate::{ + client::ClientConfig, connect::{BoxedSocket, ConnectRequest}, error::{HttpError, InvalidUrl, SendRequestError, WsClientError}, http::{ header::{self, HeaderName, HeaderValue, TryIntoHeaderValue, AUTHORIZATION}, ConnectionType, Method, StatusCode, Uri, Version, }, - response::ClientResponse, - ClientConfig, + ClientResponse, }; #[cfg(feature = "cookies")] @@ -300,13 +300,16 @@ impl WebsocketsRequest { } self.head.set_connection_type(ConnectionType::Upgrade); + + #[allow(clippy::declare_interior_mutable_const)] + const HV_WEBSOCKET: HeaderValue = HeaderValue::from_static("websocket"); + self.head.headers.insert(header::UPGRADE, HV_WEBSOCKET); + + #[allow(clippy::declare_interior_mutable_const)] + const HV_THIRTEEN: HeaderValue = HeaderValue::from_static("13"); self.head .headers - .insert(header::UPGRADE, HeaderValue::from_static("websocket")); - self.head.headers.insert( - header::SEC_WEBSOCKET_VERSION, - HeaderValue::from_static("13"), - ); + .insert(header::SEC_WEBSOCKET_VERSION, HV_THIRTEEN); if let Some(protocols) = self.protocols.take() { self.head.headers.insert( diff --git a/src/app.rs b/src/app.rs index b4b952734..10868d18d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -122,9 +122,10 @@ impl App { self.app_data(Data::new(data)) } - /// 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. + /// Add application data factory that resolves asynchronously. + /// + /// Data items are constructed during application initialization, before the server starts + /// accepting requests. pub fn data_factory(mut self, data: F) -> Self where F: Fn() -> Out + 'static, @@ -150,6 +151,7 @@ impl App { } .boxed_local() })); + self } @@ -200,11 +202,9 @@ impl App { /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new() - /// .route("/test1", web::get().to(index)) - /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())); - /// } + /// let app = App::new() + /// .route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())); /// ``` pub fn route(self, path: &str, mut route: Route) -> Self { self.service( @@ -243,13 +243,11 @@ impl App { /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new() - /// .service( - /// web::resource("/index.html").route(web::get().to(index))) - /// .default_service( - /// web::route().to(|| HttpResponse::NotFound())); - /// } + /// let app = App::new() + /// .service( + /// web::resource("/index.html").route(web::get().to(index))) + /// .default_service( + /// web::route().to(|| HttpResponse::NotFound())); /// ``` /// /// It is also possible to use static files as default service. @@ -257,14 +255,12 @@ impl App { /// ``` /// use actix_web::{web, App, HttpResponse}; /// - /// fn main() { - /// let app = App::new() - /// .service( - /// web::resource("/index.html").to(|| HttpResponse::Ok())) - /// .default_service( - /// web::to(|| HttpResponse::NotFound()) - /// ); - /// } + /// let app = App::new() + /// .service( + /// web::resource("/index.html").to(|| HttpResponse::Ok())) + /// .default_service( + /// web::to(|| HttpResponse::NotFound()) + /// ); /// ``` pub fn default_service(mut self, svc: F) -> Self where diff --git a/src/app_service.rs b/src/app_service.rs index 4e84cb201..56b24f0d8 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -1,14 +1,16 @@ use std::{cell::RefCell, mem, rc::Rc}; -use actix_http::{Extensions, Request}; +use actix_http::Request; use actix_router::{Path, ResourceDef, Router, Url}; use actix_service::{boxed, fn_service, Service, ServiceFactory}; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; use crate::{ + body::BoxBody, config::{AppConfig, AppService}, data::FnDataFactory, + dev::Extensions, guard::Guard, request::{HttpRequest, HttpRequestPool}, rmap::ResourceMap, @@ -236,6 +238,7 @@ where } pub struct AppRoutingFactory { + #[allow(clippy::type_complexity)] services: Rc< [( ResourceDef, @@ -296,7 +299,7 @@ pub struct AppRouting { } impl Service for AppRouting { - type Response = ServiceResponse; + type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; @@ -305,12 +308,15 @@ impl Service for AppRouting { fn call(&self, mut req: ServiceRequest) -> Self::Future { let res = self.router.recognize_fn(&mut req, |req, guards| { if let Some(ref guards) = guards { - for f in guards { - if !f.check(req.head()) { + let guard_ctx = req.guard_ctx(); + + for guard in guards { + if !guard.check(&guard_ctx) { return false; } } } + true }); diff --git a/src/config.rs b/src/config.rs index 9e77c0f96..d68374387 100644 --- a/src/config.rs +++ b/src/config.rs @@ -24,6 +24,7 @@ pub struct AppService { config: AppConfig, root: bool, default: Rc, + #[allow(clippy::type_complexity)] services: Vec<( ResourceDef, HttpNewService, @@ -48,6 +49,7 @@ impl AppService { self.root } + #[allow(clippy::type_complexity)] pub(crate) fn into_services( self, ) -> ( @@ -126,7 +128,7 @@ impl AppConfig { /// Server host name. /// - /// Host name is used by application router as a hostname for url generation. + /// Host name is used by application router as a hostname for URL generation. /// Check [ConnectionInfo](super::dev::ConnectionInfo::host()) /// documentation for more information. /// @@ -135,7 +137,7 @@ impl AppConfig { &self.host } - /// Returns true if connection is secure(https) + /// Returns true if connection is secure (i.e., running over `https:`). pub fn secure(&self) -> bool { self.secure } diff --git a/src/dev.rs b/src/dev.rs index a28735b38..1d4f0893a 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -3,6 +3,16 @@ //! Most users will not have to interact with the types in this module, but it is useful for those //! writing extractors, middleware, libraries, or interacting with the service API directly. +pub use actix_http::{Extensions, Payload, RequestHead, Response, ResponseHead}; +pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; +pub use actix_server::{Server, ServerHandle}; +pub use actix_service::{ + always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform, +}; + +#[cfg(feature = "__compress")] +pub use actix_http::encoding::Decoder as Decompress; + pub use crate::config::{AppConfig, AppService}; #[doc(hidden)] pub use crate::handler::Handler; @@ -12,16 +22,6 @@ pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, We pub use crate::types::{JsonBody, Readlines, UrlEncoded}; -pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, Response, ResponseHead}; -pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; -pub use actix_server::{Server, ServerHandle}; -pub use actix_service::{ - always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform, -}; - -#[cfg(feature = "__compress")] -pub use actix_http::encoding::Decoder as Decompress; - use crate::http::header::ContentEncoding; use actix_router::Patterns; @@ -44,7 +44,6 @@ pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns { patterns } -struct Enc(ContentEncoding); /// Helper trait that allows to set specific encoding for response. pub trait BodyEncoding { @@ -68,6 +67,8 @@ impl BodyEncoding for actix_http::ResponseBuilder { } } +struct Enc(ContentEncoding); + impl BodyEncoding for actix_http::Response { fn get_encoding(&self) -> Option { self.extensions().get::().map(|enc| enc.0) diff --git a/src/guard.rs b/src/guard.rs index a5770df89..ebda69cb9 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -1,168 +1,232 @@ -//! Route match guards. +//! Route guards. //! -//! Guards are one of the ways how actix-web router chooses a -//! handler service. In essence it is just a function that accepts a -//! reference to a `RequestHead` instance and returns a boolean. -//! It is possible to add guards to *scopes*, *resources* -//! and *routes*. Actix provide several guards by default, like various -//! http methods, header, etc. To become a guard, type must implement `Guard` -//! trait. Simple functions could be guards as well. +//! Guards are used during routing to help select a matching service or handler using some aspect of +//! the request; though guards should not be used for path matching since it is a built-in function +//! of the Actix Web router. //! -//! Guards can not modify the request object. But it is possible -//! to store extra attributes on a request by using the `Extensions` container. -//! Extensions containers are available via the `RequestHead::extensions()` method. +//! Guards can be used on [`Scope`]s, [`Resource`]s, [`Route`]s, and other custom services. //! +//! Fundamentally, a guard is a predicate function that receives a reference to a request context +//! object and returns a boolean; true if the request _should_ be handled by the guarded service +//! or handler. This interface is defined by the [`Guard`] trait. +//! +//! Commonly-used guards are provided in this module as well as a way of creating a guard from a +//! closure ([`fn_guard`]). The [`Not`], [`Any`], and [`All`] guards are noteworthy, as they can be +//! used to compose other guards in a more flexible and semantic way than calling `.guard(...)` on +//! services multiple times (which might have different combining behavior than you want). +//! +//! There are shortcuts for routes with method guards in the [`web`](crate::web) module: +//! [`web::get()`](crate::web::get), [`web::post()`](crate::web::post), etc. The routes created by +//! the following calls are equivalent: +//! - `web::get()` (recommended form) +//! - `web::route().guard(guard::Get())` +//! +//! Guards can not modify anything about the request. However, it is possible to store extra +//! attributes in the request-local data container obtained with [`GuardContext::req_data_mut`]. +//! +//! Guards can prevent resource definitions from overlapping which, when only considering paths, +//! would result in inaccessible routes. See the [`Host`] guard for an example of virtual hosting. +//! +//! # Examples +//! In the following code, the `/guarded` resource has one defined route whose handler will only be +//! called if the request method is `POST` and there is a request header with name and value equal +//! to `x-guarded` and `secret`, respectively. //! ``` -//! use actix_web::{web, http, dev, guard, App, HttpResponse}; +//! use actix_web::{web, http::Method, guard, HttpResponse}; //! -//! fn main() { -//! App::new().service(web::resource("/index.html").route( -//! web::route() -//! .guard(guard::Post()) -//! .guard(guard::fn_guard(|head| head.method == http::Method::GET)) -//! .to(|| HttpResponse::MethodNotAllowed())) -//! ); -//! } +//! web::resource("/guarded").route( +//! web::route() +//! .guard(guard::Any(guard::Get()).or(guard::Post())) +//! .guard(guard::Header("x-guarded", "secret")) +//! .to(|| HttpResponse::Ok()) +//! ); //! ``` +//! +//! [`Scope`]: crate::Scope::guard() +//! [`Resource`]: crate::Resource::guard() +//! [`Route`]: crate::Route::guard() -#![allow(non_snake_case)] +use std::{ + cell::{Ref, RefMut}, + convert::TryFrom, + rc::Rc, +}; -use std::rc::Rc; -use std::{convert::TryFrom, ops::Deref}; +use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead}; -use actix_http::{header, uri::Uri, Method as HttpMethod, RequestHead}; +use crate::service::ServiceRequest; -/// Trait defines resource guards. Guards are used for route selection. -/// -/// Guards can not modify the request object. But it is possible -/// to store extra attributes on a request by using the `Extensions` container. -/// Extensions containers are available via the `RequestHead::extensions()` method. -pub trait Guard { - /// Check if request matches predicate - fn check(&self, request: &RequestHead) -> bool; +/// Provides access to request parts that are useful during routing. +#[derive(Debug)] +pub struct GuardContext<'a> { + pub(crate) req: &'a ServiceRequest, } -impl Guard for Rc { - fn check(&self, request: &RequestHead) -> bool { - self.deref().check(request) +impl<'a> GuardContext<'a> { + /// Returns reference to the request head. + #[inline] + pub fn head(&self) -> &RequestHead { + self.req.head() + } + + /// Returns reference to the request-local data container. + #[inline] + pub fn req_data(&self) -> Ref<'a, Extensions> { + self.req.req_data() + } + + /// Returns mutable reference to the request-local data container. + #[inline] + pub fn req_data_mut(&self) -> RefMut<'a, Extensions> { + self.req.req_data_mut() } } -/// Create guard object for supplied function. +/// Interface for routing guards. /// +/// See [module level documentation](self) for more. +pub trait Guard { + /// Returns true if predicate condition is met for a given request. + fn check(&self, ctx: &GuardContext<'_>) -> bool; +} + +impl Guard for Rc { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + (**self).check(ctx) + } +} + +/// Creates a guard using the given function. +/// +/// # Examples /// ``` -/// use actix_web::{guard, web, App, HttpResponse}; +/// use actix_web::{guard, web, HttpResponse}; /// -/// fn main() { -/// App::new().service(web::resource("/index.html").route( -/// web::route() -/// .guard( -/// guard::fn_guard( -/// |req| req.headers() -/// .contains_key("content-type"))) -/// .to(|| HttpResponse::MethodNotAllowed())) -/// ); -/// } +/// web::route() +/// .guard(guard::fn_guard(|ctx| { +/// ctx.head().headers().contains_key("content-type") +/// })) +/// .to(|| HttpResponse::Ok()); /// ``` pub fn fn_guard(f: F) -> impl Guard where - F: Fn(&RequestHead) -> bool, + F: Fn(&GuardContext<'_>) -> bool, { FnGuard(f) } -struct FnGuard bool>(F); +struct FnGuard) -> bool>(F); impl Guard for FnGuard where - F: Fn(&RequestHead) -> bool, + F: Fn(&GuardContext<'_>) -> bool, { - fn check(&self, head: &RequestHead) -> bool { - (self.0)(head) + fn check(&self, ctx: &GuardContext<'_>) -> bool { + (self.0)(ctx) } } impl Guard for F where - F: Fn(&RequestHead) -> bool, + F: Fn(&GuardContext<'_>) -> bool, { - fn check(&self, head: &RequestHead) -> bool { - (self)(head) + fn check(&self, ctx: &GuardContext<'_>) -> bool { + (self)(ctx) } } -/// Return guard that matches if any of supplied guards. +/// Creates a guard that matches if any added guards match. /// +/// # Examples +/// The handler below will be called for either request method `GET` or `POST`. /// ``` -/// use actix_web::{web, guard, App, HttpResponse}; +/// use actix_web::{web, guard, HttpResponse}; /// -/// fn main() { -/// App::new().service(web::resource("/index.html").route( -/// web::route() -/// .guard(guard::Any(guard::Get()).or(guard::Post())) -/// .to(|| HttpResponse::MethodNotAllowed())) -/// ); -/// } +/// web::route() +/// .guard( +/// guard::Any(guard::Get()) +/// .or(guard::Post())) +/// .to(|| HttpResponse::Ok()); /// ``` +#[allow(non_snake_case)] pub fn Any(guard: F) -> AnyGuard { - AnyGuard(vec![Box::new(guard)]) + AnyGuard { + guards: vec![Box::new(guard)], + } } -/// Matches any of supplied guards. -pub struct AnyGuard(Vec>); +/// A collection of guards that match if the disjunction of their `check` outcomes is true. +/// +/// That is, only one contained guard needs to match in order for the aggregate guard to match. +/// +/// Construct an `AnyGuard` using [`Any`]. +pub struct AnyGuard { + guards: Vec>, +} impl AnyGuard { - /// Add guard to a list of guards to check + /// Adds new guard to the collection of guards to check. pub fn or(mut self, guard: F) -> Self { - self.0.push(Box::new(guard)); + self.guards.push(Box::new(guard)); self } } impl Guard for AnyGuard { - fn check(&self, req: &RequestHead) -> bool { - for p in &self.0 { - if p.check(req) { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + for guard in &self.guards { + if guard.check(ctx) { return true; } } + false } } -/// Return guard that matches if all of the supplied guards. +/// Creates a guard that matches if all added guards match. /// +/// # Examples +/// The handler below will only be called if the request method is `GET` **and** the specified +/// header name and value match exactly. /// ``` -/// use actix_web::{guard, web, App, HttpResponse}; +/// use actix_web::{guard, web, HttpResponse}; /// -/// fn main() { -/// App::new().service(web::resource("/index.html").route( -/// web::route() -/// .guard( -/// guard::All(guard::Get()).and(guard::Header("content-type", "text/plain"))) -/// .to(|| HttpResponse::MethodNotAllowed())) -/// ); -/// } +/// web::route() +/// .guard( +/// guard::All(guard::Get()) +/// .and(guard::Header("accept", "text/plain")) +/// ) +/// .to(|| HttpResponse::Ok()); /// ``` +#[allow(non_snake_case)] pub fn All(guard: F) -> AllGuard { - AllGuard(vec![Box::new(guard)]) + AllGuard { + guards: vec![Box::new(guard)], + } } -/// Matches if all of supplied guards. -pub struct AllGuard(Vec>); +/// A collection of guards that match if the conjunction of their `check` outcomes is true. +/// +/// That is, **all** contained guard needs to match in order for the aggregate guard to match. +/// +/// Construct an `AllGuard` using [`All`]. +pub struct AllGuard { + guards: Vec>, +} impl AllGuard { - /// Add new guard to the list of guards to check + /// Adds new guard to the collection of guards to check. pub fn and(mut self, guard: F) -> Self { - self.0.push(Box::new(guard)); + self.guards.push(Box::new(guard)); self } } impl Guard for AllGuard { - fn check(&self, request: &RequestHead) -> bool { - for p in &self.0 { - if !p.check(request) { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + for guard in &self.guards { + if !guard.check(ctx) { return false; } } @@ -170,161 +234,212 @@ impl Guard for AllGuard { } } -/// Return guard that matches if supplied guard does not match. -pub fn Not(guard: F) -> NotGuard { - NotGuard(Box::new(guard)) -} +/// Wraps a guard and inverts the outcome of it's `Guard` implementation. +/// +/// # Examples +/// The handler below will be called for any request method apart from `GET`. +/// ``` +/// use actix_web::{guard, web, HttpResponse}; +/// +/// web::route() +/// .guard(guard::Not(guard::Get())) +/// .to(|| HttpResponse::Ok()); +/// ``` +pub struct Not(pub G); -#[doc(hidden)] -pub struct NotGuard(Box); - -impl Guard for NotGuard { - fn check(&self, request: &RequestHead) -> bool { - !self.0.check(request) +impl Guard for Not { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + !self.0.check(ctx) } } -/// HTTP method guard. -#[doc(hidden)] -pub struct MethodGuard(HttpMethod); - -impl Guard for MethodGuard { - fn check(&self, request: &RequestHead) -> bool { - request.method == self.0 - } -} - -/// Guard to match *GET* HTTP method. -pub fn Get() -> MethodGuard { - MethodGuard(HttpMethod::GET) -} - -/// Predicate to match *POST* HTTP method. -pub fn Post() -> MethodGuard { - MethodGuard(HttpMethod::POST) -} - -/// Predicate to match *PUT* HTTP method. -pub fn Put() -> MethodGuard { - MethodGuard(HttpMethod::PUT) -} - -/// Predicate to match *DELETE* HTTP method. -pub fn Delete() -> MethodGuard { - MethodGuard(HttpMethod::DELETE) -} - -/// Predicate to match *HEAD* HTTP method. -pub fn Head() -> MethodGuard { - MethodGuard(HttpMethod::HEAD) -} - -/// Predicate to match *OPTIONS* HTTP method. -pub fn Options() -> MethodGuard { - MethodGuard(HttpMethod::OPTIONS) -} - -/// Predicate to match *CONNECT* HTTP method. -pub fn Connect() -> MethodGuard { - MethodGuard(HttpMethod::CONNECT) -} - -/// Predicate to match *PATCH* HTTP method. -pub fn Patch() -> MethodGuard { - MethodGuard(HttpMethod::PATCH) -} - -/// Predicate to match *TRACE* HTTP method. -pub fn Trace() -> MethodGuard { - MethodGuard(HttpMethod::TRACE) -} - -/// Predicate to match specified HTTP method. -pub fn Method(method: HttpMethod) -> MethodGuard { +/// Creates a guard that matches a specified HTTP method. +#[allow(non_snake_case)] +pub fn Method(method: HttpMethod) -> impl Guard { MethodGuard(method) } -/// Return predicate that matches if request contains specified header and -/// value. -pub fn Header(name: &'static str, value: &'static str) -> HeaderGuard { +/// HTTP method guard. +struct MethodGuard(HttpMethod); + +impl Guard for MethodGuard { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + ctx.head().method == self.0 + } +} + +macro_rules! method_guard { + ($method_fn:ident, $method_const:ident) => { + paste::paste! { + #[doc = " Creates a guard that matches the `" $method_const "` request method."] + /// + /// # Examples + #[doc = " The route in this example will only respond to `" $method_const "` requests."] + /// ``` + /// use actix_web::{guard, web, HttpResponse}; + /// + /// web::route() + #[doc = " .guard(guard::" $method_fn "())"] + /// .to(|| HttpResponse::Ok()); + /// ``` + #[allow(non_snake_case)] + pub fn $method_fn() -> impl Guard { + MethodGuard(HttpMethod::$method_const) + } + } + }; +} + +method_guard!(Get, GET); +method_guard!(Post, POST); +method_guard!(Put, PUT); +method_guard!(Delete, DELETE); +method_guard!(Head, HEAD); +method_guard!(Options, OPTIONS); +method_guard!(Connect, CONNECT); +method_guard!(Patch, PATCH); +method_guard!(Trace, TRACE); + +/// Creates a guard that matches if request contains given header name and value. +/// +/// # Examples +/// The handler below will be called when the request contains an `x-guarded` header with value +/// equal to `secret`. +/// ``` +/// use actix_web::{guard, web, HttpResponse}; +/// +/// web::route() +/// .guard(guard::Header("x-guarded", "secret")) +/// .to(|| HttpResponse::Ok()); +/// ``` +#[allow(non_snake_case)] +pub fn Header(name: &'static str, value: &'static str) -> impl Guard { HeaderGuard( header::HeaderName::try_from(name).unwrap(), header::HeaderValue::from_static(value), ) } -#[doc(hidden)] -pub struct HeaderGuard(header::HeaderName, header::HeaderValue); +struct HeaderGuard(header::HeaderName, header::HeaderValue); impl Guard for HeaderGuard { - fn check(&self, req: &RequestHead) -> bool { - if let Some(val) = req.headers.get(&self.0) { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + if let Some(val) = ctx.head().headers.get(&self.0) { return val == self.1; } + false } } -/// Return predicate that matches if request contains specified Host name. +/// Creates a guard that matches requests targetting a specific host. /// +/// # Matching Host +/// This guard will: +/// - match against the `Host` header, if present; +/// - fall-back to matching against the request target's host, if present; +/// - return false if host cannot be determined; +/// +/// # Matching Scheme +/// Optionally, this guard can match against the host's scheme. Set the scheme for matching using +/// `Host(host).scheme(protocol)`. If the request's scheme cannot be determined, it will not prevent +/// the guard from matching successfully. +/// +/// # Examples +/// The [module-level documentation](self) has an example of virtual hosting using `Host` guards. +/// +/// The example below additionally guards on the host URI's scheme. This could allow routing to +/// different handlers for `http:` vs `https:` visitors; to redirect, for example. /// ``` -/// use actix_web::{web, guard::Host, App, HttpResponse}; +/// use actix_web::{web, guard::Host, HttpResponse}; /// -/// fn main() { -/// App::new().service( -/// web::resource("/index.html") -/// .guard(Host("www.rust-lang.org")) -/// .to(|| HttpResponse::MethodNotAllowed()) +/// web::scope("/admin") +/// .guard(Host("admin.rust-lang.org").scheme("https")) +/// .default_service(web::to(|| HttpResponse::Ok().body("admin connection is secure"))); +/// ``` +/// +/// The `Host` guard can be used to set up some form of [virtual hosting] within a single app. +/// Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard +/// definitions they become safe to use in this way. Without these host guards, only routes under +/// the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1` +/// and `localhost` as the `Host` guards. +/// ``` +/// use actix_web::{web, http::Method, guard, App, HttpResponse}; +/// +/// App::new() +/// .service( +/// web::scope("") +/// .guard(guard::Host("www.rust-lang.org")) +/// .default_service(web::to(|| HttpResponse::Ok().body("marketing site"))), +/// ) +/// .service( +/// web::scope("") +/// .guard(guard::Host("play.rust-lang.org")) +/// .default_service(web::to(|| HttpResponse::Ok().body("playground frontend"))), /// ); -/// } /// ``` -pub fn Host>(host: H) -> HostGuard { - HostGuard(host.as_ref().to_string(), None) +/// +/// [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting +#[allow(non_snake_case)] +pub fn Host(host: impl AsRef) -> HostGuard { + HostGuard { + host: host.as_ref().to_string(), + scheme: None, + } } fn get_host_uri(req: &RequestHead) -> Option { - use core::str::FromStr; req.headers .get(header::HOST) .and_then(|host_value| host_value.to_str().ok()) .or_else(|| req.uri.host()) - .map(|host: &str| Uri::from_str(host).ok()) - .and_then(|host_success| host_success) + .and_then(|host| host.parse().ok()) } #[doc(hidden)] -pub struct HostGuard(String, Option); +pub struct HostGuard { + host: String, + scheme: Option, +} impl HostGuard { /// Set request scheme to match pub fn scheme>(mut self, scheme: H) -> HostGuard { - self.1 = Some(scheme.as_ref().to_string()); + self.scheme = Some(scheme.as_ref().to_string()); self } } impl Guard for HostGuard { - fn check(&self, req: &RequestHead) -> bool { - let req_host_uri = if let Some(uri) = get_host_uri(req) { - uri - } else { - return false; + fn check(&self, ctx: &GuardContext<'_>) -> bool { + // parse host URI from header or request target + let req_host_uri = match get_host_uri(ctx.head()) { + Some(uri) => uri, + + // no match if host cannot be determined + None => return false, }; - if let Some(uri_host) = req_host_uri.host() { - if self.0 != uri_host { - return false; - } - } else { - return false; + match req_host_uri.host() { + // fall through to scheme checks + Some(uri_host) if self.host == uri_host => {} + + // Either: + // - request's host does not match guard's host; + // - It was possible that the parsed URI from request target did not contain a host. + _ => return false, } - if let Some(ref scheme) = self.1 { + if let Some(ref scheme) = self.scheme { if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() { return scheme == req_host_uri_scheme; } + + // TODO: is the the correct behavior? + // falls through if scheme cannot be determined } + // all conditions passed true } } @@ -337,171 +452,214 @@ mod tests { use crate::test::TestRequest; #[test] - fn test_header() { + fn header_match() { let req = TestRequest::default() .insert_header((header::TRANSFER_ENCODING, "chunked")) - .to_http_request(); + .to_srv_request(); - let pred = Header("transfer-encoding", "chunked"); - assert!(pred.check(req.head())); + let hdr = Header("transfer-encoding", "chunked"); + assert!(hdr.check(&req.guard_ctx())); - let pred = Header("transfer-encoding", "other"); - assert!(!pred.check(req.head())); + let hdr = Header("transfer-encoding", "other"); + assert!(!hdr.check(&req.guard_ctx())); - let pred = Header("content-type", "other"); - assert!(!pred.check(req.head())); + let hdr = Header("content-type", "chunked"); + assert!(!hdr.check(&req.guard_ctx())); + + let hdr = Header("content-type", "other"); + assert!(!hdr.check(&req.guard_ctx())); } #[test] - fn test_host() { + fn host_from_header() { let req = TestRequest::default() .insert_header(( header::HOST, header::HeaderValue::from_static("www.rust-lang.org"), )) - .to_http_request(); + .to_srv_request(); - let pred = Host("www.rust-lang.org"); - assert!(pred.check(req.head())); + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); - let pred = Host("www.rust-lang.org").scheme("https"); - assert!(pred.check(req.head())); + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); - let pred = Host("blog.rust-lang.org"); - assert!(!pred.check(req.head())); + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("blog.rust-lang.org").scheme("https"); - assert!(!pred.check(req.head())); + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("crates.io"); - assert!(!pred.check(req.head())); + let host = Host("crates.io"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("localhost"); - assert!(!pred.check(req.head())); + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); } #[test] - fn test_host_scheme() { + fn host_without_header() { + let req = TestRequest::default() + .uri("www.rust-lang.org") + .to_srv_request(); + + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("crates.io"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); + } + + #[test] + fn host_scheme() { let req = TestRequest::default() .insert_header(( header::HOST, header::HeaderValue::from_static("https://www.rust-lang.org"), )) - .to_http_request(); + .to_srv_request(); - let pred = Host("www.rust-lang.org").scheme("https"); - assert!(pred.check(req.head())); + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); - let pred = Host("www.rust-lang.org"); - assert!(pred.check(req.head())); + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); - let pred = Host("www.rust-lang.org").scheme("http"); - assert!(!pred.check(req.head())); + let host = Host("www.rust-lang.org").scheme("http"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("blog.rust-lang.org"); - assert!(!pred.check(req.head())); + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("blog.rust-lang.org").scheme("https"); - assert!(!pred.check(req.head())); + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("crates.io").scheme("https"); - assert!(!pred.check(req.head())); + let host = Host("crates.io").scheme("https"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("localhost"); - assert!(!pred.check(req.head())); + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); } #[test] - fn test_host_without_header() { + fn method_guards() { + let get_req = TestRequest::get().to_srv_request(); + let post_req = TestRequest::post().to_srv_request(); + + assert!(Get().check(&get_req.guard_ctx())); + assert!(!Get().check(&post_req.guard_ctx())); + + assert!(Post().check(&post_req.guard_ctx())); + assert!(!Post().check(&get_req.guard_ctx())); + + let req = TestRequest::put().to_srv_request(); + assert!(Put().check(&req.guard_ctx())); + assert!(!Put().check(&get_req.guard_ctx())); + + let req = TestRequest::patch().to_srv_request(); + assert!(Patch().check(&req.guard_ctx())); + assert!(!Patch().check(&get_req.guard_ctx())); + + let r = TestRequest::delete().to_srv_request(); + assert!(Delete().check(&r.guard_ctx())); + assert!(!Delete().check(&get_req.guard_ctx())); + + let req = TestRequest::default().method(Method::HEAD).to_srv_request(); + assert!(Head().check(&req.guard_ctx())); + assert!(!Head().check(&get_req.guard_ctx())); + let req = TestRequest::default() - .uri("www.rust-lang.org") - .to_http_request(); - - let pred = Host("www.rust-lang.org"); - assert!(pred.check(req.head())); - - let pred = Host("www.rust-lang.org").scheme("https"); - assert!(pred.check(req.head())); - - let pred = Host("blog.rust-lang.org"); - assert!(!pred.check(req.head())); - - let pred = Host("blog.rust-lang.org").scheme("https"); - assert!(!pred.check(req.head())); - - let pred = Host("crates.io"); - assert!(!pred.check(req.head())); - - let pred = Host("localhost"); - assert!(!pred.check(req.head())); - } - - #[test] - fn test_methods() { - let req = TestRequest::default().to_http_request(); - let req2 = TestRequest::default() - .method(Method::POST) - .to_http_request(); - - assert!(Get().check(req.head())); - assert!(!Get().check(req2.head())); - assert!(Post().check(req2.head())); - assert!(!Post().check(req.head())); - - let r = TestRequest::default().method(Method::PUT).to_http_request(); - assert!(Put().check(r.head())); - assert!(!Put().check(req.head())); - - let r = TestRequest::default() - .method(Method::DELETE) - .to_http_request(); - assert!(Delete().check(r.head())); - assert!(!Delete().check(req.head())); - - let r = TestRequest::default() - .method(Method::HEAD) - .to_http_request(); - assert!(Head().check(r.head())); - assert!(!Head().check(req.head())); - - let r = TestRequest::default() .method(Method::OPTIONS) - .to_http_request(); - assert!(Options().check(r.head())); - assert!(!Options().check(req.head())); + .to_srv_request(); + assert!(Options().check(&req.guard_ctx())); + assert!(!Options().check(&get_req.guard_ctx())); - let r = TestRequest::default() + let req = TestRequest::default() .method(Method::CONNECT) - .to_http_request(); - assert!(Connect().check(r.head())); - assert!(!Connect().check(req.head())); + .to_srv_request(); + assert!(Connect().check(&req.guard_ctx())); + assert!(!Connect().check(&get_req.guard_ctx())); - let r = TestRequest::default() - .method(Method::PATCH) - .to_http_request(); - assert!(Patch().check(r.head())); - assert!(!Patch().check(req.head())); - - let r = TestRequest::default() + let req = TestRequest::default() .method(Method::TRACE) - .to_http_request(); - assert!(Trace().check(r.head())); - assert!(!Trace().check(req.head())); + .to_srv_request(); + assert!(Trace().check(&req.guard_ctx())); + assert!(!Trace().check(&get_req.guard_ctx())); } #[test] - fn test_preds() { - let r = TestRequest::default() + fn aggregate_any() { + let req = TestRequest::default() .method(Method::TRACE) - .to_http_request(); + .to_srv_request(); - assert!(Not(Get()).check(r.head())); - assert!(!Not(Trace()).check(r.head())); + assert!(Any(Trace()).check(&req.guard_ctx())); + assert!(Any(Trace()).or(Get()).check(&req.guard_ctx())); + assert!(!Any(Get()).or(Get()).check(&req.guard_ctx())); + } - assert!(All(Trace()).and(Trace()).check(r.head())); - assert!(!All(Get()).and(Trace()).check(r.head())); + #[test] + fn aggregate_all() { + let req = TestRequest::default() + .method(Method::TRACE) + .to_srv_request(); - assert!(Any(Get()).or(Trace()).check(r.head())); - assert!(!Any(Get()).or(Get()).check(r.head())); + assert!(All(Trace()).check(&req.guard_ctx())); + assert!(All(Trace()).and(Trace()).check(&req.guard_ctx())); + assert!(!All(Trace()).and(Get()).check(&req.guard_ctx())); + } + + #[test] + fn nested_not() { + let req = TestRequest::default().to_srv_request(); + + let get = Get(); + assert!(get.check(&req.guard_ctx())); + + let not_get = Not(get); + assert!(!not_get.check(&req.guard_ctx())); + + let not_not_get = Not(not_get); + assert!(not_not_get.check(&req.guard_ctx())); + } + + #[test] + fn function_guard() { + let domain = "rust-lang.org".to_owned(); + let guard = fn_guard(|ctx| ctx.head().uri.host().unwrap().ends_with(&domain)); + + let req = TestRequest::default() + .uri("blog.rust-lang.org") + .to_srv_request(); + assert!(guard.check(&req.guard_ctx())); + + let req = TestRequest::default().uri("crates.io").to_srv_request(); + assert!(!guard.check(&req.guard_ctx())); + } + + #[test] + fn mega_nesting() { + let guard = fn_guard(|ctx| All(Not(Any(Not(Trace())))).check(ctx)); + + let req = TestRequest::default().to_srv_request(); + assert!(!guard.check(&req.guard_ctx())); + + let req = TestRequest::default() + .method(Method::TRACE) + .to_srv_request(); + assert!(guard.check(&req.guard_ctx())); } } diff --git a/src/handler.rs b/src/handler.rs index e543ecc7f..d458e22e1 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -3,35 +3,92 @@ use std::future::Future; use actix_service::{boxed, fn_service}; use crate::{ - body::MessageBody, service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse}, - BoxError, FromRequest, HttpResponse, Responder, + FromRequest, HttpResponse, Responder, }; -/// A request handler is an async function that accepts zero or more parameters that can be -/// extracted from a request (i.e., [`impl FromRequest`]) and returns a type that can be converted -/// into an [`HttpResponse`] (that is, it impls the [`Responder`] trait). +/// The interface for request handlers. /// -/// If you got the error `the trait Handler<_, _, _> is not implemented`, then your function is not -/// a valid handler. See for more information. +/// # What Is A Request Handler +/// A request handler has three requirements: +/// 1. It is an async function (or a function/closure that returns an appropriate future); +/// 1. The function accepts zero or more parameters that implement [`FromRequest`]; +/// 1. The async function (or future) resolves to a type that can be converted into an +/// [`HttpResponse`] (i.e., it implements the [`Responder`] trait). /// -/// [`impl FromRequest`]: crate::FromRequest -pub trait Handler: Clone + 'static -where - R: Future, - R::Output: Responder, -{ - fn call(&self, param: T) -> R; +/// # Compiler Errors +/// If you get the error `the trait Handler<_> is not implemented`, then your handler does not +/// fulfill one or more of the above requirements. +/// +/// Unfortunately we cannot provide a better compile error message (while keeping the trait's +/// flexibility) unless a stable alternative to [`#[rustc_on_unimplemented]`][on_unimpl] is added +/// to Rust. +/// +/// # How Do Handlers Receive Variable Numbers Of Arguments +/// Rest assured there is no macro magic here; it's just traits. +/// +/// The first thing to note is that [`FromRequest`] is implemented for tuples (up to 12 in length). +/// +/// Secondly, the `Handler` trait is implemented for functions (up to an [arity] of 12) in a way +/// that aligns their parameter positions with a corresponding tuple of types (becoming the `Args` +/// type parameter for this trait). +/// +/// Thanks to Rust's type system, Actix Web can infer the function parameter types. During the +/// extraction step, the parameter types are described as a tuple type, [`from_request`] is run on +/// that tuple, and the `Handler::call` implementation for that particular function arity +/// destructures the tuple into it's component types and calls your handler function with them. +/// +/// In pseudo-code the process looks something like this: +/// ```ignore +/// async fn my_handler(body: String, state: web::Data) -> impl Responder { +/// ... +/// } +/// +/// // the function params above described as a tuple, names do not matter, only position +/// type InferredMyHandlerArgs = (String, web::Data); +/// +/// // create tuple of arguments to be passed to handler +/// let args = InferredMyHandlerArgs::from_request(&request, &payload).await; +/// +/// // call handler with argument tuple +/// let response = Handler::call(&my_handler, args).await; +/// +/// // which is effectively... +/// +/// let (body, state) = args; +/// let response = my_handler(body, state).await; +/// ``` +/// +/// This is the source code for the 2-parameter implementation of `Handler` to help illustrate the +/// bounds of the handler call after argument extraction: +/// ```ignore +/// impl Handler<(Arg1, Arg2), R> for Func +/// where +/// Func: Fn(Arg1, Arg2) -> R + Clone + 'static, +/// R: Future, +/// R::Output: Responder, +/// { +/// fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> R { +/// (self)(arg1, arg2) +/// } +/// } +/// ``` +/// +/// [arity]: https://en.wikipedia.org/wiki/Arity +/// [`from_request`]: FromRequest::from_request +/// [on_unimpl]: https://github.com/rust-lang/rust/issues/29628 +pub trait Handler: Clone + 'static { + type Output; + type Future: Future; + + fn call(&self, args: Args) -> Self::Future; } -pub(crate) fn handler_service(handler: F) -> BoxedHttpServiceFactory +pub(crate) fn handler_service(handler: F) -> BoxedHttpServiceFactory where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, - ::Body: MessageBody, - <::Body as MessageBody>::Error: Into, + F: Handler, + Args: FromRequest, + F::Output: Responder, { boxed::factory(fn_service(move |req: ServiceRequest| { let handler = handler.clone(); @@ -39,7 +96,7 @@ where async move { let (req, mut payload) = req.into_parts(); - let res = match T::from_request(&req, &mut payload).await { + let res = match Args::from_request(&req, &mut payload).await { Err(err) => HttpResponse::from_error(err), Ok(data) => handler @@ -59,17 +116,20 @@ where /// /// # Examples /// ```ignore -/// factory_tuple! {} // implements Handler for types: fn() -> Res -/// factory_tuple! { A B C } // implements Handler for types: fn(A, B, C) -> Res +/// factory_tuple! {} // implements Handler for types: fn() -> R +/// factory_tuple! { A B C } // implements Handler for types: fn(A, B, C) -> R /// ``` macro_rules! factory_tuple ({ $($param:ident)* } => { - impl Handler<($($param,)*), Res> for Func - where Func: Fn($($param),*) -> Res + Clone + 'static, - Res: Future, - Res::Output: Responder, + impl Handler<($($param,)*)> for Func + where Func: Fn($($param),*) -> Fut + Clone + 'static, + Fut: Future, { + type Output = Fut::Output; + type Future = Fut; + + #[inline] #[allow(non_snake_case)] - fn call(&self, ($($param,)*): ($($param,)*)) -> Res { + fn call(&self, ($($param,)*): ($($param,)*)) -> Self::Future { (self)($($param,)*) } } diff --git a/src/http/mod.rs b/src/http/mod.rs index bbd94a60f..2581532cd 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -2,4 +2,5 @@ pub mod header; +// TODO: figure out how best to expose http::Error vs actix_http::Error pub use actix_http::{uri, ConnectionType, Error, Method, StatusCode, Uri, Version}; diff --git a/src/info.rs b/src/info.rs index 71194b24d..ce1ef97c6 100644 --- a/src/info.rs +++ b/src/info.rs @@ -67,7 +67,7 @@ fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option< pub struct ConnectionInfo { host: String, scheme: String, - remote_addr: Option, + peer_addr: Option, realip_remote_addr: Option, } @@ -134,67 +134,70 @@ impl ConnectionInfo { .or_else(|| first_header_value(req, &*X_FORWARDED_FOR)) .map(str::to_owned); - let remote_addr = req.peer_addr.map(|addr| addr.to_string()); + let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string()); ConnectionInfo { host, scheme, - remote_addr, + peer_addr, realip_remote_addr, } } + /// Real IP (remote address) of client that initiated request. + /// + /// The address is resolved through the following, in order: + /// - `Forwarded` header + /// - `X-Forwarded-For` header + /// - peer address of opened socket (same as [`remote_addr`](Self::remote_addr)) + /// + /// # Security + /// Do not use this function for security purposes unless you can be sure that the `Forwarded` + /// and `X-Forwarded-For` headers cannot be spoofed by the client. If you are running without a + /// proxy then [obtaining the peer address](Self::peer_addr) would be more appropriate. + #[inline] + pub fn realip_remote_addr(&self) -> Option<&str> { + self.realip_remote_addr + .as_deref() + .or_else(|| self.peer_addr.as_deref()) + } + + /// Returns serialized IP address of the peer connection. + /// + /// See [`HttpRequest::peer_addr`] for more details. + #[inline] + pub fn peer_addr(&self) -> Option<&str> { + self.peer_addr.as_deref() + } + + /// Hostname of the request. + /// + /// Hostname is resolved through the following, in order: + /// - `Forwarded` header + /// - `X-Forwarded-Host` header + /// - `Host` header + /// - request target / URI + /// - configured server hostname + #[inline] + pub fn host(&self) -> &str { + &self.host + } + /// Scheme of the request. /// - /// Scheme is resolved through the following headers, in this order: - /// - /// - Forwarded - /// - X-Forwarded-Proto - /// - Uri + /// Scheme is resolved through the following, in order: + /// - `Forwarded` header + /// - `X-Forwarded-Proto` header + /// - request target / URI #[inline] pub fn scheme(&self) -> &str { &self.scheme } - /// Hostname of the request. - /// - /// Hostname is resolved through the following headers, in this order: - /// - /// - Forwarded - /// - X-Forwarded-Host - /// - Host - /// - Uri - /// - Server hostname - pub fn host(&self) -> &str { - &self.host - } - - /// Remote address of the connection. - /// - /// Get remote_addr address from socket address. + #[doc(hidden)] + #[deprecated(since = "4.0.0", note = "Renamed to `peer_addr`.")] pub fn remote_addr(&self) -> Option<&str> { - self.remote_addr.as_deref() - } - - /// Real IP (remote address) of client that initiated request. - /// - /// The address is resolved through the following headers, in this order: - /// - /// - Forwarded - /// - X-Forwarded-For - /// - remote_addr name of opened socket - /// - /// # 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()`][peer_addr] instead. - /// - /// [peer_addr]: crate::web::HttpRequest::peer_addr() - #[inline] - pub fn realip_remote_addr(&self) -> Option<&str> { - self.realip_remote_addr - .as_deref() - .or_else(|| self.remote_addr.as_deref()) + self.peer_addr() } } @@ -209,7 +212,7 @@ impl FromRequest for ConnectionInfo { /// Extractor for peer's socket address. /// -/// Also see [`HttpRequest::peer_addr`]. +/// Also see [`HttpRequest::peer_addr`] and [`ConnectionInfo::peer_addr`]. /// /// # Examples /// ``` @@ -432,13 +435,37 @@ mod tests { #[actix_rt::test] async fn peer_addr_extract() { + let req = TestRequest::default().to_http_request(); + let res = PeerAddr::extract(&req).await; + assert!(res.is_err()); + 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)); + } + #[actix_rt::test] + async fn remote_address() { let req = TestRequest::default().to_http_request(); - let res = PeerAddr::extract(&req).await; - assert!(res.is_err()); + let res = ConnectionInfo::extract(&req).await.unwrap(); + assert!(res.peer_addr().is_none()); + + let addr = "127.0.0.1:8080".parse().unwrap(); + let req = TestRequest::default().peer_addr(addr).to_http_request(); + let conn_info = ConnectionInfo::extract(&req).await.unwrap(); + assert_eq!(conn_info.peer_addr().unwrap(), "127.0.0.1"); + } + + #[actix_rt::test] + async fn real_ip_from_socket_addr() { + let req = TestRequest::default().to_http_request(); + let res = ConnectionInfo::extract(&req).await.unwrap(); + assert!(res.realip_remote_addr().is_none()); + + let addr = "127.0.0.1:8080".parse().unwrap(); + let req = TestRequest::default().peer_addr(addr).to_http_request(); + let conn_info = ConnectionInfo::extract(&req).await.unwrap(); + assert_eq!(conn_info.realip_remote_addr().unwrap(), "127.0.0.1"); } } diff --git a/src/lib.rs b/src/lib.rs index 171a2d101..5f5b915b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,7 +66,6 @@ #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible)] -#![allow(clippy::needless_doctest_main, clippy::type_complexity)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] @@ -106,6 +105,7 @@ pub use cookie; pub use crate::app::App; pub use crate::error::{Error, ResponseError, Result}; pub use crate::extract::FromRequest; +pub use crate::handler::Handler; pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::response::{CustomizeResponder, HttpResponse, HttpResponseBuilder, Responder}; diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs index d49c461c4..18c9ff6a7 100644 --- a/src/middleware/compat.rs +++ b/src/middleware/compat.rs @@ -17,7 +17,7 @@ use crate::{ }; /// Middleware for enabling any middleware to be used in [`Resource::wrap`](crate::Resource::wrap), -/// [`Scope::wrap`](crate::Scope::wrap) and [`Condition`](super::Condition). +/// and [`Condition`](super::Condition). /// /// # Examples /// ``` @@ -38,6 +38,15 @@ pub struct Compat { transform: T, } +#[cfg(test)] +impl Compat { + pub(crate) fn noop() -> Self { + Self { + transform: super::Noop, + } + } +} + impl Compat { /// Wrap a middleware to give it broader compatibility. pub fn new(middleware: T) -> Self { diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index af4a107e3..d3cdf5763 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -113,6 +113,7 @@ where { type Response = ServiceResponse>>; type Error = Error; + #[allow(clippy::type_complexity)] type Future = Either, Ready>>; actix_service::forward_ready!(service); diff --git a/src/middleware/default_headers.rs b/src/middleware/default_headers.rs index 89210b156..003abd40d 100644 --- a/src/middleware/default_headers.rs +++ b/src/middleware/default_headers.rs @@ -100,10 +100,9 @@ impl DefaultHeaders { /// /// Default is `application/octet-stream`. pub fn add_content_type(self) -> Self { - self.add(( - CONTENT_TYPE, - HeaderValue::from_static("application/octet-stream"), - )) + #[allow(clippy::declare_interior_mutable_const)] + const HV_MIME: HeaderValue = HeaderValue::from_static("application/octet-stream"); + self.add((CONTENT_TYPE, HV_MIME)) } } diff --git a/src/middleware/err_handlers.rs b/src/middleware/err_handlers.rs index 6d064372f..bde054330 100644 --- a/src/middleware/err_handlers.rs +++ b/src/middleware/err_handlers.rs @@ -37,27 +37,21 @@ type ErrorHandler = dyn Fn(ServiceResponse) -> Result(mut res: dev::ServiceResponse) -> Result> { -/// res.response_mut() -/// .headers_mut() -/// .insert(header::CONTENT_TYPE, header::HeaderValue::from_static("Error")); +/// use actix_web::http::{header, StatusCode}; +/// use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers}; +/// use actix_web::{dev, web, App, HttpResponse, Result}; /// +/// fn add_error_header(mut res: dev::ServiceResponse) -> Result> { +/// res.response_mut().headers_mut().insert( +/// header::CONTENT_TYPE, +/// header::HeaderValue::from_static("Error"), +/// ); /// Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) /// } /// /// let app = App::new() -/// .wrap( -/// ErrorHandlers::new() -/// .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), -/// ) -/// .service(web::resource("/test") -/// .route(web::get().to(|| HttpResponse::Ok())) -/// .route(web::head().to(|| HttpResponse::MethodNotAllowed()) -/// )); +/// .wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_header)) +/// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError))); /// ``` pub struct ErrorHandlers { handlers: Handlers, diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index d7fdb234f..969cb0c10 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -547,7 +547,7 @@ impl FormatText { *self = FormatText::Str(s.to_string()); } FormatText::RemoteAddr => { - let s = if let Some(peer) = req.connection_info().remote_addr() { + let s = if let Some(peer) = req.connection_info().peer_addr() { FormatText::Str((*peer).to_string()) } else { FormatText::Str("-".to_string()) diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 0da9b9b2e..a781052a6 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -5,6 +5,8 @@ mod condition; mod default_headers; mod err_handlers; mod logger; +#[cfg(test)] +mod noop; mod normalize; pub use self::compat::Compat; @@ -12,6 +14,8 @@ pub use self::condition::Condition; pub use self::default_headers::DefaultHeaders; pub use self::err_handlers::{ErrorHandlerResponse, ErrorHandlers}; pub use self::logger::Logger; +#[cfg(test)] +pub(crate) use self::noop::Noop; pub use self::normalize::{NormalizePath, TrailingSlash}; #[cfg(feature = "__compress")] diff --git a/src/middleware/noop.rs b/src/middleware/noop.rs new file mode 100644 index 000000000..ae7da1d81 --- /dev/null +++ b/src/middleware/noop.rs @@ -0,0 +1,37 @@ +//! A no-op middleware. See [Noop] for docs. + +use actix_utils::future::{ready, Ready}; + +use crate::dev::{Service, Transform}; + +/// A no-op middleware that passes through request and response untouched. +pub(crate) struct Noop; + +impl, Req> Transform for Noop { + type Response = S::Response; + type Error = S::Error; + type Transform = NoopService; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(NoopService { service })) + } +} + +#[doc(hidden)] +pub(crate) struct NoopService { + service: S, +} + +impl, Req> Service for NoopService { + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + crate::dev::forward_ready!(service); + + fn call(&self, req: Req) -> Self::Future { + self.service.call(req) + } +} diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index 18dcaeefa..3ab908481 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -225,7 +225,7 @@ mod tests { .service(web::resource("/v1/something").to(HttpResponse::Ok)) .service( web::resource("/v2/something") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) @@ -261,7 +261,7 @@ mod tests { .service(web::resource("/v1/something").to(HttpResponse::Ok)) .service( web::resource("/v2/something") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) @@ -294,7 +294,7 @@ mod tests { let app = init_service( App::new().wrap(NormalizePath(TrailingSlash::Trim)).service( web::resource("/") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) @@ -318,7 +318,7 @@ mod tests { .service(web::resource("/v1/something/").to(HttpResponse::Ok)) .service( web::resource("/v2/something/") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) @@ -353,7 +353,7 @@ mod tests { .wrap(NormalizePath(TrailingSlash::Always)) .service( web::resource("/") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) @@ -378,7 +378,7 @@ mod tests { .service(web::resource("/v1/").to(HttpResponse::Ok)) .service( web::resource("/v2/something") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) diff --git a/src/request.rs b/src/request.rs index 07fb4eb2d..b59369317 100644 --- a/src/request.rs +++ b/src/request.rs @@ -228,23 +228,28 @@ impl HttpRequest { self.app_state().rmap() } - /// Peer socket address. + /// Returns peer socket address. /// /// Peer address is the directly connected peer's socket address. If a proxy is used in front of /// the Actix Web server, then it would be address of this proxy. /// - /// To get client connection information `.connection_info()` should be used. + /// For expanded client connection information, use [`connection_info`] instead. /// - /// Will only return None when called in unit tests. + /// Will only return None when called in unit tests unless [`TestRequest::peer_addr`] is used. + /// + /// [`TestRequest::peer_addr`]: crate::test::TestRequest::peer_addr + /// [`connection_info`]: Self::connection_info #[inline] pub fn peer_addr(&self) -> Option { self.head().peer_addr } - /// Get *ConnectionInfo* for the current request. + /// Returns connection info for the current request. /// - /// This method panics if request's extensions container is already - /// borrowed. + /// The return type, [`ConnectionInfo`], can also be used as an extractor. + /// + /// # Panics + /// Panics if request's extensions container is already borrowed. #[inline] pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> { if !self.extensions().contains::() { @@ -252,7 +257,7 @@ impl HttpRequest { self.extensions_mut().insert(info); } - Ref::map(self.extensions(), |e| e.get().unwrap()) + Ref::map(self.extensions(), |data| data.get().unwrap()) } /// App config diff --git a/src/resource.rs b/src/resource.rs index c13544063..564c4d3ef 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,6 +1,6 @@ -use std::{cell::RefCell, fmt, future::Future, rc::Rc}; +use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, rc::Rc}; -use actix_http::Extensions; +use actix_http::{body::BoxBody, Extensions}; use actix_router::{IntoPatterns, Patterns}; use actix_service::{ apply, apply_fn_factory, boxed, fn_service, IntoServiceFactory, Service, ServiceFactory, @@ -20,32 +20,29 @@ use crate::{ BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest, ServiceResponse, }, - BoxError, Error, FromRequest, HttpResponse, Responder, + Error, FromRequest, HttpResponse, Responder, }; -/// *Resource* is an entry in resources table which corresponds to requested URL. +/// A collection of [`Route`]s that respond to the same path pattern. /// -/// Resource in turn has at least one route. -/// Route consists of an handlers objects and list of guards -/// (objects that implement `Guard` trait). -/// Resources and routes uses builder-like pattern for configuration. -/// During request handling, resource object iterate through all routes -/// and check guards for specific route, if request matches all -/// guards, route considered matched and route handler get called. +/// Resource in turn has at least one route. Route consists of an handlers objects and list of +/// guards (objects that implement `Guard` trait). Resources and routes uses builder-like pattern +/// for configuration. During request handling, resource object iterate through all routes and check +/// guards for specific route, if request matches all guards, route considered matched and route +/// handler get called. /// +/// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/") -/// .route(web::get().to(|| HttpResponse::Ok()))); -/// } +/// let app = App::new().service( +/// web::resource("/") +/// .route(web::get().to(|| HttpResponse::Ok()))); /// ``` /// -/// If no matching route could be found, *405* response code get returned. -/// Default behavior could be overridden with `default_resource()` method. -pub struct Resource { +/// If no matching route could be found, *405* response code get returned. Default behavior could be +/// overridden with `default_resource()` method. +pub struct Resource { endpoint: T, rdef: Patterns, name: Option, @@ -54,6 +51,7 @@ pub struct Resource { guards: Vec>, default: BoxedHttpServiceFactory, factory_ref: Rc>>, + _phantom: PhantomData, } impl Resource { @@ -71,19 +69,21 @@ impl Resource { default: boxed::factory(fn_service(|req: ServiceRequest| async { Ok(req.into_response(HttpResponse::MethodNotAllowed())) })), + _phantom: PhantomData, } } } -impl Resource +impl Resource where T: ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, + B: MessageBody, { /// Set resource name. /// @@ -131,15 +131,13 @@ where /// ``` /// use actix_web::{web, guard, App, HttpResponse}; /// - /// fn main() { - /// let app = App::new().service( - /// web::resource("/").route( - /// web::route() - /// .guard(guard::Any(guard::Get()).or(guard::Put())) - /// .guard(guard::Header("Content-Type", "text/plain")) - /// .to(|| HttpResponse::Ok())) - /// ); - /// } + /// let app = App::new().service( + /// web::resource("/").route( + /// web::route() + /// .guard(guard::Any(guard::Get()).or(guard::Put())) + /// .guard(guard::Header("Content-Type", "text/plain")) + /// .to(|| HttpResponse::Ok())) + /// ); /// ``` /// /// Multiple routes could be added to a resource. Resource object uses @@ -232,14 +230,11 @@ where /// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } /// App::new().service(web::resource("/").route(web::route().to(index))); /// ``` - pub fn to(mut self, handler: F) -> Self + pub fn to(mut self, handler: F) -> Self where - F: Handler, - I: FromRequest + 'static, - R: Future + 'static, - R::Output: Responder + 'static, - ::Body: MessageBody, - <::Body as MessageBody>::Error: Into, + F: Handler, + Args: FromRequest + 'static, + F::Output: Responder + 'static, { self.routes.push(Route::new().to(handler)); self @@ -252,26 +247,28 @@ where /// type (i.e modify response's body). /// /// **Note**: middlewares get called in opposite order of middlewares registration. - pub fn wrap( + pub fn wrap( self, mw: M, ) -> Resource< impl ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, + B1, > where M: Transform< T::Service, ServiceRequest, - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, + B1: MessageBody, { Resource { endpoint: apply(mw, self.endpoint), @@ -282,6 +279,7 @@ where default: self.default, app_data: self.app_data, factory_ref: self.factory_ref, + _phantom: PhantomData, } } @@ -319,21 +317,23 @@ where /// .route(web::get().to(index))); /// } /// ``` - pub fn wrap_fn( + pub fn wrap_fn( self, mw: F, ) -> Resource< impl ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, + B1, > where F: Fn(ServiceRequest, &T::Service) -> R + Clone, - R: Future>, + R: Future, Error>>, + B1: MessageBody, { Resource { endpoint: apply_fn_factory(self.endpoint, mw), @@ -344,6 +344,7 @@ where default: self.default, app_data: self.app_data, factory_ref: self.factory_ref, + _phantom: PhantomData, } } @@ -371,15 +372,16 @@ where } } -impl HttpServiceFactory for Resource +impl HttpServiceFactory for Resource where T: ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), > + 'static, + B: MessageBody + 'static, { fn register(mut self, config: &mut AppService) { let guards = if self.guards.is_empty() { @@ -411,7 +413,9 @@ where req.add_data_container(Rc::clone(data)); } - srv.call(req) + let fut = srv.call(req); + + async { Ok(fut.await?.map_into_boxed_body()) } }); config.register_service(rdef, guards, endpoint, None) @@ -534,11 +538,11 @@ mod tests { >, > { web::resource("/test-compat") - // .wrap_fn(|req, srv| { - // let fut = srv.call(req); - // async { Ok(fut.await?.map_into_right_body::<()>()) } - // }) - .wrap(Compat::new(DefaultHeaders::new())) + .wrap_fn(|req, srv| { + let fut = srv.call(req); + async { Ok(fut.await?.map_into_right_body::<()>()) } + }) + .wrap(Compat::noop()) .route(web::get().to(|| async { "hello" })) } @@ -801,4 +805,26 @@ mod tests { let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } + + #[actix_rt::test] + async fn test_middleware_body_type() { + let srv = init_service( + App::new().service( + web::resource("/test") + .wrap_fn(|req, srv| { + let fut = srv.call(req); + async { Ok(fut.await?.map_into_right_body::<()>()) } + }) + .route(web::get().to(|| async { "hello" })), + ), + ) + .await; + + // test if `try_into_bytes()` is preserved across scope layer + use actix_http::body::MessageBody as _; + let req = TestRequest::with_uri("/test").to_request(); + let resp = call_service(&srv, req).await; + let body = resp.into_body(); + assert_eq!(body.try_into_bytes().unwrap(), b"hello".as_ref()); + } } diff --git a/src/response/builder.rs b/src/response/builder.rs index b500ab331..93d8ab567 100644 --- a/src/response/builder.rs +++ b/src/response/builder.rs @@ -429,9 +429,12 @@ mod tests { use actix_http::body; use super::*; - use crate::http::{ - header::{self, HeaderValue, CONTENT_TYPE}, - StatusCode, + use crate::{ + http::{ + header::{self, HeaderValue, CONTENT_TYPE}, + StatusCode, + }, + test::assert_body_eq, }; #[test] @@ -472,32 +475,23 @@ mod tests { #[actix_rt::test] async fn test_json() { - let resp = HttpResponse::Ok().json(vec!["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); + let res = HttpResponse::Ok().json(vec!["v1", "v2", "v3"]); + let ct = res.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("application/json")); - assert_eq!( - body::to_bytes(resp.into_body()).await.unwrap().as_ref(), - br#"["v1","v2","v3"]"# - ); + assert_body_eq!(res, br#"["v1","v2","v3"]"#); - let resp = HttpResponse::Ok().json(&["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); + let res = HttpResponse::Ok().json(&["v1", "v2", "v3"]); + let ct = res.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("application/json")); - assert_eq!( - body::to_bytes(resp.into_body()).await.unwrap().as_ref(), - br#"["v1","v2","v3"]"# - ); + assert_body_eq!(res, br#"["v1","v2","v3"]"#); // content type override - let resp = HttpResponse::Ok() + let res = HttpResponse::Ok() .insert_header((CONTENT_TYPE, "text/json")) .json(&vec!["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); + let ct = res.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("text/json")); - assert_eq!( - body::to_bytes(resp.into_body()).await.unwrap().as_ref(), - br#"["v1","v2","v3"]"# - ); + assert_body_eq!(res, br#"["v1","v2","v3"]"#); } #[actix_rt::test] diff --git a/src/route.rs b/src/route.rs index 4447bff50..0410b99dd 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,4 +1,4 @@ -use std::{future::Future, mem, rc::Rc}; +use std::{mem, rc::Rc}; use actix_http::Method; use actix_service::{ @@ -8,17 +8,16 @@ use actix_service::{ use futures_core::future::LocalBoxFuture; use crate::{ - body::MessageBody, guard::{self, Guard}, handler::{handler_service, Handler}, service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse}, - BoxError, Error, FromRequest, HttpResponse, Responder, + Error, FromRequest, HttpResponse, Responder, }; -/// Resource route definition +/// A request handler with [guards](guard). /// -/// Route uses builder-like pattern for configuration. -/// If handler is not explicitly set, default *404 Not Found* handler is used. +/// Route uses a builder-like pattern for configuration. If handler is not set, a `404 Not Found` +/// handler is used. pub struct Route { service: BoxedHttpServiceFactory, guards: Rc>>, @@ -66,9 +65,12 @@ pub struct RouteService { } impl RouteService { + // TODO: does this need to take &mut ? pub fn check(&self, req: &mut ServiceRequest) -> bool { - for f in self.guards.iter() { - if !f.check(req.head()) { + let guard_ctx = req.guard_ctx(); + + for guard in self.guards.iter() { + if !guard.check(&guard_ctx) { return false; } } @@ -91,6 +93,7 @@ impl Service for RouteService { impl Route { /// Add method guard to the route. /// + /// # Examples /// ``` /// # use actix_web::*; /// # fn main() { @@ -111,6 +114,7 @@ impl Route { /// Add guard to the route. /// + /// # Examples /// ``` /// # use actix_web::*; /// # fn main() { @@ -144,16 +148,13 @@ impl Route { /// format!("Welcome {}!", info.username) /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::resource("/{username}/index.html") // <- define path parameters - /// .route(web::get().to(index)) // <- register handler - /// ); - /// } + /// let app = App::new().service( + /// web::resource("/{username}/index.html") // <- define path parameters + /// .route(web::get().to(index)) // <- register handler + /// ); /// ``` /// /// It is possible to use multiple extractors for one handler function. - /// /// ``` /// # use std::collections::HashMap; /// # use serde::Deserialize; @@ -165,25 +166,24 @@ impl Route { /// } /// /// /// extract path info using serde - /// async fn index(path: web::Path, query: web::Query>, body: web::Json) -> String { + /// async fn index( + /// path: web::Path, + /// query: web::Query>, + /// body: web::Json + /// ) -> String { /// format!("Welcome {}!", path.username) /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::resource("/{username}/index.html") // <- define path parameters - /// .route(web::get().to(index)) - /// ); - /// } + /// let app = App::new().service( + /// web::resource("/{username}/index.html") // <- define path parameters + /// .route(web::get().to(index)) + /// ); /// ``` - pub fn to(mut self, handler: F) -> Self + pub fn to(mut self, handler: F) -> Self where - F: Handler, - T: FromRequest + 'static, - R: Future + 'static, - R::Output: Responder + 'static, - ::Body: MessageBody, - <::Body as MessageBody>::Error: Into, + F: Handler, + Args: FromRequest + 'static, + F::Output: Responder + 'static, { self.service = handler_service(handler); self @@ -203,7 +203,7 @@ impl Route { /// type Error = Infallible; /// type Future = LocalBoxFuture<'static, Result>; /// - /// always_ready!(); + /// dev::always_ready!(); /// /// fn call(&self, req: ServiceRequest) -> Self::Future { /// let (req, _) = req.into_parts(); diff --git a/src/scope.rs b/src/scope.rs index 35bbb50ba..b4618bb6c 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,6 +1,9 @@ use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, mem, rc::Rc}; -use actix_http::{body::BoxBody, Extensions}; +use actix_http::{ + body::{BoxBody, MessageBody}, + Extensions, +}; use actix_router::{ResourceDef, Router}; use actix_service::{ apply, apply_fn_factory, boxed, IntoServiceFactory, Service, ServiceFactory, @@ -24,34 +27,36 @@ use crate::{ type Guards = Vec>; -/// Resources scope. +/// A collection of [`Route`]s, [`Resource`]s, or other services that share a common path prefix. /// -/// Scope is a set of resources with common root path. -/// Scopes collect multiple paths under a common path prefix. -/// Scope path can contain variable path segments as resources. -/// Scope prefix is always complete path segment, i.e `/app` would -/// be converted to a `/app/` and it would not match `/app` path. +/// The `Scope`'s path can contain [dynamic segments]. The dynamic segments can be extracted from +/// requests using the [`Path`](crate::web::Path) extractor or +/// with [`HttpRequest::match_info()`](crate::HttpRequest::match_info). /// -/// You can get variable path segments from `HttpRequest::match_info()`. -/// `Path` extractor also is able to extract scope level variable segments. +/// # Avoid Trailing Slashes +/// Avoid using trailing slashes in the scope prefix (e.g., `web::scope("/scope/")`). It will almost +/// certainly not have the expected behavior. See the [documentation on resource definitions][pat] +/// to understand why this is the case and how to correctly construct scope/prefix definitions. /// +/// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// -/// fn main() { -/// let app = App::new().service( -/// web::scope("/{project_id}/") -/// .service(web::resource("/path1").to(|| async { "OK" })) -/// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok()))) -/// .service(web::resource("/path3").route(web::head().to(HttpResponse::MethodNotAllowed))) -/// ); -/// } +/// let app = App::new().service( +/// web::scope("/{project_id}/") +/// .service(web::resource("/path1").to(|| async { "OK" })) +/// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok()))) +/// .service(web::resource("/path3").route(web::head().to(HttpResponse::MethodNotAllowed))) +/// ); /// ``` /// /// In the above example three routes get registered: -/// * /{project_id}/path1 - responds to all http method -/// * /{project_id}/path2 - `GET` requests -/// * /{project_id}/path3 - `HEAD` requests +/// - /{project_id}/path1 - responds to all HTTP methods +/// - /{project_id}/path2 - responds to `GET` requests +/// - /{project_id}/path3 - responds to `HEAD` requests +/// +/// [pat]: crate::dev::ResourceDef#prefix-resources +/// [dynamic segments]: crate::dev::ResourceDef#dynamic-segments pub struct Scope { endpoint: T, rdef: String, @@ -103,16 +108,14 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app") - /// .guard(guard::Header("content-type", "text/plain")) - /// .route("/test1", web::get().to(index)) - /// .route("/test2", web::post().to(|r: HttpRequest| { - /// HttpResponse::MethodNotAllowed() - /// })) - /// ); - /// } + /// let app = App::new().service( + /// web::scope("/app") + /// .guard(guard::Header("content-type", "text/plain")) + /// .route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|r: HttpRequest| { + /// HttpResponse::MethodNotAllowed() + /// })) + /// ); /// ``` pub fn guard(mut self, guard: G) -> Self { self.guards.push(Box::new(guard)); @@ -183,15 +186,13 @@ where /// ); /// } /// - /// fn main() { - /// let app = App::new() - /// .wrap(middleware::Logger::default()) - /// .service( - /// web::scope("/api") - /// .configure(config) - /// ) - /// .route("/index.html", web::get().to(|| HttpResponse::Ok())); - /// } + /// let app = App::new() + /// .wrap(middleware::Logger::default()) + /// .service( + /// web::scope("/api") + /// .configure(config) + /// ) + /// .route("/index.html", web::get().to(|| HttpResponse::Ok())); /// ``` pub fn configure(mut self, cfg_fn: F) -> Self where @@ -230,13 +231,11 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app").service( - /// web::scope("/v1") - /// .service(web::resource("/test1").to(index))) - /// ); - /// } + /// let app = App::new().service( + /// web::scope("/app").service( + /// web::scope("/v1") + /// .service(web::resource("/test1").to(index))) + /// ); /// ``` pub fn service(mut self, factory: F) -> Self where @@ -260,13 +259,11 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app") - /// .route("/test1", web::get().to(index)) - /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())) - /// ); - /// } + /// let app = App::new().service( + /// web::scope("/app") + /// .route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())) + /// ); /// ``` pub fn route(self, path: &str, mut route: Route) -> Self { self.service( @@ -352,21 +349,19 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app") - /// .wrap_fn(|req, srv| { - /// let fut = srv.call(req); - /// async { - /// let mut res = fut.await?; - /// res.headers_mut().insert( - /// CONTENT_TYPE, HeaderValue::from_static("text/plain"), - /// ); - /// Ok(res) - /// } - /// }) - /// .route("/index.html", web::get().to(index))); - /// } + /// let app = App::new().service( + /// web::scope("/app") + /// .wrap_fn(|req, srv| { + /// let fut = srv.call(req); + /// async { + /// let mut res = fut.await?; + /// res.headers_mut().insert( + /// CONTENT_TYPE, HeaderValue::from_static("text/plain"), + /// ); + /// Ok(res) + /// } + /// }) + /// .route("/index.html", web::get().to(index))); /// ``` pub fn wrap_fn( self, @@ -399,15 +394,16 @@ where } } -impl HttpServiceFactory for Scope +impl HttpServiceFactory for Scope where T: ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), > + 'static, + B: MessageBody + 'static, { fn register(mut self, config: &mut AppService) { // update default resource if needed @@ -457,7 +453,9 @@ where req.add_data_container(Rc::clone(data)); } - srv.call(req) + let fut = srv.call(req); + + async { Ok(fut.await?.map_into_boxed_body()) } }); // register final service @@ -471,6 +469,7 @@ where } pub struct ScopeFactory { + #[allow(clippy::type_complexity)] services: Rc< [( ResourceDef, @@ -539,12 +538,15 @@ impl Service for ScopeService { fn call(&self, mut req: ServiceRequest) -> Self::Future { let res = self.router.recognize_fn(&mut req, |req, guards| { if let Some(ref guards) = guards { - for f in guards { - if !f.check(req.head()) { + let guard_ctx = req.guard_ctx(); + + for guard in guards { + if !guard.check(&guard_ctx) { return false; } } } + true }); @@ -616,11 +618,11 @@ mod tests { >, > { web::scope("/test-compat") - // .wrap_fn(|req, srv| { - // let fut = srv.call(req); - // async { Ok(fut.await?.map_into_right_body::<()>()) } - // }) - .wrap(Compat::new(DefaultHeaders::new())) + .wrap_fn(|req, srv| { + let fut = srv.call(req); + async { Ok(fut.await?.map_into_right_body::<()>()) } + }) + .wrap(Compat::noop()) .service(web::resource("").route(web::get().to(|| async { "hello" }))) } @@ -980,6 +982,29 @@ mod tests { ); } + #[actix_rt::test] + async fn test_middleware_body_type() { + // Compile test that Scope accepts any body type; test for `EitherBody` + let srv = init_service( + App::new().service( + web::scope("app") + .wrap_fn(|req, srv| { + let fut = srv.call(req); + async { Ok(fut.await?.map_into_right_body::<()>()) } + }) + .service(web::resource("/test").route(web::get().to(|| async { "hello" }))), + ), + ) + .await; + + // test if `MessageBody::try_into_bytes()` is preserved across scope layer + use actix_http::body::MessageBody as _; + let req = TestRequest::with_uri("/app/test").to_request(); + let resp = call_service(&srv, req).await; + let body = resp.into_body(); + assert_eq!(body.try_into_bytes().unwrap(), b"hello".as_ref()); + } + #[actix_rt::test] async fn test_middleware_fn() { let srv = init_service( diff --git a/src/server.rs b/src/server.rs index b2ff423f1..ed0c965b3 100644 --- a/src/server.rs +++ b/src/server.rs @@ -63,6 +63,7 @@ where backlog: u32, sockets: Vec, builder: ServerBuilder, + #[allow(clippy::type_complexity)] on_connect_fn: Option>, _phantom: PhantomData<(S, B)>, } diff --git a/src/service.rs b/src/service.rs index 9ccf5274d..975556197 100644 --- a/src/service.rs +++ b/src/service.rs @@ -7,7 +7,7 @@ use std::{ use actix_http::{ body::{BoxBody, EitherBody, MessageBody}, header::HeaderMap, - Extensions, HttpMessage, Method, Payload, PayloadStream, RequestHead, Response, + BoxedPayloadStream, Extensions, HttpMessage, Method, Payload, RequestHead, Response, ResponseHead, StatusCode, Uri, Version, }; use actix_router::{IntoPatterns, Path, Patterns, Resource, ResourceDef, Url}; @@ -21,7 +21,7 @@ use cookie::{Cookie, ParseError as CookieParseError}; use crate::{ config::{AppConfig, AppService}, dev::ensure_leading_slash, - guard::Guard, + guard::{Guard, GuardContext}, info::ConnectionInfo, rmap::ResourceMap, Error, HttpRequest, HttpResponse, @@ -172,7 +172,7 @@ impl ServiceRequest { self.head().uri.path() } - /// Counterpart to [`HttpRequest::query_string`](super::HttpRequest::query_string()). + /// Counterpart to [`HttpRequest::query_string`]. #[inline] pub fn query_string(&self) -> &str { self.req.query_string() @@ -208,13 +208,13 @@ impl ServiceRequest { self.req.match_info() } - /// Counterpart to [`HttpRequest::match_name`](super::HttpRequest::match_name()). + /// Counterpart to [`HttpRequest::match_name`]. #[inline] pub fn match_name(&self) -> Option<&str> { self.req.match_name() } - /// Counterpart to [`HttpRequest::match_pattern`](super::HttpRequest::match_pattern()). + /// Counterpart to [`HttpRequest::match_pattern`]. #[inline] pub fn match_pattern(&self) -> Option { self.req.match_pattern() @@ -238,7 +238,7 @@ impl ServiceRequest { self.req.app_config() } - /// Counterpart to [`HttpRequest::app_data`](super::HttpRequest::app_data()). + /// Counterpart to [`HttpRequest::app_data`]. #[inline] pub fn app_data(&self) -> Option<&T> { for container in self.req.inner.app_data.iter().rev() { @@ -250,19 +250,33 @@ impl ServiceRequest { None } - /// Counterpart to [`HttpRequest::conn_data`](super::HttpRequest::conn_data()). + /// Counterpart to [`HttpRequest::conn_data`]. #[inline] pub fn conn_data(&self) -> Option<&T> { self.req.conn_data() } + /// Counterpart to [`HttpRequest::req_data`]. + #[inline] + pub fn req_data(&self) -> Ref<'_, Extensions> { + self.req.req_data() + } + + /// Counterpart to [`HttpRequest::req_data_mut`]. + #[inline] + pub fn req_data_mut(&self) -> RefMut<'_, Extensions> { + self.req.req_data_mut() + } + #[cfg(feature = "cookies")] + #[inline] pub fn cookies(&self) -> Result>>, CookieParseError> { self.req.cookies() } /// Return request cookie. #[cfg(feature = "cookies")] + #[inline] pub fn cookie(&self, name: &str) -> Option> { self.req.cookie(name) } @@ -283,6 +297,14 @@ impl ServiceRequest { .app_data .push(extensions); } + + /// Creates a context object for use with a [guard](crate::guard). + /// + /// Useful if you are implementing + #[inline] + pub fn guard_ctx(&self) -> GuardContext<'_> { + GuardContext { req: self } + } } impl Resource for ServiceRequest { @@ -293,7 +315,7 @@ impl Resource for ServiceRequest { } impl HttpMessage for ServiceRequest { - type Stream = PayloadStream; + type Stream = BoxedPayloadStream; #[inline] /// Returns Request's headers. diff --git a/src/test/test_request.rs b/src/test/test_request.rs index fd3355ef3..fc42253d7 100644 --- a/src/test/test_request.rs +++ b/src/test/test_request.rs @@ -124,7 +124,7 @@ impl TestRequest { self } - /// Set HTTP Uri of this request + /// Set HTTP URI of this request pub fn uri(mut self, path: &str) -> Self { self.req.uri(path); self @@ -174,25 +174,28 @@ impl TestRequest { } /// Set request payload. - pub fn set_payload>(mut self, data: B) -> Self { + pub fn set_payload(mut self, data: impl Into) -> Self { self.req.set_payload(data); self } - /// Serialize `data` to a URL encoded form and set it as the request payload. The `Content-Type` - /// header is set to `application/x-www-form-urlencoded`. - pub fn set_form(mut self, data: &T) -> Self { - let bytes = serde_urlencoded::to_string(data) + /// Serialize `data` to a URL encoded form and set it as the request payload. + /// + /// The `Content-Type` header is set to `application/x-www-form-urlencoded`. + pub fn set_form(mut self, data: impl Serialize) -> Self { + let bytes = serde_urlencoded::to_string(&data) .expect("Failed to serialize test data as a urlencoded form"); self.req.set_payload(bytes); self.req.insert_header(ContentType::form_url_encoded()); self } - /// Serialize `data` to JSON and set it as the request payload. The `Content-Type` header is - /// set to `application/json`. - pub fn set_json(mut self, data: &T) -> Self { - let bytes = serde_json::to_string(data).expect("Failed to serialize test data to json"); + /// Serialize `data` to JSON and set it as the request payload. + /// + /// The `Content-Type` header is set to `application/json`. + pub fn set_json(mut self, data: impl Serialize) -> Self { + let bytes = + serde_json::to_string(&data).expect("Failed to serialize test data to json"); self.req.set_payload(bytes); self.req.insert_header(ContentType::json()); self diff --git a/src/web.rs b/src/web.rs index 042b8a008..47bff36a3 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,14 +1,14 @@ //! Essentials helper functions and types for application registration. -use std::{error::Error as StdError, future::Future}; +use std::future::Future; use actix_http::Method; use actix_router::IntoPatterns; pub use bytes::{Buf, BufMut, Bytes, BytesMut}; use crate::{ - body::MessageBody, error::BlockingError, extract::FromRequest, handler::Handler, - resource::Resource, route::Route, scope::Scope, service::WebService, Responder, + error::BlockingError, extract::FromRequest, handler::Handler, resource::Resource, + route::Route, scope::Scope, service::WebService, Responder, }; pub use crate::config::ServiceConfig; @@ -52,11 +52,16 @@ pub fn resource(path: T) -> Resource { /// Scopes collect multiple paths under a common path prefix. The scope's path can contain dynamic /// path segments. /// +/// # Avoid Trailing Slashes +/// Avoid using trailing slashes in the scope prefix (e.g., `web::scope("/scope/")`). It will almost +/// certainly not have the expected behavior. See the [documentation on resource definitions][pat] +/// to understand why this is the case and how to correctly construct scope/prefix definitions. +/// /// # Examples /// In this example, three routes are set up (and will handle any method): -/// * `/{project_id}/path1` -/// * `/{project_id}/path2` -/// * `/{project_id}/path3` +/// - `/{project_id}/path1` +/// - `/{project_id}/path2` +/// - `/{project_id}/path3` /// /// ``` /// use actix_web::{web, App, HttpResponse}; @@ -68,6 +73,8 @@ pub fn resource(path: T) -> Resource { /// .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed())) /// ); /// ``` +/// +/// [pat]: crate::dev::ResourceDef#prefix-resources pub fn scope(path: &str) -> Scope { Scope::new(path) } @@ -139,14 +146,11 @@ pub fn method(method: Method) -> Route { /// web::to(index)) /// ); /// ``` -pub fn to(handler: F) -> Route +pub fn to(handler: F) -> Route where - F: Handler, - I: FromRequest + 'static, - R: Future + 'static, - R::Output: Responder + 'static, - ::Body: MessageBody + 'static, - <::Body as MessageBody>::Error: Into>, + F: Handler, + Args: FromRequest + 'static, + F::Output: Responder + 'static, { Route::new().to(handler) }