diff --git a/.cargo/config.toml b/.cargo/config.toml index 4425e0dda..deb300749 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -6,9 +6,12 @@ lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dcli ci-check-min = "hack --workspace check --no-default-features" ci-check-default = "hack --workspace check" ci-check-default-tests = "check --workspace --tests" -ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,io-uring check" +ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,experimental-io-uring check" ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check" # testing ci-doctest-default = "test --workspace --doc --no-fail-fast -- --nocapture" ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture" + +# compile docs as docs.rs would +# RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-post-merge.yml similarity index 78% rename from .github/workflows/ci-master.yml rename to .github/workflows/ci-post-merge.yml index b78617dc5..1ee97b591 100644 --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-post-merge.yml @@ -1,4 +1,4 @@ -name: CI (master only) +name: CI (post-merge) on: push: @@ -23,6 +23,7 @@ jobs: CI: 1 CARGO_INCREMENTAL: 0 VCPKGRS_DYNAMIC: 1 + CARGO_UNSTABLE_SPARSE_REGISTRY: true steps: - uses: actions/checkout@v2 @@ -44,18 +45,15 @@ jobs: profile: minimal override: true + - name: Install cargo-hack + uses: taiki-e/install-action@cargo-hack + - 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 minimal uses: actions-rs/cargo@v1 with: { command: ci-check-min } @@ -78,23 +76,19 @@ jobs: cargo test --lib --tests -p=actix-multipart --all-features cargo test --lib --tests -p=actix-web-actors --all-features - - name: tests (io-uring) - if: matrix.target.os == 'ubuntu-latest' - timeout-minutes: 60 - run: > - sudo bash -c "ulimit -Sl 512 - && ulimit -Hl 512 - && PATH=$PATH:/usr/share/rust/.cargo/bin - && RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features" - - name: Clear the cargo caches run: | - cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean + cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean cargo-cache ci_feature_powerset_check: name: Verify Feature Combinations runs-on: ubuntu-latest + + env: + CI: 1 + CARGO_INCREMENTAL: 0 + steps: - uses: actions/checkout@v2 @@ -105,18 +99,15 @@ jobs: profile: minimal override: true + - name: Install cargo-hack + uses: taiki-e/install-action@cargo-hack + - 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 } @@ -125,29 +116,35 @@ jobs: uses: actions-rs/cargo@v1 with: { command: ci-check-all-feature-powerset-linux } - coverage: - name: coverage + nextest: + name: nextest runs-on: ubuntu-latest + + env: + CI: 1 + CARGO_INCREMENTAL: 0 + steps: - uses: actions/checkout@v2 - - name: Install stable + - name: Install Rust uses: actions-rs/toolchain@v1 with: - toolchain: stable-x86_64-unknown-linux-gnu + toolchain: stable profile: minimal override: true + - name: Install nextest + uses: taiki-e/install-action@nextest + - name: Generate Cargo.lock uses: actions-rs/cargo@v1 with: { command: generate-lockfile } - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.2.0 + uses: Swatinem/rust-cache@v1.3.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 } + - name: Test with cargo-nextest + uses: actions-rs/cargo@v1 + with: + command: nextest + args: run diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f41aa972f..2ea920808 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc } version: - - 1.54.0 # MSRV + - 1.57.0 # MSRV - stable name: ${{ matrix.target.name }} / ${{ matrix.version }} @@ -47,18 +47,22 @@ jobs: profile: minimal override: true + - name: Install cargo-hack + uses: taiki-e/install-action@cargo-hack + + - name: workaround MSRV issues + if: matrix.version != 'stable' + run: | + cargo install cargo-edit --version=0.8.0 + cargo add const-str@0.3 --dev -p=actix-web + cargo add const-str@0.3 --dev -p=awc + - 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 minimal uses: actions-rs/cargo@v1 with: { command: ci-check-min } @@ -81,19 +85,37 @@ jobs: cargo test --lib --tests -p=actix-multipart --all-features cargo test --lib --tests -p=actix-web-actors --all-features + - name: Clear the cargo caches + run: | + cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean + cargo-cache + + io-uring: + name: io-uring tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install Rust + 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.3.0 + - name: tests (io-uring) - if: matrix.target.os == 'ubuntu-latest' timeout-minutes: 60 run: > sudo bash -c "ulimit -Sl 512 && ulimit -Hl 512 && PATH=$PATH:/usr/share/rust/.cargo/bin - && RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features" - - - name: Clear the cargo caches - run: | - cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean - cargo-cache + && RUSTUP_TOOLCHAIN=stable cargo test --lib --tests -p=actix-files --all-features" rustdoc: name: doc tests diff --git a/.github/workflows/clippy-fmt.yml b/.github/workflows/clippy-fmt.yml index 9fcb0a561..bc2cec145 100644 --- a/.github/workflows/clippy-fmt.yml +++ b/.github/workflows/clippy-fmt.yml @@ -46,3 +46,21 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} args: --workspace --tests --examples --all-features + + lint-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + components: rust-docs + - name: Check for broken intra-doc links + uses: actions-rs/cargo@v1 + env: + RUSTDOCFLAGS: "-D warnings" + with: + command: doc + args: --no-deps --all-features --workspace diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 000000000..137a413d0 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,36 @@ +# disabled because `cargo tarpaulin` currently segfaults + +name: Coverage + +on: + push: + branches: [master] + +jobs: + # job currently (1st Feb 2022) segfaults + 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/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..677ba8ef2 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "proseWrap": "never" +} diff --git a/CHANGES.md b/CHANGES.md index 8c3997663..7bec65640 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,1024 +1,5 @@ -# Changes +# Changelog -## Unreleased - 2021-xx-xx -### Removed -- `impl Future for HttpResponse`. [#2601] +Changelogs are kept separately for each crate in this repo. -[#2601]: https://github.com/actix/actix-web/pull/2601 - - -## 4.0.0-beta.21 - 2022-01-21 -### Added -- `HttpResponse::add_removal_cookie`. [#2586] -- `Logger::log_target`. [#2594] - -### Removed -- `HttpRequest::req_data[_mut]()`; request-local data is still available through `.extensions()`. [#2585] -- `HttpRequestBuilder::del_cookie`. [#2591] - -[#2585]: https://github.com/actix/actix-web/pull/2585 -[#2586]: https://github.com/actix/actix-web/pull/2586 -[#2591]: https://github.com/actix/actix-web/pull/2591 -[#2594]: https://github.com/actix/actix-web/pull/2594 - - -## 4.0.0-beta.20 - 2022-01-14 -### Added -- `GuardContext::header` [#2569] -- `ServiceConfig::configure` to allow easy nesting of configuration functions. [#1988] - -### Changed -- `HttpResponse` can now be used as a `Responder` with any body type. [#2567] -- `Result` extractor wrapper can now convert error types. [#2581] -- Associated types in `FromRequest` impl for `Option` and `Result` has changed. [#2581] -- Maximum number of handler extractors has increased to 12. [#2582] -- Removed bound `::Error: Debug` in test utility functions in order to support returning opaque apps. [#2584] - -[#1988]: https://github.com/actix/actix-web/pull/1988 -[#2567]: https://github.com/actix/actix-web/pull/2567 -[#2569]: https://github.com/actix/actix-web/pull/2569 -[#2581]: https://github.com/actix/actix-web/pull/2581 -[#2582]: https://github.com/actix/actix-web/pull/2582 -[#2584]: https://github.com/actix/actix-web/pull/2584 - - -## 4.0.0-beta.19 - 2022-01-04 -### Added -- `impl Hash` for `http::header::Encoding`. [#2501] -- `AcceptEncoding::negotiate()`. [#2501] - -### Changed -- `AcceptEncoding::preference` now returns `Option>`. [#2501] -- Rename methods `BodyEncoding::{encoding => encode_with, get_encoding => preferred_encoding}`. [#2501] -- `http::header::Encoding` now only represents `Content-Encoding` types. [#2501] - -### Fixed -- Auto-negotiation of content encoding is more fault-tolerant when using the `Compress` middleware. [#2501] - -### Removed -- `Compress::new`; restricting compression algorithm is done through feature flags. [#2501] -- `BodyEncoding` trait; signalling content encoding is now only done via the `Content-Encoding` header. [#2565] - -[#2501]: https://github.com/actix/actix-web/pull/2501 -[#2565]: https://github.com/actix/actix-web/pull/2565 - - -## 4.0.0-beta.18 - 2021-12-29 -### Changed -- Update `cookie` dependency (re-exported) to `0.16`. [#2555] -- Minimum supported Rust version (MSRV) is now 1.54. - -### Security -- `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. - -[#2555]: https://github.com/actix/actix-web/pull/2555 -[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html - - -## 4.0.0-beta.17 - 2021-12-29 -### 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 -### Added -- Method on `Responder` trait (`customize`) for customizing responders and `CustomizeResponder` struct. [#2510] -- Implement `Debug` for `DefaultHeaders`. [#2510] - -### Changed -- Align `DefaultHeader` method terminology, deprecating previous methods. [#2510] -- Response service types in `ErrorHandlers` middleware now use `ServiceResponse>` to allow changing the body type. [#2515] -- Both variants in `ErrorHandlerResponse` now use `ServiceResponse>`. [#2515] -- Rename `test::{default_service => simple_service}`. Old name is deprecated. [#2518] -- Rename `test::{read_response_json => call_and_read_body_json}`. Old name is deprecated. [#2518] -- Rename `test::{read_response => call_and_read_body}`. Old name is deprecated. [#2518] -- Relax body type and error bounds on test utilities. [#2518] - -### Removed -- Top-level `EitherExtractError` export. [#2510] -- Conversion implementations for `either` crate. [#2516] -- `test::load_stream` and `test::load_body`; replace usage with `body::to_bytes`. [#2518] - -[#2510]: https://github.com/actix/actix-web/pull/2510 -[#2515]: https://github.com/actix/actix-web/pull/2515 -[#2516]: https://github.com/actix/actix-web/pull/2516 -[#2518]: https://github.com/actix/actix-web/pull/2518 - - -## 4.0.0-beta.14 - 2021-12-11 -### Added -- Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480] -- `AcceptEncoding` typed header. [#2482] -- `Range` typed header. [#2485] -- `HttpResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468] -- `ServiceResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468] -- Connection data set through the `HttpServer::on_connect` callback is now accessible only from the new `HttpRequest::conn_data()` and `ServiceRequest::conn_data()` methods. [#2491] -- `HttpRequest::{req_data,req_data_mut}`. [#2487] -- `ServiceResponse::into_parts`. [#2499] - -### Changed -- Rename `Accept::{mime_precedence => ranked}`. [#2480] -- Rename `Accept::{mime_preference => preference}`. [#2480] -- Un-deprecate `App::data_factory`. [#2484] -- `HttpRequest::url_for` no longer constructs URLs with query or fragment components. [#2430] -- Remove `B` (body) type parameter on `App`. [#2493] -- Add `B` (body) type parameter on `Scope`. [#2492] -- Request-local data container is no longer part of a `RequestHead`. Instead it is a distinct part of a `Request`. [#2487] - -### Fixed -- Accept wildcard `*` items in `AcceptLanguage`. [#2480] -- Re-exports `dev::{BodySize, MessageBody, SizedStream}`. They are exposed through the `body` module. [#2468] -- Typed headers containing lists that require one or more items now enforce this minimum. [#2482] - -### Removed -- `ConnectionInfo::get`. [#2487] - -[#2430]: https://github.com/actix/actix-web/pull/2430 -[#2468]: https://github.com/actix/actix-web/pull/2468 -[#2480]: https://github.com/actix/actix-web/pull/2480 -[#2482]: https://github.com/actix/actix-web/pull/2482 -[#2484]: https://github.com/actix/actix-web/pull/2484 -[#2485]: https://github.com/actix/actix-web/pull/2485 -[#2487]: https://github.com/actix/actix-web/pull/2487 -[#2491]: https://github.com/actix/actix-web/pull/2491 -[#2492]: https://github.com/actix/actix-web/pull/2492 -[#2493]: https://github.com/actix/actix-web/pull/2493 -[#2499]: https://github.com/actix/actix-web/pull/2499 - - -## 4.0.0-beta.13 - 2021-11-30 -### Changed -- Update `actix-tls` to `3.0.0-rc.1`. [#2474] - -[#2474]: https://github.com/actix/actix-web/pull/2474 - - -## 4.0.0-beta.12 - 2021-11-22 -### Changed -- Compress middleware's response type is now `AnyBody>`. [#2448] - -### Fixed -- Relax `Unpin` bound on `S` (stream) parameter of `HttpResponseBuilder::streaming`. [#2448] - -### Removed -- `dev::ResponseBody` re-export; is function is replaced by the new `dev::AnyBody` enum. [#2446] - -[#2446]: https://github.com/actix/actix-web/pull/2446 -[#2448]: https://github.com/actix/actix-web/pull/2448 - - -## 4.0.0-beta.11 - 2021-11-15 -### Added -- Re-export `dev::ServerHandle` from `actix-server`. [#2442] - -### Changed -- `ContentType::html` now produces `text/html; charset=utf-8` instead of `text/html`. [#2423] -- Update `actix-server` to `2.0.0-beta.9`. [#2442] - -[#2423]: https://github.com/actix/actix-web/pull/2423 -[#2442]: https://github.com/actix/actix-web/pull/2442 - - -## 4.0.0-beta.10 - 2021-10-20 -### Added -- Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362] -- `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] - -### Changed -- Associated type `FromRequest::Config` was removed. [#2233] -- Inner field made private on `web::Payload`. [#2384] -- `Data::into_inner` and `Data::get_ref` no longer requires `T: Sized`. [#2403] -- Updated rustls to v0.20. [#2414] -- Minimum supported Rust version (MSRV) is now 1.52. - -### Removed -- Useless `ServiceResponse::checked_expr` method. [#2401] - -[#2233]: https://github.com/actix/actix-web/pull/2233 -[#2362]: https://github.com/actix/actix-web/pull/2362 -[#2384]: https://github.com/actix/actix-web/pull/2384 -[#2401]: https://github.com/actix/actix-web/pull/2401 -[#2403]: https://github.com/actix/actix-web/pull/2403 -[#2409]: https://github.com/actix/actix-web/pull/2409 -[#2414]: https://github.com/actix/actix-web/pull/2414 - - -## 4.0.0-beta.9 - 2021-09-09 -### Added -- Re-export actix-service `ServiceFactory` in `dev` module. [#2325] - -### Changed -- Compress middleware will return 406 Not Acceptable when no content encoding is acceptable to the client. [#2344] -- Move `BaseHttpResponse` to `dev::Response`. [#2379] -- Enable `TestRequest::param` to accept more than just static strings. [#2172] -- Minimum supported Rust version (MSRV) is now 1.51. - -### Fixed -- Fix quality parse error in Accept-Encoding header. [#2344] -- Re-export correct type at `web::HttpResponse`. [#2379] - -[#2172]: https://github.com/actix/actix-web/pull/2172 -[#2325]: https://github.com/actix/actix-web/pull/2325 -[#2344]: https://github.com/actix/actix-web/pull/2344 -[#2379]: https://github.com/actix/actix-web/pull/2379 - - -## 4.0.0-beta.8 - 2021-06-26 -### Added -- Add `ServiceRequest::parts_mut`. [#2177] -- Add extractors for `Uri` and `Method`. [#2263] -- Add extractors for `ConnectionInfo` and `PeerAddr`. [#2263] -- Add `Route::service` for using hand-written services as handlers. [#2262] - -### Changed -- Change compression algorithm features flags. [#2250] -- Deprecate `App::data` and `App::data_factory`. [#2271] -- Smarter extraction of `ConnectionInfo` parts. [#2282] - -### Fixed -- Scope and Resource middleware can access data items set on their own layer. [#2288] - -[#2177]: https://github.com/actix/actix-web/pull/2177 -[#2250]: https://github.com/actix/actix-web/pull/2250 -[#2271]: https://github.com/actix/actix-web/pull/2271 -[#2262]: https://github.com/actix/actix-web/pull/2262 -[#2263]: https://github.com/actix/actix-web/pull/2263 -[#2282]: https://github.com/actix/actix-web/pull/2282 -[#2288]: https://github.com/actix/actix-web/pull/2288 - - -## 4.0.0-beta.7 - 2021-06-17 -### Added -- `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200] - -### Changed -- Adjusted default JSON payload limit to 2MB (from 32kb) and included size and limits in the `JsonPayloadError::Overflow` error variant. [#2162] -[#2162]: (https://github.com/actix/actix-web/pull/2162) -- `ServiceResponse::error_response` now uses body type of `Body`. [#2201] -- `ServiceResponse::checked_expr` now returns a `Result`. [#2201] -- Update `language-tags` to `0.3`. -- `ServiceResponse::take_body`. [#2201] -- `ServiceResponse::map_body` closure receives and returns `B` instead of `ResponseBody` types. [#2201] -- All error trait bounds in server service builders have changed from `Into` to `Into>`. [#2253] -- All error trait bounds in message body and stream impls changed from `Into` to `Into>`. [#2253] -- `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226] -- `middleware::normalize` now will not try to normalize URIs with no valid path [#2246] - -### Removed -- `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201] - -[#2200]: https://github.com/actix/actix-web/pull/2200 -[#2201]: https://github.com/actix/actix-web/pull/2201 -[#2253]: https://github.com/actix/actix-web/pull/2253 -[#2246]: https://github.com/actix/actix-web/pull/2246 - - -## 4.0.0-beta.6 - 2021-04-17 -### Added -- `HttpResponse` and `HttpResponseBuilder` structs. [#2065] - -### Changed -- Most error types are now marked `#[non_exhaustive]`. [#2148] -- Methods on `ContentDisposition` that took `T: AsRef` now take `impl AsRef`. - -[#2065]: https://github.com/actix/actix-web/pull/2065 -[#2148]: https://github.com/actix/actix-web/pull/2148 - - -## 4.0.0-beta.5 - 2021-04-02 -### Added -- `Header` extractor for extracting common HTTP headers in handlers. [#2094] -- Added `TestServer::client_headers` method. [#2097] - -### Fixed -- Double ampersand in Logger format is escaped correctly. [#2067] - -### Changed -- `CustomResponder` would return error as `HttpResponse` when `CustomResponder::with_header` failed - instead of skipping. (Only the first error is kept when multiple error occur) [#2093] - -### Removed -- The `client` mod was removed. Clients should now use `awc` directly. - [871ca5e4](https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7) -- Integration testing was moved to new `actix-test` crate. Namely these items from the `test` - module: `TestServer`, `TestServerConfig`, `start`, `start_with`, and `unused_addr`. [#2112] - -[#2067]: https://github.com/actix/actix-web/pull/2067 -[#2093]: https://github.com/actix/actix-web/pull/2093 -[#2094]: https://github.com/actix/actix-web/pull/2094 -[#2097]: https://github.com/actix/actix-web/pull/2097 -[#2112]: https://github.com/actix/actix-web/pull/2112 - - -## 4.0.0-beta.4 - 2021-03-09 -### Changed -- Feature `cookies` is now optional and enabled by default. [#1981] -- `JsonBody::new` returns a default limit of 32kB to be consistent with `JsonConfig` and the default - behaviour of the `web::Json` extractor. [#2010] - -[#1981]: https://github.com/actix/actix-web/pull/1981 -[#2010]: https://github.com/actix/actix-web/pull/2010 - - -## 4.0.0-beta.3 - 2021-02-10 -- Update `actix-web-codegen` to `0.5.0-beta.1`. - - -## 4.0.0-beta.2 - 2021-02-10 -### Added -- The method `Either, web::Form>::into_inner()` which returns the inner type for - whichever variant was created. Also works for `Either, web::Json>`. [#1894] -- Add `services!` macro for helping register multiple services to `App`. [#1933] -- Enable registering a vec of services of the same type to `App` [#1933] - -### Changed -- Rework `Responder` trait to be sync and returns `Response`/`HttpResponse` directly. - Making it simpler and more performant. [#1891] -- `ServiceRequest::into_parts` and `ServiceRequest::from_parts` can no longer fail. [#1893] -- `ServiceRequest::from_request` can no longer fail. [#1893] -- Our `Either` type now uses `Left`/`Right` variants (instead of `A`/`B`) [#1894] -- `test::{call_service, read_response, read_response_json, send_request}` take `&Service` - in argument [#1905] -- `App::wrap_fn`, `Resource::wrap_fn` and `Scope::wrap_fn` provide `&Service` in closure - argument. [#1905] -- `web::block` no longer requires the output is a Result. [#1957] - -### Fixed -- Multiple calls to `App::data` with the same type now keeps the latest call's data. [#1906] - -### Removed -- Public field of `web::Path` has been made private. [#1894] -- Public field of `web::Query` has been made private. [#1894] -- `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869] -- `AppService::set_service_data`; for custom HTTP service factories adding application data, use the - layered data model by calling `ServiceRequest::add_data_container` when handling - requests instead. [#1906] - -[#1891]: https://github.com/actix/actix-web/pull/1891 -[#1893]: https://github.com/actix/actix-web/pull/1893 -[#1894]: https://github.com/actix/actix-web/pull/1894 -[#1869]: https://github.com/actix/actix-web/pull/1869 -[#1905]: https://github.com/actix/actix-web/pull/1905 -[#1906]: https://github.com/actix/actix-web/pull/1906 -[#1933]: https://github.com/actix/actix-web/pull/1933 -[#1957]: https://github.com/actix/actix-web/pull/1957 - - -## 4.0.0-beta.1 - 2021-01-07 -### Added -- `Compat` middleware enabling generic response body/error type of middlewares like `Logger` and - `Compress` to be used in `middleware::Condition` and `Resource`, `Scope` services. [#1865] - -### Changed -- Update `actix-*` dependencies to tokio `1.0` based versions. [#1813] -- Bumped `rand` to `0.8`. -- Update `rust-tls` to `0.19`. [#1813] -- Rename `Handler` to `HandlerService` and rename `Factory` to `Handler`. [#1852] -- The default `TrailingSlash` is now `Trim`, in line with existing documentation. See migration - guide for implications. [#1875] -- Rename `DefaultHeaders::{content_type => add_content_type}`. [#1875] -- MSRV is now 1.46.0. - -### Fixed -- Added the underlying parse error to `test::read_body_json`'s panic message. [#1812] - -### Removed -- Public modules `middleware::{normalize, err_handlers}`. All necessary middleware structs are now - exposed directly by the `middleware` module. -- Remove `actix-threadpool` as dependency. `actix_threadpool::BlockingError` error type can be imported - from `actix_web::error` module. [#1878] - -[#1812]: https://github.com/actix/actix-web/pull/1812 -[#1813]: https://github.com/actix/actix-web/pull/1813 -[#1852]: https://github.com/actix/actix-web/pull/1852 -[#1865]: https://github.com/actix/actix-web/pull/1865 -[#1875]: https://github.com/actix/actix-web/pull/1875 -[#1878]: https://github.com/actix/actix-web/pull/1878 - - -## 3.3.3 - 2021-12-18 -### Changed -- Soft-deprecate `NormalizePath::default()`, noting upcoming behavior change in v4. [#2529] - -[#2529]: https://github.com/actix/actix-web/pull/2529 - - -## 3.3.2 - 2020-12-01 -### Fixed -- Removed an occasional `unwrap` on `None` panic in `NormalizePathNormalization`. [#1762] -- Fix `match_pattern()` returning `None` for scope with empty path resource. [#1798] -- Increase minimum `socket2` version. [#1803] - -[#1762]: https://github.com/actix/actix-web/pull/1762 -[#1798]: https://github.com/actix/actix-web/pull/1798 -[#1803]: https://github.com/actix/actix-web/pull/1803 - - -## 3.3.1 - 2020-11-29 -- Ensure `actix-http` dependency uses same `serde_urlencoded`. - - -## 3.3.0 - 2020-11-25 -### Added -- Add `Either` extractor helper. [#1788] - -### Changed -- Upgrade `serde_urlencoded` to `0.7`. [#1773] - -[#1773]: https://github.com/actix/actix-web/pull/1773 -[#1788]: https://github.com/actix/actix-web/pull/1788 - - -## 3.2.0 - 2020-10-30 -### Added -- Implement `exclude_regex` for Logger middleware. [#1723] -- Add request-local data extractor `web::ReqData`. [#1748] -- Add ability to register closure for request middleware logging. [#1749] -- Add `app_data` to `ServiceConfig`. [#1757] -- Expose `on_connect` for access to the connection stream before request is handled. [#1754] - -### Changed -- Updated actix-web-codegen dependency for access to new `#[route(...)]` multi-method macro. -- Print non-configured `Data` type when attempting extraction. [#1743] -- Re-export bytes::Buf{Mut} in web module. [#1750] -- Upgrade `pin-project` to `1.0`. - -[#1723]: https://github.com/actix/actix-web/pull/1723 -[#1743]: https://github.com/actix/actix-web/pull/1743 -[#1748]: https://github.com/actix/actix-web/pull/1748 -[#1750]: https://github.com/actix/actix-web/pull/1750 -[#1754]: https://github.com/actix/actix-web/pull/1754 -[#1749]: https://github.com/actix/actix-web/pull/1749 - - -## 3.1.0 - 2020-09-29 -### Changed -- Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allows `NormalizePath` - to retain any trailing slashes. [#1695] -- Remove bound `std::marker::Sized` from `web::Data` to support storing `Arc` - via `web::Data::from` [#1710] - -### Fixed -- `ResourceMap` debug printing is no longer infinitely recursive. [#1708] - -[#1695]: https://github.com/actix/actix-web/pull/1695 -[#1708]: https://github.com/actix/actix-web/pull/1708 -[#1710]: https://github.com/actix/actix-web/pull/1710 - - -## 3.0.2 - 2020-09-15 -### Fixed -- `NormalizePath` when used with `TrailingSlash::Trim` no longer trims the root path "/". [#1678] - -[#1678]: https://github.com/actix/actix-web/pull/1678 - - -## 3.0.1 - 2020-09-13 -### Changed -- `middleware::normalize::TrailingSlash` enum is now accessible. [#1673] - -[#1673]: https://github.com/actix/actix-web/pull/1673 - - -## 3.0.0 - 2020-09-11 -- No significant changes from `3.0.0-beta.4`. - - -## 3.0.0-beta.4 - 2020-09-09 -### Added -- `middleware::NormalizePath` now has configurable behavior for either always having a trailing - slash, or as the new addition, always trimming trailing slashes. [#1639] - -### Changed -- Update actix-codec and actix-utils dependencies. [#1634] -- `FormConfig` and `JsonConfig` configurations are now also considered when set - using `App::data`. [#1641] -- `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. [#1655] -- `HttpServer::maxconnrate` is renamed to the more expressive - `HttpServer::max_connection_rate`. [#1655] - -[#1639]: https://github.com/actix/actix-web/pull/1639 -[#1641]: https://github.com/actix/actix-web/pull/1641 -[#1634]: https://github.com/actix/actix-web/pull/1634 -[#1655]: https://github.com/actix/actix-web/pull/1655 - -## 3.0.0-beta.3 - 2020-08-17 -### Changed -- Update `rustls` to 0.18 - - -## 3.0.0-beta.2 - 2020-08-17 -### Changed -- `PayloadConfig` is now also considered in `Bytes` and `String` extractors when set - using `App::data`. [#1610] -- `web::Path` now has a public representation: `web::Path(pub T)` that enables - destructuring. [#1594] -- `ServiceRequest::app_data` allows retrieval of non-Data data without splitting into parts to - access `HttpRequest` which already allows this. [#1618] -- Re-export all error types from `awc`. [#1621] -- MSRV is now 1.42.0. - -### Fixed -- Memory leak of app data in pooled requests. [#1609] - -[#1594]: https://github.com/actix/actix-web/pull/1594 -[#1609]: https://github.com/actix/actix-web/pull/1609 -[#1610]: https://github.com/actix/actix-web/pull/1610 -[#1618]: https://github.com/actix/actix-web/pull/1618 -[#1621]: https://github.com/actix/actix-web/pull/1621 - - -## 3.0.0-beta.1 - 2020-07-13 -### Added -- Re-export `actix_rt::main` as `actix_web::main`. -- `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched - resource pattern. -- `HttpRequest::match_name` and `ServiceRequest::match_name` for extracting matched resource name. - -### Changed -- Fix actix_http::h1::dispatcher so it returns when HW_BUFFER_SIZE is reached. Should reduce peak memory consumption during large uploads. [#1550] -- Migrate cookie handling to `cookie` crate. Actix-web no longer requires `ring` dependency. -- MSRV is now 1.41.1 - -### Fixed -- `NormalizePath` improved consistency when path needs slashes added _and_ removed. - - -## 3.0.0-alpha.3 - 2020-05-21 -### Added -- Add option to create `Data` from `Arc` [#1509] - -### Changed -- Resources and Scopes can now access non-overridden data types set on App (or containing scopes) when setting their own data. [#1486] -- Fix audit issue logging by default peer address [#1485] -- Bump minimum supported Rust version to 1.40 -- Replace deprecated `net2` crate with `socket2` - -[#1485]: https://github.com/actix/actix-web/pull/1485 -[#1509]: https://github.com/actix/actix-web/pull/1509 - -## [3.0.0-alpha.2] - 2020-05-08 - -### Changed - -- `{Resource,Scope}::default_service(f)` handlers now support app data extraction. [#1452] -- Implement `std::error::Error` for our custom errors [#1422] -- NormalizePath middleware now appends trailing / so that routes of form /example/ respond to /example requests. [#1433] -- Remove the `failure` feature and support. - -[#1422]: https://github.com/actix/actix-web/pull/1422 -[#1433]: https://github.com/actix/actix-web/pull/1433 -[#1452]: https://github.com/actix/actix-web/pull/1452 -[#1486]: https://github.com/actix/actix-web/pull/1486 - - -## [3.0.0-alpha.1] - 2020-03-11 - -### Added - -- Add helper function for creating routes with `TRACE` method guard `web::trace()` -- Add convenience functions `test::read_body_json()` and `test::TestRequest::send_request()` for testing. - -### Changed - -- Use `sha-1` crate instead of unmaintained `sha1` crate -- Skip empty chunks when returning response from a `Stream` [#1308] -- Update the `time` dependency to 0.2.7 -- Update `actix-tls` dependency to 2.0.0-alpha.1 -- Update `rustls` dependency to 0.17 - -[#1308]: https://github.com/actix/actix-web/pull/1308 - -## [2.0.0] - 2019-12-25 - -### Changed - -- Rename `HttpServer::start()` to `HttpServer::run()` - -- Allow to gracefully stop test server via `TestServer::stop()` - -- Allow to specify multi-patterns for resources - -## [2.0.0-rc] - 2019-12-20 - -### Changed - -- Move `BodyEncoding` to `dev` module #1220 - -- Allow to set `peer_addr` for TestRequest #1074 - -- Make web::Data deref to Arc #1214 - -- Rename `App::register_data()` to `App::app_data()` - -- `HttpRequest::app_data()` returns `Option<&T>` instead of `Option<&Data>` - -### Fixed - -- Fix `AppConfig::secure()` is always false. #1202 - - -## [2.0.0-alpha.6] - 2019-12-15 - -### Fixed - -- Fixed compilation with default features off - -## [2.0.0-alpha.5] - 2019-12-13 - -### Added - -- Add test server, `test::start()` and `test::start_with()` - -## [2.0.0-alpha.4] - 2019-12-08 - -### Deleted - -- Delete HttpServer::run(), it is not useful with async/await - -## [2.0.0-alpha.3] - 2019-12-07 - -### Changed - -- Migrate to tokio 0.2 - - -## [2.0.0-alpha.1] - 2019-11-22 - -### Changed - -- Migrated to `std::future` - -- Remove implementation of `Responder` for `()`. (#1167) - - -## [1.0.9] - 2019-11-14 - -### Added - -- Add `Payload::into_inner` method and make stored `def::Payload` public. (#1110) - -### Changed - -- Support `Host` guards when the `Host` header is unset (e.g. HTTP/2 requests) (#1129) - - -## [1.0.8] - 2019-09-25 - -### Added - -- Add `Scope::register_data` and `Resource::register_data` methods, parallel to - `App::register_data`. - -- Add `middleware::Condition` that conditionally enables another middleware - -- Allow to re-construct `ServiceRequest` from `HttpRequest` and `Payload` - -- Add `HttpServer::listen_uds` for ability to listen on UDS FD rather than path, - which is useful for example with systemd. - -### Changed - -- Make UrlEncodedError::Overflow more informative - -- Use actix-testing for testing utils - - -## [1.0.7] - 2019-08-29 - -### Fixed - -- Request Extensions leak #1062 - - -## [1.0.6] - 2019-08-28 - -### Added - -- Re-implement Host predicate (#989) - -- Form implements Responder, returning a `application/x-www-form-urlencoded` response - -- Add `into_inner` to `Data` - -- Add `test::TestRequest::set_form()` convenience method to automatically serialize data and set - the header in test requests. - -### Changed - -- `Query` payload made `pub`. Allows user to pattern-match the payload. - -- Enable `rust-tls` feature for client #1045 - -- Update serde_urlencoded to 0.6.1 - -- Update url to 2.1 - - -## [1.0.5] - 2019-07-18 - -### Added - -- Unix domain sockets (HttpServer::bind_uds) #92 - -- Actix now logs errors resulting in "internal server error" responses always, with the `error` - logging level - -### Fixed - -- Restored logging of errors through the `Logger` middleware - - -## [1.0.4] - 2019-07-17 - -### Added - -- Add `Responder` impl for `(T, StatusCode) where T: Responder` - -- Allow to access app's resource map via - `ServiceRequest::resource_map()` and `HttpRequest::resource_map()` methods. - -### Changed - -- Upgrade `rand` dependency version to 0.7 - - -## [1.0.3] - 2019-06-28 - -### Added - -- Support asynchronous data factories #850 - -### Changed - -- Use `encoding_rs` crate instead of unmaintained `encoding` crate - - -## [1.0.2] - 2019-06-17 - -### Changed - -- Move cors middleware to `actix-cors` crate. - -- Move identity middleware to `actix-identity` crate. - - -## [1.0.1] - 2019-06-17 - -### Added - -- Add support for PathConfig #903 - -- Add `middleware::identity::RequestIdentity` trait to `get_identity` from `HttpMessage`. - -### Changed - -- Move cors middleware to `actix-cors` crate. - -- Move identity middleware to `actix-identity` crate. - -- Disable default feature `secure-cookies`. - -- Allow to test an app that uses async actors #897 - -- Re-apply patch from #637 #894 - -### Fixed - -- HttpRequest::url_for is broken with nested scopes #915 - - -## [1.0.0] - 2019-06-05 - -### Added - -- Add `Scope::configure()` method. - -- Add `ServiceRequest::set_payload()` method. - -- Add `test::TestRequest::set_json()` convenience method to automatically - serialize data and set header in test requests. - -- Add macros for head, options, trace, connect and patch http methods - -### Changed - -- Drop an unnecessary `Option<_>` indirection around `ServerBuilder` from `HttpServer`. #863 - -### Fixed - -- Fix Logger request time format, and use rfc3339. #867 - -- Clear http requests pool on app service drop #860 - - -## [1.0.0-rc] - 2019-05-18 - -### Added - -- Add `Query::from_query()` to extract parameters from a query string. #846 -- `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors. - -### Changed - -- `JsonConfig` is now `Send + Sync`, this implies that `error_handler` must be `Send + Sync` too. - -### Fixed - -- Codegen with parameters in the path only resolves the first registered endpoint #841 - - -## [1.0.0-beta.4] - 2019-05-12 - -### Added - -- Allow to set/override app data on scope level - -### Changed - -- `App::configure` take an `FnOnce` instead of `Fn` -- Upgrade actix-net crates - - -## [1.0.0-beta.3] - 2019-05-04 - -### Added - -- Add helper function for executing futures `test::block_fn()` - -### Changed - -- Extractor configuration could be registered with `App::data()` - or with `Resource::data()` #775 - -- Route data is unified with app data, `Route::data()` moved to resource - level to `Resource::data()` - -- CORS handling without headers #702 - -- Allow constructing `Data` instances to avoid double `Arc` for `Send + Sync` types. - -### Fixed - -- Fix `NormalizePath` middleware impl #806 - -### Deleted - -- `App::data_factory()` is deleted. - - -## [1.0.0-beta.2] - 2019-04-24 - -### Added - -- Add raw services support via `web::service()` - -- Add helper functions for reading response body `test::read_body()` - -- Add support for `remainder match` (i.e "/path/{tail}*") - -- Extend `Responder` trait, allow to override status code and headers. - -- Store visit and login timestamp in the identity cookie #502 - -### Changed - -- `.to_async()` handler can return `Responder` type #792 - -### Fixed - -- Fix async web::Data factory handling - - -## [1.0.0-beta.1] - 2019-04-20 - -### Added - -- Add helper functions for reading test response body, - `test::read_response()` and test::read_response_json()` - -- Add `.peer_addr()` #744 - -- Add `NormalizePath` middleware - -### Changed - -- Rename `RouterConfig` to `ServiceConfig` - -- Rename `test::call_success` to `test::call_service` - -- Removed `ServiceRequest::from_parts()` as it is unsafe to create from parts. - -- `CookieIdentityPolicy::max_age()` accepts value in seconds - -### Fixed - -- Fixed `TestRequest::app_data()` - - -## [1.0.0-alpha.6] - 2019-04-14 - -### Changed - -- Allow using any service as default service. - -- Remove generic type for request payload, always use default. - -- Removed `Decompress` middleware. Bytes, String, Json, Form extractors - automatically decompress payload. - -- Make extractor config type explicit. Add `FromRequest::Config` associated type. - - -## [1.0.0-alpha.5] - 2019-04-12 - -### Added - -- Added async io `TestBuffer` for testing. - -### Deleted - -- Removed native-tls support - - -## [1.0.0-alpha.4] - 2019-04-08 - -### Added - -- `App::configure()` allow to offload app configuration to different methods - -- Added `URLPath` option for logger - -- Added `ServiceRequest::app_data()`, returns `Data` - -- Added `ServiceFromRequest::app_data()`, returns `Data` - -### Changed - -- `FromRequest` trait refactoring - -- Move multipart support to actix-multipart crate - -### Fixed - -- Fix body propagation in Response::from_error. #760 - - -## [1.0.0-alpha.3] - 2019-04-02 - -### Changed - -- Renamed `TestRequest::to_service()` to `TestRequest::to_srv_request()` - -- Renamed `TestRequest::to_response()` to `TestRequest::to_srv_response()` - -- Removed `Deref` impls - -### Removed - -- Removed unused `actix_web::web::md()` - - -## [1.0.0-alpha.2] - 2019-03-29 - -### Added - -- Rustls support - -### Changed - -- Use forked cookie - -- Multipart::Field renamed to MultipartField - -## [1.0.0-alpha.1] - 2019-03-28 - -### Changed - -- Complete architecture re-design. - -- Return 405 response if no matching route found within resource #538 +Actix Web changelog [is now here →](./actix-web/CHANGES.md). diff --git a/Cargo.toml b/Cargo.toml index 99ff85e8d..26b5b91b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,36 +1,6 @@ -[package] -name = "actix-web" -version = "4.0.0-beta.21" -authors = [ - "Nikolay Kim ", - "Rob Ede ", -] -description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" -keywords = ["actix", "http", "web", "framework", "async"] -categories = [ - "network-programming", - "asynchronous", - "web-programming::http-server", - "web-programming::websocket" -] -homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -license = "MIT OR Apache-2.0" -edition = "2018" - -[package.metadata.docs.rs] -# features that docs.rs will build with -features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"] -rustdoc-args = ["--cfg", "docsrs"] - -[lib] -name = "actix_web" -path = "src/lib.rs" - [workspace] resolver = "2" members = [ - ".", "actix-files", "actix-http-test", "actix-http", @@ -39,93 +9,10 @@ members = [ "actix-test", "actix-web-actors", "actix-web-codegen", + "actix-web", "awc", ] -[features] -default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] - -# Brotli algorithm content-encoding support -compress-brotli = ["actix-http/compress-brotli", "__compress"] -# Gzip and deflate algorithms content-encoding support -compress-gzip = ["actix-http/compress-gzip", "__compress"] -# Zstd algorithm content-encoding support -compress-zstd = ["actix-http/compress-zstd", "__compress"] - -# support for cookies -cookies = ["cookie"] - -# secure cookies feature -secure-cookies = ["cookie/secure"] - -# openssl -openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] - -# rustls -rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] - -# Internal (PRIVATE!) features used to aid testing and checking feature status. -# Don't rely on these whatsoever. They may disappear at anytime. -__compress = [] - -# io-uring feature only avaiable for Linux OSes. -experimental-io-uring = ["actix-server/io-uring"] - -[dependencies] -actix-codec = "0.4.1" -actix-macros = "0.2.3" -actix-rt = "2.6" -actix-server = "2" -actix-service = "2.0.0" -actix-utils = "3.0.0" -actix-tls = { version = "3.0.0", default-features = false, optional = true } - -actix-http = "3.0.0-beta.19" -actix-router = "0.5.0-rc.2" -actix-web-codegen = "0.5.0-rc.1" - -ahash = "0.7" -bytes = "1" -cfg-if = "1" -cookie = { version = "0.16", features = ["percent-encode"], optional = true } -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 = "1" -language-tags = "0.3" -once_cell = "1.5" -log = "0.4" -mime = "0.3" -pin-project-lite = "0.2.7" -regex = "1.4" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -serde_urlencoded = "0.7" -smallvec = "1.6.1" -socket2 = "0.4.0" -time = { version = "0.3", default-features = false, features = ["formatting"] } -url = "2.1" - -[dev-dependencies] -actix-files = "0.6.0-beta.15" -actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.19", features = ["openssl"] } - -brotli = "3.3.3" -const-str = "0.3" -criterion = { version = "0.3", features = ["html_reports"] } -env_logger = "0.9" -flate2 = "1.0.13" -futures-util = { version = "0.3.7", default-features = false, features = ["std"] } -rand = "0.8" -rcgen = "0.8" -rustls-pemfile = "0.2" -static_assertions = "1" -tls-openssl = { package = "openssl", version = "0.10.9" } -tls-rustls = { package = "rustls", version = "0.20.0" } -zstd = "0.9" - [profile.dev] # Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. debug = 0 @@ -142,7 +29,7 @@ actix-http-test = { path = "actix-http-test" } actix-multipart = { path = "actix-multipart" } actix-router = { path = "actix-router" } actix-test = { path = "actix-test" } -actix-web = { path = "." } +actix-web = { path = "actix-web" } actix-web-actors = { path = "actix-web-actors" } actix-web-codegen = { path = "actix-web-codegen" } awc = { path = "awc" } @@ -155,35 +42,3 @@ awc = { path = "awc" } # actix-utils = { path = "../actix-net/actix-utils" } # actix-tls = { path = "../actix-net/actix-tls" } # actix-server = { path = "../actix-net/actix-server" } - -[[test]] -name = "test_server" -required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] - -[[test]] -name = "compression" -required-features = ["compress-brotli", "compress-gzip", "compress-zstd"] - -[[example]] -name = "basic" -required-features = ["compress-gzip"] - -[[example]] -name = "uds" -required-features = ["compress-gzip"] - -[[example]] -name = "on-connect" -required-features = [] - -[[bench]] -name = "server" -harness = false - -[[bench]] -name = "service" -harness = false - -[[bench]] -name = "responder" -harness = false diff --git a/MIGRATION.md b/MIGRATION.md deleted file mode 100644 index 338a04389..000000000 --- a/MIGRATION.md +++ /dev/null @@ -1,677 +0,0 @@ -## Unreleased - -- The default `NormalizePath` behavior now strips trailing slashes by default. This was - previously documented to be the case in v3 but the behavior now matches. The effect is that - routes defined with trailing slashes will become inaccessible when - using `NormalizePath::default()`. As such, calling `NormalizePath::default()` will log a warning. - It is advised that the `new` method be used instead. - - Before: `#[get("/test/")]` - After: `#[get("/test")]` - - Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`. - -- The `type Config` of `FromRequest` was removed. - -- Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd). - By default all compression algorithms are enabled. - To select algorithm you want to include with `middleware::Compress` use following flags: - - `compress-brotli` - - `compress-gzip` - - `compress-zstd` - If you have set in your `Cargo.toml` dedicated `actix-web` features and you still want - to have compression enabled. Please change features selection like bellow: - - Before: `"compress"` - After: `"compress-brotli", "compress-gzip", "compress-zstd"` - - -## 3.0.0 - -- The return type for `ServiceRequest::app_data::()` was changed from returning a `Data` to - simply a `T`. To access a `Data` use `ServiceRequest::app_data::>()`. - -- Cookie handling has been offloaded to the `cookie` crate: - * `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs. - * Some types now require lifetime parameters. - -- The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects - any `actix-web` method previously expecting a time v0.1 input. - -- Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now - result in `SameSite=None` being sent with the response Set-Cookie header. - To create a cookie without a SameSite attribute, remove any calls setting same_site. - -- actix-http support for Actors messages was moved to actix-http crate and is enabled - with feature `actors` - -- content_length function is removed from actix-http. - You can set Content-Length by normally setting the response body or calling no_chunking function. - -- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a - `u64` instead of a `usize`. - -- Code that was using `path.` to access a `web::Path<(A, B, C)>`s elements now needs to use - destructuring or `.into_inner()`. For example: - - ```rust - // Previously: - async fn some_route(path: web::Path<(String, String)>) -> String { - format!("Hello, {} {}", path.0, path.1) - } - - // Now (this also worked before): - async fn some_route(path: web::Path<(String, String)>) -> String { - let (first_name, last_name) = path.into_inner(); - format!("Hello, {} {}", first_name, last_name) - } - // Or (this wasn't previously supported): - async fn some_route(web::Path((first_name, last_name)): web::Path<(String, String)>) -> String { - format!("Hello, {} {}", first_name, last_name) - } - ``` - -- `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one. - It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`, - or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`. - -- `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. - -- `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`. - - -## 2.0.0 - -- `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to - `.await` on `run` method result, in that case it awaits server exit. - -- `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`. - Stored data is available via `HttpRequest::app_data()` method at runtime. - -- Extractor configuration must be registered with `App::app_data()` instead of `App::data()` - -- Sync handlers has been removed. `.to_async()` method has been renamed to `.to()` - replace `fn` with `async fn` to convert sync handler to async - -- `actix_http_test::TestServer` moved to `actix_web::test` module. To start - test server use `test::start()` or `test_start_with_config()` methods - -- `ResponseError` trait has been reafctored. `ResponseError::error_response()` renders - http response. - -- Feature `rust-tls` renamed to `rustls` - - instead of - - ```rust - actix-web = { version = "2.0.0", features = ["rust-tls"] } - ``` - - use - - ```rust - actix-web = { version = "2.0.0", features = ["rustls"] } - ``` - -- Feature `ssl` renamed to `openssl` - - instead of - - ```rust - actix-web = { version = "2.0.0", features = ["ssl"] } - ``` - - use - - ```rust - actix-web = { version = "2.0.0", features = ["openssl"] } - ``` -- `Cors` builder now requires that you call `.finish()` to construct the middleware - -## 1.0.1 - -- Cors middleware has been moved to `actix-cors` crate - - instead of - - ```rust - use actix_web::middleware::cors::Cors; - ``` - - use - - ```rust - use actix_cors::Cors; - ``` - -- Identity middleware has been moved to `actix-identity` crate - - instead of - - ```rust - use actix_web::middleware::identity::{Identity, CookieIdentityPolicy, IdentityService}; - ``` - - use - - ```rust - use actix_identity::{Identity, CookieIdentityPolicy, IdentityService}; - ``` - - -## 1.0.0 - -- Extractor configuration. In version 1.0 this is handled with the new `Data` mechanism for both setting and retrieving the configuration - - instead of - - ```rust - - #[derive(Default)] - struct ExtractorConfig { - config: String, - } - - impl FromRequest for YourExtractor { - type Config = ExtractorConfig; - type Result = Result; - - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - println!("use the config: {:?}", cfg.config); - ... - } - } - - App::new().resource("/route_with_config", |r| { - r.post().with_config(handler_fn, |cfg| { - cfg.0.config = "test".to_string(); - }) - }) - - ``` - - use the HttpRequest to get the configuration like any other `Data` with `req.app_data::()` and set it with the `data()` method on the `resource` - - ```rust - #[derive(Default)] - struct ExtractorConfig { - config: String, - } - - impl FromRequest for YourExtractor { - type Error = Error; - type Future = Result; - type Config = ExtractorConfig; - - fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { - let cfg = req.app_data::(); - println!("config data?: {:?}", cfg.unwrap().role); - ... - } - } - - App::new().service( - resource("/route_with_config") - .data(ExtractorConfig { - config: "test".to_string(), - }) - .route(post().to(handler_fn)), - ) - ``` - -- Resource registration. 1.0 version uses generalized resource - registration via `.service()` method. - - instead of - - ```rust - App.new().resource("/welcome", |r| r.f(welcome)) - ``` - - use App's or Scope's `.service()` method. `.service()` method accepts - object that implements `HttpServiceFactory` trait. By default - actix-web provides `Resource` and `Scope` services. - - ```rust - App.new().service( - web::resource("/welcome") - .route(web::get().to(welcome)) - .route(web::post().to(post_handler)) - ``` - -- Scope registration. - - instead of - - ```rust - let app = App::new().scope("/{project_id}", |scope| { - scope - .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - .resource("/path2", |r| r.f(|_| HttpResponse::Ok())) - .resource("/path3", |r| r.f(|_| HttpResponse::MethodNotAllowed())) - }); - ``` - - use `.service()` for registration and `web::scope()` as scope object factory. - - ```rust - let app = App::new().service( - web::scope("/{project_id}") - .service(web::resource("/path1").to(|| HttpResponse::Ok())) - .service(web::resource("/path2").to(|| HttpResponse::Ok())) - .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed())) - ); - ``` - -- `.with()`, `.with_async()` registration methods have been renamed to `.to()` and `.to_async()`. - - instead of - - ```rust - App.new().resource("/welcome", |r| r.with(welcome)) - ``` - - use `.to()` or `.to_async()` methods - - ```rust - App.new().service(web::resource("/welcome").to(welcome)) - ``` - -- Passing arguments to handler with extractors, multiple arguments are allowed - - instead of - - ```rust - fn welcome((body, req): (Bytes, HttpRequest)) -> ... { - ... - } - ``` - - use multiple arguments - - ```rust - fn welcome(body: Bytes, req: HttpRequest) -> ... { - ... - } - ``` - -- `.f()`, `.a()` and `.h()` handler registration methods have been removed. - Use `.to()` for handlers and `.to_async()` for async handlers. Handler function - must use extractors. - - instead of - - ```rust - App.new().resource("/welcome", |r| r.f(welcome)) - ``` - - use App's `to()` or `to_async()` methods - - ```rust - App.new().service(web::resource("/welcome").to(welcome)) - ``` - -- `HttpRequest` does not provide access to request's payload stream. - - instead of - - ```rust - fn index(req: &HttpRequest) -> Box> { - req - .payload() - .from_err() - .fold((), |_, chunk| { - ... - }) - .map(|_| HttpResponse::Ok().finish()) - .responder() - } - ``` - - use `Payload` extractor - - ```rust - fn index(stream: web::Payload) -> impl Future { - stream - .from_err() - .fold((), |_, chunk| { - ... - }) - .map(|_| HttpResponse::Ok().finish()) - } - ``` - -- `State` is now `Data`. You register Data during the App initialization process - and then access it from handlers either using a Data extractor or using - HttpRequest's api. - - instead of - - ```rust - App.with_state(T) - ``` - - use App's `data` method - - ```rust - App.new() - .data(T) - ``` - - and either use the Data extractor within your handler - - ```rust - use actix_web::web::Data; - - fn endpoint_handler(Data)){ - ... - } - ``` - - .. or access your Data element from the HttpRequest - - ```rust - fn endpoint_handler(req: HttpRequest) { - let data: Option> = req.app_data::(); - } - ``` - - -- AsyncResponder is removed, use `.to_async()` registration method and `impl Future<>` as result type. - - instead of - - ```rust - use actix_web::AsyncResponder; - - fn endpoint_handler(...) -> impl Future{ - ... - .responder() - } - ``` - - .. simply omit AsyncResponder and the corresponding responder() finish method - - -- Middleware - - instead of - - ```rust - let app = App::new() - .middleware(middleware::Logger::default()) - ``` - - use `.wrap()` method - - ```rust - let app = App::new() - .wrap(middleware::Logger::default()) - .route("/index.html", web::get().to(index)); - ``` - -- `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()` - method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead. - - instead of - - ```rust - fn index(req: &HttpRequest) -> Responder { - req.body() - .and_then(|body| { - ... - }) - } - ``` - - use - - ```rust - fn index(body: Bytes) -> Responder { - ... - } - ``` - -- `actix_web::server` module has been removed. To start http server use `actix_web::HttpServer` type - -- StaticFiles and NamedFile have been moved to a separate crate. - - instead of `use actix_web::fs::StaticFile` - - use `use actix_files::Files` - - instead of `use actix_web::fs::Namedfile` - - use `use actix_files::NamedFile` - -- Multipart has been moved to a separate crate. - - instead of `use actix_web::multipart::Multipart` - - use `use actix_multipart::Multipart` - -- Response compression is not enabled by default. - To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`. - -- Session middleware moved to actix-session crate - -- Actors support have been moved to `actix-web-actors` crate - -- Custom Error - - Instead of error_response method alone, ResponseError now provides two methods: error_response and render_response respectively. Where, error_response creates the error response and render_response returns the error response to the caller. - - Simplest migration from 0.7 to 1.0 shall include below method to the custom implementation of ResponseError: - - ```rust - fn render_response(&self) -> HttpResponse { - self.error_response() - } - ``` - -## 0.7.15 - -- The `' '` character is not percent decoded anymore before matching routes. If you need to use it in - your routes, you should use `%20`. - - instead of - - ```rust - fn main() { - let app = App::new().resource("/my index", |r| { - r.method(http::Method::GET) - .with(index); - }); - } - ``` - - use - - ```rust - fn main() { - let app = App::new().resource("/my%20index", |r| { - r.method(http::Method::GET) - .with(index); - }); - } - ``` - -- If you used `AsyncResult::async` you need to replace it with `AsyncResult::future` - - -## 0.7.4 - -- `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple - even for handler with one parameter. - - -## 0.7 - -- `HttpRequest` does not implement `Stream` anymore. If you need to read request payload - use `HttpMessage::payload()` method. - - instead of - - ```rust - fn index(req: HttpRequest) -> impl Responder { - req - .from_err() - .fold(...) - .... - } - ``` - - use `.payload()` - - ```rust - fn index(req: HttpRequest) -> impl Responder { - req - .payload() // <- get request payload stream - .from_err() - .fold(...) - .... - } - ``` - -- [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html) - trait uses `&HttpRequest` instead of `&mut HttpRequest`. - -- Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead. - - instead of - - ```rust - fn index(query: Query<..>, info: Json impl Responder {} - ``` - - use tuple of extractors and use `.with()` for registration: - - ```rust - fn index((query, json): (Query<..>, Json impl Responder {} - ``` - -- `Handler::handle()` uses `&self` instead of `&mut self` - -- `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value - -- Removed deprecated `HttpServer::threads()`, use - [HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead. - -- Renamed `client::ClientConnectorError::Connector` to - `client::ClientConnectorError::Resolver` - -- `Route::with()` does not return `ExtractorConfig`, to configure - extractor use `Route::with_config()` - - instead of - - ```rust - fn main() { - let app = App::new().resource("/index.html", |r| { - r.method(http::Method::GET) - .with(index) - .limit(4096); // <- limit size of the payload - }); - } - ``` - - use - - ```rust - - fn main() { - let app = App::new().resource("/index.html", |r| { - r.method(http::Method::GET) - .with_config(index, |cfg| { // <- register handler - cfg.limit(4096); // <- limit size of the payload - }) - }); - } - ``` - -- `Route::with_async()` does not return `ExtractorConfig`, to configure - extractor use `Route::with_async_config()` - - -## 0.6 - -- `Path` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest` - -- `ws::Message::Close` now includes optional close reason. - `ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed. - -- `HttpServer::threads()` renamed to `HttpServer::workers()`. - -- `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated. - Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead. - -- `HttpRequest::extensions()` returns read only reference to the request's Extension - `HttpRequest::extensions_mut()` returns mutable reference. - -- Instead of - - `use actix_web::middleware::{ - CookieSessionBackend, CookieSessionError, RequestSession, - Session, SessionBackend, SessionImpl, SessionStorage};` - - use `actix_web::middleware::session` - - `use actix_web::middleware::session{CookieSessionBackend, CookieSessionError, - RequestSession, Session, SessionBackend, SessionImpl, SessionStorage};` - -- `FromRequest::from_request()` accepts mutable reference to a request - -- `FromRequest::Result` has to implement `Into>` - -- [`Responder::respond_to()`]( - https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to) - is generic over `S` - -- Use `Query` extractor instead of HttpRequest::query()`. - - ```rust - fn index(q: Query>) -> Result<..> { - ... - } - ``` - - or - - ```rust - let q = Query::>::extract(req); - ``` - -- Websocket operations are implemented as `WsWriter` trait. - you need to use `use actix_web::ws::WsWriter` - - -## 0.5 - -- `HttpResponseBuilder::body()`, `.finish()`, `.json()` - methods return `HttpResponse` instead of `Result` - -- `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version` - moved to `actix_web::http` module - -- `actix_web::header` moved to `actix_web::http::header` - -- `NormalizePath` moved to `actix_web::http` module - -- `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function, - shortcut for `actix_web::server::HttpServer::new()` - -- `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself - -- `StaticFiles::new()`'s show_index parameter removed, use `show_files_listing()` method instead. - -- `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type - -- `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()` - functions should be used instead - -- `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>` - instead of `Result<_, http::Error>` - -- `Application` renamed to a `App` - -- `actix_web::Reply`, `actix_web::Resource` moved to `actix_web::dev` diff --git a/README.md b/README.md deleted file mode 100644 index c3ea70f2c..000000000 --- a/README.md +++ /dev/null @@ -1,109 +0,0 @@ -
-

Actix Web

-

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

-

- -[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) -[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.21)](https://docs.rs/actix-web/4.0.0-beta.21) -![MSRV](https://img.shields.io/badge/rustc-1.54+-ab6000.svg) -![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.21/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.21) -
-[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) -[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) -![downloads](https://img.shields.io/crates/d/actix-web.svg) -[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) - -

-
- -## Features - -- Supports *HTTP/1.x* and *HTTP/2* -- Streaming and pipelining -- Keep-alive and slow requests handling -- Client/server [WebSockets](https://actix.rs/docs/websockets/) support -- Transparent content compression/decompression (br, gzip, deflate, zstd) -- Powerful [request routing](https://actix.rs/docs/url-dispatch/) -- Multipart streams -- Static assets -- SSL support using OpenSSL or Rustls -- Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) -- Includes an async [HTTP client](https://docs.rs/awc/) -- Runs on stable Rust 1.54+ - -## Documentation - -- [Website & User Guide](https://actix.rs) -- [Examples Repository](https://github.com/actix/examples) -- [API Documentation](https://docs.rs/actix-web) -- [API Documentation (master branch)](https://actix.rs/actix-web/actix_web) - -## Example - -Dependencies: - -```toml -[dependencies] -actix-web = "3" -``` - -Code: - -```rust -use actix_web::{get, web, App, HttpServer, Responder}; - -#[get("/{id}/{name}/index.html")] -async fn index(web::Path((id, name)): web::Path<(u32, String)>) -> impl Responder { - format!("Hello {}! id:{}", name, id) -} - -#[actix_web::main] -async fn main() -> std::io::Result<()> { - HttpServer::new(|| App::new().service(index)) - .bind("127.0.0.1:8080")? - .run() - .await -} -``` - -### More examples - -- [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics/) -- [Application State](https://github.com/actix/examples/tree/master/basics/state/) -- [JSON Handling](https://github.com/actix/examples/tree/master/json/json/) -- [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart/) -- [Diesel Integration](https://github.com/actix/examples/tree/master/database_interactions/diesel/) -- [r2d2 Integration](https://github.com/actix/examples/tree/master/database_interactions/r2d2/) -- [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets/websocket/) -- [Tera Templates](https://github.com/actix/examples/tree/master/template_engines/tera/) -- [Askama Templates](https://github.com/actix/examples/tree/master/template_engines/askama/) -- [HTTPS using Rustls](https://github.com/actix/examples/tree/master/security/rustls/) -- [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/security/openssl/) -- [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat/) - -You may consider checking out -[this directory](https://github.com/actix/examples/tree/master/) for more examples. - -## Benchmarks - -One of the fastest web frameworks available according to the -[TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite). - -## License - -This project is licensed under either of - -- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or - [http://www.apache.org/licenses/LICENSE-2.0]) -- MIT license ([LICENSE-MIT](LICENSE-MIT) or - [http://opensource.org/licenses/MIT]) - -at your option. - -## Code of Conduct - -Contribution to the actix-web repo is organized under the terms of the Contributor Covenant. -The Actix team promises to intervene to uphold that code of conduct. diff --git a/README.md b/README.md new file mode 120000 index 000000000..16b750c17 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +actix-web/README.md \ No newline at end of file diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index fa9647e62..b127cd9ea 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,6 +1,25 @@ # Changes -## Unreleased - 2021-xx-xx +## Unreleased - 2022-xx-xx +- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. + + +## 0.6.1 - 2022-06-11 +- Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021] +- Update `tokio-uring` dependency to `0.3`. +- Audio files now use `Content-Disposition: inline` instead of `attachment`. [#2645] +- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency. + +[#2021]: https://github.com/actix/actix-web/pull/2021 +[#2645]: https://github.com/actix/actix-web/pull/2645 + + +## 0.6.0 - 2022-02-25 +- No significant changes since `0.6.0-beta.16`. + + +## 0.6.0-beta.16 - 2022-01-31 +- No significant changes since `0.6.0-beta.15`. ## 0.6.0-beta.15 - 2022-01-21 diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 68a3399a5..02543095f 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.0-beta.15" +version = "0.6.1" 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.19" +actix-http = "3" actix-service = "2" actix-utils = "3" -actix-web = { version = "4.0.0-beta.21", default-features = false } +actix-web = { version = "4", default-features = false } askama_escape = "0.10" bitflags = "1" @@ -39,10 +39,13 @@ mime_guess = "2.0.1" percent-encoding = "2.1" pin-project-lite = "0.2.7" -tokio-uring = { version = "0.2", optional = true, features = ["bytes"] } +# experimental-io-uring +[target.'cfg(target_os = "linux")'.dependencies] +tokio-uring = { version = "0.3", optional = true, features = ["bytes"] } +actix-server = { version = "2.1", optional = true } # ensure matching tokio-uring versions [dev-dependencies] -actix-rt = "2.2" -actix-test = "0.1.0-beta.11" -actix-web = "4.0.0-beta.21" +actix-rt = "2.7" +actix-test = "0.1.0-beta.13" +actix-web = "4" tempfile = "3.2" diff --git a/actix-files/README.md b/actix-files/README.md index 8395957c5..35db41c9a 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,16 +3,16 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.15)](https://docs.rs/actix-files/0.6.0-beta.15) -[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.1)](https://docs.rs/actix-files/0.6.1) +![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.15/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.15) +[![dependency status](https://deps.rs/crate/actix-files/0.6.1/status.svg)](https://deps.rs/crate/actix-files/0.6.1) [![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) ## Documentation & Resources -- [API Documentation](https://docs.rs/actix-files/) -- [Example Project](https://github.com/actix/examples/tree/master/basics/static_index) +- [API Documentation](https://docs.rs/actix-files) +- [Example Project](https://github.com/actix/examples/tree/master/basics/static-files) - Minimum Supported Rust Version (MSRV): 1.54 diff --git a/actix-files/src/chunked.rs b/actix-files/src/chunked.rs index 3ee2ee072..241b4dccb 100644 --- a/actix-files/src/chunked.rs +++ b/actix-files/src/chunked.rs @@ -81,7 +81,7 @@ async fn chunked_read_file_callback( ) -> Result<(File, Bytes), Error> { use io::{Read as _, Seek as _}; - let res = actix_web::rt::task::spawn_blocking(move || { + let res = actix_web::web::block(move || { let mut buf = Vec::with_capacity(max_bytes); file.seek(io::SeekFrom::Start(offset))?; @@ -94,8 +94,7 @@ async fn chunked_read_file_callback( Ok((file, Bytes::from(buf))) } }) - .await - .map_err(|_| actix_web::error::BlockingError)??; + .await??; Ok(res) } diff --git a/actix-files/src/directory.rs b/actix-files/src/directory.rs index 26225ea5c..32dd6365b 100644 --- a/actix-files/src/directory.rs +++ b/actix-files/src/directory.rs @@ -75,7 +75,7 @@ pub(crate) fn directory_listing( if dir.is_visible(&entry) { let entry = entry.unwrap(); let p = match entry.path().strip_prefix(&dir.path) { - Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace("\\", "/"), + Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace('\\', "/"), Ok(p) => base.join(p).to_string_lossy().into_owned(), Err(_) => continue, }; diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 41113f2ab..40327e5e8 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -364,20 +364,43 @@ mod tests { ); } + #[allow(deprecated)] #[actix_rt::test] - async fn test_named_file_status_code_text() { - let mut file = NamedFile::open_async("Cargo.toml") + async fn status_code_customize_same_output() { + let file1 = NamedFile::open_async("Cargo.toml") .await .unwrap() .set_status_code(StatusCode::NOT_FOUND); + + let file2 = NamedFile::open_async("Cargo.toml") + .await + .unwrap() + .customize() + .with_status(StatusCode::NOT_FOUND); + + let req = TestRequest::default().to_http_request(); + let res1 = file1.respond_to(&req); + let res2 = file2.respond_to(&req); + + assert_eq!(res1.status(), StatusCode::NOT_FOUND); + assert_eq!(res2.status(), StatusCode::NOT_FOUND); + } + + #[actix_rt::test] + async fn test_named_file_status_code_text() { + let mut file = NamedFile::open_async("Cargo.toml").await.unwrap(); + { file.file(); let _f: &File = &file; } + { let _f: &mut File = &mut file; } + let file = file.customize().with_status(StatusCode::NOT_FOUND); + let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req); assert_eq!( diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 14495e660..5580e6f7e 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -23,6 +23,7 @@ use actix_web::{ use bitflags::bitflags; use derive_more::{Deref, DerefMut}; use futures_core::future::LocalBoxFuture; +use mime::Mime; use mime_guess::from_path; use crate::{encoding::equiv_utf8_text, range::HttpRange}; @@ -76,8 +77,8 @@ pub struct NamedFile { pub(crate) md: Metadata, pub(crate) flags: Flags, pub(crate) status_code: StatusCode, - pub(crate) content_type: mime::Mime, - pub(crate) content_disposition: header::ContentDisposition, + pub(crate) content_type: Mime, + pub(crate) content_disposition: ContentDisposition, pub(crate) encoding: Option, } @@ -96,18 +97,18 @@ impl NamedFile { /// /// # Examples /// ```ignore + /// use std::{ + /// io::{self, Write as _}, + /// env, + /// fs::File + /// }; /// use actix_files::NamedFile; - /// use std::io::{self, Write}; - /// use std::env; - /// use std::fs::File; /// - /// fn main() -> io::Result<()> { - /// let mut file = File::create("foo.txt")?; - /// file.write_all(b"Hello, world!")?; - /// let named_file = NamedFile::from_file(file, "bar.txt")?; - /// # std::fs::remove_file("foo.txt"); - /// Ok(()) - /// } + /// let mut file = File::create("foo.txt")?; + /// file.write_all(b"Hello, world!")?; + /// let named_file = NamedFile::from_file(file, "bar.txt")?; + /// # std::fs::remove_file("foo.txt"); + /// Ok(()) /// ``` pub fn from_file>(file: File, path: P) -> io::Result { let path = path.as_ref().to_path_buf(); @@ -128,7 +129,7 @@ impl NamedFile { let ct = from_path(&path).first_or_octet_stream(); let disposition = match ct.type_() { - mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, + mime::IMAGE | mime::TEXT | mime::AUDIO | mime::VIDEO => DispositionType::Inline, mime::APPLICATION => match ct.subtype() { mime::JAVASCRIPT | mime::JSON => DispositionType::Inline, name if name == "wasm" => DispositionType::Inline, @@ -211,8 +212,8 @@ impl NamedFile { /// Attempts to open a file asynchronously in read-only mode. /// - /// When the `experimental-io-uring` crate feature is enabled, this will be async. - /// Otherwise, it will be just like [`open`][Self::open]. + /// When the `experimental-io-uring` crate feature is enabled, this will be async. Otherwise, it + /// will behave just like `open`. /// /// # Examples /// ``` @@ -237,13 +238,13 @@ impl NamedFile { Self::from_file(file, path) } - /// Returns reference to the underlying `File` object. + /// Returns reference to the underlying file object. #[inline] pub fn file(&self) -> &File { &self.file } - /// Retrieve the path of this file. + /// Returns the filesystem path to this file. /// /// # Examples /// ``` @@ -261,16 +262,53 @@ impl NamedFile { self.path.as_path() } - /// Set response **Status Code** + /// Returns the time the file was last modified. + /// + /// Returns `None` only on unsupported platforms; see [`std::fs::Metadata::modified()`]. + /// Therefore, it is usually safe to unwrap this. + #[inline] + pub fn modified(&self) -> Option { + self.modified + } + + /// Returns the filesystem metadata associated with this file. + #[inline] + pub fn metadata(&self) -> &Metadata { + &self.md + } + + /// Returns the `Content-Type` header that will be used when serving this file. + #[inline] + pub fn content_type(&self) -> &Mime { + &self.content_type + } + + /// Returns the `Content-Disposition` that will be used when serving this file. + #[inline] + pub fn content_disposition(&self) -> &ContentDisposition { + &self.content_disposition + } + + /// Returns the `Content-Encoding` that will be used when serving this file. + /// + /// A return value of `None` indicates that the content is not already using a compressed + /// representation and may be subject to compression downstream. + #[inline] + pub fn content_encoding(&self) -> Option { + self.encoding + } + + /// Set response status code. + #[deprecated(since = "0.7.0", note = "Prefer `Responder::customize()`.")] pub fn set_status_code(mut self, status: StatusCode) -> Self { self.status_code = status; self } - /// Set the MIME Content-Type for serving this file. By default the Content-Type is inferred - /// from the filename extension. + /// Sets the `Content-Type` header that will be used when serving this file. By default the + /// `Content-Type` is inferred from the filename extension. #[inline] - pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self { + pub fn set_content_type(mut self, mime_type: Mime) -> Self { self.content_type = mime_type; self } @@ -283,24 +321,26 @@ impl NamedFile { /// filename is taken from the path provided in the `open` method after converting it to UTF-8 /// (using `to_string_lossy`). #[inline] - pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self { + pub fn set_content_disposition(mut self, cd: ContentDisposition) -> Self { self.content_disposition = cd; self.flags.insert(Flags::CONTENT_DISPOSITION); self } - /// Disable `Content-Disposition` header. + /// Disables `Content-Disposition` header. /// - /// By default Content-Disposition` header is enabled. + /// By default, the `Content-Disposition` header is sent. #[inline] pub fn disable_content_disposition(mut self) -> Self { self.flags.remove(Flags::CONTENT_DISPOSITION); self } - /// Set content encoding for serving this file + /// Sets content encoding for this file. /// - /// Must be used with [`actix_web::middleware::Compress`] to take effect. + /// This prevents the `Compress` middleware from modifying the file contents and signals to + /// browsers/clients how to decode it. For example, if serving a compressed HTML file (e.g., + /// `index.html.gz`) then use `.set_content_encoding(ContentEncoding::Gzip)`. #[inline] pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { self.encoding = Some(enc); diff --git a/actix-files/src/path_buf.rs b/actix-files/src/path_buf.rs index f7f7cdab6..9ee1338c6 100644 --- a/actix-files/src/path_buf.rs +++ b/actix-files/src/path_buf.rs @@ -59,6 +59,8 @@ impl PathBufWrap { continue; } else if cfg!(windows) && segment.contains('\\') { return Err(UriSegmentError::BadChar('\\')); + } else if cfg!(windows) && segment.contains(':') { + return Err(UriSegmentError::BadChar(':')); } else { buf.push(segment) } @@ -66,7 +68,11 @@ impl PathBufWrap { // make sure we agree with stdlib parser for (i, component) in buf.components().enumerate() { - assert!(matches!(component, Component::Normal(_))); + assert!( + matches!(component, Component::Normal(_)), + "component `{:?}` is not normal", + component + ); assert!(i < segment_count); } @@ -159,4 +165,26 @@ mod tests { PathBuf::from_iter(vec!["etc/passwd"]) ); } + + #[test] + #[cfg_attr(windows, should_panic)] + fn windows_drive_traversal() { + // detect issues in windows that could lead to path traversal + // see "] description = "Various helpers for Actix applications to use during testing" keywords = ["http", "web", "framework", "async", "futures"] @@ -29,13 +29,13 @@ default = [] openssl = ["tls-openssl", "awc/openssl"] [dependencies] -actix-service = "2.0.0" -actix-codec = "0.4.1" -actix-tls = "3.0.0" -actix-utils = "3.0.0" +actix-service = "2" +actix-codec = "0.5" +actix-tls = "3" +actix-utils = "3" actix-rt = "2.2" actix-server = "2" -awc = { version = "3.0.0-beta.19", default-features = false } +awc = { version = "3", default-features = false } base64 = "0.13" bytes = "1" @@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tokio = { version = "1.8.4", features = ["sync"] } [dev-dependencies] -actix-web = { version = "4.0.0-beta.21", default-features = false, features = ["cookies"] } -actix-http = "3.0.0-beta.19" +actix-web = { version = "4", default-features = false, features = ["cookies"] } +actix-http = "3" diff --git a/actix-http-test/README.md b/actix-http-test/README.md index 10c04b368..9429bb760 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.11)](https://docs.rs/actix-http-test/3.0.0-beta.11) -[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) +[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.13)](https://docs.rs/actix-http-test/3.0.0-beta.13) +![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg) ![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.11/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.11) +[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.13/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.13) [![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/CHANGES.md b/actix-http/CHANGES.md index 115cb9c47..480f63819 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,11 +1,407 @@ # Changes -## Unreleased - 2021-xx-xx +## Unreleased - 2022-xx-xx - Rename `ws::{Item => ContinuationItem}`. [#????] [#????]: https://github.com/actix/actix-web/pull/???? +## 3.2.1 - 2022-07-02 +### Fixed +- Fix parsing ambiguity in Transfer-Encoding and Content-Length headers for HTTP/1.0 requests. [#2794] + +[#2794]: https://github.com/actix/actix-web/pull/2794 + + +## 3.2.0 - 2022-06-30 +### Changed +- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. + +### Fixed +- Websocket parser no longer throws endless overflow errors after receiving an oversized frame. [#2790] +- Retain previously set Vary headers when using compression encoder. [#2798] + +[#2790]: https://github.com/actix/actix-web/pull/2790 +[#2798]: https://github.com/actix/actix-web/pull/2798 + + +## 3.1.0 - 2022-06-11 +### Changed +- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency. + +### Fixed +- Revert broken fix in [#2624] that caused erroneous 500 error responses. Temporarily re-introduces [#2357] bug. [#2779] + +[#2357]: https://github.com/actix/actix-web/issues/2357 +[#2624]: https://github.com/actix/actix-web/issues/2624 +[#2779]: https://github.com/actix/actix-web/issues/2779 + + +## 3.0.4 - 2022-03-09 +### Fixed +- Document on docs.rs with `ws` feature enabled. + + +## 3.0.3 - 2022-03-08 +### Fixed +- Allow spaces between header name and colon when parsing responses. [#2684] + +[#2684]: https://github.com/actix/actix-web/issues/2684 + + +## 3.0.2 - 2022-03-05 +### Fixed +- Fix encoding camel-case header names with more than one hyphen. [#2683] + +[#2683]: https://github.com/actix/actix-web/issues/2683 + + +## 3.0.1 - 2022-03-04 +- Fix panic in H1 dispatcher when pipelining is used with keep-alive. [#2678] + +[#2678]: https://github.com/actix/actix-web/issues/2678 + +## 3.0.0 - 2022-02-25 +### Dependencies +- Updated `actix-*` to Tokio v1-based versions. [#1813] +- Updated `bytes` to `1.0`. [#1813] +- Updated `h2` to `0.3`. [#1813] +- Updated `rustls` to `0.20.0`. [#2414] +- Updated `language-tags` to `0.3`. +- Updated `tokio` to `1`. + +### Added +- Crate Features: + - `ws`; disabled by default. [#2618] + - `http2`; disabled by default. [#2618] + - `compress-brotli`; disabled by default. [#2618] + - `compress-gzip`; disabled by default. [#2618] + - `compress-zstd`; disabled by default. [#2618] +- Functions: + - `body::to_bytes` for async collecting message body into Bytes. [#2158] +- Traits: + - `TryIntoHeaderPair`; allows using typed and untyped headers in the same methods. [#1869] +- Types: + - `body::BoxBody`; a boxed message body with boxed errors. [#2183] + - `body::EitherBody` enum. [#2468] + - `body::None` struct. [#2468] + - Re-export `http` crate's `Error` type as `error::HttpError`. [#2171] +- Variants: + - `ContentEncoding::Zstd` along with . [#2244] + - `Protocol::Http3` for future compatibility and also mark `#[non_exhaustive]`. [00ba8d55] +- Methods: + - `ContentEncoding::to_header_value()`. [#2501] + - `header::QualityItem::{max, min}()`. [#2486] + - `header::QualityItem::zero()` that uses `Quality::ZERO`. [#2501] + - `HeaderMap::drain()` as an efficient draining iterator. [#1964] + - `HeaderMap::len_keys()` has the behavior of the old `len` method. [#1964] + - `MessageBody::boxed` trait method for wrapping boxing types efficiently. [#2520] + - `MessageBody::try_into_bytes` trait method, with default implementation, for optimizations on body types that complete in exactly one poll. [#2522] + - `Request::conn_data()`. [#2491] + - `Request::take_conn_data()`. [#2491] + - `Request::take_req_data()`. [#2487] + - `Response::{ok, bad_request, not_found, internal_server_error}()`. [#2159] + - `Response::into_body()` that consumes response and returns body type. [#2201] + - `Response::map_into_boxed_body()`. [#2468] + - `ResponseBuilder::append_header()` method which allows using typed and untyped headers. [#1869] + - `ResponseBuilder::insert_header()` method which allows using typed and untyped headers. [#1869] + - `ResponseHead::set_camel_case_headers()`. [#2587] + - `TestRequest::insert_header()` method which allows using typed and untyped headers. [#1869] +- Implementations: + - Implement `Clone for ws::HandshakeError`. [#2468] + - Implement `Clone` for `body::AnyBody where S: Clone`. [#2448] + - Implement `Clone` for `RequestHead`. [#2487] + - Implement `Clone` for `ResponseHead`. [#2585] + - Implement `Copy` for `QualityItem where T: Copy`. [#2501] + - Implement `Default` for `ContentEncoding`. [#1912] + - Implement `Default` for `HttpServiceBuilder`. [#2611] + - Implement `Default` for `KeepAlive`. [#2611] + - Implement `Default` for `Response`. [#2201] + - Implement `Default` for `ws::Codec`. [#1920] + - Implement `Display` for `header::Quality`. [#2486] + - Implement `Eq` for `header::ContentEncoding`. [#2501] + - Implement `ExactSizeIterator` and `FusedIterator` for all `HeaderMap` iterators. [#2470] + - Implement `From` for `KeepAlive`. [#2611] + - Implement `From>` for `KeepAlive`. [#2611] + - Implement `From>` for `Response>`. [#2625] + - Implement `FromStr` for `ContentEncoding`. [#1912] + - Implement `Header` for `ContentEncoding`. [#1912] + - Implement `IntoHeaderValue` for `ContentEncoding`. [#1912] + - Implement `IntoIterator` for `HeaderMap`. [#1964] + - Implement `MessageBody` for `bytestring::ByteString`. [#2468] + - Implement `MessageBody` for `Pin> where T: MessageBody`. [#2152] +- Misc: + - Re-export `StatusCode`, `Method`, `Version` and `Uri` at the crate root. [#2171] + - Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171] + - `Quality::ZERO` associated constant equivalent to `q=0`. [#2501] + - `header::Quality::{MAX, MIN}` associated constants equivalent to `q=1` and `q=0.001`, respectively. [#2486] + - Timeout for canceling HTTP/2 server side connection handshake. Configurable with `ServiceConfig::client_timeout`; defaults to 5 seconds. [#2483] + - `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920] + +### Changed +- Traits: + - Rename `IntoHeaderValue => TryIntoHeaderValue`. [#2510] + - `MessageBody` now has an associated `Error` type. [#2183] +- Types: + - `Protocol` enum is now marked `#[non_exhaustive]`. + - `error::DispatcherError` enum is now marked `#[non_exhaustive]`. [#2624] + - `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377] + - Error enums are marked `#[non_exhaustive]`. [#2161] + - Rename `PayloadStream` to `BoxedPayloadStream`. [#2545] + - The body type parameter of `Response` no longer has a default. [#2152] +- Enum Variants: + - Rename `ContentEncoding::{Br => Brotli}`. [#2501] + - `Payload` inner fields are now named. [#2545] + - `ws::Message::Text` now contains a `bytestring::ByteString`. [#1864] +- Methods: + - Rename `ServiceConfig::{client_timer_expire => client_request_deadline}`. [#2611] + - Rename `ServiceConfig::{client_disconnect_timer => client_disconnect_deadline}`. [#2611] + - Rename `h1::Codec::{keepalive => keep_alive}`. [#2611] + - Rename `h1::Codec::{keepalive_enabled => keep_alive_enabled}`. [#2611] + - Rename `h1::ClientCodec::{keepalive => keep_alive}`. [#2611] + - Rename `h1::ClientPayloadCodec::{keepalive => keep_alive}`. [#2611] + - Rename `header::EntityTag::{weak => new_weak, strong => new_strong}`. [#2565] + - Rename `TryIntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std `TryInto` trait. [#1894] + - Deadline methods in `ServiceConfig` now return `std::time::Instant`s instead of Tokio's wrapper type. [#2611] + - Places in `Response` where `ResponseBody` was received or returned now simply use `B`. [#2201] + - `encoding::Encoder::response` now returns `AnyBody>`. [#2448] + - `Extensions::insert` returns replaced item. [#1904] + - `HeaderMap::get_all` now returns a `std::slice::Iter`. [#2527] + - `HeaderMap::insert` now returns iterator of removed values. [#1964] + - `HeaderMap::len` now returns number of values instead of number of keys. [#1964] + - `HeaderMap::remove` now returns iterator of removed values. [#1964] + - `ResponseBuilder::body(B)` now returns `Response>`. [#2468] + - `ResponseBuilder::content_type` now takes an `impl TryIntoHeaderValue` to support using typed `mime` types. [#1894] + - `ResponseBuilder::finish()` now returns `Response>`. [#2468] + - `ResponseBuilder::json` now takes `impl Serialize`. [#2052] + - `ResponseBuilder::message_body` now returns a `Result`. [#2201]∑ + - `ServiceConfig::keep_alive` now returns a `KeepAlive`. [#2611] + - `ws::hash_key` now returns array. [#2035] +- Trait Implementations: + - Implementation of `Stream` for `Payload` no longer requires the `Stream` variant be `Unpin`. [#2545] + - Implementation of `Future` for `h1::SendResponse` no longer requires the body type be `Unpin`. [#2545] + - Implementation of `Stream` for `encoding::Decoder` no longer requires the stream type be `Unpin`. [#2545] + - Implementation of `From` for error types now return a `Response`. [#2468] +- Misc: + - `header` module is now public. [#2171] + - `uri` module is now public. [#2171] + - Request-local data container is no longer part of a `RequestHead`. Instead it is a distinct part of a `Request`. [#2487] + - All error trait bounds in server service builders have changed from `Into` to `Into>`. [#2253] + - All error trait bounds in message body and stream impls changed from `Into` to `Into>`. [#2253] + - Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467] + - Connection data set through the `on_connect_ext` callbacks is now accessible only from the new `Request::conn_data()` method. [#2491] + - Brotli (de)compression support is now provided by the `brotli` crate. [#2538] + - Minimum supported Rust version (MSRV) is now 1.54. + +### Fixed +- A `Vary` header is now correctly sent along with compressed content. [#2501] +- HTTP/1.1 dispatcher correctly uses client request timeout. [#2611] +- Fixed issue where handlers that took payload but then dropped without reading it to EOF it would cause keep-alive connections to become stuck. [#2624] +- `ContentEncoding`'s `Identity` variant can now be parsed from a string. [#2501] +- `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuration parameter. [#2226] +- Remove unnecessary `Into` bound on `Encoder` body types. [#2375] +- Remove unnecessary `Unpin` bound on `ResponseBuilder::streaming`. [#2253] +- `BodyStream` and `SizedStream` are no longer restricted to `Unpin` types. [#2152] +- Fixed slice creation pointing to potential uninitialized data on h1 encoder. [#2364] +- Fixed quality parse error in Accept-Encoding header. [#2344] + +### Removed +- Crate Features: + - `compress` feature. [#2065] + - `cookies` feature. [#2065] + - `trust-dns` feature. [#2425] + - `actors` optional feature and trait implementation for `actix` types. [#1969] +- Functions: + - `header::qitem` helper. Replaced with `header::QualityItem::max`. [#2486] +- Types: + - `body::Body`; replaced with `EitherBody` and `BoxBody`. [#2468] + - `body::ResponseBody`. [#2446] + - `ConnectError::SslHandshakeError` and re-export of `HandshakeError`. Due to the removal of this type from `tokio-openssl` crate. OpenSSL handshake error now returns `ConnectError::SslError`. [#1813] + - `error::Canceled` re-export. [#1994] + - `error::Result` type alias. [#2201] + - `error::BlockingError` [#2660] + - `InternalError` and all the error types it constructed were moved up to `actix-web`. [#2215] + - Typed HTTP headers; they have moved up to `actix-web`. [2094] + - Re-export of `http` crate's `HeaderMap` types in addition to ours. [#2171] +- Enum Variants: + - `body::BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446] + - `ContentEncoding::Auto`. [#2501] + - `EncoderError::Boxed`. [#2446] +- Methods: + - `ContentEncoding::is_compression()`. [#2501] + - `h1::Payload::readany()`. [#2545] + - `HttpMessage::cookie[s]()` trait methods. [#2065] + - `HttpServiceBuilder::new()`; use `default` instead. [#2611] + - `on_connect` (previously deprecated) methods have been removed; use `on_connect_ext`. [#1857] + - `Response::build_from()`. [#2159] + - `Response::error()` [#2205] + - `Response::take_body()` and old `Response::into_body()` method that casted body type. [#2201] + - `Response`'s status code builders. [#2159] + - `ResponseBuilder::{if_true, if_some}()` (previously deprecated). [#2148] + - `ResponseBuilder::{set, set_header}()`; use `ResponseBuilder::insert_header()`. [#1869] + - `ResponseBuilder::extensions[_mut]()`. [#2585] + - `ResponseBuilder::header()`; use `ResponseBuilder::append_header()`. [#1869] + - `ResponseBuilder::json()`. [#2148] + - `ResponseBuilder::json2()`. [#1903] + - `ResponseBuilder::streaming()`. [#2468] + - `ResponseHead::extensions[_mut]()`. [#2585] + - `ServiceConfig::{client_timer, keep_alive_timer}()`. [#2611] + - `TestRequest::with_hdr()`; use `TestRequest::default().insert_header()`. [#1869] + - `TestRequest::with_header()`; use `TestRequest::default().insert_header()`. [#1869] +- Trait implementations: + - Implementation of `Copy` for `ws::Codec`. [#1920] + - Implementation of `From> for KeepAlive`; use `Duration`s instead. [#2611] + - Implementation of `From` for `Body`. [#2148] + - Implementation of `From for KeepAlive`; use `Duration`s instead. [#2611] + - Implementation of `Future` for `Response`. [#2201] + - Implementation of `Future` for `ResponseBuilder`. [#2468] + - Implementation of `Into` for `Response`. [#2215] + - Implementation of `Into` for `ResponseBuilder`. [#2215] + - Implementation of `ResponseError` for `actix_utils::timeout::TimeoutError`. [#2127] + - Implementation of `ResponseError` for `CookieParseError`. [#2065] + - Implementation of `TryFrom` for `header::Quality`. [#2486] +- Misc: + - `http` module; most everything it contained is exported at the crate root. [#2488] + - `cookies` module (re-export). [#2065] + - `client` module. Connector types now live in `awc`. [#2425] + - `error` field from `Response`. [#2205] + - `downcast` and `downcast_get_type_id` macros. [#2291] + - Down-casting for `MessageBody` types; use standard `Any` trait. [#2183] + + +[#1813]: https://github.com/actix/actix-web/pull/1813 +[#1845]: https://github.com/actix/actix-web/pull/1845 +[#1857]: https://github.com/actix/actix-web/pull/1857 +[#1864]: https://github.com/actix/actix-web/pull/1864 +[#1869]: https://github.com/actix/actix-web/pull/1869 +[#1878]: https://github.com/actix/actix-web/pull/1878 +[#1894]: https://github.com/actix/actix-web/pull/1894 +[#1903]: https://github.com/actix/actix-web/pull/1903 +[#1904]: https://github.com/actix/actix-web/pull/1904 +[#1912]: https://github.com/actix/actix-web/pull/1912 +[#1920]: https://github.com/actix/actix-web/pull/1920 +[#1964]: https://github.com/actix/actix-web/pull/1964 +[#1969]: https://github.com/actix/actix-web/pull/1969 +[#1981]: https://github.com/actix/actix-web/pull/1981 +[#1994]: https://github.com/actix/actix-web/pull/1994 +[#2035]: https://github.com/actix/actix-web/pull/2035 +[#2052]: https://github.com/actix/actix-web/pull/2052 +[#2065]: https://github.com/actix/actix-web/pull/2065 +[#2094]: https://github.com/actix/actix-web/pull/2094 +[#2127]: https://github.com/actix/actix-web/pull/2127 +[#2148]: https://github.com/actix/actix-web/pull/2148 +[#2152]: https://github.com/actix/actix-web/pull/2152 +[#2158]: https://github.com/actix/actix-web/pull/2158 +[#2159]: https://github.com/actix/actix-web/pull/2159 +[#2161]: https://github.com/actix/actix-web/pull/2161 +[#2171]: https://github.com/actix/actix-web/pull/2171 +[#2183]: https://github.com/actix/actix-web/pull/2183 +[#2196]: https://github.com/actix/actix-web/pull/2196 +[#2201]: https://github.com/actix/actix-web/pull/2201 +[#2205]: https://github.com/actix/actix-web/pull/2205 +[#2215]: https://github.com/actix/actix-web/pull/2215 +[#2244]: https://github.com/actix/actix-web/pull/2244 +[#2250]: https://github.com/actix/actix-web/pull/2250 +[#2253]: https://github.com/actix/actix-web/pull/2253 +[#2291]: https://github.com/actix/actix-web/pull/2291 +[#2344]: https://github.com/actix/actix-web/pull/2344 +[#2364]: https://github.com/actix/actix-web/pull/2364 +[#2375]: https://github.com/actix/actix-web/pull/2375 +[#2377]: https://github.com/actix/actix-web/pull/2377 +[#2414]: https://github.com/actix/actix-web/pull/2414 +[#2425]: https://github.com/actix/actix-web/pull/2425 +[#2442]: https://github.com/actix/actix-web/pull/2442 +[#2446]: https://github.com/actix/actix-web/pull/2446 +[#2448]: https://github.com/actix/actix-web/pull/2448 +[#2456]: https://github.com/actix/actix-web/pull/2456 +[#2467]: https://github.com/actix/actix-web/pull/2467 +[#2468]: https://github.com/actix/actix-web/pull/2468 +[#2470]: https://github.com/actix/actix-web/pull/2470 +[#2474]: https://github.com/actix/actix-web/pull/2474 +[#2483]: https://github.com/actix/actix-web/pull/2483 +[#2486]: https://github.com/actix/actix-web/pull/2486 +[#2487]: https://github.com/actix/actix-web/pull/2487 +[#2488]: https://github.com/actix/actix-web/pull/2488 +[#2491]: https://github.com/actix/actix-web/pull/2491 +[#2497]: https://github.com/actix/actix-web/pull/2497 +[#2501]: https://github.com/actix/actix-web/pull/2501 +[#2510]: https://github.com/actix/actix-web/pull/2510 +[#2520]: https://github.com/actix/actix-web/pull/2520 +[#2522]: https://github.com/actix/actix-web/pull/2522 +[#2527]: https://github.com/actix/actix-web/pull/2527 +[#2538]: https://github.com/actix/actix-web/pull/2538 +[#2545]: https://github.com/actix/actix-web/pull/2545 +[#2565]: https://github.com/actix/actix-web/pull/2565 +[#2585]: https://github.com/actix/actix-web/pull/2585 +[#2587]: https://github.com/actix/actix-web/pull/2587 +[#2611]: https://github.com/actix/actix-web/pull/2611 +[#2618]: https://github.com/actix/actix-web/pull/2618 +[#2624]: https://github.com/actix/actix-web/pull/2624 +[#2625]: https://github.com/actix/actix-web/pull/2625 +[#2660]: https://github.com/actix/actix-web/pull/2660 +[00ba8d55]: https://github.com/actix/actix-web/commit/00ba8d55492284581695d824648590715a8bd386 + + +
+3.0.0 Pre-Releases + +## 3.0.0-rc.4 - 2022-02-22 +### Fixed +- Fix h1 dispatcher panic. [1ce58ecb] + +[1ce58ecb]: https://github.com/actix/actix-web/commit/1ce58ecb305c60e51db06e6c913b7a1344e229ca + + +## 3.0.0-rc.3 - 2022-02-16 +- No significant changes since `3.0.0-rc.2`. + + +## 3.0.0-rc.2 - 2022-02-08 +### Added +- Implement `From>` for `Response>`. [#2625] + +### Changed +- `error::DispatcherError` enum is now marked `#[non_exhaustive]`. [#2624] + +### Fixed +- Issue where handlers that took payload but then dropped without reading it to EOF it would cause keep-alive connections to become stuck. [#2624] + +[#2624]: https://github.com/actix/actix-web/pull/2624 +[#2625]: https://github.com/actix/actix-web/pull/2625 + + +## 3.0.0-rc.1 - 2022-01-31 +### Added +- Implement `Default` for `KeepAlive`. [#2611] +- Implement `From` for `KeepAlive`. [#2611] +- Implement `From>` for `KeepAlive`. [#2611] +- Implement `Default` for `HttpServiceBuilder`. [#2611] +- Crate `ws` feature flag, disabled by default. [#2618] +- Crate `http2` feature flag, disabled by default. [#2618] + +### Changed +- Rename `ServiceConfig::{client_timer_expire => client_request_deadline}`. [#2611] +- Rename `ServiceConfig::{client_disconnect_timer => client_disconnect_deadline}`. [#2611] +- Deadline methods in `ServiceConfig` now return `std::time::Instant`s instead of Tokio's wrapper type. [#2611] +- Rename `h1::Codec::{keepalive => keep_alive}`. [#2611] +- Rename `h1::Codec::{keepalive_enabled => keep_alive_enabled}`. [#2611] +- Rename `h1::ClientCodec::{keepalive => keep_alive}`. [#2611] +- Rename `h1::ClientPayloadCodec::{keepalive => keep_alive}`. [#2611] +- `ServiceConfig::keep_alive` now returns a `KeepAlive`. [#2611] + +### Fixed +- HTTP/1.1 dispatcher correctly uses client request timeout. [#2611] + +### Removed +- `ServiceConfig::{client_timer, keep_alive_timer}`. [#2611] +- `impl From for KeepAlive`; use `Duration`s instead. [#2611] +- `impl From> for KeepAlive`; use `Duration`s instead. [#2611] +- `HttpServiceBuilder::new`; use `default` instead. [#2611] + +[#2611]: https://github.com/actix/actix-web/pull/2611 +[#2618]: https://github.com/actix/actix-web/pull/2618 + + ## 3.0.0-beta.19 - 2022-01-21 ### Added - Response headers can be sent as camel case using `res.head_mut().set_camel_case_headers(true)`. [#2587] @@ -51,7 +447,7 @@ ## 3.0.0-beta.17 - 2021-12-27 -### Changes +### Changed - `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] @@ -274,7 +670,7 @@ - `Response::{ok, bad_request, not_found, internal_server_error}`. [#2159] - Helper `body::to_bytes` for async collecting message body into Bytes. [#2158] -### Changes +### Changed - The type parameter of `Response` no longer has a default. [#2152] - The `Message` variant of `body::Body` is now `Pin>`. [#2152] - `BodyStream` and `SizedStream` are no longer restricted to Unpin types. [#2152] @@ -407,10 +803,10 @@ - Remove `ResponseError` impl for `actix::actors::resolver::ResolverError` due to deprecate of resolver actor. [#1813] - Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`. - due to the removal of this type from `tokio-openssl` crate. openssl handshake + due to the removal of this type from `tokio-openssl` crate. openssl handshake error would return as `ConnectError::SslError`. [#1813] - Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`. - Due to this change `actix_threadpool::BlockingError` type is moved into + Due to this change `actix_threadpool::BlockingError` type is moved into `actix_http::error` module. [#1878] [#1813]: https://github.com/actix/actix-web/pull/1813 @@ -418,6 +814,8 @@ [#1864]: https://github.com/actix/actix-web/pull/1864 [#1878]: https://github.com/actix/actix-web/pull/1878 +
+ ## 2.2.2 - 2022-01-21 ### Changed diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index e93d1b7af..03767ca4e 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.0.0-beta.19" +version = "3.2.1" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -20,7 +20,7 @@ edition = "2018" [package.metadata.docs.rs] # features that docs.rs will build with -features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"] +features = ["http2", "ws", "openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"] [lib] name = "actix_http" @@ -29,72 +29,89 @@ path = "src/lib.rs" [features] default = [] -# openssl +# HTTP/2 protocol support +http2 = ["h2"] + +# WebSocket protocol implementation +ws = [ + "local-channel", + "base64", + "rand", + "sha1", +] + +# TLS via OpenSSL openssl = ["actix-tls/accept", "actix-tls/openssl"] -# rustls support +# TLS via Rustls rustls = ["actix-tls/accept", "actix-tls/rustls"] -# enable compression support -compress-brotli = ["brotli", "__compress"] -compress-gzip = ["flate2", "__compress"] -compress-zstd = ["zstd", "__compress"] +# Compression codecs +compress-brotli = ["__compress", "brotli"] +compress-gzip = ["__compress", "flate2"] +compress-zstd = ["__compress", "zstd"] # Internal (PRIVATE!) features used to aid testing and cheking feature status. -# Don't rely on these whatsoever. They may disappear at anytime. +# Don't rely on these whatsoever. They are semver-exempt and may disappear at anytime. __compress = [] [dependencies] -actix-service = "2.0.0" -actix-codec = "0.4.1" -actix-utils = "3.0.0" +actix-service = "2" +actix-codec = "0.5" +actix-utils = "3" actix-rt = { version = "2.2", default-features = false } ahash = "0.7" -base64 = "0.13" bitflags = "1.2" bytes = "1" bytestring = "1" derive_more = "0.99.5" encoding_rs = "0.8" futures-core = { 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 = "1" language-tags = "0.3" -local-channel = "0.1" -log = "0.4" mime = "0.3" percent-encoding = "2.1" pin-project-lite = "0.2" -rand = "0.8" -sha-1 = "0.10" smallvec = "1.6.1" +tracing = { version = "0.1.30", default-features = false, features = ["log"] } -# tls -actix-tls = { version = "3.0.0", default-features = false, optional = true } +# http2 +h2 = { version = "0.3.9", optional = true } -# compression +# websockets +local-channel = { version = "0.1", optional = true } +base64 = { version = "0.13", optional = true } +rand = { version = "0.8", optional = true } +sha1 = { version = "0.10", optional = true } + +# openssl/rustls +actix-tls = { version = "3", default-features = false, optional = true } + +# compress-* brotli = { version = "3.3.3", optional = true } flate2 = { version = "1.0.13", optional = true } -zstd = { version = "0.9", optional = true } +zstd = { version = "0.11", optional = true } [dev-dependencies] -actix-http-test = { version = "3.0.0-beta.11", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] } actix-server = "2" -actix-tls = { version = "3.0.0", features = ["openssl"] } -actix-web = "4.0.0-beta.21" +actix-tls = { version = "3", features = ["openssl"] } +actix-web = "4" async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } env_logger = "0.9" futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } memchr = "2.4" -rcgen = "0.8" +once_cell = "1.9" +rcgen = "0.9" regex = "1.3" -rustls-pemfile = "0.2" +rustversion = "1" +rustls-pemfile = "1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" static_assertions = "1" @@ -104,7 +121,7 @@ tokio = { version = "1.8.4", features = ["net", "rt", "macros"] } [[example]] name = "ws" -required-features = ["rustls"] +required-features = ["ws", "rustls"] [[bench]] name = "write-camel-case" diff --git a/actix-http/README.md b/actix-http/README.md index 9f275f597..787d2f653 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.19)](https://docs.rs/actix-http/3.0.0-beta.19) -[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.2.1)](https://docs.rs/actix-http/3.2.1) +![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg) ![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.19/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.19) +[![dependency status](https://deps.rs/crate/actix-http/3.2.1/status.svg)](https://deps.rs/crate/actix-http/3.2.1) [![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) @@ -25,7 +25,7 @@ use actix_http::{HttpService, Response}; use actix_server::Server; use futures_util::future; use http::header::HeaderValue; -use log::info; +use tracing::info; #[actix_rt::main] async fn main() -> io::Result<()> { diff --git a/actix-http/benches/uninit-headers.rs b/actix-http/benches/uninit-headers.rs index 5dfd3bc11..688c64d6e 100644 --- a/actix-http/benches/uninit-headers.rs +++ b/actix-http/benches/uninit-headers.rs @@ -114,11 +114,12 @@ mod _original { use std::mem::MaybeUninit; pub fn parse_headers(src: &mut BytesMut) -> usize { - #![allow(clippy::uninit_assumed_init)] + #![allow(invalid_value, clippy::uninit_assumed_init)] let mut headers: [HeaderIndex; MAX_HEADERS] = unsafe { MaybeUninit::uninit().assume_init() }; + #[allow(invalid_value)] let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = unsafe { MaybeUninit::uninit().assume_init() }; diff --git a/actix-http/examples/actix-web.rs b/actix-http/examples/actix-web.rs index f8226507f..449e5899b 100644 --- a/actix-http/examples/actix-web.rs +++ b/actix-http/examples/actix-web.rs @@ -18,7 +18,8 @@ async fn main() -> std::io::Result<()> { HttpService::build() // pass the app to service builder // map_config is used to map App's configuration to ServiceBuilder - .finish(map_config(app, |_| AppConfig::default())) + // h1 will configure server to only use HTTP/1.1 + .h1(map_config(app, |_| AppConfig::default())) .tcp() })? .run() diff --git a/actix-http/examples/bench.rs b/actix-http/examples/bench.rs new file mode 100644 index 000000000..e41c0bb4f --- /dev/null +++ b/actix-http/examples/bench.rs @@ -0,0 +1,27 @@ +use std::{convert::Infallible, io, time::Duration}; + +use actix_http::{HttpService, Request, Response, StatusCode}; +use actix_server::Server; +use once_cell::sync::Lazy; + +static STR: Lazy = Lazy::new(|| "HELLO WORLD ".repeat(20)); + +#[actix_rt::main] +async fn main() -> io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + Server::build() + .bind("dispatcher-benchmark", ("127.0.0.1", 8080), || { + HttpService::build() + .client_request_timeout(Duration::from_secs(1)) + .finish(|_: Request| async move { + let mut res = Response::build(StatusCode::OK); + Ok::<_, Infallible>(res.body(&**STR)) + }) + .tcp() + })? + // limiting number of workers so that bench client is not sharing as many resources + .workers(4) + .run() + .await +} diff --git a/actix-http/examples/echo.rs b/actix-http/examples/echo.rs index f9188ed9f..ae6f00cce 100644 --- a/actix-http/examples/echo.rs +++ b/actix-http/examples/echo.rs @@ -1,10 +1,11 @@ -use std::io; +use std::{io, time::Duration}; use actix_http::{Error, HttpService, Request, Response, StatusCode}; use actix_server::Server; use bytes::BytesMut; use futures_util::StreamExt as _; use http::header::HeaderValue; +use tracing::info; #[actix_rt::main] async fn main() -> io::Result<()> { @@ -13,8 +14,8 @@ async fn main() -> io::Result<()> { Server::build() .bind("echo", ("127.0.0.1", 8080), || { HttpService::build() - .client_timeout(1000) - .client_disconnect(1000) + .client_request_timeout(Duration::from_secs(1)) + .client_disconnect_timeout(Duration::from_secs(1)) // handles HTTP/1.1 and HTTP/2 .finish(|mut req: Request| async move { let mut body = BytesMut::new(); @@ -22,7 +23,7 @@ async fn main() -> io::Result<()> { body.extend_from_slice(&item?); } - log::info!("request body: {:?}", body); + info!("request body: {:?}", body); let res = Response::build(StatusCode::OK) .insert_header(("x-head", HeaderValue::from_static("dummy value!"))) diff --git a/actix-http/examples/h2spec.rs b/actix-http/examples/h2spec.rs new file mode 100644 index 000000000..4ab426c6c --- /dev/null +++ b/actix-http/examples/h2spec.rs @@ -0,0 +1,25 @@ +use std::{convert::Infallible, io}; + +use actix_http::{HttpService, Request, Response, StatusCode}; +use actix_server::Server; +use once_cell::sync::Lazy; + +static STR: Lazy = Lazy::new(|| "HELLO WORLD ".repeat(100)); + +#[actix_rt::main] +async fn main() -> io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + Server::build() + .bind("h2spec", ("127.0.0.1", 8080), || { + HttpService::build() + .h2(|_: Request| async move { + let mut res = Response::build(StatusCode::OK); + Ok::<_, Infallible>(res.body(&**STR)) + }) + .tcp() + })? + .workers(4) + .run() + .await +} diff --git a/actix-http/examples/hello-world.rs b/actix-http/examples/hello-world.rs index a29903cc4..c749cdd00 100644 --- a/actix-http/examples/hello-world.rs +++ b/actix-http/examples/hello-world.rs @@ -1,9 +1,8 @@ -use std::{convert::Infallible, io}; +use std::{convert::Infallible, io, time::Duration}; -use actix_http::{ - header::HeaderValue, HttpMessage, HttpService, Request, Response, StatusCode, -}; +use actix_http::{header::HeaderValue, HttpService, Request, Response, StatusCode}; use actix_server::Server; +use tracing::info; #[actix_rt::main] async fn main() -> io::Result<()> { @@ -12,18 +11,18 @@ async fn main() -> io::Result<()> { Server::build() .bind("hello-world", ("127.0.0.1", 8080), || { HttpService::build() - .client_timeout(1000) - .client_disconnect(1000) + .client_request_timeout(Duration::from_secs(1)) + .client_disconnect_timeout(Duration::from_secs(1)) .on_connect_ext(|_, ext| { ext.insert(42u32); }) .finish(|req: Request| async move { - log::info!("{:?}", req); + info!("{:?}", req); let mut res = Response::build(StatusCode::OK); res.insert_header(("x-head", HeaderValue::from_static("dummy value!"))); - let forty_two = req.extensions().get::().unwrap().to_string(); + let forty_two = req.conn_data::().unwrap().to_string(); res.insert_header(( "x-forty-two", HeaderValue::from_str(&forty_two).unwrap(), diff --git a/actix-http/examples/streaming-error.rs b/actix-http/examples/streaming-error.rs index 3988cbac2..8c8a249cb 100644 --- a/actix-http/examples/streaming-error.rs +++ b/actix-http/examples/streaming-error.rs @@ -12,6 +12,7 @@ use actix_http::{body::BodyStream, HttpService, Response}; use actix_server::Server; use async_stream::stream; use bytes::Bytes; +use tracing::info; #[actix_rt::main] async fn main() -> io::Result<()> { @@ -21,7 +22,7 @@ async fn main() -> io::Result<()> { .bind("streaming-error", ("127.0.0.1", 8080), || { HttpService::build() .finish(|req| async move { - log::info!("{:?}", req); + info!("{:?}", req); let res = Response::ok(); Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! { diff --git a/actix-http/examples/ws.rs b/actix-http/examples/ws.rs index d70e43314..c4f0503cd 100644 --- a/actix-http/examples/ws.rs +++ b/actix-http/examples/ws.rs @@ -17,6 +17,7 @@ use actix_server::Server; use bytes::{Bytes, BytesMut}; use bytestring::ByteString; use futures_core::{ready, Stream}; +use tracing::{info, trace}; #[actix_rt::main] async fn main() -> io::Result<()> { @@ -34,13 +35,13 @@ async fn main() -> io::Result<()> { } async fn handler(req: Request) -> Result>, Error> { - log::info!("handshaking"); + info!("handshaking"); let mut res = ws::handshake(req.head())?; // handshake will always fail under HTTP/2 - log::info!("responding"); - Ok(res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))?) + info!("responding"); + res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new()))) } struct Heartbeat { @@ -61,7 +62,7 @@ impl Stream for Heartbeat { type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - log::trace!("poll"); + trace!("poll"); ready!(self.as_mut().interval.poll_tick(cx)); diff --git a/actix-http/src/body/body_stream.rs b/actix-http/src/body/body_stream.rs index cf4f488b2..5a12c1e40 100644 --- a/actix-http/src/body/body_stream.rs +++ b/actix-http/src/body/body_stream.rs @@ -80,7 +80,7 @@ mod tests { use futures_core::ready; use futures_util::{stream, FutureExt as _}; use pin_project_lite::pin_project; - use static_assertions::{assert_impl_all, assert_not_impl_all}; + use static_assertions::{assert_impl_all, assert_not_impl_any}; use super::*; use crate::body::to_bytes; @@ -91,10 +91,10 @@ mod tests { assert_impl_all!(BodyStream>>: MessageBody); assert_impl_all!(BodyStream>>: MessageBody); - assert_not_impl_all!(BodyStream>: MessageBody); - assert_not_impl_all!(BodyStream>: MessageBody); + assert_not_impl_any!(BodyStream>: MessageBody); + assert_not_impl_any!(BodyStream>: MessageBody); // crate::Error is not Clone - assert_not_impl_all!(BodyStream>>: MessageBody); + assert_not_impl_any!(BodyStream>>: MessageBody); #[actix_rt::test] async fn skips_empty_chunks() { diff --git a/actix-http/src/body/boxed.rs b/actix-http/src/body/boxed.rs index cac6b7eb9..5fcc42f56 100644 --- a/actix-http/src/body/boxed.rs +++ b/actix-http/src/body/boxed.rs @@ -105,14 +105,13 @@ impl MessageBody for BoxBody { #[cfg(test)] mod tests { - use static_assertions::{assert_impl_all, assert_not_impl_all}; + use static_assertions::{assert_impl_all, assert_not_impl_any}; use super::*; use crate::body::to_bytes; - assert_impl_all!(BoxBody: MessageBody, fmt::Debug, Unpin); - - assert_not_impl_all!(BoxBody: Send, Sync, Unpin); + assert_impl_all!(BoxBody: fmt::Debug, MessageBody, Unpin); + assert_not_impl_any!(BoxBody: Send, Sync); #[actix_rt::test] async fn nested_boxed_body() { diff --git a/actix-http/src/body/either.rs b/actix-http/src/body/either.rs index add1eab7c..92bd89984 100644 --- a/actix-http/src/body/either.rs +++ b/actix-http/src/body/either.rs @@ -10,6 +10,17 @@ use super::{BodySize, BoxBody, MessageBody}; use crate::Error; pin_project! { + /// An "either" type specialized for body types. + /// + /// It is common, in middleware especially, to conditionally return an inner service's unknown/ + /// generic body `B` type or return early with a new response. This type's "right" variant + /// defaults to `BoxBody` since error responses are the common case. + /// + /// For example, middleware will often have `type Response = ServiceResponse>`. + /// This means that the inner service's response body type maps to the `Left` variant and the + /// middleware's own error responses use the default `Right` variant of `BoxBody`. Of course, + /// there's no reason it couldn't use `EitherBody` instead if its alternative + /// responses have a known type. #[project = EitherBodyProj] #[derive(Debug, Clone)] pub enum EitherBody { @@ -22,7 +33,10 @@ pin_project! { } impl EitherBody { - /// Creates new `EitherBody` using left variant and boxed right variant. + /// Creates new `EitherBody` left variant with a boxed right variant. + /// + /// If the expected `R` type will be inferred and is not `BoxBody` then use the + /// [`left`](Self::left) constructor instead. #[inline] pub fn new(body: L) -> Self { Self::Left { body } diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs index 86ff09fbe..ab742b9cd 100644 --- a/actix-http/src/body/message_body.rs +++ b/actix-http/src/body/message_body.rs @@ -19,7 +19,7 @@ use super::{BodySize, BoxBody}; /// It is not usually necessary to create custom body types, this trait is already [implemented for /// a large number of sensible body types](#foreign-impls) including: /// - Empty body: `()` -/// - Text-based: `String`, `&'static str`, `ByteString`. +/// - Text-based: `String`, `&'static str`, [`ByteString`](https://docs.rs/bytestring/1). /// - Byte-based: `Bytes`, `BytesMut`, `Vec`, `&'static [u8]`; /// - Streams: [`BodyStream`](super::BodyStream), [`SizedStream`](super::SizedStream) /// @@ -481,6 +481,7 @@ mod tests { assert_poll_next_none!(pl); } + #[allow(clippy::let_unit_value)] #[actix_rt::test] async fn test_unit() { let pl = (); diff --git a/actix-http/src/body/sized_stream.rs b/actix-http/src/body/sized_stream.rs index 9c1727246..e5e27b287 100644 --- a/actix-http/src/body/sized_stream.rs +++ b/actix-http/src/body/sized_stream.rs @@ -76,7 +76,7 @@ mod tests { use actix_rt::pin; use actix_utils::future::poll_fn; use futures_util::stream; - use static_assertions::{assert_impl_all, assert_not_impl_all}; + use static_assertions::{assert_impl_all, assert_not_impl_any}; use super::*; use crate::body::to_bytes; @@ -87,10 +87,10 @@ mod tests { assert_impl_all!(SizedStream>>: MessageBody); assert_impl_all!(SizedStream>>: MessageBody); - assert_not_impl_all!(SizedStream>: MessageBody); - assert_not_impl_all!(SizedStream>: MessageBody); + assert_not_impl_any!(SizedStream>: MessageBody); + assert_not_impl_any!(SizedStream>: MessageBody); // crate::Error is not Clone - assert_not_impl_all!(SizedStream>>: MessageBody); + assert_not_impl_any!(SizedStream>>: MessageBody); #[actix_rt::test] async fn skips_empty_chunks() { diff --git a/actix-http/src/builder.rs b/actix-http/src/builder.rs index 408ee7924..526a23d53 100644 --- a/actix-http/src/builder.rs +++ b/actix-http/src/builder.rs @@ -1,25 +1,22 @@ -use std::{fmt, marker::PhantomData, net, rc::Rc}; +use std::{fmt, marker::PhantomData, net, rc::Rc, time::Duration}; use actix_codec::Framed; use actix_service::{IntoServiceFactory, Service, ServiceFactory}; use crate::{ body::{BoxBody, MessageBody}, - config::{KeepAlive, ServiceConfig}, h1::{self, ExpectHandler, H1Service, UpgradeHandler}, - h2::H2Service, service::HttpService, - ConnectCallback, Extensions, Request, Response, + ConnectCallback, Extensions, KeepAlive, Request, Response, ServiceConfig, }; -/// A HTTP service builder +/// An HTTP service builder. /// -/// This type can be used to construct an instance of [`HttpService`] through a -/// builder-like pattern. +/// This type can construct an instance of [`HttpService`] through a builder-like pattern. pub struct HttpServiceBuilder { keep_alive: KeepAlive, - client_timeout: u64, - client_disconnect: u64, + client_request_timeout: Duration, + client_disconnect_timeout: Duration, secure: bool, local_addr: Option, expect: X, @@ -28,22 +25,23 @@ pub struct HttpServiceBuilder { _phantom: PhantomData, } -impl HttpServiceBuilder +impl Default for HttpServiceBuilder where S: ServiceFactory, S::Error: Into> + 'static, S::InitError: fmt::Debug, >::Future: 'static, { - /// Create instance of `ServiceConfigBuilder` - #[allow(clippy::new_without_default)] - pub fn new() -> Self { + fn default() -> Self { HttpServiceBuilder { - keep_alive: KeepAlive::Timeout(5), - client_timeout: 5000, - client_disconnect: 0, + // ServiceConfig parts (make sure defaults match) + keep_alive: KeepAlive::default(), + client_request_timeout: Duration::from_secs(5), + client_disconnect_timeout: Duration::ZERO, secure: false, local_addr: None, + + // dispatcher parts expect: ExpectHandler, upgrade: None, on_connect_ext: None, @@ -65,9 +63,11 @@ where U::Error: fmt::Display, U::InitError: fmt::Debug, { - /// Set server keep-alive setting. + /// Set connection keep-alive setting. /// - /// By default keep alive is set to a 5 seconds. + /// Applies to HTTP/1.1 keep-alive and HTTP/2 ping-pong. + /// + /// By default keep-alive is 5 seconds. pub fn keep_alive>(mut self, val: W) -> Self { self.keep_alive = val.into(); self @@ -85,33 +85,45 @@ where self } - /// Set server client timeout in milliseconds for first request. + /// Set client request timeout (for first request). /// - /// Defines a timeout for reading client request header. If a client does not transmit - /// the entire set headers within this time, the request is terminated with - /// the 408 (Request Time-out) error. + /// Defines a timeout for reading client request header. If the client does not transmit the + /// request head within this duration, the connection is terminated with a `408 Request Timeout` + /// response error. /// - /// To disable timeout set value to 0. + /// A duration of zero disables the timeout. /// - /// By default client timeout is set to 5000 milliseconds. - pub fn client_timeout(mut self, val: u64) -> Self { - self.client_timeout = val; + /// By default, the client timeout is 5 seconds. + pub fn client_request_timeout(mut self, dur: Duration) -> Self { + self.client_request_timeout = dur; self } - /// Set server connection disconnect timeout in milliseconds. + #[doc(hidden)] + #[deprecated(since = "3.0.0", note = "Renamed to `client_request_timeout`.")] + pub fn client_timeout(self, dur: Duration) -> Self { + self.client_request_timeout(dur) + } + + /// Set client connection disconnect timeout. /// /// Defines a timeout for disconnect connection. If a disconnect procedure does not complete /// within this time, the request get dropped. This timeout affects secure connections. /// - /// To disable timeout set value to 0. + /// A duration of zero disables the timeout. /// - /// By default disconnect timeout is set to 0. - pub fn client_disconnect(mut self, val: u64) -> Self { - self.client_disconnect = val; + /// By default, the disconnect timeout is disabled. + pub fn client_disconnect_timeout(mut self, dur: Duration) -> Self { + self.client_disconnect_timeout = dur; self } + #[doc(hidden)] + #[deprecated(since = "3.0.0", note = "Renamed to `client_disconnect_timeout`.")] + pub fn client_disconnect(self, dur: Duration) -> Self { + self.client_disconnect_timeout(dur) + } + /// Provide service for `EXPECT: 100-Continue` support. /// /// Service get called with request that contains `EXPECT` header. @@ -126,8 +138,8 @@ where { HttpServiceBuilder { keep_alive: self.keep_alive, - client_timeout: self.client_timeout, - client_disconnect: self.client_disconnect, + client_request_timeout: self.client_request_timeout, + client_disconnect_timeout: self.client_disconnect_timeout, secure: self.secure, local_addr: self.local_addr, expect: expect.into_factory(), @@ -150,8 +162,8 @@ where { HttpServiceBuilder { keep_alive: self.keep_alive, - client_timeout: self.client_timeout, - client_disconnect: self.client_disconnect, + client_request_timeout: self.client_request_timeout, + client_disconnect_timeout: self.client_disconnect_timeout, secure: self.secure, local_addr: self.local_addr, expect: self.expect, @@ -185,8 +197,8 @@ where { let cfg = ServiceConfig::new( self.keep_alive, - self.client_timeout, - self.client_disconnect, + self.client_request_timeout, + self.client_disconnect_timeout, self.secure, self.local_addr, ); @@ -198,7 +210,8 @@ where } /// Finish service configuration and create a HTTP service for HTTP/2 protocol. - pub fn h2(self, service: F) -> H2Service + #[cfg(feature = "http2")] + pub fn h2(self, service: F) -> crate::h2::H2Service where F: IntoServiceFactory, S::Error: Into> + 'static, @@ -209,13 +222,14 @@ where { let cfg = ServiceConfig::new( self.keep_alive, - self.client_timeout, - self.client_disconnect, + self.client_request_timeout, + self.client_disconnect_timeout, self.secure, self.local_addr, ); - H2Service::with_config(cfg, service.into_factory()).on_connect_ext(self.on_connect_ext) + crate::h2::H2Service::with_config(cfg, service.into_factory()) + .on_connect_ext(self.on_connect_ext) } /// Finish service configuration and create `HttpService` instance. @@ -230,8 +244,8 @@ where { let cfg = ServiceConfig::new( self.keep_alive, - self.client_timeout, - self.client_disconnect, + self.client_request_timeout, + self.client_disconnect_timeout, self.secure, self.local_addr, ); diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index b6d5a7d51..ac95a2802 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -1,71 +1,36 @@ use std::{ - cell::Cell, - fmt::{self, Write}, net, rc::Rc, - time::{Duration, SystemTime}, + time::{Duration, Instant}, }; -use actix_rt::{ - task::JoinHandle, - time::{interval, sleep_until, Instant, Sleep}, -}; use bytes::BytesMut; -/// "Sun, 06 Nov 1994 08:49:37 GMT".len() -pub(crate) const DATE_VALUE_LENGTH: usize = 29; +use crate::{date::DateService, KeepAlive}; -#[derive(Debug, PartialEq, Clone, Copy)] -/// Server keep-alive setting -pub enum KeepAlive { - /// Keep alive in seconds - Timeout(usize), - - /// Rely on OS to shutdown tcp connection - Os, - - /// Disabled - Disabled, -} - -impl From for KeepAlive { - fn from(keepalive: usize) -> Self { - KeepAlive::Timeout(keepalive) - } -} - -impl From> for KeepAlive { - fn from(keepalive: Option) -> Self { - if let Some(keepalive) = keepalive { - KeepAlive::Timeout(keepalive) - } else { - KeepAlive::Disabled - } - } -} - -/// Http service configuration +/// HTTP service configuration. +#[derive(Debug, Clone)] pub struct ServiceConfig(Rc); +#[derive(Debug)] struct Inner { - keep_alive: Option, - client_timeout: u64, - client_disconnect: u64, - ka_enabled: bool, + keep_alive: KeepAlive, + client_request_timeout: Duration, + client_disconnect_timeout: Duration, secure: bool, local_addr: Option, date_service: DateService, } -impl Clone for ServiceConfig { - fn clone(&self) -> Self { - ServiceConfig(self.0.clone()) - } -} - impl Default for ServiceConfig { fn default() -> Self { - Self::new(KeepAlive::Timeout(5), 0, 0, false, None) + Self::new( + KeepAlive::default(), + Duration::from_secs(5), + Duration::ZERO, + false, + None, + ) } } @@ -73,34 +38,22 @@ impl ServiceConfig { /// Create instance of `ServiceConfig` pub fn new( keep_alive: KeepAlive, - client_timeout: u64, - client_disconnect: u64, + client_request_timeout: Duration, + client_disconnect_timeout: Duration, secure: bool, local_addr: Option, ) -> ServiceConfig { - let (keep_alive, ka_enabled) = match keep_alive { - KeepAlive::Timeout(val) => (val as u64, true), - KeepAlive::Os => (0, true), - KeepAlive::Disabled => (0, false), - }; - let keep_alive = if ka_enabled && keep_alive > 0 { - Some(Duration::from_secs(keep_alive)) - } else { - None - }; - ServiceConfig(Rc::new(Inner { - keep_alive, - ka_enabled, - client_timeout, - client_disconnect, + keep_alive: keep_alive.normalize(), + client_request_timeout, + client_disconnect_timeout, secure, local_addr, date_service: DateService::new(), })) } - /// Returns true if connection is secure (HTTPS) + /// Returns `true` if connection is secure (i.e., using TLS / HTTPS). #[inline] pub fn secure(&self) -> bool { self.0.secure @@ -114,239 +67,97 @@ impl ServiceConfig { self.0.local_addr } - /// Keep alive duration if configured. + /// Connection keep-alive setting. #[inline] - pub fn keep_alive(&self) -> Option { + pub fn keep_alive(&self) -> KeepAlive { self.0.keep_alive } - /// Return state of connection keep-alive functionality - #[inline] - pub fn keep_alive_enabled(&self) -> bool { - self.0.ka_enabled - } - - /// Client timeout for first request. - #[inline] - pub fn client_timer(&self) -> Option { - let delay_time = self.0.client_timeout; - if delay_time != 0 { - Some(sleep_until(self.now() + Duration::from_millis(delay_time))) - } else { - None + /// Creates a time object representing the deadline for this connection's keep-alive period, if + /// enabled. + /// + /// When [`KeepAlive::Os`] or [`KeepAlive::Disabled`] is set, this will return `None`. + pub fn keep_alive_deadline(&self) -> Option { + match self.keep_alive() { + KeepAlive::Timeout(dur) => Some(self.now() + dur), + KeepAlive::Os => None, + KeepAlive::Disabled => None, } } - /// Client timeout for first request. - pub fn client_timer_expire(&self) -> Option { - let delay = self.0.client_timeout; - if delay != 0 { - Some(self.now() + Duration::from_millis(delay)) - } else { - None - } + /// Creates a time object representing the deadline for the client to finish sending the head of + /// its first request. + /// + /// Returns `None` if this `ServiceConfig was` constructed with `client_request_timeout: 0`. + pub fn client_request_deadline(&self) -> Option { + let timeout = self.0.client_request_timeout; + (timeout != Duration::ZERO).then(|| self.now() + timeout) } - /// Client disconnect timer - pub fn client_disconnect_timer(&self) -> Option { - let delay = self.0.client_disconnect; - if delay != 0 { - Some(self.now() + Duration::from_millis(delay)) - } else { - None - } + /// Creates a time object representing the deadline for the client to disconnect. + pub fn client_disconnect_deadline(&self) -> Option { + let timeout = self.0.client_disconnect_timeout; + (timeout != Duration::ZERO).then(|| self.now() + timeout) } - /// Return keep-alive timer delay is configured. - #[inline] - pub fn keep_alive_timer(&self) -> Option { - self.keep_alive().map(|ka| sleep_until(self.now() + ka)) - } - - /// Keep-alive expire time - pub fn keep_alive_expire(&self) -> Option { - self.keep_alive().map(|ka| self.now() + ka) - } - - #[inline] pub(crate) fn now(&self) -> Instant { self.0.date_service.now() } + /// Writes date header to `dst` buffer. + /// + /// Low-level method that utilizes the built-in efficient date service, requiring fewer syscalls + /// than normal. Note that a CRLF (`\r\n`) is included in what is written. #[doc(hidden)] - pub fn set_date(&self, dst: &mut BytesMut, camel_case: bool) { - let mut buf: [u8; 39] = [0; 39]; + pub fn write_date_header(&self, dst: &mut BytesMut, camel_case: bool) { + let mut buf: [u8; 37] = [0; 37]; buf[..6].copy_from_slice(if camel_case { b"Date: " } else { b"date: " }); self.0 .date_service - .set_date(|date| buf[6..35].copy_from_slice(&date.bytes)); + .with_date(|date| buf[6..35].copy_from_slice(&date.bytes)); - buf[35..].copy_from_slice(b"\r\n\r\n"); + buf[35..].copy_from_slice(b"\r\n"); dst.extend_from_slice(&buf); } - pub(crate) fn set_date_header(&self, dst: &mut BytesMut) { + #[allow(unused)] // used with `http2` feature flag + pub(crate) fn write_date_header_value(&self, dst: &mut BytesMut) { self.0 .date_service - .set_date(|date| dst.extend_from_slice(&date.bytes)); - } -} - -#[derive(Copy, Clone)] -struct Date { - bytes: [u8; DATE_VALUE_LENGTH], - pos: usize, -} - -impl Date { - fn new() -> Date { - let mut date = Date { - bytes: [0; DATE_VALUE_LENGTH], - pos: 0, - }; - date.update(); - date - } - - fn update(&mut self) { - self.pos = 0; - write!(self, "{}", httpdate::fmt_http_date(SystemTime::now())).unwrap(); - } -} - -impl fmt::Write for Date { - fn write_str(&mut self, s: &str) -> fmt::Result { - let len = s.len(); - self.bytes[self.pos..self.pos + len].copy_from_slice(s.as_bytes()); - self.pos += len; - Ok(()) - } -} - -/// Service for update Date and Instant periodically at 500 millis interval. -struct DateService { - current: Rc>, - handle: JoinHandle<()>, -} - -impl Drop for DateService { - fn drop(&mut self) { - // stop the timer update async task on drop. - self.handle.abort(); - } -} - -impl DateService { - fn new() -> Self { - // shared date and timer for DateService and update async task. - let current = Rc::new(Cell::new((Date::new(), Instant::now()))); - let current_clone = Rc::clone(¤t); - // spawn an async task sleep for 500 milli and update current date/timer in a loop. - // handle is used to stop the task on DateService drop. - let handle = actix_rt::spawn(async move { - #[cfg(test)] - let _notify = notify_on_drop::NotifyOnDrop::new(); - - let mut interval = interval(Duration::from_millis(500)); - loop { - let now = interval.tick().await; - let date = Date::new(); - current_clone.set((date, now)); - } - }); - - DateService { current, handle } - } - - fn now(&self) -> Instant { - self.current.get().1 - } - - fn set_date(&self, mut f: F) { - f(&self.current.get().0); - } -} - -// TODO: move to a util module for testing all spawn handle drop style tasks. -/// Test Module for checking the drop state of certain async tasks that are spawned -/// with `actix_rt::spawn` -/// -/// The target task must explicitly generate `NotifyOnDrop` when spawn the task -#[cfg(test)] -mod notify_on_drop { - use std::cell::RefCell; - - thread_local! { - static NOTIFY_DROPPED: RefCell> = RefCell::new(None); - } - - /// Check if the spawned task is dropped. - /// - /// # Panics - /// Panics when there was no `NotifyOnDrop` instance on current thread. - pub(crate) fn is_dropped() -> bool { - NOTIFY_DROPPED.with(|bool| { - bool.borrow() - .expect("No NotifyOnDrop existed on current thread") - }) - } - - pub(crate) struct NotifyOnDrop; - - impl NotifyOnDrop { - /// # Panic: - /// - /// When construct multiple instances on any given thread. - pub(crate) fn new() -> Self { - NOTIFY_DROPPED.with(|bool| { - let mut bool = bool.borrow_mut(); - if bool.is_some() { - panic!("NotifyOnDrop existed on current thread"); - } else { - *bool = Some(false); - } - }); - - NotifyOnDrop - } - } - - impl Drop for NotifyOnDrop { - fn drop(&mut self) { - NOTIFY_DROPPED.with(|bool| { - if let Some(b) = bool.borrow_mut().as_mut() { - *b = true; - } - }); - } + .with_date(|date| dst.extend_from_slice(&date.bytes)); } } #[cfg(test)] mod tests { use super::*; + use crate::{date::DATE_VALUE_LENGTH, notify_on_drop}; - use actix_rt::{task::yield_now, time::sleep}; + use actix_rt::{ + task::yield_now, + time::{sleep, sleep_until}, + }; use memchr::memmem; #[actix_rt::test] async fn test_date_service_update() { - let settings = ServiceConfig::new(KeepAlive::Os, 0, 0, false, None); + let settings = + ServiceConfig::new(KeepAlive::Os, Duration::ZERO, Duration::ZERO, false, None); yield_now().await; let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); - settings.set_date(&mut buf1, false); + settings.write_date_header(&mut buf1, false); let now1 = settings.now(); - sleep_until(Instant::now() + Duration::from_secs(2)).await; + sleep_until((Instant::now() + Duration::from_secs(2)).into()).await; yield_now().await; let now2 = settings.now(); let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); - settings.set_date(&mut buf2, false); + settings.write_date_header(&mut buf2, false); assert_ne!(now1, now2); @@ -402,10 +213,10 @@ mod tests { let settings = ServiceConfig::default(); let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); - settings.set_date(&mut buf1, false); + settings.write_date_header(&mut buf1, false); let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); - settings.set_date(&mut buf2, false); + settings.write_date_header(&mut buf2, false); assert_eq!(buf1, buf2); } @@ -415,11 +226,11 @@ mod tests { let settings = ServiceConfig::default(); let mut buf = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); - settings.set_date(&mut buf, false); + settings.write_date_header(&mut buf, false); assert!(memmem::find(&buf, b"date:").is_some()); let mut buf = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); - settings.set_date(&mut buf, true); + settings.write_date_header(&mut buf, true); assert!(memmem::find(&buf, b"Date:").is_some()); } } diff --git a/actix-http/src/date.rs b/actix-http/src/date.rs new file mode 100644 index 000000000..1358bbd8c --- /dev/null +++ b/actix-http/src/date.rs @@ -0,0 +1,92 @@ +use std::{ + cell::Cell, + fmt::{self, Write}, + rc::Rc, + time::{Duration, Instant, SystemTime}, +}; + +use actix_rt::{task::JoinHandle, time::interval}; + +/// "Thu, 01 Jan 1970 00:00:00 GMT".len() +pub(crate) const DATE_VALUE_LENGTH: usize = 29; + +#[derive(Clone, Copy)] +pub(crate) struct Date { + pub(crate) bytes: [u8; DATE_VALUE_LENGTH], + pos: usize, +} + +impl Date { + fn new() -> Date { + let mut date = Date { + bytes: [0; DATE_VALUE_LENGTH], + pos: 0, + }; + date.update(); + date + } + + fn update(&mut self) { + self.pos = 0; + write!(self, "{}", httpdate::fmt_http_date(SystemTime::now())).unwrap(); + } +} + +impl fmt::Write for Date { + fn write_str(&mut self, s: &str) -> fmt::Result { + let len = s.len(); + self.bytes[self.pos..self.pos + len].copy_from_slice(s.as_bytes()); + self.pos += len; + Ok(()) + } +} + +/// Service for update Date and Instant periodically at 500 millis interval. +pub(crate) struct DateService { + current: Rc>, + handle: JoinHandle<()>, +} + +impl DateService { + pub(crate) fn new() -> Self { + // shared date and timer for DateService and update async task. + let current = Rc::new(Cell::new((Date::new(), Instant::now()))); + let current_clone = Rc::clone(¤t); + // spawn an async task sleep for 500 millis and update current date/timer in a loop. + // handle is used to stop the task on DateService drop. + let handle = actix_rt::spawn(async move { + #[cfg(test)] + let _notify = crate::notify_on_drop::NotifyOnDrop::new(); + + let mut interval = interval(Duration::from_millis(500)); + loop { + let now = interval.tick().await; + let date = Date::new(); + current_clone.set((date, now.into_std())); + } + }); + + DateService { current, handle } + } + + pub(crate) fn now(&self) -> Instant { + self.current.get().1 + } + + pub(crate) fn with_date(&self, mut f: F) { + f(&self.current.get().0); + } +} + +impl fmt::Debug for DateService { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DateService").finish_non_exhaustive() + } +} + +impl Drop for DateService { + fn drop(&mut self) { + // stop the timer update async task on drop. + self.handle.abort(); + } +} diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index 2ed7be899..06b672fd8 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -19,7 +19,7 @@ use zstd::stream::write::Decoder as ZstdDecoder; use crate::{ encoding::Writer, - error::{BlockingError, PayloadError}, + error::PayloadError, header::{ContentEncoding, HeaderMap, CONTENT_ENCODING}, }; @@ -47,14 +47,17 @@ where ContentEncoding::Brotli => Some(ContentDecoder::Brotli(Box::new( brotli::DecompressorWriter::new(Writer::new(), 8_096), ))), + #[cfg(feature = "compress-gzip")] ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( ZlibDecoder::new(Writer::new()), ))), + #[cfg(feature = "compress-gzip")] ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(GzDecoder::new( Writer::new(), )))), + #[cfg(feature = "compress-zstd")] ContentEncoding::Zstd => Some(ContentDecoder::Zstd(Box::new( ZstdDecoder::new(Writer::new()).expect( @@ -98,8 +101,12 @@ where loop { if let Some(ref mut fut) = this.fut { - let (chunk, decoder) = - ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??; + let (chunk, decoder) = ready!(Pin::new(fut).poll(cx)).map_err(|_| { + PayloadError::Io(io::Error::new( + io::ErrorKind::Other, + "Blocking task was cancelled unexpectedly", + )) + })??; *this.decoder = Some(decoder); this.fut.take(); @@ -159,10 +166,13 @@ where enum ContentDecoder { #[cfg(feature = "compress-gzip")] Deflate(Box>), + #[cfg(feature = "compress-gzip")] Gzip(Box>), + #[cfg(feature = "compress-brotli")] Brotli(Box>), + // We need explicit 'static lifetime here because ZstdDecoder need lifetime // argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static` #[cfg(feature = "compress-zstd")] diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 116fe76ab..bbe53e8e8 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -17,13 +17,13 @@ use pin_project_lite::pin_project; #[cfg(feature = "compress-gzip")] use flate2::write::{GzEncoder, ZlibEncoder}; +use tracing::trace; #[cfg(feature = "compress-zstd")] use zstd::stream::write::Encoder as ZstdEncoder; use super::Writer; use crate::{ body::{self, BodySize, MessageBody}, - error::BlockingError, header::{self, ContentEncoding, HeaderValue, CONTENT_ENCODING}, ResponseHead, StatusCode, }; @@ -173,7 +173,12 @@ where if let Some(ref mut fut) = this.fut { let mut encoder = ready!(Pin::new(fut).poll(cx)) - .map_err(|_| EncoderError::Blocking(BlockingError))? + .map_err(|_| { + EncoderError::Io(io::Error::new( + io::ErrorKind::Other, + "Blocking task was cancelled unexpectedly", + )) + })? .map_err(EncoderError::Io)?; let chunk = encoder.take(); @@ -252,7 +257,7 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { head.headers_mut() .insert(header::CONTENT_ENCODING, encoding.to_header_value()); head.headers_mut() - .insert(header::VARY, HeaderValue::from_static("accept-encoding")); + .append(header::VARY, HeaderValue::from_static("accept-encoding")); head.no_chunking(false); } @@ -400,12 +405,11 @@ fn new_brotli_compressor() -> Box> { #[derive(Debug, Display)] #[non_exhaustive] pub enum EncoderError { + /// Wrapped body stream error. #[display(fmt = "body")] Body(Box), - #[display(fmt = "blocking")] - Blocking(BlockingError), - + /// Generic I/O error. #[display(fmt = "io")] Io(io::Error), } @@ -414,7 +418,6 @@ impl StdError for EncoderError { fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { EncoderError::Body(err) => Some(&**err), - EncoderError::Blocking(err) => Some(err), EncoderError::Io(err) => Some(err), } } diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index df6d3813a..3fce0a60b 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -5,7 +5,7 @@ use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Err use derive_more::{Display, Error, From}; use http::{uri::InvalidUri, StatusCode}; -use crate::{body::BoxBody, ws, Response}; +use crate::{body::BoxBody, Response}; pub use http::Error as HttpError; @@ -51,7 +51,7 @@ impl Error { Self::new(Kind::SendResponse) } - #[allow(unused)] // reserved for future use (TODO: remove allow when being used) + #[allow(unused)] // available for future use pub(crate) fn new_io() -> Self { Self::new(Kind::Io) } @@ -61,6 +61,7 @@ impl Error { Self::new(Kind::Encoder) } + #[allow(unused)] // used with `ws` feature flag pub(crate) fn new_ws() -> Self { Self::new(Kind::Ws) } @@ -107,8 +108,10 @@ pub(crate) enum Kind { impl fmt::Debug for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // TODO: more detail - f.write_str("actix_http::Error") + f.debug_struct("actix_http::Error") + .field("kind", &self.inner.kind) + .field("cause", &self.inner.cause) + .finish() } } @@ -139,14 +142,16 @@ impl From for Error { } } -impl From for Error { - fn from(err: ws::HandshakeError) -> Self { +#[cfg(feature = "ws")] +impl From for Error { + fn from(err: crate::ws::HandshakeError) -> Self { Self::new_ws().with_cause(err) } } -impl From for Error { - fn from(err: ws::ProtocolError) -> Self { +#[cfg(feature = "ws")] +impl From for Error { + fn from(err: crate::ws::ProtocolError) -> Self { Self::new_ws().with_cause(err) } } @@ -247,12 +252,6 @@ impl From for Response { } } -/// A set of errors that can occur running blocking tasks in thread pool. -#[derive(Debug, Display, Error)] -#[display(fmt = "Blocking thread pool is gone")] -// TODO: non-exhaustive -pub struct BlockingError; - /// A set of errors that can occur during payload parsing. #[derive(Debug, Display)] #[non_exhaustive] @@ -277,8 +276,9 @@ pub enum PayloadError { UnknownLength, /// HTTP/2 payload error. + #[cfg(feature = "http2")] #[display(fmt = "{}", _0)] - Http2Payload(h2::Error), + Http2Payload(::h2::Error), /// Generic I/O error. #[display(fmt = "{}", _0)] @@ -289,18 +289,20 @@ impl std::error::Error for PayloadError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { PayloadError::Incomplete(None) => None, - PayloadError::Incomplete(Some(err)) => Some(err as &dyn std::error::Error), + PayloadError::Incomplete(Some(err)) => Some(err), PayloadError::EncodingCorrupted => None, PayloadError::Overflow => None, PayloadError::UnknownLength => None, - PayloadError::Http2Payload(err) => Some(err as &dyn std::error::Error), - PayloadError::Io(err) => Some(err as &dyn std::error::Error), + #[cfg(feature = "http2")] + PayloadError::Http2Payload(err) => Some(err), + PayloadError::Io(err) => Some(err), } } } -impl From for PayloadError { - fn from(err: h2::Error) -> Self { +#[cfg(feature = "http2")] +impl From<::h2::Error> for PayloadError { + fn from(err: ::h2::Error) -> Self { PayloadError::Http2Payload(err) } } @@ -317,15 +319,6 @@ impl From for PayloadError { } } -impl From for PayloadError { - fn from(_: BlockingError) -> Self { - PayloadError::Io(io::Error::new( - io::ErrorKind::Other, - "Operation is canceled", - )) - } -} - impl From for Error { fn from(err: PayloadError) -> Self { Self::new_payload().with_cause(err) @@ -334,6 +327,7 @@ impl From for Error { /// A set of errors that can occur during dispatching HTTP requests. #[derive(Debug, Display, From)] +#[non_exhaustive] pub enum DispatchError { /// Service error. #[display(fmt = "Service Error")] @@ -356,6 +350,7 @@ pub enum DispatchError { /// HTTP/2 error. #[display(fmt = "{}", _0)] + #[cfg(feature = "http2")] H2(h2::Error), /// The first request did not complete within the specified timeout. @@ -366,6 +361,10 @@ pub enum DispatchError { #[display(fmt = "Connection shutdown timeout")] DisconnectTimeout, + /// Handler dropped payload before reading EOF. + #[display(fmt = "Handler dropped payload before reading EOF")] + HandlerDroppedPayload, + /// Internal error. #[display(fmt = "Internal error")] InternalError, @@ -374,12 +373,14 @@ pub enum DispatchError { impl StdError for DispatchError { fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { - // TODO: error source extraction? DispatchError::Service(_res) => None, DispatchError::Body(err) => Some(&**err), DispatchError::Io(err) => Some(err), DispatchError::Parse(err) => Some(err), + + #[cfg(feature = "http2")] DispatchError::H2(err) => Some(err), + _ => None, } } diff --git a/actix-http/src/h1/chunked.rs b/actix-http/src/h1/chunked.rs index 7d0532fcd..c1dd4b283 100644 --- a/actix-http/src/h1/chunked.rs +++ b/actix-http/src/h1/chunked.rs @@ -1,6 +1,7 @@ use std::{io, task::Poll}; use bytes::{Buf as _, Bytes, BytesMut}; +use tracing::{debug, trace}; macro_rules! byte ( ($rdr:ident) => ({ @@ -76,7 +77,7 @@ impl ChunkedState { Poll::Ready(Ok(ChunkedState::Size)) } None => { - log::debug!("chunk size would overflow u64"); + debug!("chunk size would overflow u64"); Poll::Ready(Err(io::Error::new( io::ErrorKind::InvalidInput, "Invalid chunk size line: Size is too big", @@ -124,7 +125,7 @@ impl ChunkedState { rem: &mut u64, buf: &mut Option, ) -> Poll> { - log::trace!("Chunked read, remaining={:?}", rem); + trace!("Chunked read, remaining={:?}", rem); let len = rdr.len() as u64; if len == 0 { diff --git a/actix-http/src/h1/client.rs b/actix-http/src/h1/client.rs index 9bd896ae0..75c88d00c 100644 --- a/actix-http/src/h1/client.rs +++ b/actix-http/src/h1/client.rs @@ -1,4 +1,4 @@ -use std::io; +use std::{fmt, io}; use actix_codec::{Decoder, Encoder}; use bitflags::bitflags; @@ -17,9 +17,9 @@ use crate::{ bitflags! { struct Flags: u8 { - const HEAD = 0b0000_0001; - const KEEPALIVE_ENABLED = 0b0000_1000; - const STREAM = 0b0001_0000; + const HEAD = 0b0000_0001; + const KEEP_ALIVE_ENABLED = 0b0000_1000; + const STREAM = 0b0001_0000; } } @@ -38,7 +38,7 @@ struct ClientCodecInner { decoder: decoder::MessageDecoder, payload: Option, version: Version, - ctype: ConnectionType, + conn_type: ConnectionType, // encoder part flags: Flags, @@ -51,23 +51,32 @@ impl Default for ClientCodec { } } +impl fmt::Debug for ClientCodec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("h1::ClientCodec") + .field("flags", &self.inner.flags) + .finish_non_exhaustive() + } +} + impl ClientCodec { /// Create HTTP/1 codec. /// /// `keepalive_enabled` how response `connection` header get generated. pub fn new(config: ServiceConfig) -> Self { - let flags = if config.keep_alive_enabled() { - Flags::KEEPALIVE_ENABLED + let flags = if config.keep_alive().enabled() { + Flags::KEEP_ALIVE_ENABLED } else { Flags::empty() }; + ClientCodec { inner: ClientCodecInner { config, decoder: decoder::MessageDecoder::default(), payload: None, version: Version::HTTP_11, - ctype: ConnectionType::Close, + conn_type: ConnectionType::Close, flags, encoder: encoder::MessageEncoder::default(), @@ -77,12 +86,12 @@ impl ClientCodec { /// Check if request is upgrade pub fn upgrade(&self) -> bool { - self.inner.ctype == ConnectionType::Upgrade + self.inner.conn_type == ConnectionType::Upgrade } /// Check if last response is keep-alive - pub fn keepalive(&self) -> bool { - self.inner.ctype == ConnectionType::KeepAlive + pub fn keep_alive(&self) -> bool { + self.inner.conn_type == ConnectionType::KeepAlive } /// Check last request's message type @@ -104,8 +113,8 @@ impl ClientCodec { impl ClientPayloadCodec { /// Check if last response is keep-alive - pub fn keepalive(&self) -> bool { - self.inner.ctype == ConnectionType::KeepAlive + pub fn keep_alive(&self) -> bool { + self.inner.conn_type == ConnectionType::KeepAlive } /// Transform payload codec to a message codec @@ -119,15 +128,18 @@ impl Decoder for ClientCodec { type Error = ParseError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set"); + debug_assert!( + self.inner.payload.is_none(), + "Payload decoder should not be set" + ); if let Some((req, payload)) = self.inner.decoder.decode(src)? { - if let Some(ctype) = req.conn_type() { + if let Some(conn_type) = req.conn_type() { // do not use peer's keep-alive - self.inner.ctype = if ctype == ConnectionType::KeepAlive { - self.inner.ctype + self.inner.conn_type = if conn_type == ConnectionType::KeepAlive { + self.inner.conn_type } else { - ctype + conn_type }; } @@ -192,9 +204,9 @@ impl Encoder> for ClientCodec { .set(Flags::HEAD, head.as_ref().method == Method::HEAD); // connection status - inner.ctype = match head.as_ref().connection_type() { + inner.conn_type = match head.as_ref().connection_type() { ConnectionType::KeepAlive => { - if inner.flags.contains(Flags::KEEPALIVE_ENABLED) { + if inner.flags.contains(Flags::KEEP_ALIVE_ENABLED) { ConnectionType::KeepAlive } else { ConnectionType::Close @@ -211,7 +223,7 @@ impl Encoder> for ClientCodec { false, inner.version, length, - inner.ctype, + inner.conn_type, &inner.config, )?; } diff --git a/actix-http/src/h1/codec.rs b/actix-http/src/h1/codec.rs index 9a8907579..80afd7455 100644 --- a/actix-http/src/h1/codec.rs +++ b/actix-http/src/h1/codec.rs @@ -15,9 +15,9 @@ use crate::{ bitflags! { struct Flags: u8 { - const HEAD = 0b0000_0001; - const KEEPALIVE_ENABLED = 0b0000_0010; - const STREAM = 0b0000_0100; + const HEAD = 0b0000_0001; + const KEEP_ALIVE_ENABLED = 0b0000_0010; + const STREAM = 0b0000_0100; } } @@ -42,7 +42,9 @@ impl Default for Codec { impl fmt::Debug for Codec { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "h1::Codec({:?})", self.flags) + f.debug_struct("h1::Codec") + .field("flags", &self.flags) + .finish_non_exhaustive() } } @@ -51,8 +53,8 @@ impl Codec { /// /// `keepalive_enabled` how response `connection` header get generated. pub fn new(config: ServiceConfig) -> Self { - let flags = if config.keep_alive_enabled() { - Flags::KEEPALIVE_ENABLED + let flags = if config.keep_alive().enabled() { + Flags::KEEP_ALIVE_ENABLED } else { Flags::empty() }; @@ -76,14 +78,14 @@ impl Codec { /// Check if last response is keep-alive. #[inline] - pub fn keepalive(&self) -> bool { + pub fn keep_alive(&self) -> bool { self.conn_type == ConnectionType::KeepAlive } /// Check if keep-alive enabled on server level. #[inline] - pub fn keepalive_enabled(&self) -> bool { - self.flags.contains(Flags::KEEPALIVE_ENABLED) + pub fn keep_alive_enabled(&self) -> bool { + self.flags.contains(Flags::KEEP_ALIVE_ENABLED) } /// Check last request's message type. @@ -123,11 +125,13 @@ impl Decoder for Codec { self.flags.set(Flags::HEAD, head.method == Method::HEAD); self.version = head.version; self.conn_type = head.connection_type(); + if self.conn_type == ConnectionType::KeepAlive - && !self.flags.contains(Flags::KEEPALIVE_ENABLED) + && !self.flags.contains(Flags::KEEP_ALIVE_ENABLED) { self.conn_type = ConnectionType::Close } + match payload { PayloadType::None => self.payload = None, PayloadType::Payload(pl) => self.payload = Some(pl), @@ -179,9 +183,11 @@ impl Encoder, BodySize)>> for Codec { &self.config, )?; } + Message::Chunk(Some(bytes)) => { self.encoder.encode_chunk(bytes.as_ref(), dst)?; } + Message::Chunk(None) => { self.encoder.encode_eof(dst)?; } diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index fa924f920..203b6c531 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -6,7 +6,7 @@ use http::{ header::{self, HeaderName, HeaderValue}, Method, StatusCode, Uri, Version, }; -use log::{debug, error, trace}; +use tracing::{debug, error, trace}; use super::chunked::ChunkedState; use crate::{error::ParseError, header::HeaderMap, ConnectionType, Request, ResponseHead}; @@ -46,6 +46,23 @@ pub(crate) enum PayloadLength { None, } +impl PayloadLength { + /// Returns true if variant is `None`. + fn is_none(&self) -> bool { + matches!(self, Self::None) + } + + /// Returns true if variant is represents zero-length (not none) payload. + fn is_zero(&self) -> bool { + matches!( + self, + PayloadLength::Payload(PayloadType::Payload(PayloadDecoder { + kind: Kind::Length(0) + })) + ) + } +} + pub(crate) trait MessageType: Sized { fn set_connection_type(&mut self, conn_type: Option); @@ -59,6 +76,7 @@ pub(crate) trait MessageType: Sized { &mut self, slice: &Bytes, raw_headers: &[HeaderIndex], + version: Version, ) -> Result { let mut ka = None; let mut has_upgrade_websocket = false; @@ -87,21 +105,23 @@ pub(crate) trait MessageType: Sized { return Err(ParseError::Header); } - header::CONTENT_LENGTH => match value.to_str() { - Ok(s) if s.trim().starts_with('+') => { - debug!("illegal Content-Length: {:?}", s); + header::CONTENT_LENGTH => match value.to_str().map(str::trim) { + Ok(val) if val.starts_with('+') => { + debug!("illegal Content-Length: {:?}", val); return Err(ParseError::Header); } - Ok(s) => { - if let Ok(len) = s.parse::() { - if len != 0 { - content_length = Some(len); - } + + Ok(val) => { + if let Ok(len) = val.parse::() { + // accept 0 lengths here and remove them in `decode` after all + // headers have been processed to prevent request smuggling issues + content_length = Some(len); } else { - debug!("illegal Content-Length: {:?}", s); + debug!("illegal Content-Length: {:?}", val); return Err(ParseError::Header); } } + Err(_) => { debug!("illegal Content-Length: {:?}", value); return Err(ParseError::Header); @@ -114,22 +134,23 @@ pub(crate) trait MessageType: Sized { return Err(ParseError::Header); } - header::TRANSFER_ENCODING => { + header::TRANSFER_ENCODING if version == Version::HTTP_11 => { seen_te = true; - if let Ok(s) = value.to_str().map(str::trim) { - if s.eq_ignore_ascii_case("chunked") { + if let Ok(val) = value.to_str().map(str::trim) { + if val.eq_ignore_ascii_case("chunked") { chunked = true; - } else if s.eq_ignore_ascii_case("identity") { + } else if val.eq_ignore_ascii_case("identity") { // allow silently since multiple TE headers are already checked } else { - debug!("illegal Transfer-Encoding: {:?}", s); + debug!("illegal Transfer-Encoding: {:?}", val); return Err(ParseError::Header); } } else { return Err(ParseError::Header); } } + // connection keep-alive state header::CONNECTION => { ka = if let Ok(conn) = value.to_str().map(str::trim) { @@ -146,6 +167,7 @@ pub(crate) trait MessageType: Sized { None }; } + header::UPGRADE => { if let Ok(val) = value.to_str().map(str::trim) { if val.eq_ignore_ascii_case("websocket") { @@ -153,19 +175,23 @@ pub(crate) trait MessageType: Sized { } } } + header::EXPECT => { let bytes = value.as_bytes(); if bytes.len() >= 4 && &bytes[0..4] == b"100-" { expect = true; } } + _ => {} } headers.append(name, value); } } + self.set_connection_type(ka); + if expect { self.set_expect() } @@ -209,15 +235,16 @@ impl MessageType for Request { let (len, method, uri, ver, h_len) = { // SAFETY: - // Create an uninitialized array of `MaybeUninit`. The `assume_init` is - // safe because the type we are claiming to have initialized here is a - // bunch of `MaybeUninit`s, which do not require initialization. + // Create an uninitialized array of `MaybeUninit`. The `assume_init` is safe because the + // type we are claiming to have initialized here is a bunch of `MaybeUninit`s, which + // do not require initialization. let mut parsed = unsafe { MaybeUninit::<[MaybeUninit>; MAX_HEADERS]>::uninit() .assume_init() }; let mut req = httparse::Request::new(&mut []); + match req.parse_with_uninit_headers(src, &mut parsed)? { httparse::Status::Complete(len) => { let method = Method::from_bytes(req.method.unwrap().as_bytes()) @@ -232,6 +259,7 @@ impl MessageType for Request { (len, method, uri, version, req.headers.len()) } + httparse::Status::Partial => { return if src.len() >= MAX_BUFFER_SIZE { trace!("MAX_BUFFER_SIZE unprocessed data reached, closing"); @@ -247,7 +275,22 @@ impl MessageType for Request { let mut msg = Request::new(); // convert headers - let length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?; + let mut length = + msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?; + + // disallow HTTP/1.0 POST requests that do not contain a Content-Length headers + // see https://datatracker.ietf.org/doc/html/rfc1945#section-7.2.2 + if ver == Version::HTTP_10 && method == Method::POST && length.is_none() { + debug!("no Content-Length specified for HTTP/1.0 POST request"); + return Err(ParseError::Header); + } + + // Remove CL value if 0 now that all headers and HTTP/1.0 special cases are processed. + // Protects against some request smuggling attacks. + // See https://github.com/actix/actix-web/issues/2767. + if length.is_zero() { + length = PayloadLength::None; + } // payload decoder let decoder = match length { @@ -291,22 +334,35 @@ impl MessageType for ResponseHead { let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY; let (len, ver, status, h_len) = { - let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY; + // SAFETY: + // Create an uninitialized array of `MaybeUninit`. The `assume_init` is safe because the + // type we are claiming to have initialized here is a bunch of `MaybeUninit`s, which + // do not require initialization. + let mut parsed = unsafe { + MaybeUninit::<[MaybeUninit>; MAX_HEADERS]>::uninit() + .assume_init() + }; - let mut res = httparse::Response::new(&mut parsed); - match res.parse(src)? { + let mut res = httparse::Response::new(&mut []); + + let mut config = httparse::ParserConfig::default(); + config.allow_spaces_after_header_name_in_responses(true); + + match config.parse_response_with_uninit_headers(&mut res, src, &mut parsed)? { httparse::Status::Complete(len) => { let version = if res.version.unwrap() == 1 { Version::HTTP_11 } else { Version::HTTP_10 }; + let status = StatusCode::from_u16(res.code.unwrap()) .map_err(|_| ParseError::Status)?; HeaderIndex::record(src, res.headers, &mut headers); (len, version, status, res.headers.len()) } + httparse::Status::Partial => { return if src.len() >= MAX_BUFFER_SIZE { error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); @@ -322,7 +378,15 @@ impl MessageType for ResponseHead { msg.version = ver; // convert headers - let length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?; + let mut length = + msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?; + + // Remove CL value if 0 now that all headers and HTTP/1.0 special cases are processed. + // Protects against some request smuggling attacks. + // See https://github.com/actix/actix-web/issues/2767. + if length.is_zero() { + length = PayloadLength::None; + } // message payload let decoder = if let PayloadLength::Payload(pl) = length { @@ -358,9 +422,6 @@ pub(crate) const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex { pub(crate) const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] = [EMPTY_HEADER_INDEX; MAX_HEADERS]; -pub(crate) const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] = - [httparse::EMPTY_HEADER; MAX_HEADERS]; - impl HeaderIndex { pub(crate) fn record( bytes: &[u8], @@ -594,14 +655,100 @@ mod tests { } #[test] - fn test_parse_post() { - let mut buf = BytesMut::from("POST /test2 HTTP/1.0\r\n\r\n"); + fn parse_h09_reject() { + let mut buf = BytesMut::from( + "GET /test1 HTTP/0.9\r\n\ + \r\n", + ); + + let mut reader = MessageDecoder::::default(); + reader.decode(&mut buf).unwrap_err(); + + let mut buf = BytesMut::from( + "POST /test2 HTTP/0.9\r\n\ + Content-Length: 3\r\n\ + \r\n + abc", + ); + + let mut reader = MessageDecoder::::default(); + reader.decode(&mut buf).unwrap_err(); + } + + #[test] + fn parse_h10_get() { + let mut buf = BytesMut::from( + "GET /test1 HTTP/1.0\r\n\ + \r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); + assert_eq!(req.version(), Version::HTTP_10); + assert_eq!(*req.method(), Method::GET); + assert_eq!(req.path(), "/test1"); + + let mut buf = BytesMut::from( + "GET /test2 HTTP/1.0\r\n\ + Content-Length: 0\r\n\ + \r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); + assert_eq!(req.version(), Version::HTTP_10); + assert_eq!(*req.method(), Method::GET); + assert_eq!(req.path(), "/test2"); + + let mut buf = BytesMut::from( + "GET /test3 HTTP/1.0\r\n\ + Content-Length: 3\r\n\ + \r\n + abc", + ); + + let mut reader = MessageDecoder::::default(); + let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); + assert_eq!(req.version(), Version::HTTP_10); + assert_eq!(*req.method(), Method::GET); + assert_eq!(req.path(), "/test3"); + } + + #[test] + fn parse_h10_post() { + let mut buf = BytesMut::from( + "POST /test1 HTTP/1.0\r\n\ + Content-Length: 3\r\n\ + \r\n\ + abc", + ); + + let mut reader = MessageDecoder::::default(); + let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); + assert_eq!(req.version(), Version::HTTP_10); + assert_eq!(*req.method(), Method::POST); + assert_eq!(req.path(), "/test1"); + + let mut buf = BytesMut::from( + "POST /test2 HTTP/1.0\r\n\ + Content-Length: 0\r\n\ + \r\n", + ); let mut reader = MessageDecoder::::default(); let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); assert_eq!(req.version(), Version::HTTP_10); assert_eq!(*req.method(), Method::POST); assert_eq!(req.path(), "/test2"); + + let mut buf = BytesMut::from( + "POST /test3 HTTP/1.0\r\n\ + \r\n", + ); + + let mut reader = MessageDecoder::::default(); + let err = reader.decode(&mut buf).unwrap_err(); + assert!(err.to_string().contains("Header")) } #[test] @@ -697,121 +844,98 @@ mod tests { #[test] fn test_conn_default_1_0() { - let mut buf = BytesMut::from("GET /test HTTP/1.0\r\n\r\n"); - let req = parse_ready!(&mut buf); - + let req = parse_ready!(&mut BytesMut::from("GET /test HTTP/1.0\r\n\r\n")); assert_eq!(req.head().connection_type(), ConnectionType::Close); } #[test] fn test_conn_default_1_1() { - let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n"); - let req = parse_ready!(&mut buf); - + let req = parse_ready!(&mut BytesMut::from("GET /test HTTP/1.1\r\n\r\n")); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); } #[test] fn test_conn_close() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ connection: close\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - + )); assert_eq!(req.head().connection_type(), ConnectionType::Close); - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ connection: Close\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - + )); assert_eq!(req.head().connection_type(), ConnectionType::Close); } #[test] fn test_conn_close_1_0() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.0\r\n\ connection: close\r\n\r\n", - ); - - let req = parse_ready!(&mut buf); - + )); assert_eq!(req.head().connection_type(), ConnectionType::Close); } #[test] fn test_conn_keep_alive_1_0() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.0\r\n\ connection: keep-alive\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - + )); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.0\r\n\ connection: Keep-Alive\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - + )); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); } #[test] fn test_conn_keep_alive_1_1() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ connection: keep-alive\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - + )); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); } #[test] fn test_conn_other_1_0() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.0\r\n\ connection: other\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - + )); assert_eq!(req.head().connection_type(), ConnectionType::Close); } #[test] fn test_conn_other_1_1() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ connection: other\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - + )); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); } #[test] fn test_conn_upgrade() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ upgrade: websockets\r\n\ connection: upgrade\r\n\r\n", - ); - let req = parse_ready!(&mut buf); + )); assert!(req.upgrade()); assert_eq!(req.head().connection_type(), ConnectionType::Upgrade); - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ upgrade: Websockets\r\n\ connection: Upgrade\r\n\r\n", - ); - let req = parse_ready!(&mut buf); + )); assert!(req.upgrade()); assert_eq!(req.head().connection_type(), ConnectionType::Upgrade); @@ -819,59 +943,62 @@ mod tests { #[test] fn test_conn_upgrade_connect_method() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "CONNECT /test HTTP/1.1\r\n\ content-type: text/plain\r\n\r\n", - ); - let req = parse_ready!(&mut buf); + )); assert!(req.upgrade()); } #[test] - fn test_headers_content_length_err_1() { - let mut buf = BytesMut::from( + fn test_headers_bad_content_length() { + // string CL + expect_parse_err!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ content-length: line\r\n\r\n", - ); + )); - expect_parse_err!(&mut buf) + // negative CL + expect_parse_err!(&mut BytesMut::from( + "GET /test HTTP/1.1\r\n\ + content-length: -1\r\n\r\n", + )); } #[test] - fn test_headers_content_length_err_2() { + fn octal_ish_cl_parsed_as_decimal() { let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - content-length: -1\r\n\r\n", + "POST /test HTTP/1.1\r\n\ + content-length: 011\r\n\r\n", ); - - expect_parse_err!(&mut buf); + let mut reader = MessageDecoder::::default(); + let (_req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + assert!(matches!( + pl, + PayloadType::Payload(pl) if pl == PayloadDecoder::length(11) + )); } #[test] fn test_invalid_header() { - let mut buf = BytesMut::from( + expect_parse_err!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ test line\r\n\r\n", - ); - - expect_parse_err!(&mut buf); + )); } #[test] fn test_invalid_name() { - let mut buf = BytesMut::from( + expect_parse_err!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ test[]: line\r\n\r\n", - ); - - expect_parse_err!(&mut buf); + )); } #[test] fn test_http_request_bad_status_line() { - let mut buf = BytesMut::from("getpath \r\n\r\n"); - expect_parse_err!(&mut buf); + expect_parse_err!(&mut BytesMut::from("getpath \r\n\r\n")); } #[test] @@ -911,11 +1038,10 @@ mod tests { #[test] fn test_http_request_parser_utf8() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ x-test: тест\r\n\r\n", - ); - let req = parse_ready!(&mut buf); + )); assert_eq!( req.headers().get("x-test").unwrap().as_bytes(), @@ -925,24 +1051,18 @@ mod tests { #[test] fn test_http_request_parser_two_slashes() { - let mut buf = BytesMut::from("GET //path HTTP/1.1\r\n\r\n"); - let req = parse_ready!(&mut buf); - + let req = parse_ready!(&mut BytesMut::from("GET //path HTTP/1.1\r\n\r\n")); assert_eq!(req.path(), "//path"); } #[test] fn test_http_request_parser_bad_method() { - let mut buf = BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n"); - - expect_parse_err!(&mut buf); + expect_parse_err!(&mut BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n")); } #[test] fn test_http_request_parser_bad_version() { - let mut buf = BytesMut::from("GET //get HT/11\r\n\r\n"); - - expect_parse_err!(&mut buf); + expect_parse_err!(&mut BytesMut::from("GET //get HT/11\r\n\r\n")); } #[test] @@ -959,29 +1079,66 @@ mod tests { #[test] fn hrs_multiple_content_length() { - let mut buf = BytesMut::from( + expect_parse_err!(&mut BytesMut::from( "GET / HTTP/1.1\r\n\ Host: example.com\r\n\ Content-Length: 4\r\n\ Content-Length: 2\r\n\ \r\n\ abcd", - ); + )); - expect_parse_err!(&mut buf); + expect_parse_err!(&mut BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Content-Length: 0\r\n\ + Content-Length: 2\r\n\ + \r\n\ + ab", + )); } #[test] fn hrs_content_length_plus() { - let mut buf = BytesMut::from( + expect_parse_err!(&mut BytesMut::from( "GET / HTTP/1.1\r\n\ Host: example.com\r\n\ Content-Length: +3\r\n\ \r\n\ 000", + )); + } + + #[test] + fn hrs_te_http10() { + // in HTTP/1.0 transfer encoding is ignored and must therefore contain a CL header + + expect_parse_err!(&mut BytesMut::from( + "POST / HTTP/1.0\r\n\ + Host: example.com\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 3\r\n\ + aaa\r\n\ + 0\r\n\ + ", + )); + } + + #[test] + fn hrs_cl_and_te_http10() { + // in HTTP/1.0 transfer encoding is simply ignored so it's fine to have both + + let mut buf = BytesMut::from( + "GET / HTTP/1.0\r\n\ + Host: example.com\r\n\ + Content-Length: 3\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 000", ); - expect_parse_err!(&mut buf); + parse_ready!(&mut buf); } #[test] diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 13055f08a..c2ddc06ba 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -8,14 +8,14 @@ use std::{ task::{Context, Poll}, }; -use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed, FramedParts}; -use actix_rt::time::{sleep_until, Instant, Sleep}; +use actix_codec::{AsyncRead, AsyncWrite, Decoder as _, Encoder as _, Framed, FramedParts}; +use actix_rt::time::sleep_until; use actix_service::Service; use bitflags::bitflags; use bytes::{Buf, BytesMut}; use futures_core::ready; -use log::{error, trace}; use pin_project_lite::pin_project; +use tracing::{error, trace}; use crate::{ body::{BodySize, BoxBody, MessageBody}, @@ -29,6 +29,7 @@ use super::{ codec::Codec, decoder::MAX_BUFFER_SIZE, payload::{Payload, PayloadSender, PayloadStatus}, + timer::TimerState, Message, MessageType, }; @@ -38,11 +39,23 @@ const MAX_PIPELINED_MESSAGES: usize = 16; bitflags! { pub struct Flags: u8 { - const STARTED = 0b0000_0001; - const KEEPALIVE = 0b0000_0010; - const SHUTDOWN = 0b0000_0100; - const READ_DISCONNECT = 0b0000_1000; - const WRITE_DISCONNECT = 0b0001_0000; + /// Set when stream is read for first time. + const STARTED = 0b0000_0001; + + /// Set when full request-response cycle has occurred. + const FINISHED = 0b0000_0010; + + /// Set if connection is in keep-alive (inactive) state. + const KEEP_ALIVE = 0b0000_0100; + + /// Set if in shutdown procedure. + const SHUTDOWN = 0b0000_1000; + + /// Set if read-half is disconnected. + const READ_DISCONNECT = 0b0001_0000; + + /// Set if write-half is disconnected. + const WRITE_DISCONNECT = 0b0010_0000; } } @@ -89,16 +102,16 @@ pin_project! { U::Error: fmt::Display, { #[pin] - inner: DispatcherState, + pub(super) inner: DispatcherState, // used in tests - poll_count: u64, + pub(super) poll_count: u64, } } pin_project! { #[project = DispatcherStateProj] - enum DispatcherState + pub(super) enum DispatcherState where S: Service, S::Error: Into>, @@ -118,7 +131,7 @@ pin_project! { pin_project! { #[project = InnerDispatcherProj] - struct InnerDispatcher + pub(super) struct InnerDispatcher where S: Service, S::Error: Into>, @@ -132,21 +145,23 @@ pin_project! { U::Error: fmt::Display, { flow: Rc>, - flags: Flags, + pub(super) flags: Flags, peer_addr: Option, conn_data: Option>, + config: ServiceConfig, error: Option, #[pin] - state: State, + pub(super) state: State, + // when Some(_) dispatcher is in state of receiving request payload payload: Option, messages: VecDeque, - ka_expire: Instant, - #[pin] - ka_timer: Option, + head_timer: TimerState, + ka_timer: TimerState, + shutdown_timer: TimerState, - io: Option, + pub(super) io: Option, read_buf: BytesMut, write_buf: BytesMut, codec: Codec, @@ -161,11 +176,10 @@ enum DispatcherMessage { pin_project! { #[project = StateProj] - enum State + pub(super) enum State where S: Service, X: Service, - B: MessageBody, { None, @@ -179,16 +193,40 @@ pin_project! { impl State where S: Service, - X: Service, - B: MessageBody, { - fn is_empty(&self) -> bool { + pub(super) fn is_none(&self) -> bool { matches!(self, State::None) } } +impl fmt::Debug for State +where + S: Service, + X: Service, + B: MessageBody, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "State::None"), + Self::ExpectCall { .. } => { + f.debug_struct("State::ExpectCall").finish_non_exhaustive() + } + Self::ServiceCall { .. } => { + f.debug_struct("State::ServiceCall").finish_non_exhaustive() + } + Self::SendPayload { .. } => { + f.debug_struct("State::SendPayload").finish_non_exhaustive() + } + Self::SendErrorPayload { .. } => f + .debug_struct("State::SendErrorPayload") + .finish_non_exhaustive(), + } + } +} + +#[derive(Debug)] enum PollResponse { Upgrade(Request), DoNothing, @@ -219,33 +257,25 @@ where peer_addr: Option, conn_data: OnConnectData, ) -> Self { - let flags = if config.keep_alive_enabled() { - Flags::KEEPALIVE - } else { - Flags::empty() - }; - - // keep-alive timer - let (ka_expire, ka_timer) = match config.keep_alive_timer() { - Some(delay) => (delay.deadline(), Some(delay)), - None => (config.now(), None), - }; - Dispatcher { inner: DispatcherState::Normal { inner: InnerDispatcher { flow, - flags, + flags: Flags::empty(), peer_addr, conn_data: conn_data.0.map(Rc::new), + config: config.clone(), error: None, state: State::None, payload: None, messages: VecDeque::new(), - ka_expire, - ka_timer, + head_timer: TimerState::new(config.client_request_deadline().is_some()), + ka_timer: TimerState::new(config.keep_alive().enabled()), + shutdown_timer: TimerState::new( + config.client_disconnect_deadline().is_some(), + ), io: Some(io), read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), @@ -286,11 +316,12 @@ where } } - // if checked is set to true, delay disconnect until all tasks have finished. fn client_disconnected(self: Pin<&mut Self>) { let this = self.project(); + this.flags .insert(Flags::READ_DISCONNECT | Flags::WRITE_DISCONNECT); + if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::Incomplete(None)); } @@ -306,9 +337,12 @@ where while written < len { match io.as_mut().poll_write(cx, &write_buf[written..])? { Poll::Ready(0) => { - return Poll::Ready(Err(io::Error::new(io::ErrorKind::WriteZero, ""))) + error!("write zero; closing"); + return Poll::Ready(Err(io::Error::new(io::ErrorKind::WriteZero, ""))); } + Poll::Ready(n) => written += n, + Poll::Pending => { write_buf.advance(written); return Poll::Pending; @@ -316,59 +350,68 @@ where } } - // everything has written to io. clear buffer. + // everything has written to I/O; clear buffer write_buf.clear(); - // flush the io and check if get blocked. + // flush the I/O and check if get blocked io.poll_flush(cx) } fn send_response_inner( self: Pin<&mut Self>, - message: Response<()>, + res: Response<()>, body: &impl MessageBody, ) -> Result { - let size = body.size(); let this = self.project(); + + let size = body.size(); + this.codec - .encode(Message::Item((message, size)), this.write_buf) + .encode(Message::Item((res, size)), this.write_buf) .map_err(|err| { if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::Incomplete(None)); } + DispatchError::Io(err) })?; - this.flags.set(Flags::KEEPALIVE, this.codec.keepalive()); - Ok(size) } fn send_response( mut self: Pin<&mut Self>, - message: Response<()>, + res: Response<()>, body: B, ) -> Result<(), DispatchError> { - let size = self.as_mut().send_response_inner(message, &body)?; - let state = match size { - BodySize::None | BodySize::Sized(0) => State::None, + let size = self.as_mut().send_response_inner(res, &body)?; + let mut this = self.project(); + this.state.set(match size { + BodySize::None | BodySize::Sized(0) => { + this.flags.insert(Flags::FINISHED); + State::None + } _ => State::SendPayload { body }, - }; - self.project().state.set(state); + }); + Ok(()) } fn send_error_response( mut self: Pin<&mut Self>, - message: Response<()>, + res: Response<()>, body: BoxBody, ) -> Result<(), DispatchError> { - let size = self.as_mut().send_response_inner(message, &body)?; - let state = match size { - BodySize::None | BodySize::Sized(0) => State::None, + let size = self.as_mut().send_response_inner(res, &body)?; + let mut this = self.project(); + this.state.set(match size { + BodySize::None | BodySize::Sized(0) => { + this.flags.insert(Flags::FINISHED); + State::None + } _ => State::SendErrorPayload { body }, - }; - self.project().state.set(state); + }); + Ok(()) } @@ -385,63 +428,71 @@ where 'res: loop { let mut this = self.as_mut().project(); match this.state.as_mut().project() { - // no future is in InnerDispatcher state. pop next message. + // no future is in InnerDispatcher state; pop next message StateProj::None => match this.messages.pop_front() { - // handle request message. + // handle request message Some(DispatcherMessage::Item(req)) => { // Handle `EXPECT: 100-Continue` header if req.head().expect() { - // set InnerDispatcher state and continue loop to poll it. + // set InnerDispatcher state and continue loop to poll it let fut = this.flow.expect.call(req); this.state.set(State::ExpectCall { fut }); } else { - // the same as expect call. + // set InnerDispatcher state and continue loop to poll it let fut = this.flow.service.call(req); this.state.set(State::ServiceCall { fut }); }; } - // handle error message. + // handle error message Some(DispatcherMessage::Error(res)) => { - // send_response would update InnerDispatcher state to SendPayload or - // None(If response body is empty). - // continue loop to poll it. + // send_response would update InnerDispatcher state to SendPayload or None + // (If response body is empty) + // continue loop to poll it self.as_mut().send_error_response(res, BoxBody::new(()))?; } - // return with upgrade request and poll it exclusively. + // return with upgrade request and poll it exclusively Some(DispatcherMessage::Upgrade(req)) => { - return Ok(PollResponse::Upgrade(req)); + return Ok(PollResponse::Upgrade(req)) } - // all messages are dealt with. - None => return Ok(PollResponse::DoNothing), + // all messages are dealt with + None => { + // start keep-alive if last request allowed it + this.flags.set(Flags::KEEP_ALIVE, this.codec.keep_alive()); + + return Ok(PollResponse::DoNothing); + } }, - StateProj::ServiceCall { fut } => match fut.poll(cx) { - // service call resolved. send response. - Poll::Ready(Ok(res)) => { - let (res, body) = res.into().replace_body(()); - self.as_mut().send_response(res, body)?; - } - // send service call error as response - Poll::Ready(Err(err)) => { - let res: Response = err.into(); - let (res, body) = res.replace_body(()); - self.as_mut().send_error_response(res, body)?; - } - - // service call pending and could be waiting for more chunk messages. - // (pipeline message limit and/or payload can_read limit) - Poll::Pending => { - // no new message is decoded and no new payload is feed. - // nothing to do except waiting for new incoming data from client. - if !self.as_mut().poll_request(cx)? { - return Ok(PollResponse::DoNothing); + StateProj::ServiceCall { fut } => { + match fut.poll(cx) { + // service call resolved. send response. + Poll::Ready(Ok(res)) => { + let (res, body) = res.into().replace_body(()); + self.as_mut().send_response(res, body)?; + } + + // send service call error as response + Poll::Ready(Err(err)) => { + let res: Response = err.into(); + let (res, body) = res.replace_body(()); + self.as_mut().send_error_response(res, body)?; + } + + // service call pending and could be waiting for more chunk messages + // (pipeline message limit and/or payload can_read limit) + Poll::Pending => { + // no new message is decoded and no new payload is fed + // nothing to do except waiting for new incoming data from client + if !self.as_mut().poll_request(cx)? { + return Ok(PollResponse::DoNothing); + } + // else loop } - // otherwise keep loop. } - }, + } StateProj::SendPayload { mut body } => { // keep populate writer buffer until buffer size limit hit, @@ -455,21 +506,26 @@ where Poll::Ready(None) => { this.codec.encode(Message::Chunk(None), this.write_buf)?; + // payload stream finished. // set state to None and handle next message this.state.set(State::None); + this.flags.insert(Flags::FINISHED); + continue 'res; } Poll::Ready(Some(Err(err))) => { - return Err(DispatchError::Body(err.into())) + this.flags.insert(Flags::FINISHED); + return Err(DispatchError::Body(err.into())); } Poll::Pending => return Ok(PollResponse::DoNothing), } } - // buffer is beyond max size. - // return and try to write the whole buffer to io stream. + + // buffer is beyond max size + // return and try to write the whole buffer to I/O stream. return Ok(PollResponse::DrainWriteBuf); } @@ -487,46 +543,55 @@ where Poll::Ready(None) => { this.codec.encode(Message::Chunk(None), this.write_buf)?; - // payload stream finished. + + // payload stream finished // set state to None and handle next message this.state.set(State::None); + this.flags.insert(Flags::FINISHED); + continue 'res; } Poll::Ready(Some(Err(err))) => { + this.flags.insert(Flags::FINISHED); return Err(DispatchError::Body( Error::new_body().with_cause(err).into(), - )) + )); } Poll::Pending => return Ok(PollResponse::DoNothing), } } - // buffer is beyond max size. - // return and try to write the whole buffer to io stream. + + // buffer is beyond max size + // return and try to write the whole buffer to stream return Ok(PollResponse::DrainWriteBuf); } - StateProj::ExpectCall { fut } => match fut.poll(cx) { - // expect resolved. write continue to buffer and set InnerDispatcher state - // to service call. - Poll::Ready(Ok(req)) => { - this.write_buf - .extend_from_slice(b"HTTP/1.1 100 Continue\r\n\r\n"); - let fut = this.flow.service.call(req); - this.state.set(State::ServiceCall { fut }); - } + StateProj::ExpectCall { fut } => { + trace!(" calling expect service"); - // send expect error as response - Poll::Ready(Err(err)) => { - let res: Response = err.into(); - let (res, body) = res.replace_body(()); - self.as_mut().send_error_response(res, body)?; - } + match fut.poll(cx) { + // expect resolved. write continue to buffer and set InnerDispatcher state + // to service call. + Poll::Ready(Ok(req)) => { + this.write_buf + .extend_from_slice(b"HTTP/1.1 100 Continue\r\n\r\n"); + let fut = this.flow.service.call(req); + this.state.set(State::ServiceCall { fut }); + } - // expect must be solved before progress can be made. - Poll::Pending => return Ok(PollResponse::DoNothing), - }, + // send expect error as response + Poll::Ready(Err(err)) => { + let res: Response = err.into(); + let (res, body) = res.replace_body(()); + self.as_mut().send_error_response(res, body)?; + } + + // expect must be solved before progress can be made. + Poll::Pending => return Ok(PollResponse::DoNothing), + } + } } } } @@ -536,64 +601,76 @@ where req: Request, cx: &mut Context<'_>, ) -> Result<(), DispatchError> { - // Handle `EXPECT: 100-Continue` header - let mut this = self.as_mut().project(); - if req.head().expect() { - // set dispatcher state so the future is pinned. - let fut = this.flow.expect.call(req); - this.state.set(State::ExpectCall { fut }); - } else { - // the same as above. - let fut = this.flow.service.call(req); - this.state.set(State::ServiceCall { fut }); + // initialize dispatcher state + { + let mut this = self.as_mut().project(); + + // Handle `EXPECT: 100-Continue` header + if req.head().expect() { + // set dispatcher state to call expect handler + let fut = this.flow.expect.call(req); + this.state.set(State::ExpectCall { fut }); + } else { + // set dispatcher state to call service handler + let fut = this.flow.service.call(req); + this.state.set(State::ServiceCall { fut }); + }; }; - // eagerly poll the future for once(or twice if expect is resolved immediately). + // eagerly poll the future once (or twice if expect is resolved immediately). loop { match self.as_mut().project().state.project() { StateProj::ExpectCall { fut } => { match fut.poll(cx) { - // expect is resolved. continue loop and poll the service call branch. + // expect is resolved; continue loop and poll the service call branch. Poll::Ready(Ok(req)) => { self.as_mut().send_continue(); + let mut this = self.as_mut().project(); let fut = this.flow.service.call(req); this.state.set(State::ServiceCall { fut }); + continue; } - // future is pending. return Ok(()) to notify that a new state is - // set and the outer loop should be continue. - Poll::Pending => return Ok(()), - // future is error. send response and return a result. On success - // to notify the dispatcher a new state is set and the outer loop - // should be continue. + + // future is error; send response and return a result + // on success to notify the dispatcher a new state is set and the outer loop + // should be continued Poll::Ready(Err(err)) => { let res: Response = err.into(); let (res, body) = res.replace_body(()); return self.send_error_response(res, body); } + + // future is pending; return Ok(()) to notify that a new state is + // set and the outer loop should be continue. + Poll::Pending => return Ok(()), } } + StateProj::ServiceCall { fut } => { // return no matter the service call future's result. return match fut.poll(cx) { - // future is resolved. send response and return a result. On success + // Future is resolved. Send response and return a result. On success // to notify the dispatcher a new state is set and the outer loop // should be continue. Poll::Ready(Ok(res)) => { let (res, body) = res.into().replace_body(()); - self.send_response(res, body) + self.as_mut().send_response(res, body) } - // see the comment on ExpectCall state branch's Pending. + + // see the comment on ExpectCall state branch's Pending Poll::Pending => Ok(()), - // see the comment on ExpectCall state branch's Ready(Err(err)). + + // see the comment on ExpectCall state branch's Ready(Err(_)) Poll::Ready(Err(err)) => { let res: Response = err.into(); let (res, body) = res.replace_body(()); - self.send_error_response(res, body) + self.as_mut().send_error_response(res, body) } }; } + _ => { unreachable!( "State must be set to ServiceCall or ExceptCall in handle_request" @@ -604,67 +681,73 @@ where } /// Process one incoming request. + /// + /// Returns true if any meaningful work was done. fn poll_request( mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Result { + let pipeline_queue_full = self.messages.len() >= MAX_PIPELINED_MESSAGES; + let can_not_read = !self.can_read(cx); + // limit amount of non-processed requests - if self.messages.len() >= MAX_PIPELINED_MESSAGES || !self.can_read(cx) { + if pipeline_queue_full || can_not_read { return Ok(false); } - let mut updated = false; let mut this = self.as_mut().project(); + + let mut updated = false; + + // decode from read buf as many full requests as possible loop { match this.codec.decode(this.read_buf) { Ok(Some(msg)) => { updated = true; - this.flags.insert(Flags::STARTED); match msg { Message::Item(mut req) => { + // head timer only applies to first request on connection + this.head_timer.clear(line!()); + req.head_mut().peer_addr = *this.peer_addr; req.conn_data = this.conn_data.as_ref().map(Rc::clone); match this.codec.message_type() { - // Request is upgradable. add upgrade message and break. - // everything remain in read buffer would be handed to + // request has no payload + MessageType::None => {} + + // Request is upgradable. Add upgrade message and break. + // Everything remaining in read buffer will be handed to // upgraded Request. MessageType::Stream if this.flow.upgrade.is_some() => { this.messages.push_back(DispatcherMessage::Upgrade(req)); break; } - // Request is not upgradable. + // request is not upgradable MessageType::Payload | MessageType::Stream => { - /* - PayloadSender and Payload are smart pointers share the - same state. - PayloadSender is attached to dispatcher and used to sink - new chunked request data to state. - Payload is attached to Request and passed to Service::call - where the state can be collected and consumed. - */ + // PayloadSender and Payload are smart pointers share the + // same state. PayloadSender is attached to dispatcher and used + // to sink new chunked request data to state. Payload is + // attached to Request and passed to Service::call where the + // state can be collected and consumed. let (sender, payload) = Payload::create(false); - let (req1, _) = - req.replace_payload(crate::Payload::H1 { payload }); - req = req1; + *req.payload() = crate::Payload::H1 { payload }; *this.payload = Some(sender); } - - // Request has no payload. - MessageType::None => {} } // handle request early when no future in InnerDispatcher state. - if this.state.is_empty() { + if this.state.is_none() { self.as_mut().handle_request(req, cx)?; this = self.as_mut().project(); } else { this.messages.push_back(DispatcherMessage::Item(req)); } } + Message::Chunk(Some(chunk)) => { if let Some(ref mut payload) = this.payload { payload.feed_data(chunk); @@ -678,6 +761,7 @@ where break; } } + Message::Chunk(None) => { if let Some(mut payload) = this.payload.take() { payload.feed_eof(); @@ -693,38 +777,51 @@ where } } } - // decode is partial and buffer is not full yet. - // break and wait for more read. + + // decode is partial and buffer is not full yet + // break and wait for more read Ok(None) => break, + Err(ParseError::Io(err)) => { + trace!("I/O error: {}", &err); self.as_mut().client_disconnected(); this = self.as_mut().project(); *this.error = Some(DispatchError::Io(err)); break; } + Err(ParseError::TooLarge) => { + trace!("request head was too big; returning 431 response"); + if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::Overflow); } - // Requests overflow buffer size should be responded with 431 + + // request heads that overflow buffer size return a 431 error this.messages .push_back(DispatcherMessage::Error(Response::with_body( StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE, (), ))); + this.flags.insert(Flags::READ_DISCONNECT); *this.error = Some(ParseError::TooLarge.into()); + break; } + Err(err) => { + trace!("parse error {}", &err); + if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::EncodingCorrupted); } - // Malformed requests should be responded with 400 + // malformed requests should be responded with 400 this.messages.push_back(DispatcherMessage::Error( Response::bad_request().drop_body(), )); + this.flags.insert(Flags::READ_DISCONNECT); *this.error = Some(err.into()); break; @@ -732,92 +829,118 @@ where } } - if updated && this.ka_timer.is_some() { - if let Some(expire) = this.codec.config().keep_alive_expire() { - *this.ka_expire = expire; - } - } Ok(updated) } - /// keep-alive timer - fn poll_keepalive( + fn poll_head_timer( mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Result<(), DispatchError> { - let mut this = self.as_mut().project(); + let this = self.as_mut().project(); - // when a branch is not explicit return early it's meant to fall through - // and return as Ok(()) - match this.ka_timer.as_mut().as_pin_mut() { - None => { - // conditionally go into shutdown timeout - if this.flags.contains(Flags::SHUTDOWN) { - if let Some(deadline) = this.codec.config().client_disconnect_timer() { - // write client disconnect time out and poll again to - // go into Some> branch - this.ka_timer.set(Some(sleep_until(deadline))); - return self.poll_keepalive(cx); - } - } - } - Some(mut timer) => { - // only operate when keep-alive timer is resolved. - if timer.as_mut().poll(cx).is_ready() { - // got timeout during shutdown, drop connection - if this.flags.contains(Flags::SHUTDOWN) { - return Err(DispatchError::DisconnectTimeout); - // exceed deadline. check for any outstanding tasks - } else if timer.deadline() >= *this.ka_expire { - // have no task at hand. - if this.state.is_empty() && this.write_buf.is_empty() { - if this.flags.contains(Flags::STARTED) { - trace!("Keep-alive timeout, close connection"); - this.flags.insert(Flags::SHUTDOWN); + if let TimerState::Active { timer } = this.head_timer { + if timer.as_mut().poll(cx).is_ready() { + // timeout on first request (slow request) return 408 - // start shutdown timeout - if let Some(deadline) = - this.codec.config().client_disconnect_timer() - { - timer.as_mut().reset(deadline); - let _ = timer.poll(cx); - } else { - // no shutdown timeout, drop socket - this.flags.insert(Flags::WRITE_DISCONNECT); - } - } else { - // timeout on first request (slow request) return 408 - trace!("Slow request timeout"); - let _ = self.as_mut().send_error_response( - Response::with_body(StatusCode::REQUEST_TIMEOUT, ()), - BoxBody::new(()), - ); - this = self.project(); - this.flags.insert(Flags::STARTED | Flags::SHUTDOWN); - } - // still have unfinished task. try to reset and register keep-alive. - } else if let Some(deadline) = this.codec.config().keep_alive_expire() { - timer.as_mut().reset(deadline); - let _ = timer.poll(cx); - } - // timer resolved but still have not met the keep-alive expire deadline. - // reset and register for later wakeup. - } else { - timer.as_mut().reset(*this.ka_expire); - let _ = timer.poll(cx); - } - } + trace!("timed out on slow request; replying with 408 and closing connection"); + + let _ = self.as_mut().send_error_response( + Response::with_body(StatusCode::REQUEST_TIMEOUT, ()), + BoxBody::new(()), + ); + + self.project().flags.insert(Flags::SHUTDOWN); } - } + }; + Ok(()) } - /// Returns true when io stream can be disconnected after write to it. + fn poll_ka_timer( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Result<(), DispatchError> { + let this = self.as_mut().project(); + if let TimerState::Active { timer } = this.ka_timer { + debug_assert!( + this.flags.contains(Flags::KEEP_ALIVE), + "keep-alive flag should be set when timer is active", + ); + debug_assert!( + this.state.is_none(), + "dispatcher should not be in keep-alive phase if state is not none: {:?}", + this.state, + ); + + // Assert removed by @robjtede on account of issue #2655. There are cases where an I/O + // flush can be pending after entering the keep-alive state causing the subsequent flush + // wake up to panic here. This appears to be a Linux-only problem. Leaving original code + // below for posterity because a simple and reliable test could not be found to trigger + // the behavior. + // debug_assert!( + // this.write_buf.is_empty(), + // "dispatcher should not be in keep-alive phase if write_buf is not empty", + // ); + + // keep-alive timer has timed out + if timer.as_mut().poll(cx).is_ready() { + // no tasks at hand + trace!("timer timed out; closing connection"); + this.flags.insert(Flags::SHUTDOWN); + + if let Some(deadline) = this.config.client_disconnect_deadline() { + // start shutdown timeout if enabled + this.shutdown_timer + .set_and_init(cx, sleep_until(deadline.into()), line!()); + } else { + // no shutdown timeout, drop socket + this.flags.insert(Flags::WRITE_DISCONNECT); + } + } + } + + Ok(()) + } + + fn poll_shutdown_timer( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Result<(), DispatchError> { + let this = self.as_mut().project(); + if let TimerState::Active { timer } = this.shutdown_timer { + debug_assert!( + this.flags.contains(Flags::SHUTDOWN), + "shutdown flag should be set when timer is active", + ); + + // timed-out during shutdown; drop connection + if timer.as_mut().poll(cx).is_ready() { + trace!("timed-out during shutdown"); + return Err(DispatchError::DisconnectTimeout); + } + } + + Ok(()) + } + + /// Poll head, keep-alive, and disconnect timer. + fn poll_timers( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Result<(), DispatchError> { + self.as_mut().poll_head_timer(cx)?; + self.as_mut().poll_ka_timer(cx)?; + self.as_mut().poll_shutdown_timer(cx)?; + + Ok(()) + } + + /// Returns true when I/O stream can be disconnected after write to it. /// /// It covers these conditions: - /// - `std::io::ErrorKind::ConnectionReset` after partial read. + /// - `std::io::ErrorKind::ConnectionReset` after partial read; /// - all data read done. - #[inline(always)] + #[inline(always)] // TODO: bench this inline fn read_available( self: Pin<&mut Self>, cx: &mut Context<'_>, @@ -846,13 +969,12 @@ where // When read_buf is beyond max buffer size the early return could be successfully // be parsed as a new Request. This case would not generate ParseError::TooLarge and // at this point IO stream is not fully read to Pending and would result in - // dispatcher stuck until timeout (KA) + // dispatcher stuck until timeout (keep-alive). // // Note: // This is a perf choice to reduce branch on ::decode. // - // A Request head too large to parse is only checked on - // `httparse::Status::Partial` condition. + // A Request head too large to parse is only checked on `httparse::Status::Partial`. if this.payload.is_none() { // When dispatcher has a payload the responsibility of wake up it would be shift @@ -881,18 +1003,29 @@ where match actix_codec::poll_read_buf(io.as_mut(), cx, this.read_buf) { Poll::Ready(Ok(n)) => { + this.flags.remove(Flags::FINISHED); + if n == 0 { return Ok(true); } + read_some = true; } - Poll::Pending => return Ok(false), + + Poll::Pending => { + return Ok(false); + } + Poll::Ready(Err(err)) => { return match err.kind() { + // convert WouldBlock error to the same as Pending return io::ErrorKind::WouldBlock => Ok(false), + + // connection reset after partial read io::ErrorKind::ConnectionReset if read_some => Ok(true), + _ => Err(DispatchError::Io(err)), - } + }; } } } @@ -940,27 +1073,60 @@ where } match this.inner.project() { - DispatcherStateProj::Normal { mut inner } => { - inner.as_mut().poll_keepalive(cx)?; + DispatcherStateProj::Upgrade { fut: upgrade } => upgrade.poll(cx).map_err(|err| { + error!("Upgrade handler error: {}", err); + DispatchError::Upgrade + }), - if inner.flags.contains(Flags::SHUTDOWN) { + DispatcherStateProj::Normal { mut inner } => { + trace!("start flags: {:?}", &inner.flags); + + trace_timer_states( + "start", + &inner.head_timer, + &inner.ka_timer, + &inner.shutdown_timer, + ); + + inner.as_mut().poll_timers(cx)?; + + let poll = if inner.flags.contains(Flags::SHUTDOWN) { if inner.flags.contains(Flags::WRITE_DISCONNECT) { Poll::Ready(Ok(())) } else { - // flush buffer and wait on blocked. + // flush buffer and wait on blocked ready!(inner.as_mut().poll_flush(cx))?; - Pin::new(inner.project().io.as_mut().unwrap()) + Pin::new(inner.as_mut().project().io.as_mut().unwrap()) .poll_shutdown(cx) .map_err(DispatchError::from) } } else { - // read from io stream and fill read buffer. + // read from I/O stream and fill read buffer let should_disconnect = inner.as_mut().read_available(cx)?; + // after reading something from stream, clear keep-alive timer + if !inner.read_buf.is_empty() && inner.flags.contains(Flags::KEEP_ALIVE) { + let inner = inner.as_mut().project(); + inner.flags.remove(Flags::KEEP_ALIVE); + inner.ka_timer.clear(line!()); + } + + if !inner.flags.contains(Flags::STARTED) { + inner.as_mut().project().flags.insert(Flags::STARTED); + + if let Some(deadline) = inner.config.client_request_deadline() { + inner.as_mut().project().head_timer.set_and_init( + cx, + sleep_until(deadline.into()), + line!(), + ); + } + } + inner.as_mut().poll_request(cx)?; - // io stream should to be closed. if should_disconnect { + // I/O stream should to be closed let inner = inner.as_mut().project(); inner.flags.insert(Flags::READ_DISCONNECT); if let Some(mut payload) = inner.payload.take() { @@ -969,11 +1135,27 @@ where }; loop { - // poll_response and populate write buffer. - // drain indicate if write buffer should be emptied before next run. + // poll response to populate write buffer + // drain indicates whether write buffer should be emptied before next run let drain = match inner.as_mut().poll_response(cx)? { PollResponse::DrainWriteBuf => true, - PollResponse::DoNothing => false, + + PollResponse::DoNothing => { + // KEEP_ALIVE is set in send_response_inner if client allows it + // FINISHED is set after writing last chunk of response + if inner.flags.contains(Flags::KEEP_ALIVE | Flags::FINISHED) { + if let Some(timer) = inner.config.keep_alive_deadline() { + inner.as_mut().project().ka_timer.set_and_init( + cx, + sleep_until(timer.into()), + line!(), + ); + } + } + + false + } + // upgrade request and goes Upgrade variant of DispatcherState. PollResponse::Upgrade(req) => { let upgrade = inner.upgrade(req); @@ -985,451 +1167,96 @@ where } }; - // we didn't get WouldBlock from write operation, - // so data get written to kernel completely (macOS) - // and we have to write again otherwise response can get stuck + // we didn't get WouldBlock from write operation, so data get written to + // kernel completely (macOS) and we have to write again otherwise response + // can get stuck // - // TODO: what? is WouldBlock good or bad? - // want to find a reference for this macOS behavior - if inner.as_mut().poll_flush(cx)?.is_pending() || !drain { + // TODO: want to find a reference for this behavior + // see introduced commit: 3872d3ba + let flush_was_ready = inner.as_mut().poll_flush(cx)?.is_ready(); + + // this assert seems to always be true but not willing to commit to it until + // we understand what Nikolay meant when writing the above comment + // debug_assert!(flush_was_ready); + + if !flush_was_ready || !drain { break; } } // client is gone if inner.flags.contains(Flags::WRITE_DISCONNECT) { + trace!("client is gone; disconnecting"); return Poll::Ready(Ok(())); } - let is_empty = inner.state.is_empty(); - let inner_p = inner.as_mut().project(); - // read half is closed and we do not processing any responses - if inner_p.flags.contains(Flags::READ_DISCONNECT) && is_empty { + let state_is_none = inner_p.state.is_none(); + + // read half is closed; we do not process any responses + if inner_p.flags.contains(Flags::READ_DISCONNECT) && state_is_none { + trace!("read half closed; start shutdown"); inner_p.flags.insert(Flags::SHUTDOWN); } // keep-alive and stream errors - if is_empty && inner_p.write_buf.is_empty() { + if state_is_none && inner_p.write_buf.is_empty() { if let Some(err) = inner_p.error.take() { - Poll::Ready(Err(err)) + error!("stream error: {}", &err); + return Poll::Ready(Err(err)); } + // disconnect if keep-alive is not enabled - else if inner_p.flags.contains(Flags::STARTED) - && !inner_p.flags.intersects(Flags::KEEPALIVE) + if inner_p.flags.contains(Flags::FINISHED) + && !inner_p.flags.contains(Flags::KEEP_ALIVE) { + inner_p.flags.remove(Flags::FINISHED); inner_p.flags.insert(Flags::SHUTDOWN); - self.poll(cx) + return self.poll(cx); } + // disconnect if shutdown - else if inner_p.flags.contains(Flags::SHUTDOWN) { - self.poll(cx) - } else { - Poll::Pending + if inner_p.flags.contains(Flags::SHUTDOWN) { + return self.poll(cx); } - } else { - Poll::Pending } - } + + trace_timer_states( + "end", + inner_p.head_timer, + inner_p.ka_timer, + inner_p.shutdown_timer, + ); + + Poll::Pending + }; + + trace!("end flags: {:?}", &inner.flags); + + poll } - DispatcherStateProj::Upgrade { fut: upgrade } => upgrade.poll(cx).map_err(|err| { - error!("Upgrade handler error: {}", err); - DispatchError::Upgrade - }), } } } -#[cfg(test)] -mod tests { - use std::str; +#[allow(dead_code)] +fn trace_timer_states( + label: &str, + head_timer: &TimerState, + ka_timer: &TimerState, + shutdown_timer: &TimerState, +) { + trace!("{} timers:", label); - use actix_service::fn_service; - use actix_utils::future::{ready, Ready}; - use bytes::Bytes; - use futures_util::future::lazy; - - use super::*; - use crate::{ - error::Error, - h1::{ExpectHandler, UpgradeHandler}, - test::{TestBuffer, TestSeqBuffer}, - HttpMessage, KeepAlive, Method, - }; - - fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option { - haystack[from..] - .windows(needle.len()) - .position(|window| window == needle) + if head_timer.is_enabled() { + trace!(" head {}", &head_timer); } - fn stabilize_date_header(payload: &mut [u8]) { - let mut from = 0; - - while let Some(pos) = find_slice(payload, b"date", from) { - payload[(from + pos)..(from + pos + 35)] - .copy_from_slice(b"date: Thu, 01 Jan 1970 12:34:56 UTC"); - from += 35; - } + if ka_timer.is_enabled() { + trace!(" keep-alive {}", &ka_timer); } - fn ok_service( - ) -> impl Service, Error = Error> { - fn_service(|_req: Request| ready(Ok::<_, Error>(Response::ok()))) - } - - fn echo_path_service( - ) -> impl Service, Error = Error> { - fn_service(|req: Request| { - let path = req.path().as_bytes(); - ready(Ok::<_, Error>( - Response::ok().set_body(Bytes::copy_from_slice(path)), - )) - }) - } - - fn echo_payload_service() -> impl Service, Error = Error> - { - fn_service(|mut req: Request| { - Box::pin(async move { - use futures_util::stream::StreamExt as _; - - let mut pl = req.take_payload(); - let mut body = BytesMut::new(); - while let Some(chunk) = pl.next().await { - body.extend_from_slice(chunk.unwrap().chunk()) - } - - Ok::<_, Error>(Response::ok().set_body(body.freeze())) - }) - }) - } - - #[actix_rt::test] - async fn test_req_parse_err() { - lazy(|cx| { - let buf = TestBuffer::new("GET /test HTTP/1\r\n\r\n"); - - let services = HttpFlow::new(ok_service(), ExpectHandler, None); - - let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( - buf, - services, - ServiceConfig::default(), - None, - OnConnectData::default(), - ); - - actix_rt::pin!(h1); - - match h1.as_mut().poll(cx) { - Poll::Pending => panic!(), - Poll::Ready(res) => assert!(res.is_err()), - } - - if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() { - assert!(inner.flags.contains(Flags::READ_DISCONNECT)); - assert_eq!( - &inner.project().io.take().unwrap().write_buf[..26], - b"HTTP/1.1 400 Bad Request\r\n" - ); - } - }) - .await; - } - - #[actix_rt::test] - async fn test_pipelining() { - lazy(|cx| { - let buf = TestBuffer::new( - "\ - GET /abcd HTTP/1.1\r\n\r\n\ - GET /def HTTP/1.1\r\n\r\n\ - ", - ); - - let cfg = ServiceConfig::new(KeepAlive::Disabled, 1, 1, false, None); - - let services = HttpFlow::new(echo_path_service(), ExpectHandler, None); - - let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( - buf, - services, - cfg, - None, - OnConnectData::default(), - ); - - actix_rt::pin!(h1); - - assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); - - match h1.as_mut().poll(cx) { - Poll::Pending => panic!("first poll should not be pending"), - Poll::Ready(res) => assert!(res.is_ok()), - } - - // polls: initial => shutdown - assert_eq!(h1.poll_count, 2); - - if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() { - let res = &mut inner.project().io.take().unwrap().write_buf[..]; - stabilize_date_header(res); - - let exp = b"\ - HTTP/1.1 200 OK\r\n\ - content-length: 5\r\n\ - connection: close\r\n\ - date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\ - /abcd\ - HTTP/1.1 200 OK\r\n\ - content-length: 4\r\n\ - connection: close\r\n\ - date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\ - /def\ - "; - - assert_eq!(res.to_vec(), exp.to_vec()); - } - }) - .await; - - lazy(|cx| { - let buf = TestBuffer::new( - "\ - GET /abcd HTTP/1.1\r\n\r\n\ - GET /def HTTP/1\r\n\r\n\ - ", - ); - - let cfg = ServiceConfig::new(KeepAlive::Disabled, 1, 1, false, None); - - let services = HttpFlow::new(echo_path_service(), ExpectHandler, None); - - let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( - buf, - services, - cfg, - None, - OnConnectData::default(), - ); - - actix_rt::pin!(h1); - - assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); - - match h1.as_mut().poll(cx) { - Poll::Pending => panic!("first poll should not be pending"), - Poll::Ready(res) => assert!(res.is_err()), - } - - // polls: initial => shutdown - assert_eq!(h1.poll_count, 1); - - if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() { - let res = &mut inner.project().io.take().unwrap().write_buf[..]; - stabilize_date_header(res); - - let exp = b"\ - HTTP/1.1 200 OK\r\n\ - content-length: 5\r\n\ - connection: close\r\n\ - date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\ - /abcd\ - HTTP/1.1 400 Bad Request\r\n\ - content-length: 0\r\n\ - connection: close\r\n\ - date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\ - "; - - assert_eq!(res.to_vec(), exp.to_vec()); - } - }) - .await; - } - - #[actix_rt::test] - async fn test_expect() { - lazy(|cx| { - let mut buf = TestSeqBuffer::empty(); - let cfg = ServiceConfig::new(KeepAlive::Disabled, 0, 0, false, None); - - let services = HttpFlow::new(echo_payload_service(), ExpectHandler, None); - - let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( - buf.clone(), - services, - cfg, - None, - OnConnectData::default(), - ); - - buf.extend_read_buf( - "\ - POST /upload HTTP/1.1\r\n\ - Content-Length: 5\r\n\ - Expect: 100-continue\r\n\ - \r\n\ - ", - ); - - actix_rt::pin!(h1); - - assert!(h1.as_mut().poll(cx).is_pending()); - assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); - - // polls: manual - assert_eq!(h1.poll_count, 1); - eprintln!("poll count: {}", h1.poll_count); - - if let DispatcherState::Normal { ref inner } = h1.inner { - let io = inner.io.as_ref().unwrap(); - let res = &io.write_buf()[..]; - assert_eq!( - str::from_utf8(res).unwrap(), - "HTTP/1.1 100 Continue\r\n\r\n" - ); - } - - buf.extend_read_buf("12345"); - assert!(h1.as_mut().poll(cx).is_ready()); - - // polls: manual manual shutdown - assert_eq!(h1.poll_count, 3); - - if let DispatcherState::Normal { ref inner } = h1.inner { - let io = inner.io.as_ref().unwrap(); - let mut res = (&io.write_buf()[..]).to_owned(); - stabilize_date_header(&mut res); - - assert_eq!( - str::from_utf8(&res).unwrap(), - "\ - HTTP/1.1 100 Continue\r\n\ - \r\n\ - HTTP/1.1 200 OK\r\n\ - content-length: 5\r\n\ - connection: close\r\n\ - date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\ - \r\n\ - 12345\ - " - ); - } - }) - .await; - } - - #[actix_rt::test] - async fn test_eager_expect() { - lazy(|cx| { - let mut buf = TestSeqBuffer::empty(); - let cfg = ServiceConfig::new(KeepAlive::Disabled, 0, 0, false, None); - - let services = HttpFlow::new(echo_path_service(), ExpectHandler, None); - - let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( - buf.clone(), - services, - cfg, - None, - OnConnectData::default(), - ); - - buf.extend_read_buf( - "\ - POST /upload HTTP/1.1\r\n\ - Content-Length: 5\r\n\ - Expect: 100-continue\r\n\ - \r\n\ - ", - ); - - actix_rt::pin!(h1); - - assert!(h1.as_mut().poll(cx).is_ready()); - assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); - - // polls: manual shutdown - assert_eq!(h1.poll_count, 2); - - if let DispatcherState::Normal { ref inner } = h1.inner { - let io = inner.io.as_ref().unwrap(); - let mut res = (&io.write_buf()[..]).to_owned(); - stabilize_date_header(&mut res); - - // Despite the content-length header and even though the request payload has not - // been sent, this test expects a complete service response since the payload - // is not used at all. The service passed to dispatcher is path echo and doesn't - // consume payload bytes. - assert_eq!( - str::from_utf8(&res).unwrap(), - "\ - HTTP/1.1 100 Continue\r\n\ - \r\n\ - HTTP/1.1 200 OK\r\n\ - content-length: 7\r\n\ - connection: close\r\n\ - date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\ - \r\n\ - /upload\ - " - ); - } - }) - .await; - } - - #[actix_rt::test] - async fn test_upgrade() { - struct TestUpgrade; - - impl Service<(Request, Framed)> for TestUpgrade { - type Response = (); - type Error = Error; - type Future = Ready>; - - actix_service::always_ready!(); - - fn call(&self, (req, _framed): (Request, Framed)) -> Self::Future { - assert_eq!(req.method(), Method::GET); - assert!(req.upgrade()); - assert_eq!(req.headers().get("upgrade").unwrap(), "websocket"); - ready(Ok(())) - } - } - - lazy(|cx| { - let mut buf = TestSeqBuffer::empty(); - let cfg = ServiceConfig::new(KeepAlive::Disabled, 0, 0, false, None); - - let services = HttpFlow::new(ok_service(), ExpectHandler, Some(TestUpgrade)); - - let h1 = Dispatcher::<_, _, _, _, TestUpgrade>::new( - buf.clone(), - services, - cfg, - None, - OnConnectData::default(), - ); - - buf.extend_read_buf( - "\ - GET /ws HTTP/1.1\r\n\ - Connection: Upgrade\r\n\ - Upgrade: websocket\r\n\ - \r\n\ - ", - ); - - actix_rt::pin!(h1); - - assert!(h1.as_mut().poll(cx).is_ready()); - assert!(matches!(&h1.inner, DispatcherState::Upgrade { .. })); - - // polls: manual shutdown - assert_eq!(h1.poll_count, 2); - }) - .await; + if shutdown_timer.is_enabled() { + trace!(" shutdown {}", &shutdown_timer); } } diff --git a/actix-http/src/h1/dispatcher_tests.rs b/actix-http/src/h1/dispatcher_tests.rs new file mode 100644 index 000000000..b3ee3d2bb --- /dev/null +++ b/actix-http/src/h1/dispatcher_tests.rs @@ -0,0 +1,976 @@ +use std::{future::Future, str, task::Poll, time::Duration}; + +use actix_rt::{pin, time::sleep}; +use actix_service::fn_service; +use actix_utils::future::{ready, Ready}; +use bytes::Bytes; +use futures_util::future::lazy; + +use actix_codec::Framed; +use actix_service::Service; +use bytes::{Buf, BytesMut}; + +use super::dispatcher::{Dispatcher, DispatcherState, DispatcherStateProj, Flags}; +use crate::{ + body::MessageBody, + config::ServiceConfig, + h1::{Codec, ExpectHandler, UpgradeHandler}, + service::HttpFlow, + test::{TestBuffer, TestSeqBuffer}, + Error, HttpMessage, KeepAlive, Method, OnConnectData, Request, Response, StatusCode, +}; + +fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option { + memchr::memmem::find(&haystack[from..], needle) +} + +fn stabilize_date_header(payload: &mut [u8]) { + let mut from = 0; + while let Some(pos) = find_slice(payload, b"date", from) { + payload[(from + pos)..(from + pos + 35)] + .copy_from_slice(b"date: Thu, 01 Jan 1970 12:34:56 UTC"); + from += 35; + } +} + +fn ok_service() -> impl Service, Error = Error> { + status_service(StatusCode::OK) +} + +fn status_service( + status: StatusCode, +) -> impl Service, Error = Error> { + fn_service(move |_req: Request| ready(Ok::<_, Error>(Response::new(status)))) +} + +fn echo_path_service( +) -> impl Service, Error = Error> { + fn_service(|req: Request| { + let path = req.path().as_bytes(); + ready(Ok::<_, Error>( + Response::ok().set_body(Bytes::copy_from_slice(path)), + )) + }) +} + +fn drop_payload_service( +) -> impl Service, Error = Error> { + fn_service(|mut req: Request| async move { + let _ = req.take_payload(); + Ok::<_, Error>(Response::with_body(StatusCode::OK, "payload dropped")) + }) +} + +fn echo_payload_service() -> impl Service, Error = Error> { + fn_service(|mut req: Request| { + Box::pin(async move { + use futures_util::stream::StreamExt as _; + + let mut pl = req.take_payload(); + let mut body = BytesMut::new(); + while let Some(chunk) = pl.next().await { + body.extend_from_slice(chunk.unwrap().chunk()) + } + + Ok::<_, Error>(Response::ok().set_body(body.freeze())) + }) + }) +} + +#[actix_rt::test] +async fn late_request() { + let mut buf = TestBuffer::empty(); + + let cfg = ServiceConfig::new( + KeepAlive::Disabled, + Duration::from_millis(100), + Duration::ZERO, + false, + None, + ); + let services = HttpFlow::new(ok_service(), ExpectHandler, None); + + let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( + buf.clone(), + services, + cfg, + None, + OnConnectData::default(), + ); + pin!(h1); + + lazy(|cx| { + assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); + + match h1.as_mut().poll(cx) { + Poll::Ready(_) => panic!("first poll should not be ready"), + Poll::Pending => {} + } + + // polls: initial + assert_eq!(h1.poll_count, 1); + + buf.extend_read_buf("GET /abcd HTTP/1.1\r\nConnection: close\r\n\r\n"); + + match h1.as_mut().poll(cx) { + Poll::Pending => panic!("second poll should not be pending"), + Poll::Ready(res) => assert!(res.is_ok()), + } + + // polls: initial pending => handle req => shutdown + assert_eq!(h1.poll_count, 3); + + let mut res = buf.take_write_buf().to_vec(); + stabilize_date_header(&mut res); + let res = &res[..]; + + let exp = b"\ + HTTP/1.1 200 OK\r\n\ + content-length: 0\r\n\ + connection: close\r\n\ + date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\ + "; + + assert_eq!( + res, + exp, + "\nexpected response not in write buffer:\n\ + response: {:?}\n\ + expected: {:?}", + String::from_utf8_lossy(res), + String::from_utf8_lossy(exp) + ); + }) + .await; +} + +#[actix_rt::test] +async fn oneshot_connection() { + let buf = TestBuffer::new("GET /abcd HTTP/1.1\r\n\r\n"); + + let cfg = ServiceConfig::new( + KeepAlive::Disabled, + Duration::from_millis(100), + Duration::ZERO, + false, + None, + ); + let services = HttpFlow::new(echo_path_service(), ExpectHandler, None); + + let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( + buf.clone(), + services, + cfg, + None, + OnConnectData::default(), + ); + pin!(h1); + + lazy(|cx| { + assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); + + match h1.as_mut().poll(cx) { + Poll::Pending => panic!("first poll should not be pending"), + Poll::Ready(res) => assert!(res.is_ok()), + } + + // polls: initial => shutdown + assert_eq!(h1.poll_count, 2); + + let mut res = buf.take_write_buf().to_vec(); + stabilize_date_header(&mut res); + let res = &res[..]; + + let exp = http_msg( + r" + HTTP/1.1 200 OK + content-length: 5 + connection: close + date: Thu, 01 Jan 1970 12:34:56 UTC + + /abcd + ", + ); + + assert_eq!( + res, + exp, + "\nexpected response not in write buffer:\n\ + response: {:?}\n\ + expected: {:?}", + String::from_utf8_lossy(res), + String::from_utf8_lossy(&exp) + ); + }) + .await; +} + +#[actix_rt::test] +async fn keep_alive_timeout() { + let buf = TestBuffer::new("GET /abcd HTTP/1.1\r\n\r\n"); + + let cfg = ServiceConfig::new( + KeepAlive::Timeout(Duration::from_millis(200)), + Duration::from_millis(100), + Duration::ZERO, + false, + None, + ); + let services = HttpFlow::new(echo_path_service(), ExpectHandler, None); + + let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( + buf.clone(), + services, + cfg, + None, + OnConnectData::default(), + ); + pin!(h1); + + lazy(|cx| { + assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); + + assert!( + h1.as_mut().poll(cx).is_pending(), + "keep-alive should prevent poll from resolving" + ); + + // polls: initial + assert_eq!(h1.poll_count, 1); + + let mut res = buf.take_write_buf().to_vec(); + stabilize_date_header(&mut res); + let res = &res[..]; + + let exp = b"\ + HTTP/1.1 200 OK\r\n\ + content-length: 5\r\n\ + date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\ + /abcd\ + "; + + assert_eq!( + res, + exp, + "\nexpected response not in write buffer:\n\ + response: {:?}\n\ + expected: {:?}", + String::from_utf8_lossy(res), + String::from_utf8_lossy(exp) + ); + }) + .await; + + // sleep slightly longer than keep-alive timeout + sleep(Duration::from_millis(250)).await; + + lazy(|cx| { + assert!( + h1.as_mut().poll(cx).is_ready(), + "keep-alive should have resolved", + ); + + // polls: initial => keep-alive wake-up shutdown + assert_eq!(h1.poll_count, 2); + + if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() { + // connection closed + assert!(inner.flags.contains(Flags::SHUTDOWN)); + assert!(inner.flags.contains(Flags::WRITE_DISCONNECT)); + // and nothing added to write buffer + assert!(buf.write_buf_slice().is_empty()); + } + }) + .await; +} + +#[actix_rt::test] +async fn keep_alive_follow_up_req() { + let mut buf = TestBuffer::new("GET /abcd HTTP/1.1\r\n\r\n"); + + let cfg = ServiceConfig::new( + KeepAlive::Timeout(Duration::from_millis(500)), + Duration::from_millis(100), + Duration::ZERO, + false, + None, + ); + let services = HttpFlow::new(echo_path_service(), ExpectHandler, None); + + let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( + buf.clone(), + services, + cfg, + None, + OnConnectData::default(), + ); + pin!(h1); + + lazy(|cx| { + assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); + + assert!( + h1.as_mut().poll(cx).is_pending(), + "keep-alive should prevent poll from resolving" + ); + + // polls: initial + assert_eq!(h1.poll_count, 1); + + let mut res = buf.take_write_buf().to_vec(); + stabilize_date_header(&mut res); + let res = &res[..]; + + let exp = b"\ + HTTP/1.1 200 OK\r\n\ + content-length: 5\r\n\ + date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\ + /abcd\ + "; + + assert_eq!( + res, + exp, + "\nexpected response not in write buffer:\n\ + response: {:?}\n\ + expected: {:?}", + String::from_utf8_lossy(res), + String::from_utf8_lossy(exp) + ); + }) + .await; + + // sleep for less than KA timeout + sleep(Duration::from_millis(100)).await; + + lazy(|cx| { + assert!( + h1.as_mut().poll(cx).is_pending(), + "keep-alive should not have resolved dispatcher yet", + ); + + // polls: initial => manual + assert_eq!(h1.poll_count, 2); + + if let DispatcherStateProj::Normal { inner } = h1.as_mut().project().inner.project() { + // connection not closed + assert!(!inner.flags.contains(Flags::SHUTDOWN)); + assert!(!inner.flags.contains(Flags::WRITE_DISCONNECT)); + // and nothing added to write buffer + assert!(buf.write_buf_slice().is_empty()); + } + }) + .await; + + lazy(|cx| { + buf.extend_read_buf( + "\ + GET /efg HTTP/1.1\r\n\ + Connection: close\r\n\ + \r\n\r\n", + ); + + assert!( + h1.as_mut().poll(cx).is_ready(), + "connection close header should override keep-alive setting", + ); + + // polls: initial => manual => follow-up req => shutdown + assert_eq!(h1.poll_count, 4); + + if let DispatcherStateProj::Normal { inner } = h1.as_mut().project().inner.project() { + // connection closed + assert!(inner.flags.contains(Flags::SHUTDOWN)); + assert!(!inner.flags.contains(Flags::WRITE_DISCONNECT)); + } + + let mut res = buf.take_write_buf().to_vec(); + stabilize_date_header(&mut res); + let res = &res[..]; + + let exp = b"\ + HTTP/1.1 200 OK\r\n\ + content-length: 4\r\n\ + connection: close\r\n\ + date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\ + /efg\ + "; + + assert_eq!( + res, + exp, + "\nexpected response not in write buffer:\n\ + response: {:?}\n\ + expected: {:?}", + String::from_utf8_lossy(res), + String::from_utf8_lossy(exp) + ); + }) + .await; +} + +#[actix_rt::test] +async fn req_parse_err() { + lazy(|cx| { + let buf = TestBuffer::new("GET /test HTTP/1\r\n\r\n"); + + let services = HttpFlow::new(ok_service(), ExpectHandler, None); + + let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( + buf.clone(), + services, + ServiceConfig::default(), + None, + OnConnectData::default(), + ); + + pin!(h1); + + match h1.as_mut().poll(cx) { + Poll::Pending => panic!(), + Poll::Ready(res) => assert!(res.is_err()), + } + + if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() { + assert!(inner.flags.contains(Flags::READ_DISCONNECT)); + assert_eq!( + &buf.write_buf_slice()[..26], + b"HTTP/1.1 400 Bad Request\r\n" + ); + } + }) + .await; +} + +#[actix_rt::test] +async fn pipelining_ok_then_ok() { + lazy(|cx| { + let buf = TestBuffer::new( + "\ + GET /abcd HTTP/1.1\r\n\r\n\ + GET /def HTTP/1.1\r\n\r\n\ + ", + ); + + let cfg = ServiceConfig::new( + KeepAlive::Disabled, + Duration::from_millis(1), + Duration::from_millis(1), + false, + None, + ); + + let services = HttpFlow::new(echo_path_service(), ExpectHandler, None); + + let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( + buf.clone(), + services, + cfg, + None, + OnConnectData::default(), + ); + + pin!(h1); + + assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); + + match h1.as_mut().poll(cx) { + Poll::Pending => panic!("first poll should not be pending"), + Poll::Ready(res) => assert!(res.is_ok()), + } + + // polls: initial => shutdown + assert_eq!(h1.poll_count, 2); + + let mut res = buf.write_buf_slice_mut(); + stabilize_date_header(&mut res); + let res = &res[..]; + + let exp = b"\ + HTTP/1.1 200 OK\r\n\ + content-length: 5\r\n\ + connection: close\r\n\ + date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\ + /abcd\ + HTTP/1.1 200 OK\r\n\ + content-length: 4\r\n\ + connection: close\r\n\ + date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\ + /def\ + "; + + assert_eq!( + res, + exp, + "\nexpected response not in write buffer:\n\ + response: {:?}\n\ + expected: {:?}", + String::from_utf8_lossy(res), + String::from_utf8_lossy(exp) + ); + }) + .await; +} + +#[actix_rt::test] +async fn pipelining_ok_then_bad() { + lazy(|cx| { + let buf = TestBuffer::new( + "\ + GET /abcd HTTP/1.1\r\n\r\n\ + GET /def HTTP/1\r\n\r\n\ + ", + ); + + let cfg = ServiceConfig::new( + KeepAlive::Disabled, + Duration::from_millis(1), + Duration::from_millis(1), + false, + None, + ); + + let services = HttpFlow::new(echo_path_service(), ExpectHandler, None); + + let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( + buf.clone(), + services, + cfg, + None, + OnConnectData::default(), + ); + + pin!(h1); + + assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); + + match h1.as_mut().poll(cx) { + Poll::Pending => panic!("first poll should not be pending"), + Poll::Ready(res) => assert!(res.is_err()), + } + + // polls: initial => shutdown + assert_eq!(h1.poll_count, 1); + + let mut res = buf.write_buf_slice_mut(); + stabilize_date_header(&mut res); + let res = &res[..]; + + let exp = b"\ + HTTP/1.1 200 OK\r\n\ + content-length: 5\r\n\ + connection: close\r\n\ + date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\ + /abcd\ + HTTP/1.1 400 Bad Request\r\n\ + content-length: 0\r\n\ + connection: close\r\n\ + date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\ + "; + + assert_eq!( + res, + exp, + "\nexpected response not in write buffer:\n\ + response: {:?}\n\ + expected: {:?}", + String::from_utf8_lossy(res), + String::from_utf8_lossy(exp) + ); + }) + .await; +} + +#[actix_rt::test] +async fn expect_handling() { + lazy(|cx| { + let mut buf = TestSeqBuffer::empty(); + let cfg = ServiceConfig::new( + KeepAlive::Disabled, + Duration::ZERO, + Duration::ZERO, + false, + None, + ); + + let services = HttpFlow::new(echo_payload_service(), ExpectHandler, None); + + let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( + buf.clone(), + services, + cfg, + None, + OnConnectData::default(), + ); + + buf.extend_read_buf( + "\ + POST /upload HTTP/1.1\r\n\ + Content-Length: 5\r\n\ + Expect: 100-continue\r\n\ + \r\n\ + ", + ); + + pin!(h1); + + assert!(h1.as_mut().poll(cx).is_pending()); + assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); + + // polls: manual + assert_eq!(h1.poll_count, 1); + + if let DispatcherState::Normal { ref inner } = h1.inner { + let io = inner.io.as_ref().unwrap(); + let res = &io.write_buf()[..]; + assert_eq!( + str::from_utf8(res).unwrap(), + "HTTP/1.1 100 Continue\r\n\r\n" + ); + } + + buf.extend_read_buf("12345"); + assert!(h1.as_mut().poll(cx).is_ready()); + + // polls: manual manual shutdown + assert_eq!(h1.poll_count, 3); + + if let DispatcherState::Normal { ref inner } = h1.inner { + let io = inner.io.as_ref().unwrap(); + let mut res = (&io.write_buf()[..]).to_owned(); + stabilize_date_header(&mut res); + + assert_eq!( + str::from_utf8(&res).unwrap(), + "\ + HTTP/1.1 100 Continue\r\n\ + \r\n\ + HTTP/1.1 200 OK\r\n\ + content-length: 5\r\n\ + connection: close\r\n\ + date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\ + \r\n\ + 12345\ + " + ); + } + }) + .await; +} + +#[actix_rt::test] +async fn expect_eager() { + lazy(|cx| { + let mut buf = TestSeqBuffer::empty(); + let cfg = ServiceConfig::new( + KeepAlive::Disabled, + Duration::ZERO, + Duration::ZERO, + false, + None, + ); + + let services = HttpFlow::new(echo_path_service(), ExpectHandler, None); + + let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( + buf.clone(), + services, + cfg, + None, + OnConnectData::default(), + ); + + buf.extend_read_buf( + "\ + POST /upload HTTP/1.1\r\n\ + Content-Length: 5\r\n\ + Expect: 100-continue\r\n\ + \r\n\ + ", + ); + + pin!(h1); + + assert!(h1.as_mut().poll(cx).is_ready()); + assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); + + // polls: manual shutdown + assert_eq!(h1.poll_count, 2); + + if let DispatcherState::Normal { ref inner } = h1.inner { + let io = inner.io.as_ref().unwrap(); + let mut res = (&io.write_buf()[..]).to_owned(); + stabilize_date_header(&mut res); + + // Despite the content-length header and even though the request payload has not + // been sent, this test expects a complete service response since the payload + // is not used at all. The service passed to dispatcher is path echo and doesn't + // consume payload bytes. + assert_eq!( + str::from_utf8(&res).unwrap(), + "\ + HTTP/1.1 100 Continue\r\n\ + \r\n\ + HTTP/1.1 200 OK\r\n\ + content-length: 7\r\n\ + connection: close\r\n\ + date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\ + \r\n\ + /upload\ + " + ); + } + }) + .await; +} + +#[actix_rt::test] +async fn upgrade_handling() { + struct TestUpgrade; + + impl Service<(Request, Framed)> for TestUpgrade { + type Response = (); + type Error = Error; + type Future = Ready>; + + actix_service::always_ready!(); + + fn call(&self, (req, _framed): (Request, Framed)) -> Self::Future { + assert_eq!(req.method(), Method::GET); + assert!(req.upgrade()); + assert_eq!(req.headers().get("upgrade").unwrap(), "websocket"); + ready(Ok(())) + } + } + + lazy(|cx| { + let mut buf = TestSeqBuffer::empty(); + let cfg = ServiceConfig::new( + KeepAlive::Disabled, + Duration::ZERO, + Duration::ZERO, + false, + None, + ); + + let services = HttpFlow::new(ok_service(), ExpectHandler, Some(TestUpgrade)); + + let h1 = Dispatcher::<_, _, _, _, TestUpgrade>::new( + buf.clone(), + services, + cfg, + None, + OnConnectData::default(), + ); + + buf.extend_read_buf( + "\ + GET /ws HTTP/1.1\r\n\ + Connection: Upgrade\r\n\ + Upgrade: websocket\r\n\ + \r\n\ + ", + ); + + pin!(h1); + + assert!(h1.as_mut().poll(cx).is_ready()); + assert!(matches!(&h1.inner, DispatcherState::Upgrade { .. })); + + // polls: manual shutdown + assert_eq!(h1.poll_count, 2); + }) + .await; +} + +// fix in #2624 reverted temporarily +// complete fix tracked in #2745 +#[ignore] +#[actix_rt::test] +async fn handler_drop_payload() { + let _ = env_logger::try_init(); + + let mut buf = TestBuffer::new(http_msg( + r" + POST /drop-payload HTTP/1.1 + Content-Length: 3 + + abc + ", + )); + + let services = HttpFlow::new( + drop_payload_service(), + ExpectHandler, + None::, + ); + + let h1 = Dispatcher::new( + buf.clone(), + services, + ServiceConfig::default(), + None, + OnConnectData::default(), + ); + pin!(h1); + + lazy(|cx| { + assert!(h1.as_mut().poll(cx).is_pending()); + + // polls: manual + assert_eq!(h1.poll_count, 1); + + let mut res = BytesMut::from(buf.take_write_buf().as_ref()); + stabilize_date_header(&mut res); + let res = &res[..]; + + let exp = http_msg( + r" + HTTP/1.1 200 OK + content-length: 15 + date: Thu, 01 Jan 1970 12:34:56 UTC + + payload dropped + ", + ); + + assert_eq!( + res, + exp, + "\nexpected response not in write buffer:\n\ + response: {:?}\n\ + expected: {:?}", + String::from_utf8_lossy(res), + String::from_utf8_lossy(&exp) + ); + + if let DispatcherStateProj::Normal { inner } = h1.as_mut().project().inner.project() { + assert!(inner.state.is_none()); + } + }) + .await; + + lazy(|cx| { + // add message that claims to have payload longer than provided + buf.extend_read_buf(http_msg( + r" + POST /drop-payload HTTP/1.1 + Content-Length: 200 + + abc + ", + )); + + assert!(h1.as_mut().poll(cx).is_pending()); + + // polls: manual => manual + assert_eq!(h1.poll_count, 2); + + let mut res = BytesMut::from(buf.take_write_buf().as_ref()); + stabilize_date_header(&mut res); + let res = &res[..]; + + // expect response immediately even though request side has not finished reading payload + let exp = http_msg( + r" + HTTP/1.1 200 OK + content-length: 15 + date: Thu, 01 Jan 1970 12:34:56 UTC + + payload dropped + ", + ); + + assert_eq!( + res, + exp, + "\nexpected response not in write buffer:\n\ + response: {:?}\n\ + expected: {:?}", + String::from_utf8_lossy(res), + String::from_utf8_lossy(&exp) + ); + }) + .await; + + lazy(|cx| { + assert!(h1.as_mut().poll(cx).is_ready()); + + // polls: manual => manual => manual + assert_eq!(h1.poll_count, 3); + + let mut res = BytesMut::from(buf.take_write_buf().as_ref()); + stabilize_date_header(&mut res); + let res = &res[..]; + + // expect that unrequested error response is sent back since connection could not be cleaned + let exp = http_msg( + r" + HTTP/1.1 500 Internal Server Error + content-length: 0 + connection: close + date: Thu, 01 Jan 1970 12:34:56 UTC + + ", + ); + + assert_eq!( + res, + exp, + "\nexpected response not in write buffer:\n\ + response: {:?}\n\ + expected: {:?}", + String::from_utf8_lossy(res), + String::from_utf8_lossy(&exp) + ); + }) + .await; +} + +fn http_msg(msg: impl AsRef) -> BytesMut { + let mut msg = msg + .as_ref() + .trim() + .split('\n') + .into_iter() + .map(|line| [line.trim_start(), "\r"].concat()) + .collect::>() + .join("\n"); + + // remove trailing \r + msg.pop(); + + if !msg.is_empty() && !msg.contains("\r\n\r\n") { + msg.push_str("\r\n\r\n"); + } + + BytesMut::from(msg.as_bytes()) +} + +#[test] +fn http_msg_creates_msg() { + assert_eq!(http_msg(r""), ""); + + assert_eq!( + http_msg( + r" + POST / HTTP/1.1 + Content-Length: 3 + + abc + " + ), + "POST / HTTP/1.1\r\nContent-Length: 3\r\n\r\nabc" + ); + + assert_eq!( + http_msg( + r" + GET / HTTP/1.1 + Content-Length: 3 + + " + ), + "GET / HTTP/1.1\r\nContent-Length: 3\r\n\r\n" + ); +} diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index 5fcb2f688..21cfd75c4 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -210,14 +210,14 @@ pub(crate) trait MessageType: Sized { dst.advance_mut(pos); } - // optimized date header, set_date writes \r\n if !has_date { - config.set_date(dst, camel_case); - } else { - // msg eof - dst.extend_from_slice(b"\r\n"); + // optimized date header, write_date_header writes its own \r\n + config.write_date_header(dst, camel_case); } + // end-of-headers marker + dst.extend_from_slice(b"\r\n"); + Ok(()) } @@ -318,16 +318,17 @@ impl MessageType for RequestHeadType { } impl MessageEncoder { - /// Encode message + /// Encode chunk. pub fn encode_chunk(&mut self, msg: &[u8], buf: &mut BytesMut) -> io::Result { self.te.encode(msg, buf) } - /// Encode eof + /// Encode EOF. pub fn encode_eof(&mut self, buf: &mut BytesMut) -> io::Result<()> { self.te.encode_eof(buf) } + /// Encode message. pub fn encode( &mut self, dst: &mut BytesMut, @@ -516,6 +517,7 @@ unsafe fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) { if let Some(c @ b'a'..=b'z') = iter.next() { buffer[index] = c & 0b1101_1111; } + index += 1; } index += 1; @@ -527,7 +529,7 @@ mod tests { use std::rc::Rc; use bytes::Bytes; - use http::header::AUTHORIZATION; + use http::header::{AUTHORIZATION, UPGRADE_INSECURE_REQUESTS}; use super::*; use crate::{ @@ -558,6 +560,9 @@ mod tests { head.headers .insert(CONTENT_TYPE, HeaderValue::from_static("plain/text")); + head.headers + .insert(UPGRADE_INSECURE_REQUESTS, HeaderValue::from_static("1")); + let mut head = RequestHeadType::Owned(head); let _ = head.encode_headers( @@ -573,6 +578,7 @@ mod tests { assert!(data.contains("Connection: close\r\n")); assert!(data.contains("Content-Type: plain/text\r\n")); assert!(data.contains("Date: date\r\n")); + assert!(data.contains("Upgrade-Insecure-Requests: 1\r\n")); let _ = head.encode_headers( &mut bytes, diff --git a/actix-http/src/h1/mod.rs b/actix-http/src/h1/mod.rs index 64586a2dc..858cf542a 100644 --- a/actix-http/src/h1/mod.rs +++ b/actix-http/src/h1/mod.rs @@ -7,10 +7,13 @@ mod client; mod codec; mod decoder; mod dispatcher; +#[cfg(test)] +mod dispatcher_tests; mod encoder; mod expect; mod payload; mod service; +mod timer; mod upgrade; mod utils; @@ -26,9 +29,10 @@ pub use self::utils::SendResponse; #[derive(Debug)] /// Codec message pub enum Message { - /// Http message + /// HTTP message. Item(T), - /// Payload chunk + + /// Payload chunk. Chunk(Option), } diff --git a/actix-http/src/h1/payload.rs b/actix-http/src/h1/payload.rs index 4d031c15a..a8c632396 100644 --- a/actix-http/src/h1/payload.rs +++ b/actix-http/src/h1/payload.rs @@ -263,7 +263,8 @@ mod tests { assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe); assert_impl_all!(Inner: Unpin, Send, Sync); - assert_not_impl_any!(Inner: UnwindSafe, RefUnwindSafe); + // assertion not stable wrt rustc versions yet + // assert_impl_all!(Inner: UnwindSafe, RefUnwindSafe); #[actix_rt::test] async fn test_unread_data() { diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index 43b7919a7..a791ea8c3 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -13,6 +13,7 @@ use actix_service::{ }; use actix_utils::future::ready; use futures_core::future::LocalBoxFuture; +use tracing::error; use crate::{ body::{BoxBody, MessageBody}, @@ -305,13 +306,13 @@ where Box::pin(async move { let expect = expect .await - .map_err(|e| log::error!("Init http expect service error: {:?}", e))?; + .map_err(|e| error!("Init http expect service error: {:?}", e))?; let upgrade = match upgrade { Some(upgrade) => { let upgrade = upgrade .await - .map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?; + .map_err(|e| error!("Init http upgrade service error: {:?}", e))?; Some(upgrade) } None => None, @@ -319,7 +320,7 @@ where let service = service .await - .map_err(|e| log::error!("Init http service error: {:?}", e))?; + .map_err(|e| error!("Init http service error: {:?}", e))?; Ok(H1ServiceHandler::new( cfg, @@ -357,7 +358,7 @@ where fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { self._poll_ready(cx).map_err(|err| { - log::error!("HTTP/1 service readiness error: {:?}", err); + error!("HTTP/1 service readiness error: {:?}", err); DispatchError::Service(err) }) } diff --git a/actix-http/src/h1/timer.rs b/actix-http/src/h1/timer.rs new file mode 100644 index 000000000..0ea70a319 --- /dev/null +++ b/actix-http/src/h1/timer.rs @@ -0,0 +1,81 @@ +use std::{fmt, future::Future, pin::Pin, task::Context}; + +use actix_rt::time::{Instant, Sleep}; +use tracing::trace; + +#[derive(Debug)] +pub(super) enum TimerState { + Disabled, + Inactive, + Active { timer: Pin> }, +} + +impl TimerState { + pub(super) fn new(enabled: bool) -> Self { + if enabled { + Self::Inactive + } else { + Self::Disabled + } + } + + pub(super) fn is_enabled(&self) -> bool { + matches!(self, Self::Active { .. } | Self::Inactive) + } + + pub(super) fn set(&mut self, timer: Sleep, line: u32) { + if matches!(self, Self::Disabled) { + trace!("setting disabled timer from line {}", line); + } + + *self = Self::Active { + timer: Box::pin(timer), + }; + } + + pub(super) fn set_and_init(&mut self, cx: &mut Context<'_>, timer: Sleep, line: u32) { + self.set(timer, line); + self.init(cx); + } + + pub(super) fn clear(&mut self, line: u32) { + if matches!(self, Self::Disabled) { + trace!("trying to clear a disabled timer from line {}", line); + } + + if matches!(self, Self::Inactive) { + trace!("trying to clear an inactive timer from line {}", line); + } + + *self = Self::Inactive; + } + + pub(super) fn init(&mut self, cx: &mut Context<'_>) { + if let TimerState::Active { timer } = self { + let _ = timer.as_mut().poll(cx); + } + } +} + +impl fmt::Display for TimerState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TimerState::Disabled => f.write_str("timer is disabled"), + TimerState::Inactive => f.write_str("timer is inactive"), + TimerState::Active { timer } => { + let deadline = timer.deadline(); + let now = Instant::now(); + + if deadline < now { + f.write_str("timer is active and has reached deadline") + } else { + write!( + f, + "timer is active and due to expire in {} milliseconds", + ((deadline - now).as_secs_f32() * 1000.0) + ) + } + } + } + } +} diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index a90eb3466..85516cccc 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -19,13 +19,15 @@ use h2::{ server::{Connection, SendResponse}, Ping, PingPong, }; -use log::{error, trace}; use pin_project_lite::pin_project; +use tracing::{error, trace, warn}; use crate::{ body::{BodySize, BoxBody, MessageBody}, config::ServiceConfig, - header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}, + header::{ + HeaderName, HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE, + }, service::HttpFlow, Extensions, OnConnectData, Payload, Request, Response, ResponseHead, }; @@ -57,11 +59,11 @@ where conn_data: OnConnectData, timer: Option>>, ) -> Self { - let ping_pong = config.keep_alive().map(|dur| H2PingPong { + let ping_pong = config.keep_alive().duration().map(|dur| H2PingPong { timer: timer .map(|mut timer| { - // reset timer if it's received from new function. - timer.as_mut().reset(config.now() + dur); + // reuse timer slot if it was initialized for handshake + timer.as_mut().reset((config.now() + dur).into()); timer }) .unwrap_or_else(|| Box::pin(sleep(dur))), @@ -160,8 +162,8 @@ where Poll::Ready(_) => { ping_pong.on_flight = false; - let dead_line = this.config.keep_alive_expire().unwrap(); - ping_pong.timer.as_mut().reset(dead_line); + let dead_line = this.config.keep_alive_deadline().unwrap(); + ping_pong.timer.as_mut().reset(dead_line.into()); } Poll::Pending => { return ping_pong.timer.as_mut().poll(cx).map(|_| Ok(())) @@ -174,8 +176,8 @@ where ping_pong.ping_pong.send_ping(Ping::opaque())?; - let dead_line = this.config.keep_alive_expire().unwrap(); - ping_pong.timer.as_mut().reset(dead_line); + let dead_line = this.config.keep_alive_deadline().unwrap(); + ping_pong.timer.as_mut().reset(dead_line.into()); ping_pong.on_flight = true; } @@ -306,13 +308,22 @@ fn prepare_response( // copy headers for (key, value) in head.headers.iter() { - match *key { - // TODO: consider skipping other headers according to: - // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2 - // omit HTTP/1.x only headers - CONNECTION | TRANSFER_ENCODING => continue, - CONTENT_LENGTH if skip_len => continue, - DATE => has_date = true, + match key { + // omit HTTP/1.x only headers according to: + // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2 + &CONNECTION | &TRANSFER_ENCODING | &UPGRADE => continue, + + &CONTENT_LENGTH if skip_len => continue, + &DATE => has_date = true, + + // omit HTTP/1.x only headers according to: + // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2 + hdr if hdr == HeaderName::from_static("keep-alive") + || hdr == HeaderName::from_static("proxy-connection") => + { + continue + } + _ => {} } @@ -322,7 +333,7 @@ fn prepare_response( // set date header if !has_date { let mut bytes = BytesMut::with_capacity(29); - config.set_date_header(&mut bytes); + config.write_date_header_value(&mut bytes); res.headers_mut().insert( DATE, // SAFETY: serialized date-times are known ASCII strings diff --git a/actix-http/src/h2/mod.rs b/actix-http/src/h2/mod.rs index 47d51b420..c8aaaaa5f 100644 --- a/actix-http/src/h2/mod.rs +++ b/actix-http/src/h2/mod.rs @@ -7,7 +7,7 @@ use std::{ }; use actix_codec::{AsyncRead, AsyncWrite}; -use actix_rt::time::Sleep; +use actix_rt::time::{sleep_until, Sleep}; use bytes::Bytes; use futures_core::{ready, Stream}; use h2::{ @@ -15,17 +15,17 @@ use h2::{ RecvStream, }; +use crate::{ + config::ServiceConfig, + error::{DispatchError, PayloadError}, +}; + mod dispatcher; mod service; pub use self::dispatcher::Dispatcher; pub use self::service::H2Service; -use crate::{ - config::ServiceConfig, - error::{DispatchError, PayloadError}, -}; - /// HTTP/2 peer stream. pub struct Payload { stream: RecvStream, @@ -67,7 +67,9 @@ where { HandshakeWithTimeout { handshake: handshake(io), - timer: config.client_timer().map(Box::pin), + timer: config + .client_request_deadline() + .map(|deadline| Box::pin(sleep_until(deadline.into()))), } } @@ -86,7 +88,7 @@ where let this = self.get_mut(); match Pin::new(&mut this.handshake).poll(cx)? { - // return the timer on success handshake. It can be re-used for h2 ping-pong. + // return the timer on success handshake; its slot can be re-used for h2 ping-pong Poll::Ready(conn) => Poll::Ready(Ok((conn, this.timer.take()))), Poll::Pending => match this.timer.as_mut() { Some(timer) => { diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 469648054..e526918c7 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -14,7 +14,7 @@ use actix_service::{ }; use actix_utils::future::ready; use futures_core::{future::LocalBoxFuture, ready}; -use log::error; +use tracing::{error, trace}; use crate::{ body::{BoxBody, MessageBody}, diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index 33fb262c4..8f6d1cead 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -630,7 +630,7 @@ impl Removed { /// Returns true if iterator contains no elements, without consuming it. /// /// If called immediately after [`HeaderMap::insert`] or [`HeaderMap::remove`], it will indicate - /// wether any items were actually replaced or removed, respectively. + /// whether any items were actually replaced or removed, respectively. pub fn is_empty(&self) -> bool { match self.inner { // size hint lower bound of smallvec is the correct length diff --git a/actix-http/src/header/shared/http_date.rs b/actix-http/src/header/shared/http_date.rs index 473d6cad0..21ed49f0c 100644 --- a/actix-http/src/header/shared/http_date.rs +++ b/actix-http/src/header/shared/http_date.rs @@ -4,8 +4,7 @@ use bytes::BytesMut; use http::header::{HeaderValue, InvalidHeaderValue}; use crate::{ - config::DATE_VALUE_LENGTH, error::ParseError, header::TryIntoHeaderValue, - helpers::MutWriter, + date::DATE_VALUE_LENGTH, error::ParseError, header::TryIntoHeaderValue, helpers::MutWriter, }; /// A timestamp with HTTP-style formatting and parsing. diff --git a/actix-http/src/http_message.rs b/actix-http/src/http_message.rs index 068e23b96..198254e02 100644 --- a/actix-http/src/http_message.rs +++ b/actix-http/src/http_message.rs @@ -55,7 +55,7 @@ pub trait HttpMessage: Sized { "" } - /// Get content type encoding + /// Get content type encoding. /// /// UTF-8 is used by default, If request charset is not set. fn encoding(&self) -> Result<&'static Encoding, ContentTypeError> { diff --git a/actix-http/src/keep_alive.rs b/actix-http/src/keep_alive.rs new file mode 100644 index 000000000..feb7ff5df --- /dev/null +++ b/actix-http/src/keep_alive.rs @@ -0,0 +1,84 @@ +use std::time::Duration; + +/// Connection keep-alive config. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum KeepAlive { + /// Keep-alive duration. + /// + /// `KeepAlive::Timeout(Duration::ZERO)` is mapped to `KeepAlive::Disabled`. + Timeout(Duration), + + /// Rely on OS to shutdown TCP connection. + /// + /// Some defaults can be very long, check your OS documentation. + Os, + + /// Keep-alive is disabled. + /// + /// Connections will be closed immediately. + Disabled, +} + +impl KeepAlive { + pub(crate) fn enabled(&self) -> bool { + !matches!(self, Self::Disabled) + } + + #[allow(unused)] // used with `http2` feature flag + pub(crate) fn duration(&self) -> Option { + match self { + KeepAlive::Timeout(dur) => Some(*dur), + _ => None, + } + } + + /// Map zero duration to disabled. + pub(crate) fn normalize(self) -> KeepAlive { + match self { + KeepAlive::Timeout(Duration::ZERO) => KeepAlive::Disabled, + ka => ka, + } + } +} + +impl Default for KeepAlive { + fn default() -> Self { + Self::Timeout(Duration::from_secs(5)) + } +} + +impl From for KeepAlive { + fn from(dur: Duration) -> Self { + KeepAlive::Timeout(dur).normalize() + } +} + +impl From> for KeepAlive { + fn from(ka_dur: Option) -> Self { + match ka_dur { + Some(dur) => KeepAlive::from(dur), + None => KeepAlive::Disabled, + } + .normalize() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_impls() { + let test: KeepAlive = Duration::from_secs(1).into(); + assert_eq!(test, KeepAlive::Timeout(Duration::from_secs(1))); + + let test: KeepAlive = Duration::from_secs(0).into(); + assert_eq!(test, KeepAlive::Disabled); + + let test: KeepAlive = Some(Duration::from_secs(0)).into(); + assert_eq!(test, KeepAlive::Disabled); + + let test: KeepAlive = None.into(); + assert_eq!(test, KeepAlive::Disabled); + } +} diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index f2b415790..184049860 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -3,6 +3,7 @@ //! ## Crate Features //! | Feature | Functionality | //! | ------------------- | ------------------------------------------- | +//! | `http2` | HTTP/2 support via [h2]. | //! | `openssl` | TLS support via [OpenSSL]. | //! | `rustls` | TLS support via [rustls]. | //! | `compress-brotli` | Payload compression support: Brotli. | @@ -10,6 +11,7 @@ //! | `compress-zstd` | Payload compression support: Zstd. | //! | `trust-dns` | Use [trust-dns] as the client DNS resolver. | //! +//! [h2]: https://crates.io/crates/h2 //! [OpenSSL]: https://crates.io/crates/openssl //! [rustls]: https://crates.io/crates/rustls //! [trust-dns]: https://crates.io/crates/trust-dns @@ -23,9 +25,7 @@ )] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] - -#[macro_use] -extern crate log; +#![cfg_attr(docsrs, feature(doc_cfg))] pub use ::http::{uri, uri::Uri}; pub use ::http::{Method, StatusCode, Version}; @@ -33,29 +33,36 @@ pub use ::http::{Method, StatusCode, Version}; pub mod body; mod builder; mod config; +mod date; #[cfg(feature = "__compress")] pub mod encoding; pub mod error; mod extensions; pub mod h1; +#[cfg(feature = "http2")] pub mod h2; pub mod header; mod helpers; mod http_message; +mod keep_alive; mod message; +#[cfg(test)] +mod notify_on_drop; mod payload; mod requests; mod responses; mod service; pub mod test; +#[cfg(feature = "ws")] pub mod ws; pub use self::builder::HttpServiceBuilder; -pub use self::config::{KeepAlive, ServiceConfig}; +pub use self::config::ServiceConfig; pub use self::error::Error; pub use self::extensions::Extensions; pub use self::header::ContentEncoding; pub use self::http_message::HttpMessage; +pub use self::keep_alive::KeepAlive; pub use self::message::ConnectionType; pub use self::message::Message; #[allow(deprecated)] @@ -63,6 +70,8 @@ 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; +#[cfg(any(feature = "openssl", feature = "rustls"))] +pub use self::service::TlsAcceptorConfig; /// A major HTTP protocol version. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] diff --git a/actix-http/src/notify_on_drop.rs b/actix-http/src/notify_on_drop.rs new file mode 100644 index 000000000..98544bb5d --- /dev/null +++ b/actix-http/src/notify_on_drop.rs @@ -0,0 +1,49 @@ +/// Test Module for checking the drop state of certain async tasks that are spawned +/// with `actix_rt::spawn` +/// +/// The target task must explicitly generate `NotifyOnDrop` when spawn the task +use std::cell::RefCell; + +thread_local! { + static NOTIFY_DROPPED: RefCell> = RefCell::new(None); +} + +/// Check if the spawned task is dropped. +/// +/// # Panics +/// Panics when there was no `NotifyOnDrop` instance on current thread. +pub(crate) fn is_dropped() -> bool { + NOTIFY_DROPPED.with(|bool| { + bool.borrow() + .expect("No NotifyOnDrop existed on current thread") + }) +} + +pub(crate) struct NotifyOnDrop; + +impl NotifyOnDrop { + /// # Panics + /// Panics hen construct multiple instances on any given thread. + pub(crate) fn new() -> Self { + NOTIFY_DROPPED.with(|bool| { + let mut bool = bool.borrow_mut(); + if bool.is_some() { + panic!("NotifyOnDrop existed on current thread"); + } else { + *bool = Some(false); + } + }); + + NotifyOnDrop + } +} + +impl Drop for NotifyOnDrop { + fn drop(&mut self) { + NOTIFY_DROPPED.with(|bool| { + if let Some(b) = bool.borrow_mut().as_mut() { + *b = true; + } + }); + } +} diff --git a/actix-http/src/payload.rs b/actix-http/src/payload.rs index aed24e963..ee0128af4 100644 --- a/actix-http/src/payload.rs +++ b/actix-http/src/payload.rs @@ -13,9 +13,22 @@ use crate::error::PayloadError; /// A boxed payload stream. pub type BoxedPayloadStream = Pin>>>; -#[deprecated(since = "4.0.0", note = "Renamed to `BoxedPayloadStream`.")] +#[doc(hidden)] +#[deprecated(since = "3.0.0", note = "Renamed to `BoxedPayloadStream`.")] pub type PayloadStream = BoxedPayloadStream; +#[cfg(not(feature = "http2"))] +pin_project! { + /// A streaming payload. + #[project = PayloadProj] + pub enum Payload { + None, + H1 { payload: crate::h1::Payload }, + Stream { #[pin] payload: S }, + } +} + +#[cfg(feature = "http2")] pin_project! { /// A streaming payload. #[project = PayloadProj] @@ -33,14 +46,16 @@ impl From for Payload { } } +#[cfg(feature = "http2")] impl From for Payload { fn from(payload: crate::h2::Payload) -> Self { Payload::H2 { payload } } } -impl From for Payload { - fn from(stream: h2::RecvStream) -> Self { +#[cfg(feature = "http2")] +impl From<::h2::RecvStream> for Payload { + fn from(stream: ::h2::RecvStream) -> Self { Payload::H2 { payload: crate::h2::Payload::new(stream), } @@ -71,7 +86,10 @@ where match self.project() { PayloadProj::None => Poll::Ready(None), PayloadProj::H1 { payload } => Pin::new(payload).poll_next(cx), + + #[cfg(feature = "http2")] PayloadProj::H2 { payload } => Pin::new(payload).poll_next(cx), + PayloadProj::Stream { payload } => payload.poll_next(cx), } } diff --git a/actix-http/src/requests/head.rs b/actix-http/src/requests/head.rs index 06fd0429e..4558801f3 100644 --- a/actix-http/src/requests/head.rs +++ b/actix-http/src/requests/head.rs @@ -130,8 +130,8 @@ impl RequestHead { } } + /// Request contains `EXPECT` header. #[inline] - /// Request contains `EXPECT` header pub fn expect(&self) -> bool { self.flags.contains(Flags::EXPECT) } diff --git a/actix-http/src/responses/builder.rs b/actix-http/src/responses/builder.rs index 4a67423b1..063af92da 100644 --- a/actix-http/src/responses/builder.rs +++ b/actix-http/src/responses/builder.rs @@ -144,7 +144,7 @@ impl ResponseBuilder { self } - /// Set connection type to Upgrade + /// Set connection type to `Upgrade`. #[inline] pub fn upgrade(&mut self, value: V) -> &mut Self where @@ -161,7 +161,7 @@ impl ResponseBuilder { self } - /// Force close connection, even if it is marked as keep-alive + /// Force-close connection, even if it is marked as keep-alive. #[inline] pub fn force_close(&mut self) -> &mut Self { if let Some(parts) = self.inner() { diff --git a/actix-http/src/responses/head.rs b/actix-http/src/responses/head.rs index 870073ab3..01bca6c5b 100644 --- a/actix-http/src/responses/head.rs +++ b/actix-http/src/responses/head.rs @@ -42,7 +42,7 @@ impl ResponseHead { &mut self.headers } - /// Sets the flag that controls wether to send headers formatted as Camel-Case. + /// Sets the flag that controls whether to send headers formatted as Camel-Case. /// /// Only applicable to HTTP/1.x responses; HTTP/2 header names are always lowercase. #[inline] @@ -210,14 +210,15 @@ mod tests { use memchr::memmem; use crate::{ + h1::H1Service, header::{HeaderName, HeaderValue}, - Error, HttpService, Request, Response, + Error, Request, Response, ServiceConfig, }; #[actix_rt::test] async fn camel_case_headers() { let mut srv = actix_http_test::test_server(|| { - HttpService::new(|req: Request| async move { + H1Service::with_config(ServiceConfig::default(), |req: Request| async move { let mut res = Response::ok(); if req.path().contains("camel") { @@ -228,6 +229,7 @@ mod tests { HeaderName::from_static("foo-bar"), HeaderValue::from_static("baz"), ); + Ok::<_, Error>(res) }) .tcp() @@ -235,9 +237,11 @@ mod tests { .await; let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); - let _ = stream.write_all(b"GET /camel HTTP/1.1\r\nConnection: Close\r\n\r\n"); - let mut data = vec![0; 1024]; - let _ = stream.read(&mut data); + stream + .write_all(b"GET /camel HTTP/1.1\r\nConnection: Close\r\n\r\n") + .unwrap(); + let mut data = vec![]; + let _ = stream.read_to_end(&mut data).unwrap(); assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); assert!(memmem::find(&data, b"Foo-Bar").is_some()); assert!(memmem::find(&data, b"foo-bar").is_none()); @@ -247,9 +251,11 @@ mod tests { assert!(memmem::find(&data, b"content-length").is_none()); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); - let _ = stream.write_all(b"GET /lower HTTP/1.1\r\nConnection: Close\r\n\r\n"); - let mut data = vec![0; 1024]; - let _ = stream.read(&mut data); + stream + .write_all(b"GET /lower HTTP/1.1\r\nConnection: Close\r\n\r\n") + .unwrap(); + let mut data = vec![]; + let _ = stream.read_to_end(&mut data).unwrap(); assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); assert!(memmem::find(&data, b"Foo-Bar").is_none()); assert!(memmem::find(&data, b"foo-bar").is_some()); diff --git a/actix-http/src/responses/response.rs b/actix-http/src/responses/response.rs index da5503c1c..ceb158f65 100644 --- a/actix-http/src/responses/response.rs +++ b/actix-http/src/responses/response.rs @@ -285,6 +285,24 @@ impl From<&'static [u8]> for Response<&'static [u8]> { } } +impl From> for Response> { + fn from(val: Vec) -> Self { + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res + } +} + +impl From<&Vec> for Response> { + fn from(val: &Vec) -> Self { + let mut res = Response::with_body(StatusCode::OK, val.clone()); + let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res + } +} + impl From for Response { fn from(val: String) -> Self { let mut res = Response::with_body(StatusCode::OK, val); diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index cd2efe678..27029cb8e 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -15,13 +15,13 @@ use actix_service::{ }; use futures_core::{future::LocalBoxFuture, ready}; use pin_project_lite::pin_project; +use tracing::error; use crate::{ body::{BoxBody, MessageBody}, builder::HttpServiceBuilder, - config::{KeepAlive, ServiceConfig}, error::DispatchError, - h1, h2, ConnectCallback, OnConnectData, Protocol, Request, Response, + h1, ConnectCallback, OnConnectData, Protocol, Request, Response, ServiceConfig, }; /// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol. @@ -43,9 +43,9 @@ where >::Future: 'static, B: MessageBody + 'static, { - /// Create builder for `HttpService` instance. + /// Constructs builder for `HttpService` instance. pub fn build() -> HttpServiceBuilder { - HttpServiceBuilder::new() + HttpServiceBuilder::default() } } @@ -58,12 +58,10 @@ where >::Future: 'static, B: MessageBody + 'static, { - /// Create new `HttpService` instance. + /// Constructs new `HttpService` instance from service with default config. pub fn new>(service: F) -> Self { - let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0, false, None); - HttpService { - cfg, + cfg: ServiceConfig::default(), srv: service.into_factory(), expect: h1::ExpectHandler, upgrade: None, @@ -72,7 +70,7 @@ where } } - /// Create new `HttpService` instance with config. + /// Constructs new `HttpService` instance from config and service. pub(crate) fn with_config>( cfg: ServiceConfig, service: F, @@ -97,11 +95,10 @@ where >::Future: 'static, B: MessageBody, { - /// Provide service for `EXPECT: 100-Continue` support. + /// Sets service for `Expect: 100-Continue` handling. /// - /// Service get called with request that contains `EXPECT` header. - /// Service must return request in case of success, in that case - /// request will be forwarded to main service. + /// An expect service is called with requests that contain an `Expect` header. A successful + /// response type is also a request which will be forwarded to the main service. pub fn expect(self, expect: X1) -> HttpService where X1: ServiceFactory, @@ -118,10 +115,10 @@ where } } - /// Provide service for custom `Connection: UPGRADE` support. + /// Sets service for custom `Connection: Upgrade` handling. /// - /// If service is provided then normal requests handling get halted - /// and this service get called with original request and framed object. + /// If service is provided then normal requests handling get halted and this service get called + /// with original request and framed object. pub fn upgrade(self, upgrade: Option) -> HttpService where U1: ServiceFactory<(Request, Framed), Config = (), Response = ()>, @@ -184,6 +181,25 @@ where } } +/// Configuration options used when accepting TLS connection. +#[cfg(any(feature = "openssl", feature = "rustls"))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))] +#[derive(Debug, Default)] +pub struct TlsAcceptorConfig { + pub(crate) handshake_timeout: Option, +} + +#[cfg(any(feature = "openssl", feature = "rustls"))] +impl TlsAcceptorConfig { + /// Set TLS handshake timeout duration. + pub fn handshake_timeout(self, dur: std::time::Duration) -> Self { + Self { + handshake_timeout: Some(dur), + // ..self + } + } +} + #[cfg(feature = "openssl")] mod openssl { use actix_service::ServiceFactoryExt as _; @@ -233,7 +249,28 @@ mod openssl { Error = TlsError, InitError = (), > { - Acceptor::new(acceptor) + self.openssl_with_config(acceptor, TlsAcceptorConfig::default()) + } + + /// Create OpenSSL based service with custom TLS acceptor configuration. + pub fn openssl_with_config( + self, + acceptor: SslAcceptor, + tls_acceptor_config: TlsAcceptorConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = (), + > { + let mut acceptor = Acceptor::new(acceptor); + + if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout { + acceptor.set_handshake_timeout(handshake_timeout); + } + + acceptor .map_init_err(|_| { unreachable!("TLS acceptor service factory does not error on init") }) @@ -296,8 +333,23 @@ mod rustls { { /// Create Rustls based service. pub fn rustls( + self, + config: ServerConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = (), + > { + self.rustls_with_config(config, TlsAcceptorConfig::default()) + } + + /// Create Rustls based service with custom TLS acceptor configuration. + pub fn rustls_with_config( self, mut config: ServerConfig, + tls_acceptor_config: TlsAcceptorConfig, ) -> impl ServiceFactory< TcpStream, Config = (), @@ -309,7 +361,13 @@ mod rustls { protos.extend_from_slice(&config.alpn_protocols); config.alpn_protocols = protos; - Acceptor::new(config) + let mut acceptor = Acceptor::new(config); + + if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout { + acceptor.set_handshake_timeout(handshake_timeout); + } + + acceptor .map_init_err(|_| { unreachable!("TLS acceptor service factory does not error on init") }) @@ -373,13 +431,13 @@ where Box::pin(async move { let expect = expect .await - .map_err(|e| log::error!("Init http expect service error: {:?}", e))?; + .map_err(|e| error!("Init http expect service error: {:?}", e))?; let upgrade = match upgrade { Some(upgrade) => { let upgrade = upgrade .await - .map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?; + .map_err(|e| error!("Init http upgrade service error: {:?}", e))?; Some(upgrade) } None => None, @@ -387,7 +445,7 @@ where let service = service .await - .map_err(|e| log::error!("Init http service error: {:?}", e))?; + .map_err(|e| error!("Init http service error: {:?}", e))?; Ok(HttpServiceHandler::new( cfg, @@ -494,7 +552,7 @@ where fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { self._poll_ready(cx).map_err(|err| { - log::error!("HTTP service readiness error: {:?}", err); + error!("HTTP service readiness error: {:?}", err); DispatchError::Service(err) }) } @@ -506,10 +564,11 @@ where let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref()); match proto { + #[cfg(feature = "http2")] Protocol::Http2 => HttpServiceHandlerResponse { state: State::H2Handshake { handshake: Some(( - h2::handshake_with_timeout(io, &self.cfg), + crate::h2::handshake_with_timeout(io, &self.cfg), self.cfg.clone(), self.flow.clone(), conn_data, @@ -518,6 +577,11 @@ where }, }, + #[cfg(not(feature = "http2"))] + Protocol::Http2 => { + panic!("HTTP/2 support is disabled (enable with the `http2` feature flag)") + } + Protocol::Http1 => HttpServiceHandlerResponse { state: State::H1 { dispatcher: h1::Dispatcher::new( @@ -535,6 +599,7 @@ where } } +#[cfg(not(feature = "http2"))] pin_project! { #[project = StateProj] enum State @@ -556,10 +621,37 @@ pin_project! { U::Error: fmt::Display, { H1 { #[pin] dispatcher: h1::Dispatcher }, - H2 { #[pin] dispatcher: h2::Dispatcher }, + } +} + +#[cfg(feature = "http2")] +pin_project! { + #[project = StateProj] + enum State + where + T: AsyncRead, + T: AsyncWrite, + T: Unpin, + + S: Service, + S::Future: 'static, + S::Error: Into>, + + B: MessageBody, + + X: Service, + X::Error: Into>, + + U: Service<(Request, Framed), Response = ()>, + U::Error: fmt::Display, + { + H1 { #[pin] dispatcher: h1::Dispatcher }, + + H2 { #[pin] dispatcher: crate::h2::Dispatcher }, + H2Handshake { handshake: Option<( - h2::HandshakeWithTimeout, + crate::h2::HandshakeWithTimeout, ServiceConfig, Rc>, OnConnectData, @@ -618,21 +710,25 @@ where fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match self.as_mut().project().state.project() { StateProj::H1 { dispatcher } => dispatcher.poll(cx), + + #[cfg(feature = "http2")] StateProj::H2 { dispatcher } => dispatcher.poll(cx), + + #[cfg(feature = "http2")] StateProj::H2Handshake { handshake: data } => { match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) { Ok((conn, timer)) => { let (_, config, flow, conn_data, peer_addr) = data.take().unwrap(); self.as_mut().project().state.set(State::H2 { - dispatcher: h2::Dispatcher::new( + dispatcher: crate::h2::Dispatcher::new( conn, flow, config, peer_addr, conn_data, timer, ), }); self.poll(cx) } Err(err) => { - trace!("H2 handshake error: {}", err); + tracing::trace!("H2 handshake error: {}", err); Poll::Ready(Err(err)) } } diff --git a/actix-http/src/test.rs b/actix-http/src/test.rs index 1f76498ef..3815e64c6 100644 --- a/actix-http/src/test.rs +++ b/actix-http/src/test.rs @@ -1,7 +1,7 @@ //! Various testing helpers for use in internal and app tests. use std::{ - cell::{Ref, RefCell}, + cell::{Ref, RefCell, RefMut}, io::{self, Read, Write}, pin::Pin, rc::Rc, @@ -19,29 +19,7 @@ use crate::{ Request, }; -/// Test `Request` builder -/// -/// ```ignore -/// # use http::{header, StatusCode}; -/// # use actix_web::*; -/// use actix_web::test::TestRequest; -/// -/// fn index(req: &HttpRequest) -> Response { -/// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { -/// Response::Ok().into() -/// } else { -/// Response::BadRequest().into() -/// } -/// } -/// -/// let resp = TestRequest::default().insert_header("content-type", "text/plain") -/// .run(&index) -/// .unwrap(); -/// assert_eq!(resp.status(), StatusCode::OK); -/// -/// let resp = TestRequest::default().run(&index).unwrap(); -/// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); -/// ``` +/// Test `Request` builder. pub struct TestRequest(Option); struct Inner { @@ -157,10 +135,11 @@ fn parts(parts: &mut Option) -> &mut Inner { } /// Async I/O test buffer. +#[derive(Debug)] pub struct TestBuffer { - pub read_buf: BytesMut, - pub write_buf: BytesMut, - pub err: Option, + pub read_buf: Rc>, + pub write_buf: Rc>, + pub err: Option>, } impl TestBuffer { @@ -170,34 +149,69 @@ impl TestBuffer { T: Into, { Self { - read_buf: data.into(), - write_buf: BytesMut::new(), + read_buf: Rc::new(RefCell::new(data.into())), + write_buf: Rc::new(RefCell::new(BytesMut::new())), err: None, } } + // intentionally not using Clone trait + #[allow(dead_code)] + pub(crate) fn clone(&self) -> Self { + Self { + read_buf: self.read_buf.clone(), + write_buf: self.write_buf.clone(), + err: self.err.clone(), + } + } + /// Create new empty `TestBuffer` instance. pub fn empty() -> Self { Self::new("") } + #[allow(dead_code)] + pub(crate) fn read_buf_slice(&self) -> Ref<'_, [u8]> { + Ref::map(self.read_buf.borrow(), |b| b.as_ref()) + } + + #[allow(dead_code)] + pub(crate) fn read_buf_slice_mut(&self) -> RefMut<'_, [u8]> { + RefMut::map(self.read_buf.borrow_mut(), |b| b.as_mut()) + } + + #[allow(dead_code)] + pub(crate) fn write_buf_slice(&self) -> Ref<'_, [u8]> { + Ref::map(self.write_buf.borrow(), |b| b.as_ref()) + } + + #[allow(dead_code)] + pub(crate) fn write_buf_slice_mut(&self) -> RefMut<'_, [u8]> { + RefMut::map(self.write_buf.borrow_mut(), |b| b.as_mut()) + } + + #[allow(dead_code)] + pub(crate) fn take_write_buf(&self) -> Bytes { + self.write_buf.borrow_mut().split().freeze() + } + /// Add data to read buffer. pub fn extend_read_buf>(&mut self, data: T) { - self.read_buf.extend_from_slice(data.as_ref()) + self.read_buf.borrow_mut().extend_from_slice(data.as_ref()) } } impl io::Read for TestBuffer { fn read(&mut self, dst: &mut [u8]) -> Result { - if self.read_buf.is_empty() { + if self.read_buf.borrow().is_empty() { if self.err.is_some() { - Err(self.err.take().unwrap()) + Err(Rc::try_unwrap(self.err.take().unwrap()).unwrap()) } else { Err(io::Error::new(io::ErrorKind::WouldBlock, "")) } } else { - let size = std::cmp::min(self.read_buf.len(), dst.len()); - let b = self.read_buf.split_to(size); + let size = std::cmp::min(self.read_buf.borrow().len(), dst.len()); + let b = self.read_buf.borrow_mut().split_to(size); dst[..size].copy_from_slice(&b); Ok(size) } @@ -206,7 +220,7 @@ impl io::Read for TestBuffer { impl io::Write for TestBuffer { fn write(&mut self, buf: &[u8]) -> io::Result { - self.write_buf.extend(buf); + self.write_buf.borrow_mut().extend(buf); Ok(buf.len()) } diff --git a/actix-http/src/ws/codec.rs b/actix-http/src/ws/codec.rs index 861855fef..588a6fd95 100644 --- a/actix-http/src/ws/codec.rs +++ b/actix-http/src/ws/codec.rs @@ -2,10 +2,13 @@ use actix_codec::{Decoder, Encoder}; use bitflags::bitflags; use bytes::{Bytes, BytesMut}; use bytestring::ByteString; +use tracing::error; -use super::frame::Parser; -use super::proto::{CloseReason, OpCode}; -use super::ProtocolError; +use super::{ + frame::Parser, + proto::{CloseReason, OpCode}, + ProtocolError, +}; /// A WebSocket message. #[derive(Debug, PartialEq)] diff --git a/actix-http/src/ws/dispatcher.rs b/actix-http/src/ws/dispatcher.rs index f12ae1b1a..2f6b2363b 100644 --- a/actix-http/src/ws/dispatcher.rs +++ b/actix-http/src/ws/dispatcher.rs @@ -1,6 +1,8 @@ -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_service::{IntoService, Service}; @@ -71,8 +73,8 @@ mod inner { use actix_service::{IntoService, Service}; use futures_core::stream::Stream; use local_channel::mpsc; - use log::debug; use pin_project_lite::pin_project; + use tracing::debug; use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed}; diff --git a/actix-http/src/ws/frame.rs b/actix-http/src/ws/frame.rs index b58ef7362..3659b6c3b 100644 --- a/actix-http/src/ws/frame.rs +++ b/actix-http/src/ws/frame.rs @@ -1,11 +1,13 @@ use std::convert::TryFrom; use bytes::{Buf, BufMut, BytesMut}; -use log::debug; +use tracing::debug; -use crate::ws::mask::apply_mask; -use crate::ws::proto::{CloseCode, CloseReason, OpCode}; -use crate::ws::ProtocolError; +use super::{ + mask::apply_mask, + proto::{CloseCode, CloseReason, OpCode}, + ProtocolError, +}; /// A struct representing a WebSocket frame. #[derive(Debug)] @@ -15,7 +17,6 @@ impl Parser { fn parse_metadata( src: &[u8], server: bool, - max_size: usize, ) -> Result)>, ProtocolError> { let chunk_len = src.len(); @@ -58,20 +59,12 @@ impl Parser { return Ok(None); } let len = u64::from_be_bytes(TryFrom::try_from(&src[idx..idx + 8]).unwrap()); - if len > max_size as u64 { - return Err(ProtocolError::Overflow); - } idx += 8; len as usize } else { len as usize }; - // check for max allowed size - if length > max_size { - return Err(ProtocolError::Overflow); - } - let mask = if server { if chunk_len < idx + 4 { return Ok(None); @@ -96,11 +89,10 @@ impl Parser { max_size: usize, ) -> Result)>, ProtocolError> { // try to parse ws frame metadata - let (idx, finished, opcode, length, mask) = - match Parser::parse_metadata(src, server, max_size)? { - None => return Ok(None), - Some(res) => res, - }; + let (idx, finished, opcode, length, mask) = match Parser::parse_metadata(src, server)? { + None => return Ok(None), + Some(res) => res, + }; // not enough data if src.len() < idx + length { @@ -110,6 +102,13 @@ impl Parser { // remove prefix src.advance(idx); + // check for max allowed size + if length > max_size { + // drop the payload + src.advance(length); + return Err(ProtocolError::Overflow); + } + // no need for body if length == 0 { return Ok(Some((finished, opcode, None))); @@ -337,6 +336,30 @@ mod tests { } } + #[test] + fn test_parse_frame_max_size_recoverability() { + let mut buf = BytesMut::new(); + // The first text frame with length == 2, payload doesn't matter. + buf.extend(&[0b0000_0001u8, 0b0000_0010u8, 0b0000_0000u8, 0b0000_0000u8]); + // Next binary frame with length == 2 and payload == `[0x1111_1111u8, 0x1111_1111u8]`. + buf.extend(&[0b0000_0010u8, 0b0000_0010u8, 0b1111_1111u8, 0b1111_1111u8]); + + assert_eq!(buf.len(), 8); + assert!(matches!( + Parser::parse(&mut buf, false, 1), + Err(ProtocolError::Overflow) + )); + assert_eq!(buf.len(), 4); + let frame = extract(Parser::parse(&mut buf, false, 2)); + assert!(!frame.finished); + assert_eq!(frame.opcode, OpCode::Binary); + assert_eq!( + frame.payload, + Bytes::from(vec![0b1111_1111u8, 0b1111_1111u8]) + ); + assert_eq!(buf.len(), 0); + } + #[test] fn test_ping_frame() { let mut buf = BytesMut::new(); diff --git a/actix-http/src/ws/mask.rs b/actix-http/src/ws/mask.rs index 20b4372a0..be72e5631 100644 --- a/actix-http/src/ws/mask.rs +++ b/actix-http/src/ws/mask.rs @@ -47,40 +47,6 @@ pub fn apply_mask_fast32(buf: &mut [u8], mask: [u8; 4]) { mod tests { use super::*; - // legacy test from old apply mask test. kept for now for back compat test. - // TODO: remove it and favor the other test. - #[test] - fn test_apply_mask_legacy() { - let mask = [0x6d, 0xb6, 0xb2, 0x80]; - - let unmasked = vec![ - 0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, 0x74, 0xf9, - 0x12, 0x03, - ]; - - // Check masking with proper alignment. - { - let mut masked = unmasked.clone(); - apply_mask_fallback(&mut masked, mask); - - let mut masked_fast = unmasked.clone(); - apply_mask(&mut masked_fast, mask); - - assert_eq!(masked, masked_fast); - } - - // Check masking without alignment. - { - let mut masked = unmasked.clone(); - apply_mask_fallback(&mut masked[1..], mask); - - let mut masked_fast = unmasked; - apply_mask(&mut masked_fast[1..], mask); - - assert_eq!(masked, masked_fast); - } - } - #[test] fn test_apply_mask() { let mask = [0x6d, 0xb6, 0xb2, 0x80]; diff --git a/actix-http/src/ws/proto.rs b/actix-http/src/ws/proto.rs index 4227f221d..01fe9dd3c 100644 --- a/actix-http/src/ws/proto.rs +++ b/actix-http/src/ws/proto.rs @@ -3,6 +3,8 @@ use std::{ fmt, }; +use tracing::error; + /// Operation codes defined in [RFC 6455 §11.8]. /// /// [RFC 6455]: https://datatracker.ietf.org/doc/html/rfc6455#section-11.8 @@ -58,7 +60,7 @@ impl From for u8 { Ping => 9, Pong => 10, Bad => { - log::error!("Attempted to convert invalid opcode to u8. This is a bug."); + error!("Attempted to convert invalid opcode to u8. This is a bug."); 8 // if this somehow happens, a close frame will help us tear down quickly } } diff --git a/actix-http/tests/test_client.rs b/actix-http/tests/test_client.rs index a3adcdfd6..5888527f1 100644 --- a/actix-http/tests/test_client.rs +++ b/actix-http/tests/test_client.rs @@ -31,7 +31,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World"; #[actix_rt::test] -async fn test_h1_v2() { +async fn h1_v2() { let srv = test_server(move || { HttpService::build() .finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR))) @@ -59,7 +59,7 @@ async fn test_h1_v2() { } #[actix_rt::test] -async fn test_connection_close() { +async fn connection_close() { let srv = test_server(move || { HttpService::build() .finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR))) @@ -73,7 +73,7 @@ async fn test_connection_close() { } #[actix_rt::test] -async fn test_with_query_parameter() { +async fn with_query_parameter() { let srv = test_server(move || { HttpService::build() .finish(|req: Request| async move { @@ -104,7 +104,7 @@ impl From for Response { } #[actix_rt::test] -async fn test_h1_expect() { +async fn h1_expect() { let srv = test_server(move || { HttpService::build() .expect(|req: Request| async { diff --git a/actix-http/tests/test_h2_timer.rs b/actix-http/tests/test_h2_timer.rs index 2b9c26e4a..2e1480297 100644 --- a/actix-http/tests/test_h2_timer.rs +++ b/actix-http/tests/test_h2_timer.rs @@ -1,4 +1,4 @@ -use std::io; +use std::{io, time::Duration}; use actix_http::{error::Error, HttpService, Response}; use actix_server::Server; @@ -19,7 +19,7 @@ async fn h2_ping_pong() -> io::Result<()> { .workers(1) .listen("h2_ping_pong", lst, || { HttpService::build() - .keep_alive(3) + .keep_alive(Duration::from_secs(3)) .h2(|_| async { Ok::<_, Error>(Response::ok()) }) .tcp() })? @@ -92,10 +92,10 @@ async fn h2_handshake_timeout() -> io::Result<()> { .workers(1) .listen("h2_ping_pong", lst, || { HttpService::build() - .keep_alive(30) + .keep_alive(Duration::from_secs(30)) // set first request timeout to 5 seconds. // this is the timeout used for http2 handshake. - .client_timeout(5000) + .client_request_timeout(Duration::from_secs(5)) .h2(|_| async { Ok::<_, Error>(Response::ok()) }) .tcp() })? diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index 1e371473f..b97b2e45b 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -2,13 +2,13 @@ extern crate tls_openssl as openssl; -use std::{convert::Infallible, io}; +use std::{convert::Infallible, io, time::Duration}; use actix_http::{ body::{BodyStream, BoxBody, SizedStream}, error::PayloadError, header::{self, HeaderValue}, - Error, HttpService, Method, Request, Response, StatusCode, Version, + Error, HttpService, Method, Request, Response, StatusCode, TlsAcceptorConfig, Version, }; use actix_http_test::test_server; use actix_service::{fn_service, ServiceFactoryExt}; @@ -66,7 +66,7 @@ fn tls_config() -> SslAcceptor { } #[actix_rt::test] -async fn test_h2() -> io::Result<()> { +async fn h2() -> io::Result<()> { let srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Error>(Response::ok())) @@ -81,7 +81,7 @@ async fn test_h2() -> io::Result<()> { } #[actix_rt::test] -async fn test_h2_1() -> io::Result<()> { +async fn h2_1() -> io::Result<()> { let srv = test_server(move || { HttpService::build() .finish(|req: Request| { @@ -89,7 +89,10 @@ async fn test_h2_1() -> io::Result<()> { assert_eq!(req.version(), Version::HTTP_2); ok::<_, Error>(Response::ok()) }) - .openssl(tls_config()) + .openssl_with_config( + tls_config(), + TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)), + ) .map_err(|_| ()) }) .await; @@ -100,7 +103,7 @@ async fn test_h2_1() -> io::Result<()> { } #[actix_rt::test] -async fn test_h2_body() -> io::Result<()> { +async fn h2_body() -> io::Result<()> { let data = "HELLOWORLD".to_owned().repeat(64 * 1024); // 640 KiB let mut srv = test_server(move || { HttpService::build() @@ -122,7 +125,7 @@ async fn test_h2_body() -> io::Result<()> { } #[actix_rt::test] -async fn test_h2_content_length() { +async fn h2_content_length() { let srv = test_server(move || { HttpService::build() .h2(|req: Request| { @@ -164,7 +167,7 @@ async fn test_h2_content_length() { } #[actix_rt::test] -async fn test_h2_headers() { +async fn h2_headers() { let data = STR.repeat(10); let data2 = data.clone(); @@ -229,7 +232,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World"; #[actix_rt::test] -async fn test_h2_body2() { +async fn h2_body2() { let mut srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) @@ -247,7 +250,7 @@ async fn test_h2_body2() { } #[actix_rt::test] -async fn test_h2_head_empty() { +async fn h2_head_empty() { let mut srv = test_server(move || { HttpService::build() .finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) @@ -271,7 +274,7 @@ async fn test_h2_head_empty() { } #[actix_rt::test] -async fn test_h2_head_binary() { +async fn h2_head_binary() { let mut srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) @@ -294,7 +297,7 @@ async fn test_h2_head_binary() { } #[actix_rt::test] -async fn test_h2_head_binary2() { +async fn h2_head_binary2() { let srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) @@ -313,7 +316,7 @@ async fn test_h2_head_binary2() { } #[actix_rt::test] -async fn test_h2_body_length() { +async fn h2_body_length() { let mut srv = test_server(move || { HttpService::build() .h2(|_| async { @@ -338,7 +341,7 @@ async fn test_h2_body_length() { } #[actix_rt::test] -async fn test_h2_body_chunked_explicit() { +async fn h2_body_chunked_explicit() { let mut srv = test_server(move || { HttpService::build() .h2(|_| { @@ -366,7 +369,7 @@ async fn test_h2_body_chunked_explicit() { } #[actix_rt::test] -async fn test_h2_response_http_error_handling() { +async fn h2_response_http_error_handling() { let mut srv = test_server(move || { HttpService::build() .h2(fn_service(|_| { @@ -406,7 +409,7 @@ impl From for Response { } #[actix_rt::test] -async fn test_h2_service_error() { +async fn h2_service_error() { let mut srv = test_server(move || { HttpService::build() .h2(|_| err::, _>(BadRequest)) @@ -424,7 +427,7 @@ async fn test_h2_service_error() { } #[actix_rt::test] -async fn test_h2_on_connect() { +async fn h2_on_connect() { let srv = test_server(move || { HttpService::build() .on_connect_ext(|_, data| { diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 51fefae72..2bbf1524b 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -8,13 +8,14 @@ use std::{ net::{SocketAddr, TcpStream as StdTcpStream}, sync::Arc, task::Poll, + time::Duration, }; use actix_http::{ body::{BodyStream, BoxBody, SizedStream}, error::PayloadError, header::{self, HeaderName, HeaderValue}, - Error, HttpService, Method, Request, Response, StatusCode, Version, + Error, HttpService, Method, Request, Response, StatusCode, TlsAcceptorConfig, Version, }; use actix_http_test::test_server; use actix_rt::pin; @@ -106,7 +107,7 @@ pub fn get_negotiated_alpn_protocol( } #[actix_rt::test] -async fn test_h1() -> io::Result<()> { +async fn h1() -> io::Result<()> { let srv = test_server(move || { HttpService::build() .h1(|_| ok::<_, Error>(Response::ok())) @@ -120,7 +121,7 @@ async fn test_h1() -> io::Result<()> { } #[actix_rt::test] -async fn test_h2() -> io::Result<()> { +async fn h2() -> io::Result<()> { let srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Error>(Response::ok())) @@ -134,7 +135,7 @@ async fn test_h2() -> io::Result<()> { } #[actix_rt::test] -async fn test_h1_1() -> io::Result<()> { +async fn h1_1() -> io::Result<()> { let srv = test_server(move || { HttpService::build() .h1(|req: Request| { @@ -152,7 +153,7 @@ async fn test_h1_1() -> io::Result<()> { } #[actix_rt::test] -async fn test_h2_1() -> io::Result<()> { +async fn h2_1() -> io::Result<()> { let srv = test_server(move || { HttpService::build() .finish(|req: Request| { @@ -160,7 +161,10 @@ async fn test_h2_1() -> io::Result<()> { assert_eq!(req.version(), Version::HTTP_2); ok::<_, Error>(Response::ok()) }) - .rustls(tls_config()) + .rustls_with_config( + tls_config(), + TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)), + ) }) .await; @@ -170,7 +174,7 @@ async fn test_h2_1() -> io::Result<()> { } #[actix_rt::test] -async fn test_h2_body1() -> io::Result<()> { +async fn h2_body1() -> io::Result<()> { let data = "HELLOWORLD".to_owned().repeat(64 * 1024); let mut srv = test_server(move || { HttpService::build() @@ -191,7 +195,7 @@ async fn test_h2_body1() -> io::Result<()> { } #[actix_rt::test] -async fn test_h2_content_length() { +async fn h2_content_length() { let srv = test_server(move || { HttpService::build() .h2(|req: Request| { @@ -212,6 +216,7 @@ async fn test_h2_content_length() { let value = HeaderValue::from_static("0"); { + #[allow(clippy::single_element_loop)] for &i in &[0] { let req = srv .request(Method::HEAD, srv.surl(&format!("/{}", i))) @@ -226,6 +231,7 @@ async fn test_h2_content_length() { // assert_eq!(response.headers().get(&header), None); } + #[allow(clippy::single_element_loop)] for &i in &[1] { let req = srv .request(Method::GET, srv.surl(&format!("/{}", i))) @@ -245,7 +251,7 @@ async fn test_h2_content_length() { } #[actix_rt::test] -async fn test_h2_headers() { +async fn h2_headers() { let data = STR.repeat(10); let data2 = data.clone(); @@ -309,7 +315,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World"; #[actix_rt::test] -async fn test_h2_body2() { +async fn h2_body2() { let mut srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) @@ -326,7 +332,7 @@ async fn test_h2_body2() { } #[actix_rt::test] -async fn test_h2_head_empty() { +async fn h2_head_empty() { let mut srv = test_server(move || { HttpService::build() .finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) @@ -352,7 +358,7 @@ async fn test_h2_head_empty() { } #[actix_rt::test] -async fn test_h2_head_binary() { +async fn h2_head_binary() { let mut srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) @@ -377,7 +383,7 @@ async fn test_h2_head_binary() { } #[actix_rt::test] -async fn test_h2_head_binary2() { +async fn h2_head_binary2() { let srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) @@ -398,7 +404,7 @@ async fn test_h2_head_binary2() { } #[actix_rt::test] -async fn test_h2_body_length() { +async fn h2_body_length() { let mut srv = test_server(move || { HttpService::build() .h2(|_| { @@ -420,7 +426,7 @@ async fn test_h2_body_length() { } #[actix_rt::test] -async fn test_h2_body_chunked_explicit() { +async fn h2_body_chunked_explicit() { let mut srv = test_server(move || { HttpService::build() .h2(|_| { @@ -447,7 +453,7 @@ async fn test_h2_body_chunked_explicit() { } #[actix_rt::test] -async fn test_h2_response_http_error_handling() { +async fn h2_response_http_error_handling() { let mut srv = test_server(move || { HttpService::build() .h2(fn_factory_with_config(|_: ()| { @@ -486,7 +492,7 @@ impl From for Response { } #[actix_rt::test] -async fn test_h2_service_error() { +async fn h2_service_error() { let mut srv = test_server(move || { HttpService::build() .h2(|_| err::, _>(BadRequest)) @@ -503,7 +509,7 @@ async fn test_h2_service_error() { } #[actix_rt::test] -async fn test_h1_service_error() { +async fn h1_service_error() { let mut srv = test_server(move || { HttpService::build() .h1(|_| err::, _>(BadRequest)) @@ -524,7 +530,7 @@ const HTTP1_1_ALPN_PROTOCOL: &[u8] = b"http/1.1"; const CUSTOM_ALPN_PROTOCOL: &[u8] = b"custom"; #[actix_rt::test] -async fn test_alpn_h1() -> io::Result<()> { +async fn alpn_h1() -> io::Result<()> { let srv = test_server(move || { let mut config = tls_config(); config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec()); @@ -546,7 +552,7 @@ async fn test_alpn_h1() -> io::Result<()> { } #[actix_rt::test] -async fn test_alpn_h2() -> io::Result<()> { +async fn alpn_h2() -> io::Result<()> { let srv = test_server(move || { let mut config = tls_config(); config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec()); @@ -572,7 +578,7 @@ async fn test_alpn_h2() -> io::Result<()> { } #[actix_rt::test] -async fn test_alpn_h2_1() -> io::Result<()> { +async fn alpn_h2_1() -> io::Result<()> { let srv = test_server(move || { let mut config = tls_config(); config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec()); diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index 1bb574fd6..e8d103c96 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -2,7 +2,7 @@ use std::{ convert::Infallible, io::{Read, Write}, net, thread, - time::Duration, + time::{Duration, Instant}, }; use actix_http::{ @@ -22,12 +22,12 @@ use futures_util::{ use regex::Regex; #[actix_rt::test] -async fn test_h1() { +async fn h1_basic() { let mut srv = test_server(|| { HttpService::build() .keep_alive(KeepAlive::Disabled) - .client_timeout(1000) - .client_disconnect(1000) + .client_request_timeout(Duration::from_secs(1)) + .client_disconnect_timeout(Duration::from_secs(1)) .h1(|req: Request| { assert!(req.peer_addr().is_some()); ok::<_, Infallible>(Response::ok()) @@ -43,12 +43,12 @@ async fn test_h1() { } #[actix_rt::test] -async fn test_h1_2() { +async fn h1_2() { let mut srv = test_server(|| { HttpService::build() .keep_alive(KeepAlive::Disabled) - .client_timeout(1000) - .client_disconnect(1000) + .client_request_timeout(Duration::from_secs(1)) + .client_disconnect_timeout(Duration::from_secs(1)) .finish(|req: Request| { assert!(req.peer_addr().is_some()); assert_eq!(req.version(), http::Version::HTTP_11); @@ -75,7 +75,7 @@ impl From for Response { } #[actix_rt::test] -async fn test_expect_continue() { +async fn expect_continue() { let mut srv = test_server(|| { HttpService::build() .expect(fn_service(|req: Request| { @@ -106,7 +106,7 @@ async fn test_expect_continue() { } #[actix_rt::test] -async fn test_expect_continue_h1() { +async fn expect_continue_h1() { let mut srv = test_server(|| { HttpService::build() .expect(fn_service(|req: Request| { @@ -139,7 +139,7 @@ async fn test_expect_continue_h1() { } #[actix_rt::test] -async fn test_chunked_payload() { +async fn chunked_payload() { let chunk_sizes = vec![32768, 32, 32768]; let total_size: usize = chunk_sizes.iter().sum(); @@ -197,26 +197,43 @@ async fn test_chunked_payload() { } #[actix_rt::test] -async fn test_slow_request() { +async fn slow_request_408() { let mut srv = test_server(|| { HttpService::build() - .client_timeout(100) + .client_request_timeout(Duration::from_millis(200)) + .keep_alive(Duration::from_secs(2)) .finish(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; + let start = Instant::now(); + let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); - let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n"); + let _ = stream.write_all(b"GET /test HTTP/1.1\r\n"); let mut data = String::new(); let _ = stream.read_to_string(&mut data); - assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); + assert!( + data.starts_with("HTTP/1.1 408 Request Timeout"), + "response was not 408: {}", + data + ); + + let diff = start.elapsed(); + + if diff < Duration::from_secs(1) { + // test success + } else if diff < Duration::from_secs(3) { + panic!("request seems to have wrongly timed-out according to keep-alive"); + } else { + panic!("request took way too long to time out"); + } srv.stop().await; } #[actix_rt::test] -async fn test_http1_malformed_request() { +async fn http1_malformed_request() { let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok())) @@ -234,7 +251,7 @@ async fn test_http1_malformed_request() { } #[actix_rt::test] -async fn test_http1_keepalive() { +async fn http1_keepalive() { let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok())) @@ -257,23 +274,25 @@ async fn test_http1_keepalive() { } #[actix_rt::test] -async fn test_http1_keepalive_timeout() { +async fn http1_keepalive_timeout() { let mut srv = test_server(|| { HttpService::build() - .keep_alive(1) + .keep_alive(Duration::from_secs(1)) .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); - let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n\r\n"); - let mut data = vec![0; 1024]; + + let _ = stream.write_all(b"GET /test HTTP/1.1\r\n\r\n"); + let mut data = vec![0; 256]; let _ = stream.read(&mut data); assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); + thread::sleep(Duration::from_millis(1100)); - let mut data = vec![0; 1024]; + let mut data = vec![0; 256]; let res = stream.read(&mut data).unwrap(); assert_eq!(res, 0); @@ -281,7 +300,7 @@ async fn test_http1_keepalive_timeout() { } #[actix_rt::test] -async fn test_http1_keepalive_close() { +async fn http1_keepalive_close() { let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok())) @@ -303,7 +322,7 @@ async fn test_http1_keepalive_close() { } #[actix_rt::test] -async fn test_http10_keepalive_default_close() { +async fn http10_keepalive_default_close() { let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok())) @@ -325,7 +344,7 @@ async fn test_http10_keepalive_default_close() { } #[actix_rt::test] -async fn test_http10_keepalive() { +async fn http10_keepalive() { let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok())) @@ -354,7 +373,7 @@ async fn test_http10_keepalive() { } #[actix_rt::test] -async fn test_http1_keepalive_disabled() { +async fn http1_keepalive_disabled() { let mut srv = test_server(|| { HttpService::build() .keep_alive(KeepAlive::Disabled) @@ -377,7 +396,7 @@ async fn test_http1_keepalive_disabled() { } #[actix_rt::test] -async fn test_content_length() { +async fn content_length() { use actix_http::{ header::{HeaderName, HeaderValue}, StatusCode, @@ -426,7 +445,7 @@ async fn test_content_length() { } #[actix_rt::test] -async fn test_h1_headers() { +async fn h1_headers() { let data = STR.repeat(10); let data2 = data.clone(); @@ -492,7 +511,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World"; #[actix_rt::test] -async fn test_h1_body() { +async fn h1_body() { let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) @@ -511,7 +530,7 @@ async fn test_h1_body() { } #[actix_rt::test] -async fn test_h1_head_empty() { +async fn h1_head_empty() { let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) @@ -538,7 +557,7 @@ async fn test_h1_head_empty() { } #[actix_rt::test] -async fn test_h1_head_binary() { +async fn h1_head_binary() { let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) @@ -565,7 +584,7 @@ async fn test_h1_head_binary() { } #[actix_rt::test] -async fn test_h1_head_binary2() { +async fn h1_head_binary2() { let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) @@ -588,7 +607,7 @@ async fn test_h1_head_binary2() { } #[actix_rt::test] -async fn test_h1_body_length() { +async fn h1_body_length() { let mut srv = test_server(|| { HttpService::build() .h1(|_| { @@ -612,7 +631,7 @@ async fn test_h1_body_length() { } #[actix_rt::test] -async fn test_h1_body_chunked_explicit() { +async fn h1_body_chunked_explicit() { let mut srv = test_server(|| { HttpService::build() .h1(|_| { @@ -649,7 +668,7 @@ async fn test_h1_body_chunked_explicit() { } #[actix_rt::test] -async fn test_h1_body_chunked_implicit() { +async fn h1_body_chunked_implicit() { let mut srv = test_server(|| { HttpService::build() .h1(|_| { @@ -680,7 +699,7 @@ async fn test_h1_body_chunked_implicit() { } #[actix_rt::test] -async fn test_h1_response_http_error_handling() { +async fn h1_response_http_error_handling() { let mut srv = test_server(|| { HttpService::build() .h1(fn_service(|_| { @@ -719,7 +738,7 @@ impl From for Response { } #[actix_rt::test] -async fn test_h1_service_error() { +async fn h1_service_error() { let mut srv = test_server(|| { HttpService::build() .h1(|_| err::, _>(BadRequest)) @@ -738,7 +757,7 @@ async fn test_h1_service_error() { } #[actix_rt::test] -async fn test_h1_on_connect() { +async fn h1_on_connect() { let mut srv = test_server(|| { HttpService::build() .on_connect_ext(|_, data| { @@ -761,7 +780,7 @@ async fn test_h1_on_connect() { /// Tests compliance with 304 Not Modified spec in RFC 7232 §4.1. /// https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 #[actix_rt::test] -async fn test_not_modified_spec_h1() { +async fn not_modified_spec_h1() { // TODO: this test needing a few seconds to complete reveals some weirdness with either the // dispatcher or the client, though similar hangs occur on other tests in this file, only // succeeding, it seems, because of the keepalive timer @@ -831,7 +850,8 @@ async fn test_not_modified_spec_h1() { Some(&header::HeaderValue::from_static("4")), ); // server does not prevent payload from being sent but clients may choose not to read it - // TODO: this is probably a bug, especially since CL header can differ in length from the body + // TODO: this is probably a bug in the client, especially since CL header can differ in length + // from the body assert!(!srv.load_body(res).await.unwrap().is_empty()); // TODO: add stream response tests diff --git a/actix-http/tests/test_ws.rs b/actix-http/tests/test_ws.rs index 5a9bf8108..0f6c2da8d 100644 --- a/actix-http/tests/test_ws.rs +++ b/actix-http/tests/test_ws.rs @@ -109,7 +109,7 @@ async fn service(msg: Frame) -> Result { } #[actix_rt::test] -async fn test_simple() { +async fn simple() { let mut srv = test_server(|| { HttpService::build() .upgrade(fn_factory(|| async { diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 92feade3b..ed5c97e1d 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,6 +1,15 @@ # Changes -## Unreleased - 2021-xx-xx +## Unreleased - 2022-xx-xx +- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. + + +## 0.4.0 - 2022-02-25 +- No significant changes since `0.4.0-beta.13`. + + +## 0.4.0-beta.13 - 2022-01-31 +- No significant changes since `0.4.0-beta.12`. ## 0.4.0-beta.12 - 2022-01-04 diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 327252dbf..507e92752 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.4.0-beta.12" +version = "0.4.0" authors = ["Nikolay Kim "] description = "Multipart form support for Actix Web" keywords = ["http", "web", "framework", "async", "futures"] @@ -14,8 +14,8 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.21", default-features = false } +actix-utils = "3" +actix-web = { version = "4", 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.19" +actix-http = "3.0.0" futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } tokio = { version = "1.8.4", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-multipart/README.md b/actix-multipart/README.md index 91cd8a6e9..0b1e2df17 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.12)](https://docs.rs/actix-multipart/0.4.0-beta.12) -[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0)](https://docs.rs/actix-multipart/0.4.0) +![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg) ![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.12/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.12) +[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0) [![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-router/CHANGES.md b/actix-router/CHANGES.md index 6253b522a..1e4fc41f2 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -1,6 +1,90 @@ # Changes -## Unreleased - 2021-xx-xx +## Unreleased - 2022-xx-xx +- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. + + +## 0.5.0 - 2022-02-22 +### Added +- Add `Path::as_str`. [#2590] +- Add `ResourceDef::set_name`. [#373][net#373] +- Add `RouterBuilder::push`. [#2612] +- Implement `IntoPatterns` for `bytestring::ByteString`. [#372][net#372] +- Introduce `ResourceDef::join`. [#380][net#380] +- Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373][net#373] +- `Resource` is now implemented for `&mut Path<_>` and `RefMut>`. [#2568] +- Support `build_resource_path` on multi-pattern resources. [#2356] +- Support multi-pattern prefixes and joins. [#2356] + +### Changed +- Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612] +- Deprecate `Path::path`. [#2590] +- Disallow prefix routes with tail segments. [#379][net#379] +- Enforce path separators on dynamic prefixes. [#378][net#378] +- Minimum supported Rust version (MSRV) is now 1.54. +- Prefix segments now always end with with a segment delimiter or end-of-input. [#2355] +- Prefix segments with trailing slashes define a trailing empty segment. [#2355] +- `Quoter::requote` now returns `Option>`. [#2613] +- Re-work `IntoPatterns` trait, adding a `Patterns` enum. [#372][net#372] +- Rename `Path::{len => segment_count}` to be more descriptive of its purpose. [#370][net#370] +- Rename `ResourceDef::{is_prefix_match => find_match}`. [#373][net#373] +- Rename `ResourceDef::{match_path => capture_match_info}`. [#373][net#373] +- Rename `ResourceDef::{match_path_checked => capture_match_info_fn}`. [#373][net#373] +- Rename `ResourceDef::{resource_path => resource_path_from_iter}`. [#371][net#371] +- Rename `ResourceDef::{resource_path_named => resource_path_from_map}`. [#371][net#371] +- Rename `Router::{*_checked => *_fn}`. [#373][net#373] +- Replace `Option` with `U` in `Router` API. [#2612] +- `Resource` trait now uses an associated type, `Path`, instead of a generic parameter. [#2568] +- `ResourceDef::pattern` now returns the first pattern in multi-pattern resources. [#2356] +- `ResourceDef::resource_path_from_iter` now takes an `IntoIterator`. [#373][net#373] +- Return type of `ResourceDef::name` is now `Option<&str>`. [#373][net#373] +- Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373][net#373] + +### Fixed +- Fix `ResourceDef`'s `PartialEq` implementation. [#373][net#373] +- Fix segment interpolation leaving `Path` in unintended state after matching. [#368][net#368] +- Improve malformed path error message. [#384][net#384] +- `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566] +- Relax bounds on `Router::recognize*` and `ResourceDef::capture_match_info`. [#2612] +- Static patterns in multi-patterns are no longer interpreted as regex. [#366][net#366] + +### Removed +- `ResourceDef::name_mut`. [#373][net#373] +- Unused `ResourceInfo`. [#2612] + +[#2355]: https://github.com/actix/actix-web/pull/2355 +[#2356]: https://github.com/actix/actix-web/pull/2356 +[#2566]: https://github.com/actix/actix-net/pull/2566 +[#2568]: https://github.com/actix/actix-web/pull/2568 +[#2590]: https://github.com/actix/actix-web/pull/2590 +[#2612]: https://github.com/actix/actix-web/pull/2612 +[#2613]: https://github.com/actix/actix-web/pull/2613 +[net#366]: https://github.com/actix/actix-net/pull/366 +[net#368]: https://github.com/actix/actix-net/pull/368 +[net#368]: https://github.com/actix/actix-net/pull/368 +[net#370]: https://github.com/actix/actix-net/pull/370 +[net#371]: https://github.com/actix/actix-net/pull/371 +[net#372]: https://github.com/actix/actix-net/pull/372 +[net#373]: https://github.com/actix/actix-net/pull/373 +[net#378]: https://github.com/actix/actix-net/pull/378 +[net#379]: https://github.com/actix/actix-net/pull/379 +[net#380]: https://github.com/actix/actix-net/pull/380 +[net#384]: https://github.com/actix/actix-net/pull/384 + + +
+0.5.0 Pre-Releases + +## 0.5.0-rc.3 - 2022-01-31 +- Remove unused `ResourceInfo`. [#2612] +- Add `RouterBuilder::push`. [#2612] +- Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612] +- Replace `Option` with `U` in `Router` API. [#2612] +- Relax bounds on `Router::recognize*` and `ResourceDef::capture_match_info`. [#2612] +- `Quoter::requote` now returns `Option>`. [#2613] + +[#2612]: https://github.com/actix/actix-web/pull/2612 +[#2613]: https://github.com/actix/actix-web/pull/2613 ## 0.5.0-rc.2 - 2022-01-21 @@ -29,10 +113,10 @@ ## 0.5.0-beta.2 - 2021-09-09 -- Introduce `ResourceDef::join`. [#380] -- Disallow prefix routes with tail segments. [#379] -- Enforce path separators on dynamic prefixes. [#378] -- Improve malformed path error message. [#384] +- Introduce `ResourceDef::join`. [#380][net#380] +- Disallow prefix routes with tail segments. [#379][net#379] +- Enforce path separators on dynamic prefixes. [#378][net#378] +- Improve malformed path error message. [#384][net#384] - Prefix segments now always end with with a segment delimiter or end-of-input. [#2355] - Prefix segments with trailing slashes define a trailing empty segment. [#2355] - Support multi-pattern prefixes and joins. [#2356] @@ -40,52 +124,54 @@ - Support `build_resource_path` on multi-pattern resources. [#2356] - Minimum supported Rust version (MSRV) is now 1.51. -[#378]: https://github.com/actix/actix-net/pull/378 -[#379]: https://github.com/actix/actix-net/pull/379 -[#380]: https://github.com/actix/actix-net/pull/380 -[#384]: https://github.com/actix/actix-net/pull/384 +[net#378]: https://github.com/actix/actix-net/pull/378 +[net#379]: https://github.com/actix/actix-net/pull/379 +[net#380]: https://github.com/actix/actix-net/pull/380 +[net#384]: https://github.com/actix/actix-net/pull/384 [#2355]: https://github.com/actix/actix-web/pull/2355 [#2356]: https://github.com/actix/actix-web/pull/2356 ## 0.5.0-beta.1 - 2021-07-20 -- Fix a bug in multi-patterns where static patterns are interpreted as regex. [#366] -- Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373] -- Fix segment interpolation leaving `Path` in unintended state after matching. [#368] -- Fix `ResourceDef` `PartialEq` implementation. [#373] -- Re-work `IntoPatterns` trait, adding a `Patterns` enum. [#372] -- Implement `IntoPatterns` for `bytestring::ByteString`. [#372] -- Rename `Path::{len => segment_count}` to be more descriptive of it's purpose. [#370] -- Rename `ResourceDef::{resource_path => resource_path_from_iter}`. [#371] -- `ResourceDef::resource_path_from_iter` now takes an `IntoIterator`. [#373] -- Rename `ResourceDef::{resource_path_named => resource_path_from_map}`. [#371] -- Rename `ResourceDef::{is_prefix_match => find_match}`. [#373] -- Rename `ResourceDef::{match_path => capture_match_info}`. [#373] -- Rename `ResourceDef::{match_path_checked => capture_match_info_fn}`. [#373] -- Remove `ResourceDef::name_mut` and introduce `ResourceDef::set_name`. [#373] -- Rename `Router::{*_checked => *_fn}`. [#373] -- Return type of `ResourceDef::name` is now `Option<&str>`. [#373] -- Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373] +- Fix a bug in multi-patterns where static patterns are interpreted as regex. [#366][net#366] +- Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373][net#373] +- Fix segment interpolation leaving `Path` in unintended state after matching. [#368][net#368] +- Fix `ResourceDef` `PartialEq` implementation. [#373][net#373] +- Re-work `IntoPatterns` trait, adding a `Patterns` enum. [#372][net#372] +- Implement `IntoPatterns` for `bytestring::ByteString`. [#372][net#372] +- Rename `Path::{len => segment_count}` to be more descriptive of it's purpose. [#370][net#370] +- Rename `ResourceDef::{resource_path => resource_path_from_iter}`. [#371][net#371] +- `ResourceDef::resource_path_from_iter` now takes an `IntoIterator`. [#373][net#373] +- Rename `ResourceDef::{resource_path_named => resource_path_from_map}`. [#371][net#371] +- Rename `ResourceDef::{is_prefix_match => find_match}`. [#373][net#373] +- Rename `ResourceDef::{match_path => capture_match_info}`. [#373][net#373] +- Rename `ResourceDef::{match_path_checked => capture_match_info_fn}`. [#373][net#373] +- Remove `ResourceDef::name_mut` and introduce `ResourceDef::set_name`. [#373][net#373] +- Rename `Router::{*_checked => *_fn}`. [#373][net#373] +- Return type of `ResourceDef::name` is now `Option<&str>`. [#373][net#373] +- Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373][net#373] -[#368]: https://github.com/actix/actix-net/pull/368 -[#366]: https://github.com/actix/actix-net/pull/366 -[#368]: https://github.com/actix/actix-net/pull/368 -[#370]: https://github.com/actix/actix-net/pull/370 -[#371]: https://github.com/actix/actix-net/pull/371 -[#372]: https://github.com/actix/actix-net/pull/372 -[#373]: https://github.com/actix/actix-net/pull/373 +[net#368]: https://github.com/actix/actix-net/pull/368 +[net#366]: https://github.com/actix/actix-net/pull/366 +[net#368]: https://github.com/actix/actix-net/pull/368 +[net#370]: https://github.com/actix/actix-net/pull/370 +[net#371]: https://github.com/actix/actix-net/pull/371 +[net#372]: https://github.com/actix/actix-net/pull/372 +[net#373]: https://github.com/actix/actix-net/pull/373 + +
## 0.4.0 - 2021-06-06 -- When matching path parameters, `%25` is now kept in the percent-encoded form; no longer decoded to `%`. [#357] -- Path tail patterns now match new lines (`\n`) in request URL. [#360] -- Fixed a safety bug where `Path` could return a malformed string after percent decoding. [#359] -- Methods `Path::{add, add_static}` now take `impl Into>`. [#345] +- When matching path parameters, `%25` is now kept in the percent-encoded form; no longer decoded to `%`. [#357][net#357] +- Path tail patterns now match new lines (`\n`) in request URL. [#360][net#360] +- Fixed a safety bug where `Path` could return a malformed string after percent decoding. [#359][net#359] +- Methods `Path::{add, add_static}` now take `impl Into>`. [#345][net#345] -[#345]: https://github.com/actix/actix-net/pull/345 -[#357]: https://github.com/actix/actix-net/pull/357 -[#359]: https://github.com/actix/actix-net/pull/359 -[#360]: https://github.com/actix/actix-net/pull/360 +[net#345]: https://github.com/actix/actix-net/pull/345 +[net#357]: https://github.com/actix/actix-net/pull/357 +[net#359]: https://github.com/actix/actix-net/pull/359 +[net#360]: https://github.com/actix/actix-net/pull/360 ## 0.3.0 - 2019-12-31 @@ -93,15 +179,15 @@ ## 0.2.7 - 2021-02-06 -- Add `Router::recognize_checked` [#247] +- Add `Router::recognize_checked` [#247][net#247] -[#247]: https://github.com/actix/actix-net/pull/247 +[net#247]: https://github.com/actix/actix-net/pull/247 ## 0.2.6 - 2021-01-09 -- Use `bytestring` version range compatible with Bytes v1.0. [#246] +- Use `bytestring` version range compatible with Bytes v1.0. [#246][net#246] -[#246]: https://github.com/actix/actix-net/pull/246 +[net#246]: https://github.com/actix/actix-net/pull/246 ## 0.2.5 - 2020-09-20 diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml index 0d4e4f897..ceb5b14dc 100644 --- a/actix-router/Cargo.toml +++ b/actix-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-router" -version = "0.5.0-rc.2" +version = "0.5.0" authors = [ "Nikolay Kim ", "Ali MJ Al-Nasrawy ", @@ -21,18 +21,21 @@ default = ["http"] [dependencies] bytestring = ">=0.1.5, <2" -firestorm = "0.5" http = { version = "0.2.3", optional = true } -log = "0.4" regex = "1.5" serde = "1" +tracing = { version = "0.1.30", default-features = false, features = ["log"] } [dev-dependencies] criterion = { version = "0.3", features = ["html_reports"] } -firestorm = { version = "0.5", features = ["enable_system_time"] } http = "0.2.5" serde = { version = "1", features = ["derive"] } +percent-encoding = "2.1" [[bench]] name = "router" harness = false + +[[bench]] +name = "quoter" +harness = false diff --git a/actix-router/benches/quoter.rs b/actix-router/benches/quoter.rs new file mode 100644 index 000000000..c18f1620e --- /dev/null +++ b/actix-router/benches/quoter.rs @@ -0,0 +1,52 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +use std::borrow::Cow; + +fn compare_quoters(c: &mut Criterion) { + let mut group = c.benchmark_group("Compare Quoters"); + + let quoter = actix_router::Quoter::new(b"", b""); + let path_quoted = (0..=0x7f) + .map(|c| format!("%{:02X}", c)) + .collect::(); + let path_unquoted = ('\u{00}'..='\u{7f}').collect::(); + + group.bench_function("quoter_unquoted", |b| { + b.iter(|| { + for _ in 0..10 { + black_box(quoter.requote(path_unquoted.as_bytes())); + } + }); + }); + + group.bench_function("percent_encode_unquoted", |b| { + b.iter(|| { + for _ in 0..10 { + let decode = percent_encoding::percent_decode(path_unquoted.as_bytes()); + black_box(Into::>::into(decode)); + } + }); + }); + + group.bench_function("quoter_quoted", |b| { + b.iter(|| { + for _ in 0..10 { + black_box(quoter.requote(path_quoted.as_bytes())); + } + }); + }); + + group.bench_function("percent_encode_quoted", |b| { + b.iter(|| { + for _ in 0..10 { + let decode = percent_encoding::percent_decode(path_quoted.as_bytes()); + black_box(Into::>::into(decode)); + } + }); + }); + + group.finish(); +} + +criterion_group!(benches, compare_quoters); +criterion_main!(benches); diff --git a/actix-router/benches/router.rs b/actix-router/benches/router.rs index a428b9f13..6f6b67b48 100644 --- a/actix-router/benches/router.rs +++ b/actix-router/benches/router.rs @@ -145,7 +145,8 @@ macro_rules! register { concat!("/user/keys"), concat!("/user/keys/", $p1), ]; - std::array::IntoIter::new(arr) + + IntoIterator::into_iter(arr) }}; } @@ -158,7 +159,7 @@ fn call() -> impl Iterator { "/repos/rust-lang/rust/releases/1.51.0", ]; - std::array::IntoIter::new(arr) + IntoIterator::into_iter(arr) } fn compare_routers(c: &mut Criterion) { diff --git a/actix-router/examples/flamegraph.rs b/actix-router/examples/flamegraph.rs deleted file mode 100644 index 798cc22d9..000000000 --- a/actix-router/examples/flamegraph.rs +++ /dev/null @@ -1,169 +0,0 @@ -macro_rules! register { - (brackets) => {{ - register!(finish => "{p1}", "{p2}", "{p3}", "{p4}") - }}; - (finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{ - let arr = [ - concat!("/authorizations"), - concat!("/authorizations/", $p1), - concat!("/applications/", $p1, "/tokens/", $p2), - concat!("/events"), - concat!("/repos/", $p1, "/", $p2, "/events"), - concat!("/networks/", $p1, "/", $p2, "/events"), - concat!("/orgs/", $p1, "/events"), - concat!("/users/", $p1, "/received_events"), - concat!("/users/", $p1, "/received_events/public"), - concat!("/users/", $p1, "/events"), - concat!("/users/", $p1, "/events/public"), - concat!("/users/", $p1, "/events/orgs/", $p2), - concat!("/feeds"), - concat!("/notifications"), - concat!("/repos/", $p1, "/", $p2, "/notifications"), - concat!("/notifications/threads/", $p1), - concat!("/notifications/threads/", $p1, "/subscription"), - concat!("/repos/", $p1, "/", $p2, "/stargazers"), - concat!("/users/", $p1, "/starred"), - concat!("/user/starred"), - concat!("/user/starred/", $p1, "/", $p2), - concat!("/repos/", $p1, "/", $p2, "/subscribers"), - concat!("/users/", $p1, "/subscriptions"), - concat!("/user/subscriptions"), - concat!("/repos/", $p1, "/", $p2, "/subscription"), - concat!("/user/subscriptions/", $p1, "/", $p2), - concat!("/users/", $p1, "/gists"), - concat!("/gists"), - concat!("/gists/", $p1), - concat!("/gists/", $p1, "/star"), - concat!("/repos/", $p1, "/", $p2, "/git/blobs/", $p3), - concat!("/repos/", $p1, "/", $p2, "/git/commits/", $p3), - concat!("/repos/", $p1, "/", $p2, "/git/refs"), - concat!("/repos/", $p1, "/", $p2, "/git/tags/", $p3), - concat!("/repos/", $p1, "/", $p2, "/git/trees/", $p3), - concat!("/issues"), - concat!("/user/issues"), - concat!("/orgs/", $p1, "/issues"), - concat!("/repos/", $p1, "/", $p2, "/issues"), - concat!("/repos/", $p1, "/", $p2, "/issues/", $p3), - concat!("/repos/", $p1, "/", $p2, "/assignees"), - concat!("/repos/", $p1, "/", $p2, "/assignees/", $p3), - concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/comments"), - concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/events"), - concat!("/repos/", $p1, "/", $p2, "/labels"), - concat!("/repos/", $p1, "/", $p2, "/labels/", $p3), - concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/labels"), - concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3, "/labels"), - concat!("/repos/", $p1, "/", $p2, "/milestones/"), - concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3), - concat!("/emojis"), - concat!("/gitignore/templates"), - concat!("/gitignore/templates/", $p1), - concat!("/meta"), - concat!("/rate_limit"), - concat!("/users/", $p1, "/orgs"), - concat!("/user/orgs"), - concat!("/orgs/", $p1), - concat!("/orgs/", $p1, "/members"), - concat!("/orgs/", $p1, "/members", $p2), - concat!("/orgs/", $p1, "/public_members"), - concat!("/orgs/", $p1, "/public_members/", $p2), - concat!("/orgs/", $p1, "/teams"), - concat!("/teams/", $p1), - concat!("/teams/", $p1, "/members"), - concat!("/teams/", $p1, "/members", $p2), - concat!("/teams/", $p1, "/repos"), - concat!("/teams/", $p1, "/repos/", $p2, "/", $p3), - concat!("/user/teams"), - concat!("/repos/", $p1, "/", $p2, "/pulls"), - concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3), - concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/commits"), - concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/files"), - concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/merge"), - concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/comments"), - concat!("/user/repos"), - concat!("/users/", $p1, "/repos"), - concat!("/orgs/", $p1, "/repos"), - concat!("/repositories"), - concat!("/repos/", $p1, "/", $p2), - concat!("/repos/", $p1, "/", $p2, "/contributors"), - concat!("/repos/", $p1, "/", $p2, "/languages"), - concat!("/repos/", $p1, "/", $p2, "/teams"), - concat!("/repos/", $p1, "/", $p2, "/tags"), - concat!("/repos/", $p1, "/", $p2, "/branches"), - concat!("/repos/", $p1, "/", $p2, "/branches/", $p3), - concat!("/repos/", $p1, "/", $p2, "/collaborators"), - concat!("/repos/", $p1, "/", $p2, "/collaborators/", $p3), - concat!("/repos/", $p1, "/", $p2, "/comments"), - concat!("/repos/", $p1, "/", $p2, "/commits/", $p3, "/comments"), - concat!("/repos/", $p1, "/", $p2, "/commits"), - concat!("/repos/", $p1, "/", $p2, "/commits/", $p3), - concat!("/repos/", $p1, "/", $p2, "/readme"), - concat!("/repos/", $p1, "/", $p2, "/keys"), - concat!("/repos/", $p1, "/", $p2, "/keys", $p3), - concat!("/repos/", $p1, "/", $p2, "/downloads"), - concat!("/repos/", $p1, "/", $p2, "/downloads", $p3), - concat!("/repos/", $p1, "/", $p2, "/forks"), - concat!("/repos/", $p1, "/", $p2, "/hooks"), - concat!("/repos/", $p1, "/", $p2, "/hooks", $p3), - concat!("/repos/", $p1, "/", $p2, "/releases"), - concat!("/repos/", $p1, "/", $p2, "/releases/", $p3), - concat!("/repos/", $p1, "/", $p2, "/releases/", $p3, "/assets"), - concat!("/repos/", $p1, "/", $p2, "/stats/contributors"), - concat!("/repos/", $p1, "/", $p2, "/stats/commit_activity"), - concat!("/repos/", $p1, "/", $p2, "/stats/code_frequency"), - concat!("/repos/", $p1, "/", $p2, "/stats/participation"), - concat!("/repos/", $p1, "/", $p2, "/stats/punch_card"), - concat!("/repos/", $p1, "/", $p2, "/statuses/", $p3), - concat!("/search/repositories"), - concat!("/search/code"), - concat!("/search/issues"), - concat!("/search/users"), - concat!("/legacy/issues/search/", $p1, "/", $p2, "/", $p3, "/", $p4), - concat!("/legacy/repos/search/", $p1), - concat!("/legacy/user/search/", $p1), - concat!("/legacy/user/email/", $p1), - concat!("/users/", $p1), - concat!("/user"), - concat!("/users"), - concat!("/user/emails"), - concat!("/users/", $p1, "/followers"), - concat!("/user/followers"), - concat!("/users/", $p1, "/following"), - concat!("/user/following"), - concat!("/user/following/", $p1), - concat!("/users/", $p1, "/following", $p2), - concat!("/users/", $p1, "/keys"), - concat!("/user/keys"), - concat!("/user/keys/", $p1), - ]; - - arr.to_vec() - }}; -} - -static PATHS: [&str; 5] = [ - "/authorizations", - "/user/repos", - "/repos/rust-lang/rust/stargazers", - "/orgs/rust-lang/public_members/nikomatsakis", - "/repos/rust-lang/rust/releases/1.51.0", -]; - -fn main() { - let mut router = actix_router::Router::::build(); - - for route in register!(brackets) { - router.path(route, true); - } - - let actix = router.finish(); - - if firestorm::enabled() { - firestorm::bench("target", || { - for &route in &PATHS { - let mut path = actix_router::Path::new(route); - actix.recognize(&mut path).unwrap(); - } - }) - .unwrap(); - } -} diff --git a/actix-router/src/de.rs b/actix-router/src/de.rs index 27aa49ef2..55fcdc912 100644 --- a/actix-router/src/de.rs +++ b/actix-router/src/de.rs @@ -7,7 +7,7 @@ use crate::path::{Path, PathIter}; use crate::{Quoter, ResourcePath}; thread_local! { - static FULL_QUOTER: Quoter = Quoter::new(b"+/%", b""); + static FULL_QUOTER: Quoter = Quoter::new(b"", b""); } macro_rules! unsupported_type { @@ -52,7 +52,7 @@ macro_rules! parse_value { V: Visitor<'de>, { let decoded = FULL_QUOTER - .with(|q| q.requote(self.value.as_bytes())) + .with(|q| q.requote_str_lossy(self.value)) .map(Cow::Owned) .unwrap_or(Cow::Borrowed(self.value)); @@ -332,7 +332,7 @@ impl<'de> Deserializer<'de> for Value<'de> { where V: Visitor<'de>, { - match FULL_QUOTER.with(|q| q.requote(self.value.as_bytes())) { + match FULL_QUOTER.with(|q| q.requote_str_lossy(self.value)) { Some(s) => visitor.visit_string(s), None => visitor.visit_borrowed_str(self.value), } @@ -342,7 +342,7 @@ impl<'de> Deserializer<'de> for Value<'de> { where V: Visitor<'de>, { - match FULL_QUOTER.with(|q| q.requote(self.value.as_bytes())) { + match FULL_QUOTER.with(|q| q.requote_str_lossy(self.value)) { Some(s) => visitor.visit_byte_buf(s.into()), None => visitor.visit_borrowed_bytes(self.value.as_bytes()), } diff --git a/actix-router/src/lib.rs b/actix-router/src/lib.rs index 22f294b9d..0febcf1ac 100644 --- a/actix-router/src/lib.rs +++ b/actix-router/src/lib.rs @@ -22,7 +22,7 @@ pub use self::pattern::{IntoPatterns, Patterns}; pub use self::quoter::Quoter; pub use self::resource::ResourceDef; pub use self::resource_path::{Resource, ResourcePath}; -pub use self::router::{ResourceInfo, Router, RouterBuilder}; +pub use self::router::{ResourceId, Router, RouterBuilder}; #[cfg(feature = "http")] pub use self::url::Url; diff --git a/actix-router/src/path.rs b/actix-router/src/path.rs index dfb645d72..5eef1c1e7 100644 --- a/actix-router/src/path.rs +++ b/actix-router/src/path.rs @@ -1,7 +1,6 @@ use std::borrow::Cow; use std::ops::{DerefMut, Index}; -use firestorm::profile_method; use serde::de; use crate::{de::PathDeserializer, Resource, ResourcePath}; @@ -52,7 +51,6 @@ impl Path { /// Returns full path as a string. #[inline] pub fn as_str(&self) -> &str { - profile_method!(as_str); self.path.path() } @@ -61,7 +59,6 @@ impl Path { /// Returns empty string if no more is to be processed. #[inline] pub fn unprocessed(&self) -> &str { - profile_method!(unprocessed); // clamp skip to path length let skip = (self.skip as usize).min(self.as_str().len()); &self.path.path()[skip..] @@ -72,8 +69,6 @@ impl Path { #[deprecated(since = "0.6.0", note = "Use `.as_str()` or `.unprocessed()`.")] #[inline] pub fn path(&self) -> &str { - profile_method!(path); - let skip = self.skip as usize; let path = self.path.path(); if skip <= path.len() { @@ -86,8 +81,6 @@ impl Path { /// Set new path. #[inline] pub fn set(&mut self, path: T) { - profile_method!(set); - self.skip = 0; self.path = path; self.segments.clear(); @@ -96,8 +89,6 @@ impl Path { /// Reset state. #[inline] pub fn reset(&mut self) { - profile_method!(reset); - self.skip = 0; self.segments.clear(); } @@ -105,13 +96,10 @@ impl Path { /// Skip first `n` chars in path. #[inline] pub fn skip(&mut self, n: u16) { - profile_method!(skip); self.skip += n; } pub(crate) fn add(&mut self, name: impl Into>, value: PathItem) { - profile_method!(add); - match value { PathItem::Static(s) => self.segments.push((name.into(), PathItem::Static(s))), PathItem::Segment(begin, end) => self.segments.push(( @@ -127,8 +115,6 @@ impl Path { name: impl Into>, value: impl Into>, ) { - profile_method!(add_static); - self.segments .push((name.into(), PathItem::Static(value.into()))); } @@ -147,8 +133,6 @@ impl Path { /// Get matched parameter by name without type conversion pub fn get(&self, name: &str) -> Option<&str> { - profile_method!(get); - for (seg_name, val) in self.segments.iter() { if name == seg_name { return match val { @@ -167,8 +151,6 @@ impl Path { /// /// If keyed parameter is not available empty string is used as default value. pub fn query(&self, key: &str) -> &str { - profile_method!(query); - if let Some(s) = self.get(key) { s } else { @@ -186,7 +168,6 @@ impl Path { /// Try to deserialize matching parameters to a specified type `U` pub fn load<'de, U: serde::Deserialize<'de>>(&'de self) -> Result { - profile_method!(load); de::Deserialize::deserialize(PathDeserializer::new(self)) } } diff --git a/actix-router/src/quoter.rs b/actix-router/src/quoter.rs index 26ecc92cd..6c929d3ac 100644 --- a/actix-router/src/quoter.rs +++ b/actix-router/src/quoter.rs @@ -1,145 +1,94 @@ -#[allow(dead_code)] -const GEN_DELIMS: &[u8] = b":/?#[]@"; - -#[allow(dead_code)] -const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,"; - -#[allow(dead_code)] -const SUB_DELIMS: &[u8] = b"!$'()*,+?=;"; - -#[allow(dead_code)] -const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;"; - -#[allow(dead_code)] -const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz - ABCDEFGHIJKLMNOPQRSTUVWXYZ - 1234567890 - -._~"; - -const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz - ABCDEFGHIJKLMNOPQRSTUVWXYZ - 1234567890 - -._~ - !$'()*,"; - -const QS: &[u8] = b"+&=;b"; - -/// A quoter +/// Partial percent-decoding. +/// +/// Performs percent-decoding on a slice but can selectively skip decoding certain sequences. +/// +/// # Examples +/// ``` +/// # use actix_router::Quoter; +/// // + is set as a protected character and will not be decoded... +/// let q = Quoter::new(&[], b"+"); +/// +/// // ...but the other encoded characters (like the hyphen below) will. +/// assert_eq!(q.requote(b"/a%2Db%2Bc").unwrap(), b"/a-b%2Bc"); +/// ``` pub struct Quoter { - /// Simple bit-map of safe values in the 0-127 ASCII range. - safe_table: [u8; 16], - /// Simple bit-map of protected values in the 0-127 ASCII range. - protected_table: [u8; 16], + protected_table: AsciiBitmap, } impl Quoter { - pub fn new(safe: &[u8], protected: &[u8]) -> Quoter { - let mut quoter = Quoter { - safe_table: [0; 16], - protected_table: [0; 16], - }; - - // prepare safe table - for ch in 0..128 { - if ALLOWED.contains(&ch) { - set_bit(&mut quoter.safe_table, ch); - } - - if QS.contains(&ch) { - set_bit(&mut quoter.safe_table, ch); - } - } - - for &ch in safe { - set_bit(&mut quoter.safe_table, ch) - } + /// Constructs a new `Quoter` instance given a set of protected ASCII bytes. + /// + /// The first argument is ignored but is kept for backward compatibility. + /// + /// # Panics + /// Panics if any of the `protected` bytes are not in the 0-127 ASCII range. + pub fn new(_: &[u8], protected: &[u8]) -> Quoter { + let mut protected_table = AsciiBitmap::default(); // prepare protected table for &ch in protected { - set_bit(&mut quoter.safe_table, ch); - set_bit(&mut quoter.protected_table, ch); + protected_table.set_bit(ch); } - quoter + Quoter { protected_table } } - /// Re-quotes... ? - /// - /// Returns `None` when no modification to the original string was required. - pub fn requote(&self, val: &[u8]) -> Option { - let mut has_pct = 0; - let mut pct = [b'%', 0, 0]; - let mut idx = 0; - let mut cloned: Option> = None; - - let len = val.len(); - - while idx < len { - let ch = val[idx]; - - if has_pct != 0 { - pct[has_pct] = val[idx]; - has_pct += 1; - - if has_pct == 3 { - has_pct = 0; - let buf = cloned.as_mut().unwrap(); - - if let Some(ch) = hex_pair_to_char(pct[1], pct[2]) { - if ch < 128 { - if bit_at(&self.protected_table, ch) { - buf.extend_from_slice(&pct); - idx += 1; - continue; - } - - if bit_at(&self.safe_table, ch) { - buf.push(ch); - idx += 1; - continue; - } - } - - buf.push(ch); - } else { - buf.extend_from_slice(&pct[..]); - } + /// Decodes the next escape sequence, if any, and advances `val`. + #[inline(always)] + fn decode_next<'a>(&self, val: &mut &'a [u8]) -> Option<(&'a [u8], u8)> { + for i in 0..val.len() { + if let (prev, [b'%', p1, p2, rem @ ..]) = val.split_at(i) { + if let Some(ch) = hex_pair_to_char(*p1, *p2) + // ignore protected ascii bytes + .filter(|&ch| !(ch < 128 && self.protected_table.bit_at(ch))) + { + *val = rem; + return Some((prev, ch)); } - } else if ch == b'%' { - has_pct = 1; - - if cloned.is_none() { - let mut c = Vec::with_capacity(len); - c.extend_from_slice(&val[..idx]); - cloned = Some(c); - } - } else if let Some(ref mut cloned) = cloned { - cloned.push(ch) } - - idx += 1; } - cloned.map(|data| String::from_utf8_lossy(&data).into_owned()) + None } -} -/// Converts an ASCII character in the hex-encoded set (`0-9`, `A-F`, `a-f`) to its integer -/// representation from `0x0`–`0xF`. -/// -/// - `0x30 ('0') => 0x0` -/// - `0x39 ('9') => 0x9` -/// - `0x41 ('a') => 0xA` -/// - `0x61 ('A') => 0xA` -/// - `0x46 ('f') => 0xF` -/// - `0x66 ('F') => 0xF` -fn from_ascii_hex(v: u8) -> Option { - match v { - b'0'..=b'9' => Some(v - 0x30), // ord('0') == 0x30 - b'A'..=b'F' => Some(v - 0x41 + 10), // ord('A') == 0x41 - b'a'..=b'f' => Some(v - 0x61 + 10), // ord('a') == 0x61 - _ => None, + /// Partially percent-decodes the given bytes. + /// + /// Escape sequences of the protected set are *not* decoded. + /// + /// Returns `None` when no modification to the original bytes was required. + /// + /// Invalid/incomplete percent-encoding sequences are passed unmodified. + pub fn requote(&self, val: &[u8]) -> Option> { + let mut remaining = val; + + // early return indicates that no percent-encoded sequences exist and we can skip allocation + let (pre, decoded_char) = self.decode_next(&mut remaining)?; + + // decoded output will always be shorter than the input + let mut decoded = Vec::::with_capacity(val.len()); + + // push first segment and decoded char + decoded.extend_from_slice(pre); + decoded.push(decoded_char); + + // decode and push rest of segments and decoded chars + while let Some((prev, ch)) = self.decode_next(&mut remaining) { + // this ugly conditional achieves +50% perf in cases where this is a tight loop. + if !prev.is_empty() { + decoded.extend_from_slice(prev); + } + decoded.push(ch); + } + + decoded.extend_from_slice(remaining); + + Some(decoded) + } + + pub(crate) fn requote_str_lossy(&self, val: &str) -> Option { + self.requote(val.as_bytes()) + .map(|data| String::from_utf8_lossy(&data).into_owned()) } } @@ -150,27 +99,36 @@ fn from_ascii_hex(v: u8) -> Option { /// - `0x33 ('3'), 0x30 ('0') => 0x30 ('0')` /// - `0x34 ('4'), 0x31 ('1') => 0x41 ('A')` /// - `0x36 ('6'), 0x31 ('1') => 0x61 ('a')` +#[inline(always)] fn hex_pair_to_char(d1: u8, d2: u8) -> Option { - let (d_high, d_low) = (from_ascii_hex(d1)?, from_ascii_hex(d2)?); + let d_high = char::from(d1).to_digit(16)?; + let d_low = char::from(d2).to_digit(16)?; // left shift high nibble by 4 bits - Some(d_high << 4 | d_low) + Some((d_high as u8) << 4 | (d_low as u8)) } -/// Sets bit in given bit-map to 1=true. -/// -/// # Panics -/// Panics if `ch` index is out of bounds. -fn set_bit(array: &mut [u8], ch: u8) { - array[(ch >> 3) as usize] |= 0b1 << (ch & 0b111) +#[derive(Debug, Default, Clone)] +struct AsciiBitmap { + array: [u8; 16], } -/// Returns true if bit to true in given bit-map. -/// -/// # Panics -/// Panics if `ch` index is out of bounds. -fn bit_at(array: &[u8], ch: u8) -> bool { - array[(ch >> 3) as usize] & (0b1 << (ch & 0b111)) != 0 +impl AsciiBitmap { + /// Sets bit in given bit-map to 1=true. + /// + /// # Panics + /// Panics if `ch` index is out of bounds. + fn set_bit(&mut self, ch: u8) { + self.array[(ch >> 3) as usize] |= 0b1 << (ch & 0b111) + } + + /// Returns true if bit to true in given bit-map. + /// + /// # Panics + /// Panics if `ch` index is out of bounds. + fn bit_at(&self, ch: u8) -> bool { + self.array[(ch >> 3) as usize] & (0b1 << (ch & 0b111)) != 0 + } } #[cfg(test)] @@ -178,37 +136,32 @@ mod tests { use super::*; #[test] - fn hex_encoding() { - let hex = b"0123456789abcdefABCDEF"; + fn custom_quoter() { + let q = Quoter::new(b"", b"+"); + assert_eq!(q.requote(b"/a%25c").unwrap(), b"/a%c"); + assert_eq!(q.requote(b"/a%2Bc"), None); - for i in 0..256 { - let c = i as u8; - if hex.contains(&c) { - assert!(from_ascii_hex(c).is_some()) - } else { - assert!(from_ascii_hex(c).is_none()) - } - } - - let expected = [ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 10, 11, 12, 13, 14, 15, - ]; - for i in 0..hex.len() { - assert_eq!(from_ascii_hex(hex[i]).unwrap(), expected[i]); - } + let q = Quoter::new(b"%+", b"/"); + assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), b"/a%b+c"); + assert_eq!(q.requote(b"/a%2fb"), None); + assert_eq!(q.requote(b"/a%2Fb"), None); + assert_eq!(q.requote(b"/a%0Ab").unwrap(), b"/a\nb"); + assert_eq!(q.requote(b"/a%FE\xffb").unwrap(), b"/a\xfe\xffb"); + assert_eq!(q.requote(b"/a\xfe\xffb"), None); } #[test] - fn custom_quoter() { - let q = Quoter::new(b"", b"+"); - assert_eq!(q.requote(b"/a%25c").unwrap(), "/a%c"); - assert_eq!(q.requote(b"/a%2Bc").unwrap(), "/a%2Bc"); - + fn non_ascii() { let q = Quoter::new(b"%+", b"/"); - assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), "/a%b+c"); - assert_eq!(q.requote(b"/a%2fb").unwrap(), "/a%2fb"); - assert_eq!(q.requote(b"/a%2Fb").unwrap(), "/a%2Fb"); - assert_eq!(q.requote(b"/a%0Ab").unwrap(), "/a\nb"); + assert_eq!(q.requote(b"/a%FE\xffb").unwrap(), b"/a\xfe\xffb"); + assert_eq!(q.requote(b"/a\xfe\xffb"), None); + } + + #[test] + fn invalid_sequences() { + let q = Quoter::new(b"%+", b"/"); + assert_eq!(q.requote(b"/a%2x%2X%%"), None); + assert_eq!(q.requote(b"/a%20%2X%%").unwrap(), b"/a %2X%%"); } #[test] diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index c0b5522af..3c6754aeb 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -5,13 +5,10 @@ use std::{ mem, }; -use firestorm::{profile_fn, profile_method, profile_section}; use regex::{escape, Regex, RegexSet}; +use tracing::error; -use crate::{ - path::{Path, PathItem}, - IntoPatterns, Patterns, Resource, ResourcePath, -}; +use crate::{path::PathItem, IntoPatterns, Patterns, Resource, ResourcePath}; const MAX_DYNAMIC_SEGMENTS: usize = 16; @@ -274,7 +271,6 @@ impl ResourceDef { /// assert!(!resource.is_match("/foo")); /// ``` pub fn new(paths: T) -> Self { - profile_method!(new); Self::construct(paths, false) } @@ -302,7 +298,6 @@ impl ResourceDef { /// assert!(!resource.is_match("/foo")); /// ``` pub fn prefix(paths: T) -> Self { - profile_method!(prefix); ResourceDef::construct(paths, true) } @@ -327,7 +322,6 @@ impl ResourceDef { /// assert!(!resource.is_match("user/123")); /// ``` pub fn root_prefix(path: &str) -> Self { - profile_method!(root_prefix); ResourceDef::prefix(insert_slash(path).into_owned()) } @@ -551,8 +545,6 @@ impl ResourceDef { /// ``` #[inline] pub fn is_match(&self, path: &str) -> bool { - profile_method!(is_match); - // this function could be expressed as: // `self.find_match(path).is_some()` // but this skips some checks and uses potentially faster regex methods @@ -600,8 +592,6 @@ impl ResourceDef { /// assert_eq!(resource.find_match("/profile/1234"), Some(13)); /// ``` pub fn find_match(&self, path: &str) -> Option { - profile_method!(find_match); - match &self.pat_type { PatternType::Static(pattern) => self.static_match(pattern, path), @@ -615,7 +605,7 @@ impl ResourceDef { } } - /// Collects dynamic segment values into `path`. + /// Collects dynamic segment values into `resource`. /// /// Returns `true` if `path` matches this resource. /// @@ -635,9 +625,8 @@ impl ResourceDef { /// assert_eq!(path.get("path").unwrap(), "HEAD/Cargo.toml"); /// assert_eq!(path.unprocessed(), ""); /// ``` - pub fn capture_match_info(&self, path: &mut Path) -> bool { - profile_method!(capture_match_info); - self.capture_match_info_fn(path, |_, _| true, ()) + pub fn capture_match_info(&self, resource: &mut R) -> bool { + self.capture_match_info_fn(resource, |_| true) } /// Collects dynamic segment values into `resource` after matching paths and executing @@ -655,13 +644,12 @@ impl ResourceDef { /// use actix_router::{Path, ResourceDef}; /// /// fn try_match(resource: &ResourceDef, path: &mut Path<&str>) -> bool { - /// let admin_allowed = std::env::var("ADMIN_ALLOWED").ok(); + /// let admin_allowed = std::env::var("ADMIN_ALLOWED").is_ok(); /// /// resource.capture_match_info_fn( /// path, /// // when env var is not set, reject when path contains "admin" - /// |res, admin_allowed| !res.path().contains("admin"), - /// &admin_allowed + /// |path| !(!admin_allowed && path.as_str().contains("admin")), /// ) /// } /// @@ -678,66 +666,40 @@ impl ResourceDef { /// assert!(!try_match(&resource, &mut path)); /// assert_eq!(path.unprocessed(), "/user/admin/stars"); /// ``` - pub fn capture_match_info_fn( - &self, - resource: &mut R, - check_fn: F, - user_data: U, - ) -> bool + pub fn capture_match_info_fn(&self, resource: &mut R, check_fn: F) -> bool where R: Resource, - F: FnOnce(&R, U) -> bool, + F: FnOnce(&R) -> bool, { - profile_method!(capture_match_info_fn); - let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default(); let path = resource.resource_path(); let path_str = path.unprocessed(); let (matched_len, matched_vars) = match &self.pat_type { - PatternType::Static(pattern) => { - profile_section!(pattern_static_or_prefix); - - match self.static_match(pattern, path_str) { - Some(len) => (len, None), - None => return false, - } - } + PatternType::Static(pattern) => match self.static_match(pattern, path_str) { + Some(len) => (len, None), + None => return false, + }, PatternType::Dynamic(re, names) => { - profile_section!(pattern_dynamic); - - let captures = { - profile_section!(pattern_dynamic_regex_exec); - - match re.captures(path.unprocessed()) { - Some(captures) => captures, - _ => return false, - } + let captures = match re.captures(path.unprocessed()) { + Some(captures) => captures, + _ => return false, }; - { - profile_section!(pattern_dynamic_extract_captures); - - for (no, name) in names.iter().enumerate() { - if let Some(m) = captures.name(name) { - segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); - } else { - log::error!( - "Dynamic path match but not all segments found: {}", - name - ); - return false; - } + for (no, name) in names.iter().enumerate() { + if let Some(m) = captures.name(name) { + segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); + } else { + error!("Dynamic path match but not all segments found: {}", name); + return false; } - }; + } (captures[1].len(), Some(names)) } PatternType::DynamicSet(re, params) => { - profile_section!(pattern_dynamic_set); - let path = path.unprocessed(); let (pattern, names) = match re.matches(path).into_iter().next() { Some(idx) => ¶ms[idx], @@ -753,7 +715,7 @@ impl ResourceDef { if let Some(m) = captures.name(name) { segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); } else { - log::error!("Dynamic path match but not all segments found: {}", name); + error!("Dynamic path match but not all segments found: {}", name); return false; } } @@ -762,7 +724,7 @@ impl ResourceDef { } }; - if !check_fn(resource, user_data) { + if !check_fn(resource) { return false; } @@ -820,7 +782,6 @@ impl ResourceDef { I: IntoIterator, I::Item: AsRef, { - profile_method!(resource_path_from_iter); let mut iter = values.into_iter(); self.build_resource_path(path, |_| iter.next()) } @@ -856,8 +817,7 @@ impl ResourceDef { V: AsRef, S: BuildHasher, { - profile_method!(resource_path_from_map); - self.build_resource_path(path, |name| values.get(name).map(AsRef::::as_ref)) + self.build_resource_path(path, |name| values.get(name)) } /// Returns true if `prefix` acts as a proper prefix (i.e., separated by a slash) in `path`. @@ -877,8 +837,6 @@ impl ResourceDef { } fn construct(paths: T, is_prefix: bool) -> Self { - profile_method!(construct); - let patterns = paths.patterns(); let (pat_type, segments) = match &patterns { Patterns::Single(pattern) => ResourceDef::parse(pattern, is_prefix, false), @@ -907,7 +865,7 @@ impl ResourceDef { } let pattern_re_set = RegexSet::new(re_set).unwrap(); - let segments = segments.unwrap_or_else(Vec::new); + let segments = segments.unwrap_or_default(); ( PatternType::DynamicSet(pattern_re_set, pattern_data), @@ -937,8 +895,6 @@ impl ResourceDef { /// # Panics /// Panics if given patterns does not contain a dynamic segment. fn parse_param(pattern: &str) -> (PatternSegment, String, &str, bool) { - profile_method!(parse_param); - const DEFAULT_PATTERN: &str = "[^/]+"; const DEFAULT_PATTERN_TAIL: &str = ".*"; @@ -1008,8 +964,6 @@ impl ResourceDef { is_prefix: bool, force_dynamic: bool, ) -> (PatternType, Vec) { - profile_method!(parse); - if !force_dynamic && pattern.find('{').is_none() && !pattern.ends_with('*') { // pattern is static return ( @@ -1047,7 +1001,7 @@ impl ResourceDef { // tail segments in prefixes have no defined semantics #[cfg(not(test))] - log::warn!( + tracing::warn!( "Prefix resources should not have tail segments. \ Use `ResourceDef::new` constructor. \ This may become a panic in the future." @@ -1062,7 +1016,7 @@ impl ResourceDef { // unnamed tail segment #[cfg(not(test))] - log::warn!( + tracing::warn!( "Tail segments must have names. \ Consider `.../{{tail}}*`. \ This may become a panic in the future." @@ -1142,8 +1096,6 @@ impl From for ResourceDef { } pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> { - profile_fn!(insert_slash); - if !path.is_empty() && !path.starts_with('/') { let mut new_path = String::with_capacity(path.len() + 1); new_path.push('/'); @@ -1157,6 +1109,7 @@ pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> { #[cfg(test)] mod tests { use super::*; + use crate::Path; #[test] fn equivalence() { diff --git a/actix-router/src/router.rs b/actix-router/src/router.rs index 4652ef678..8ed966b59 100644 --- a/actix-router/src/router.rs +++ b/actix-router/src/router.rs @@ -1,91 +1,79 @@ -use firestorm::profile_method; - use crate::{IntoPatterns, Resource, ResourceDef}; #[derive(Debug, Copy, Clone, PartialEq)] pub struct ResourceId(pub u16); -/// Information about current resource -#[derive(Debug, Clone)] -pub struct ResourceInfo { - #[allow(dead_code)] - resource: ResourceId, -} - /// Resource router. -// T is the resource itself -// U is any other data needed for routing like method guards +/// +/// It matches a [routing resource](Resource) to an ordered list of _routes_. Each is defined by a +/// single [`ResourceDef`] and contains two types of custom data: +/// 1. The route _value_, of the generic type `T`. +/// 1. Some _context_ data, of the generic type `U`, which is only provided to the check function in +/// [`recognize_fn`](Self::recognize_fn). This parameter defaults to `()` and can be omitted if +/// not required. pub struct Router { - routes: Vec<(ResourceDef, T, Option)>, + routes: Vec<(ResourceDef, T, U)>, } impl Router { + /// Constructs new `RouterBuilder` with empty route list. pub fn build() -> RouterBuilder { - RouterBuilder { - resources: Vec::new(), - } + RouterBuilder { routes: Vec::new() } } + /// Finds the value in the router that matches a given [routing resource](Resource). + /// + /// The match result, including the captured dynamic segments, in the `resource`. pub fn recognize(&self, resource: &mut R) -> Option<(&T, ResourceId)> where R: Resource, { - profile_method!(recognize); - - for item in self.routes.iter() { - if item.0.capture_match_info(resource.resource_path()) { - return Some((&item.1, ResourceId(item.0.id()))); - } - } - - None + self.recognize_fn(resource, |_, _| true) } + /// Same as [`recognize`](Self::recognize) but returns a mutable reference to the matched value. pub fn recognize_mut(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)> where R: Resource, { - profile_method!(recognize_mut); - - for item in self.routes.iter_mut() { - if item.0.capture_match_info(resource.resource_path()) { - return Some((&mut item.1, ResourceId(item.0.id()))); - } - } - - None + self.recognize_mut_fn(resource, |_, _| true) } - pub fn recognize_fn(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)> + /// Finds the value in the router that matches a given [routing resource](Resource) and passes + /// an additional predicate check using context data. + /// + /// Similar to [`recognize`](Self::recognize). However, before accepting the route as matched, + /// the `check` closure is executed, passing the resource and each route's context data. If the + /// closure returns true then the match result is stored into `resource` and a reference to + /// the matched _value_ is returned. + pub fn recognize_fn(&self, resource: &mut R, mut check: F) -> Option<(&T, ResourceId)> where - F: Fn(&R, &Option) -> bool, R: Resource, + F: FnMut(&R, &U) -> bool, { - profile_method!(recognize_checked); - - for item in self.routes.iter() { - if item.0.capture_match_info_fn(resource, &check, &item.2) { - return Some((&item.1, ResourceId(item.0.id()))); + for (rdef, val, ctx) in self.routes.iter() { + if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) { + return Some((val, ResourceId(rdef.id()))); } } None } + /// Same as [`recognize_fn`](Self::recognize_fn) but returns a mutable reference to the matched + /// value. pub fn recognize_mut_fn( &mut self, resource: &mut R, - check: F, + mut check: F, ) -> Option<(&mut T, ResourceId)> where - F: Fn(&R, &Option) -> bool, R: Resource, + F: FnMut(&R, &U) -> bool, { - profile_method!(recognize_mut_checked); - - for item in self.routes.iter_mut() { - if item.0.capture_match_info_fn(resource, &check, &item.2) { - return Some((&mut item.1, ResourceId(item.0.id()))); + for (rdef, val, ctx) in self.routes.iter_mut() { + if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) { + return Some((val, ResourceId(rdef.id()))); } } @@ -93,49 +81,65 @@ impl Router { } } +/// Builder for an ordered [routing](Router) list. pub struct RouterBuilder { - resources: Vec<(ResourceDef, T, Option)>, + routes: Vec<(ResourceDef, T, U)>, } impl RouterBuilder { - /// Register resource for specified path. - pub fn path( + /// Adds a new route to the end of the routing list. + /// + /// Returns mutable references to elements of the new route. + pub fn push( &mut self, - path: P, - resource: T, - ) -> &mut (ResourceDef, T, Option) { - profile_method!(path); - - self.resources - .push((ResourceDef::new(path), resource, None)); - self.resources.last_mut().unwrap() - } - - /// Register resource for specified path prefix. - pub fn prefix(&mut self, prefix: &str, resource: T) -> &mut (ResourceDef, T, Option) { - profile_method!(prefix); - - self.resources - .push((ResourceDef::prefix(prefix), resource, None)); - self.resources.last_mut().unwrap() - } - - /// Register resource for ResourceDef - pub fn rdef(&mut self, rdef: ResourceDef, resource: T) -> &mut (ResourceDef, T, Option) { - profile_method!(rdef); - - self.resources.push((rdef, resource, None)); - self.resources.last_mut().unwrap() + rdef: ResourceDef, + val: T, + ctx: U, + ) -> (&mut ResourceDef, &mut T, &mut U) { + self.routes.push((rdef, val, ctx)); + self.routes + .last_mut() + .map(|(rdef, val, ctx)| (rdef, val, ctx)) + .unwrap() } /// Finish configuration and create router instance. pub fn finish(self) -> Router { Router { - routes: self.resources, + routes: self.routes, } } } +/// Convenience methods provided when context data impls [`Default`] +impl RouterBuilder +where + U: Default, +{ + /// Registers resource for specified path. + pub fn path( + &mut self, + path: impl IntoPatterns, + val: T, + ) -> (&mut ResourceDef, &mut T, &mut U) { + self.push(ResourceDef::new(path), val, U::default()) + } + + /// Registers resource for specified path prefix. + pub fn prefix( + &mut self, + prefix: impl IntoPatterns, + val: T, + ) -> (&mut ResourceDef, &mut T, &mut U) { + self.push(ResourceDef::prefix(prefix), val, U::default()) + } + + /// Registers resource for [`ResourceDef`]. + pub fn rdef(&mut self, rdef: ResourceDef, val: T) -> (&mut ResourceDef, &mut T, &mut U) { + self.push(rdef, val, U::default()) + } +} + #[cfg(test)] mod tests { use crate::path::Path; diff --git a/actix-router/src/url.rs b/actix-router/src/url.rs index f8d94ae4a..8ac033861 100644 --- a/actix-router/src/url.rs +++ b/actix-router/src/url.rs @@ -3,7 +3,7 @@ use crate::ResourcePath; use crate::Quoter; thread_local! { - static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+"); + static DEFAULT_QUOTER: Quoter = Quoter::new(b"", b"%/+"); } #[derive(Debug, Clone, Default)] @@ -15,14 +15,14 @@ pub struct Url { impl Url { #[inline] pub fn new(uri: http::Uri) -> Url { - let path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes())); + let path = DEFAULT_QUOTER.with(|q| q.requote_str_lossy(uri.path())); Url { uri, path } } #[inline] pub fn new_with_quoter(uri: http::Uri, quoter: &Quoter) -> Url { Url { - path: quoter.requote(uri.path().as_bytes()), + path: quoter.requote_str_lossy(uri.path()), uri, } } @@ -45,13 +45,13 @@ impl Url { #[inline] pub fn update(&mut self, uri: &http::Uri) { self.uri = uri.clone(); - self.path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes())); + self.path = DEFAULT_QUOTER.with(|q| q.requote_str_lossy(uri.path())); } #[inline] pub fn update_with_quoter(&mut self, uri: &http::Uri, quoter: &Quoter) { self.uri = uri.clone(); - self.path = quoter.requote(uri.path().as_bytes()); + self.path = quoter.requote_str_lossy(uri.path()); } } diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index 32ab2344f..43e306bb1 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,6 +1,17 @@ # Changes -## Unreleased - 2021-xx-xx +## Unreleased - 2022-xx-xx +- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. + + +## 0.1.0-beta.13 - 2022-02-16 +- No significant changes since `0.1.0-beta.12`. + + +## 0.1.0-beta.12 - 2022-01-31 +- Rename `TestServerConfig::{client_timeout => client_request_timeout}`. [#2611] + +[#2611]: https://github.com/actix/actix-web/pull/2611 ## 0.1.0-beta.11 - 2022-01-04 diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index e2c75b01e..9938be67d 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-test" -version = "0.1.0-beta.11" +version = "0.1.0-beta.13" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -28,14 +28,14 @@ rustls = ["tls-rustls", "actix-http/rustls", "awc/rustls"] openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] -actix-codec = "0.4.1" -actix-http = "3.0.0-beta.19" -actix-http-test = "3.0.0-beta.11" +actix-codec = "0.5" +actix-http = "3" +actix-http-test = "3.0.0-beta.13" actix-rt = "2.1" -actix-service = "2.0.0" -actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.21", default-features = false, features = ["cookies"] } -awc = { version = "3.0.0-beta.19", default-features = false, features = ["cookies"] } +actix-service = "2" +actix-utils = "3" +actix-web = { version = "4", default-features = false, features = ["cookies"] } +awc = { version = "3", default-features = false, features = ["cookies"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false, features = [] } diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs index f86120f2f..5efd9758e 100644 --- a/actix-test/src/lib.rs +++ b/actix-test/src/lib.rs @@ -43,7 +43,7 @@ pub use actix_http_test::unused_addr; use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _}; pub use actix_web::test::{ call_and_read_body, call_and_read_body_json, call_service, init_service, ok_service, - read_body, read_body_json, simple_service, TestRequest, + read_body, read_body_json, status_service, TestRequest, }; use actix_web::{ body::MessageBody, @@ -149,7 +149,7 @@ where let local_addr = tcp.local_addr().unwrap(); let factory = factory.clone(); let srv_cfg = cfg.clone(); - let timeout = cfg.client_timeout; + let timeout = cfg.client_request_timeout; let builder = Server::build().workers(1).disable_signals().system_exit(); @@ -167,7 +167,7 @@ where .map_err(|err| err.into().error_response()); HttpService::build() - .client_timeout(timeout) + .client_request_timeout(timeout) .h1(map_config(fac, move |_| app_cfg.clone())) .tcp() }), @@ -183,7 +183,7 @@ where .map_err(|err| err.into().error_response()); HttpService::build() - .client_timeout(timeout) + .client_request_timeout(timeout) .h2(map_config(fac, move |_| app_cfg.clone())) .tcp() }), @@ -199,7 +199,7 @@ where .map_err(|err| err.into().error_response()); HttpService::build() - .client_timeout(timeout) + .client_request_timeout(timeout) .finish(map_config(fac, move |_| app_cfg.clone())) .tcp() }), @@ -218,7 +218,7 @@ where .map_err(|err| err.into().error_response()); HttpService::build() - .client_timeout(timeout) + .client_request_timeout(timeout) .h1(map_config(fac, move |_| app_cfg.clone())) .openssl(acceptor.clone()) }), @@ -234,7 +234,7 @@ where .map_err(|err| err.into().error_response()); HttpService::build() - .client_timeout(timeout) + .client_request_timeout(timeout) .h2(map_config(fac, move |_| app_cfg.clone())) .openssl(acceptor.clone()) }), @@ -250,7 +250,7 @@ where .map_err(|err| err.into().error_response()); HttpService::build() - .client_timeout(timeout) + .client_request_timeout(timeout) .finish(map_config(fac, move |_| app_cfg.clone())) .openssl(acceptor.clone()) }), @@ -269,7 +269,7 @@ where .map_err(|err| err.into().error_response()); HttpService::build() - .client_timeout(timeout) + .client_request_timeout(timeout) .h1(map_config(fac, move |_| app_cfg.clone())) .rustls(config.clone()) }), @@ -285,7 +285,7 @@ where .map_err(|err| err.into().error_response()); HttpService::build() - .client_timeout(timeout) + .client_request_timeout(timeout) .h2(map_config(fac, move |_| app_cfg.clone())) .rustls(config.clone()) }), @@ -301,7 +301,7 @@ where .map_err(|err| err.into().error_response()); HttpService::build() - .client_timeout(timeout) + .client_request_timeout(timeout) .finish(map_config(fac, move |_| app_cfg.clone())) .rustls(config.clone()) }), @@ -388,7 +388,7 @@ pub fn config() -> TestServerConfig { pub struct TestServerConfig { tp: HttpVer, stream: StreamType, - client_timeout: u64, + client_request_timeout: Duration, } impl Default for TestServerConfig { @@ -403,7 +403,7 @@ impl TestServerConfig { TestServerConfig { tp: HttpVer::Both, stream: StreamType::Tcp, - client_timeout: 5000, + client_request_timeout: Duration::from_secs(5), } } @@ -433,9 +433,9 @@ impl TestServerConfig { self } - /// Set client timeout in milliseconds for first request. - pub fn client_timeout(mut self, val: u64) -> Self { - self.client_timeout = val; + /// Set client timeout for first request. + pub fn client_request_timeout(mut self, dur: Duration) -> Self { + self.client_request_timeout = dur; self } } diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 74ab3c785..33d4712f8 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,6 +1,25 @@ # Changes -## Unreleased - 2021-xx-xx +## Unreleased - 2022-xx-xx +- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. + + +## 4.1.0 - 2022-03-02 +- Add support for `actix` version `0.13`. [#2675] + +[#2675]: https://github.com/actix/actix-web/pull/2675 + + +## 4.0.0 - 2022-02-25 +- No significant changes since `4.0.0-beta.12`. + + +## 4.0.0-beta.12 - 2022-02-16 +- No significant changes since `4.0.0-beta.11`. + + +## 4.0.0-beta.11 - 2022-01-31 +- No significant changes since `4.0.0-beta.10`. ## 4.0.0-beta.10 - 2022-01-04 diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 3c28c0a15..284351ed3 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.10" +version = "4.1.0" authors = ["Nikolay Kim "] description = "Actix actors support for Actix Web" keywords = ["actix", "http", "web", "framework", "async"] @@ -14,21 +14,24 @@ name = "actix_web_actors" path = "src/lib.rs" [dependencies] -actix = { version = "0.12.0", default-features = false } -actix-codec = "0.4.1" -actix-http = "3.0.0-beta.19" -actix-web = { version = "4.0.0-beta.21", default-features = false } +actix = { version = ">=0.12, <0.14", default-features = false } +actix-codec = "0.5" +actix-http = "3" +actix-web = { version = "4", default-features = false } bytes = "1" bytestring = "1" futures-core = { version = "0.3.7", default-features = false } pin-project-lite = "0.2" -tokio = { version = "1.8.4", features = ["sync"] } +tokio = { version = "1.13.1", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.11" -awc = { version = "3.0.0-beta.19", default-features = false } +actix-test = "0.1.0-beta.13" +awc = { version = "3", default-features = false } +actix-web = { version = "4", features = ["macros"] } + +mime = "0.3" 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 60e6a9bd9..8d64c0851 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.10)](https://docs.rs/actix-web-actors/4.0.0-beta.10) -[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) +[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.1.0)](https://docs.rs/actix-web-actors/4.1.0) +![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
-[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.10) +[![dependency status](https://deps.rs/crate/actix-web-actors/4.1.0/status.svg)](https://deps.rs/crate/actix-web-actors/4.1.0) [![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-actors/src/context.rs b/actix-web-actors/src/context.rs index d83969ff7..f7b11c780 100644 --- a/actix-web-actors/src/context.rs +++ b/actix-web-actors/src/context.rs @@ -14,6 +14,58 @@ use futures_core::Stream; use tokio::sync::oneshot::Sender; /// Execution context for HTTP actors +/// +/// # Example +/// +/// A demonstration of [server-sent events](https://developer.mozilla.org/docs/Web/API/Server-sent_events) using actors: +/// +/// ```no_run +/// use std::time::Duration; +/// +/// use actix::{Actor, AsyncContext}; +/// use actix_web::{get, http::header, App, HttpResponse, HttpServer}; +/// use actix_web_actors::HttpContext; +/// use bytes::Bytes; +/// +/// struct MyActor { +/// count: usize, +/// } +/// +/// impl Actor for MyActor { +/// type Context = HttpContext; +/// +/// fn started(&mut self, ctx: &mut Self::Context) { +/// ctx.run_later(Duration::from_millis(100), Self::write); +/// } +/// } +/// +/// impl MyActor { +/// fn write(&mut self, ctx: &mut HttpContext) { +/// self.count += 1; +/// if self.count > 3 { +/// ctx.write_eof() +/// } else { +/// ctx.write(Bytes::from(format!("event: count\ndata: {}\n\n", self.count))); +/// ctx.run_later(Duration::from_millis(100), Self::write); +/// } +/// } +/// } +/// +/// #[get("/")] +/// async fn index() -> HttpResponse { +/// HttpResponse::Ok() +/// .insert_header(header::ContentType(mime::TEXT_EVENT_STREAM)) +/// .streaming(HttpContext::create(MyActor { count: 0 })) +/// } +/// +/// #[actix_web::main] +/// async fn main() -> std::io::Result<()> { +/// HttpServer::new(|| App::new().service(index)) +/// .bind(("127.0.0.1", 8080))? +/// .run() +/// .await +/// } +/// ``` pub struct HttpContext where A: Actor>, @@ -210,7 +262,7 @@ mod tests { type Context = HttpContext; fn started(&mut self, ctx: &mut Self::Context) { - ctx.run_later(Duration::from_millis(100), |slf, ctx| slf.write(ctx)); + ctx.run_later(Duration::from_millis(100), Self::write); } } @@ -221,7 +273,7 @@ mod tests { ctx.write_eof() } else { ctx.write(Bytes::from(format!("LINE-{}", self.count))); - ctx.run_later(Duration::from_millis(100), |slf, ctx| slf.write(ctx)); + ctx.run_later(Duration::from_millis(100), Self::write); } } } diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs index 70c957020..106bc5202 100644 --- a/actix-web-actors/src/lib.rs +++ b/actix-web-actors/src/lib.rs @@ -1,4 +1,59 @@ //! Actix actors support for Actix Web. +//! +//! # Examples +//! +//! ```no_run +//! use actix::{Actor, StreamHandler}; +//! use actix_web::{get, web, App, Error, HttpRequest, HttpResponse, HttpServer}; +//! use actix_web_actors::ws; +//! +//! /// Define Websocket actor +//! struct MyWs; +//! +//! impl Actor for MyWs { +//! type Context = ws::WebsocketContext; +//! } +//! +//! /// Handler for ws::Message message +//! impl StreamHandler> for MyWs { +//! fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { +//! match msg { +//! Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), +//! Ok(ws::Message::Text(text)) => ctx.text(text), +//! Ok(ws::Message::Binary(bin)) => ctx.binary(bin), +//! _ => (), +//! } +//! } +//! } +//! +//! #[get("/ws")] +//! async fn index(req: HttpRequest, stream: web::Payload) -> Result { +//! ws::start(MyWs, &req, stream) +//! } +//! +//! #[actix_web::main] +//! async fn main() -> std::io::Result<()> { +//! HttpServer::new(|| App::new().service(index)) +//! .bind(("127.0.0.1", 8080))? +//! .run() +//! .await +//! } +//! ``` +//! +//! # Documentation & Community Resources +//! In addition to this API documentation, several other resources are available: +//! +//! * [Website & User Guide](https://actix.rs/) +//! * [Documentation for `actix_web`](actix_web) +//! * [Examples Repository](https://github.com/actix/examples) +//! * [Community Chat on Discord](https://discord.gg/NWpN5mmg3x) +//! +//! To get started navigating the API docs, you may consider looking at the following pages first: +//! +//! * [`ws`]: This module provides actor support for WebSockets. +//! +//! * [`HttpContext`]: This struct provides actor support for streaming HTTP responses. +//! #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible)] diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index 6fde10192..9a4880159 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -1,4 +1,60 @@ //! Websocket integration. +//! +//! # Examples +//! +//! ```no_run +//! use actix::{Actor, StreamHandler}; +//! use actix_web::{get, web, App, Error, HttpRequest, HttpResponse, HttpServer}; +//! use actix_web_actors::ws; +//! +//! /// Define Websocket actor +//! struct MyWs; +//! +//! impl Actor for MyWs { +//! type Context = ws::WebsocketContext; +//! } +//! +//! /// Handler for ws::Message message +//! impl StreamHandler> for MyWs { +//! fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { +//! match msg { +//! Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), +//! Ok(ws::Message::Text(text)) => ctx.text(text), +//! Ok(ws::Message::Binary(bin)) => ctx.binary(bin), +//! _ => (), +//! } +//! } +//! } +//! +//! #[get("/ws")] +//! async fn websocket(req: HttpRequest, stream: web::Payload) -> Result { +//! ws::start(MyWs, &req, stream) +//! } +//! +//! const MAX_FRAME_SIZE: usize = 16_384; // 16KiB +//! +//! #[get("/custom-ws")] +//! async fn custom_websocket(req: HttpRequest, stream: web::Payload) -> Result { +//! // Create a Websocket session with a specific max frame size, and protocols. +//! ws::WsResponseBuilder::new(MyWs, &req, stream) +//! .frame_size(MAX_FRAME_SIZE) +//! .protocols(&["A", "B"]) +//! .start() +//! } +//! +//! #[actix_web::main] +//! async fn main() -> std::io::Result<()> { +//! HttpServer::new(|| { +//! App::new() +//! .service(websocket) +//! .service(custom_websocket) +//! }) +//! .bind(("127.0.0.1", 8080))? +//! .run() +//! .await +//! } +//! ``` +//! use std::{ collections::VecDeque, @@ -41,20 +97,51 @@ use tokio::sync::oneshot; /// /// # Examples /// -/// Create a Websocket session response with default configuration. -/// ```ignore -/// WsResponseBuilder::new(WsActor, &req, stream).start() -/// ``` +/// ```no_run +/// # use actix::{Actor, StreamHandler}; +/// # use actix_web::{get, web, App, Error, HttpRequest, HttpResponse, HttpServer}; +/// # use actix_web_actors::ws; +/// # +/// # struct MyWs; +/// # +/// # impl Actor for MyWs { +/// # type Context = ws::WebsocketContext; +/// # } +/// # +/// # /// Handler for ws::Message message +/// # impl StreamHandler> for MyWs { +/// # fn handle(&mut self, msg: Result, ctx: &mut Self::Context) {} +/// # } +/// # +/// #[get("/ws")] +/// async fn websocket(req: HttpRequest, stream: web::Payload) -> Result { +/// ws::WsResponseBuilder::new(MyWs, &req, stream).start() +/// } /// -/// Create a Websocket session with a specific max frame size, [`Codec`], and protocols. -/// ```ignore /// const MAX_FRAME_SIZE: usize = 16_384; // 16KiB /// -/// ws::WsResponseBuilder::new(WsActor, &req, stream) -/// .codec(Codec::new()) -/// .protocols(&["A", "B"]) -/// .frame_size(MAX_FRAME_SIZE) -/// .start() +/// #[get("/custom-ws")] +/// async fn custom_websocket(req: HttpRequest, stream: web::Payload) -> Result { +/// // Create a Websocket session with a specific max frame size, codec, and protocols. +/// ws::WsResponseBuilder::new(MyWs, &req, stream) +/// .codec(actix_http::ws::Codec::new()) +/// // This will overwrite the codec's max frame-size +/// .frame_size(MAX_FRAME_SIZE) +/// .protocols(&["A", "B"]) +/// .start() +/// } +/// # +/// # #[actix_web::main] +/// # async fn main() -> std::io::Result<()> { +/// # HttpServer::new(|| { +/// # App::new() +/// # .service(websocket) +/// # .service(custom_websocket) +/// # }) +/// # .bind(("127.0.0.1", 8080))? +/// # .run() +/// # .await +/// # } /// ``` pub struct WsResponseBuilder<'a, A, T> where diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index c044ff74d..6b525a441 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,6 +1,30 @@ # Changes -## Unreleased - 2021-xx-xx +## Unreleased - 2022-xx-xx +- Add `#[routes]` macro to support multiple paths for one handler. [#2718] +- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. + +[#2718]: https://github.com/actix/actix-web/pull/2718 + + +## 4.0.1 - 2022-06-11 +- Fix support for guard paths in route handler macros. [#2771] +- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency. + +[#2771]: https://github.com/actix/actix-web/pull/2771 + + +## 4.0.0 - 2022-02-24 +- Version aligned with `actix-web` and will remain in sync going forward. +- No significant changes since `0.5.0`. + + +## 0.5.0 - 2022-02-24 +- No significant changes since `0.5.0-rc.2`. + + +## 0.5.0-rc.2 - 2022-02-01 +- No significant changes since `0.5.0-rc.1`. ## 0.5.0-rc.1 - 2022-01-04 diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 3ee2fbd02..a2aac7e68 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-codegen" -version = "0.5.0-rc.1" +version = "4.0.1" description = "Routing and runtime macros for Actix Web" homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" @@ -15,17 +15,17 @@ edition = "2018" proc-macro = true [dependencies] -actix-router = "0.5.0-beta.4" +actix-router = "0.5.0" proc-macro2 = "1" quote = "1" -syn = { version = "1", features = ["full", "parsing"] } +syn = { version = "1", features = ["full", "extra-traits"] } [dev-dependencies] actix-macros = "0.2.3" actix-rt = "2.2" -actix-test = "0.1.0-beta.11" +actix-test = "0.1.0-beta.13" actix-utils = "3.0.0" -actix-web = "4.0.0-beta.21" +actix-web = "4.0.0" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } trybuild = "1" diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index 1fd97184c..26f070f18 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -3,11 +3,11 @@ > Routing and runtime macros for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) -[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-rc.1)](https://docs.rs/actix-web-codegen/0.5.0-rc.1) -[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) +[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=4.0.1)](https://docs.rs/actix-web-codegen/4.0.1) +![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
-[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-rc.1/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-rc.1) +[![dependency status](https://deps.rs/crate/actix-web-codegen/4.0.1/status.svg)](https://deps.rs/crate/actix-web-codegen/4.0.1) [![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index f41e1ce38..4b6dc43c5 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -46,9 +46,20 @@ //! ``` //! //! # Multiple Path Handlers -//! There are no macros to generate multi-path handlers. Let us know in [this issue]. +//! Acts as a wrapper for multiple single method handler macros. It takes no arguments and +//! delegates those to the macros for the individual methods. See [macro@routes] macro docs. //! -//! [this issue]: https://github.com/actix/actix-web/issues/1709 +//! ``` +//! # use actix_web::HttpResponse; +//! # use actix_web_codegen::routes; +//! #[routes] +//! #[get("/test")] +//! #[get("/test2")] +//! #[delete("/test")] +//! async fn example() -> HttpResponse { +//! HttpResponse::Ok().finish() +//! } +//! ``` //! //! [actix-web attributes docs]: https://docs.rs/actix-web/latest/actix_web/#attributes //! [GET]: macro@get @@ -104,6 +115,39 @@ pub fn route(args: TokenStream, input: TokenStream) -> TokenStream { route::with_method(None, args, input) } +/// Creates resource handler, allowing multiple HTTP methods and paths. +/// +/// # Syntax +/// ```plain +/// #[routes] +/// #[("path", ...)] +/// #[("path", ...)] +/// ... +/// ``` +/// +/// # Attributes +/// The `routes` macro itself has no parameters, but allows specifying the attribute macros for +/// the multiple paths and/or methods, e.g. [`GET`](macro@get) and [`POST`](macro@post). +/// +/// These helper attributes take the same parameters as the [single method handlers](crate#single-method-handler). +/// +/// # Examples +/// ``` +/// # use actix_web::HttpResponse; +/// # use actix_web_codegen::routes; +/// #[routes] +/// #[get("/test")] +/// #[get("/test2")] +/// #[delete("/test")] +/// async fn example() -> HttpResponse { +/// HttpResponse::Ok().finish() +/// } +/// ``` +#[proc_macro_attribute] +pub fn routes(_: TokenStream, input: TokenStream) -> TokenStream { + route::with_methods(input) +} + macro_rules! method_macro { ($variant:ident, $method:ident) => { #[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")] @@ -152,6 +196,10 @@ method_macro!(Patch, patch); /// Marks async main function as the Actix Web system entry-point. /// +/// Note that Actix Web also works under `#[tokio::main]` since version 4.0. However, this macro is +/// still necessary for actor support (since actors use a `System`). Read more in the +/// [`actix_web::rt`](https://docs.rs/actix-web/4/actix_web/rt) module docs. +/// /// # Examples /// ``` /// #[actix_web::main] diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index cb1ba1ef6..7a0658468 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -3,24 +3,12 @@ use std::{collections::HashSet, convert::TryFrom}; use actix_router::ResourceDef; use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::{format_ident, quote, ToTokens, TokenStreamExt}; -use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, NestedMeta}; - -enum ResourceType { - Async, - Sync, -} - -impl ToTokens for ResourceType { - fn to_tokens(&self, stream: &mut TokenStream2) { - let ident = format_ident!("to"); - stream.append(ident); - } -} +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path}; macro_rules! method_type { ( - $($variant:ident, $upper:ident,)+ + $($variant:ident, $upper:ident, $lower:ident,)+ ) => { #[derive(Debug, PartialEq, Eq, Hash)] pub enum MethodType { @@ -42,20 +30,27 @@ macro_rules! method_type { _ => Err(format!("Unexpected HTTP method: `{}`", method)), } } + + fn from_path(method: &Path) -> Result { + match () { + $(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+ + _ => Err(()), + } + } } }; } method_type! { - Get, GET, - Post, POST, - Put, PUT, - Delete, DELETE, - Head, HEAD, - Connect, CONNECT, - Options, OPTIONS, - Trace, TRACE, - Patch, PATCH, + Get, GET, get, + Post, POST, post, + Put, PUT, put, + Delete, DELETE, delete, + Head, HEAD, head, + Connect, CONNECT, connect, + Options, OPTIONS, options, + Trace, TRACE, trace, + Patch, PATCH, patch, } impl ToTokens for MethodType { @@ -77,7 +72,7 @@ impl TryFrom<&syn::LitStr> for MethodType { struct Args { path: syn::LitStr, resource_name: Option, - guards: Vec, + guards: Vec, wrappers: Vec, methods: HashSet, } @@ -90,6 +85,18 @@ impl Args { let mut wrappers = Vec::new(); let mut methods = HashSet::new(); + if args.is_empty() { + return Err(syn::Error::new( + Span::call_site(), + format!( + r#"invalid service definition, expected #[{}("")]"#, + method + .map_or("route", |it| it.as_str()) + .to_ascii_lowercase() + ), + )); + } + let is_route_macro = method.is_none(); if let Some(method) = method { methods.insert(method); @@ -121,7 +128,7 @@ impl Args { } } else if nv.path.is_ident("guard") { if let syn::Lit::Str(lit) = nv.lit { - guards.push(Ident::new(&lit.value(), Span::call_site())); + guards.push(lit.parse::()?); } else { return Err(syn::Error::new_spanned( nv.lit, @@ -183,55 +190,27 @@ impl Args { } pub struct Route { + /// Name of the handler function being annotated. name: syn::Ident, - args: Args, + + /// Args passed to routing macro. + /// + /// When using `#[routes]`, this will contain args for each specific routing macro. + args: Vec, + + /// AST of the handler function being annotated. ast: syn::ItemFn, - resource_type: ResourceType, /// The doc comment attributes to copy to generated struct, if any. doc_attributes: Vec, } -fn guess_resource_type(typ: &syn::Type) -> ResourceType { - let mut guess = ResourceType::Sync; - - if let syn::Type::ImplTrait(typ) = typ { - for bound in typ.bounds.iter() { - if let syn::TypeParamBound::Trait(bound) = bound { - for bound in bound.path.segments.iter() { - if bound.ident == "Future" { - guess = ResourceType::Async; - break; - } else if bound.ident == "Responder" { - guess = ResourceType::Sync; - break; - } - } - } - } - } - - guess -} - impl Route { pub fn new( args: AttributeArgs, ast: syn::ItemFn, method: Option, ) -> syn::Result { - if args.is_empty() { - return Err(syn::Error::new( - Span::call_site(), - format!( - r#"invalid service definition, expected #[{}("")]"#, - method - .map_or("route", |it| it.as_str()) - .to_ascii_lowercase() - ), - )); - } - let name = ast.sig.ident.clone(); // Try and pull out the doc comments so that we can reapply them to the generated struct. @@ -244,6 +223,7 @@ impl Route { .collect(); let args = Args::new(args, method)?; + if args.methods.is_empty() { return Err(syn::Error::new( Span::call_site(), @@ -251,25 +231,44 @@ impl Route { )); } - let resource_type = if ast.sig.asyncness.is_some() { - ResourceType::Async - } else { - match ast.sig.output { - syn::ReturnType::Default => { - return Err(syn::Error::new_spanned( - ast, - "Function has no return type. Cannot be used as handler", - )); - } - syn::ReturnType::Type(_, ref typ) => guess_resource_type(typ.as_ref()), - } - }; + if matches!(ast.sig.output, syn::ReturnType::Default) { + return Err(syn::Error::new_spanned( + ast, + "Function has no return type. Cannot be used as handler", + )); + } + + Ok(Self { + name, + args: vec![args], + ast, + doc_attributes, + }) + } + + fn multiple(args: Vec, ast: syn::ItemFn) -> syn::Result { + let name = ast.sig.ident.clone(); + + // Try and pull out the doc comments so that we can reapply them to the generated struct. + // Note that multi line doc comments are converted to multiple doc attributes. + let doc_attributes = ast + .attrs + .iter() + .filter(|attr| attr.path.is_ident("doc")) + .cloned() + .collect(); + + if matches!(ast.sig.output, syn::ReturnType::Default) { + return Err(syn::Error::new_spanned( + ast, + "Function has no return type. Cannot be used as handler", + )); + } Ok(Self { name, args, ast, - resource_type, doc_attributes, }) } @@ -280,38 +279,57 @@ impl ToTokens for Route { let Self { name, ast, - args: - Args { + args, + doc_attributes, + } = self; + + let registrations: TokenStream2 = args + .iter() + .map(|args| { + let Args { path, resource_name, guards, wrappers, methods, - }, - resource_type, - doc_attributes, - } = self; - let resource_name = resource_name - .as_ref() - .map_or_else(|| name.to_string(), LitStr::value); - let method_guards = { - let mut others = methods.iter(); - // unwrapping since length is checked to be at least one - let first = others.next().unwrap(); + } = args; + + let resource_name = resource_name + .as_ref() + .map_or_else(|| name.to_string(), LitStr::value); + + let method_guards = { + let mut others = methods.iter(); + + // unwrapping since length is checked to be at least one + let first = others.next().unwrap(); + + if methods.len() > 1 { + quote! { + .guard( + ::actix_web::guard::Any(::actix_web::guard::#first()) + #(.or(::actix_web::guard::#others()))* + ) + } + } else { + quote! { + .guard(::actix_web::guard::#first()) + } + } + }; - if methods.len() > 1 { quote! { - .guard( - ::actix_web::guard::Any(::actix_web::guard::#first()) - #(.or(::actix_web::guard::#others()))* - ) + let __resource = ::actix_web::Resource::new(#path) + .name(#resource_name) + #method_guards + #(.guard(::actix_web::guard::fn_guard(#guards)))* + #(.wrap(#wrappers))* + .to(#name); + + ::actix_web::dev::HttpServiceFactory::register(__resource, __config); } - } else { - quote! { - .guard(::actix_web::guard::#first()) - } - } - }; + }) + .collect(); let stream = quote! { #(#doc_attributes)* @@ -321,14 +339,7 @@ impl ToTokens for Route { impl ::actix_web::dev::HttpServiceFactory for #name { fn register(self, __config: &mut actix_web::dev::AppService) { #ast - let __resource = ::actix_web::Resource::new(#path) - .name(#resource_name) - #method_guards - #(.guard(::actix_web::guard::fn_guard(#guards)))* - #(.wrap(#wrappers))* - .#resource_type(#name); - - ::actix_web::dev::HttpServiceFactory::register(__resource, __config) + #registrations } } }; @@ -357,6 +368,57 @@ pub(crate) fn with_method( } } +pub(crate) fn with_methods(input: TokenStream) -> TokenStream { + let mut ast = match syn::parse::(input.clone()) { + Ok(ast) => ast, + // on parse error, make IDEs happy; see fn docs + Err(err) => return input_and_compile_error(input, err), + }; + + let (methods, others) = ast + .attrs + .into_iter() + .map(|attr| match MethodType::from_path(&attr.path) { + Ok(method) => Ok((method, attr)), + Err(_) => Err(attr), + }) + .partition::, _>(Result::is_ok); + + ast.attrs = others.into_iter().map(Result::unwrap_err).collect(); + + let methods = + match methods + .into_iter() + .map(Result::unwrap) + .map(|(method, attr)| { + attr.parse_meta().and_then(|args| { + if let Meta::List(args) = args { + Args::new(args.nested.into_iter().collect(), Some(method)) + } else { + Err(syn::Error::new_spanned(attr, "Invalid input for macro")) + } + }) + }) + .collect::, _>>() + { + Ok(methods) if methods.is_empty() => return input_and_compile_error( + input, + syn::Error::new( + Span::call_site(), + "The #[routes] macro requires at least one `#[(..)]` attribute.", + ), + ), + Ok(methods) => methods, + Err(err) => return input_and_compile_error(input, err), + }; + + match Route::multiple(methods, ast) { + Ok(route) => route.into_token_stream().into(), + // on macro related error, make IDEs happy; see fn docs + Err(err) => input_and_compile_error(input, err), + } +} + /// Converts the error to a token stream and appends it to the original input. /// /// Returning the original input in addition to the error is good for IDEs which can gracefully diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 769cf2bc3..10e906967 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -8,9 +8,11 @@ use actix_web::{ header::{HeaderName, HeaderValue}, StatusCode, }, - web, App, Error, HttpResponse, Responder, + web, App, Error, HttpRequest, HttpResponse, Responder, +}; +use actix_web_codegen::{ + connect, delete, get, head, options, patch, post, put, route, routes, trace, }; -use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, route, trace}; use futures_core::future::LocalBoxFuture; // Make sure that we can name function as 'config' @@ -89,13 +91,61 @@ async fn route_test() -> impl Responder { HttpResponse::Ok() } +#[routes] +#[get("/routes/test")] +#[get("/routes/test2")] +#[post("/routes/test")] +async fn routes_test() -> impl Responder { + HttpResponse::Ok() +} + +// routes overlap with the more specific route first, therefore accessible +#[routes] +#[get("/routes/overlap/test")] +#[get("/routes/overlap/{foo}")] +async fn routes_overlapping_test(req: HttpRequest) -> impl Responder { + // foo is only populated when route is not /routes/overlap/test + match req.match_info().get("foo") { + None => assert!(req.uri() == "/routes/overlap/test"), + Some(_) => assert!(req.uri() != "/routes/overlap/test"), + } + + HttpResponse::Ok() +} + +// routes overlap with the more specific route last, therefore inaccessible +#[routes] +#[get("/routes/overlap2/{foo}")] +#[get("/routes/overlap2/test")] +async fn routes_overlapping_inaccessible_test(req: HttpRequest) -> impl Responder { + // foo is always populated even when path is /routes/overlap2/test + assert!(req.match_info().get("foo").is_some()); + + HttpResponse::Ok() +} + #[get("/custom_resource_name", name = "custom")] -async fn custom_resource_name_test<'a>(req: actix_web::HttpRequest) -> impl Responder { +async fn custom_resource_name_test<'a>(req: HttpRequest) -> impl Responder { assert!(req.url_for_static("custom").is_ok()); assert!(req.url_for_static("custom_resource_name_test").is_err()); HttpResponse::Ok() } +mod guard_module { + use actix_web::{guard::GuardContext, http::header}; + + pub fn guard(ctx: &GuardContext) -> bool { + ctx.header::() + .map(|h| h.preference() == "image/*") + .unwrap_or(false) + } +} + +#[get("/test/guard", guard = "guard_module::guard")] +async fn guard_test() -> impl Responder { + HttpResponse::Ok() +} + pub struct ChangeStatusCode; impl Transform for ChangeStatusCode @@ -186,7 +236,11 @@ async fn test_body() { .service(patch_test) .service(test_handler) .service(route_test) + .service(routes_overlapping_test) + .service(routes_overlapping_inaccessible_test) + .service(routes_test) .service(custom_resource_name_test) + .service(guard_test) }); let request = srv.request(http::Method::GET, srv.url("/test")); let response = request.send().await.unwrap(); @@ -242,9 +296,47 @@ async fn test_body() { let response = request.send().await.unwrap(); assert!(!response.status().is_success()); + let request = srv.request(http::Method::GET, srv.url("/routes/test")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::GET, srv.url("/routes/test2")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::POST, srv.url("/routes/test")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::GET, srv.url("/routes/not-set")); + let response = request.send().await.unwrap(); + assert!(response.status().is_client_error()); + + let request = srv.request(http::Method::GET, srv.url("/routes/overlap/test")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::GET, srv.url("/routes/overlap/bar")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::GET, srv.url("/routes/overlap2/test")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::GET, srv.url("/routes/overlap2/bar")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + let request = srv.request(http::Method::GET, srv.url("/custom_resource_name")); let response = request.send().await.unwrap(); assert!(response.status().is_success()); + + let request = srv + .request(http::Method::GET, srv.url("/test/guard")) + .insert_header(("Accept", "image/*")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); } #[actix_rt::test] diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index b2d9ce186..1f7996fd0 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -1,4 +1,4 @@ -#[rustversion::stable(1.54)] // MSRV +#[rustversion::stable(1.57)] // MSRV #[test] fn compile_macros() { let t = trybuild::TestCases::new(); @@ -12,6 +12,10 @@ fn compile_macros() { t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs"); t.compile_fail("tests/trybuild/route-malformed-path-fail.rs"); + t.pass("tests/trybuild/routes-ok.rs"); + t.compile_fail("tests/trybuild/routes-missing-method-fail.rs"); + t.compile_fail("tests/trybuild/routes-missing-args-fail.rs"); + t.pass("tests/trybuild/docstring-ok.rs"); t.pass("tests/trybuild/test-runtime.rs"); diff --git a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr index 90cff1b1c..fe9274bc8 100644 --- a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr @@ -1,11 +1,13 @@ error: HTTP method defined more than once: `GET` - --> $DIR/route-duplicate-method-fail.rs:3:35 + --> tests/trybuild/route-duplicate-method-fail.rs:3:35 | 3 | #[route("/", method="GET", method="GET")] | ^^^^^ error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied - --> $DIR/route-duplicate-method-fail.rs:12:55 + --> tests/trybuild/route-duplicate-method-fail.rs:12:55 | 12 | let srv = actix_test::start(|| App::new().service(index)); - | ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` + | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` + | | + | required by a bound introduced by this call diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr index b1cefafde..284b2cf4a 100644 --- a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr @@ -10,4 +10,6 @@ error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpSer --> tests/trybuild/route-missing-method-fail.rs:12:55 | 12 | let srv = actix_test::start(|| App::new().service(index)); - | ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` + | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` + | | + | required by a bound introduced by this call diff --git a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr index dda366067..804ba69f3 100644 --- a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr @@ -1,11 +1,13 @@ error: Unexpected HTTP method: `UNEXPECTED` - --> $DIR/route-unexpected-method-fail.rs:3:21 + --> tests/trybuild/route-unexpected-method-fail.rs:3:21 | 3 | #[route("/", method="UNEXPECTED")] | ^^^^^^^^^^^^ error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied - --> $DIR/route-unexpected-method-fail.rs:12:55 + --> tests/trybuild/route-unexpected-method-fail.rs:12:55 | 12 | let srv = actix_test::start(|| App::new().service(index)); - | ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` + | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` + | | + | required by a bound introduced by this call diff --git a/actix-web-codegen/tests/trybuild/routes-missing-args-fail.rs b/actix-web-codegen/tests/trybuild/routes-missing-args-fail.rs new file mode 100644 index 000000000..65573cf79 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/routes-missing-args-fail.rs @@ -0,0 +1,14 @@ +use actix_web_codegen::*; + +#[routes] +#[get] +async fn index() -> String { + "Hello World!".to_owned() +} + +#[actix_web::main] +async fn main() { + use actix_web::App; + + let srv = actix_test::start(|| App::new().service(index)); +} diff --git a/actix-web-codegen/tests/trybuild/routes-missing-args-fail.stderr b/actix-web-codegen/tests/trybuild/routes-missing-args-fail.stderr new file mode 100644 index 000000000..8efe0682b --- /dev/null +++ b/actix-web-codegen/tests/trybuild/routes-missing-args-fail.stderr @@ -0,0 +1,21 @@ +error: invalid service definition, expected #[get("")] + --> tests/trybuild/routes-missing-args-fail.rs:4:1 + | +4 | #[get] + | ^^^^^^ + | + = note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: Invalid input for macro + --> tests/trybuild/routes-missing-args-fail.rs:4:1 + | +4 | #[get] + | ^^^^^^ + +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied + --> tests/trybuild/routes-missing-args-fail.rs:13:55 + | +13 | let srv = actix_test::start(|| App::new().service(index)); + | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` + | | + | required by a bound introduced by this call diff --git a/actix-web-codegen/tests/trybuild/routes-missing-method-fail.rs b/actix-web-codegen/tests/trybuild/routes-missing-method-fail.rs new file mode 100644 index 000000000..f0271ef44 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/routes-missing-method-fail.rs @@ -0,0 +1,13 @@ +use actix_web_codegen::*; + +#[routes] +async fn index() -> String { + "Hello World!".to_owned() +} + +#[actix_web::main] +async fn main() { + use actix_web::App; + + let srv = actix_test::start(|| App::new().service(index)); +} diff --git a/actix-web-codegen/tests/trybuild/routes-missing-method-fail.stderr b/actix-web-codegen/tests/trybuild/routes-missing-method-fail.stderr new file mode 100644 index 000000000..b3795d74a --- /dev/null +++ b/actix-web-codegen/tests/trybuild/routes-missing-method-fail.stderr @@ -0,0 +1,15 @@ +error: The #[routes] macro requires at least one `#[(..)]` attribute. + --> tests/trybuild/routes-missing-method-fail.rs:3:1 + | +3 | #[routes] + | ^^^^^^^^^ + | + = note: this error originates in the attribute macro `routes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied + --> tests/trybuild/routes-missing-method-fail.rs:12:55 + | +12 | let srv = actix_test::start(|| App::new().service(index)); + | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` + | | + | required by a bound introduced by this call diff --git a/actix-web-codegen/tests/trybuild/routes-ok.rs b/actix-web-codegen/tests/trybuild/routes-ok.rs new file mode 100644 index 000000000..98b5e553e --- /dev/null +++ b/actix-web-codegen/tests/trybuild/routes-ok.rs @@ -0,0 +1,23 @@ +use actix_web_codegen::*; + +#[routes] +#[get("/")] +#[post("/")] +async fn index() -> String { + "Hello World!".to_owned() +} + +#[actix_web::main] +async fn main() { + use actix_web::App; + + let srv = actix_test::start(|| App::new().service(index)); + + let request = srv.get("/"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.post("/"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md new file mode 100644 index 000000000..f38282b41 --- /dev/null +++ b/actix-web/CHANGES.md @@ -0,0 +1,1363 @@ +# Changelog + +## Unreleased - 2022-xx-xx +### Added +- Add `#[routes]` macro to support multiple paths for one handler. [#2718] +- Add `ServiceRequest::{parts, request}()` getter methods. [#2786] +- Add configuration options for TLS handshake timeout via `HttpServer::{rustls, openssl}_with_config` methods. [#2752] + +### Changed +- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. + +[#2718]: https://github.com/actix/actix-web/pull/2718 +[#2752]: https://github.com/actix/actix-web/pull/2752 +[#2786]: https://github.com/actix/actix-web/pull/2786 + + +## 4.1.0 - 2022-06-11 +### Added +- Add `ServiceRequest::extract()` to make it easier to use extractors when writing middlewares. [#2647] +- Add `Route::wrap()` to allow individual routes to use middleware. [#2725] +- Add `ServiceConfig::default_service()`. [#2338] [#2743] +- Implement `ResponseError` for `std::convert::Infallible` + +### Changed +- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency. + +### Fixed +- Clear connection-level data on `HttpRequest` drop. [#2742] + +[#2338]: https://github.com/actix/actix-web/pull/2338 +[#2647]: https://github.com/actix/actix-web/pull/2647 +[#2725]: https://github.com/actix/actix-web/pull/2725 +[#2742]: https://github.com/actix/actix-web/pull/2742 +[#2743]: https://github.com/actix/actix-web/pull/2743 + + +## 4.0.1 - 2022-02-25 +### Fixed +- Use stable version in readme example. + + +## 4.0.0 - 2022-02-25 +### Dependencies +- Updated `actix-*` to Tokio v1-based versions. [#1813] +- Updated `actix-web-codegen` to `4.0.0`. +- Updated `cookie` to `0.16`. [#2555] +- Updated `language-tags` to `0.3`. +- Updated `rand` to `0.8`. +- Updated `rustls` to `0.20`. [#2414] +- Updated `tokio` to `1`. + +### Added +- Crate Features: + - `cookies`; enabled by default. [#2619] + - `compress-brotli`; enabled by default. [#2618] + - `compress-gzip`; enabled by default. [#2618] + - `compress-zstd`; enabled by default. [#2618] + - `macros`; enables routing and runtime macros, enabled by default. [#2619] +- Types: + - `CustomizeResponder` for customizing response. [#2510] + - `dev::ServerHandle` re-export from `actix-server`. [#2442] + - `dev::ServiceFactory` re-export from `actix-service`. [#2325] + - `guard::GuardContext` for use with the `Guard` trait. [#2552] + - `http::header::AcceptEncoding` typed header. [#2482] + - `http::header::Range` typed header. [#2485] + - `http::KeepAlive` re-export from `actix-http`. [#2625] + - `middleware::Compat` that boxes middleware types like `Logger` and `Compress` to be used with constrained type bounds. [#1865] + - `web::Header` extractor for extracting typed HTTP headers in handlers. [#2094] +- Methods: + - `dev::ServiceRequest::guard_ctx()` for obtaining a guard context. [#2552] + - `dev::ServiceRequest::parts_mut()`. [#2177] + - `dev::ServiceResponse::map_into_{left,right}_body()` and `HttpResponse::map_into_boxed_body()`. [#2468] + - `Either, web::Form>::into_inner()` which returns the inner type for whichever variant was created. Also works for `Either, web::Json>`. [#1894] + - `http::header::AcceptLanguage::{ranked, preference}()`. [#2480] + - `HttpResponse::add_removal_cookie()`. [#2586] + - `HttpResponse::map_into_{left,right}_body()` and `HttpResponse::map_into_boxed_body()`. [#2468] + - `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200] + - `middleware::Logger::log_target()` to allow customize. [#2594] + - `Responder::customize()` trait method that wraps responder in `CustomizeResponder`. [#2510] + - `Route::service()` for using hand-written services as handlers. [#2262] + - `ServiceResponse::into_parts()`. [#2499] + - `TestServer::client_headers()` method. [#2097] + - `web::ServiceConfig::configure()` to allow easy nesting of configuration functions. [#1988] +- Trait Implementations: + - Implement `Debug` for `DefaultHeaders`. [#2510] + - Implement `FromRequest` for `ConnectionInfo` and `PeerAddr`. [#2263] + - Implement `FromRequest` for `Method`. [#2263] + - Implement `FromRequest` for `Uri`. [#2263] + - Implement `Hash` for `http::header::Encoding`. [#2501] + - Implement `Responder` for `Vec`. [#2625] +- Misc: + - `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] + - Enable registering a vec of services of the same type to `App` [#1933] + - Add `services!` macro for helping register multiple services to `App`. [#1933] + - Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362] + - Connection data set through the `HttpServer::on_connect` callback is now accessible only from the new `HttpRequest::conn_data()` and `ServiceRequest::conn_data()` methods. [#2491] + +### Changed +- Functions: + - `guard::fn_guard` functions now receives a `&GuardContext`. [#2552] + - `guard::Not` is now generic over the type of guard it wraps. [#2552] + - `test::{call_service, read_response, read_response_json, send_request}()` now receive a `&Service`. [#1905] + - Some guard functions now return `impl Guard` and their concrete types are made private: `guard::Header` and all the method guards. [#2552] + - Rename `test::{default_service => status_service}()`. Old name is deprecated. [#2518] + - Rename `test::{read_response_json => call_and_read_body_json}()`. Old name is deprecated. [#2518] + - Rename `test::{read_response => call_and_read_body}()`. Old name is deprecated. [#2518] +- Traits: + - `guard::Guard::check` now receives a `&GuardContext`. [#2552] + - `FromRequest::Config` associated type was removed. [#2233] + - `Responder` trait has been reworked and now `Response`/`HttpResponse` synchronously, making it simpler and more performant. [#1891] + - Rename `Factory` trait to `Handler`. [#1852] +- Types: + - `App`'s `B` (body) type parameter been removed. As a result, `App`s can be returned from functions now. [#2493] + - `Compress` middleware's response type is now `EitherBody>`. [#2448] + - `error::BlockingError` is now a unit struct. It's now only triggered when blocking thread pool has shutdown. [#1957] + - `ErrorHandlerResponse`'s response variants now use `ServiceResponse>`. [#2515] + - `ErrorHandlers` middleware's response types now use `ServiceResponse>`. [#2515] + - `http::header::Encoding` now only represents `Content-Encoding` types. [#2501] + - `middleware::Condition` gained a broader middleware compatibility. [#2635] + - `Resource` no longer require service body type to be boxed. [#2526] + - `Scope` no longer require service body type to be boxed. [#2523] + - `web::Path`s inner field is now private. [#1894] + - `web::Payload`'s inner field is now private. [#2384] + - Error enums are now marked `#[non_exhaustive]`. [#2148] +- Enum Variants: + - `Either` now uses `Left`/`Right` variants (instead of `A`/`B`) [#1894] + - Include size and limits in `JsonPayloadError::Overflow`. [#2162] +- Methods: + - `App::data()` is deprecated; `App::app_data()` should be preferred. [#2271] + - `dev::JsonBody::new()` returns a default limit of 32kB to be consistent with `JsonConfig` and the default behaviour of the `web::Json` extractor. [#2010] + - `dev::ServiceRequest::{into_parts, from_parts}()` can no longer fail. [#1893] + - `dev::ServiceRequest::from_request` can no longer fail. [#1893] + - `dev::ServiceResponse::error_response()` now uses body type of `BoxBody`. [#2201] + - `dev::ServiceResponse::map_body()` closure receives and returns `B` instead of `ResponseBody`. [#2201] + - `http::header::ContentType::html()` now produces `text/html; charset=utf-8` instead of `text/html`. [#2423] + - `HttpRequest::url_for`'s constructed URLs no longer contain query or fragment. [#2430] + - `HttpResponseBuilder::json()` can now receive data by value and reference. [#1903] + - `HttpServer::{listen_rustls, bind_rustls}()` now honor the ALPN protocols in the configuration parameter. [#2226] + - `middleware::NormalizePath()` now will not try to normalize URIs with no valid path [#2246] + - `test::TestRequest::param()` now accepts more than just static strings. [#2172] + - `web::Data::into_inner()` and `Data::get_ref()` no longer require `T: Sized`. [#2403] + - Rename `HttpServer::{client_timeout => client_request_timeout}()`. [#2611] + - Rename `HttpServer::{client_shutdown => client_disconnect_timeout}()`. [#2611] + - Rename `http::header::Accept::{mime_precedence => ranked}()`. [#2480] + - Rename `http::header::Accept::{mime_preference => preference}()`. [#2480] + - Rename `middleware::DefaultHeaders::{content_type => add_content_type}()`. [#1875] + - Rename `dev::ConnectionInfo::{remote_addr => peer_addr}`, deprecating the old name. [#2554] +- Trait Implementations: + - `HttpResponse` can now be used as a `Responder` with any body type. [#2567] +- Misc: + - Maximum number of handler extractors has increased to 12. [#2582] + - The default `TrailingSlash` behavior is now `Trim`, in line with existing documentation. See migration guide for implications. [#1875] + - `Result` extractor wrapper can now convert error types. [#2581] + - Compress middleware will return `406 Not Acceptable` when no content encoding is acceptable to the client. [#2344] + - Adjusted default JSON payload limit to 2MB (from 32kb). [#2162] + - All error trait bounds in server service builders have changed from `Into` to `Into>`. [#2253] + - All error trait bounds in message body and stream impls changed from `Into` to `Into>`. [#2253] + - Improve spec compliance of `dev::ConnectionInfo` extractor. [#2282] + - Associated types in `FromRequest` implementation for `Option` and `Result` have changed. [#2581] + - Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201] + - Minimum supported Rust version (MSRV) is now 1.54. + +### Fixed +- Auto-negotiation of content encoding is more fault-tolerant when using the `Compress` middleware. [#2501] +- Scope and Resource middleware can access data items set on their own layer. [#2288] +- Multiple calls to `App::data()` with the same type now keeps the latest call's data. [#1906] +- Typed headers containing lists that require one or more items now enforce this minimum. [#2482] +- `dev::ConnectionInfo::peer_addr` will no longer return the port number. [#2554] +- `dev::ConnectionInfo::realip_remote_addr` will no longer return the port number if sourcing the IP from the peer's socket address. [#2554] +- Accept wildcard `*` items in `AcceptLanguage`. [#2480] +- Relax `Unpin` bound on `S` (stream) parameter of `HttpResponseBuilder::streaming`. [#2448] +- Fix quality parse error in `http::header::AcceptEncoding` typed header. [#2344] +- Double ampersand in `middleware::Logger` format is escaped correctly. [#2067] +- Added the underlying parse error to `test::read_body_json`'s panic message. [#1812] + +### Security +- `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. + +[`rustsec-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html + +### Removed +- Crate Features: + - `compress` feature. [#2065] +- Functions: + - `test::load_stream` and `test::load_body`; replace usage with `body::to_bytes`. [#2518] + - `test::start_with`; moved to new `actix-test` crate. [#2112] + - `test::start`; moved to new `actix-test` crate. [#2112] + - `test::unused_addr`; moved to new `actix-test` crate. [#2112] +- Traits: + - `BodyEncoding`; signalling content encoding is now only done via the `Content-Encoding` header. [#2565] +- Types: + - `dev::{BodySize, MessageBody, SizedStream}` re-exports; they are exposed through the `body` module. [#2468] + - `EitherExtractError` direct export. [#2510] + - `rt::{Arbiter, ArbiterHandle}` re-exports. [#2619] + - `test::TestServer`; moved to new `actix-test` crate. [#2112] + - `test::TestServerConfig`; moved to new `actix-test` crate. [#2112] + - `web::HttpRequest` re-export. [#2663] + - `web::HttpResponse` re-export. [#2663] +- Methods: + - `AppService::set_service_data`; for custom HTTP service factories adding application data, use the layered data model by calling `ServiceRequest::add_data_container` when handling requests instead. [#1906] + - `dev::ConnectionInfo::get`. [#2487] + - `dev::ServiceResponse::checked_expr`. [#2401] + - `HttpRequestBuilder::del_cookie`. [#2591] + - `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201] + - `HttpResponseBuilder::json2()`. [#1903] + - `middleware::Compress::new`; restricting compression algorithm is done through feature flags. [#2501] + - `test::TestRequest::with_header()`; use `test::TestRequest::default().insert_header()`. [#1869] +- Trait Implementations: + - Implementation of `From` for `Either` crate. [#2516] + - Implementation of `Future` for `HttpResponse`. [#2601] +- Misc: + - The `client` module was removed; use the `awc` crate directly. [871ca5e4] + - `middleware::{normalize, err_handlers}` modules; all necessary middleware types are now exposed in the `middleware` module. + +[#1812]: https://github.com/actix/actix-web/pull/1812 +[#1813]: https://github.com/actix/actix-web/pull/1813 +[#1852]: https://github.com/actix/actix-web/pull/1852 +[#1865]: https://github.com/actix/actix-web/pull/1865 +[#1869]: https://github.com/actix/actix-web/pull/1869 +[#1875]: https://github.com/actix/actix-web/pull/1875 +[#1878]: https://github.com/actix/actix-web/pull/1878 +[#1891]: https://github.com/actix/actix-web/pull/1891 +[#1893]: https://github.com/actix/actix-web/pull/1893 +[#1894]: https://github.com/actix/actix-web/pull/1894 +[#1903]: https://github.com/actix/actix-web/pull/1903 +[#1905]: https://github.com/actix/actix-web/pull/1905 +[#1906]: https://github.com/actix/actix-web/pull/1906 +[#1933]: https://github.com/actix/actix-web/pull/1933 +[#1957]: https://github.com/actix/actix-web/pull/1957 +[#1957]: https://github.com/actix/actix-web/pull/1957 +[#1981]: https://github.com/actix/actix-web/pull/1981 +[#1988]: https://github.com/actix/actix-web/pull/1988 +[#2010]: https://github.com/actix/actix-web/pull/2010 +[#2065]: https://github.com/actix/actix-web/pull/2065 +[#2067]: https://github.com/actix/actix-web/pull/2067 +[#2093]: https://github.com/actix/actix-web/pull/2093 +[#2094]: https://github.com/actix/actix-web/pull/2094 +[#2097]: https://github.com/actix/actix-web/pull/2097 +[#2112]: https://github.com/actix/actix-web/pull/2112 +[#2148]: https://github.com/actix/actix-web/pull/2148 +[#2162]: https://github.com/actix/actix-web/pull/2162 +[#2172]: https://github.com/actix/actix-web/pull/2172 +[#2177]: https://github.com/actix/actix-web/pull/2177 +[#2200]: https://github.com/actix/actix-web/pull/2200 +[#2201]: https://github.com/actix/actix-web/pull/2201 +[#2201]: https://github.com/actix/actix-web/pull/2201 +[#2233]: https://github.com/actix/actix-web/pull/2233 +[#2246]: https://github.com/actix/actix-web/pull/2246 +[#2250]: https://github.com/actix/actix-web/pull/2250 +[#2253]: https://github.com/actix/actix-web/pull/2253 +[#2262]: https://github.com/actix/actix-web/pull/2262 +[#2263]: https://github.com/actix/actix-web/pull/2263 +[#2271]: https://github.com/actix/actix-web/pull/2271 +[#2282]: https://github.com/actix/actix-web/pull/2282 +[#2288]: https://github.com/actix/actix-web/pull/2288 +[#2325]: https://github.com/actix/actix-web/pull/2325 +[#2344]: https://github.com/actix/actix-web/pull/2344 +[#2362]: https://github.com/actix/actix-web/pull/2362 +[#2379]: https://github.com/actix/actix-web/pull/2379 +[#2384]: https://github.com/actix/actix-web/pull/2384 +[#2401]: https://github.com/actix/actix-web/pull/2401 +[#2403]: https://github.com/actix/actix-web/pull/2403 +[#2409]: https://github.com/actix/actix-web/pull/2409 +[#2414]: https://github.com/actix/actix-web/pull/2414 +[#2423]: https://github.com/actix/actix-web/pull/2423 +[#2430]: https://github.com/actix/actix-web/pull/2430 +[#2442]: https://github.com/actix/actix-web/pull/2442 +[#2446]: https://github.com/actix/actix-web/pull/2446 +[#2448]: https://github.com/actix/actix-web/pull/2448 +[#2468]: https://github.com/actix/actix-web/pull/2468 +[#2474]: https://github.com/actix/actix-web/pull/2474 +[#2480]: https://github.com/actix/actix-web/pull/2480 +[#2482]: https://github.com/actix/actix-web/pull/2482 +[#2484]: https://github.com/actix/actix-web/pull/2484 +[#2485]: https://github.com/actix/actix-web/pull/2485 +[#2487]: https://github.com/actix/actix-web/pull/2487 +[#2491]: https://github.com/actix/actix-web/pull/2491 +[#2492]: https://github.com/actix/actix-web/pull/2492 +[#2493]: https://github.com/actix/actix-web/pull/2493 +[#2499]: https://github.com/actix/actix-web/pull/2499 +[#2501]: https://github.com/actix/actix-web/pull/2501 +[#2510]: https://github.com/actix/actix-web/pull/2510 +[#2515]: https://github.com/actix/actix-web/pull/2515 +[#2516]: https://github.com/actix/actix-web/pull/2516 +[#2518]: https://github.com/actix/actix-web/pull/2518 +[#2523]: https://github.com/actix/actix-web/pull/2523 +[#2526]: https://github.com/actix/actix-web/pull/2526 +[#2552]: https://github.com/actix/actix-web/pull/2552 +[#2554]: https://github.com/actix/actix-web/pull/2554 +[#2555]: https://github.com/actix/actix-web/pull/2555 +[#2565]: https://github.com/actix/actix-web/pull/2565 +[#2567]: https://github.com/actix/actix-web/pull/2567 +[#2569]: https://github.com/actix/actix-web/pull/2569 +[#2581]: https://github.com/actix/actix-web/pull/2581 +[#2582]: https://github.com/actix/actix-web/pull/2582 +[#2584]: https://github.com/actix/actix-web/pull/2584 +[#2585]: https://github.com/actix/actix-web/pull/2585 +[#2586]: https://github.com/actix/actix-web/pull/2586 +[#2591]: https://github.com/actix/actix-web/pull/2591 +[#2594]: https://github.com/actix/actix-web/pull/2594 +[#2601]: https://github.com/actix/actix-web/pull/2601 +[#2611]: https://github.com/actix/actix-web/pull/2611 +[#2619]: https://github.com/actix/actix-web/pull/2619 +[#2625]: https://github.com/actix/actix-web/pull/2625 +[#2635]: https://github.com/actix/actix-web/pull/2635 +[#2659]: https://github.com/actix/actix-web/pull/2659 +[#2663]: https://github.com/actix/actix-web/pull/2663 +[871ca5e4]: https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7 + + +
+4.0.0 Pre-Releases + +## 4.0.0-rc.3 - 2022-02-08 +### Changed +- `middleware::Condition` gained a broader compatibility; `Compat` is needed in fewer cases. [#2635] + +### Added +- Implement `Responder` for `Vec`. [#2625] +- Re-export `KeepAlive` in `http` mod. [#2625] + +[#2625]: https://github.com/actix/actix-web/pull/2625 +[#2635]: https://github.com/actix/actix-web/pull/2635 + + +## 4.0.0-rc.2 - 2022-02-02 +### Added +- On-by-default `macros` feature flag to enable routing and runtime macros. [#2619] + +### Removed +- `rt::{Arbiter, ArbiterHandle}` re-exports. [#2619] + +[#2619]: https://github.com/actix/actix-web/pull/2619 + + +## 4.0.0-rc.1 - 2022-01-31 +### Changed +- Rename `HttpServer::{client_timeout => client_request_timeout}`. [#2611] +- Rename `HttpServer::{client_shutdown => client_disconnect_timeout}`. [#2611] + +### Removed +- `impl Future for HttpResponse`. [#2601] + +[#2601]: https://github.com/actix/actix-web/pull/2601 +[#2611]: https://github.com/actix/actix-web/pull/2611 + + +## 4.0.0-beta.21 - 2022-01-21 +### Added +- `HttpResponse::add_removal_cookie`. [#2586] +- `Logger::log_target`. [#2594] + +### Removed +- `HttpRequest::req_data[_mut]()`; request-local data is still available through `.extensions()`. [#2585] +- `HttpRequestBuilder::del_cookie`. [#2591] + +[#2585]: https://github.com/actix/actix-web/pull/2585 +[#2586]: https://github.com/actix/actix-web/pull/2586 +[#2591]: https://github.com/actix/actix-web/pull/2591 +[#2594]: https://github.com/actix/actix-web/pull/2594 + + +## 4.0.0-beta.20 - 2022-01-14 +### Added +- `GuardContext::header` [#2569] +- `ServiceConfig::configure` to allow easy nesting of configuration functions. [#1988] + +### Changed +- `HttpResponse` can now be used as a `Responder` with any body type. [#2567] +- `Result` extractor wrapper can now convert error types. [#2581] +- Associated types in `FromRequest` impl for `Option` and `Result` has changed. [#2581] +- Maximum number of handler extractors has increased to 12. [#2582] +- Removed bound `::Error: Debug` in test utility functions in order to support returning opaque apps. [#2584] + +[#1988]: https://github.com/actix/actix-web/pull/1988 +[#2567]: https://github.com/actix/actix-web/pull/2567 +[#2569]: https://github.com/actix/actix-web/pull/2569 +[#2581]: https://github.com/actix/actix-web/pull/2581 +[#2582]: https://github.com/actix/actix-web/pull/2582 +[#2584]: https://github.com/actix/actix-web/pull/2584 + + +## 4.0.0-beta.19 - 2022-01-04 +### Added +- `impl Hash` for `http::header::Encoding`. [#2501] +- `AcceptEncoding::negotiate()`. [#2501] + +### Changed +- `AcceptEncoding::preference` now returns `Option>`. [#2501] +- Rename methods `BodyEncoding::{encoding => encode_with, get_encoding => preferred_encoding}`. [#2501] +- `http::header::Encoding` now only represents `Content-Encoding` types. [#2501] + +### Fixed +- Auto-negotiation of content encoding is more fault-tolerant when using the `Compress` middleware. [#2501] + +### Removed +- `Compress::new`; restricting compression algorithm is done through feature flags. [#2501] +- `BodyEncoding` trait; signalling content encoding is now only done via the `Content-Encoding` header. [#2565] + +[#2501]: https://github.com/actix/actix-web/pull/2501 +[#2565]: https://github.com/actix/actix-web/pull/2565 + + +## 4.0.0-beta.18 - 2021-12-29 +### Changed +- Update `cookie` dependency (re-exported) to `0.16`. [#2555] +- Minimum supported Rust version (MSRV) is now 1.54. + +### Security +- `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. + +[#2555]: https://github.com/actix/actix-web/pull/2555 +[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html + + +## 4.0.0-beta.17 - 2021-12-29 +### 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 +### Added +- Method on `Responder` trait (`customize`) for customizing responders and `CustomizeResponder` struct. [#2510] +- Implement `Debug` for `DefaultHeaders`. [#2510] + +### Changed +- Align `DefaultHeader` method terminology, deprecating previous methods. [#2510] +- Response service types in `ErrorHandlers` middleware now use `ServiceResponse>` to allow changing the body type. [#2515] +- Both variants in `ErrorHandlerResponse` now use `ServiceResponse>`. [#2515] +- Rename `test::{default_service => simple_service}`. Old name is deprecated. [#2518] +- Rename `test::{read_response_json => call_and_read_body_json}`. Old name is deprecated. [#2518] +- Rename `test::{read_response => call_and_read_body}`. Old name is deprecated. [#2518] +- Relax body type and error bounds on test utilities. [#2518] + +### Removed +- Top-level `EitherExtractError` export. [#2510] +- Conversion implementations for `either` crate. [#2516] +- `test::load_stream` and `test::load_body`; replace usage with `body::to_bytes`. [#2518] + +[#2510]: https://github.com/actix/actix-web/pull/2510 +[#2515]: https://github.com/actix/actix-web/pull/2515 +[#2516]: https://github.com/actix/actix-web/pull/2516 +[#2518]: https://github.com/actix/actix-web/pull/2518 + + +## 4.0.0-beta.14 - 2021-12-11 +### Added +- Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480] +- `AcceptEncoding` typed header. [#2482] +- `Range` typed header. [#2485] +- `HttpResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468] +- `ServiceResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468] +- Connection data set through the `HttpServer::on_connect` callback is now accessible only from the new `HttpRequest::conn_data()` and `ServiceRequest::conn_data()` methods. [#2491] +- `HttpRequest::{req_data,req_data_mut}`. [#2487] +- `ServiceResponse::into_parts`. [#2499] + +### Changed +- Rename `Accept::{mime_precedence => ranked}`. [#2480] +- Rename `Accept::{mime_preference => preference}`. [#2480] +- Un-deprecate `App::data_factory`. [#2484] +- `HttpRequest::url_for` no longer constructs URLs with query or fragment components. [#2430] +- Remove `B` (body) type parameter on `App`. [#2493] +- Add `B` (body) type parameter on `Scope`. [#2492] +- Request-local data container is no longer part of a `RequestHead`. Instead it is a distinct part of a `Request`. [#2487] + +### Fixed +- Accept wildcard `*` items in `AcceptLanguage`. [#2480] +- Re-exports `dev::{BodySize, MessageBody, SizedStream}`. They are exposed through the `body` module. [#2468] +- Typed headers containing lists that require one or more items now enforce this minimum. [#2482] + +### Removed +- `ConnectionInfo::get`. [#2487] + +[#2430]: https://github.com/actix/actix-web/pull/2430 +[#2468]: https://github.com/actix/actix-web/pull/2468 +[#2480]: https://github.com/actix/actix-web/pull/2480 +[#2482]: https://github.com/actix/actix-web/pull/2482 +[#2484]: https://github.com/actix/actix-web/pull/2484 +[#2485]: https://github.com/actix/actix-web/pull/2485 +[#2487]: https://github.com/actix/actix-web/pull/2487 +[#2491]: https://github.com/actix/actix-web/pull/2491 +[#2492]: https://github.com/actix/actix-web/pull/2492 +[#2493]: https://github.com/actix/actix-web/pull/2493 +[#2499]: https://github.com/actix/actix-web/pull/2499 + + +## 4.0.0-beta.13 - 2021-11-30 +### Changed +- Update `actix-tls` to `3.0.0-rc.1`. [#2474] + +[#2474]: https://github.com/actix/actix-web/pull/2474 + + +## 4.0.0-beta.12 - 2021-11-22 +### Changed +- Compress middleware's response type is now `AnyBody>`. [#2448] + +### Fixed +- Relax `Unpin` bound on `S` (stream) parameter of `HttpResponseBuilder::streaming`. [#2448] + +### Removed +- `dev::ResponseBody` re-export; is function is replaced by the new `dev::AnyBody` enum. [#2446] + +[#2446]: https://github.com/actix/actix-web/pull/2446 +[#2448]: https://github.com/actix/actix-web/pull/2448 + + +## 4.0.0-beta.11 - 2021-11-15 +### Added +- Re-export `dev::ServerHandle` from `actix-server`. [#2442] + +### Changed +- `ContentType::html` now produces `text/html; charset=utf-8` instead of `text/html`. [#2423] +- Update `actix-server` to `2.0.0-beta.9`. [#2442] + +[#2423]: https://github.com/actix/actix-web/pull/2423 +[#2442]: https://github.com/actix/actix-web/pull/2442 + + +## 4.0.0-beta.10 - 2021-10-20 +### Added +- Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362] +- `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] + +### Changed +- Associated type `FromRequest::Config` was removed. [#2233] +- Inner field made private on `web::Payload`. [#2384] +- `Data::into_inner` and `Data::get_ref` no longer requires `T: Sized`. [#2403] +- Updated rustls to v0.20. [#2414] +- Minimum supported Rust version (MSRV) is now 1.52. + +### Removed +- Useless `ServiceResponse::checked_expr` method. [#2401] + +[#2233]: https://github.com/actix/actix-web/pull/2233 +[#2362]: https://github.com/actix/actix-web/pull/2362 +[#2384]: https://github.com/actix/actix-web/pull/2384 +[#2401]: https://github.com/actix/actix-web/pull/2401 +[#2403]: https://github.com/actix/actix-web/pull/2403 +[#2409]: https://github.com/actix/actix-web/pull/2409 +[#2414]: https://github.com/actix/actix-web/pull/2414 + + +## 4.0.0-beta.9 - 2021-09-09 +### Added +- Re-export actix-service `ServiceFactory` in `dev` module. [#2325] + +### Changed +- Compress middleware will return 406 Not Acceptable when no content encoding is acceptable to the client. [#2344] +- Move `BaseHttpResponse` to `dev::Response`. [#2379] +- Enable `TestRequest::param` to accept more than just static strings. [#2172] +- Minimum supported Rust version (MSRV) is now 1.51. + +### Fixed +- Fix quality parse error in Accept-Encoding header. [#2344] +- Re-export correct type at `web::HttpResponse`. [#2379] + +[#2172]: https://github.com/actix/actix-web/pull/2172 +[#2325]: https://github.com/actix/actix-web/pull/2325 +[#2344]: https://github.com/actix/actix-web/pull/2344 +[#2379]: https://github.com/actix/actix-web/pull/2379 + + +## 4.0.0-beta.8 - 2021-06-26 +### Added +- Add `ServiceRequest::parts_mut`. [#2177] +- Add extractors for `Uri` and `Method`. [#2263] +- Add extractors for `ConnectionInfo` and `PeerAddr`. [#2263] +- Add `Route::service` for using hand-written services as handlers. [#2262] + +### Changed +- Change compression algorithm features flags. [#2250] +- Deprecate `App::data` and `App::data_factory`. [#2271] +- Smarter extraction of `ConnectionInfo` parts. [#2282] + +### Fixed +- Scope and Resource middleware can access data items set on their own layer. [#2288] + +[#2177]: https://github.com/actix/actix-web/pull/2177 +[#2250]: https://github.com/actix/actix-web/pull/2250 +[#2271]: https://github.com/actix/actix-web/pull/2271 +[#2262]: https://github.com/actix/actix-web/pull/2262 +[#2263]: https://github.com/actix/actix-web/pull/2263 +[#2282]: https://github.com/actix/actix-web/pull/2282 +[#2288]: https://github.com/actix/actix-web/pull/2288 + + +## 4.0.0-beta.7 - 2021-06-17 +### Added +- `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200] + +### Changed +- Adjusted default JSON payload limit to 2MB (from 32kb) and included size and limits in the `JsonPayloadError::Overflow` error variant. [#2162] +- `ServiceResponse::error_response` now uses body type of `Body`. [#2201] +- `ServiceResponse::checked_expr` now returns a `Result`. [#2201] +- Update `language-tags` to `0.3`. +- `ServiceResponse::take_body`. [#2201] +- `ServiceResponse::map_body` closure receives and returns `B` instead of `ResponseBody` types. [#2201] +- All error trait bounds in server service builders have changed from `Into` to `Into>`. [#2253] +- All error trait bounds in message body and stream impls changed from `Into` to `Into>`. [#2253] +- `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuration parameter. [#2226] +- `middleware::normalize` now will not try to normalize URIs with no valid path [#2246] + +### Removed +- `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201] + +[#2162]: https://github.com/actix/actix-web/pull/2162 +[#2200]: https://github.com/actix/actix-web/pull/2200 +[#2201]: https://github.com/actix/actix-web/pull/2201 +[#2253]: https://github.com/actix/actix-web/pull/2253 +[#2246]: https://github.com/actix/actix-web/pull/2246 + + +## 4.0.0-beta.6 - 2021-04-17 +### Added +- `HttpResponse` and `HttpResponseBuilder` types. [#2065] + +### Changed +- Most error types are now marked `#[non_exhaustive]`. [#2148] +- Methods on `ContentDisposition` that took `T: AsRef` now take `impl AsRef`. + +[#2065]: https://github.com/actix/actix-web/pull/2065 +[#2148]: https://github.com/actix/actix-web/pull/2148 + + +## 4.0.0-beta.5 - 2021-04-02 +### Added +- `Header` extractor for extracting common HTTP headers in handlers. [#2094] +- Added `TestServer::client_headers` method. [#2097] + +### Changed +- `CustomResponder` would return error as `HttpResponse` when `CustomResponder::with_header` failed + instead of skipping. (Only the first error is kept when multiple error occur) [#2093] + +### Fixed +- Double ampersand in Logger format is escaped correctly. [#2067] + +### Removed +- The `client` mod was removed. Clients should now use `awc` directly. + [871ca5e4](https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7) +- Integration testing was moved to new `actix-test` crate. Namely these items from the `test` + module: `TestServer`, `TestServerConfig`, `start`, `start_with`, and `unused_addr`. [#2112] + +[#2067]: https://github.com/actix/actix-web/pull/2067 +[#2093]: https://github.com/actix/actix-web/pull/2093 +[#2094]: https://github.com/actix/actix-web/pull/2094 +[#2097]: https://github.com/actix/actix-web/pull/2097 +[#2112]: https://github.com/actix/actix-web/pull/2112 + + +## 4.0.0-beta.4 - 2021-03-09 +### Changed +- Feature `cookies` is now optional and enabled by default. [#1981] +- `JsonBody::new` returns a default limit of 32kB to be consistent with `JsonConfig` and the default + behaviour of the `web::Json` extractor. [#2010] + +[#1981]: https://github.com/actix/actix-web/pull/1981 +[#2010]: https://github.com/actix/actix-web/pull/2010 + + +## 4.0.0-beta.3 - 2021-02-10 +- Update `actix-web-codegen` to `0.5.0-beta.1`. + + +## 4.0.0-beta.2 - 2021-02-10 +### Added +- The method `Either, web::Form>::into_inner()` which returns the inner type for + whichever variant was created. Also works for `Either, web::Json>`. [#1894] +- Add `services!` macro for helping register multiple services to `App`. [#1933] +- Enable registering a vec of services of the same type to `App` [#1933] + +### Changed +- Rework `Responder` trait to be sync and returns `Response`/`HttpResponse` directly. + Making it simpler and more performant. [#1891] +- `ServiceRequest::into_parts` and `ServiceRequest::from_parts` can no longer fail. [#1893] +- `ServiceRequest::from_request` can no longer fail. [#1893] +- Our `Either` type now uses `Left`/`Right` variants (instead of `A`/`B`) [#1894] +- `test::{call_service, read_response, read_response_json, send_request}` take `&Service` + in argument [#1905] +- `App::wrap_fn`, `Resource::wrap_fn` and `Scope::wrap_fn` provide `&Service` in closure + argument. [#1905] +- `web::block` no longer requires the output is a Result. [#1957] + +### Fixed +- Multiple calls to `App::data` with the same type now keeps the latest call's data. [#1906] + +### Removed +- Public field of `web::Path` has been made private. [#1894] +- Public field of `web::Query` has been made private. [#1894] +- `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869] +- `AppService::set_service_data`; for custom HTTP service factories adding application data, use the + layered data model by calling `ServiceRequest::add_data_container` when handling + requests instead. [#1906] + +[#1891]: https://github.com/actix/actix-web/pull/1891 +[#1893]: https://github.com/actix/actix-web/pull/1893 +[#1894]: https://github.com/actix/actix-web/pull/1894 +[#1869]: https://github.com/actix/actix-web/pull/1869 +[#1905]: https://github.com/actix/actix-web/pull/1905 +[#1906]: https://github.com/actix/actix-web/pull/1906 +[#1933]: https://github.com/actix/actix-web/pull/1933 +[#1957]: https://github.com/actix/actix-web/pull/1957 + + +## 4.0.0-beta.1 - 2021-01-07 +### Added +- `Compat` middleware enabling generic response body/error type of middlewares like `Logger` and + `Compress` to be used in `middleware::Condition` and `Resource`, `Scope` services. [#1865] + +### Changed +- Update `actix-*` dependencies to tokio `1.0` based versions. [#1813] +- Bumped `rand` to `0.8`. +- Update `rust-tls` to `0.19`. [#1813] +- Rename `Handler` to `HandlerService` and rename `Factory` to `Handler`. [#1852] +- The default `TrailingSlash` is now `Trim`, in line with existing documentation. See migration + guide for implications. [#1875] +- Rename `DefaultHeaders::{content_type => add_content_type}`. [#1875] +- MSRV is now 1.46.0. + +### Fixed +- Added the underlying parse error to `test::read_body_json`'s panic message. [#1812] + +### Removed +- Public modules `middleware::{normalize, err_handlers}`. All necessary middleware types are now + exposed directly by the `middleware` module. +- Remove `actix-threadpool` as dependency. `actix_threadpool::BlockingError` error type can be imported + from `actix_web::error` module. [#1878] + +[#1812]: https://github.com/actix/actix-web/pull/1812 +[#1813]: https://github.com/actix/actix-web/pull/1813 +[#1852]: https://github.com/actix/actix-web/pull/1852 +[#1865]: https://github.com/actix/actix-web/pull/1865 +[#1875]: https://github.com/actix/actix-web/pull/1875 +[#1878]: https://github.com/actix/actix-web/pull/1878 + +
+ +## 3.3.3 - 2021-12-18 +### Changed +- Soft-deprecate `NormalizePath::default()`, noting upcoming behavior change in v4. [#2529] + +[#2529]: https://github.com/actix/actix-web/pull/2529 + + +## 3.3.2 - 2020-12-01 +### Fixed +- Removed an occasional `unwrap` on `None` panic in `NormalizePathNormalization`. [#1762] +- Fix `match_pattern()` returning `None` for scope with empty path resource. [#1798] +- Increase minimum `socket2` version. [#1803] + +[#1762]: https://github.com/actix/actix-web/pull/1762 +[#1798]: https://github.com/actix/actix-web/pull/1798 +[#1803]: https://github.com/actix/actix-web/pull/1803 + + +## 3.3.1 - 2020-11-29 +- Ensure `actix-http` dependency uses same `serde_urlencoded`. + + +## 3.3.0 - 2020-11-25 +### Added +- Add `Either` extractor helper. [#1788] + +### Changed +- Upgrade `serde_urlencoded` to `0.7`. [#1773] + +[#1773]: https://github.com/actix/actix-web/pull/1773 +[#1788]: https://github.com/actix/actix-web/pull/1788 + + +## 3.2.0 - 2020-10-30 +### Added +- Implement `exclude_regex` for Logger middleware. [#1723] +- Add request-local data extractor `web::ReqData`. [#1748] +- Add ability to register closure for request middleware logging. [#1749] +- Add `app_data` to `ServiceConfig`. [#1757] +- Expose `on_connect` for access to the connection stream before request is handled. [#1754] + +### Changed +- Updated actix-web-codegen dependency for access to new `#[route(...)]` multi-method macro. +- Print non-configured `Data` type when attempting extraction. [#1743] +- Re-export bytes::Buf{Mut} in web module. [#1750] +- Upgrade `pin-project` to `1.0`. + +[#1723]: https://github.com/actix/actix-web/pull/1723 +[#1743]: https://github.com/actix/actix-web/pull/1743 +[#1748]: https://github.com/actix/actix-web/pull/1748 +[#1750]: https://github.com/actix/actix-web/pull/1750 +[#1754]: https://github.com/actix/actix-web/pull/1754 +[#1749]: https://github.com/actix/actix-web/pull/1749 + + +## 3.1.0 - 2020-09-29 +### Changed +- Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allows `NormalizePath` + to retain any trailing slashes. [#1695] +- Remove bound `std::marker::Sized` from `web::Data` to support storing `Arc` + via `web::Data::from` [#1710] + +### Fixed +- `ResourceMap` debug printing is no longer infinitely recursive. [#1708] + +[#1695]: https://github.com/actix/actix-web/pull/1695 +[#1708]: https://github.com/actix/actix-web/pull/1708 +[#1710]: https://github.com/actix/actix-web/pull/1710 + + +## 3.0.2 - 2020-09-15 +### Fixed +- `NormalizePath` when used with `TrailingSlash::Trim` no longer trims the root path "/". [#1678] + +[#1678]: https://github.com/actix/actix-web/pull/1678 + + +## 3.0.1 - 2020-09-13 +### Changed +- `middleware::normalize::TrailingSlash` enum is now accessible. [#1673] + +[#1673]: https://github.com/actix/actix-web/pull/1673 + + +## 3.0.0 - 2020-09-11 +- No significant changes from `3.0.0-beta.4`. + + +## 3.0.0-beta.4 - 2020-09-09 +### Added +- `middleware::NormalizePath` now has configurable behavior for either always having a trailing + slash, or as the new addition, always trimming trailing slashes. [#1639] + +### Changed +- Update actix-codec and actix-utils dependencies. [#1634] +- `FormConfig` and `JsonConfig` configurations are now also considered when set + using `App::data`. [#1641] +- `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. [#1655] +- `HttpServer::maxconnrate` is renamed to the more expressive + `HttpServer::max_connection_rate`. [#1655] + +[#1639]: https://github.com/actix/actix-web/pull/1639 +[#1641]: https://github.com/actix/actix-web/pull/1641 +[#1634]: https://github.com/actix/actix-web/pull/1634 +[#1655]: https://github.com/actix/actix-web/pull/1655 + +## 3.0.0-beta.3 - 2020-08-17 +### Changed +- Update `rustls` to 0.18 + + +## 3.0.0-beta.2 - 2020-08-17 +### Changed +- `PayloadConfig` is now also considered in `Bytes` and `String` extractors when set + using `App::data`. [#1610] +- `web::Path` now has a public representation: `web::Path(pub T)` that enables + destructuring. [#1594] +- `ServiceRequest::app_data` allows retrieval of non-Data data without splitting into parts to + access `HttpRequest` which already allows this. [#1618] +- Re-export all error types from `awc`. [#1621] +- MSRV is now 1.42.0. + +### Fixed +- Memory leak of app data in pooled requests. [#1609] + +[#1594]: https://github.com/actix/actix-web/pull/1594 +[#1609]: https://github.com/actix/actix-web/pull/1609 +[#1610]: https://github.com/actix/actix-web/pull/1610 +[#1618]: https://github.com/actix/actix-web/pull/1618 +[#1621]: https://github.com/actix/actix-web/pull/1621 + + +## 3.0.0-beta.1 - 2020-07-13 +### Added +- Re-export `actix_rt::main` as `actix_web::main`. +- `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched + resource pattern. +- `HttpRequest::match_name` and `ServiceRequest::match_name` for extracting matched resource name. + +### Changed +- Fix actix_http::h1::dispatcher so it returns when HW_BUFFER_SIZE is reached. Should reduce peak memory consumption during large uploads. [#1550] +- Migrate cookie handling to `cookie` crate. Actix-web no longer requires `ring` dependency. +- MSRV is now 1.41.1 + +### Fixed +- `NormalizePath` improved consistency when path needs slashes added _and_ removed. + + +## 3.0.0-alpha.3 - 2020-05-21 +### Added +- Add option to create `Data` from `Arc` [#1509] + +### Changed +- Resources and Scopes can now access non-overridden data types set on App (or containing scopes) when setting their own data. [#1486] +- Fix audit issue logging by default peer address [#1485] +- Bump minimum supported Rust version to 1.40 +- Replace deprecated `net2` crate with `socket2` + +[#1485]: https://github.com/actix/actix-web/pull/1485 +[#1509]: https://github.com/actix/actix-web/pull/1509 + +## [3.0.0-alpha.2] - 2020-05-08 + +### Changed + +- `{Resource,Scope}::default_service(f)` handlers now support app data extraction. [#1452] +- Implement `std::error::Error` for our custom errors [#1422] +- NormalizePath middleware now appends trailing / so that routes of form /example/ respond to /example requests. [#1433] +- Remove the `failure` feature and support. + +[#1422]: https://github.com/actix/actix-web/pull/1422 +[#1433]: https://github.com/actix/actix-web/pull/1433 +[#1452]: https://github.com/actix/actix-web/pull/1452 +[#1486]: https://github.com/actix/actix-web/pull/1486 + + +## [3.0.0-alpha.1] - 2020-03-11 + +### Added + +- Add helper function for creating routes with `TRACE` method guard `web::trace()` +- Add convenience functions `test::read_body_json()` and `test::TestRequest::send_request()` for testing. + +### Changed + +- Use `sha-1` crate instead of unmaintained `sha1` crate +- Skip empty chunks when returning response from a `Stream` [#1308] +- Update the `time` dependency to 0.2.7 +- Update `actix-tls` dependency to 2.0.0-alpha.1 +- Update `rustls` dependency to 0.17 + +[#1308]: https://github.com/actix/actix-web/pull/1308 + +## [2.0.0] - 2019-12-25 + +### Changed + +- Rename `HttpServer::start()` to `HttpServer::run()` + +- Allow to gracefully stop test server via `TestServer::stop()` + +- Allow to specify multi-patterns for resources + +## [2.0.0-rc] - 2019-12-20 + +### Changed + +- Move `BodyEncoding` to `dev` module #1220 + +- Allow to set `peer_addr` for TestRequest #1074 + +- Make web::Data deref to Arc #1214 + +- Rename `App::register_data()` to `App::app_data()` + +- `HttpRequest::app_data()` returns `Option<&T>` instead of `Option<&Data>` + +### Fixed + +- Fix `AppConfig::secure()` is always false. #1202 + + +## [2.0.0-alpha.6] - 2019-12-15 + +### Fixed + +- Fixed compilation with default features off + +## [2.0.0-alpha.5] - 2019-12-13 + +### Added + +- Add test server, `test::start()` and `test::start_with()` + +## [2.0.0-alpha.4] - 2019-12-08 + +### Deleted + +- Delete HttpServer::run(), it is not useful with async/await + +## [2.0.0-alpha.3] - 2019-12-07 + +### Changed + +- Migrate to tokio 0.2 + + +## [2.0.0-alpha.1] - 2019-11-22 + +### Changed + +- Migrated to `std::future` + +- Remove implementation of `Responder` for `()`. (#1167) + + +## [1.0.9] - 2019-11-14 + +### Added + +- Add `Payload::into_inner` method and make stored `def::Payload` public. (#1110) + +### Changed + +- Support `Host` guards when the `Host` header is unset (e.g. HTTP/2 requests) (#1129) + + +## [1.0.8] - 2019-09-25 + +### Added + +- Add `Scope::register_data` and `Resource::register_data` methods, parallel to + `App::register_data`. + +- Add `middleware::Condition` that conditionally enables another middleware + +- Allow to re-construct `ServiceRequest` from `HttpRequest` and `Payload` + +- Add `HttpServer::listen_uds` for ability to listen on UDS FD rather than path, + which is useful for example with systemd. + +### Changed + +- Make UrlEncodedError::Overflow more informative + +- Use actix-testing for testing utils + + +## [1.0.7] - 2019-08-29 + +### Fixed + +- Request Extensions leak #1062 + + +## [1.0.6] - 2019-08-28 + +### Added + +- Re-implement Host predicate (#989) + +- Form implements Responder, returning a `application/x-www-form-urlencoded` response + +- Add `into_inner` to `Data` + +- Add `test::TestRequest::set_form()` convenience method to automatically serialize data and set + the header in test requests. + +### Changed + +- `Query` payload made `pub`. Allows user to pattern-match the payload. + +- Enable `rust-tls` feature for client #1045 + +- Update serde_urlencoded to 0.6.1 + +- Update url to 2.1 + + +## [1.0.5] - 2019-07-18 + +### Added + +- Unix domain sockets (HttpServer::bind_uds) #92 + +- Actix now logs errors resulting in "internal server error" responses always, with the `error` + logging level + +### Fixed + +- Restored logging of errors through the `Logger` middleware + + +## [1.0.4] - 2019-07-17 + +### Added + +- Add `Responder` impl for `(T, StatusCode) where T: Responder` + +- Allow to access app's resource map via + `ServiceRequest::resource_map()` and `HttpRequest::resource_map()` methods. + +### Changed + +- Upgrade `rand` dependency version to 0.7 + + +## [1.0.3] - 2019-06-28 + +### Added + +- Support asynchronous data factories #850 + +### Changed + +- Use `encoding_rs` crate instead of unmaintained `encoding` crate + + +## [1.0.2] - 2019-06-17 + +### Changed + +- Move cors middleware to `actix-cors` crate. + +- Move identity middleware to `actix-identity` crate. + + +## [1.0.1] - 2019-06-17 + +### Added + +- Add support for PathConfig #903 + +- Add `middleware::identity::RequestIdentity` trait to `get_identity` from `HttpMessage`. + +### Changed + +- Move cors middleware to `actix-cors` crate. + +- Move identity middleware to `actix-identity` crate. + +- Disable default feature `secure-cookies`. + +- Allow to test an app that uses async actors #897 + +- Re-apply patch from #637 #894 + +### Fixed + +- HttpRequest::url_for is broken with nested scopes #915 + + +## [1.0.0] - 2019-06-05 + +### Added + +- Add `Scope::configure()` method. + +- Add `ServiceRequest::set_payload()` method. + +- Add `test::TestRequest::set_json()` convenience method to automatically + serialize data and set header in test requests. + +- Add macros for head, options, trace, connect and patch http methods + +### Changed + +- Drop an unnecessary `Option<_>` indirection around `ServerBuilder` from `HttpServer`. #863 + +### Fixed + +- Fix Logger request time format, and use rfc3339. #867 + +- Clear http requests pool on app service drop #860 + + +## [1.0.0-rc] - 2019-05-18 + +### Added + +- Add `Query::from_query()` to extract parameters from a query string. #846 +- `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors. + +### Changed + +- `JsonConfig` is now `Send + Sync`, this implies that `error_handler` must be `Send + Sync` too. + +### Fixed + +- Codegen with parameters in the path only resolves the first registered endpoint #841 + + +## [1.0.0-beta.4] - 2019-05-12 + +### Added + +- Allow to set/override app data on scope level + +### Changed + +- `App::configure` take an `FnOnce` instead of `Fn` +- Upgrade actix-net crates + + +## [1.0.0-beta.3] - 2019-05-04 + +### Added + +- Add helper function for executing futures `test::block_fn()` + +### Changed + +- Extractor configuration could be registered with `App::data()` + or with `Resource::data()` #775 + +- Route data is unified with app data, `Route::data()` moved to resource + level to `Resource::data()` + +- CORS handling without headers #702 + +- Allow constructing `Data` instances to avoid double `Arc` for `Send + Sync` types. + +### Fixed + +- Fix `NormalizePath` middleware impl #806 + +### Deleted + +- `App::data_factory()` is deleted. + + +## [1.0.0-beta.2] - 2019-04-24 + +### Added + +- Add raw services support via `web::service()` + +- Add helper functions for reading response body `test::read_body()` + +- Add support for `remainder match` (i.e "/path/{tail}*") + +- Extend `Responder` trait, allow to override status code and headers. + +- Store visit and login timestamp in the identity cookie #502 + +### Changed + +- `.to_async()` handler can return `Responder` type #792 + +### Fixed + +- Fix async web::Data factory handling + + +## [1.0.0-beta.1] - 2019-04-20 + +### Added + +- Add helper functions for reading test response body, + `test::read_response()` and test::read_response_json()` + +- Add `.peer_addr()` #744 + +- Add `NormalizePath` middleware + +### Changed + +- Rename `RouterConfig` to `ServiceConfig` + +- Rename `test::call_success` to `test::call_service` + +- Removed `ServiceRequest::from_parts()` as it is unsafe to create from parts. + +- `CookieIdentityPolicy::max_age()` accepts value in seconds + +### Fixed + +- Fixed `TestRequest::app_data()` + + +## [1.0.0-alpha.6] - 2019-04-14 + +### Changed + +- Allow using any service as default service. + +- Remove generic type for request payload, always use default. + +- Removed `Decompress` middleware. Bytes, String, Json, Form extractors + automatically decompress payload. + +- Make extractor config type explicit. Add `FromRequest::Config` associated type. + + +## [1.0.0-alpha.5] - 2019-04-12 + +### Added + +- Added async io `TestBuffer` for testing. + +### Deleted + +- Removed native-tls support + + +## [1.0.0-alpha.4] - 2019-04-08 + +### Added + +- `App::configure()` allow to offload app configuration to different methods + +- Added `URLPath` option for logger + +- Added `ServiceRequest::app_data()`, returns `Data` + +- Added `ServiceFromRequest::app_data()`, returns `Data` + +### Changed + +- `FromRequest` trait refactoring + +- Move multipart support to actix-multipart crate + +### Fixed + +- Fix body propagation in Response::from_error. #760 + + +## [1.0.0-alpha.3] - 2019-04-02 + +### Changed + +- Renamed `TestRequest::to_service()` to `TestRequest::to_srv_request()` + +- Renamed `TestRequest::to_response()` to `TestRequest::to_srv_response()` + +- Removed `Deref` impls + +### Removed + +- Removed unused `actix_web::web::md()` + + +## [1.0.0-alpha.2] - 2019-03-29 + +### Added + +- Rustls support + +### Changed + +- Use forked cookie + +- Multipart::Field renamed to MultipartField + +## [1.0.0-alpha.1] - 2019-03-28 + +### Changed + +- Complete architecture re-design. + +- Return 405 response if no matching route found within resource #538 diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml new file mode 100644 index 000000000..c58e1604b --- /dev/null +++ b/actix-web/Cargo.toml @@ -0,0 +1,153 @@ +[package] +name = "actix-web" +version = "4.1.0" +authors = [ + "Nikolay Kim ", + "Rob Ede ", +] +description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" +keywords = ["actix", "http", "web", "framework", "async"] +categories = [ + "network-programming", + "asynchronous", + "web-programming::http-server", + "web-programming::websocket" +] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-web.git" +license = "MIT OR Apache-2.0" +edition = "2018" + +[package.metadata.docs.rs] +# features that docs.rs will build with +features = ["macros", "openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"] +rustdoc-args = ["--cfg", "docsrs"] + +[lib] +name = "actix_web" +path = "src/lib.rs" + +[features] +default = ["macros", "compress-brotli", "compress-gzip", "compress-zstd", "cookies"] + +# Brotli algorithm content-encoding support +compress-brotli = ["actix-http/compress-brotli", "__compress"] +# Gzip and deflate algorithms content-encoding support +compress-gzip = ["actix-http/compress-gzip", "__compress"] +# Zstd algorithm content-encoding support +compress-zstd = ["actix-http/compress-zstd", "__compress"] + +# Routing and runtime proc macros +macros = [ + "actix-macros", + "actix-web-codegen", +] + +# Cookies support +cookies = ["cookie"] + +# Secure & signed cookies +secure-cookies = ["cookies", "cookie/secure"] + +# TLS via OpenSSL +openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] + +# TLS via Rustls +rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] + +# Internal (PRIVATE!) features used to aid testing and checking feature status. +# Don't rely on these whatsoever. They may disappear at anytime. +__compress = [] + +# io-uring feature only available for Linux OSes. +experimental-io-uring = ["actix-server/io-uring"] + +[dependencies] +actix-codec = "0.5" +actix-macros = { version = "0.2.3", optional = true } +actix-rt = { version = "2.6", default-features = false } +actix-server = "2" +actix-service = "2" +actix-utils = "3" +actix-tls = { version = "3", default-features = false, optional = true } + +actix-http = { version = "3", features = ["http2", "ws"] } +actix-router = "0.5" +actix-web-codegen = { version = "4", optional = true } + +ahash = "0.7" +bytes = "1" +bytestring = "1" +cfg-if = "1" +cookie = { version = "0.16", features = ["percent-encode"], optional = true } +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 = "1" +language-tags = "0.3" +once_cell = "1.5" +log = "0.4" +mime = "0.3" +pin-project-lite = "0.2.7" +regex = "1.5.5" +serde = "1.0" +serde_json = "1.0" +serde_urlencoded = "0.7" +smallvec = "1.6.1" +socket2 = "0.4.0" +time = { version = "0.3", default-features = false, features = ["formatting"] } +url = "2.1" + +[dev-dependencies] +actix-files = "0.6" +actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] } +awc = { version = "3", features = ["openssl"] } + +brotli = "3.3.3" +const-str = "0.4" +criterion = { version = "0.3", features = ["html_reports"] } +env_logger = "0.9" +flate2 = "1.0.13" +futures-util = { version = "0.3.7", default-features = false, features = ["std"] } +rand = "0.8" +rcgen = "0.9" +rustls-pemfile = "1" +serde = { version = "1.0", features = ["derive"] } +static_assertions = "1" +tls-openssl = { package = "openssl", version = "0.10.9" } +tls-rustls = { package = "rustls", version = "0.20.0" } +tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] } +zstd = "0.11" + +[[test]] +name = "test_server" +required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] + +[[test]] +name = "compression" +required-features = ["compress-brotli", "compress-gzip", "compress-zstd"] + +[[example]] +name = "basic" +required-features = ["compress-gzip"] + +[[example]] +name = "uds" +required-features = ["compress-gzip"] + +[[example]] +name = "on-connect" +required-features = [] + +[[bench]] +name = "server" +harness = false + +[[bench]] +name = "service" +harness = false + +[[bench]] +name = "responder" +harness = false diff --git a/actix-web/LICENSE-APACHE b/actix-web/LICENSE-APACHE new file mode 120000 index 000000000..965b606f3 --- /dev/null +++ b/actix-web/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/actix-web/LICENSE-MIT b/actix-web/LICENSE-MIT new file mode 120000 index 000000000..76219eb72 --- /dev/null +++ b/actix-web/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/actix-web/MIGRATION-0.x.md b/actix-web/MIGRATION-0.x.md new file mode 100644 index 000000000..1b60c36d1 --- /dev/null +++ b/actix-web/MIGRATION-0.x.md @@ -0,0 +1,198 @@ +# 0.7.15 + +- The `' '` character is not percent decoded anymore before matching routes. If you need to use it in + your routes, you should use `%20`. + +instead of + +```rust +fn main() { + let app = App::new().resource("/my index", |r| { + r.method(http::Method::GET) + .with(index); + }); +} +``` + +use + +```rust +fn main() { + let app = App::new().resource("/my%20index", |r| { + r.method(http::Method::GET) + .with(index); + }); +} +``` + +- If you used `AsyncResult::async` you need to replace it with `AsyncResult::future` + +# 0.7.4 + +- `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple + even for handler with one parameter. + +# 0.7 + +- `HttpRequest` does not implement `Stream` anymore. If you need to read request payload + use `HttpMessage::payload()` method. + +instead of + +```rust +fn index(req: HttpRequest) -> impl Responder { + req + .from_err() + .fold(...) + .... +} +``` + +use `.payload()` + +```rust +fn index(req: HttpRequest) -> impl Responder { + req + .payload() // <- get request payload stream + .from_err() + .fold(...) + .... +} +``` + +- [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html) + trait uses `&HttpRequest` instead of `&mut HttpRequest`. + +- Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead. + +instead of + +```rust +fn index(query: Query<..>, info: Json impl Responder {} +``` + +use tuple of extractors and use `.with()` for registration: + +```rust +fn index((query, json): (Query<..>, Json impl Responder {} +``` + +- `Handler::handle()` uses `&self` instead of `&mut self` + +- `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value + +- Removed deprecated `HttpServer::threads()`, use + [HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead. + +- Renamed `client::ClientConnectorError::Connector` to + `client::ClientConnectorError::Resolver` + +- `Route::with()` does not return `ExtractorConfig`, to configure + extractor use `Route::with_config()` + +instead of + +```rust +fn main() { + let app = App::new().resource("/index.html", |r| { + r.method(http::Method::GET) + .with(index) + .limit(4096); // <- limit size of the payload + }); +} +``` + +use + +```rust + +fn main() { + let app = App::new().resource("/index.html", |r| { + r.method(http::Method::GET) + .with_config(index, |cfg| { // <- register handler + cfg.limit(4096); // <- limit size of the payload + }) + }); +} +``` + +- `Route::with_async()` does not return `ExtractorConfig`, to configure + extractor use `Route::with_async_config()` + +# 0.6 + +- `Path` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest` + +- `ws::Message::Close` now includes optional close reason. + `ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed. + +- `HttpServer::threads()` renamed to `HttpServer::workers()`. + +- `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated. + Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead. + +- `HttpRequest::extensions()` returns read only reference to the request's Extension + `HttpRequest::extensions_mut()` returns mutable reference. + +- Instead of + + `use actix_web::middleware::{ CookieSessionBackend, CookieSessionError, RequestSession, Session, SessionBackend, SessionImpl, SessionStorage};` + + use `actix_web::middleware::session` + + `use actix_web::middleware::session{CookieSessionBackend, CookieSessionError, RequestSession, Session, SessionBackend, SessionImpl, SessionStorage};` + +- `FromRequest::from_request()` accepts mutable reference to a request + +- `FromRequest::Result` has to implement `Into>` + +- [`Responder::respond_to()`](https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to) + is generic over `S` + +- Use `Query` extractor instead of HttpRequest::query()`. + +```rust +fn index(q: Query>) -> Result<..> { + ... +} +``` + +or + +```rust +let q = Query::>::extract(req); +``` + +- Websocket operations are implemented as `WsWriter` trait. + you need to use `use actix_web::ws::WsWriter` + +# 0.5 + +- `HttpResponseBuilder::body()`, `.finish()`, `.json()` + methods return `HttpResponse` instead of `Result` + +- `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version` + moved to `actix_web::http` module + +- `actix_web::header` moved to `actix_web::http::header` + +- `NormalizePath` moved to `actix_web::http` module + +- `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function, + shortcut for `actix_web::server::HttpServer::new()` + +- `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself + +- `StaticFiles::new()`'s show_index parameter removed, use `show_files_listing()` method instead. + +- `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type + +- `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()` + functions should be used instead + +- `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>` + instead of `Result<_, http::Error>` + +- `Application` renamed to a `App` + +- `actix_web::Reply`, `actix_web::Resource` moved to `actix_web::dev` diff --git a/actix-web/MIGRATION-1.0.md b/actix-web/MIGRATION-1.0.md new file mode 100644 index 000000000..94c6321ac --- /dev/null +++ b/actix-web/MIGRATION-1.0.md @@ -0,0 +1,337 @@ +## 1.0.1 + +- Cors middleware has been moved to `actix-cors` crate + + instead of + + ```rust + use actix_web::middleware::cors::Cors; + ``` + + use + + ```rust + use actix_cors::Cors; + ``` + +- Identity middleware has been moved to `actix-identity` crate + + instead of + + ```rust + use actix_web::middleware::identity::{Identity, CookieIdentityPolicy, IdentityService}; + ``` + + use + + ```rust + use actix_identity::{Identity, CookieIdentityPolicy, IdentityService}; + ``` + +## 1.0.0 + +- Extractor configuration. In version 1.0 this is handled with the new `Data` mechanism for both setting and retrieving the configuration + + instead of + + ```rust + + #[derive(Default)] + struct ExtractorConfig { + config: String, + } + + impl FromRequest for YourExtractor { + type Config = ExtractorConfig; + type Result = Result; + + fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { + println!("use the config: {:?}", cfg.config); + ... + } + } + + App::new().resource("/route_with_config", |r| { + r.post().with_config(handler_fn, |cfg| { + cfg.0.config = "test".to_string(); + }) + }) + + ``` + + use the HttpRequest to get the configuration like any other `Data` with `req.app_data::()` and set it with the `data()` method on the `resource` + + ```rust + #[derive(Default)] + struct ExtractorConfig { + config: String, + } + + impl FromRequest for YourExtractor { + type Error = Error; + type Future = Result; + type Config = ExtractorConfig; + + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + let cfg = req.app_data::(); + println!("config data?: {:?}", cfg.unwrap().role); + ... + } + } + + App::new().service( + resource("/route_with_config") + .data(ExtractorConfig { + config: "test".to_string(), + }) + .route(post().to(handler_fn)), + ) + ``` + +- Resource registration. 1.0 version uses generalized resource + registration via `.service()` method. + + instead of + + ```rust + App.new().resource("/welcome", |r| r.f(welcome)) + ``` + + use App's or Scope's `.service()` method. `.service()` method accepts + object that implements `HttpServiceFactory` trait. By default + actix-web provides `Resource` and `Scope` services. + + ```rust + App.new().service( + web::resource("/welcome") + .route(web::get().to(welcome)) + .route(web::post().to(post_handler)) + ``` + +- Scope registration. + + instead of + + ```rust + let app = App::new().scope("/{project_id}", |scope| { + scope + .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) + .resource("/path2", |r| r.f(|_| HttpResponse::Ok())) + .resource("/path3", |r| r.f(|_| HttpResponse::MethodNotAllowed())) + }); + ``` + + use `.service()` for registration and `web::scope()` as scope object factory. + + ```rust + let app = App::new().service( + web::scope("/{project_id}") + .service(web::resource("/path1").to(|| HttpResponse::Ok())) + .service(web::resource("/path2").to(|| HttpResponse::Ok())) + .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed())) + ); + ``` + +- `.with()`, `.with_async()` registration methods have been renamed to `.to()` and `.to_async()`. + + instead of + + ```rust + App.new().resource("/welcome", |r| r.with(welcome)) + ``` + + use `.to()` or `.to_async()` methods + + ```rust + App.new().service(web::resource("/welcome").to(welcome)) + ``` + +- Passing arguments to handler with extractors, multiple arguments are allowed + + instead of + + ```rust + fn welcome((body, req): (Bytes, HttpRequest)) -> ... { + ... + } + ``` + + use multiple arguments + + ```rust + fn welcome(body: Bytes, req: HttpRequest) -> ... { + ... + } + ``` + +- `.f()`, `.a()` and `.h()` handler registration methods have been removed. + Use `.to()` for handlers and `.to_async()` for async handlers. Handler function + must use extractors. + + instead of + + ```rust + App.new().resource("/welcome", |r| r.f(welcome)) + ``` + + use App's `to()` or `to_async()` methods + + ```rust + App.new().service(web::resource("/welcome").to(welcome)) + ``` + +- `HttpRequest` does not provide access to request's payload stream. + + instead of + + ```rust + fn index(req: &HttpRequest) -> Box> { + req + .payload() + .from_err() + .fold((), |_, chunk| { + ... + }) + .map(|_| HttpResponse::Ok().finish()) + .responder() + } + ``` + + use `Payload` extractor + + ```rust + fn index(stream: web::Payload) -> impl Future { + stream + .from_err() + .fold((), |_, chunk| { + ... + }) + .map(|_| HttpResponse::Ok().finish()) + } + ``` + +- `State` is now `Data`. You register Data during the App initialization process + and then access it from handlers either using a Data extractor or using + HttpRequest's api. + + instead of + + ```rust + App.with_state(T) + ``` + + use App's `data` method + + ```rust + App.new() + .data(T) + ``` + + and either use the Data extractor within your handler + + ```rust + use actix_web::web::Data; + + fn endpoint_handler(Data)){ + ... + } + ``` + + .. or access your Data element from the HttpRequest + + ```rust + fn endpoint_handler(req: HttpRequest) { + let data: Option> = req.app_data::(); + } + ``` + +- AsyncResponder is removed, use `.to_async()` registration method and `impl Future<>` as result type. + + instead of + + ```rust + use actix_web::AsyncResponder; + + fn endpoint_handler(...) -> impl Future{ + ... + .responder() + } + ``` + + .. simply omit AsyncResponder and the corresponding responder() finish method + +- Middleware + + instead of + + ```rust + let app = App::new() + .middleware(middleware::Logger::default()) + ``` + + use `.wrap()` method + + ```rust + let app = App::new() + .wrap(middleware::Logger::default()) + .route("/index.html", web::get().to(index)); + ``` + +- `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()` + method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead. + + instead of + + ```rust + fn index(req: &HttpRequest) -> Responder { + req.body() + .and_then(|body| { + ... + }) + } + ``` + + use + + ```rust + fn index(body: Bytes) -> Responder { + ... + } + ``` + +- `actix_web::server` module has been removed. To start http server use `actix_web::HttpServer` type + +- StaticFiles and NamedFile have been moved to a separate crate. + + instead of `use actix_web::fs::StaticFile` + + use `use actix_files::Files` + + instead of `use actix_web::fs::Namedfile` + + use `use actix_files::NamedFile` + +- Multipart has been moved to a separate crate. + + instead of `use actix_web::multipart::Multipart` + + use `use actix_multipart::Multipart` + +- Response compression is not enabled by default. + To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`. + +- Session middleware moved to actix-session crate + +- Actors support have been moved to `actix-web-actors` crate + +- Custom Error + + Instead of error_response method alone, ResponseError now provides two methods: error_response and render_response respectively. Where, error_response creates the error response and render_response returns the error response to the caller. + + Simplest migration from 0.7 to 1.0 shall include below method to the custom implementation of ResponseError: + + ```rust + fn render_response(&self) -> HttpResponse { + self.error_response() + } + ``` diff --git a/actix-web/MIGRATION-2.0.md b/actix-web/MIGRATION-2.0.md new file mode 100644 index 000000000..0455062d1 --- /dev/null +++ b/actix-web/MIGRATION-2.0.md @@ -0,0 +1,48 @@ +# Migrating to 2.0.0 + +- `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to + `.await` on `run` method result, in that case it awaits server exit. + +- `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`. + Stored data is available via `HttpRequest::app_data()` method at runtime. + +- Extractor configuration must be registered with `App::app_data()` instead of `App::data()` + +- Sync handlers has been removed. `.to_async()` method has been renamed to `.to()` + replace `fn` with `async fn` to convert sync handler to async + +- `actix_http_test::TestServer` moved to `actix_web::test` module. To start + test server use `test::start()` or `test_start_with_config()` methods + +- `ResponseError` trait has been reafctored. `ResponseError::error_response()` renders + http response. + +- Feature `rust-tls` renamed to `rustls` + + instead of + + ```rust + actix-web = { version = "2.0.0", features = ["rust-tls"] } + ``` + + use + + ```rust + actix-web = { version = "2.0.0", features = ["rustls"] } + ``` + +- Feature `ssl` renamed to `openssl` + + instead of + + ```rust + actix-web = { version = "2.0.0", features = ["ssl"] } + ``` + + use + + ```rust + actix-web = { version = "2.0.0", features = ["openssl"] } + ``` + +- `Cors` builder now requires that you call `.finish()` to construct the middleware diff --git a/actix-web/MIGRATION-3.0.md b/actix-web/MIGRATION-3.0.md new file mode 100644 index 000000000..54bcd58bd --- /dev/null +++ b/actix-web/MIGRATION-3.0.md @@ -0,0 +1,53 @@ +# Migrating to 3.0.0 + +- The return type for `ServiceRequest::app_data::()` was changed from returning a `Data` to + simply a `T`. To access a `Data` use `ServiceRequest::app_data::>()`. + +- Cookie handling has been offloaded to the `cookie` crate: + + - `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs. + - Some types now require lifetime parameters. + +- The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects + any `actix-web` method previously expecting a time v0.1 input. + +- Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now + result in `SameSite=None` being sent with the response Set-Cookie header. + To create a cookie without a SameSite attribute, remove any calls setting same_site. + +- actix-http support for Actors messages was moved to actix-http crate and is enabled + with feature `actors` + +- content_length function is removed from actix-http. + You can set Content-Length by normally setting the response body or calling no_chunking function. + +- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a + `u64` instead of a `usize`. + +- Code that was using `path.` to access a `web::Path<(A, B, C)>`s elements now needs to use + destructuring or `.into_inner()`. For example: + + ```rust + // Previously: + async fn some_route(path: web::Path<(String, String)>) -> String { + format!("Hello, {} {}", path.0, path.1) + } + + // Now (this also worked before): + async fn some_route(path: web::Path<(String, String)>) -> String { + let (first_name, last_name) = path.into_inner(); + format!("Hello, {} {}", first_name, last_name) + } + // Or (this wasn't previously supported): + async fn some_route(web::Path((first_name, last_name)): web::Path<(String, String)>) -> String { + format!("Hello, {} {}", first_name, last_name) + } + ``` + +- `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one. + It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`, + or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`. + +- `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. + +- `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`. diff --git a/actix-web/MIGRATION-4.0.md b/actix-web/MIGRATION-4.0.md new file mode 100644 index 000000000..fbeae0680 --- /dev/null +++ b/actix-web/MIGRATION-4.0.md @@ -0,0 +1,507 @@ +# Migrating to 4.0.0 + +This guide walks you through the process of migrating from v3.x.y to v4.x.y. +If you are migrating to v4.x.y from an older version of Actix Web (v2.x.y or earlier), check out the other historical migration notes in this folder. + +This document is not designed to be exhaustive—it focuses on the most significant changes in v4. You can find an exhaustive changelog in the changelogs for [`actix-web`](./CHANGES.md#400---2022-02-25) and [`actix-http`](../actix-http/CHANGES.md#300---2022-02-25), complete with PR links. If you think there are any changes that deserve to be called out in this document, please open an issue or pull request. + +Headings marked with :warning: are **breaking behavioral changes**. They will probably not surface as compile-time errors though automated tests _might_ detect their effects on your app. + +## Table of Contents: + +- [MSRV](#msrv) +- [Tokio v1 Ecosystem](#tokio-v1-ecosystem) +- [Module Structure](#module-structure) +- [`NormalizePath` Middleware :warning:](#normalizepath-middleware-warning) +- [Server Settings :warning:](#server-settings-warning) +- [`FromRequest` Trait](#fromrequest-trait) +- [Compression Feature Flags](#compression-feature-flags) +- [`web::Path`](#webpath) +- [Rustls Crate Upgrade](#rustls-crate-upgrade) +- [Removed `awc` Client Re-export](#removed-awc-client-re-export) +- [Integration Testing Utils Moved To `actix-test`](#integration-testing-utils-moved-to-actix-test) +- [Header APIs](#header-apis) +- [Response Body Types](#response-body-types) +- [Middleware Trait APIs](#middleware-trait-apis) +- [`Responder` Trait](#responder-trait) +- [`App::data` Deprecation :warning:](#appdata-deprecation-warning) +- [Direct Dependency On `actix-rt` And `actix-service`](#direct-dependency-on-actix-rt-and-actix-service) +- [Server Must Be Polled :warning:](#server-must-be-polled-warning) +- [Guards API](#guards-api) +- [Returning `HttpResponse` synchronously](#returning-httpresponse-synchronously) +- [`#[actix_web::main]` and `#[tokio::main]`](#actix_webmain-and-tokiomain) +- [`web::block`](#webblock) +- + +## MSRV + +The MSRV of Actix Web has been raised from 1.42 to 1.54. + +## Tokio v1 Ecosystem + +Actix Web v4 is now underpinned by `tokio`'s v1 ecosystem. + +`cargo` supports having multiple versions of the same crate within the same dependency tree, but `tokio` v1 does not interoperate transparently with its previous versions (v0.2, v0.1). Some of your dependencies might rely on `tokio`, either directly or indirectly—if they are using an older version of `tokio`, check if an update is available. +The following command can help you to identify these dependencies: + +```sh +# Find all crates in your dependency tree that depend on `tokio` +# It also reports the different versions of `tokio` in your dependency tree. +cargo tree -i tokio + +# if you depend on multiple versions of tokio, use this command to +# list the dependencies relying on a specific version of tokio: +cargo tree -i tokio:0.2.25 +``` + +## Module Structure + +Lots of modules have been re-organized in this release. If a compile error refers to "item XYZ not found in module..." or "module XYZ not found", check the [documentation on docs.rs](https://docs.rs/actix-web) to search for items' new locations. + +## `NormalizePath` Middleware :warning: + +The default `NormalizePath` behavior now strips trailing slashes by default. This was the _documented_ behaviour in Actix Web v3, but the _actual_ behaviour differed. The discrepancy has now been resolved. + +As a consequence of this change, routes defined with trailing slashes will become inaccessible when using `NormalizePath::default()`. Calling `NormalizePath::default()` will log a warning. We suggest to use `new` or `trim`. + +```diff +- #[get("/test/")] ++ #[get("/test")] + async fn handler() { + + App::new() +- .wrap(NormalizePath::default()) ++ .wrap(NormalizePath::trim()) +``` + +Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`. + +## Server Settings :warning: + +Until Actix Web v4, the underlying `actix-server` crate used the number of available **logical** cores as the default number of worker threads. The new default is the number of [physical CPU cores available](https://github.com/actix/actix-net/commit/3a3d654c). For more information about this change, refer to [this analysis](https://github.com/actix/actix-web/issues/957). + +If you notice performance regressions, please open a new issue detailing your observations. + +## `FromRequest` Trait + +The associated type `Config` of `FromRequest` was removed. If you have custom extractors, you can just remove this implementation and refer to config types directly, if required. + +```diff + impl FromRequest for MyExtractor { +- type Config = (); + } +``` + +Consequently, the `FromRequest::configure` method was also removed. Config for extractors is still provided using `App::app_data` but should now be constructed in a standalone way. + +## Compression Feature Flags + +The `compress` feature flag has been split into more granular feature flags, one for each supported algorithm (brotli, gzip, zstd). By default, all compression algorithms are enabled. If you want to select specific compression codecs, the new flags are: + +- `compress-brotli` +- `compress-gzip` +- `compress-zstd` + +## `web::Path` + +The inner field for `web::Path` is now private. It was causing ambiguity when trying to use tuple indexing due to its `Deref` implementation. + +```diff +- async fn handler(web::Path((foo, bar)): web::Path<(String, String)>) { ++ async fn handler(params: web::Path<(String, String)>) { ++ let (foo, bar) = params.into_inner(); +``` + +An alternative [path param type with public field but no `Deref` impl is available in `actix-web-lab`](https://docs.rs/actix-web-lab/0.12.0/actix_web_lab/extract/struct.Path.html). + +## Rustls Crate Upgrade + +Actix Web now depends on version 0.20 of `rustls`. As a result, the server config builder has changed. [See the updated example project.](https://github.com/actix/examples/tree/master/https-tls/rustls/) + +## Removed `awc` Client Re-export + +Actix Web's sister crate `awc` is no longer re-exported through the `client` module. This allows `awc` to have its own release cadence—its breaking changes are no longer blocked by Actix Web's (more conservative) release schedule. + +```diff +- use actix_web::client::Client; ++ use awc::Client; +``` + +## Integration Testing Utils Moved To `actix-test` + +`TestServer` has been moved to its own crate, [`actix-test`](https://docs.rs/actix-test). + +```diff +- use use actix_web::test::start; ++ use use actix_test::start; +``` + +`TestServer` previously lived in `actix_web::test`, but it depends on `awc` which is no longer part of Actix Web's public API (see above). + +## Header APIs + +Header related APIs have been standardized across all `actix-*` crates. The terminology now better matches the underlying `HeaderMap` naming conventions. + +In short, "insert" always indicates that any existing headers with the same name are overridden, while "append" is used for adding with no removal (e.g. multi-valued headers). + +For request and response builder APIs, the new methods provide a unified interface for adding key-value pairs _and_ typed headers, which can often be more expressive. + +```diff +- .set_header("Api-Key", "1234") ++ .insert_header(("Api-Key", "1234")) + +- .header("Api-Key", "1234") ++ .append_header(("Api-Key", "1234")) + +- .set(ContentType::json()) ++ .insert_header(ContentType::json()) +``` + +We chose to deprecate most of the old methods instead of removing them immediately—the warning notes will guide you on how to update. + +## Response Body Types + +There have been a lot of changes to response body types. They are now more expressive and their purpose should be more intuitive. + +We have boosted the quality and completeness of the documentation for all items in the [`body` module](https://docs.rs/actix-web/4/actix_web/body). + +### `ResponseBody` + +`ResponseBody` is gone. Its purpose was confusing and has been replaced by better components. + +### `Body` + +`Body` is also gone. In combination with `ResponseBody`, the API it provided was sub-optimal and did not encourage expressive types. Here are the equivalents in the new system (check docs): + +- `Body::None` => `body::None::new()` +- `Body::Empty` => `()` / `web::Bytes::new()` +- `Body::Bytes` => `web::Bytes::from(...)` +- `Body::Message` => `.boxed()` / `BoxBody` + +### `BoxBody` + +`BoxBody` is a new type-erased body type. + +It can be useful when writing handlers, responders, and middleware when you want to trade a (very) small amount of performance for a simpler type. + +Creating a boxed body is done most efficiently by calling [`.boxed()`](https://docs.rs/actix-web/4/actix_web/body/trait.MessageBody.html#method.boxed) on a `MessageBody` type. + +### `EitherBody` + +`EitherBody` is a new "either" type that implements `MessageBody` + +It is particularly useful in middleware that can bail early, returning their own response plus body type. By default the "right" variant is `BoxBody` (i.e., `EitherBody` === `EitherBody`) but it can be anything that implements `MessageBody`. + +For example, it will be common among middleware which value performance of the hot path to use: + +```rust +type Response = Result>, Error> +``` + +This can be read (ignoring the `Result`) as "resolves with a `ServiceResponse` that is either the inner service's `B` body type or a boxed body type from elsewhere, likely constructed within the middleware itself". Of course, if your middleware contains only simple string other/error responses, it's possible to use them without boxes at the cost of a less simple implementation: + +```rust +type Response = Result>, Error> +``` + +### Error Handlers + +`ErrorHandlers` is a commonly used middleware that has changed in design slightly due to the other body type changes. + +In particular, an implicit `EitherBody` is used in the `ErrorHandlerResponse` type. An `ErrorHandlerResponse` now expects a `ServiceResponse>` to be returned within response variants. The following is a migration for an error handler that **only modifies** the response argument (left body). + +```diff + fn add_error_header(mut res: ServiceResponse) -> Result, Error> { + res.response_mut().headers_mut().insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("Error"), + ); +- Ok(ErrorHandlerResponse::Response(res)) ++ Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) + } +``` + +The following is a migration for an error handler that creates a new response instead (right body). + +```diff + fn error_handler(res: ServiceResponse) -> Result, Error> { +- let req = res.request().clone(); ++ let (req, _res) = res.into_parts(); + + let res = actix_files::NamedFile::open("./templates/404.html")? + .set_status_code(StatusCode::NOT_FOUND) +- .into_response(&req)? +- .into_body(); ++ .into_response(&req); + +- let res = ServiceResponse::new(req, res); ++ let res = ServiceResponse::new(req, res).map_into_right_body(); + Ok(ErrorHandlerResponse::Response(res)) + } +``` + +## Middleware Trait APIs + +The underlying traits that are used for creating middleware, `Service`, `ServiceFactory`, and `Transform`, have changed in design. + +- The associated `Request` type has moved to the type parameter position in order to allow multiple request implementations in other areas of the service stack. +- The `self` arguments in `Service` have changed from exclusive (mutable) borrows to shared (immutable) borrows. Since most service layers, such as middleware, do not host mutable state, it reduces the runtime overhead in places where a `RefCell` used to be required for wrapping an inner service. +- We've also introduced some macros that reduce boilerplate when implementing `poll_ready`. +- Further to the guidance on [response body types](#response-body-types), any use of the old methods on `ServiceResponse` designed to match up body types (e.g., the old `into_body` method), should be replaced with an explicit response body type utilizing `EitherBody`. + +A typical migration would look like this: + +```diff + use std::{ +- cell::RefCell, + future::Future, + pin::Pin, + rc::Rc, +- task::{Context, Poll}, + }; + + use actix_web::{ + dev::{Service, ServiceRequest, ServiceResponse, Transform}, + Error, + }; + use futures_util::future::{ok, LocalBoxFuture, Ready}; + + pub struct SayHi; + +- impl Transform for SayHi ++ impl Transform for SayHi + where +- S: Service, Error = Error>, ++ S: Service, Error = Error>, + S::Future: 'static, + B: 'static, + { +- type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type InitError = (); + type Transform = SayHiMiddleware; + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ok(SayHiMiddleware { +- service: Rc::new(RefCell::new(service)), ++ service: Rc::new(service), + }) + } + } + + pub struct SayHiMiddleware { +- service: Rc>, ++ service: Rc, + } + +- impl Service for SayHiMiddleware ++ impl Service for SayHiMiddleware + where +- S: Service, Error = Error>, ++ S: Service, Error = Error>, + S::Future: 'static, + B: 'static, + { +- type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + +- fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { +- self.service.poll_ready(cx) +- } ++ actix_web::dev::forward_ready!(service); + +- fn call(&mut self, req: ServiceRequest) -> Self::Future { ++ fn call(&self, req: ServiceRequest) -> Self::Future { + println!("Hi from start. You requested: {}", req.path()); + + let fut = self.service.call(req); + + Box::pin(async move { + let res = fut.await?; + + println!("Hi from response"); + Ok(res) + }) + } + } +``` + +This new design is forward-looking and should ease transition to traits that support the upcoming Generic Associated Type (GAT) feature in Rust while also trimming down the boilerplate required to implement middleware. + +We understand that creating middleware is still a pain point for Actix Web and we hope to provide [an even more ergonomic solution](https://docs.rs/actix-web-lab/0.11.0/actix_web_lab/middleware/fn.from_fn.html) in a v4.x release. + +## `Responder` Trait + +The `Responder` trait's interface has changed. Errors should be handled and converted to responses within the `respond_to` method. It's also no longer async so the associated `type Future` has been removed; there was no compelling use case found for it. These changes simplify the interface and implementation a lot. + +Now that more emphasis is placed on expressive body types, as explained in the [body types migration section](#response-body-types), this trait has introduced an associated `type Body`. The simplest migration will be to use `BoxBody` + `.map_into_boxed_body()` but if there is a more expressive type for your responder then try to use that instead. + +```diff + impl Responder for &'static str { +- type Error = Error; +- type Future = Ready>; ++ type Body = &'static str; + +- fn respond_to(self, req: &HttpRequest) -> Self::Future { ++ fn respond_to(self, req: &HttpRequest) -> HttpResponse { + let res = HttpResponse::build(StatusCode::OK) + .content_type("text/plain; charset=utf-8") + .body(self); + +- ok(res) ++ res + } + } +``` + +## `App::data` Deprecation :warning: + +The `App::data` method is deprecated. Replace instances of this with `App::app_data`. Exposing both methods led to lots of confusion when trying to extract the data in handlers. Now, when using the `Data` wrapper, the type you put in to `app_data` is the same type you extract in handler arguments. + +You may need to review the [guidance on shared mutable state](https://docs.rs/actix-web/4/actix_web/struct.App.html#shared-mutable-state) in order to migrate this correctly. + +```diff + use actix_web::web::Data; + + #[get("/")] + async fn handler(my_state: Data) -> { todo!() } + + HttpServer::new(|| { +- App::new() +- .data(MyState::default()) +- .service(hander) + ++ let my_state: Data = Data::new(MyState::default()); ++ ++ App::new() ++ .app_data(my_state) ++ .service(hander) + }) +``` + +## Direct Dependency On `actix-rt` And `actix-service` + +Improvements to module management and re-exports have resulted in not needing direct dependencies on these underlying crates for the vast majority of cases. In particular: + +- all traits necessary for creating middlewares are now re-exported through the `dev` modules; +- `#[actix_web::test]` now exists for async test definitions. + +Relying on these re-exports will ease the transition to future versions of Actix Web. + +```diff +- use actix_service::{Service, Transform}; ++ use actix_web::dev::{Service, Transform}; +``` + +```diff +- #[actix_rt::test] ++ #[actix_web::test] + async fn test_thing() { +``` + +## Server Must Be Polled :warning: + +In order to _start_ serving requests, the `Server` object returned from `run` **must** be `poll`ed, `await`ed, or `spawn`ed. This was done to prevent unexpected behavior and ensure that things like signal handlers are able to function correctly when enabled. + +For example, in this contrived example where the server is started and then the main thread is sent to sleep, the server will no longer be able to serve requests with v4.0: + +```rust +#[actix_web::main] +async fn main() { + HttpServer::new(|| App::new().default_service(web::to(HttpResponse::Conflict))) + .bind(("127.0.0.1", 8080)) + .unwrap() + .run(); + + thread::sleep(Duration::from_secs(1000)); +} +``` + +## Guards API + +Implementors of routing guards will need to use the modified interface of the `Guard` trait. The API is more flexible than before. See [guard module docs](https://docs.rs/actix-web/4/actix_web/guard/struct.GuardContext.html) for more details. + +```diff + struct MethodGuard(HttpMethod); + + impl Guard for MethodGuard { +- fn check(&self, request: &RequestHead) -> bool { ++ fn check(&self, ctx: &GuardContext<'_>) -> bool { +- request.method == self.0 ++ ctx.head().method == self.0 + } + } +``` + +## Returning `HttpResponse` synchronously + +The implementation of `Future` for `HttpResponse` was removed because it was largely useless for all but the simplest handlers like `web::to(|| HttpResponse::Ok().finish())`. It also caused false positives on the `async_yields_async` clippy lint in reasonable scenarios. The compiler errors will looks something like: + +``` +web::to(|| HttpResponse::Ok().finish()) +^^^^^^^ the trait `Handler<_>` is not implemented for `[closure@...]` +``` + +This form should be replaced with explicit async functions and closures: + +```diff +- fn handler() -> HttpResponse { ++ async fn handler() -> HttpResponse { + HttpResponse::Ok().finish() + } +``` + +```diff +- web::to(|| HttpResponse::Ok().finish()) ++ web::to(|| async { HttpResponse::Ok().finish() }) +``` + +Or, for these extremely simple cases, utilise an `HttpResponseBuilder`: + +```diff +- web::to(|| HttpResponse::Ok().finish()) ++ web::to(HttpResponse::Ok) +``` + +## `#[actix_web::main]` and `#[tokio::main]` + +Actix Web now works seamlessly with the primary way of starting a multi-threaded Tokio runtime, `#[tokio::main]`. Therefore, it is no longer necessary to spawn a thread when you need to run something alongside Actix Web that uses Tokio's multi-threaded mode; you can simply await the server within this context or, if preferred, use `tokio::spawn` just like any other async task. + +For now, `actix` actor support (and therefore WebSocket support via `actix-web-actors`) still requires `#[actix_web::main]` so that a `System` context is created. Designs are being created for an alternative WebSocket interface that does not require actors that should land sometime in the v4.x cycle. + +## `web::block` + +The `web::block` helper has changed return type from roughly `async fn(fn() -> Result) Result>` to `async fn(fn() -> T) Result`. That's to say that the blocking function can now return things that are not `Result`s and it does not wrap error types anymore. If you still need to return `Result`s then you'll likely want to use double `?` after the `.await`. + +```diff +- let n: u32 = web::block(|| Ok(123)).await?; ++ let n: u32 = web::block(|| 123).await?; + +- let n: u32 = web::block(|| Ok(123)).await?; ++ let n: u32 = web::block(|| Ok(123)).await??; +``` + +## `HttpResponse` as a `ResponseError` + +The implementation of `ResponseError` for `HttpResponse` has been removed. + +It was common in v3 to use `HttpResponse` as an error type in fallible handlers. The problem is that `HttpResponse` contains no knowledge or reference to the source error. Being able to guarantee that an "error" response actually contains an error reference makes middleware and other parts of Actix Web more effective. + +The error response builders in the `error` module were available in v3 but are now the best method for simple error responses without requiring you to implement the trait on your own custom error types. These builders can receive simple strings and third party errors that can not implement the `ResponseError` trait. + +A few common patterns are affected by this change: + +```diff +- Err(HttpResponse::InternalServerError().finish()) ++ Err(error::ErrorInternalServerError("reason")) + +- Err(HttpResponse::InternalServerError().body(third_party_error.to_string())) ++ Err(error::ErrorInternalServerError(err)) + +- .map_err(|err| HttpResponse::InternalServerError().finish())? ++ .map_err(error::ErrorInternalServerError)? +``` diff --git a/actix-web/README.md b/actix-web/README.md new file mode 100644 index 000000000..fdd4a8648 --- /dev/null +++ b/actix-web/README.md @@ -0,0 +1,108 @@ +
+

Actix Web

+

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

+

+ +[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.1.0)](https://docs.rs/actix-web/4.1.0) +![MSRV](https://img.shields.io/badge/rustc-1.57+-ab6000.svg) +![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) +[![Dependency Status](https://deps.rs/crate/actix-web/4.1.0/status.svg)](https://deps.rs/crate/actix-web/4.1.0) +
+[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) +![downloads](https://img.shields.io/crates/d/actix-web.svg) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) + +

+
+ +## Features + +- Supports _HTTP/1.x_ and _HTTP/2_ +- Streaming and pipelining +- Powerful [request routing](https://actix.rs/docs/url-dispatch/) with optional macros +- Full [Tokio](https://tokio.rs) compatibility +- Keep-alive and slow requests handling +- Client/server [WebSockets](https://actix.rs/docs/websockets/) support +- Transparent content compression/decompression (br, gzip, deflate, zstd) +- Multipart streams +- Static assets +- SSL support using OpenSSL or Rustls +- Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) +- Integrates with the [`awc` HTTP client](https://docs.rs/awc/) +- Runs on stable Rust 1.57+ + +## Documentation + +- [Website & User Guide](https://actix.rs) +- [Examples Repository](https://github.com/actix/examples) +- [API Documentation](https://docs.rs/actix-web) +- [API Documentation (master branch)](https://actix.rs/actix-web/actix_web) + +## Example + +Dependencies: + +```toml +[dependencies] +actix-web = "4" +``` + +Code: + +```rust +use actix_web::{get, web, App, HttpServer, Responder}; + +#[get("/hello/{name}")] +async fn greet(name: web::Path) -> impl Responder { + format!("Hello {name}!") +} + +#[actix_web::main] // or #[tokio::main] +async fn main() -> std::io::Result<()> { + HttpServer::new(|| { + App::new().service(greet) + }) + .bind(("127.0.0.1", 8080))? + .run() + .await +} +``` + +### More Examples + +- [Hello World](https://github.com/actix/examples/tree/master/basics/hello-world) +- [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics) +- [Application State](https://github.com/actix/examples/tree/master/basics/state) +- [JSON Handling](https://github.com/actix/examples/tree/master/json/json) +- [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart) +- [MongoDB Integration](https://github.com/actix/examples/tree/master/databases/mongodb) +- [Diesel Integration](https://github.com/actix/examples/tree/master/databases/diesel) +- [SQLite Integration](https://github.com/actix/examples/tree/master/databases/sqlite) +- [Postgres Integration](https://github.com/actix/examples/tree/master/databases/postgres) +- [Tera Templates](https://github.com/actix/examples/tree/master/templating/tera) +- [Askama Templates](https://github.com/actix/examples/tree/master/templating/askama) +- [HTTPS using Rustls](https://github.com/actix/examples/tree/master/https-tls/rustls) +- [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/https-tls/openssl) +- [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets) +- [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat) + +You may consider checking out [this directory](https://github.com/actix/examples/tree/master) for more examples. + +## Benchmarks + +One of the fastest web frameworks available according to the [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite). + +## License + +This project is licensed under either of the following licenses, at your option: + +- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0]) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT]) + +## Code of Conduct + +Contribution to the actix-web repo is organized under the terms of the Contributor Covenant. The Actix team promises to intervene to uphold that code of conduct. diff --git a/benches/responder.rs b/actix-web/benches/responder.rs similarity index 100% rename from benches/responder.rs rename to actix-web/benches/responder.rs diff --git a/benches/server.rs b/actix-web/benches/server.rs similarity index 100% rename from benches/server.rs rename to actix-web/benches/server.rs diff --git a/benches/service.rs b/actix-web/benches/service.rs similarity index 100% rename from benches/service.rs rename to actix-web/benches/service.rs diff --git a/actix-web/examples/README.md b/actix-web/examples/README.md new file mode 100644 index 000000000..163f67b46 --- /dev/null +++ b/actix-web/examples/README.md @@ -0,0 +1,3 @@ +# Actix Web Examples + +This folder contain just a few standalone code samples. There is a much larger registry of example projects [in the examples repo](https://github.com/actix/examples). diff --git a/examples/basic.rs b/actix-web/examples/basic.rs similarity index 100% rename from examples/basic.rs rename to actix-web/examples/basic.rs diff --git a/actix-web/examples/macroless.rs b/actix-web/examples/macroless.rs new file mode 100644 index 000000000..78ffd45c1 --- /dev/null +++ b/actix-web/examples/macroless.rs @@ -0,0 +1,21 @@ +use actix_web::{middleware, rt, web, App, HttpRequest, HttpServer}; + +async fn index(req: HttpRequest) -> &'static str { + println!("REQ: {:?}", req); + "Hello world!\r\n" +} + +fn main() -> std::io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + rt::System::new().block_on( + HttpServer::new(|| { + App::new() + .wrap(middleware::Logger::default()) + .service(web::resource("/").route(web::get().to(index))) + }) + .bind(("127.0.0.1", 8080))? + .workers(1) + .run(), + ) +} diff --git a/examples/on-connect.rs b/actix-web/examples/on-connect.rs similarity index 95% rename from examples/on-connect.rs rename to actix-web/examples/on-connect.rs index d76e9ce56..24c6f8418 100644 --- a/examples/on-connect.rs +++ b/actix-web/examples/on-connect.rs @@ -2,7 +2,7 @@ //! properties and pass them to a handler through request-local data. //! //! For an example of extracting a client TLS certificate, see: -//! +//! use std::{any::Any, io, net::SocketAddr}; diff --git a/examples/uds.rs b/actix-web/examples/uds.rs similarity index 100% rename from examples/uds.rs rename to actix-web/examples/uds.rs diff --git a/src/app.rs b/actix-web/src/app.rs similarity index 94% rename from src/app.rs rename to actix-web/src/app.rs index da33ebc4b..213c8beff 100644 --- a/src/app.rs +++ b/actix-web/src/app.rs @@ -21,8 +21,7 @@ use crate::{ }, }; -/// Application builder - structure that follows the builder pattern -/// for building application instances. +/// The top-level builder for an Actix Web application. pub struct App { endpoint: T, services: Vec>, @@ -61,7 +60,7 @@ where /// [`HttpRequest::app_data`](crate::HttpRequest::app_data) method at runtime. /// /// # [`Data`] - /// Any [`Data`] type added here can utilize it's extractor implementation in handlers. + /// Any [`Data`] type added here can utilize its extractor implementation in handlers. /// Types not wrapped in `Data` cannot use this extractor. See [its docs](Data) for more /// about its usage and patterns. /// @@ -186,10 +185,17 @@ where F: FnOnce(&mut ServiceConfig), { let mut cfg = ServiceConfig::new(); + f(&mut cfg); + self.services.extend(cfg.services); self.external.extend(cfg.external); self.extensions.extend(cfg.app_data); + + if let Some(default) = cfg.default { + self.default = Some(default); + } + self } @@ -236,10 +242,14 @@ where self } - /// Default service to be used if no matching resource could be found. + /// Default service that is invoked when no matching resource could be found. /// - /// It is possible to use services like `Resource`, `Route`. + /// You can use a [`Route`] as default service. /// + /// If a default service is not registered, an empty `404 Not Found` response will be sent to + /// the client instead. + /// + /// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// @@ -248,23 +258,8 @@ where /// } /// /// 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. - /// - /// ``` - /// use actix_web::{web, App, HttpResponse}; - /// - /// let app = App::new() - /// .service( - /// web::resource("/index.html").to(|| HttpResponse::Ok())) - /// .default_service( - /// web::to(|| HttpResponse::NotFound()) - /// ); + /// .service(web::resource("/index.html").route(web::get().to(index))) + /// .default_service(web::to(|| HttpResponse::NotFound())); /// ``` pub fn default_service(mut self, svc: F) -> Self where @@ -279,7 +274,6 @@ where { let svc = svc .into_factory() - .map(|res| res.map_into_boxed_body()) .map_init_err(|e| log::error!("Can not construct default service: {:?}", e)); self.default = Some(Rc::new(boxed::factory(svc))); @@ -302,12 +296,10 @@ where /// Ok(HttpResponse::Ok().into()) /// } /// - /// fn main() { - /// let app = App::new() - /// .service(web::resource("/index.html").route( - /// web::get().to(index))) - /// .external_resource("youtube", "https://youtube.com/watch/{video_id}"); - /// } + /// let app = App::new() + /// .service(web::resource("/index.html").route( + /// web::get().to(index))) + /// .external_resource("youtube", "https://youtube.com/watch/{video_id}"); /// ``` pub fn external_resource(mut self, name: N, url: U) -> Self where @@ -322,7 +314,7 @@ where /// Registers an app-wide middleware. /// - /// Registers middleware, in the form of a middleware compo nen t (type), that runs during + /// Registers middleware, in the form of a middleware component (type), that runs during /// inbound and/or outbound processing in the request life-cycle (request -> response), /// modifying request/response as necessary, across all requests managed by the `App`. /// diff --git a/src/app_service.rs b/actix-web/src/app_service.rs similarity index 94% rename from src/app_service.rs rename to actix-web/src/app_service.rs index edfb3e4a2..28ff8c614 100644 --- a/src/app_service.rs +++ b/actix-web/src/app_service.rs @@ -21,8 +21,6 @@ use crate::{ Error, HttpResponse, }; -type Guards = Vec>; - /// Service factory to convert `Request` to a `ServiceRequest`. /// /// It also executes data factories. @@ -72,7 +70,7 @@ where }))) }); - // App config + // create App config to pass to child services let mut config = AppService::new(config, default.clone()); // register services @@ -244,7 +242,7 @@ pub struct AppRoutingFactory { [( ResourceDef, BoxedHttpServiceFactory, - RefCell>, + RefCell>>>, )], >, default: Rc, @@ -259,10 +257,10 @@ impl ServiceFactory for AppRoutingFactory { type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { - // construct all services factory future with it's resource def and guards. + // construct all services factory future with its resource def and guards. let factory_fut = join_all(self.services.iter().map(|(path, factory, guards)| { let path = path.clone(); - let guards = guards.borrow_mut().take(); + let guards = guards.borrow_mut().take().unwrap_or_default(); let factory_fut = factory.new_service(()); async move { let service = factory_fut.await?; @@ -283,7 +281,7 @@ impl ServiceFactory for AppRoutingFactory { .collect::, _>>()? .drain(..) .fold(Router::build(), |mut router, (path, guards, service)| { - router.rdef(path, service).2 = guards; + router.push(path, service, guards); router }) .finish(); @@ -295,7 +293,7 @@ impl ServiceFactory for AppRoutingFactory { /// The Actix Web router default entry point. pub struct AppRouting { - router: Router, + router: Router>>, default: BoxedHttpService, } @@ -308,17 +306,8 @@ 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 { - let guard_ctx = req.guard_ctx(); - - for guard in guards { - if !guard.check(&guard_ctx) { - return false; - } - } - } - - true + let guard_ctx = req.guard_ctx(); + guards.iter().all(|guard| guard.check(&guard_ctx)) }); if let Some((srv, _info)) = res { diff --git a/src/config.rs b/actix-web/src/config.rs similarity index 73% rename from src/config.rs rename to actix-web/src/config.rs index 77fba18ed..58a099c75 100644 --- a/src/config.rs +++ b/actix-web/src/config.rs @@ -1,33 +1,32 @@ -use std::net::SocketAddr; -use std::rc::Rc; +use std::{net::SocketAddr, rc::Rc}; -use actix_http::Extensions; -use actix_router::ResourceDef; -use actix_service::{boxed, IntoServiceFactory, ServiceFactory}; +use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _}; -use crate::data::Data; -use crate::error::Error; -use crate::guard::Guard; -use crate::resource::Resource; -use crate::rmap::ResourceMap; -use crate::route::Route; -use crate::service::{ - AppServiceFactory, HttpServiceFactory, ServiceFactoryWrapper, ServiceRequest, - ServiceResponse, +use crate::{ + data::Data, + dev::{Extensions, ResourceDef}, + error::Error, + guard::Guard, + resource::Resource, + rmap::ResourceMap, + route::Route, + service::{ + AppServiceFactory, BoxedHttpServiceFactory, HttpServiceFactory, ServiceFactoryWrapper, + ServiceRequest, ServiceResponse, + }, }; type Guards = Vec>; -type HttpNewService = boxed::BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; /// Application configuration pub struct AppService { config: AppConfig, root: bool, - default: Rc, + default: Rc, #[allow(clippy::type_complexity)] services: Vec<( ResourceDef, - HttpNewService, + BoxedHttpServiceFactory, Option, Option>, )>, @@ -35,7 +34,7 @@ pub struct AppService { impl AppService { /// Crate server settings instance. - pub(crate) fn new(config: AppConfig, default: Rc) -> Self { + pub(crate) fn new(config: AppConfig, default: Rc) -> Self { AppService { config, default, @@ -56,7 +55,7 @@ impl AppService { AppConfig, Vec<( ResourceDef, - HttpNewService, + BoxedHttpServiceFactory, Option, Option>, )>, @@ -81,7 +80,7 @@ impl AppService { } /// Returns default handler factory. - pub fn default_service(&self) -> Rc { + pub fn default_service(&self) -> Rc { self.default.clone() } @@ -154,6 +153,16 @@ impl AppConfig { } impl Default for AppConfig { + /// Returns the default AppConfig. + /// Note: The included socket address is "127.0.0.1". + /// + /// 127.0.0.1: non-routable meta address that denotes an unknown, invalid or non-applicable target. + /// If you need a service only accessed by itself, use a loopback address. + /// A loopback address for IPv4 is any loopback address that begins with "127". + /// Loopback addresses should be only used to test your application locally. + /// The default configuration provides a loopback address. + /// + /// 0.0.0.0: if configured to use this special address, the application will listen to any IP address configured on the machine. fn default() -> Self { AppConfig::new( false, @@ -187,6 +196,7 @@ pub struct ServiceConfig { pub(crate) services: Vec>, pub(crate) external: Vec, pub(crate) app_data: Extensions, + pub(crate) default: Option>, } impl ServiceConfig { @@ -195,6 +205,7 @@ impl ServiceConfig { services: Vec::new(), external: Vec::new(), app_data: Extensions::new(), + default: None, } } @@ -215,6 +226,29 @@ impl ServiceConfig { self } + /// Default service to be used if no matching resource could be found. + /// + /// Counterpart to [`App::default_service()`](crate::App::default_service). + pub fn default_service(&mut self, f: F) -> &mut Self + where + F: IntoServiceFactory, + U: ServiceFactory< + ServiceRequest, + Config = (), + Response = ServiceResponse, + Error = Error, + > + 'static, + U::InitError: std::fmt::Debug, + { + let svc = f + .into_factory() + .map_init_err(|err| log::error!("Can not construct default service: {:?}", err)); + + self.default = Some(Rc::new(boxed::factory(svc))); + + self + } + /// Run external configuration as part of the application building process /// /// Counterpart to [`App::configure()`](crate::App::configure) that allows for easy nesting. @@ -322,6 +356,38 @@ mod tests { assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345")); } + #[actix_rt::test] + async fn registers_default_service() { + let srv = init_service( + App::new() + .configure(|cfg| { + cfg.default_service( + web::get().to(|| HttpResponse::NotFound().body("four oh four")), + ); + }) + .service(web::scope("/scoped").configure(|cfg| { + cfg.default_service( + web::get().to(|| HttpResponse::NotFound().body("scoped four oh four")), + ); + })), + ) + .await; + + // app registers default service + let req = TestRequest::with_uri("/path/i/did/not-configure").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + let body = read_body(resp).await; + assert_eq!(body, Bytes::from_static(b"four oh four")); + + // scope registers default service + let req = TestRequest::with_uri("/scoped/path/i/did/not-configure").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + let body = read_body(resp).await; + assert_eq!(body, Bytes::from_static(b"scoped four oh four")); + } + #[actix_rt::test] async fn test_service() { let srv = init_service(App::new().configure(|cfg| { diff --git a/src/data.rs b/actix-web/src/data.rs similarity index 98% rename from src/data.rs rename to actix-web/src/data.rs index ce7b1fee6..a689d13e0 100644 --- a/src/data.rs +++ b/actix-web/src/data.rs @@ -5,10 +5,7 @@ use actix_utils::future::{err, ok, Ready}; use futures_core::future::LocalBoxFuture; use serde::Serialize; -use crate::{ - dev::Payload, error::ErrorInternalServerError, extract::FromRequest, request::HttpRequest, - Error, -}; +use crate::{dev::Payload, error, Error, FromRequest, HttpRequest}; /// Data factory. pub(crate) trait DataFactory { @@ -160,7 +157,7 @@ impl FromRequest for Data { req.match_name().unwrap_or_else(|| req.path()) ); - err(ErrorInternalServerError( + err(error::ErrorInternalServerError( "Requested application data is not configured correctly. \ View/enable debug logs for more details.", )) diff --git a/src/dev.rs b/actix-web/src/dev.rs similarity index 92% rename from src/dev.rs rename to actix-web/src/dev.rs index def545ec7..5c7adfdaf 100644 --- a/src/dev.rs +++ b/actix-web/src/dev.rs @@ -2,6 +2,10 @@ //! //! 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. +//! +//! # Request Extractors +//! - [`ConnectionInfo`]: Connection information +//! - [`PeerAddr`]: Connection information pub use actix_http::{Extensions, Payload, RequestHead, Response, ResponseHead}; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; diff --git a/src/error/error.rs b/actix-web/src/error/error.rs similarity index 90% rename from src/error/error.rs rename to actix-web/src/error/error.rs index 8450bed35..3a5a128f6 100644 --- a/src/error/error.rs +++ b/actix-web/src/error/error.rs @@ -47,17 +47,10 @@ impl fmt::Debug for Error { impl StdError for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - // TODO: populate if replacement for Box is found None } } -impl From for Error { - fn from(val: std::convert::Infallible) -> Self { - match val {} - } -} - /// `Error` for any error that implements `ResponseError` impl From for Error { fn from(err: T) -> Error { diff --git a/src/error/internal.rs b/actix-web/src/error/internal.rs similarity index 100% rename from src/error/internal.rs rename to actix-web/src/error/internal.rs diff --git a/src/error/macros.rs b/actix-web/src/error/macros.rs similarity index 100% rename from src/error/macros.rs rename to actix-web/src/error/macros.rs diff --git a/src/error/mod.rs b/actix-web/src/error/mod.rs similarity index 95% rename from src/error/mod.rs rename to actix-web/src/error/mod.rs index 64df9f553..6095cd5d2 100644 --- a/src/error/mod.rs +++ b/actix-web/src/error/mod.rs @@ -6,7 +6,7 @@ // // See pub use actix_http::error::{ - BlockingError, ContentTypeError, DispatchError, HttpError, ParseError, PayloadError, + ContentTypeError, DispatchError, HttpError, ParseError, PayloadError, }; use derive_more::{Display, Error, From}; @@ -33,6 +33,14 @@ pub(crate) use macros::{downcast_dyn, downcast_get_type_id}; /// This type alias is generally used to avoid writing out `actix_http::Error` directly. pub type Result = std::result::Result; +/// An error representing a problem running a blocking task on a thread pool. +#[derive(Debug, Display, Error)] +#[display(fmt = "Blocking thread pool is shut down unexpectedly")] +#[non_exhaustive] +pub struct BlockingError; + +impl ResponseError for crate::error::BlockingError {} + /// Errors which can occur when attempting to generate resource uri. #[derive(Debug, PartialEq, Display, Error, From)] #[non_exhaustive] diff --git a/src/error/response_error.rs b/actix-web/src/error/response_error.rs similarity index 90% rename from src/error/response_error.rs rename to actix-web/src/error/response_error.rs index e0b4af44c..7d2c06154 100644 --- a/src/error/response_error.rs +++ b/actix-web/src/error/response_error.rs @@ -1,25 +1,28 @@ //! `ResponseError` trait and foreign impls. use std::{ + convert::Infallible, error::Error as StdError, fmt, io::{self, Write as _}, }; -use actix_http::{ - body::BoxBody, - header::{self, TryIntoHeaderValue}, - Response, StatusCode, -}; +use actix_http::Response; use bytes::BytesMut; use crate::{ + body::BoxBody, error::{downcast_dyn, downcast_get_type_id}, - helpers, HttpResponse, + helpers, + http::{ + header::{self, TryIntoHeaderValue}, + StatusCode, + }, + HttpResponse, }; /// Errors that can generate responses. -// TODO: add std::error::Error bound when replacement for Box is found +// TODO: flesh out documentation pub trait ResponseError: fmt::Debug + fmt::Display { /// Returns appropriate status code for error. /// @@ -52,6 +55,15 @@ downcast_dyn!(ResponseError); impl ResponseError for Box {} +impl ResponseError for Infallible { + fn status_code(&self) -> StatusCode { + match *self {} + } + fn error_response(&self) -> HttpResponse { + match *self {} + } +} + #[cfg(feature = "openssl")] impl ResponseError for actix_tls::accept::openssl::reexports::Error {} @@ -73,7 +85,6 @@ impl ResponseError for std::str::Utf8Error { impl ResponseError for std::io::Error { fn status_code(&self) -> StatusCode { - // TODO: decide if these errors should consider not found or permission errors match self.kind() { io::ErrorKind::NotFound => StatusCode::NOT_FOUND, io::ErrorKind::PermissionDenied => StatusCode::FORBIDDEN, @@ -86,7 +97,6 @@ impl ResponseError for actix_http::error::HttpError {} impl ResponseError for actix_http::Error { fn status_code(&self) -> StatusCode { - // TODO: map error kinds to status code better StatusCode::INTERNAL_SERVER_ERROR } @@ -107,8 +117,6 @@ impl ResponseError for actix_http::error::ParseError { } } -impl ResponseError for actix_http::error::BlockingError {} - impl ResponseError for actix_http::error::PayloadError { fn status_code(&self) -> StatusCode { match *self { diff --git a/src/extract.rs b/actix-web/src/extract.rs similarity index 91% rename from src/extract.rs rename to actix-web/src/extract.rs index f16c29ca5..d4f5cc91f 100644 --- a/src/extract.rs +++ b/actix-web/src/extract.rs @@ -18,9 +18,11 @@ use crate::{dev::Payload, Error, HttpRequest}; /// A type that implements [`FromRequest`] is called an **extractor** and can extract data from /// the request. Some types that implement this trait are: [`Json`], [`Header`], and [`Path`]. /// +/// Check out [`ServiceRequest::extract`](crate::dev::ServiceRequest::extract) if you want to +/// leverage extractors when implementing middlewares. +/// /// # Configuration /// An extractor can be customized by injecting the corresponding configuration with one of: -/// /// - [`App::app_data()`][crate::App::app_data] /// - [`Scope::app_data()`][crate::Scope::app_data] /// - [`Resource::app_data()`][crate::Resource::app_data] @@ -64,13 +66,29 @@ pub trait FromRequest: Sized { /// The associated error which can be returned. type Error: Into; - /// Future that resolves to a Self. + /// Future that resolves to a `Self`. + /// + /// To use an async function or block, the futures must be boxed. The following snippet will be + /// common when creating async/await extractors (that do not consume the body). + /// + /// ```ignore + /// type Future = Pin>>>; + /// // or + /// type Future = futures_util::future::LocalBoxFuture<'static, Result>; + /// + /// fn from_request(req: HttpRequest, ...) -> Self::Future { + /// let req = req.clone(); + /// Box::pin(async move { + /// ... + /// }) + /// } + /// ``` type Future: Future>; - /// Create a Self from request parts asynchronously. + /// Create a `Self` from request parts asynchronously. fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future; - /// Create a Self from request head asynchronously. + /// Create a `Self` from request head asynchronously. /// /// This method is short for `T::from_request(req, &mut Payload::None)`. fn extract(req: &HttpRequest) -> Self::Future { @@ -78,9 +96,9 @@ pub trait FromRequest: Sized { } } -/// Optionally extract a field from the request +/// Optionally extract from the request. /// -/// If the FromRequest for T fails, return None rather than returning an error response +/// If the inner `T::from_request` returns an error, handler will receive `None` instead. /// /// # Examples /// ``` @@ -118,12 +136,10 @@ pub trait FromRequest: Sized { /// } /// } /// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/users/:first").route( -/// web::post().to(index)) -/// ); -/// } +/// let app = App::new().service( +/// web::resource("/users/:first").route( +/// web::post().to(index)) +/// ); /// ``` impl FromRequest for Option where @@ -167,9 +183,10 @@ where } } -/// Optionally extract a field from the request or extract the Error if unsuccessful +/// Extract from the request, passing error type through to handler. /// -/// If the `FromRequest` for T fails, inject Err into handler rather than returning an error response +/// If the inner `T::from_request` returns an error, allow handler to receive the error rather than +/// immediately returning an error response. /// /// # Examples /// ``` @@ -205,11 +222,9 @@ where /// } /// } /// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/users/:first").route(web::post().to(index)) -/// ); -/// } +/// let app = App::new().service( +/// web::resource("/users/:first").route(web::post().to(index)) +/// ); /// ``` impl FromRequest for Result where diff --git a/src/guard.rs b/actix-web/src/guard.rs similarity index 99% rename from src/guard.rs rename to actix-web/src/guard.rs index 9f7514644..ef1301075 100644 --- a/src/guard.rs +++ b/actix-web/src/guard.rs @@ -254,7 +254,7 @@ impl Guard for AllGuard { } } -/// Wraps a guard and inverts the outcome of it's `Guard` implementation. +/// Wraps a guard and inverts the outcome of its `Guard` implementation. /// /// # Examples /// The handler below will be called for any request method apart from `GET`. @@ -459,7 +459,7 @@ impl Guard for HostGuard { return scheme == req_host_uri_scheme; } - // TODO: is the the correct behavior? + // TODO: is this the correct behavior? // falls through if scheme cannot be determined } diff --git a/src/handler.rs b/actix-web/src/handler.rs similarity index 94% rename from src/handler.rs rename to actix-web/src/handler.rs index 7eb70ed25..522a48b82 100644 --- a/src/handler.rs +++ b/actix-web/src/handler.rs @@ -10,12 +10,16 @@ use crate::{ /// The interface for request handlers. /// /// # What Is A Request Handler -/// A request handler has three requirements: +/// In short, a handler is just an async function that receives request-based arguments, in any +/// order, and returns something that can be converted to a response. +/// +/// In particular, a request handler has three requirements: /// 1. It is an async function (or a function/closure that returns an appropriate future); /// 1. The function parameters (up to 12) 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). /// +/// /// # Compiler Errors /// If you get the error `the trait Handler<_> is not implemented`, then your handler does not /// fulfill the _first_ of the above requirements. Missing other requirements manifest as errors on @@ -33,7 +37,7 @@ use crate::{ /// 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. +/// destructures the tuple into its component types and calls your handler function with them. /// /// In pseudo-code the process looks something like this: /// ```ignore diff --git a/src/helpers.rs b/actix-web/src/helpers.rs similarity index 100% rename from src/helpers.rs rename to actix-web/src/helpers.rs diff --git a/src/http/header/accept.rs b/actix-web/src/http/header/accept.rs similarity index 100% rename from src/http/header/accept.rs rename to actix-web/src/http/header/accept.rs diff --git a/src/http/header/accept_charset.rs b/actix-web/src/http/header/accept_charset.rs similarity index 100% rename from src/http/header/accept_charset.rs rename to actix-web/src/http/header/accept_charset.rs diff --git a/src/http/header/accept_encoding.rs b/actix-web/src/http/header/accept_encoding.rs similarity index 100% rename from src/http/header/accept_encoding.rs rename to actix-web/src/http/header/accept_encoding.rs diff --git a/src/http/header/accept_language.rs b/actix-web/src/http/header/accept_language.rs similarity index 100% rename from src/http/header/accept_language.rs rename to actix-web/src/http/header/accept_language.rs diff --git a/src/http/header/allow.rs b/actix-web/src/http/header/allow.rs similarity index 100% rename from src/http/header/allow.rs rename to actix-web/src/http/header/allow.rs diff --git a/src/http/header/any_or_some.rs b/actix-web/src/http/header/any_or_some.rs similarity index 100% rename from src/http/header/any_or_some.rs rename to actix-web/src/http/header/any_or_some.rs diff --git a/src/http/header/cache_control.rs b/actix-web/src/http/header/cache_control.rs similarity index 100% rename from src/http/header/cache_control.rs rename to actix-web/src/http/header/cache_control.rs diff --git a/src/http/header/content_disposition.rs b/actix-web/src/http/header/content_disposition.rs similarity index 100% rename from src/http/header/content_disposition.rs rename to actix-web/src/http/header/content_disposition.rs diff --git a/src/http/header/content_language.rs b/actix-web/src/http/header/content_language.rs similarity index 100% rename from src/http/header/content_language.rs rename to actix-web/src/http/header/content_language.rs diff --git a/src/http/header/content_range.rs b/actix-web/src/http/header/content_range.rs similarity index 100% rename from src/http/header/content_range.rs rename to actix-web/src/http/header/content_range.rs diff --git a/src/http/header/content_type.rs b/actix-web/src/http/header/content_type.rs similarity index 100% rename from src/http/header/content_type.rs rename to actix-web/src/http/header/content_type.rs diff --git a/src/http/header/date.rs b/actix-web/src/http/header/date.rs similarity index 100% rename from src/http/header/date.rs rename to actix-web/src/http/header/date.rs diff --git a/src/http/header/encoding.rs b/actix-web/src/http/header/encoding.rs similarity index 100% rename from src/http/header/encoding.rs rename to actix-web/src/http/header/encoding.rs diff --git a/src/http/header/entity.rs b/actix-web/src/http/header/entity.rs similarity index 100% rename from src/http/header/entity.rs rename to actix-web/src/http/header/entity.rs diff --git a/src/http/header/etag.rs b/actix-web/src/http/header/etag.rs similarity index 100% rename from src/http/header/etag.rs rename to actix-web/src/http/header/etag.rs diff --git a/src/http/header/expires.rs b/actix-web/src/http/header/expires.rs similarity index 100% rename from src/http/header/expires.rs rename to actix-web/src/http/header/expires.rs diff --git a/src/http/header/if_match.rs b/actix-web/src/http/header/if_match.rs similarity index 100% rename from src/http/header/if_match.rs rename to actix-web/src/http/header/if_match.rs diff --git a/src/http/header/if_modified_since.rs b/actix-web/src/http/header/if_modified_since.rs similarity index 100% rename from src/http/header/if_modified_since.rs rename to actix-web/src/http/header/if_modified_since.rs diff --git a/src/http/header/if_none_match.rs b/actix-web/src/http/header/if_none_match.rs similarity index 97% rename from src/http/header/if_none_match.rs rename to actix-web/src/http/header/if_none_match.rs index 863be70cf..86d7da9b2 100644 --- a/src/http/header/if_none_match.rs +++ b/actix-web/src/http/header/if_none_match.rs @@ -62,18 +62,18 @@ crate::http::header::common_header! { #[cfg(test)] mod tests { + use actix_http::test::TestRequest; + use super::IfNoneMatch; use crate::http::header::{EntityTag, Header, IF_NONE_MATCH}; - use actix_http::test::TestRequest; #[test] fn test_if_none_match() { - let mut if_none_match: Result; - let req = TestRequest::default() .insert_header((IF_NONE_MATCH, "*")) .finish(); - if_none_match = Header::parse(&req); + + let mut if_none_match = IfNoneMatch::parse(&req); assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any)); let req = TestRequest::default() diff --git a/src/http/header/if_range.rs b/actix-web/src/http/header/if_range.rs similarity index 100% rename from src/http/header/if_range.rs rename to actix-web/src/http/header/if_range.rs diff --git a/src/http/header/if_unmodified_since.rs b/actix-web/src/http/header/if_unmodified_since.rs similarity index 100% rename from src/http/header/if_unmodified_since.rs rename to actix-web/src/http/header/if_unmodified_since.rs diff --git a/src/http/header/last_modified.rs b/actix-web/src/http/header/last_modified.rs similarity index 100% rename from src/http/header/last_modified.rs rename to actix-web/src/http/header/last_modified.rs diff --git a/src/http/header/macros.rs b/actix-web/src/http/header/macros.rs similarity index 100% rename from src/http/header/macros.rs rename to actix-web/src/http/header/macros.rs diff --git a/src/http/header/mod.rs b/actix-web/src/http/header/mod.rs similarity index 100% rename from src/http/header/mod.rs rename to actix-web/src/http/header/mod.rs diff --git a/src/http/header/preference.rs b/actix-web/src/http/header/preference.rs similarity index 100% rename from src/http/header/preference.rs rename to actix-web/src/http/header/preference.rs diff --git a/src/http/header/range.rs b/actix-web/src/http/header/range.rs similarity index 100% rename from src/http/header/range.rs rename to actix-web/src/http/header/range.rs diff --git a/actix-web/src/http/mod.rs b/actix-web/src/http/mod.rs new file mode 100644 index 000000000..2866e1a2c --- /dev/null +++ b/actix-web/src/http/mod.rs @@ -0,0 +1,5 @@ +//! Various HTTP related types. + +pub mod header; + +pub use actix_http::{uri, ConnectionType, Error, KeepAlive, Method, StatusCode, Uri, Version}; diff --git a/src/info.rs b/actix-web/src/info.rs similarity index 99% rename from src/info.rs rename to actix-web/src/info.rs index ce1ef97c6..77b98110e 100644 --- a/src/info.rs +++ b/actix-web/src/info.rs @@ -159,7 +159,7 @@ impl ConnectionInfo { pub fn realip_remote_addr(&self) -> Option<&str> { self.realip_remote_addr .as_deref() - .or_else(|| self.peer_addr.as_deref()) + .or(self.peer_addr.as_deref()) } /// Returns serialized IP address of the peer connection. diff --git a/src/lib.rs b/actix-web/src/lib.rs similarity index 53% rename from src/lib.rs rename to actix-web/src/lib.rs index 18f0d581d..8d9e2dbcd 100644 --- a/src/lib.rs +++ b/actix-web/src/lib.rs @@ -4,18 +4,19 @@ //! ```no_run //! use actix_web::{get, web, App, HttpServer, Responder}; //! -//! #[get("/{id}/{name}/index.html")] -//! async fn index(path: web::Path<(u32, String)>) -> impl Responder { -//! let (id, name) = path.into_inner(); -//! format!("Hello {}! id:{}", name, id) +//! #[get("/hello/{name}")] +//! async fn greet(name: web::Path) -> impl Responder { +//! format!("Hello {}!", name) //! } //! -//! #[actix_web::main] +//! #[actix_web::main] // or #[tokio::main] //! async fn main() -> std::io::Result<()> { -//! HttpServer::new(|| App::new().service(index)) -//! .bind("127.0.0.1:8080")? -//! .run() -//! .await +//! HttpServer::new(|| { +//! App::new().service(greet) +//! }) +//! .bind(("127.0.0.1", 8080))? +//! .run() +//! .await //! } //! ``` //! @@ -42,32 +43,35 @@ //! and otherwise utilizing them. //! //! # Features -//! * Supports *HTTP/1.x* and *HTTP/2* -//! * Streaming and pipelining -//! * Keep-alive and slow requests handling -//! * Client/server [WebSockets](https://actix.rs/docs/websockets/) support -//! * Transparent content compression/decompression (br, gzip, deflate, zstd) -//! * Powerful [request routing](https://actix.rs/docs/url-dispatch/) -//! * Multipart streams -//! * Static assets -//! * SSL support using OpenSSL or Rustls -//! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) -//! * Includes an async [HTTP client](https://docs.rs/awc/) -//! * Runs on stable Rust 1.54+ +//! - Supports HTTP/1.x and HTTP/2 +//! - Streaming and pipelining +//! - Powerful [request routing](https://actix.rs/docs/url-dispatch/) with optional macros +//! - Full [Tokio](https://tokio.rs) compatibility +//! - Keep-alive and slow requests handling +//! - Client/server [WebSockets](https://actix.rs/docs/websockets/) support +//! - Transparent content compression/decompression (br, gzip, deflate, zstd) +//! - Multipart streams +//! - Static assets +//! - SSL support using OpenSSL or Rustls +//! - Middlewares ([Logger, Session, CORS, etc](middleware)) +//! - Integrates with the [`awc` HTTP client](https://docs.rs/awc/) +//! - Runs on stable Rust 1.54+ //! //! # Crate Features -//! * `cookies` - cookies support (enabled by default) -//! * `compress-brotli` - brotli content encoding compression support (enabled by default) -//! * `compress-gzip` - gzip and deflate content encoding compression support (enabled by default) -//! * `compress-zstd` - zstd content encoding compression support (enabled by default) -//! * `openssl` - HTTPS support via `openssl` crate, supports `HTTP/2` -//! * `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2` -//! * `secure-cookies` - secure cookies support +//! - `cookies` - cookies support (enabled by default) +//! - `macros` - routing and runtime macros (enabled by default) +//! - `compress-brotli` - brotli content encoding compression support (enabled by default) +//! - `compress-gzip` - gzip and deflate content encoding compression support (enabled by default) +//! - `compress-zstd` - zstd content encoding compression support (enabled by default) +//! - `openssl` - HTTPS support via `openssl` crate, supports `HTTP/2` +//! - `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2` +//! - `secure-cookies` - secure cookies support #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] +#![cfg_attr(docsrs, feature(doc_cfg))] mod app; mod app_service; @@ -88,6 +92,7 @@ mod resource; mod response; mod rmap; mod route; +pub mod rt; mod scope; mod server; mod service; @@ -95,15 +100,10 @@ pub mod test; pub(crate) mod types; pub mod web; -pub use actix_http::{body, HttpMessage}; -#[doc(inline)] -pub use actix_rt as rt; -pub use actix_web_codegen::*; -#[cfg(feature = "cookies")] -pub use cookie; - pub use crate::app::App; -pub use crate::error::{Error, ResponseError, Result}; +#[doc(inline)] +pub use crate::error::Result; +pub use crate::error::{Error, ResponseError}; pub use crate::extract::FromRequest; pub use crate::handler::Handler; pub use crate::request::HttpRequest; @@ -114,4 +114,33 @@ pub use crate::scope::Scope; pub use crate::server::HttpServer; pub use crate::types::Either; +pub use actix_http::{body, HttpMessage}; + +#[cfg(feature = "cookies")] +#[cfg_attr(docsrs, doc(cfg(feature = "cookies")))] +#[doc(inline)] +pub use cookie; + +macro_rules! codegen_reexport { + ($name:ident) => { + #[cfg(feature = "macros")] + #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] + pub use actix_web_codegen::$name; + }; +} + +codegen_reexport!(main); +codegen_reexport!(test); +codegen_reexport!(route); +codegen_reexport!(routes); +codegen_reexport!(head); +codegen_reexport!(get); +codegen_reexport!(post); +codegen_reexport!(patch); +codegen_reexport!(put); +codegen_reexport!(delete); +codegen_reexport!(trace); +codegen_reexport!(connect); +codegen_reexport!(options); + pub(crate) type BoxError = Box; diff --git a/actix-web/src/middleware/authors-guide.md b/actix-web/src/middleware/authors-guide.md new file mode 100644 index 000000000..a8d1edea4 --- /dev/null +++ b/actix-web/src/middleware/authors-guide.md @@ -0,0 +1,16 @@ +# Middleware Author's Guide + +## What Is A Middleware? + +## Middleware Traits + +## Understanding Body Types + +## Best Practices + +## Error Propagation + +## When To (Not) Use Middleware + +## Author's References +- `EitherBody` + when is middleware appropriate: https://discord.com/channels/771444961383153695/952016890723729428 diff --git a/src/middleware/compat.rs b/actix-web/src/middleware/compat.rs similarity index 100% rename from src/middleware/compat.rs rename to actix-web/src/middleware/compat.rs diff --git a/src/middleware/compress.rs b/actix-web/src/middleware/compress.rs similarity index 91% rename from src/middleware/compress.rs rename to actix-web/src/middleware/compress.rs index 4fdd74779..ed4291bed 100644 --- a/src/middleware/compress.rs +++ b/actix-web/src/middleware/compress.rs @@ -251,6 +251,8 @@ static SUPPORTED_ENCODINGS: Lazy> = Lazy::new(|| { #[cfg(feature = "compress-gzip")] #[cfg(test)] mod tests { + use std::collections::HashSet; + use super::*; use crate::{middleware::DefaultHeaders, test, web, App}; @@ -305,4 +307,27 @@ mod tests { let bytes = test::read_body(res).await; assert_eq!(gzip_decode(bytes), DATA.as_bytes()); } + + #[actix_rt::test] + async fn retains_previously_set_vary_header() { + let app = test::init_service({ + App::new() + .wrap(Compress::default()) + .default_service(web::to(move || { + HttpResponse::Ok() + .insert_header((header::VARY, "x-test")) + .finish() + })) + }) + .await; + + let req = test::TestRequest::default() + .insert_header((header::ACCEPT_ENCODING, "gzip")) + .to_request(); + let res = test::call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::OK); + let vary_headers = res.headers().get_all(header::VARY).collect::>(); + assert!(vary_headers.contains(&HeaderValue::from_static("x-test"))); + assert!(vary_headers.contains(&HeaderValue::from_static("accept-encoding"))); + } } diff --git a/actix-web/src/middleware/condition.rs b/actix-web/src/middleware/condition.rs new file mode 100644 index 000000000..65f25a67c --- /dev/null +++ b/actix-web/src/middleware/condition.rs @@ -0,0 +1,207 @@ +//! For middleware documentation, see [`Condition`]. + +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use futures_core::{future::LocalBoxFuture, ready}; +use futures_util::future::FutureExt as _; +use pin_project_lite::pin_project; + +use crate::{ + body::EitherBody, + dev::{Service, ServiceResponse, Transform}, +}; + +/// Middleware for conditionally enabling other middleware. +/// +/// # Examples +/// ``` +/// use actix_web::middleware::{Condition, NormalizePath}; +/// use actix_web::App; +/// +/// let enable_normalize = std::env::var("NORMALIZE_PATH").is_ok(); +/// let app = App::new() +/// .wrap(Condition::new(enable_normalize, NormalizePath::default())); +/// ``` +pub struct Condition { + transformer: T, + enable: bool, +} + +impl Condition { + pub fn new(enable: bool, transformer: T) -> Self { + Self { + transformer, + enable, + } + } +} + +impl Transform for Condition +where + S: Service, Error = Err> + 'static, + T: Transform, Error = Err>, + T::Future: 'static, + T::InitError: 'static, + T::Transform: 'static, +{ + type Response = ServiceResponse>; + type Error = Err; + type Transform = ConditionMiddleware; + type InitError = T::InitError; + type Future = LocalBoxFuture<'static, Result>; + + fn new_transform(&self, service: S) -> Self::Future { + if self.enable { + let fut = self.transformer.new_transform(service); + async move { + let wrapped_svc = fut.await?; + Ok(ConditionMiddleware::Enable(wrapped_svc)) + } + .boxed_local() + } else { + async move { Ok(ConditionMiddleware::Disable(service)) }.boxed_local() + } + } +} + +pub enum ConditionMiddleware { + Enable(E), + Disable(D), +} + +impl Service for ConditionMiddleware +where + E: Service, Error = Err>, + D: Service, Error = Err>, +{ + type Response = ServiceResponse>; + type Error = Err; + type Future = ConditionMiddlewareFuture; + + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { + match self { + ConditionMiddleware::Enable(service) => service.poll_ready(cx), + ConditionMiddleware::Disable(service) => service.poll_ready(cx), + } + } + + fn call(&self, req: Req) -> Self::Future { + match self { + ConditionMiddleware::Enable(service) => ConditionMiddlewareFuture::Enabled { + fut: service.call(req), + }, + ConditionMiddleware::Disable(service) => ConditionMiddlewareFuture::Disabled { + fut: service.call(req), + }, + } + } +} + +pin_project! { + #[doc(hidden)] + #[project = ConditionProj] + pub enum ConditionMiddlewareFuture { + Enabled { #[pin] fut: E, }, + Disabled { #[pin] fut: D, }, + } +} + +impl Future for ConditionMiddlewareFuture +where + E: Future, Err>>, + D: Future, Err>>, +{ + type Output = Result>, Err>; + + #[inline] + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let res = match self.project() { + ConditionProj::Enabled { fut } => ready!(fut.poll(cx))?.map_into_left_body(), + ConditionProj::Disabled { fut } => ready!(fut.poll(cx))?.map_into_right_body(), + }; + + Poll::Ready(Ok(res)) + } +} + +#[cfg(test)] +mod tests { + use actix_service::IntoService as _; + + use super::*; + use crate::{ + body::BoxBody, + dev::{ServiceRequest, ServiceResponse}, + error::Result, + http::{ + header::{HeaderValue, CONTENT_TYPE}, + StatusCode, + }, + middleware::{self, ErrorHandlerResponse, ErrorHandlers}, + test::{self, TestRequest}, + web::Bytes, + HttpResponse, + }; + + #[allow(clippy::unnecessary_wraps)] + fn render_500(mut res: ServiceResponse) -> Result> { + res.response_mut() + .headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); + + Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) + } + + #[test] + fn compat_with_builtin_middleware() { + let _ = Condition::new(true, middleware::Compat::noop()); + let _ = Condition::new(true, middleware::Logger::default()); + let _ = Condition::new(true, middleware::Compress::default()); + let _ = Condition::new(true, middleware::NormalizePath::trim()); + let _ = Condition::new(true, middleware::DefaultHeaders::new()); + let _ = Condition::new(true, middleware::ErrorHandlers::::new()); + let _ = Condition::new(true, middleware::ErrorHandlers::::new()); + } + + #[actix_rt::test] + async fn test_handler_enabled() { + let srv = |req: ServiceRequest| async move { + let resp = HttpResponse::InternalServerError().message_body(String::new())?; + Ok(req.into_response(resp)) + }; + + let mw = ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); + + let mw = Condition::new(true, mw) + .new_transform(srv.into_service()) + .await + .unwrap(); + + let resp: ServiceResponse, String>> = + test::call_service(&mw, TestRequest::default().to_srv_request()).await; + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); + } + + #[actix_rt::test] + async fn test_handler_disabled() { + let srv = |req: ServiceRequest| async move { + let resp = HttpResponse::InternalServerError().message_body(String::new())?; + Ok(req.into_response(resp)) + }; + + let mw = ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); + + let mw = Condition::new(false, mw) + .new_transform(srv.into_service()) + .await + .unwrap(); + + let resp: ServiceResponse, String>> = + test::call_service(&mw, TestRequest::default().to_srv_request()).await; + assert_eq!(resp.headers().get(CONTENT_TYPE), None); + } +} diff --git a/src/middleware/default_headers.rs b/actix-web/src/middleware/default_headers.rs similarity index 100% rename from src/middleware/default_headers.rs rename to actix-web/src/middleware/default_headers.rs diff --git a/src/middleware/err_handlers.rs b/actix-web/src/middleware/err_handlers.rs similarity index 87% rename from src/middleware/err_handlers.rs rename to actix-web/src/middleware/err_handlers.rs index bde054330..f74220cd2 100644 --- a/src/middleware/err_handlers.rs +++ b/actix-web/src/middleware/err_handlers.rs @@ -185,6 +185,7 @@ mod tests { use super::*; use crate::{ + body, http::{ header::{HeaderValue, CONTENT_TYPE}, StatusCode, @@ -203,7 +204,7 @@ mod tests { Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) } - let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR); + let srv = test::status_service(StatusCode::INTERNAL_SERVER_ERROR); let mw = ErrorHandlers::new() .handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler) @@ -230,7 +231,7 @@ mod tests { )) } - let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR); + let srv = test::status_service(StatusCode::INTERNAL_SERVER_ERROR); let mw = ErrorHandlers::new() .handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler) @@ -245,9 +246,7 @@ mod tests { #[actix_rt::test] async fn changes_body_type() { #[allow(clippy::unnecessary_wraps)] - fn error_handler( - res: ServiceResponse, - ) -> Result> { + fn error_handler(res: ServiceResponse) -> Result> { let (req, res) = res.into_parts(); let res = res.set_body(Bytes::from("sorry, that's no bueno")); @@ -258,7 +257,7 @@ mod tests { Ok(ErrorHandlerResponse::Response(res)) } - let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR); + let srv = test::status_service(StatusCode::INTERNAL_SERVER_ERROR); let mw = ErrorHandlers::new() .handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler) @@ -270,5 +269,33 @@ mod tests { assert_eq!(test::read_body(res).await, "sorry, that's no bueno"); } - // TODO: test where error is thrown + #[actix_rt::test] + async fn error_thrown() { + #[allow(clippy::unnecessary_wraps)] + fn error_handler(_res: ServiceResponse) -> Result> { + Err(crate::error::ErrorInternalServerError( + "error in error handler", + )) + } + + let srv = test::status_service(StatusCode::BAD_REQUEST); + + let mw = ErrorHandlers::new() + .handler(StatusCode::BAD_REQUEST, error_handler) + .new_transform(srv.into_service()) + .await + .unwrap(); + + let err = mw + .call(TestRequest::default().to_srv_request()) + .await + .unwrap_err(); + let res = err.error_response(); + + assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!( + body::to_bytes(res.into_body()).await.unwrap(), + "error in error handler" + ); + } } diff --git a/src/middleware/logger.rs b/actix-web/src/middleware/logger.rs similarity index 100% rename from src/middleware/logger.rs rename to actix-web/src/middleware/logger.rs diff --git a/src/middleware/mod.rs b/actix-web/src/middleware/mod.rs similarity index 100% rename from src/middleware/mod.rs rename to actix-web/src/middleware/mod.rs diff --git a/src/middleware/noop.rs b/actix-web/src/middleware/noop.rs similarity index 100% rename from src/middleware/noop.rs rename to actix-web/src/middleware/noop.rs diff --git a/src/middleware/normalize.rs b/actix-web/src/middleware/normalize.rs similarity index 100% rename from src/middleware/normalize.rs rename to actix-web/src/middleware/normalize.rs diff --git a/src/request.rs b/actix-web/src/request.rs similarity index 98% rename from src/request.rs rename to actix-web/src/request.rs index f04e551d4..d26488e2b 100644 --- a/src/request.rs +++ b/actix-web/src/request.rs @@ -381,12 +381,16 @@ impl Drop for HttpRequest { inner.app_data.truncate(1); // Inner is borrowed mut here and; get req data mutably to reduce borrow check. Also - // we know the req_data Rc will not have any cloned at this point to unwrap is okay. + // we know the req_data Rc will not have any clones at this point to unwrap is okay. Rc::get_mut(&mut inner.extensions) .unwrap() .get_mut() .clear(); + // We can't use the same trick as req data because the conn_data is held by the + // dispatcher, too. + inner.conn_data = None; + // a re-borrow of pool is necessary here. let req = Rc::clone(&self.inner); self.app_state().pool().push(req); @@ -407,12 +411,10 @@ impl Drop for HttpRequest { /// format!("Got thing: {:?}", req) /// } /// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/users/{first}").route( -/// web::get().to(index)) -/// ); -/// } +/// let app = App::new().service( +/// web::resource("/users/{first}").route( +/// web::get().to(index)) +/// ); /// ``` impl FromRequest for HttpRequest { type Error = Error; @@ -763,10 +765,8 @@ mod tests { assert_eq!(body, Bytes::from_static(b"1")); } - // allow deprecated App::data - #[allow(deprecated)] #[actix_rt::test] - async fn test_extensions_dropped() { + async fn test_app_data_dropped() { struct Tracker { pub dropped: bool, } @@ -782,7 +782,7 @@ mod tests { let tracker = Rc::new(RefCell::new(Tracker { dropped: false })); { let tracker2 = Rc::clone(&tracker); - let srv = init_service(App::new().data(10u32).service(web::resource("/").to( + let srv = init_service(App::new().service(web::resource("/").to( move |req: HttpRequest| { req.extensions_mut().insert(Foo { tracker: Rc::clone(&tracker2), diff --git a/src/request_data.rs b/actix-web/src/request_data.rs similarity index 100% rename from src/request_data.rs rename to actix-web/src/request_data.rs diff --git a/src/resource.rs b/actix-web/src/resource.rs similarity index 96% rename from src/resource.rs rename to actix-web/src/resource.rs index a0fc19faf..c5c6701e6 100644 --- a/src/resource.rs +++ b/actix-web/src/resource.rs @@ -93,19 +93,17 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new() - /// .service( - /// web::resource("/app") - /// .guard(guard::Header("content-type", "text/plain")) - /// .route(web::get().to(index)) - /// ) - /// .service( - /// web::resource("/app") - /// .guard(guard::Header("content-type", "text/json")) - /// .route(web::get().to(|| HttpResponse::MethodNotAllowed())) - /// ); - /// } + /// let app = App::new() + /// .service( + /// web::resource("/app") + /// .guard(guard::Header("content-type", "text/plain")) + /// .route(web::get().to(index)) + /// ) + /// .service( + /// web::resource("/app") + /// .guard(guard::Header("content-type", "text/json")) + /// .route(web::get().to(|| HttpResponse::MethodNotAllowed())) + /// ); /// ``` pub fn guard(mut self, guard: G) -> Self { self.guards.push(Box::new(guard)); @@ -137,14 +135,13 @@ where /// ``` /// use actix_web::{web, guard, App}; /// - /// fn main() { - /// let app = App::new().service( - /// web::resource("/container/") - /// .route(web::get().to(get_handler)) - /// .route(web::post().to(post_handler)) - /// .route(web::delete().to(delete_handler)) - /// ); - /// } + /// let app = App::new().service( + /// web::resource("/container/") + /// .route(web::get().to(get_handler)) + /// .route(web::post().to(post_handler)) + /// .route(web::delete().to(delete_handler)) + /// ); + /// /// # async fn get_handler() -> impl actix_web::Responder { actix_web::HttpResponse::Ok() } /// # async fn post_handler() -> impl actix_web::Responder { actix_web::HttpResponse::Ok() } /// # async fn delete_handler() -> impl actix_web::Responder { actix_web::HttpResponse::Ok() } @@ -313,8 +310,12 @@ where } /// Default service to be used if no matching route could be found. - /// By default *405* response get returned. Resource does not use - /// default handler from `App` or `Scope`. + /// + /// You can use a [`Route`] as default service. + /// + /// If a default service is not registered, an empty `405 Method Not Allowed` response will be + /// sent to the client instead. Unlike [`Scope`](crate::Scope)s, a [`Resource`] does **not** + /// inherit its parent's default service. pub fn default_service(mut self, f: F) -> Self where F: IntoServiceFactory, diff --git a/src/response/builder.rs b/actix-web/src/response/builder.rs similarity index 100% rename from src/response/builder.rs rename to actix-web/src/response/builder.rs diff --git a/src/response/customize_responder.rs b/actix-web/src/response/customize_responder.rs similarity index 98% rename from src/response/customize_responder.rs rename to actix-web/src/response/customize_responder.rs index 8cb146dda..f6f4b9236 100644 --- a/src/response/customize_responder.rs +++ b/actix-web/src/response/customize_responder.rs @@ -7,7 +7,7 @@ use crate::{HttpRequest, HttpResponse, Responder}; /// Allows overriding status code and headers for a [`Responder`]. /// -/// Created by the [`Responder::customize`] method. +/// Created by calling the [`customize`](Responder::customize) method on a [`Responder`] type. pub struct CustomizeResponder { inner: CustomizeResponderInner, error: Option, diff --git a/src/response/http_codes.rs b/actix-web/src/response/http_codes.rs similarity index 100% rename from src/response/http_codes.rs rename to actix-web/src/response/http_codes.rs diff --git a/src/response/mod.rs b/actix-web/src/response/mod.rs similarity index 100% rename from src/response/mod.rs rename to actix-web/src/response/mod.rs diff --git a/src/response/responder.rs b/actix-web/src/response/responder.rs similarity index 81% rename from src/response/responder.rs rename to actix-web/src/response/responder.rs index d1b9e49e0..da8091981 100644 --- a/src/response/responder.rs +++ b/actix-web/src/response/responder.rs @@ -7,14 +7,35 @@ use actix_http::{ }; use bytes::{Bytes, BytesMut}; -use crate::{Error, HttpRequest, HttpResponse}; - use super::CustomizeResponder; +use crate::{Error, HttpRequest, HttpResponse}; /// Trait implemented by types that can be converted to an HTTP response. /// -/// Any types that implement this trait can be used in the return type of a handler. -// # TODO: more about implementation notes and foreign impls +/// Any types that implement this trait can be used in the return type of a handler. Since handlers +/// will only have one return type, it is idiomatic to use opaque return types `-> impl Responder`. +/// +/// # Implementations +/// It is often not required to implement `Responder` for your own types due to a broad base of +/// built-in implementations: +/// - `HttpResponse` and `HttpResponseBuilder` +/// - `Option` where `R: Responder` +/// - `Result` where `R: Responder` and [`E: ResponseError`](crate::ResponseError) +/// - `(R, StatusCode) where `R: Responder` +/// - `&'static str`, `String`, `&'_ String`, `Cow<'_, str>`, [`ByteString`](bytestring::ByteString) +/// - `&'static [u8]`, `Vec`, `Bytes`, `BytesMut` +/// - [`Json`](crate::web::Json) and [`Form`](crate::web::Form) where `T: Serialize` +/// - [`Either`](crate::web::Either) where `L: Serialize` and `R: Serialize` +/// - [`CustomizeResponder`] +/// - [`actix_files::NamedFile`](https://docs.rs/actix-files/latest/actix_files/struct.NamedFile.html) +/// - [Experimental responders from `actix-web-lab`](https://docs.rs/actix-web-lab/latest/actix_web_lab/respond/index.html) +/// - Third party integrations may also have implemented `Responder` where appropriate. For example, +/// HTML templating engines. +/// +/// # Customizing Responder Output +/// Calling [`.customize()`](Responder::customize) on any responder type will wrap it in a +/// [`CustomizeResponder`] capable of overriding various parts of the response such as the status +/// code and header map. pub trait Responder { type Body: MessageBody + 'static; @@ -23,7 +44,7 @@ pub trait Responder { /// Wraps responder to allow alteration of its response. /// - /// See [`CustomizeResponder`] docs for its capabilities. + /// See [`CustomizeResponder`] docs for more details on its capabilities. /// /// # Examples /// ``` @@ -47,6 +68,15 @@ pub trait Responder { CustomizeResponder::new(self) } + #[doc(hidden)] + #[deprecated(since = "4.0.0", note = "Prefer `.customize().with_status(header)`.")] + fn with_status(self, status: StatusCode) -> CustomizeResponder + where + Self: Sized, + { + self.customize().with_status(status) + } + #[doc(hidden)] #[deprecated(since = "4.0.0", note = "Prefer `.customize().insert_header(header)`.")] fn with_header(self, header: impl TryIntoHeaderPair) -> CustomizeResponder @@ -75,11 +105,8 @@ impl Responder for actix_http::ResponseBuilder { } } -impl Responder for Option -where - T: Responder, -{ - type Body = EitherBody; +impl Responder for Option { + type Body = EitherBody; fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self { @@ -89,12 +116,12 @@ where } } -impl Responder for Result +impl Responder for Result where - T: Responder, + R: Responder, E: Into, { - type Body = EitherBody; + type Body = EitherBody; fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self { @@ -104,8 +131,8 @@ where } } -impl Responder for (T, StatusCode) { - type Body = T::Body; +impl Responder for (R, StatusCode) { + type Body = R::Body; fn respond_to(self, req: &HttpRequest) -> HttpResponse { let mut res = self.0.respond_to(req); @@ -132,11 +159,13 @@ macro_rules! impl_responder_by_forward_into_base_response { } impl_responder_by_forward_into_base_response!(&'static [u8]); +impl_responder_by_forward_into_base_response!(Vec); impl_responder_by_forward_into_base_response!(Bytes); impl_responder_by_forward_into_base_response!(BytesMut); impl_responder_by_forward_into_base_response!(&'static str); impl_responder_by_forward_into_base_response!(String); +impl_responder_by_forward_into_base_response!(bytestring::ByteString); macro_rules! impl_into_string_responder { ($res:ty) => { diff --git a/src/response/response.rs b/actix-web/src/response/response.rs similarity index 98% rename from src/response/response.rs rename to actix-web/src/response/response.rs index 6449c586d..ead8badba 100644 --- a/src/response/response.rs +++ b/actix-web/src/response/response.rs @@ -323,12 +323,6 @@ impl From for HttpResponse { impl From> for Response { fn from(res: HttpResponse) -> Self { // this impl will always be called as part of dispatcher - - // TODO: expose cause somewhere? - // if let Some(err) = res.error { - // return Response::from_error(err); - // } - res.res } } @@ -349,7 +343,7 @@ mod response_fut_impl { // Future is only implemented for BoxBody payload type because it's the most useful for making // simple handlers without async blocks. Making it generic over all MessageBody types requires a - // future impl on Response which would cause it's body field to be, undesirably, Option. + // future impl on Response which would cause its body field to be, undesirably, Option. // // This impl is not particularly efficient due to the Response construction and should probably // not be invoked if performance is important. Prefer an async fn/block in such cases. diff --git a/src/rmap.rs b/actix-web/src/rmap.rs similarity index 99% rename from src/rmap.rs rename to actix-web/src/rmap.rs index 932f7acde..6a1a187b2 100644 --- a/src/rmap.rs +++ b/actix-web/src/rmap.rs @@ -151,7 +151,7 @@ impl ResourceMap { .char_indices() .filter_map(|(i, c)| (c == '/').then(|| i)) .nth(2) - .unwrap_or_else(|| path.len()); + .unwrap_or(path.len()); ( Cow::Borrowed(&path[..third_slash_index]), diff --git a/src/route.rs b/actix-web/src/route.rs similarity index 80% rename from src/route.rs rename to actix-web/src/route.rs index 0410b99dd..b37128f2c 100644 --- a/src/route.rs +++ b/actix-web/src/route.rs @@ -1,15 +1,17 @@ use std::{mem, rc::Rc}; -use actix_http::Method; +use actix_http::{body::MessageBody, Method}; use actix_service::{ + apply, boxed::{self, BoxService}, - fn_service, Service, ServiceFactory, ServiceFactoryExt, + fn_service, Service, ServiceFactory, ServiceFactoryExt, Transform, }; use futures_core::future::LocalBoxFuture; use crate::{ guard::{self, Guard}, handler::{handler_service, Handler}, + middleware::Compat, service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse}, Error, FromRequest, HttpResponse, Responder, }; @@ -35,6 +37,31 @@ impl Route { } } + /// Registers a route middleware. + /// + /// `mw` is a middleware component (type), that can modify the requests and responses handled by + /// this `Route`. + /// + /// See [`App::wrap`](crate::App::wrap) for more details. + #[doc(alias = "middleware")] + #[doc(alias = "use")] // nodejs terminology + pub fn wrap(self, mw: M) -> Route + where + M: Transform< + BoxService, + ServiceRequest, + Response = ServiceResponse, + Error = Error, + InitError = (), + > + 'static, + B: MessageBody + 'static, + { + Route { + service: boxed::factory(apply(Compat::new(mw), self.service)), + guards: self.guards, + } + } + pub(crate) fn take_guards(&mut self) -> Vec> { mem::take(Rc::get_mut(&mut self.guards).unwrap()) } @@ -246,11 +273,15 @@ mod tests { use futures_core::future::LocalBoxFuture; use serde::Serialize; - use crate::dev::{always_ready, fn_factory, fn_service, Service}; - use crate::http::{header, Method, StatusCode}; - use crate::service::{ServiceRequest, ServiceResponse}; - use crate::test::{call_service, init_service, read_body, TestRequest}; - use crate::{error, web, App, HttpResponse}; + use crate::{ + dev::{always_ready, fn_factory, fn_service, Service}, + error, + http::{header, Method, StatusCode}, + middleware::{DefaultHeaders, Logger}, + service::{ServiceRequest, ServiceResponse}, + test::{call_service, init_service, read_body, TestRequest}, + web, App, HttpResponse, + }; #[derive(Serialize, PartialEq, Debug)] struct MyObject { @@ -323,6 +354,44 @@ mod tests { assert_eq!(body, Bytes::from_static(b"{\"name\":\"test\"}")); } + #[actix_rt::test] + async fn route_middleware() { + let srv = init_service( + App::new() + .route("/", web::get().to(HttpResponse::Ok).wrap(Logger::default())) + .service( + web::resource("/test") + .route(web::get().to(HttpResponse::Ok)) + .route( + web::post() + .to(HttpResponse::Created) + .wrap(DefaultHeaders::new().add(("x-test", "x-posted"))), + ) + .route( + web::delete() + .to(HttpResponse::Accepted) + // logger changes body type, proving Compat is not needed + .wrap(Logger::default()), + ), + ), + ) + .await; + + let req = TestRequest::get().uri("/test").to_request(); + let res = call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + assert!(!res.headers().contains_key("x-test")); + + let req = TestRequest::post().uri("/test").to_request(); + let res = call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::CREATED); + assert_eq!(res.headers().get("x-test").unwrap(), "x-posted"); + + let req = TestRequest::delete().uri("/test").to_request(); + let res = call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::ACCEPTED); + } + #[actix_rt::test] async fn test_service_handler() { struct HelloWorld; diff --git a/actix-web/src/rt.rs b/actix-web/src/rt.rs new file mode 100644 index 000000000..929eadfd8 --- /dev/null +++ b/actix-web/src/rt.rs @@ -0,0 +1,73 @@ +//! A selection of re-exports from [`tokio`] and [`actix-rt`]. +//! +//! Actix Web runs on [Tokio], providing full[^compat] compatibility with its huge ecosystem of +//! crates. Each of the server's workers uses a single-threaded runtime. Read more about the +//! architecture in [`actix-rt`]'s docs. +//! +//! # Running Actix Web Without Macros +//! ```no_run +//! use actix_web::{middleware, rt, web, App, HttpRequest, HttpServer}; +//! +//! async fn index(req: HttpRequest) -> &'static str { +//! println!("REQ: {:?}", req); +//! "Hello world!\r\n" +//! } +//! +//! fn main() -> std::io::Result<()> { +//! rt::System::new().block_on( +//! HttpServer::new(|| { +//! App::new().service(web::resource("/").route(web::get().to(index))) +//! }) +//! .bind(("127.0.0.1", 8080))? +//! .run() +//! ) +//! } +//! ``` +//! +//! # Running Actix Web Using `#[tokio::main]` +//! If you need to run something alongside Actix Web that uses Tokio's work stealing functionality, +//! you can run Actix Web under `#[tokio::main]`. The [`Server`](crate::dev::Server) object returned +//! from [`HttpServer::run`](crate::HttpServer::run) can also be [`spawn`]ed, if preferred. +//! +//! Note that `actix` actor support (and therefore WebSocket support through `actix-web-actors`) +//! still require `#[actix_web::main]` since they require a [`System`] to be set up. +//! +//! ```no_run +//! use actix_web::{get, middleware, rt, web, App, HttpRequest, HttpServer}; +//! +//! #[get("/")] +//! async fn index(req: HttpRequest) -> &'static str { +//! println!("REQ: {:?}", req); +//! "Hello world!\r\n" +//! } +//! +//! #[tokio::main] +//! async fn main() -> std::io::Result<()> { +//! HttpServer::new(|| { +//! App::new().service(index) +//! }) +//! .bind(("127.0.0.1", 8080))? +//! .run() +//! .await +//! } +//! ``` +//! +//! [^compat]: Crates that use Tokio's [`block_in_place`] will not work with Actix Web. Fortunately, +//! the vast majority of Tokio-based crates do not use it. +//! +//! [`actix-rt`]: https://docs.rs/actix-rt +//! [`tokio`]: https://docs.rs/tokio +//! [Tokio]: https://docs.rs/tokio +//! [`spawn`]: https://docs.rs/tokio/1/tokio/fn.spawn.html +//! [`block_in_place`]: https://docs.rs/tokio/1/tokio/task/fn.block_in_place.html + +// In particular: +// - Omit the `Arbiter` types because they have limited value here. +// - Re-export but hide the runtime macros because they won't work directly but are required for +// `#[actix_web::main]` and `#[actix_web::test]` to work. + +pub use actix_rt::{net, pin, signal, spawn, task, time, Runtime, System, SystemRunner}; + +#[cfg(feature = "macros")] +#[doc(hidden)] +pub use actix_macros::{main, test}; diff --git a/src/scope.rs b/actix-web/src/scope.rs similarity index 98% rename from src/scope.rs rename to actix-web/src/scope.rs index c05ce054d..f8c042a5f 100644 --- a/src/scope.rs +++ b/actix-web/src/scope.rs @@ -198,6 +198,10 @@ where .get_or_insert_with(Extensions::new) .extend(cfg.app_data); + if let Some(default) = cfg.default { + self.default = Some(default); + } + self } @@ -262,9 +266,10 @@ where ) } - /// Default service to be used if no matching route could be found. + /// Default service to be used if no matching resource could be found. /// - /// If default resource is not registered, app's default resource is being used. + /// If a default service is not registered, it will fall back to the default service of + /// the parent [`App`](crate::App) (see [`App::default_service`](crate::App::default_service)). pub fn default_service(mut self, f: F) -> Self where F: IntoServiceFactory, @@ -466,7 +471,7 @@ impl ServiceFactory for ScopeFactory { // construct all services factory future with it's resource def and guards. let factory_fut = join_all(self.services.iter().map(|(path, factory, guards)| { let path = path.clone(); - let guards = guards.borrow_mut().take(); + let guards = guards.borrow_mut().take().unwrap_or_default(); let factory_fut = factory.new_service(()); async move { let service = factory_fut.await?; @@ -484,7 +489,7 @@ impl ServiceFactory for ScopeFactory { .collect::, _>>()? .drain(..) .fold(Router::build(), |mut router, (path, guards, service)| { - router.rdef(path, service).2 = guards; + router.push(path, service, guards); router }) .finish(); @@ -508,17 +513,8 @@ 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 { - let guard_ctx = req.guard_ctx(); - - for guard in guards { - if !guard.check(&guard_ctx) { - return false; - } - } - } - - true + let guard_ctx = req.guard_ctx(); + guards.iter().all(|guard| guard.check(&guard_ctx)) }); if let Some((srv, _info)) = res { diff --git a/src/server.rs b/actix-web/src/server.rs similarity index 85% rename from src/server.rs rename to actix-web/src/server.rs index ed0c965b3..169eafab0 100644 --- a/src/server.rs +++ b/actix-web/src/server.rs @@ -4,6 +4,7 @@ use std::{ marker::PhantomData, net, sync::{Arc, Mutex}, + time::Duration, }; use actix_http::{body::MessageBody, Extensions, HttpService, KeepAlive, Request, Response}; @@ -17,6 +18,9 @@ use actix_tls::accept::openssl::reexports::{AlpnError, SslAcceptor, SslAcceptorB #[cfg(feature = "rustls")] use actix_tls::accept::rustls::reexports::ServerConfig as RustlsServerConfig; +#[cfg(any(feature = "openssl", feature = "rustls"))] +use actix_http::TlsAcceptorConfig; + use crate::{config::AppConfig, Error}; struct Socket { @@ -27,8 +31,10 @@ struct Socket { struct Config { host: Option, keep_alive: KeepAlive, - client_timeout: u64, - client_shutdown: u64, + client_request_timeout: Duration, + client_disconnect_timeout: Duration, + #[cfg(any(feature = "openssl", feature = "rustls"))] + tls_handshake_timeout: Option, } /// An HTTP Server. @@ -88,9 +94,11 @@ where factory, config: Arc::new(Mutex::new(Config { host: None, - keep_alive: KeepAlive::Timeout(5), - client_timeout: 5000, - client_shutdown: 5000, + keep_alive: KeepAlive::default(), + client_request_timeout: Duration::from_secs(5), + client_disconnect_timeout: Duration::from_secs(1), + #[cfg(any(feature = "rustls", feature = "openssl"))] + tls_handshake_timeout: None, })), backlog: 1024, sockets: Vec::new(), @@ -127,7 +135,7 @@ where /// Set number of workers to start. /// - /// By default, server uses number of available logical CPU as thread count. + /// By default, the number of available physical CPUs is used as the worker count. pub fn workers(mut self, num: usize) -> Self { self.builder = self.builder.workers(num); self @@ -200,11 +208,17 @@ where /// To disable timeout set value to 0. /// /// By default client timeout is set to 5000 milliseconds. - pub fn client_timeout(self, val: u64) -> Self { - self.config.lock().unwrap().client_timeout = val; + pub fn client_request_timeout(self, dur: Duration) -> Self { + self.config.lock().unwrap().client_request_timeout = dur; self } + #[doc(hidden)] + #[deprecated(since = "4.0.0", note = "Renamed to `client_request_timeout`.")] + pub fn client_timeout(self, dur: Duration) -> Self { + self.client_request_timeout(dur) + } + /// Set server connection shutdown timeout in milliseconds. /// /// Defines a timeout for shutdown connection. If a shutdown procedure does not complete @@ -213,11 +227,35 @@ where /// To disable timeout set value to 0. /// /// By default client timeout is set to 5000 milliseconds. - pub fn client_shutdown(self, val: u64) -> Self { - self.config.lock().unwrap().client_shutdown = val; + pub fn client_disconnect_timeout(self, dur: Duration) -> Self { + self.config.lock().unwrap().client_disconnect_timeout = dur; self } + /// Set TLS handshake timeout. + /// + /// Defines a timeout for TLS handshake. If the TLS handshake does not complete + /// within this time, the connection is closed. + /// + /// By default handshake timeout is set to 3000 milliseconds. + #[cfg(any(feature = "openssl", feature = "rustls"))] + #[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))] + pub fn tls_handshake_timeout(self, dur: Duration) -> Self { + self.config + .lock() + .unwrap() + .tls_handshake_timeout + .replace(dur); + + self + } + + #[doc(hidden)] + #[deprecated(since = "4.0.0", note = "Renamed to `client_disconnect_timeout`.")] + pub fn client_shutdown(self, dur: u64) -> Self { + self.client_disconnect_timeout(Duration::from_millis(dur)) + } + /// Set server host name. /// /// Host name is used by application router as a hostname for url generation. @@ -291,8 +329,8 @@ where let mut svc = HttpService::build() .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout) - .client_disconnect(c.client_shutdown) + .client_request_timeout(c.client_request_timeout) + .client_disconnect_timeout(c.client_disconnect_timeout) .local_addr(addr); if let Some(handler) = on_connect_fn.clone() { @@ -349,14 +387,12 @@ where let svc = HttpService::build() .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout) - .client_disconnect(c.client_shutdown) + .client_request_timeout(c.client_request_timeout) + .client_disconnect_timeout(c.client_disconnect_timeout) .local_addr(addr); let svc = if let Some(handler) = on_connect_fn.clone() { - svc.on_connect_ext(move |io: &_, ext: _| { - (&*handler)(io as &dyn Any, ext) - }) + svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) } else { svc }; @@ -365,10 +401,15 @@ where .into_factory() .map_err(|err| err.into().error_response()); + let acceptor_config = match c.tls_handshake_timeout { + Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur), + None => TlsAcceptorConfig::default(), + }; + svc.finish(map_config(fac, move |_| { AppConfig::new(true, host.clone(), addr) })) - .openssl(acceptor.clone()) + .openssl_with_config(acceptor.clone(), acceptor_config) })?; Ok(self) @@ -410,8 +451,8 @@ where let svc = HttpService::build() .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout) - .client_disconnect(c.client_shutdown); + .client_request_timeout(c.client_request_timeout) + .client_disconnect_timeout(c.client_disconnect_timeout); let svc = if let Some(handler) = on_connect_fn.clone() { svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) @@ -423,10 +464,15 @@ where .into_factory() .map_err(|err| err.into().error_response()); + let acceptor_config = match c.tls_handshake_timeout { + Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur), + None => TlsAcceptorConfig::default(), + }; + svc.finish(map_config(fac, move |_| { AppConfig::new(true, host.clone(), addr) })) - .rustls(config.clone()) + .rustls_with_config(config.clone(), acceptor_config) })?; Ok(self) @@ -537,12 +583,12 @@ where fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then({ let mut svc = HttpService::build() .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout) - .client_disconnect(c.client_shutdown); + .client_request_timeout(c.client_request_timeout) + .client_disconnect_timeout(c.client_disconnect_timeout); if let Some(handler) = on_connect_fn.clone() { svc = svc - .on_connect_ext(move |io: &_, ext: _| (&*handler)(io as &dyn Any, ext)); + .on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)); } let fac = factory() @@ -593,8 +639,8 @@ where fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then( HttpService::build() .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout) - .client_disconnect(c.client_shutdown) + .client_request_timeout(c.client_request_timeout) + .client_disconnect_timeout(c.client_disconnect_timeout) .finish(map_config(fac, move |_| config.clone())), ) }, diff --git a/src/service.rs b/actix-web/src/service.rs similarity index 89% rename from src/service.rs rename to actix-web/src/service.rs index 061c3e044..0ad92d8a1 100644 --- a/src/service.rs +++ b/actix-web/src/service.rs @@ -24,7 +24,7 @@ use crate::{ guard::{Guard, GuardContext}, info::ConnectionInfo, rmap::ResourceMap, - Error, HttpRequest, HttpResponse, + Error, FromRequest, HttpRequest, HttpResponse, }; pub(crate) type BoxedHttpService = BoxService, Error>; @@ -78,23 +78,60 @@ pub struct ServiceRequest { } impl ServiceRequest { - /// Construct service request + /// Construct `ServiceRequest` from parts. pub(crate) fn new(req: HttpRequest, payload: Payload) -> Self { Self { req, payload } } - /// Deconstruct request into parts + /// Deconstruct `ServiceRequest` into inner parts. #[inline] pub fn into_parts(self) -> (HttpRequest, Payload) { (self.req, self.payload) } - /// Get mutable access to inner `HttpRequest` and `Payload` + /// Returns mutable accessors to inner parts. #[inline] pub fn parts_mut(&mut self) -> (&mut HttpRequest, &mut Payload) { (&mut self.req, &mut self.payload) } + /// Returns immutable accessors to inner parts. + #[inline] + pub fn parts(&self) -> (&HttpRequest, &Payload) { + (&self.req, &self.payload) + } + + /// Returns immutable accessor to inner [`HttpRequest`]. + #[inline] + pub fn request(&self) -> &HttpRequest { + &self.req + } + + /// Derives a type from this request using an [extractor](crate::FromRequest). + /// + /// Returns the `T` extractor's `Future` type which can be `await`ed. This is particularly handy + /// when you want to use an extractor in a middleware implementation. + /// + /// # Examples + /// ``` + /// use actix_web::{ + /// dev::{ServiceRequest, ServiceResponse}, + /// web::Path, Error + /// }; + /// + /// async fn my_helper(mut srv_req: ServiceRequest) -> Result { + /// let path = srv_req.extract::>().await?; + /// // [...] + /// # todo!() + /// } + /// ``` + pub fn extract(&mut self) -> ::Future + where + T: FromRequest, + { + T::from_request(&self.req, &mut self.payload) + } + /// Construct request from parts. pub fn from_parts(req: HttpRequest, payload: Payload) -> Self { #[cfg(debug_assertions)] @@ -105,9 +142,7 @@ impl ServiceRequest { Self { req, payload } } - /// Construct request from request. - /// - /// The returned `ServiceRequest` would have no payload. + /// Construct `ServiceRequest` with no payload from given `HttpRequest`. #[inline] pub fn from_request(req: HttpRequest) -> Self { ServiceRequest { @@ -116,63 +151,63 @@ impl ServiceRequest { } } - /// Create service response + /// Create `ServiceResponse` from this request and given response. #[inline] pub fn into_response>>(self, res: R) -> ServiceResponse { let res = HttpResponse::from(res.into()); ServiceResponse::new(self.req, res) } - /// Create service response for error + /// Create `ServiceResponse` from this request and given error. #[inline] pub fn error_response>(self, err: E) -> ServiceResponse { let res = HttpResponse::from_error(err.into()); ServiceResponse::new(self.req, res) } - /// This method returns reference to the request head + /// Returns a reference to the request head. #[inline] pub fn head(&self) -> &RequestHead { self.req.head() } - /// This method returns reference to the request head + /// Returns a mutable reference to the request head. #[inline] pub fn head_mut(&mut self) -> &mut RequestHead { self.req.head_mut() } - /// Request's uri. + /// Returns the request URI. #[inline] pub fn uri(&self) -> &Uri { &self.head().uri } - /// Read the Request method. + /// Returns the request method. #[inline] pub fn method(&self) -> &Method { &self.head().method } - /// Read the Request Version. + /// Returns the request version. #[inline] pub fn version(&self) -> Version { self.head().version } + /// Returns a reference to request headers. #[inline] - /// Returns request's headers. pub fn headers(&self) -> &HeaderMap { &self.head().headers } + /// Returns a mutable reference to request headers. #[inline] - /// Returns mutable request's headers. pub fn headers_mut(&mut self) -> &mut HeaderMap { &mut self.head_mut().headers } - /// The target path of this Request. + /// Returns request path. #[inline] pub fn path(&self) -> &str { self.head().uri.path() @@ -184,7 +219,7 @@ impl ServiceRequest { self.req.query_string() } - /// Peer socket address. + /// Returns peer's 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. @@ -197,24 +232,23 @@ impl ServiceRequest { self.head().peer_addr } - /// Get *ConnectionInfo* for the current request. + /// Returns a reference to connection info. #[inline] pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> { self.req.connection_info() } - /// Returns a reference to the Path parameters. + /// Returns reference to the Path parameters. /// - /// Params is a container for URL parameters. - /// A variable segment is specified in the form `{identifier}`, - /// where the identifier can be used later in a request handler to - /// access the matched value for that segment. + /// Params is a container for URL parameters. A variable segment is specified in the form + /// `{identifier}`, where the identifier can be used later in a request handler to access the + /// matched value for that segment. #[inline] pub fn match_info(&self) -> &Path { self.req.match_info() } - /// Returns a mutable reference to the Path parameters. + /// Returns a mutable reference to the path match information. #[inline] pub fn match_info_mut(&mut self) -> &mut Path { self.req.match_info_mut() @@ -232,13 +266,13 @@ impl ServiceRequest { self.req.match_pattern() } - /// Get a reference to a `ResourceMap` of current application. + /// Returns a reference to the application's resource map. #[inline] pub fn resource_map(&self) -> &ResourceMap { self.req.resource_map() } - /// Service configuration + /// Returns a reference to the application's configuration. #[inline] pub fn app_config(&self) -> &AppConfig { self.req.app_config() @@ -262,6 +296,7 @@ impl ServiceRequest { self.req.conn_data() } + /// Return request cookies. #[cfg(feature = "cookies")] #[inline] pub fn cookies(&self) -> Result>>, CookieParseError> { @@ -610,11 +645,10 @@ where } } -/// Macro helping register different types of services at the sametime. +/// Macro to help register different types of services at the same time. /// -/// The service type must be implementing [`HttpServiceFactory`](self::HttpServiceFactory) trait. -/// -/// The max number of services can be grouped together is 12. +/// The max number of services that can be grouped together is 12 and all must implement the +/// [`HttpServiceFactory`] trait. /// /// # Examples /// ``` diff --git a/src/test/mod.rs b/actix-web/src/test/mod.rs similarity index 97% rename from src/test/mod.rs rename to actix-web/src/test/mod.rs index a29dfc437..9c6121151 100644 --- a/src/test/mod.rs +++ b/actix-web/src/test/mod.rs @@ -5,7 +5,7 @@ //! //! # Off-The-Shelf Test Services //! - [`ok_service`] -//! - [`simple_service`] +//! - [`status_service`] //! //! # Calling Test Service //! - [`TestRequest`] @@ -27,7 +27,7 @@ mod test_utils; pub use self::test_request::TestRequest; #[allow(deprecated)] -pub use self::test_services::{default_service, ok_service, simple_service}; +pub use self::test_services::{default_service, ok_service, simple_service, status_service}; #[allow(deprecated)] pub use self::test_utils::{ call_and_read_body, call_and_read_body_json, call_service, init_service, read_body, diff --git a/src/test/test_request.rs b/actix-web/src/test/test_request.rs similarity index 94% rename from src/test/test_request.rs rename to actix-web/src/test/test_request.rs index fc42253d7..e81561d17 100644 --- a/src/test/test_request.rs +++ b/actix-web/src/test/test_request.rs @@ -24,16 +24,16 @@ use crate::cookie::{Cookie, CookieJar}; /// /// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern. /// You can generate various types of request via TestRequest's methods: -/// * `TestRequest::to_request` creates `actix_http::Request` instance. -/// * `TestRequest::to_srv_request` creates `ServiceRequest` instance, which is used for testing middlewares and chain adapters. -/// * `TestRequest::to_srv_response` creates `ServiceResponse` instance. -/// * `TestRequest::to_http_request` creates `HttpRequest` instance, which is used for testing handlers. +/// - [`TestRequest::to_request`] creates an [`actix_http::Request`](Request). +/// - [`TestRequest::to_srv_request`] creates a [`ServiceRequest`], which is used for testing middlewares and chain adapters. +/// - [`TestRequest::to_srv_response`] creates a [`ServiceResponse`]. +/// - [`TestRequest::to_http_request`] creates an [`HttpRequest`], which is used for testing handlers. /// /// ``` /// use actix_web::{test, HttpRequest, HttpResponse, HttpMessage}; /// use actix_web::http::{header, StatusCode}; /// -/// async fn index(req: HttpRequest) -> HttpResponse { +/// async fn handler(req: HttpRequest) -> HttpResponse { /// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { /// HttpResponse::Ok().into() /// } else { @@ -42,15 +42,18 @@ use crate::cookie::{Cookie, CookieJar}; /// } /// /// #[actix_web::test] +/// # // force rustdoc to display the correct thing and also compile check the test +/// # async fn _test() {} /// async fn test_index() { -/// let req = test::TestRequest::default().insert_header("content-type", "text/plain") +/// let req = test::TestRequest::default() +/// .insert_header(header::ContentType::plaintext()) /// .to_http_request(); /// -/// let resp = index(req).await.unwrap(); +/// let resp = handler(req).await; /// assert_eq!(resp.status(), StatusCode::OK); /// /// let req = test::TestRequest::default().to_http_request(); -/// let resp = index(req).await.unwrap(); +/// let resp = handler(req).await; /// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); /// } /// ``` diff --git a/src/test/test_services.rs b/actix-web/src/test/test_services.rs similarity index 69% rename from src/test/test_services.rs rename to actix-web/src/test/test_services.rs index b4810cfd8..e6feea82d 100644 --- a/src/test/test_services.rs +++ b/actix-web/src/test/test_services.rs @@ -10,11 +10,11 @@ use crate::{ /// Creates service that always responds with `200 OK` and no body. pub fn ok_service( ) -> impl Service, Error = Error> { - simple_service(StatusCode::OK) + status_service(StatusCode::OK) } /// Creates service that always responds with given status code and no body. -pub fn simple_service( +pub fn status_service( status_code: StatusCode, ) -> impl Service, Error = Error> { fn_service(move |req: ServiceRequest| { @@ -23,9 +23,17 @@ pub fn simple_service( } #[doc(hidden)] -#[deprecated(since = "4.0.0", note = "Renamed to `simple_service`.")] +#[deprecated(since = "4.0.0", note = "Renamed to `status_service`.")] +pub fn simple_service( + status_code: StatusCode, +) -> impl Service, Error = Error> { + status_service(status_code) +} + +#[doc(hidden)] +#[deprecated(since = "4.0.0", note = "Renamed to `status_service`.")] pub fn default_service( status_code: StatusCode, ) -> impl Service, Error = Error> { - simple_service(status_code) + status_service(status_code) } diff --git a/src/test/test_utils.rs b/actix-web/src/test/test_utils.rs similarity index 99% rename from src/test/test_utils.rs rename to actix-web/src/test/test_utils.rs index 8207ce270..6f0926f35 100644 --- a/src/test/test_utils.rs +++ b/actix-web/src/test/test_utils.rs @@ -89,7 +89,7 @@ where /// ``` /// /// # Panics -/// Panics if service call returns error. +/// Panics if service call returns error. To handle errors use `app.call(req)`. pub async fn call_service(app: &S, req: R) -> S::Response where S: Service, Error = E>, diff --git a/src/types/either.rs b/actix-web/src/types/either.rs similarity index 99% rename from src/types/either.rs rename to actix-web/src/types/either.rs index 0eafb9e43..c0faf04b1 100644 --- a/src/types/either.rs +++ b/actix-web/src/types/either.rs @@ -49,7 +49,7 @@ use crate::{ /// ``` /// /// # Responder -/// It may be desireable to use a concrete type for a response with multiple branches. As long as +/// It may be desirable to use a concrete type for a response with multiple branches. As long as /// both types implement `Responder`, so will the `Either` type, enabling it to be used as a /// handler's return type. /// diff --git a/src/types/form.rs b/actix-web/src/types/form.rs similarity index 100% rename from src/types/form.rs rename to actix-web/src/types/form.rs diff --git a/src/types/header.rs b/actix-web/src/types/header.rs similarity index 100% rename from src/types/header.rs rename to actix-web/src/types/header.rs diff --git a/src/types/json.rs b/actix-web/src/types/json.rs similarity index 100% rename from src/types/json.rs rename to actix-web/src/types/json.rs diff --git a/src/types/mod.rs b/actix-web/src/types/mod.rs similarity index 100% rename from src/types/mod.rs rename to actix-web/src/types/mod.rs diff --git a/src/types/path.rs b/actix-web/src/types/path.rs similarity index 98% rename from src/types/path.rs rename to actix-web/src/types/path.rs index 869269d09..34c335ba9 100644 --- a/src/types/path.rs +++ b/actix-web/src/types/path.rs @@ -53,9 +53,7 @@ use crate::{ /// format!("Welcome {}!", info.name) /// } /// ``` -#[derive( - Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut, AsRef, Display, From, -)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut, AsRef, Display, From)] pub struct Path(T); impl Path { @@ -185,6 +183,7 @@ mod tests { assert!(Path::::from_request(&req, &mut pl).await.is_err()); } + #[allow(clippy::let_unit_value)] #[actix_rt::test] async fn test_tuple_extract() { let resource = ResourceDef::new("/{key}/{value}/"); diff --git a/src/types/payload.rs b/actix-web/src/types/payload.rs similarity index 99% rename from src/types/payload.rs rename to actix-web/src/types/payload.rs index b47a39e97..af195b867 100644 --- a/src/types/payload.rs +++ b/actix-web/src/types/payload.rs @@ -113,7 +113,7 @@ pub struct BytesExtractFut { body_fut: HttpMessageBody, } -impl<'a> Future for BytesExtractFut { +impl Future for BytesExtractFut { type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -167,7 +167,7 @@ pub struct StringExtractFut { encoding: &'static Encoding, } -impl<'a> Future for StringExtractFut { +impl Future for StringExtractFut { type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { diff --git a/src/types/query.rs b/actix-web/src/types/query.rs similarity index 100% rename from src/types/query.rs rename to actix-web/src/types/query.rs diff --git a/src/types/readlines.rs b/actix-web/src/types/readlines.rs similarity index 91% rename from src/types/readlines.rs rename to actix-web/src/types/readlines.rs index 6c456e21c..8a775a073 100644 --- a/src/types/readlines.rs +++ b/actix-web/src/types/readlines.rs @@ -20,7 +20,7 @@ use crate::{ /// Stream that reads request line by line. pub struct Readlines { stream: Payload, - buff: BytesMut, + buf: BytesMut, limit: usize, checked_buff: bool, encoding: &'static Encoding, @@ -41,7 +41,7 @@ where Readlines { stream: req.take_payload(), - buff: BytesMut::with_capacity(262_144), + buf: BytesMut::with_capacity(262_144), limit: 262_144, checked_buff: true, err: None, @@ -58,7 +58,7 @@ where fn err(err: ReadlinesError) -> Self { Readlines { stream: Payload::None, - buff: BytesMut::new(), + buf: BytesMut::new(), limit: 262_144, checked_buff: true, encoding: UTF_8, @@ -84,7 +84,7 @@ where // check if there is a newline in the buffer if !this.checked_buff { let mut found: Option = None; - for (ind, b) in this.buff.iter().enumerate() { + for (ind, b) in this.buf.iter().enumerate() { if *b == b'\n' { found = Some(ind); break; @@ -96,13 +96,13 @@ where return Poll::Ready(Some(Err(ReadlinesError::LimitOverflow))); } let line = if this.encoding == UTF_8 { - str::from_utf8(&this.buff.split_to(ind + 1)) + str::from_utf8(&this.buf.split_to(ind + 1)) .map_err(|_| ReadlinesError::EncodingError)? .to_owned() } else { this.encoding .decode_without_bom_handling_and_without_replacement( - &this.buff.split_to(ind + 1), + &this.buf.split_to(ind + 1), ) .map(Cow::into_owned) .ok_or(ReadlinesError::EncodingError)? @@ -141,32 +141,32 @@ where .ok_or(ReadlinesError::EncodingError)? }; // extend buffer with rest of the bytes; - this.buff.extend_from_slice(&bytes); + this.buf.extend_from_slice(&bytes); this.checked_buff = false; return Poll::Ready(Some(Ok(line))); } - this.buff.extend_from_slice(&bytes); + this.buf.extend_from_slice(&bytes); Poll::Pending } None => { - if this.buff.is_empty() { + if this.buf.is_empty() { return Poll::Ready(None); } - if this.buff.len() > this.limit { + if this.buf.len() > this.limit { return Poll::Ready(Some(Err(ReadlinesError::LimitOverflow))); } let line = if this.encoding == UTF_8 { - str::from_utf8(&this.buff) + str::from_utf8(&this.buf) .map_err(|_| ReadlinesError::EncodingError)? .to_owned() } else { this.encoding - .decode_without_bom_handling_and_without_replacement(&this.buff) + .decode_without_bom_handling_and_without_replacement(&this.buf) .map(Cow::into_owned) .ok_or(ReadlinesError::EncodingError)? }; - this.buff.clear(); + this.buf.clear(); Poll::Ready(Some(Ok(line))) } diff --git a/src/web.rs b/actix-web/src/web.rs similarity index 93% rename from src/web.rs rename to actix-web/src/web.rs index 4858600af..f5845d7f6 100644 --- a/src/web.rs +++ b/actix-web/src/web.rs @@ -1,4 +1,18 @@ //! Essentials helper functions and types for application registration. +//! +//! # Request Extractors +//! - [`Data`]: Application data item +//! - [`ReqData`]: Request-local data item +//! - [`Path`]: URL path parameters / dynamic segments +//! - [`Query`]: URL query parameters +//! - [`Header`]: Typed header +//! - [`Json`]: JSON payload +//! - [`Form`]: URL-encoded payload +//! - [`Bytes`]: Raw payload +//! +//! # Responders +//! - [`Json`]: JSON request payload +//! - [`Bytes`]: Raw request payload use std::future::Future; @@ -12,9 +26,7 @@ use crate::{ pub use crate::config::ServiceConfig; pub use crate::data::Data; -pub use crate::request::HttpRequest; pub use crate::request_data::ReqData; -pub use crate::response::HttpResponse; pub use crate::types::*; /// Creates a new resource for a specific path. diff --git a/tests/compression.rs b/actix-web/tests/compression.rs similarity index 100% rename from tests/compression.rs rename to actix-web/tests/compression.rs diff --git a/tests/fixtures/lorem.txt b/actix-web/tests/fixtures/lorem.txt similarity index 100% rename from tests/fixtures/lorem.txt rename to actix-web/tests/fixtures/lorem.txt diff --git a/tests/fixtures/lorem.txt.br b/actix-web/tests/fixtures/lorem.txt.br similarity index 100% rename from tests/fixtures/lorem.txt.br rename to actix-web/tests/fixtures/lorem.txt.br diff --git a/tests/fixtures/lorem.txt.gz b/actix-web/tests/fixtures/lorem.txt.gz similarity index 100% rename from tests/fixtures/lorem.txt.gz rename to actix-web/tests/fixtures/lorem.txt.gz diff --git a/tests/fixtures/lorem.txt.xz b/actix-web/tests/fixtures/lorem.txt.xz similarity index 100% rename from tests/fixtures/lorem.txt.xz rename to actix-web/tests/fixtures/lorem.txt.xz diff --git a/tests/fixtures/lorem.txt.zst b/actix-web/tests/fixtures/lorem.txt.zst similarity index 100% rename from tests/fixtures/lorem.txt.zst rename to actix-web/tests/fixtures/lorem.txt.zst diff --git a/tests/test-macro-import-conflict.rs b/actix-web/tests/test-macro-import-conflict.rs similarity index 100% rename from tests/test-macro-import-conflict.rs rename to actix-web/tests/test-macro-import-conflict.rs diff --git a/tests/test_error_propagation.rs b/actix-web/tests/test_error_propagation.rs similarity index 100% rename from tests/test_error_propagation.rs rename to actix-web/tests/test_error_propagation.rs diff --git a/tests/test_httpserver.rs b/actix-web/tests/test_httpserver.rs similarity index 95% rename from tests/test_httpserver.rs rename to actix-web/tests/test_httpserver.rs index 6ea8e520c..86e0575f3 100644 --- a/tests/test_httpserver.rs +++ b/actix-web/tests/test_httpserver.rs @@ -26,9 +26,9 @@ async fn test_start() { .backlog(1) .max_connections(10) .max_connection_rate(10) - .keep_alive(10) - .client_timeout(5000) - .client_shutdown(0) + .keep_alive(Duration::from_secs(10)) + .client_request_timeout(Duration::from_secs(5)) + .client_disconnect_timeout(Duration::ZERO) .server_hostname("localhost") .system_exit() .disable_signals() diff --git a/tests/test_server.rs b/actix-web/tests/test_server.rs similarity index 95% rename from tests/test_server.rs rename to actix-web/tests/test_server.rs index b8193a004..270223d69 100644 --- a/tests/test_server.rs +++ b/actix-web/tests/test_server.rs @@ -8,6 +8,7 @@ use std::{ io::{Read, Write}, pin::Pin, task::{Context, Poll}, + time::Duration, }; use actix_web::{ @@ -798,34 +799,36 @@ async fn test_server_cookies() { let res = req.send().await.unwrap(); assert!(res.status().is_success()); - let first_cookie = Cookie::build("first", "first_value") - .http_only(true) - .finish(); - let second_cookie = Cookie::new("second", "first_value"); + { + let first_cookie = Cookie::build("first", "first_value") + .http_only(true) + .finish(); + let second_cookie = Cookie::new("second", "first_value"); - let cookies = res.cookies().expect("To have cookies"); - assert_eq!(cookies.len(), 3); - if cookies[0] == first_cookie { - assert_eq!(cookies[1], second_cookie); - } else { - assert_eq!(cookies[0], second_cookie); - assert_eq!(cookies[1], first_cookie); - } + let cookies = res.cookies().expect("To have cookies"); + assert_eq!(cookies.len(), 3); + if cookies[0] == first_cookie { + assert_eq!(cookies[1], second_cookie); + } else { + assert_eq!(cookies[0], second_cookie); + assert_eq!(cookies[1], first_cookie); + } - let first_cookie = first_cookie.to_string(); - let second_cookie = second_cookie.to_string(); - // Check that we have exactly two instances of raw cookie headers - let cookies = res - .headers() - .get_all(http::header::SET_COOKIE) - .map(|header| header.to_str().expect("To str").to_string()) - .collect::>(); - assert_eq!(cookies.len(), 3); - if cookies[0] == first_cookie { - assert_eq!(cookies[1], second_cookie); - } else { - assert_eq!(cookies[0], second_cookie); - assert_eq!(cookies[1], first_cookie); + let first_cookie = first_cookie.to_string(); + let second_cookie = second_cookie.to_string(); + // Check that we have exactly two instances of raw cookie headers + let cookies = res + .headers() + .get_all(http::header::SET_COOKIE) + .map(|header| header.to_str().expect("To str").to_string()) + .collect::>(); + assert_eq!(cookies.len(), 3); + if cookies[0] == first_cookie { + assert_eq!(cookies[1], second_cookie); + } else { + assert_eq!(cookies[0], second_cookie); + assert_eq!(cookies[1], first_cookie); + } } srv.stop().await; @@ -835,9 +838,10 @@ async fn test_server_cookies() { async fn test_slow_request() { use std::net; - let srv = actix_test::start_with(actix_test::config().client_timeout(200), || { - App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))) - }); + let srv = actix_test::start_with( + actix_test::config().client_request_timeout(Duration::from_millis(200)), + || App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))), + ); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let mut data = String::new(); diff --git a/tests/test_weird_poll.rs b/actix-web/tests/test_weird_poll.rs similarity index 100% rename from tests/test_weird_poll.rs rename to actix-web/tests/test_weird_poll.rs diff --git a/tests/utils.rs b/actix-web/tests/utils.rs similarity index 100% rename from tests/utils.rs rename to actix-web/tests/utils.rs diff --git a/tests/weird_poll.rs b/actix-web/tests/weird_poll.rs similarity index 100% rename from tests/weird_poll.rs rename to actix-web/tests/weird_poll.rs diff --git a/awc/CHANGES.md b/awc/CHANGES.md index e12dc8c27..e229a6d96 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,6 +1,113 @@ # Changes -## Unreleased - 2021-xx-xx +## Unreleased - 2022-xx-xx +### Changed +- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. + + +## 3.0.0 - 2022-03-07 +### Dependencies +- Updated `actix-*` to Tokio v1-based versions. [#1813] +- Updated `bytes` to `1.0`. [#1813] +- Updated `cookie` to `0.16`. [#2555] +- Updated `rand` to `0.8`. +- Updated `rustls` to `0.20`. [#2414] +- Updated `tokio` to `1`. + +### Added +- `trust-dns` crate feature to enable `trust-dns-resolver` as client DNS resolver; disabled by default. [#1969] +- `cookies` crate feature; enabled by default. [#2619] +- `compress-brotli` crate feature; enabled by default. [#2250] +- `compress-gzip` crate feature; enabled by default. [#2250] +- `compress-zstd` crate feature; enabled by default. [#2250] +- `client::Connector::handshake_timeout()` for customizing TLS connection handshake timeout. [#2081] +- `client::ConnectorService` as `client::Connector::finish` method's return type [#2081] +- `client::ConnectionIo` trait alias [#2081] +- `Client::headers()` to get default mut reference of `HeaderMap` of client object. [#2114] +- `ClientResponse::timeout()` for set the timeout of collecting response body. [#1931] +- `ClientBuilder::local_address()` for binding to a local IP address for this client. [#2024] +- `ClientRequest::insert_header()` method which allows using typed and untyped headers. [#1869] +- `ClientRequest::append_header()` method which allows using typed and untyped headers. [#1869] +- `ClientBuilder::add_default_header()` (and deprecate `ClientBuilder::header()`). [#2510] + +### Changed +- `client::Connector` type now only has one generic type for `actix_service::Service`. [#2063] +- `client::error::ConnectError` Resolver variant contains `Box` type. [#1905] +- `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905] +- `ConnectorService` type is renamed to `BoxConnectorService`. [#2081] +- Fix http/https encoding when enabling `compress` feature. [#2116] +- Rename `TestResponse::{header => append_header, set => insert_header}`. These methods now take a `TryIntoHeaderPair`. [#2094] +- `ClientBuilder::connector()` method now takes `Connector` type. [#2008] +- Basic auth now accepts blank passwords as an empty string instead of an `Option`. [#2050] +- Relax default timeout for `Connector` to 5 seconds (up from 1 second). [#1905] +- `*::send_json()` and `*::send_form()` methods now receive `impl Serialize`. [#2553] +- `FrozenClientRequest::extra_header()` now uses receives an `impl TryIntoHeaderPair`. [#2553] +- Rename `Connector::{ssl => openssl}()`. [#2503] +- `ClientRequest::send_body` now takes an `impl MessageBody`. [#2546] +- Rename `MessageBody => ResponseBody` to avoid conflicts with `MessageBody` trait. [#2546] +- Minimum supported Rust version (MSRV) is now 1.54. + +### Fixed +- Send headers along with redirected requests. [#2310] +- Improve `Client` instantiation efficiency when using `openssl` by only building connectors once. [#2503] +- Remove unnecessary `Unpin` bounds on `*::send_stream`. [#2553] +- `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] + +### Removed +- `compress` crate feature. [#2250] +- `ClientRequest::set`; use `ClientRequest::insert_header`. [#1869] +- `ClientRequest::set_header`; use `ClientRequest::insert_header`. [#1869] +- `ClientRequest::set_header_if_none`; use `ClientRequest::insert_header_if_none`. [#1869] +- `ClientRequest::header`; use `ClientRequest::append_header`. [#1869] +- Deprecated methods on `ClientRequest`: `if_true`, `if_some`. [#2148] +- `ClientBuilder::default` function [#2008] + +### Security +- `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. + +[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html + +[#1813]: https://github.com/actix/actix-web/pull/1813 +[#1869]: https://github.com/actix/actix-web/pull/1869 +[#1905]: https://github.com/actix/actix-web/pull/1905 +[#1905]: https://github.com/actix/actix-web/pull/1905 +[#1931]: https://github.com/actix/actix-web/pull/1931 +[#1969]: https://github.com/actix/actix-web/pull/1969 +[#1969]: https://github.com/actix/actix-web/pull/1969 +[#1981]: https://github.com/actix/actix-web/pull/1981 +[#2008]: https://github.com/actix/actix-web/pull/2008 +[#2024]: https://github.com/actix/actix-web/pull/2024 +[#2050]: https://github.com/actix/actix-web/pull/2050 +[#2063]: https://github.com/actix/actix-web/pull/2063 +[#2081]: https://github.com/actix/actix-web/pull/2081 +[#2081]: https://github.com/actix/actix-web/pull/2081 +[#2094]: https://github.com/actix/actix-web/pull/2094 +[#2114]: https://github.com/actix/actix-web/pull/2114 +[#2116]: https://github.com/actix/actix-web/pull/2116 +[#2148]: https://github.com/actix/actix-web/pull/2148 +[#2250]: https://github.com/actix/actix-web/pull/2250 +[#2310]: https://github.com/actix/actix-web/pull/2310 +[#2414]: https://github.com/actix/actix-web/pull/2414 +[#2425]: https://github.com/actix/actix-web/pull/2425 +[#2474]: https://github.com/actix/actix-web/pull/2474 +[#2503]: https://github.com/actix/actix-web/pull/2503 +[#2510]: https://github.com/actix/actix-web/pull/2510 +[#2546]: https://github.com/actix/actix-web/pull/2546 +[#2553]: https://github.com/actix/actix-web/pull/2553 +[#2555]: https://github.com/actix/actix-web/pull/2555 + + +
+3.0.0 Pre-Releases + +## 3.0.0-beta.21 - 2022-02-16 +- No significant changes since `3.0.0-beta.20`. + + +## 3.0.0-beta.20 - 2022-01-31 +- No significant changes since `3.0.0-beta.19`. ## 3.0.0-beta.19 - 2022-01-21 @@ -162,6 +269,7 @@ [#1813]: https://github.com/actix/actix-web/pull/1813 +
## 2.0.3 - 2020-11-29 ### Fixed diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 222765991..9da103cb0 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "awc" -version = "3.0.0-beta.19" +version = "3.0.0" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", ] -description = "Async HTTP and WebSocket client library built on the Actix ecosystem" +description = "Async HTTP and WebSocket client library" keywords = ["actix", "http", "framework", "async", "web"] categories = [ "network-programming", @@ -58,12 +58,12 @@ __compress = [] dangerous-h2c = [] [dependencies] -actix-codec = "0.4.1" -actix-service = "2.0.0" -actix-http = "3.0.0-beta.19" +actix-codec = "0.5" +actix-service = "2" +actix-http = { version = "3", features = ["http2", "ws"] } actix-rt = { version = "2.1", default-features = false } -actix-tls = { version = "3.0.0", features = ["connect", "uri"] } -actix-utils = "3.0.0" +actix-tls = { version = "3", features = ["connect", "uri"] } +actix-utils = "3" ahash = "0.7" base64 = "0.13" @@ -90,27 +90,27 @@ cookie = { version = "0.16", features = ["percent-encode"], optional = true } tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] } -trust-dns-resolver = { version = "0.20.0", optional = true } +trust-dns-resolver = { version = "0.21", optional = true } [dev-dependencies] -actix-http = { version = "3.0.0-beta.19", features = ["openssl"] } -actix-http-test = { version = "3.0.0-beta.11", features = ["openssl"] } +actix-http = { version = "3", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] } actix-server = "2" -actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] } -actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] } -actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.21", features = ["openssl"] } +actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] } +actix-tls = { version = "3", features = ["openssl", "rustls"] } +actix-utils = "3" +actix-web = { version = "4", features = ["openssl"] } brotli = "3.3.3" -const-str = "0.3" +const-str = "0.4" env_logger = "0.9" flate2 = "1.0.13" futures-util = { version = "0.3.7", default-features = false } static_assertions = "1.1" -rcgen = "0.8" -rustls-pemfile = "0.2" +rcgen = "0.9" +rustls-pemfile = "1" tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] } -zstd = "0.9" +zstd = "0.11" [[example]] name = "client" diff --git a/awc/README.md b/awc/README.md index 97e555d0d..db70f7332 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,15 +3,15 @@ > Async HTTP and WebSocket client library. [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.19)](https://docs.rs/awc/3.0.0-beta.19) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0)](https://docs.rs/awc/3.0.0) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.19/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.19) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0/status.svg)](https://deps.rs/crate/awc/3.0.0) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/awc) -- [Example Project](https://github.com/actix/examples/tree/HEAD/security/awc_https) +- [Example Project](https://github.com/actix/examples/tree/master/https-tls/awc-https) - Minimum Supported Rust Version (MSRV): 1.54 ## Example diff --git a/awc/src/any_body.rs b/awc/src/any_body.rs index d09a943ab..d9c259d8f 100644 --- a/awc/src/any_body.rs +++ b/awc/src/any_body.rs @@ -160,7 +160,7 @@ impl fmt::Debug for AnyBody { mod tests { use std::marker::PhantomPinned; - use static_assertions::{assert_impl_all, assert_not_impl_all}; + use static_assertions::{assert_impl_all, assert_not_impl_any}; use super::*; @@ -181,12 +181,12 @@ mod tests { } } - assert_impl_all!(AnyBody<()>: MessageBody, fmt::Debug, Send, Sync, Unpin); - assert_impl_all!(AnyBody>: MessageBody, fmt::Debug, Send, Sync, Unpin); - assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Send, Sync, Unpin); - assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Unpin); - assert_impl_all!(AnyBody: MessageBody); + assert_impl_all!(AnyBody<()>: Send, Sync, Unpin, fmt::Debug, MessageBody); + assert_impl_all!(AnyBody>: Send, Sync, Unpin, fmt::Debug, MessageBody); + assert_impl_all!(AnyBody: Send, Sync, Unpin, fmt::Debug, MessageBody); + assert_impl_all!(AnyBody: Unpin, fmt::Debug, MessageBody); + assert_impl_all!(AnyBody: Send, Sync, MessageBody); - assert_not_impl_all!(AnyBody: Send, Sync, Unpin); - assert_not_impl_all!(AnyBody: Send, Sync, Unpin); + assert_not_impl_any!(AnyBody: Send, Sync); + assert_not_impl_any!(AnyBody: Unpin); } diff --git a/awc/src/builder.rs b/awc/src/builder.rs index 16a4e9cb5..c101d18f0 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -165,7 +165,7 @@ where /// Add default header. /// - /// Headers added by this method get added to every request unless overriden by . + /// Headers added by this method get added to every request unless overridden by other methods. /// /// # Panics /// Panics if header name or value is invalid. diff --git a/awc/src/client/connection.rs b/awc/src/client/connection.rs index 456f119aa..9de4ece4f 100644 --- a/awc/src/client/connection.rs +++ b/awc/src/client/connection.rs @@ -337,7 +337,7 @@ where match self.get_mut() { Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_write(cx, buf), Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_write(cx, buf), - _ => unreachable!(H2_UNREACHABLE_WRITE), + _ => unreachable!("{}", H2_UNREACHABLE_WRITE), } } @@ -345,7 +345,7 @@ where match self.get_mut() { Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_flush(cx), Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_flush(cx), - _ => unreachable!(H2_UNREACHABLE_WRITE), + _ => unreachable!("{}", H2_UNREACHABLE_WRITE), } } @@ -353,7 +353,7 @@ where match self.get_mut() { Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_shutdown(cx), Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_shutdown(cx), - _ => unreachable!(H2_UNREACHABLE_WRITE), + _ => unreachable!("{}", H2_UNREACHABLE_WRITE), } } @@ -369,7 +369,7 @@ where Connection::Tls(ConnectionType::H1(conn)) => { Pin::new(conn).poll_write_vectored(cx, bufs) } - _ => unreachable!(H2_UNREACHABLE_WRITE), + _ => unreachable!("{}", H2_UNREACHABLE_WRITE), } } @@ -377,7 +377,7 @@ where match *self { Connection::Tcp(ConnectionType::H1(ref conn)) => conn.is_write_vectored(), Connection::Tls(ConnectionType::H1(ref conn)) => conn.is_write_vectored(), - _ => unreachable!(H2_UNREACHABLE_WRITE), + _ => unreachable!("{}", H2_UNREACHABLE_WRITE), } } } diff --git a/awc/src/client/connector.rs b/awc/src/client/connector.rs index 26c62b924..51d6e180b 100644 --- a/awc/src/client/connector.rs +++ b/awc/src/client/connector.rs @@ -246,7 +246,12 @@ where /// /// The default limit size is 100. pub fn limit(mut self, limit: usize) -> Self { - self.config.limit = limit; + if limit == 0 { + self.config.limit = u32::MAX as usize; + } else { + self.config.limit = limit; + } + self } diff --git a/awc/src/client/h1proto.rs b/awc/src/client/h1proto.rs index cf716db72..8738c2f7f 100644 --- a/awc/src/client/h1proto.rs +++ b/awc/src/client/h1proto.rs @@ -70,7 +70,7 @@ where let is_expect = if head.as_ref().headers.contains_key(EXPECT) { match body.size() { BodySize::None | BodySize::Sized(0) => { - let keep_alive = framed.codec_ref().keepalive(); + let keep_alive = framed.codec_ref().keep_alive(); framed.io_mut().on_release(keep_alive); // TODO: use a new variant or a new type better describing error violate @@ -83,12 +83,12 @@ where false }; - framed.send((head, body.size()).into()).await?; - let mut pin_framed = Pin::new(&mut framed); // special handle for EXPECT request. let (do_send, mut res_head) = if is_expect { + pin_framed.send((head, body.size()).into()).await?; + let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx)) .await .ok_or(ConnectError::Disconnected)??; @@ -97,13 +97,17 @@ where // and current head would be used as final response head. (head.status == StatusCode::CONTINUE, Some(head)) } else { + pin_framed.feed((head, body.size()).into()).await?; + (true, None) }; if do_send { // send request body match body.size() { - BodySize::None | BodySize::Sized(0) => {} + BodySize::None | BodySize::Sized(0) => { + poll_fn(|cx| pin_framed.as_mut().flush(cx)).await?; + } _ => send_body(body, pin_framed.as_mut()).await?, }; @@ -119,7 +123,7 @@ where match pin_framed.codec_ref().message_type() { h1::MessageType::None => { - let keep_alive = pin_framed.codec_ref().keepalive(); + let keep_alive = pin_framed.codec_ref().keep_alive(); pin_framed.io_mut().on_release(keep_alive); Ok((head, Payload::None)) @@ -223,7 +227,7 @@ impl Stream for PlStream { match ready!(this.framed.as_mut().next_item(cx)?) { Some(Some(chunk)) => Poll::Ready(Some(Ok(chunk))), Some(None) => { - let keep_alive = this.framed.codec_ref().keepalive(); + let keep_alive = this.framed.codec_ref().keep_alive(); this.framed.io_mut().on_release(keep_alive); Poll::Ready(None) } diff --git a/awc/src/client/pool.rs b/awc/src/client/pool.rs index 9d130412b..cc3e4d7c0 100644 --- a/awc/src/client/pool.rs +++ b/awc/src/client/pool.rs @@ -232,7 +232,7 @@ where None => { let (io, proto) = connector.call(req).await?; - // TODO: remove when http3 is added in support. + // NOTE: remove when http3 is added in support. assert!(proto != Protocol::Http3); if proto == Protocol::Http1 { diff --git a/awc/src/connect.rs b/awc/src/connect.rs index f93014a67..be1ea0fee 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -30,17 +30,35 @@ pub type BoxConnectorService = Rc< pub type BoxedSocket = Box; +/// Combined HTTP and WebSocket request type received by connection service. pub enum ConnectRequest { + /// Standard HTTP request. + /// + /// Contains the request head, body type, and optional pre-resolved socket address. Client(RequestHeadType, AnyBody, Option), + + /// Tunnel used by WebSocket connection requests. + /// + /// Contains the request head and optional pre-resolved socket address. Tunnel(RequestHead, Option), } +/// Combined HTTP response & WebSocket tunnel type returned from connection service. pub enum ConnectResponse { + /// Standard HTTP response. Client(ClientResponse), + + /// Tunnel used for WebSocket communication. + /// + /// Contains response head and framed HTTP/1.1 codec. Tunnel(ResponseHead, Framed), } impl ConnectResponse { + /// Unwraps type into HTTP response. + /// + /// # Panics + /// Panics if enum variant is not `Client`. pub fn into_client_response(self) -> ClientResponse { match self { ConnectResponse::Client(res) => res, @@ -50,6 +68,10 @@ impl ConnectResponse { } } + /// Unwraps type into WebSocket tunnel response. + /// + /// # Panics + /// Panics if enum variant is not `Tunnel`. pub fn into_tunnel_response(self) -> (ResponseHead, Framed) { match self { ConnectResponse::Tunnel(head, framed) => (head, framed), @@ -136,30 +158,37 @@ where ConnectRequestProj::Connection { fut, req } => { let connection = ready!(fut.poll(cx))?; let req = req.take().unwrap(); + match req { ConnectRequest::Client(head, body, ..) => { // send request let fut = ConnectRequestFuture::Client { fut: connection.send_request(head, body), }; + self.set(fut); } + ConnectRequest::Tunnel(head, ..) => { // send request let fut = ConnectRequestFuture::Tunnel { fut: connection.open_tunnel(RequestHeadType::from(head)), }; + self.set(fut); } } + self.poll(cx) } + ConnectRequestProj::Client { fut } => { let (head, payload) = ready!(fut.as_mut().poll(cx))?; Poll::Ready(Ok(ConnectResponse::Client(ClientResponse::new( head, payload, )))) } + ConnectRequestProj::Tunnel { fut } => { let (head, framed) = ready!(fut.as_mut().poll(cx))?; let framed = framed.into_map_io(|io| Box::new(io) as _); diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 970ca2d92..8d6ea759a 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -1,22 +1,25 @@ -//! `awc` is a HTTP and WebSocket client library built on the Actix ecosystem. +//! `awc` is an asynchronous HTTP and WebSocket client library. //! -//! # Making a GET request +//! # `GET` Requests //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), awc::error::SendRequestError> { +//! // create client //! let mut client = awc::Client::default(); -//! let response = client.get("http://www.rust-lang.org") // <- Create request builder -//! .insert_header(("User-Agent", "Actix-web")) -//! .send() // <- Send http request -//! .await?; //! -//! println!("Response: {:?}", response); +//! // construct request +//! let req = client.get("http://www.rust-lang.org") +//! .insert_header(("User-Agent", "awc/3.0")); +//! +//! // send request and await response +//! let res = req.send().await?; +//! println!("Response: {:?}", res); //! # Ok(()) //! # } //! ``` //! -//! # Making POST requests -//! ## Raw body contents +//! # `POST` Requests +//! ## Raw Body //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), awc::error::SendRequestError> { @@ -28,20 +31,6 @@ //! # } //! ``` //! -//! ## Forms -//! ```no_run -//! # #[actix_rt::main] -//! # async fn main() -> Result<(), awc::error::SendRequestError> { -//! let params = [("foo", "bar"), ("baz", "quux")]; -//! -//! let mut client = awc::Client::default(); -//! let response = client.post("http://httpbin.org/post") -//! .send_form(¶ms) -//! .await?; -//! # Ok(()) -//! # } -//! ``` -//! //! ## JSON //! ```no_run //! # #[actix_rt::main] @@ -59,6 +48,20 @@ //! # } //! ``` //! +//! ## URL Encoded Form +//! ```no_run +//! # #[actix_rt::main] +//! # async fn main() -> Result<(), awc::error::SendRequestError> { +//! let params = [("foo", "bar"), ("baz", "quux")]; +//! +//! let mut client = awc::Client::default(); +//! let response = client.post("http://httpbin.org/post") +//! .send_form(¶ms) +//! .await?; +//! # Ok(()) +//! # } +//! ``` +//! //! # Response Compression //! All [official][iana-encodings] and common content encoding codecs are supported, optionally. //! @@ -76,11 +79,12 @@ //! //! [iana-encodings]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding //! -//! # WebSocket support +//! # WebSockets //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), Box> { -//! use futures_util::{sink::SinkExt, stream::StreamExt}; +//! use futures_util::{sink::SinkExt as _, stream::StreamExt as _}; +//! //! let (_resp, mut connection) = awc::Client::new() //! .ws("ws://echo.websocket.org") //! .connect() @@ -89,8 +93,9 @@ //! connection //! .send(awc::ws::Message::Text("Echo".into())) //! .await?; +//! //! let response = connection.next().await.unwrap()?; -//! # assert_eq!(response, awc::ws::Frame::Text("Echo".as_bytes().into())); +//! assert_eq!(response, awc::ws::Frame::Text("Echo".into())); //! # Ok(()) //! # } //! ``` diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index ac6690471..d48822168 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -161,7 +161,8 @@ where | StatusCode::SEE_OTHER | StatusCode::TEMPORARY_REDIRECT | StatusCode::PERMANENT_REDIRECT - if *max_redirect_times > 0 => + if *max_redirect_times > 0 + && res.headers().contains_key(header::LOCATION) => { let reuse_body = res.head().status == StatusCode::TEMPORARY_REDIRECT || res.head().status == StatusCode::PERMANENT_REDIRECT; @@ -245,26 +246,32 @@ where } fn build_next_uri(res: &ClientResponse, prev_uri: &Uri) -> Result { - let uri = res - .headers() - .get(header::LOCATION) - .map(|value| { - // try to parse the location to a full uri - let uri = Uri::try_from(value.as_bytes()) - .map_err(|e| SendRequestError::Url(InvalidUrl::HttpError(e.into())))?; - if uri.scheme().is_none() || uri.authority().is_none() { - let uri = Uri::builder() - .scheme(prev_uri.scheme().cloned().unwrap()) - .authority(prev_uri.authority().cloned().unwrap()) - .path_and_query(value.as_bytes()) - .build()?; - Ok::<_, SendRequestError>(uri) - } else { - Ok(uri) - } - }) - // TODO: this error type is wrong. - .ok_or(SendRequestError::Url(InvalidUrl::MissingScheme))??; + // responses without this header are not processed + let location = res.headers().get(header::LOCATION).unwrap(); + + // try to parse the location and resolve to a full URI but fall back to default if it fails + let uri = Uri::try_from(location.as_bytes()).unwrap_or_else(|_| Uri::default()); + + let uri = if uri.scheme().is_none() || uri.authority().is_none() { + let builder = Uri::builder() + .scheme(prev_uri.scheme().cloned().unwrap()) + .authority(prev_uri.authority().cloned().unwrap()); + + // when scheme or authority is missing treat the location value as path and query + // recover error where location does not have leading slash + let path = if location.as_bytes().starts_with(b"/") { + location.as_bytes().to_owned() + } else { + [b"/", location.as_bytes()].concat() + }; + + builder + .path_and_query(path) + .build() + .map_err(|err| SendRequestError::Url(InvalidUrl::HttpError(err)))? + } else { + uri + }; Ok(uri) } @@ -287,10 +294,13 @@ mod tests { use actix_web::{web, App, Error, HttpRequest, HttpResponse}; use super::*; - use crate::{http::header::HeaderValue, ClientBuilder}; + use crate::{ + http::{header::HeaderValue, StatusCode}, + ClientBuilder, + }; #[actix_rt::test] - async fn test_basic_redirect() { + async fn basic_redirect() { let client = ClientBuilder::new() .disable_redirects() .wrap(Redirect::new().max_redirect_times(10)) @@ -315,6 +325,44 @@ mod tests { assert_eq!(res.status().as_u16(), 400); } + #[actix_rt::test] + async fn redirect_relative_without_leading_slash() { + let client = ClientBuilder::new().finish(); + + let srv = actix_test::start(|| { + App::new() + .service(web::resource("/").route(web::to(|| async { + HttpResponse::Found() + .insert_header(("location", "abc/")) + .finish() + }))) + .service( + web::resource("/abc/") + .route(web::to(|| async { HttpResponse::Accepted().finish() })), + ) + }); + + let res = client.get(srv.url("/")).send().await.unwrap(); + assert_eq!(res.status(), StatusCode::ACCEPTED); + } + + #[actix_rt::test] + async fn redirect_without_location() { + let client = ClientBuilder::new() + .disable_redirects() + .wrap(Redirect::new().max_redirect_times(10)) + .finish(); + + let srv = actix_test::start(|| { + App::new().service(web::resource("/").route(web::to(|| async { + Ok::<_, Error>(HttpResponse::Found().finish()) + }))) + }); + + let res = client.get(srv.url("/")).send().await.unwrap(); + assert_eq!(res.status(), StatusCode::FOUND); + } + #[actix_rt::test] async fn test_redirect_limit() { let client = ClientBuilder::new() @@ -328,14 +376,14 @@ mod tests { .service(web::resource("/").route(web::to(|| async { Ok::<_, Error>( HttpResponse::Found() - .append_header(("location", "/test")) + .insert_header(("location", "/test")) .finish(), ) }))) .service(web::resource("/test").route(web::to(|| async { Ok::<_, Error>( HttpResponse::Found() - .append_header(("location", "/test2")) + .insert_header(("location", "/test2")) .finish(), ) }))) @@ -345,8 +393,15 @@ mod tests { }); let res = client.get(srv.url("/")).send().await.unwrap(); - - assert_eq!(res.status().as_u16(), 302); + assert_eq!(res.status(), StatusCode::FOUND); + assert_eq!( + res.headers() + .get(header::LOCATION) + .unwrap() + .to_str() + .unwrap(), + "/test2" + ); } #[actix_rt::test] diff --git a/awc/src/request.rs b/awc/src/request.rs index 8bcf1ee01..102db3c16 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -505,7 +505,7 @@ impl fmt::Debug for ClientRequest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!( f, - "\nClientRequest {:?} {}:{}", + "\nClientRequest {:?} {} {}", self.head.version, self.head.method, self.head.uri )?; writeln!(f, " headers:")?; diff --git a/awc/src/responses/response.rs b/awc/src/responses/response.rs index 4e6a05f0f..c7c0a6362 100644 --- a/awc/src/responses/response.rs +++ b/awc/src/responses/response.rs @@ -160,7 +160,7 @@ where /// /// # Errors /// `Future` implementation returns error if: - /// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB) + /// - content length is greater than [limit](ResponseBody::limit) (default: 2 MiB) /// /// # Examples /// ```no_run diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index dceaf467d..165d8faf0 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -28,7 +28,7 @@ const S: &str = "Hello World "; const STR: &str = const_str::repeat!(S, 100); #[actix_rt::test] -async fn test_simple() { +async fn simple() { let srv = actix_test::start(|| { App::new().service( web::resource("/").route(web::to(|| async { HttpResponse::Ok().body(STR) })), @@ -56,7 +56,7 @@ async fn test_simple() { } #[actix_rt::test] -async fn test_json() { +async fn json() { let srv = actix_test::start(|| { App::new().service( web::resource("/").route(web::to(|_: web::Json| HttpResponse::Ok())), @@ -72,7 +72,7 @@ async fn test_json() { } #[actix_rt::test] -async fn test_form() { +async fn form() { let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to( |_: web::Form>| HttpResponse::Ok(), @@ -91,7 +91,7 @@ async fn test_form() { } #[actix_rt::test] -async fn test_timeout() { +async fn timeout() { let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|| async { actix_rt::time::sleep(Duration::from_millis(200)).await; @@ -116,7 +116,7 @@ async fn test_timeout() { } #[actix_rt::test] -async fn test_timeout_override() { +async fn timeout_override() { let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|| async { actix_rt::time::sleep(Duration::from_millis(200)).await; @@ -138,7 +138,7 @@ async fn test_timeout_override() { } #[actix_rt::test] -async fn test_response_timeout() { +async fn response_timeout() { use futures_util::stream::{once, StreamExt as _}; let srv = actix_test::start(|| { @@ -211,7 +211,7 @@ async fn test_response_timeout() { } #[actix_rt::test] -async fn test_connection_reuse() { +async fn connection_reuse() { let num = Arc::new(AtomicUsize::new(0)); let num2 = num.clone(); @@ -248,7 +248,7 @@ async fn test_connection_reuse() { } #[actix_rt::test] -async fn test_connection_force_close() { +async fn connection_force_close() { let num = Arc::new(AtomicUsize::new(0)); let num2 = num.clone(); @@ -285,7 +285,7 @@ async fn test_connection_force_close() { } #[actix_rt::test] -async fn test_connection_server_close() { +async fn connection_server_close() { let num = Arc::new(AtomicUsize::new(0)); let num2 = num.clone(); @@ -324,7 +324,7 @@ async fn test_connection_server_close() { } #[actix_rt::test] -async fn test_connection_wait_queue() { +async fn connection_wait_queue() { let num = Arc::new(AtomicUsize::new(0)); let num2 = num.clone(); @@ -373,7 +373,7 @@ async fn test_connection_wait_queue() { } #[actix_rt::test] -async fn test_connection_wait_queue_force_close() { +async fn connection_wait_queue_force_close() { let num = Arc::new(AtomicUsize::new(0)); let num2 = num.clone(); @@ -421,7 +421,7 @@ async fn test_connection_wait_queue_force_close() { } #[actix_rt::test] -async fn test_with_query_parameter() { +async fn with_query_parameter() { let srv = actix_test::start(|| { App::new().service(web::resource("/").to(|req: HttpRequest| { if req.query_string().contains("qp") { @@ -442,7 +442,7 @@ async fn test_with_query_parameter() { #[cfg(feature = "compress-gzip")] #[actix_rt::test] -async fn test_no_decompress() { +async fn no_decompress() { let srv = actix_test::start(|| { App::new() .wrap(actix_web::middleware::Compress::default()) @@ -480,7 +480,7 @@ async fn test_no_decompress() { #[cfg(feature = "compress-gzip")] #[actix_rt::test] -async fn test_client_gzip_encoding() { +async fn client_gzip_encoding() { let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|| async { HttpResponse::Ok() @@ -500,7 +500,7 @@ async fn test_client_gzip_encoding() { #[cfg(feature = "compress-gzip")] #[actix_rt::test] -async fn test_client_gzip_encoding_large() { +async fn client_gzip_encoding_large() { let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|| async { HttpResponse::Ok() @@ -520,7 +520,7 @@ async fn test_client_gzip_encoding_large() { #[cfg(feature = "compress-gzip")] #[actix_rt::test] -async fn test_client_gzip_encoding_large_random() { +async fn client_gzip_encoding_large_random() { let data = rand::thread_rng() .sample_iter(&rand::distributions::Alphanumeric) .take(100_000) @@ -546,7 +546,7 @@ async fn test_client_gzip_encoding_large_random() { #[cfg(feature = "compress-brotli")] #[actix_rt::test] -async fn test_client_brotli_encoding() { +async fn client_brotli_encoding() { let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|data: Bytes| async { HttpResponse::Ok() @@ -566,7 +566,7 @@ async fn test_client_brotli_encoding() { #[cfg(feature = "compress-brotli")] #[actix_rt::test] -async fn test_client_brotli_encoding_large_random() { +async fn client_brotli_encoding_large_random() { let data = rand::thread_rng() .sample_iter(&rand::distributions::Alphanumeric) .take(70_000) @@ -591,7 +591,7 @@ async fn test_client_brotli_encoding_large_random() { } #[actix_rt::test] -async fn test_client_deflate_encoding() { +async fn client_deflate_encoding() { let srv = actix_test::start(|| { App::new().default_service(web::to(|body: Bytes| async { HttpResponse::Ok().body(body) @@ -611,7 +611,7 @@ async fn test_client_deflate_encoding() { } #[actix_rt::test] -async fn test_client_deflate_encoding_large_random() { +async fn client_deflate_encoding_large_random() { let data = rand::thread_rng() .sample_iter(rand::distributions::Alphanumeric) .map(char::from) @@ -637,7 +637,7 @@ async fn test_client_deflate_encoding_large_random() { } #[actix_rt::test] -async fn test_client_streaming_explicit() { +async fn client_streaming_explicit() { let srv = actix_test::start(|| { App::new().default_service(web::to(|body: web::Payload| async { HttpResponse::Ok().streaming(body) @@ -659,7 +659,7 @@ async fn test_client_streaming_explicit() { } #[actix_rt::test] -async fn test_body_streaming_implicit() { +async fn body_streaming_implicit() { let srv = actix_test::start(|| { App::new().default_service(web::to(|| async { let body = @@ -681,7 +681,7 @@ async fn test_body_streaming_implicit() { } #[actix_rt::test] -async fn test_client_cookie_handling() { +async fn client_cookie_handling() { use std::io::{Error as IoError, ErrorKind}; let cookie1 = Cookie::build("cookie1", "value1").finish(); @@ -833,7 +833,7 @@ async fn client_bearer_auth() { } #[actix_rt::test] -async fn test_local_address() { +async fn local_address() { let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let srv = actix_test::start(move || { diff --git a/clippy.toml b/clippy.toml index ece14b8d2..5cccb362c 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.54" +msrv = "1.57" diff --git a/docs/graphs/dependency-graphs.md b/docs/graphs/README.md similarity index 100% rename from docs/graphs/dependency-graphs.md rename to docs/graphs/README.md diff --git a/docs/graphs/web-focus.dot b/docs/graphs/web-focus.dot index 63b3eaa82..a8c800b48 100644 --- a/docs/graphs/web-focus.dot +++ b/docs/graphs/web-focus.dot @@ -19,6 +19,7 @@ digraph { "web" -> { "codec" "service" "utils" "router" "rt" "server" "macros" "web-codegen" "http" "awc" } "web" -> { "tls" }[color=blue] // optional + "web-codegen" -> { "router" } "awc" -> { "codec" "service" "http" "rt" } "web-actors" -> { "actix" "web" "http" "codec" } "multipart" -> { "web" "service" "utils" } @@ -33,7 +34,7 @@ digraph { "utils" -> { "service" "rt" "codec" } "tracing" -> { "service" } "tls" -> { "service" "codec" "utils" } - "server" -> { "service" "rt" "codec" "utils" } + "server" -> { "service" "rt" "utils" } "rt" -> { "macros" } { rank=same; "utils" "codec" }; diff --git a/docs/graphs/web-only.dot b/docs/graphs/web-only.dot index b27dd0943..dad285bdf 100644 --- a/docs/graphs/web-only.dot +++ b/docs/graphs/web-only.dot @@ -2,23 +2,23 @@ digraph { subgraph cluster_web { label="actix/actix-web" "awc" - "actix-web" - "actix-files" - "actix-http" - "actix-multipart" - "actix-web-actors" - "actix-web-codegen" - "actix-http-test" - "actix-test" - "actix-router" + "web" + "files" + "http" + "multipart" + "web-actors" + "web-codegen" + "http-test" + "test" + "router" } - "actix-web" -> { "actix-web-codegen" "actix-http" "actix-router" } - "awc" -> { "actix-http" } - "actix-web-codegen" -> { "actix-router" } - "actix-web-actors" -> { "actix" "actix-web" "actix-http" } - "actix-multipart" -> { "actix-web" } - "actix-files" -> { "actix-web" } - "actix-http-test" -> { "awc" } - "actix-test" -> { "actix-web" "awc" "actix-http-test" } + "web" -> { "web-codegen" "http" "router" } + "awc" -> { "http" } + "web-codegen" -> { "router" }[color = red] + "web-actors" -> { "actix" "web" "http" } + "multipart" -> { "web" } + "files" -> { "web" } + "http-test" -> { "awc" } + "test" -> { "web" "awc" "http-test" } } diff --git a/scripts/ci-test b/scripts/ci-test index 567012d33..bdea1283a 100755 --- a/scripts/ci-test +++ b/scripts/ci-test @@ -14,7 +14,7 @@ save_exit_code() { save_exit_code cargo test --lib --tests -p=actix-router --all-features -- --nocapture save_exit_code cargo test --lib --tests -p=actix-http --all-features -- --nocapture -save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --nocapture --skip=test_reading_deflate_encoding_large_random_rustls +save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --nocapture save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features -- --nocapture save_exit_code cargo test --lib --tests -p=awc --all-features -- --nocapture save_exit_code cargo test --lib --tests -p=actix-http-test --all-features -- --nocapture @@ -25,4 +25,14 @@ save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features -- -- save_exit_code cargo test --workspace --doc +if [ "$EXIT" = "0" ]; then + PASSED="All tests passed!" + + if [ "$(command -v figlet)" ]; then + figlet "$PASSED" + else + echo "$PASSED" + fi +fi + exit $EXIT diff --git a/scripts/unreleased b/scripts/unreleased index 4dfa2d9ae..e664c0879 100755 --- a/scripts/unreleased +++ b/scripts/unreleased @@ -9,7 +9,16 @@ unreleased_for() { DIR=$1 CARGO_MANIFEST=$DIR/Cargo.toml - CHANGELOG_FILE=$DIR/CHANGES.md + + # determine changelog file name + if [ -f "$DIR/CHANGES.md" ]; then + CHANGELOG_FILE=$DIR/CHANGES.md + elif [ -f "$DIR/CHANGELOG.md" ]; then + CHANGELOG_FILE=$DIR/CHANGELOG.md + else + echo "No changelog file found" + exit 1 + fi # get current version PACKAGE_NAME="$(sed -nE 's/^name ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST" | head -n 1)" @@ -36,6 +45,6 @@ unreleased_for() { cat "$CHANGE_CHUNK_FILE" } -for f in $(fd --absolute-path CHANGES.md); do +for f in $(fd --absolute-path 'CHANGE\w+.md'); do unreleased_for $(dirname $f) done diff --git a/src/http/mod.rs b/src/http/mod.rs deleted file mode 100644 index 2581532cd..000000000 --- a/src/http/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Various HTTP related types. - -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/middleware/condition.rs b/src/middleware/condition.rs deleted file mode 100644 index 659f88bc9..000000000 --- a/src/middleware/condition.rs +++ /dev/null @@ -1,159 +0,0 @@ -//! For middleware documentation, see [`Condition`]. - -use std::task::{Context, Poll}; - -use actix_service::{Service, Transform}; -use actix_utils::future::Either; -use futures_core::future::LocalBoxFuture; -use futures_util::future::FutureExt as _; - -/// Middleware for conditionally enabling other middleware. -/// -/// The controlled middleware must not change the `Service` interfaces. This means you cannot -/// control such middlewares like `Logger` or `Compress` directly. See the [`Compat`](super::Compat) -/// middleware for a workaround. -/// -/// # Examples -/// ``` -/// use actix_web::middleware::{Condition, NormalizePath}; -/// use actix_web::App; -/// -/// let enable_normalize = std::env::var("NORMALIZE_PATH").is_ok(); -/// let app = App::new() -/// .wrap(Condition::new(enable_normalize, NormalizePath::default())); -/// ``` -pub struct Condition { - transformer: T, - enable: bool, -} - -impl Condition { - pub fn new(enable: bool, transformer: T) -> Self { - Self { - transformer, - enable, - } - } -} - -impl Transform for Condition -where - S: Service + 'static, - T: Transform, - T::Future: 'static, - T::InitError: 'static, - T::Transform: 'static, -{ - type Response = S::Response; - type Error = S::Error; - type Transform = ConditionMiddleware; - type InitError = T::InitError; - type Future = LocalBoxFuture<'static, Result>; - - fn new_transform(&self, service: S) -> Self::Future { - if self.enable { - let fut = self.transformer.new_transform(service); - async move { - let wrapped_svc = fut.await?; - Ok(ConditionMiddleware::Enable(wrapped_svc)) - } - .boxed_local() - } else { - async move { Ok(ConditionMiddleware::Disable(service)) }.boxed_local() - } - } -} - -pub enum ConditionMiddleware { - Enable(E), - Disable(D), -} - -impl Service for ConditionMiddleware -where - E: Service, - D: Service, -{ - type Response = E::Response; - type Error = E::Error; - type Future = Either; - - fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { - match self { - ConditionMiddleware::Enable(service) => service.poll_ready(cx), - ConditionMiddleware::Disable(service) => service.poll_ready(cx), - } - } - - fn call(&self, req: Req) -> Self::Future { - match self { - ConditionMiddleware::Enable(service) => Either::left(service.call(req)), - ConditionMiddleware::Disable(service) => Either::right(service.call(req)), - } - } -} - -#[cfg(test)] -mod tests { - use actix_service::IntoService; - use actix_utils::future::ok; - - use super::*; - use crate::{ - dev::{ServiceRequest, ServiceResponse}, - error::Result, - http::{ - header::{HeaderValue, CONTENT_TYPE}, - StatusCode, - }, - middleware::{err_handlers::*, Compat}, - test::{self, TestRequest}, - HttpResponse, - }; - - #[allow(clippy::unnecessary_wraps)] - fn render_500(mut res: ServiceResponse) -> Result> { - res.response_mut() - .headers_mut() - .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); - - Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) - } - - #[actix_rt::test] - async fn test_handler_enabled() { - let srv = |req: ServiceRequest| { - ok(req.into_response(HttpResponse::InternalServerError().finish())) - }; - - let mw = Compat::new( - ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), - ); - - let mw = Condition::new(true, mw) - .new_transform(srv.into_service()) - .await - .unwrap(); - let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await; - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - } - - #[actix_rt::test] - async fn test_handler_disabled() { - let srv = |req: ServiceRequest| { - ok(req.into_response(HttpResponse::InternalServerError().finish())) - }; - - let mw = Compat::new( - ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), - ); - - let mw = Condition::new(false, mw) - .new_transform(srv.into_service()) - .await - .unwrap(); - - let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await; - assert_eq!(resp.headers().get(CONTENT_TYPE), None); - } -}