diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..6164c657c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [robjtede] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2df863ae8..fa06a137a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -33,5 +33,5 @@ Please search on the [Actix Web issue tracker](https://github.com/actix/actix-we ## Your Environment -* Rust Version (I.e, output of `rustc -V`): -* Actix Web Version: +- Rust Version (I.e, output of `rustc -V`): +- Actix Web Version: diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml new file mode 100644 index 000000000..548ec21b7 --- /dev/null +++ b/.github/workflows/ci-master.yml @@ -0,0 +1,66 @@ +name: CI (master only) + +on: + push: + branches: [master] + +jobs: + ci_feature_powerset_check: + name: Verify Feature Combinations + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.2.0 + + - name: Install cargo-hack + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-hack + + - name: check feature combinations + uses: actions-rs/cargo@v1 + with: { command: ci-check-all-feature-powerset } + + - name: check feature combinations + uses: actions-rs/cargo@v1 + with: { command: ci-check-all-feature-powerset-linux } + + coverage: + name: coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.2.0 + + - name: Generate coverage file + run: | + cargo install cargo-tarpaulin --vers "^0.13" + cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose + - name: Upload to Codecov + uses: codecov/codecov-action@v1 + with: { file: cobertura.xml } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9b98a7b8..fe464bf27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,68 +96,6 @@ jobs: cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean cargo-cache - ci_feature_powerset_check: - name: Verify Feature Combinations - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Install stable - uses: actions-rs/toolchain@v1 - with: - toolchain: stable-x86_64-unknown-linux-gnu - profile: minimal - override: true - - - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: { command: generate-lockfile } - - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.2.0 - - - name: Install cargo-hack - uses: actions-rs/cargo@v1 - with: - command: install - args: cargo-hack - - - name: check feature combinations - uses: actions-rs/cargo@v1 - with: { command: ci-check-all-feature-powerset } - - - name: check feature combinations - uses: actions-rs/cargo@v1 - with: { command: ci-check-all-feature-powerset-linux } - - coverage: - name: coverage - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Install stable - uses: actions-rs/toolchain@v1 - with: - toolchain: stable-x86_64-unknown-linux-gnu - profile: minimal - override: true - - - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: { command: generate-lockfile } - - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.2.0 - - - name: Generate coverage file - if: github.ref == 'refs/heads/master' - run: | - cargo install cargo-tarpaulin --vers "^0.13" - cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose - - name: Upload to Codecov - if: github.ref == 'refs/heads/master' - uses: codecov/codecov-action@v1 - with: { file: cobertura.xml } - rustdoc: name: doc tests runs-on: ubuntu-latest diff --git a/CHANGES.md b/CHANGES.md index 1b108fee6..c870f10e7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,23 +1,89 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.17 - 2021-12-29 ### 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] +- `guard::GuardContext` for use with the `Guard` trait. [#2552] +- `ServiceRequest::guard_ctx` for obtaining a guard context. [#2552] ### 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] +- `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 -* 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] +- 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 @@ -25,24 +91,29 @@ [#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] +- 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] +- Compress middleware's response type is now `AnyBody>`. [#2448] ### Fixed -* Relax `Unpin` bound on `S` (stream) parameter of `HttpResponseBuilder::streaming`. [#2448] +- 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] +- `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 @@ -50,11 +121,11 @@ ## 4.0.0-beta.11 - 2021-11-15 ### Added -* Re-export `dev::ServerHandle` from `actix-server`. [#2442] +- 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] +- `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 @@ -62,18 +133,18 @@ ## 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] +- 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. +- 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] +- Useless `ServiceResponse::checked_expr` method. [#2401] [#2233]: https://github.com/actix/actix-web/pull/2233 [#2362]: https://github.com/actix/actix-web/pull/2362 @@ -86,17 +157,17 @@ ## 4.0.0-beta.9 - 2021-09-09 ### Added -* Re-export actix-service `ServiceFactory` in `dev` module. [#2325] +- 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. +- 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] +- 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 @@ -106,18 +177,18 @@ ## 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] +- 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] +- 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] +- 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 @@ -130,23 +201,23 @@ ## 4.0.0-beta.7 - 2021-06-17 ### Added -* `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200] +- `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] +- 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] +- `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] +- `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 @@ -156,11 +227,11 @@ ## 4.0.0-beta.6 - 2021-04-17 ### Added -* `HttpResponse` and `HttpResponseBuilder` structs. [#2065] +- `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`. +- 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 @@ -168,20 +239,20 @@ ## 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] +- `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] +- Double ampersand in Logger format is escaped correctly. [#2067] ### Changed -* `CustomResponder` would return error as `HttpResponse` when `CustomResponder::with_header` failed +- `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. +- 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` +- 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 @@ -193,8 +264,8 @@ ## 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 +- 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 @@ -202,36 +273,36 @@ ## 4.0.0-beta.3 - 2021-02-10 -* Update `actix-web-codegen` to `0.5.0-beta.1`. +- 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 +- 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] +- 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. +- 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` +- `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 +- `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] +- `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] +- 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 +- 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] @@ -247,26 +318,26 @@ ## 4.0.0-beta.1 - 2021-01-07 ### Added -* `Compat` middleware enabling generic response body/error type of middlewares like `Logger` and +- `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 +- 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. +- 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] +- 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 +- 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 +- 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 @@ -276,11 +347,19 @@ [#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] +- 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 @@ -288,15 +367,15 @@ ## 3.3.1 - 2020-11-29 -* Ensure `actix-http` dependency uses same `serde_urlencoded`. +- Ensure `actix-http` dependency uses same `serde_urlencoded`. ## 3.3.0 - 2020-11-25 ### Added -* Add `Either` extractor helper. [#1788] +- Add `Either` extractor helper. [#1788] ### Changed -* Upgrade `serde_urlencoded` to `0.7`. [#1773] +- 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 @@ -304,17 +383,17 @@ ## 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] +- 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`. +- 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 @@ -326,13 +405,13 @@ ## 3.1.0 - 2020-09-29 ### Changed -* Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allows `NormalizePath` +- 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` +- 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] +- `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 @@ -341,33 +420,33 @@ ## 3.0.2 - 2020-09-15 ### Fixed -* `NormalizePath` when used with `TrailingSlash::Trim` no longer trims the root path "/". [#1678] +- `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] +- `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`. +- 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 +- `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 +- 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::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 @@ -377,22 +456,22 @@ ## 3.0.0-beta.3 - 2020-08-17 ### Changed -* Update `rustls` to 0.18 +- 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 +- `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 +- `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 +- `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. +- Re-export all error types from `awc`. [#1621] +- MSRV is now 1.42.0. ### Fixed -* Memory leak of app data in pooled requests. [#1609] +- 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 @@ -403,29 +482,29 @@ ## 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 +- 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. +- `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 +- 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. +- `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] +- 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` +- 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 @@ -434,10 +513,10 @@ ### 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. +- `{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 @@ -449,16 +528,16 @@ ### 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. +- 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 +- 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 @@ -466,408 +545,408 @@ ### Changed -* Rename `HttpServer::start()` to `HttpServer::run()` +- Rename `HttpServer::start()` to `HttpServer::run()` -* Allow to gracefully stop test server via `TestServer::stop()` +- Allow to gracefully stop test server via `TestServer::stop()` -* Allow to specify multi-patterns for resources +- Allow to specify multi-patterns for resources ## [2.0.0-rc] - 2019-12-20 ### Changed -* Move `BodyEncoding` to `dev` module #1220 +- Move `BodyEncoding` to `dev` module #1220 -* Allow to set `peer_addr` for TestRequest #1074 +- Allow to set `peer_addr` for TestRequest #1074 -* Make web::Data deref to Arc #1214 +- Make web::Data deref to Arc #1214 -* Rename `App::register_data()` to `App::app_data()` +- Rename `App::register_data()` to `App::app_data()` -* `HttpRequest::app_data()` returns `Option<&T>` instead of `Option<&Data>` +- `HttpRequest::app_data()` returns `Option<&T>` instead of `Option<&Data>` ### Fixed -* Fix `AppConfig::secure()` is always false. #1202 +- Fix `AppConfig::secure()` is always false. #1202 ## [2.0.0-alpha.6] - 2019-12-15 ### Fixed -* Fixed compilation with default features off +- Fixed compilation with default features off ## [2.0.0-alpha.5] - 2019-12-13 ### Added -* Add test server, `test::start()` and `test::start_with()` +- 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 +- Delete HttpServer::run(), it is not useful with async/await ## [2.0.0-alpha.3] - 2019-12-07 ### Changed -* Migrate to tokio 0.2 +- Migrate to tokio 0.2 ## [2.0.0-alpha.1] - 2019-11-22 ### Changed -* Migrated to `std::future` +- Migrated to `std::future` -* Remove implementation of `Responder` for `()`. (#1167) +- 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) +- 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) +- 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 +- Add `Scope::register_data` and `Resource::register_data` methods, parallel to `App::register_data`. -* Add `middleware::Condition` that conditionally enables another middleware +- Add `middleware::Condition` that conditionally enables another middleware -* Allow to re-construct `ServiceRequest` from `HttpRequest` and `Payload` +- Allow to re-construct `ServiceRequest` from `HttpRequest` and `Payload` -* Add `HttpServer::listen_uds` for ability to listen on UDS FD rather than path, +- 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 +- Make UrlEncodedError::Overflow more informative -* Use actix-testing for testing utils +- Use actix-testing for testing utils ## [1.0.7] - 2019-08-29 ### Fixed -* Request Extensions leak #1062 +- Request Extensions leak #1062 ## [1.0.6] - 2019-08-28 ### Added -* Re-implement Host predicate (#989) +- Re-implement Host predicate (#989) -* Form implements Responder, returning a `application/x-www-form-urlencoded` response +- Form implements Responder, returning a `application/x-www-form-urlencoded` response -* Add `into_inner` to `Data` +- Add `into_inner` to `Data` -* Add `test::TestRequest::set_form()` convenience method to automatically serialize data and set +- 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. +- `Query` payload made `pub`. Allows user to pattern-match the payload. -* Enable `rust-tls` feature for client #1045 +- Enable `rust-tls` feature for client #1045 -* Update serde_urlencoded to 0.6.1 +- Update serde_urlencoded to 0.6.1 -* Update url to 2.1 +- Update url to 2.1 ## [1.0.5] - 2019-07-18 ### Added -* Unix domain sockets (HttpServer::bind_uds) #92 +- Unix domain sockets (HttpServer::bind_uds) #92 -* Actix now logs errors resulting in "internal server error" responses always, with the `error` +- 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 +- Restored logging of errors through the `Logger` middleware ## [1.0.4] - 2019-07-17 ### Added -* Add `Responder` impl for `(T, StatusCode) where T: Responder` +- Add `Responder` impl for `(T, StatusCode) where T: Responder` -* Allow to access app's resource map via +- Allow to access app's resource map via `ServiceRequest::resource_map()` and `HttpRequest::resource_map()` methods. ### Changed -* Upgrade `rand` dependency version to 0.7 +- Upgrade `rand` dependency version to 0.7 ## [1.0.3] - 2019-06-28 ### Added -* Support asynchronous data factories #850 +- Support asynchronous data factories #850 ### Changed -* Use `encoding_rs` crate instead of unmaintained `encoding` crate +- Use `encoding_rs` crate instead of unmaintained `encoding` crate ## [1.0.2] - 2019-06-17 ### Changed -* Move cors middleware to `actix-cors` crate. +- Move cors middleware to `actix-cors` crate. -* Move identity middleware to `actix-identity` crate. +- Move identity middleware to `actix-identity` crate. ## [1.0.1] - 2019-06-17 ### Added -* Add support for PathConfig #903 +- Add support for PathConfig #903 -* Add `middleware::identity::RequestIdentity` trait to `get_identity` from `HttpMessage`. +- Add `middleware::identity::RequestIdentity` trait to `get_identity` from `HttpMessage`. ### Changed -* Move cors middleware to `actix-cors` crate. +- Move cors middleware to `actix-cors` crate. -* Move identity middleware to `actix-identity` crate. +- Move identity middleware to `actix-identity` crate. -* Disable default feature `secure-cookies`. +- Disable default feature `secure-cookies`. -* Allow to test an app that uses async actors #897 +- Allow to test an app that uses async actors #897 -* Re-apply patch from #637 #894 +- Re-apply patch from #637 #894 ### Fixed -* HttpRequest::url_for is broken with nested scopes #915 +- HttpRequest::url_for is broken with nested scopes #915 ## [1.0.0] - 2019-06-05 ### Added -* Add `Scope::configure()` method. +- Add `Scope::configure()` method. -* Add `ServiceRequest::set_payload()` method. +- Add `ServiceRequest::set_payload()` method. -* Add `test::TestRequest::set_json()` convenience method to automatically +- 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 +- Add macros for head, options, trace, connect and patch http methods ### Changed -* Drop an unnecessary `Option<_>` indirection around `ServerBuilder` from `HttpServer`. #863 +- Drop an unnecessary `Option<_>` indirection around `ServerBuilder` from `HttpServer`. #863 ### Fixed -* Fix Logger request time format, and use rfc3339. #867 +- Fix Logger request time format, and use rfc3339. #867 -* Clear http requests pool on app service drop #860 +- 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. +- 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. +- `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 +- 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 +- Allow to set/override app data on scope level ### Changed -* `App::configure` take an `FnOnce` instead of `Fn` -* Upgrade actix-net crates +- `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()` +- Add helper function for executing futures `test::block_fn()` ### Changed -* Extractor configuration could be registered with `App::data()` +- 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 +- Route data is unified with app data, `Route::data()` moved to resource level to `Resource::data()` -* CORS handling without headers #702 +- CORS handling without headers #702 -* Allow constructing `Data` instances to avoid double `Arc` for `Send + Sync` types. +- Allow constructing `Data` instances to avoid double `Arc` for `Send + Sync` types. ### Fixed -* Fix `NormalizePath` middleware impl #806 +- Fix `NormalizePath` middleware impl #806 ### Deleted -* `App::data_factory()` is deleted. +- `App::data_factory()` is deleted. ## [1.0.0-beta.2] - 2019-04-24 ### Added -* Add raw services support via `web::service()` +- Add raw services support via `web::service()` -* Add helper functions for reading response body `test::read_body()` +- Add helper functions for reading response body `test::read_body()` -* Add support for `remainder match` (i.e "/path/{tail}*") +- Add support for `remainder match` (i.e "/path/{tail}*") -* Extend `Responder` trait, allow to override status code and headers. +- Extend `Responder` trait, allow to override status code and headers. -* Store visit and login timestamp in the identity cookie #502 +- Store visit and login timestamp in the identity cookie #502 ### Changed -* `.to_async()` handler can return `Responder` type #792 +- `.to_async()` handler can return `Responder` type #792 ### Fixed -* Fix async web::Data factory handling +- Fix async web::Data factory handling ## [1.0.0-beta.1] - 2019-04-20 ### Added -* Add helper functions for reading test response body, +- Add helper functions for reading test response body, `test::read_response()` and test::read_response_json()` -* Add `.peer_addr()` #744 +- Add `.peer_addr()` #744 -* Add `NormalizePath` middleware +- Add `NormalizePath` middleware ### Changed -* Rename `RouterConfig` to `ServiceConfig` +- Rename `RouterConfig` to `ServiceConfig` -* Rename `test::call_success` to `test::call_service` +- Rename `test::call_success` to `test::call_service` -* Removed `ServiceRequest::from_parts()` as it is unsafe to create from parts. +- Removed `ServiceRequest::from_parts()` as it is unsafe to create from parts. -* `CookieIdentityPolicy::max_age()` accepts value in seconds +- `CookieIdentityPolicy::max_age()` accepts value in seconds ### Fixed -* Fixed `TestRequest::app_data()` +- Fixed `TestRequest::app_data()` ## [1.0.0-alpha.6] - 2019-04-14 ### Changed -* Allow using any service as default service. +- Allow using any service as default service. -* Remove generic type for request payload, always use default. +- Remove generic type for request payload, always use default. -* Removed `Decompress` middleware. Bytes, String, Json, Form extractors +- Removed `Decompress` middleware. Bytes, String, Json, Form extractors automatically decompress payload. -* Make extractor config type explicit. Add `FromRequest::Config` associated type. +- 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. +- Added async io `TestBuffer` for testing. ### Deleted -* Removed native-tls support +- Removed native-tls support ## [1.0.0-alpha.4] - 2019-04-08 ### Added -* `App::configure()` allow to offload app configuration to different methods +- `App::configure()` allow to offload app configuration to different methods -* Added `URLPath` option for logger +- Added `URLPath` option for logger -* Added `ServiceRequest::app_data()`, returns `Data` +- Added `ServiceRequest::app_data()`, returns `Data` -* Added `ServiceFromRequest::app_data()`, returns `Data` +- Added `ServiceFromRequest::app_data()`, returns `Data` ### Changed -* `FromRequest` trait refactoring +- `FromRequest` trait refactoring -* Move multipart support to actix-multipart crate +- Move multipart support to actix-multipart crate ### Fixed -* Fix body propagation in Response::from_error. #760 +- 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_service()` to `TestRequest::to_srv_request()` -* Renamed `TestRequest::to_response()` to `TestRequest::to_srv_response()` +- Renamed `TestRequest::to_response()` to `TestRequest::to_srv_response()` -* Removed `Deref` impls +- Removed `Deref` impls ### Removed -* Removed unused `actix_web::web::md()` +- Removed unused `actix_web::web::md()` ## [1.0.0-alpha.2] - 2019-03-29 ### Added -* Rustls support +- Rustls support ### Changed -* Use forked cookie +- Use forked cookie -* Multipart::Field renamed to MultipartField +- Multipart::Field renamed to MultipartField ## [1.0.0-alpha.1] - 2019-03-28 ### Changed -* Complete architecture re-design. +- Complete architecture re-design. -* Return 405 response if no matching route found within resource #538 +- Return 405 response if no matching route found within resource #538 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index ae97b3240..dbd092095 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities diff --git a/Cargo.toml b/Cargo.toml index cee0680a5..1b85e8e75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.13" +version = "4.0.0-beta.17" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" keywords = ["actix", "http", "web", "framework", "async"] @@ -72,25 +72,24 @@ experimental-io-uring = ["actix-server/io-uring"] actix-codec = "0.4.1" actix-macros = "0.2.3" actix-rt = "2.3" -actix-server = "2.0.0-rc.1" +actix-server = "2.0.0-rc.2" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true } +actix-tls = { version = "3.0.0", default-features = false, optional = true } -actix-http = "3.0.0-beta.14" -actix-router = "0.5.0-beta.2" -actix-web-codegen = "0.5.0-beta.5" +actix-http = "3.0.0-beta.17" +actix-router = "0.5.0-beta.3" +actix-web-codegen = "0.5.0-beta.6" ahash = "0.7" bytes = "1" cfg-if = "1" cookie = { version = "0.15", features = ["percent-encode"], optional = true } derive_more = "0.99.5" -either = "1.5.3" encoding_rs = "0.8" futures-core = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false } -itoa = "0.4" +itoa = "1" language-tags = "0.3" once_cell = "1.5" log = "0.4" @@ -107,8 +106,8 @@ time = { version = "0.3", default-features = false, features = ["formatting"] } url = "2.1" [dev-dependencies] -actix-test = { version = "0.1.0-beta.7", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.11", features = ["openssl"] } +actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } +awc = { version = "3.0.0-beta.16", features = ["openssl"] } brotli2 = "0.3.2" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/MIGRATION.md b/MIGRATION.md index d53bd7bf8..338a04389 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,6 +1,6 @@ ## Unreleased -* The default `NormalizePath` behavior now strips trailing slashes by default. This was +- 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. @@ -11,9 +11,9 @@ Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`. -* The `type Config` of `FromRequest` was removed. +- The `type Config` of `FromRequest` was removed. -* Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd). +- 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` @@ -28,30 +28,30 @@ ## 3.0.0 -* The return type for `ServiceRequest::app_data::()` was changed from returning a `Data` to +- 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: +- 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 +- 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 +- 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 +- 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. +- 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 +- `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 +- 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 @@ -71,35 +71,35 @@ } ``` -* `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one. +- `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::maxconn` is renamed to the more expressive `HttpServer::max_connections`. -* `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`. +- `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 +- `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`. +- `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()` +- 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()` +- 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 +- `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 +- `ResponseError` trait has been reafctored. `ResponseError::error_response()` renders http response. -* Feature `rust-tls` renamed to `rustls` +- Feature `rust-tls` renamed to `rustls` instead of @@ -113,7 +113,7 @@ actix-web = { version = "2.0.0", features = ["rustls"] } ``` -* Feature `ssl` renamed to `openssl` +- Feature `ssl` renamed to `openssl` instead of @@ -126,11 +126,11 @@ ```rust actix-web = { version = "2.0.0", features = ["openssl"] } ``` -* `Cors` builder now requires that you call `.finish()` to construct the middleware +- `Cors` builder now requires that you call `.finish()` to construct the middleware ## 1.0.1 -* Cors middleware has been moved to `actix-cors` crate +- Cors middleware has been moved to `actix-cors` crate instead of @@ -144,7 +144,7 @@ use actix_cors::Cors; ``` -* Identity middleware has been moved to `actix-identity` crate +- Identity middleware has been moved to `actix-identity` crate instead of @@ -161,7 +161,7 @@ ## 1.0.0 -* Extractor configuration. In version 1.0 this is handled with the new `Data` mechanism for both setting and retrieving the configuration +- Extractor configuration. In version 1.0 this is handled with the new `Data` mechanism for both setting and retrieving the configuration instead of @@ -219,7 +219,7 @@ ) ``` -* Resource registration. 1.0 version uses generalized resource +- Resource registration. 1.0 version uses generalized resource registration via `.service()` method. instead of @@ -239,7 +239,7 @@ .route(web::post().to(post_handler)) ``` -* Scope registration. +- Scope registration. instead of @@ -263,7 +263,7 @@ ); ``` -* `.with()`, `.with_async()` registration methods have been renamed to `.to()` and `.to_async()`. +- `.with()`, `.with_async()` registration methods have been renamed to `.to()` and `.to_async()`. instead of @@ -277,7 +277,7 @@ App.new().service(web::resource("/welcome").to(welcome)) ``` -* Passing arguments to handler with extractors, multiple arguments are allowed +- Passing arguments to handler with extractors, multiple arguments are allowed instead of @@ -295,7 +295,7 @@ } ``` -* `.f()`, `.a()` and `.h()` handler registration methods have been removed. +- `.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. @@ -311,7 +311,7 @@ App.new().service(web::resource("/welcome").to(welcome)) ``` -* `HttpRequest` does not provide access to request's payload stream. +- `HttpRequest` does not provide access to request's payload stream. instead of @@ -341,7 +341,7 @@ } ``` -* `State` is now `Data`. You register Data during the App initialization process +- `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. @@ -377,7 +377,7 @@ ``` -* AsyncResponder is removed, use `.to_async()` registration method and `impl Future<>` as result type. +- AsyncResponder is removed, use `.to_async()` registration method and `impl Future<>` as result type. instead of @@ -393,7 +393,7 @@ .. simply omit AsyncResponder and the corresponding responder() finish method -* Middleware +- Middleware instead of @@ -410,7 +410,7 @@ .route("/index.html", web::get().to(index)); ``` -* `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()` +- `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()` method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead. instead of @@ -432,9 +432,9 @@ } ``` -* `actix_web::server` module has been removed. To start http server use `actix_web::HttpServer` type +- `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. +- StaticFiles and NamedFile have been moved to a separate crate. instead of `use actix_web::fs::StaticFile` @@ -444,20 +444,20 @@ use `use actix_files::NamedFile` -* Multipart has been moved to a separate crate. +- 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. +- Response compression is not enabled by default. To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`. -* Session middleware moved to actix-session crate +- Session middleware moved to actix-session crate -* Actors support have been moved to `actix-web-actors` crate +- Actors support have been moved to `actix-web-actors` crate -* Custom Error +- 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. @@ -471,7 +471,7 @@ ## 0.7.15 -* The `' '` character is not percent decoded anymore before matching routes. If you need to use it in +- 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 @@ -496,18 +496,18 @@ } ``` -* If you used `AsyncResult::async` you need to replace it with `AsyncResult::future` +- 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 +- `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 +- `HttpRequest` does not implement `Stream` anymore. If you need to read request payload use `HttpMessage::payload()` method. instead of @@ -533,10 +533,10 @@ } ``` -* [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html) +- [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. +- Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead. instead of @@ -550,17 +550,17 @@ fn index((query, json): (Query<..>, Json impl Responder {} ``` -* `Handler::handle()` uses `&self` instead of `&mut self` +- `Handler::handle()` uses `&self` instead of `&mut self` -* `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value +- `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value -* Removed deprecated `HttpServer::threads()`, use +- 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 +- Renamed `client::ClientConnectorError::Connector` to `client::ClientConnectorError::Resolver` -* `Route::with()` does not return `ExtractorConfig`, to configure +- `Route::with()` does not return `ExtractorConfig`, to configure extractor use `Route::with_config()` instead of @@ -589,26 +589,26 @@ } ``` -* `Route::with_async()` does not return `ExtractorConfig`, to configure +- `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` +- `Path` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest` -* `ws::Message::Close` now includes optional close reason. +- `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::threads()` renamed to `HttpServer::workers()`. -* `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated. +- `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()` returns read only reference to the request's Extension `HttpRequest::extensions_mut()` returns mutable reference. -* Instead of +- Instead of `use actix_web::middleware::{ CookieSessionBackend, CookieSessionError, RequestSession, @@ -619,15 +619,15 @@ `use actix_web::middleware::session{CookieSessionBackend, CookieSessionError, RequestSession, Session, SessionBackend, SessionImpl, SessionStorage};` -* `FromRequest::from_request()` accepts mutable reference to a request +- `FromRequest::from_request()` accepts mutable reference to a request -* `FromRequest::Result` has to implement `Into>` +- `FromRequest::Result` has to implement `Into>` -* [`Responder::respond_to()`]( +- [`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()`. +- Use `Query` extractor instead of HttpRequest::query()`. ```rust fn index(q: Query>) -> Result<..> { @@ -641,37 +641,37 @@ let q = Query::>::extract(req); ``` -* Websocket operations are implemented as `WsWriter` trait. +- Websocket operations are implemented as `WsWriter` trait. you need to use `use actix_web::ws::WsWriter` ## 0.5 -* `HttpResponseBuilder::body()`, `.finish()`, `.json()` +- `HttpResponseBuilder::body()`, `.finish()`, `.json()` methods return `HttpResponse` instead of `Result` -* `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version` +- `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version` moved to `actix_web::http` module -* `actix_web::header` moved to `actix_web::http::header` +- `actix_web::header` moved to `actix_web::http::header` -* `NormalizePath` moved to `actix_web::http` module +- `NormalizePath` moved to `actix_web::http` module -* `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function, +- `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 +- `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. +- `StaticFiles::new()`'s show_index parameter removed, use `show_files_listing()` method instead. -* `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type +- `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type -* `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()` +- `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>` +- `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>` instead of `Result<_, http::Error>` -* `Application` renamed to a `App` +- `Application` renamed to a `App` -* `actix_web::Reply`, `actix_web::Resource` moved to `actix_web::dev` +- `actix_web::Reply`, `actix_web::Resource` moved to `actix_web::dev` diff --git a/README.md b/README.md index c363ece9b..afe6b1f8e 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) -[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.13)](https://docs.rs/actix-web/4.0.0-beta.13) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.17)](https://docs.rs/actix-web/4.0.0-beta.17) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.13/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.13) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.17/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.17)
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) @@ -21,25 +21,25 @@ ## 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.52+ +- 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.52+ ## 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) +- [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 @@ -71,18 +71,18 @@ async fn main() -> std::io::Result<()> { ### 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/) +- [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. @@ -96,9 +96,9 @@ One of the fastest web frameworks available according to the This project is licensed under either of -* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or +- 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 +- MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT]) at your option. diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 63d8efc3f..65007c955 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -3,39 +3,51 @@ ## Unreleased - 2021-xx-xx +## 0.6.0-beta.12 - 2021-12-29 +* No significant changes since `0.6.0-beta.11`. + + +## 0.6.0-beta.11 - 2021-12-27 +* No significant changes since `0.6.0-beta.10`. + + +## 0.6.0-beta.10 - 2021-12-11 +- No significant changes since `0.6.0-beta.9`. + + ## 0.6.0-beta.9 - 2021-11-22 -* Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408] -* Add `NamedFile::open_async`. [#2408] -* Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453] -* The `Responder` impl for `NamedFile` now has a boxed future associated type. [#2408] -* The `Service` impl for `NamedFileService` now has a boxed future associated type. [#2408] -* Add `impl Clone` for `FilesService`. [#2408] +- Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408] +- Add `NamedFile::open_async`. [#2408] +- Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453] +- The `Responder` impl for `NamedFile` now has a boxed future associated type. [#2408] +- The `Service` impl for `NamedFileService` now has a boxed future associated type. [#2408] +- Add `impl Clone` for `FilesService`. [#2408] [#2408]: https://github.com/actix/actix-web/pull/2408 [#2453]: https://github.com/actix/actix-web/pull/2453 ## 0.6.0-beta.8 - 2021-10-20 -* Minimum supported Rust version (MSRV) is now 1.52. +- Minimum supported Rust version (MSRV) is now 1.52. ## 0.6.0-beta.7 - 2021-09-09 -* Minimum supported Rust version (MSRV) is now 1.51. +- Minimum supported Rust version (MSRV) is now 1.51. ## 0.6.0-beta.6 - 2021-06-26 -* Added `Files::path_filter()`. [#2274] -* `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228] +- Added `Files::path_filter()`. [#2274] +- `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228] [#2274]: https://github.com/actix/actix-web/pull/2274 [#2228]: https://github.com/actix/actix-web/pull/2228 ## 0.6.0-beta.5 - 2021-06-17 -* `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135] -* For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156] -* `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225] -* `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257] +- `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135] +- For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156] +- `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225] +- `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257] [#2135]: https://github.com/actix/actix-web/pull/2135 [#2156]: https://github.com/actix/actix-web/pull/2156 @@ -44,130 +56,130 @@ ## 0.6.0-beta.4 - 2021-04-02 -* Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046] +- Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046] [#2046]: https://github.com/actix/actix-web/pull/2046 ## 0.6.0-beta.3 - 2021-03-09 -* No notable changes. +- No notable changes. ## 0.6.0-beta.2 - 2021-02-10 -* Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887] -* Replace `v_htmlescape` with `askama_escape`. [#1953] +- Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887] +- Replace `v_htmlescape` with `askama_escape`. [#1953] [#1887]: https://github.com/actix/actix-web/pull/1887 [#1953]: https://github.com/actix/actix-web/pull/1953 ## 0.6.0-beta.1 - 2021-01-07 -* `HttpRange::parse` now has its own error type. -* Update `bytes` to `1.0`. [#1813] +- `HttpRange::parse` now has its own error type. +- Update `bytes` to `1.0`. [#1813] [#1813]: https://github.com/actix/actix-web/pull/1813 ## 0.5.0 - 2020-12-26 -* Optionally support hidden files/directories. [#1811] +- Optionally support hidden files/directories. [#1811] [#1811]: https://github.com/actix/actix-web/pull/1811 ## 0.4.1 - 2020-11-24 -* Clarify order of parameters in `Files::new` and improve docs. +- Clarify order of parameters in `Files::new` and improve docs. ## 0.4.0 - 2020-10-06 -* Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714] +- Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714] [#1714]: https://github.com/actix/actix-web/pull/1714 ## 0.3.0 - 2020-09-11 -* No significant changes from 0.3.0-beta.1. +- No significant changes from 0.3.0-beta.1. ## 0.3.0-beta.1 - 2020-07-15 -* Update `v_htmlescape` to 0.10 -* Update `actix-web` and `actix-http` dependencies to beta.1 +- Update `v_htmlescape` to 0.10 +- Update `actix-web` and `actix-http` dependencies to beta.1 ## 0.3.0-alpha.1 - 2020-05-23 -* Update `actix-web` and `actix-http` dependencies to alpha -* Fix some typos in the docs -* Bump minimum supported Rust version to 1.40 -* Support sending Content-Length when Content-Range is specified [#1384] +- Update `actix-web` and `actix-http` dependencies to alpha +- Fix some typos in the docs +- Bump minimum supported Rust version to 1.40 +- Support sending Content-Length when Content-Range is specified [#1384] [#1384]: https://github.com/actix/actix-web/pull/1384 ## 0.2.1 - 2019-12-22 -* Use the same format for file URLs regardless of platforms +- Use the same format for file URLs regardless of platforms ## 0.2.0 - 2019-12-20 -* Fix BodyEncoding trait import #1220 +- Fix BodyEncoding trait import #1220 ## 0.2.0-alpha.1 - 2019-12-07 -* Migrate to `std::future` +- Migrate to `std::future` ## 0.1.7 - 2019-11-06 -* Add an additional `filename*` param in the `Content-Disposition` header of +- Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151) ## 0.1.6 - 2019-10-14 -* Add option to redirect to a slash-ended path `Files` #1132 +- Add option to redirect to a slash-ended path `Files` #1132 ## 0.1.5 - 2019-10-08 -* Bump up `mime_guess` crate version to 2.0.1 -* Bump up `percent-encoding` crate version to 2.1 -* Allow user defined request guards for `Files` #1113 +- Bump up `mime_guess` crate version to 2.0.1 +- Bump up `percent-encoding` crate version to 2.1 +- Allow user defined request guards for `Files` #1113 ## 0.1.4 - 2019-07-20 -* Allow to disable `Content-Disposition` header #686 +- Allow to disable `Content-Disposition` header #686 ## 0.1.3 - 2019-06-28 -* Do not set `Content-Length` header, let actix-http set it #930 +- Do not set `Content-Length` header, let actix-http set it #930 ## 0.1.2 - 2019-06-13 -* Content-Length is 0 for NamedFile HEAD request #914 -* Fix ring dependency from actix-web default features for #741 +- Content-Length is 0 for NamedFile HEAD request #914 +- Fix ring dependency from actix-web default features for #741 ## 0.1.1 - 2019-06-01 -* Static files are incorrectly served as both chunked and with length #812 +- Static files are incorrectly served as both chunked and with length #812 ## 0.1.0 - 2019-05-25 -* NamedFile last-modified check always fails due to nano-seconds in file modified date #820 +- NamedFile last-modified check always fails due to nano-seconds in file modified date #820 ## 0.1.0-beta.4 - 2019-05-12 -* Update actix-web to beta.4 +- Update actix-web to beta.4 ## 0.1.0-beta.1 - 2019-04-20 -* Update actix-web to beta.1 +- Update actix-web to beta.1 ## 0.1.0-alpha.6 - 2019-04-14 -* Update actix-web to alpha6 +- Update actix-web to alpha6 ## 0.1.0-alpha.4 - 2019-04-08 -* Update actix-web to alpha4 +- Update actix-web to alpha4 ## 0.1.0-alpha.2 - 2019-04-02 -* Add default handler support +- Add default handler support ## 0.1.0-alpha.1 - 2019-03-28 -* Initial impl +- Initial impl diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 55b7988b1..f46bbe357 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.0-beta.9" +version = "0.6.0-beta.12" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -22,17 +22,17 @@ path = "src/lib.rs" experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"] [dependencies] -actix-web = { version = "4.0.0-beta.11", default-features = false } -actix-http = "3.0.0-beta.14" +actix-http = "3.0.0-beta.17" actix-service = "2" actix-utils = "3" +actix-web = { version = "4.0.0-beta.17", default-features = false } askama_escape = "0.10" bitflags = "1" bytes = "1" +derive_more = "0.99.5" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } http-range = "0.1.4" -derive_more = "0.99.5" log = "0.4" mime = "0.3" mime_guess = "2.0.1" @@ -43,6 +43,6 @@ tokio-uring = { version = "0.1", optional = true } [dev-dependencies] actix-rt = "2.2" -actix-web = "4.0.0-beta.11" -actix-test = "0.1.0-beta.7" +actix-test = "0.1.0-beta.10" +actix-web = "4.0.0-beta.17" tempfile = "3.2" diff --git a/actix-files/README.md b/actix-files/README.md index 84e556fa9..3f310a607 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,11 +3,11 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.9)](https://docs.rs/actix-files/0.6.0-beta.9) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.12)](https://docs.rs/actix-files/0.6.0-beta.12) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.9/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.9) +[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.12/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.12) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index 06909bf08..d1dd6739d 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -262,9 +262,9 @@ impl Files { self } + /// See [`Files::method_guard`]. #[doc(hidden)] #[deprecated(since = "0.6.0", note = "Renamed to `method_guard`.")] - /// See [`Files::method_guard`]. pub fn use_guards(self, guard: G) -> Self { self.method_guard(guard) } diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index c488f66a5..050c7df69 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -11,8 +11,8 @@ //! .service(Files::new("/static", ".").prefer_utf8(true)); //! ``` -#![deny(rust_2018_idioms)] -#![warn(missing_docs, missing_debug_implementations)] +#![deny(rust_2018_idioms, nonstandard_style)] +#![warn(future_incompatible, missing_docs, missing_debug_implementations)] use actix_service::boxed::{BoxService, BoxServiceFactory}; use actix_web::{ diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 0848543a8..810988f0c 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -2,14 +2,10 @@ use std::{ fmt, fs::Metadata, io, - ops::{Deref, DerefMut}, path::{Path, PathBuf}, time::{SystemTime, UNIX_EPOCH}, }; -#[cfg(unix)] -use std::os::unix::fs::MetadataExt; - use actix_service::{Service, ServiceFactory}; use actix_web::{ body::{self, BoxBody, SizedStream}, @@ -27,6 +23,7 @@ use actix_web::{ Error, HttpMessage, HttpRequest, HttpResponse, Responder, }; use bitflags::bitflags; +use derive_more::{Deref, DerefMut}; use futures_core::future::LocalBoxFuture; use mime_guess::from_path; @@ -71,8 +68,11 @@ impl Default for Flags { /// NamedFile::open_async("./static/index.html").await /// } /// ``` +#[derive(Deref, DerefMut)] pub struct NamedFile { path: PathBuf, + #[deref] + #[deref_mut] file: File, modified: Option, pub(crate) md: Metadata, @@ -364,14 +364,18 @@ impl NamedFile { self } + /// Creates a etag in a format is similar to Apache's. pub(crate) fn etag(&self) -> Option { - // This etag format is similar to Apache's. self.modified.as_ref().map(|mtime| { let ino = { #[cfg(unix)] { + #[cfg(unix)] + use std::os::unix::fs::MetadataExt as _; + self.md.ino() } + #[cfg(not(unix))] { 0 @@ -472,17 +476,17 @@ impl NamedFile { false }; - let mut resp = HttpResponse::build(self.status_code); + let mut res = HttpResponse::build(self.status_code); if self.flags.contains(Flags::PREFER_UTF8) { let ct = equiv_utf8_text(self.content_type.clone()); - resp.insert_header((header::CONTENT_TYPE, ct.to_string())); + res.insert_header((header::CONTENT_TYPE, ct.to_string())); } else { - resp.insert_header((header::CONTENT_TYPE, self.content_type.to_string())); + res.insert_header((header::CONTENT_TYPE, self.content_type.to_string())); } if self.flags.contains(Flags::CONTENT_DISPOSITION) { - resp.insert_header(( + res.insert_header(( header::CONTENT_DISPOSITION, self.content_disposition.to_string(), )); @@ -490,18 +494,18 @@ impl NamedFile { // default compressing if let Some(current_encoding) = self.encoding { - resp.encoding(current_encoding); + res.encoding(current_encoding); } if let Some(lm) = last_modified { - resp.insert_header((header::LAST_MODIFIED, lm.to_string())); + res.insert_header((header::LAST_MODIFIED, lm.to_string())); } if let Some(etag) = etag { - resp.insert_header((header::ETAG, etag.to_string())); + res.insert_header((header::ETAG, etag.to_string())); } - resp.insert_header((header::ACCEPT_RANGES, "bytes")); + res.insert_header((header::ACCEPT_RANGES, "bytes")); let mut length = self.md.len(); let mut offset = 0; @@ -513,24 +517,24 @@ impl NamedFile { length = ranges[0].length; offset = ranges[0].start; - resp.encoding(ContentEncoding::Identity); - resp.insert_header(( + res.encoding(ContentEncoding::Identity); + res.insert_header(( header::CONTENT_RANGE, format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()), )); } else { - resp.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length))); - return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish(); + res.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length))); + return res.status(StatusCode::RANGE_NOT_SATISFIABLE).finish(); }; } else { - return resp.status(StatusCode::BAD_REQUEST).finish(); + return res.status(StatusCode::BAD_REQUEST).finish(); }; }; if precondition_failed { - return resp.status(StatusCode::PRECONDITION_FAILED).finish(); + return res.status(StatusCode::PRECONDITION_FAILED).finish(); } else if not_modified { - return resp + return res .status(StatusCode::NOT_MODIFIED) .body(body::None::new()) .map_into_boxed_body(); @@ -539,10 +543,10 @@ impl NamedFile { let reader = chunked::new_chunked_read(length, offset, self.file); if offset != 0 || length != self.md.len() { - resp.status(StatusCode::PARTIAL_CONTENT); + res.status(StatusCode::PARTIAL_CONTENT); } - resp.body(SizedStream::new(length, reader)) + res.body(SizedStream::new(length, reader)) } } @@ -586,20 +590,6 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { } } -impl Deref for NamedFile { - type Target = File; - - fn deref(&self) -> &Self::Target { - &self.file - } -} - -impl DerefMut for NamedFile { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.file - } -} - impl Responder for NamedFile { type Body = BoxBody; diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index f6e1c2e11..057dbe5a3 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -1,8 +1,8 @@ use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc}; -use actix_service::Service; use actix_web::{ - dev::{ServiceRequest, ServiceResponse}, + body::BoxBody, + dev::{Service, ServiceRequest, ServiceResponse}, error::Error, guard::Guard, http::{header, Method}, @@ -94,7 +94,7 @@ impl fmt::Debug for FilesService { } impl Service for FilesService { - type Response = ServiceResponse; + type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; @@ -103,7 +103,7 @@ impl Service for FilesService { fn call(&self, req: ServiceRequest) -> Self::Future { let is_method_valid = if let Some(guard) = &self.guards { // execute user defined guards - (**guard).check(req.head()) + (**guard).check(&req.guard_ctx()) } else { // default behavior matches!(*req.method(), Method::HEAD | Method::GET) diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 6984e5962..8c6a63b72 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -3,122 +3,132 @@ ## Unreleased - 2021-xx-xx +## 3.0.0-beta.10 - 2021-12-27 +- Update `actix-server` to `2.0.0-rc.2`. [#2550] + +[#2550]: https://github.com/actix/actix-web/pull/2550 + + +## 3.0.0-beta.9 - 2021-12-11 +- No significant changes since `3.0.0-beta.8`. + + ## 3.0.0-beta.8 - 2021-11-30 -* Update `actix-tls` to `3.0.0-rc.1`. [#2474] +- Update `actix-tls` to `3.0.0-rc.1`. [#2474] [#2474]: https://github.com/actix/actix-web/pull/2474 ## 3.0.0-beta.7 - 2021-11-22 -* Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] +- Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] [#2408]: https://github.com/actix/actix-web/pull/2408 ## 3.0.0-beta.6 - 2021-11-15 -* `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442] -* Update `actix-server` to `2.0.0-beta.9`. [#2442] -* Minimum supported Rust version (MSRV) is now 1.52. +- `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442] +- Update `actix-server` to `2.0.0-beta.9`. [#2442] +- Minimum supported Rust version (MSRV) is now 1.52. [#2442]: https://github.com/actix/actix-web/pull/2442 ## 3.0.0-beta.5 - 2021-09-09 -* Minimum supported Rust version (MSRV) is now 1.51. +- Minimum supported Rust version (MSRV) is now 1.51. ## 3.0.0-beta.4 - 2021-04-02 -* Added `TestServer::client_headers` method. [#2097] +- Added `TestServer::client_headers` method. [#2097] [#2097]: https://github.com/actix/actix-web/pull/2097 ## 3.0.0-beta.3 - 2021-03-09 -* No notable changes. +- No notable changes. ## 3.0.0-beta.2 - 2021-02-10 -* No notable changes. +- No notable changes. ## 3.0.0-beta.1 - 2021-01-07 -* Update `bytes` to `1.0`. [#1813] +- Update `bytes` to `1.0`. [#1813] [#1813]: https://github.com/actix/actix-web/pull/1813 ## 2.1.0 - 2020-11-25 -* Add ability to set address for `TestServer`. [#1645] -* Upgrade `base64` to `0.13`. -* Upgrade `serde_urlencoded` to `0.7`. [#1773] +- Add ability to set address for `TestServer`. [#1645] +- Upgrade `base64` to `0.13`. +- Upgrade `serde_urlencoded` to `0.7`. [#1773] [#1773]: https://github.com/actix/actix-web/pull/1773 [#1645]: https://github.com/actix/actix-web/pull/1645 ## 2.0.0 - 2020-09-11 -* Update actix-codec and actix-utils dependencies. +- Update actix-codec and actix-utils dependencies. ## 2.0.0-alpha.1 - 2020-05-23 -* Update the `time` dependency to 0.2.7 -* Update `actix-connect` dependency to 2.0.0-alpha.2 -* Make `test_server` `async` fn. -* Bump minimum supported Rust version to 1.40 -* Replace deprecated `net2` crate with `socket2` -* Update `base64` dependency to 0.12 -* Update `env_logger` dependency to 0.7 +- Update the `time` dependency to 0.2.7 +- Update `actix-connect` dependency to 2.0.0-alpha.2 +- Make `test_server` `async` fn. +- Bump minimum supported Rust version to 1.40 +- Replace deprecated `net2` crate with `socket2` +- Update `base64` dependency to 0.12 +- Update `env_logger` dependency to 0.7 ## 1.0.0 - 2019-12-13 -* Replaced `TestServer::start()` with `test_server()` +- Replaced `TestServer::start()` with `test_server()` ## 1.0.0-alpha.3 - 2019-12-07 -* Migrate to `std::future` +- Migrate to `std::future` ## 0.2.5 - 2019-09-17 -* Update serde_urlencoded to "0.6.1" -* Increase TestServerRuntime timeouts from 500ms to 3000ms -* Do not override current `System` +- Update serde_urlencoded to "0.6.1" +- Increase TestServerRuntime timeouts from 500ms to 3000ms +- Do not override current `System` ## 0.2.4 - 2019-07-18 -* Update actix-server to 0.6 +- Update actix-server to 0.6 ## 0.2.3 - 2019-07-16 -* Add `delete`, `options`, `patch` methods to `TestServerRunner` +- Add `delete`, `options`, `patch` methods to `TestServerRunner` ## 0.2.2 - 2019-06-16 -* Add .put() and .sput() methods +- Add .put() and .sput() methods ## 0.2.1 - 2019-06-05 -* Add license files +- Add license files ## 0.2.0 - 2019-05-12 -* Update awc and actix-http deps +- Update awc and actix-http deps ## 0.1.1 - 2019-04-24 -* Always make new connection for http client +- Always make new connection for http client ## 0.1.0 - 2019-04-16 -* No changes +- No changes ## 0.1.0-alpha.3 - 2019-04-02 -* Request functions accept path #743 +- Request functions accept path #743 ## 0.1.0-alpha.2 - 2019-03-29 -* Added TestServerRuntime::load_body() method -* Update actix-http and awc libraries +- Added TestServerRuntime::load_body() method +- Update actix-http and awc libraries ## 0.1.0-alpha.1 - 2019-03-28 -* Initial impl +- Initial impl diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 7a22cbcc1..2883a8f7e 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http-test" -version = "3.0.0-beta.8" +version = "3.0.0-beta.10" authors = ["Nikolay Kim "] description = "Various helpers for Actix applications to use during testing" keywords = ["http", "web", "framework", "async", "futures"] @@ -31,11 +31,11 @@ openssl = ["tls-openssl", "awc/openssl"] [dependencies] actix-service = "2.0.0" actix-codec = "0.4.1" -actix-tls = "3.0.0-rc.1" +actix-tls = "3.0.0" actix-utils = "3.0.0" actix-rt = "2.2" -actix-server = "2.0.0-rc.1" -awc = { version = "3.0.0-beta.11", default-features = false } +actix-server = "2.0.0-rc.2" +awc = { version = "3.0.0-beta.16", default-features = false } base64 = "0.13" bytes = "1" @@ -48,8 +48,8 @@ serde_json = "1.0" slab = "0.4" serde_urlencoded = "0.7" tls-openssl = { version = "0.10.9", package = "openssl", optional = true } -tokio = { version = "1.2", features = ["sync"] } +tokio = { version = "1.8.4", features = ["sync"] } [dev-dependencies] -actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] } -actix-http = "3.0.0-beta.14" +actix-web = { version = "4.0.0-beta.17", default-features = false, features = ["cookies"] } +actix-http = "3.0.0-beta.17" diff --git a/actix-http-test/README.md b/actix-http-test/README.md index c3e99d259..589c54c23 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -3,11 +3,11 @@ > Various helpers for Actix applications to use during testing. [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) -[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.8)](https://docs.rs/actix-http-test/3.0.0-beta.8) +[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.10)](https://docs.rs/actix-http-test/3.0.0-beta.10) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
-[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.8/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.8) +[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.10) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index ff86e565a..8636ef9c4 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -1,6 +1,7 @@ //! Various helpers for Actix applications to use during testing. -#![deny(rust_2018_idioms)] +#![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")] @@ -11,7 +12,7 @@ use std::{net, thread, time::Duration}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_rt::{net::TcpStream, System}; -use actix_server::{Server, ServiceFactory}; +use actix_server::{Server, ServerServiceFactory}; use awc::{ error::PayloadError, http::header::HeaderMap, ws, Client, ClientRequest, ClientResponse, Connector, @@ -50,13 +51,13 @@ use tokio::sync::mpsc; /// assert!(response.status().is_success()); /// } /// ``` -pub async fn test_server>(factory: F) -> TestServer { +pub async fn test_server>(factory: F) -> TestServer { let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); test_server_with_addr(tcp, factory).await } /// Start [`test server`](test_server()) on an existing address binding. -pub async fn test_server_with_addr>( +pub async fn test_server_with_addr>( tcp: net::TcpListener, factory: F, ) -> TestServer { @@ -106,7 +107,7 @@ pub async fn test_server_with_addr>( Connector::new() .conn_lifetime(Duration::from_secs(0)) .timeout(Duration::from_millis(30000)) - .ssl(builder.build()) + .openssl(builder.build()) }; #[cfg(not(feature = "openssl"))] diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 1a59b233a..d74a754ac 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,52 +1,97 @@ # Changes ## Unreleased - 2021-xx-xx -### Added -* Add timeout for canceling HTTP/2 server side connection handshake. Default to 5 seconds. [#2483] -* HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483] -* `Response::map_into_boxed_body`. [#2468] -* `body::EitherBody` enum. [#2468] -* `body::None` struct. [#2468] -* Impl `MessageBody` for `bytestring::ByteString`. [#2468] -* `impl Clone for ws::HandshakeError`. [#2468] -* `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920] -* `impl Default ` for `ws::Codec`. [#1920] -* `header::QualityItem::{max, min}`. [#2486] -* `header::Quality::{MAX, MIN}`. [#2486] -* `impl Display` for `header::Quality`. [#2486] -### Changed -* Rename `body::BoxBody::{from_body => new}`. [#2468] -* Body type for `Responses` returned from `Response::{new, ok, etc...}` is now `BoxBody`. [#2468] -* The `Error` associated type on `MessageBody` type now requires `impl Error` (or similar). [#2468] -* Error types using in service builders now require `Into>`. [#2468] -* `From` implementations on error types now return a `Response`. [#2468] -* `ResponseBuilder::body(B)` now returns `Response>`. [#2468] -* `ResponseBuilder::finish()` now returns `Response>`. [#2468] + +## 3.0.0-beta.17 - 2021-12-27 +### Changes +- `HeaderMap::get_all` now returns a `std::slice::Iter`. [#2527] +- `Payload` inner fields are now named. [#2545] +- `impl Stream` for `Payload` no longer requires the `Stream` variant be `Unpin`. [#2545] +- `impl Future` for `h1::SendResponse` no longer requires the body type be `Unpin`. [#2545] +- `impl Stream` for `encoding::Decoder` no longer requires the stream type be `Unpin`. [#2545] +- Rename `PayloadStream` to `BoxedPayloadStream`. [#2545] ### Removed -* `ResponseBuilder::streaming`. [#2468] -* `impl Future` for `ResponseBuilder`. [#2468] -* Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468] -* Move `body::AnyBody` to `awc`. Replaced with `EitherBody` and `BoxBody`. [#2468] -* `impl Copy` for `ws::Codec`. [#1920] -* `header::qitem` helper. Replaced with `header::QualityItem::max`. [#2486] -* `impl TryFrom` for `header::Quality`. [#2486] -* `http` module. Most everything it contained is exported at the crate root. [#2488] +- `h1::Payload::readany`. [#2545] + +[#2527]: https://github.com/actix/actix-web/pull/2527 +[#2545]: https://github.com/actix/actix-web/pull/2545 + + +## 3.0.0-beta.16 - 2021-12-17 +### Added +- New method on `MessageBody` trait, `try_into_bytes`, with default implementation, for optimizations on body types that complete in exactly one poll. Replaces `is_complete_body` and `take_complete_body`. [#2522] + +### Changed +- Rename trait `IntoHeaderPair => TryIntoHeaderPair`. [#2510] +- Rename `TryIntoHeaderPair::{try_into_header_pair => try_into_pair}`. [#2510] +- Rename trait `IntoHeaderValue => TryIntoHeaderValue`. [#2510] + +### Removed +- `MessageBody::{is_complete_body,take_complete_body}`. [#2522] + +[#2510]: https://github.com/actix/actix-web/pull/2510 +[#2522]: https://github.com/actix/actix-web/pull/2522 + + +## 3.0.0-beta.15 - 2021-12-11 +### Added +- Add timeout for canceling HTTP/2 server side connection handshake. Default to 5 seconds. [#2483] +- HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483] +- `Response::map_into_boxed_body`. [#2468] +- `body::EitherBody` enum. [#2468] +- `body::None` struct. [#2468] +- Impl `MessageBody` for `bytestring::ByteString`. [#2468] +- `impl Clone for ws::HandshakeError`. [#2468] +- `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920] +- `impl Default ` for `ws::Codec`. [#1920] +- `header::QualityItem::{max, min}`. [#2486] +- `header::Quality::{MAX, MIN}`. [#2486] +- `impl Display` for `header::Quality`. [#2486] +- Connection data set through the `on_connect_ext` callbacks is now accessible only from the new `Request::conn_data()` method. [#2491] +- `Request::take_conn_data()`. [#2491] +- `Request::take_req_data()`. [#2487] +- `impl Clone` for `RequestHead`. [#2487] +- New methods on `MessageBody` trait, `is_complete_body` and `take_complete_body`, both with default implementations, for optimizations on body types that are done in exactly one poll/chunk. [#2497] +- New `boxed` method on `MessageBody` trait for wrapping body type. [#2520] + +### Changed +- Rename `body::BoxBody::{from_body => new}`. [#2468] +- Body type for `Responses` returned from `Response::{new, ok, etc...}` is now `BoxBody`. [#2468] +- The `Error` associated type on `MessageBody` type now requires `impl Error` (or similar). [#2468] +- Error types using in service builders now require `Into>`. [#2468] +- `From` implementations on error types now return a `Response`. [#2468] +- `ResponseBuilder::body(B)` now returns `Response>`. [#2468] +- `ResponseBuilder::finish()` now returns `Response>`. [#2468] + +### Removed +- `ResponseBuilder::streaming`. [#2468] +- `impl Future` for `ResponseBuilder`. [#2468] +- Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468] +- Move `body::AnyBody` to `awc`. Replaced with `EitherBody` and `BoxBody`. [#2468] +- `impl Copy` for `ws::Codec`. [#1920] +- `header::qitem` helper. Replaced with `header::QualityItem::max`. [#2486] +- `impl TryFrom` for `header::Quality`. [#2486] +- `http` module. Most everything it contained is exported at the crate root. [#2488] [#2483]: https://github.com/actix/actix-web/pull/2483 [#2468]: https://github.com/actix/actix-web/pull/2468 [#1920]: https://github.com/actix/actix-web/pull/1920 [#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 +[#2520]: https://github.com/actix/actix-web/pull/2520 ## 3.0.0-beta.14 - 2021-11-30 ### Changed -* Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467] -* Expose `header::map` module. [#2467] -* Implement `ExactSizeIterator` and `FusedIterator` for all `HeaderMap` iterators. [#2470] -* Update `actix-tls` to `3.0.0-rc.1`. [#2474] +- Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467] +- Expose `header::map` module. [#2467] +- Implement `ExactSizeIterator` and `FusedIterator` for all `HeaderMap` iterators. [#2470] +- Update `actix-tls` to `3.0.0-rc.1`. [#2474] [#2467]: https://github.com/actix/actix-web/pull/2467 [#2470]: https://github.com/actix/actix-web/pull/2470 @@ -55,24 +100,24 @@ ## 3.0.0-beta.13 - 2021-11-22 ### Added -* `body::AnyBody::empty` for quickly creating an empty body. [#2446] -* `body::AnyBody::none` for quickly creating a "none" body. [#2456] -* `impl Clone` for `body::AnyBody where S: Clone`. [#2448] -* `body::AnyBody::into_boxed` for quickly converting to a type-erased, boxed body type. [#2448] +- `body::AnyBody::empty` for quickly creating an empty body. [#2446] +- `body::AnyBody::none` for quickly creating a "none" body. [#2456] +- `impl Clone` for `body::AnyBody where S: Clone`. [#2448] +- `body::AnyBody::into_boxed` for quickly converting to a type-erased, boxed body type. [#2448] ### Changed -* Rename `body::AnyBody::{Message => Body}`. [#2446] -* Rename `body::AnyBody::{from_message => new_boxed}`. [#2448] -* Rename `body::AnyBody::{from_slice => copy_from_slice}`. [#2448] -* Rename `body::{BoxAnyBody => BoxBody}`. [#2448] -* Change representation of `AnyBody` to include a type parameter in `Body` variant. Defaults to `BoxBody`. [#2448] -* `Encoder::response` now returns `AnyBody>`. [#2448] +- Rename `body::AnyBody::{Message => Body}`. [#2446] +- Rename `body::AnyBody::{from_message => new_boxed}`. [#2448] +- Rename `body::AnyBody::{from_slice => copy_from_slice}`. [#2448] +- Rename `body::{BoxAnyBody => BoxBody}`. [#2448] +- Change representation of `AnyBody` to include a type parameter in `Body` variant. Defaults to `BoxBody`. [#2448] +- `Encoder::response` now returns `AnyBody>`. [#2448] ### Removed -* `body::AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446] -* `body::BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446] -* `EncoderError::Boxed`; it is no longer required. [#2446] -* `body::ResponseBody`; is function is replaced by the new `body::AnyBody` enum. [#2446] +- `body::AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446] +- `body::BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446] +- `EncoderError::Boxed`; it is no longer required. [#2446] +- `body::ResponseBody`; is function is replaced by the new `body::AnyBody` enum. [#2446] [#2446]: https://github.com/actix/actix-web/pull/2446 [#2448]: https://github.com/actix/actix-web/pull/2448 @@ -81,11 +126,11 @@ ## 3.0.0-beta.12 - 2021-11-15 ### Changed -* Update `actix-server` to `2.0.0-beta.9`. [#2442] +- Update `actix-server` to `2.0.0-beta.9`. [#2442] ### Removed -* `client` module. [#2425] -* `trust-dns` feature. [#2425] +- `client` module. [#2425] +- `trust-dns` feature. [#2425] [#2425]: https://github.com/actix/actix-web/pull/2425 [#2442]: https://github.com/actix/actix-web/pull/2442 @@ -93,21 +138,21 @@ ## 3.0.0-beta.11 - 2021-10-20 ### Changed -* Updated rustls to v0.20. [#2414] -* Minimum supported Rust version (MSRV) is now 1.52. +- Updated rustls to v0.20. [#2414] +- Minimum supported Rust version (MSRV) is now 1.52. [#2414]: https://github.com/actix/actix-web/pull/2414 ## 3.0.0-beta.10 - 2021-09-09 ### Changed -* `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377] -* Minimum supported Rust version (MSRV) is now 1.51. +- `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377] +- Minimum supported Rust version (MSRV) is now 1.51. ### Fixed -* Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364] -* Remove `Into` bound on `Encoder` body types. [#2375] -* Fix quality parse error in Accept-Encoding header. [#2344] +- Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364] +- Remove `Into` bound on `Encoder` body types. [#2375] +- Fix quality parse error in Accept-Encoding header. [#2344] [#2364]: https://github.com/actix/actix-web/pull/2364 [#2375]: https://github.com/actix/actix-web/pull/2375 @@ -117,15 +162,15 @@ ## 3.0.0-beta.9 - 2021-08-09 ### Fixed -* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) +- Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) ## 3.0.0-beta.8 - 2021-06-26 ### Changed -* Change compression algorithm features flags. [#2250] +- Change compression algorithm features flags. [#2250] ### Removed -* `downcast` and `downcast_get_type_id` macros. [#2291] +- `downcast` and `downcast_get_type_id` macros. [#2291] [#2291]: https://github.com/actix/actix-web/pull/2291 [#2250]: https://github.com/actix/actix-web/pull/2250 @@ -133,37 +178,37 @@ ## 3.0.0-beta.7 - 2021-06-17 ### Added -* Alias `body::Body` as `body::AnyBody`. [#2215] -* `BoxAnyBody`: a boxed message body with boxed errors. [#2183] -* Re-export `http` crate's `Error` type as `error::HttpError`. [#2171] -* Re-export `StatusCode`, `Method`, `Version` and `Uri` at the crate root. [#2171] -* Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171] -* `Response::into_body` that consumes response and returns body type. [#2201] -* `impl Default` for `Response`. [#2201] -* Add zstd support for `ContentEncoding`. [#2244] +- Alias `body::Body` as `body::AnyBody`. [#2215] +- `BoxAnyBody`: a boxed message body with boxed errors. [#2183] +- Re-export `http` crate's `Error` type as `error::HttpError`. [#2171] +- Re-export `StatusCode`, `Method`, `Version` and `Uri` at the crate root. [#2171] +- Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171] +- `Response::into_body` that consumes response and returns body type. [#2201] +- `impl Default` for `Response`. [#2201] +- Add zstd support for `ContentEncoding`. [#2244] ### Changed -* The `MessageBody` trait now has an associated `Error` type. [#2183] -* All error trait bounds in server service builders have changed from `Into` to `Into>`. [#2253] -* All error trait bounds in message body and stream impls changed from `Into` to `Into>`. [#2253] -* Places in `Response` where `ResponseBody` was received or returned now simply use `B`. [#2201] -* `header` mod is now public. [#2171] -* `uri` mod is now public. [#2171] -* Update `language-tags` to `0.3`. -* Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201] -* `ResponseBuilder::message_body` now returns a `Result`. [#2201] -* Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253] -* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226] +- The `MessageBody` trait now has an associated `Error` type. [#2183] +- All error trait bounds in server service builders have changed from `Into` to `Into>`. [#2253] +- All error trait bounds in message body and stream impls changed from `Into` to `Into>`. [#2253] +- Places in `Response` where `ResponseBody` was received or returned now simply use `B`. [#2201] +- `header` mod is now public. [#2171] +- `uri` mod is now public. [#2171] +- Update `language-tags` to `0.3`. +- Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201] +- `ResponseBuilder::message_body` now returns a `Result`. [#2201] +- Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253] +- `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226] ### Removed -* Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171] -* Down-casting for `MessageBody` types. [#2183] -* `error::Result` alias. [#2201] -* Error field from `Response` and `Response::error`. [#2205] -* `impl Future` for `Response`. [#2201] -* `Response::take_body` and old `Response::into_body` method that casted body type. [#2201] -* `InternalError` and all the error types it constructed. [#2215] -* Conversion (`impl Into`) of `Response` and `ResponseBuilder` to `Error`. [#2215] +- Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171] +- Down-casting for `MessageBody` types. [#2183] +- `error::Result` alias. [#2201] +- Error field from `Response` and `Response::error`. [#2205] +- `impl Future` for `Response`. [#2201] +- `Response::take_body` and old `Response::into_body` method that casted body type. [#2201] +- `InternalError` and all the error types it constructed. [#2215] +- Conversion (`impl Into`) of `Response` and `ResponseBuilder` to `Error`. [#2215] [#2171]: https://github.com/actix/actix-web/pull/2171 [#2183]: https://github.com/actix/actix-web/pull/2183 @@ -178,27 +223,27 @@ ## 3.0.0-beta.6 - 2021-04-17 ### Added -* `impl MessageBody for Pin>`. [#2152] -* `Response::{ok, bad_request, not_found, internal_server_error}`. [#2159] -* Helper `body::to_bytes` for async collecting message body into Bytes. [#2158] +- `impl MessageBody for Pin>`. [#2152] +- `Response::{ok, bad_request, not_found, internal_server_error}`. [#2159] +- Helper `body::to_bytes` for async collecting message body into Bytes. [#2158] ### Changes -* 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] -* Error enum types are marked `#[non_exhaustive]`. [#2161] +- 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] +- Error enum types are marked `#[non_exhaustive]`. [#2161] ### Removed -* `cookies` feature flag. [#2065] -* Top-level `cookies` mod (re-export). [#2065] -* `HttpMessage` trait loses the `cookies` and `cookie` methods. [#2065] -* `impl ResponseError for CookieParseError`. [#2065] -* Deprecated methods on `ResponseBuilder`: `if_true`, `if_some`. [#2148] -* `ResponseBuilder::json`. [#2148] -* `ResponseBuilder::{set_header, header}`. [#2148] -* `impl From for Body`. [#2148] -* `Response::build_from`. [#2159] -* Most of the status code builders on `Response`. [#2159] +- `cookies` feature flag. [#2065] +- Top-level `cookies` mod (re-export). [#2065] +- `HttpMessage` trait loses the `cookies` and `cookie` methods. [#2065] +- `impl ResponseError for CookieParseError`. [#2065] +- Deprecated methods on `ResponseBuilder`: `if_true`, `if_some`. [#2148] +- `ResponseBuilder::json`. [#2148] +- `ResponseBuilder::{set_header, header}`. [#2148] +- `impl From for Body`. [#2148] +- `Response::build_from`. [#2159] +- Most of the status code builders on `Response`. [#2159] [#2065]: https://github.com/actix/actix-web/pull/2065 [#2148]: https://github.com/actix/actix-web/pull/2148 @@ -210,16 +255,16 @@ ## 3.0.0-beta.5 - 2021-04-02 ### Added -* `client::Connector::handshake_timeout` method for customizing TLS connection handshake timeout. [#2081] -* `client::ConnectorService` as `client::Connector::finish` method's return type [#2081] -* `client::ConnectionIo` trait alias [#2081] +- `client::Connector::handshake_timeout` method for customizing TLS connection handshake timeout. [#2081] +- `client::ConnectorService` as `client::Connector::finish` method's return type [#2081] +- `client::ConnectionIo` trait alias [#2081] ### Changed -* `client::Connector` type now only have one generic type for `actix_service::Service`. [#2063] +- `client::Connector` type now only have one generic type for `actix_service::Service`. [#2063] ### Removed -* Common typed HTTP headers were moved to actix-web. [2094] -* `ResponseError` impl for `actix_utils::timeout::TimeoutError`. [#2127] +- Common typed HTTP headers were moved to actix-web. [2094] +- `ResponseError` impl for `actix_utils::timeout::TimeoutError`. [#2127] [#2063]: https://github.com/actix/actix-web/pull/2063 [#2081]: https://github.com/actix/actix-web/pull/2081 @@ -229,13 +274,13 @@ ## 3.0.0-beta.4 - 2021-03-08 ### Changed -* Feature `cookies` is now optional and disabled by default. [#1981] -* `ws::hash_key` now returns array. [#2035] -* `ResponseBuilder::json` now takes `impl Serialize`. [#2052] +- Feature `cookies` is now optional and disabled by default. [#1981] +- `ws::hash_key` now returns array. [#2035] +- `ResponseBuilder::json` now takes `impl Serialize`. [#2052] ### Removed -* Re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994] -* `ResponseError` impl for `futures_channel::oneshot::Canceled` is removed. [#1994] +- Re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994] +- `ResponseError` impl for `futures_channel::oneshot::Canceled` is removed. [#1994] [#1981]: https://github.com/actix/actix-web/pull/1981 [#1994]: https://github.com/actix/actix-web/pull/1994 @@ -244,48 +289,48 @@ ## 3.0.0-beta.3 - 2021-02-10 -* No notable changes. +- No notable changes. ## 3.0.0-beta.2 - 2021-02-10 ### Added -* `IntoHeaderPair` trait that allows using typed and untyped headers in the same methods. [#1869] -* `ResponseBuilder::insert_header` method which allows using typed headers. [#1869] -* `ResponseBuilder::append_header` method which allows using typed headers. [#1869] -* `TestRequest::insert_header` method which allows using typed headers. [#1869] -* `ContentEncoding` implements all necessary header traits. [#1912] -* `HeaderMap::len_keys` has the behavior of the old `len` method. [#1964] -* `HeaderMap::drain` as an efficient draining iterator. [#1964] -* Implement `IntoIterator` for owned `HeaderMap`. [#1964] -* `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969] +- `TryIntoHeaderPair` trait that allows using typed and untyped headers in the same methods. [#1869] +- `ResponseBuilder::insert_header` method which allows using typed headers. [#1869] +- `ResponseBuilder::append_header` method which allows using typed headers. [#1869] +- `TestRequest::insert_header` method which allows using typed headers. [#1869] +- `ContentEncoding` implements all necessary header traits. [#1912] +- `HeaderMap::len_keys` has the behavior of the old `len` method. [#1964] +- `HeaderMap::drain` as an efficient draining iterator. [#1964] +- Implement `IntoIterator` for owned `HeaderMap`. [#1964] +- `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969] ### Changed -* `ResponseBuilder::content_type` now takes an `impl IntoHeaderValue` to support using typed +- `ResponseBuilder::content_type` now takes an `impl TryIntoHeaderValue` to support using typed `mime` types. [#1894] -* Renamed `IntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std +- Renamed `TryIntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std `TryInto` trait. [#1894] -* `Extensions::insert` returns Option of replaced item. [#1904] -* Remove `HttpResponseBuilder::json2()`. [#1903] -* Enable `HttpResponseBuilder::json()` to receive data by value and reference. [#1903] -* `client::error::ConnectError` Resolver variant contains `Box` type. [#1905] -* `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905] -* Simplify `BlockingError` type to a unit struct. It's now only triggered when blocking thread pool +- `Extensions::insert` returns Option of replaced item. [#1904] +- Remove `HttpResponseBuilder::json2()`. [#1903] +- Enable `HttpResponseBuilder::json()` to receive data by value and reference. [#1903] +- `client::error::ConnectError` Resolver variant contains `Box` type. [#1905] +- `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905] +- Simplify `BlockingError` type to a unit struct. It's now only triggered when blocking thread pool is dead. [#1957] -* `HeaderMap::len` now returns number of values instead of number of keys. [#1964] -* `HeaderMap::insert` now returns iterator of removed values. [#1964] -* `HeaderMap::remove` now returns iterator of removed values. [#1964] +- `HeaderMap::len` now returns number of values instead of number of keys. [#1964] +- `HeaderMap::insert` now returns iterator of removed values. [#1964] +- `HeaderMap::remove` now returns iterator of removed values. [#1964] ### Removed -* `ResponseBuilder::set`; use `ResponseBuilder::insert_header`. [#1869] -* `ResponseBuilder::set_header`; use `ResponseBuilder::insert_header`. [#1869] -* `ResponseBuilder::header`; use `ResponseBuilder::append_header`. [#1869] -* `TestRequest::with_hdr`; use `TestRequest::default().insert_header()`. [#1869] -* `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869] -* `actors` optional feature. [#1969] -* `ResponseError` impl for `actix::MailboxError`. [#1969] +- `ResponseBuilder::set`; use `ResponseBuilder::insert_header`. [#1869] +- `ResponseBuilder::set_header`; use `ResponseBuilder::insert_header`. [#1869] +- `ResponseBuilder::header`; use `ResponseBuilder::append_header`. [#1869] +- `TestRequest::with_hdr`; use `TestRequest::default().insert_header()`. [#1869] +- `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869] +- `actors` optional feature. [#1969] +- `ResponseError` impl for `actix::MailboxError`. [#1969] ### Documentation -* Vastly improve docs and add examples for `HeaderMap`. [#1964] +- Vastly improve docs and add examples for `HeaderMap`. [#1964] [#1869]: https://github.com/actix/actix-web/pull/1869 [#1894]: https://github.com/actix/actix-web/pull/1894 @@ -300,24 +345,24 @@ ## 3.0.0-beta.1 - 2021-01-07 ### Added -* Add `Http3` to `Protocol` enum for future compatibility and also mark `#[non_exhaustive]`. +- Add `Http3` to `Protocol` enum for future compatibility and also mark `#[non_exhaustive]`. ### Changed -* Update `actix-*` dependencies to tokio `1.0` based versions. [#1813] -* Bumped `rand` to `0.8`. -* Update `bytes` to `1.0`. [#1813] -* Update `h2` to `0.3`. [#1813] -* The `ws::Message::Text` enum variant now contains a `bytestring::ByteString`. [#1864] +- Update `actix-*` dependencies to tokio `1.0` based versions. [#1813] +- Bumped `rand` to `0.8`. +- Update `bytes` to `1.0`. [#1813] +- Update `h2` to `0.3`. [#1813] +- The `ws::Message::Text` enum variant now contains a `bytestring::ByteString`. [#1864] ### Removed -* Deprecated `on_connect` methods have been removed. Prefer the new +- Deprecated `on_connect` methods have been removed. Prefer the new `on_connect_ext` technique. [#1857] -* Remove `ResponseError` impl for `actix::actors::resolver::ResolverError` +- Remove `ResponseError` impl for `actix::actors::resolver::ResolverError` due to deprecate of resolver actor. [#1813] -* Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`. +- Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`. 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`. +- Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`. Due to this change `actix_threadpool::BlockingError` type is moved into `actix_http::error` module. [#1878] @@ -329,20 +374,20 @@ ## 2.2.1 - 2021-08-09 ### Fixed -* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) +- Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) ## 2.2.0 - 2020-11-25 ### Added -* HttpResponse builders for 1xx status codes. [#1768] -* `Accept::mime_precedence` and `Accept::mime_preference`. [#1793] -* `TryFrom` and `TryFrom` for `http::header::Quality`. [#1797] +- HttpResponse builders for 1xx status codes. [#1768] +- `Accept::mime_precedence` and `Accept::mime_preference`. [#1793] +- `TryFrom` and `TryFrom` for `http::header::Quality`. [#1797] ### Fixed -* Started dropping `transfer-encoding: chunked` and `Content-Length` for 1XX and 204 responses. [#1767] +- Started dropping `transfer-encoding: chunked` and `Content-Length` for 1XX and 204 responses. [#1767] ### Changed -* Upgrade `serde_urlencoded` to `0.7`. [#1773] +- Upgrade `serde_urlencoded` to `0.7`. [#1773] [#1773]: https://github.com/actix/actix-web/pull/1773 [#1767]: https://github.com/actix/actix-web/pull/1767 @@ -353,12 +398,12 @@ ## 2.1.0 - 2020-10-30 ### Added -* Added more flexible `on_connect_ext` methods for on-connect handling. [#1754] +- Added more flexible `on_connect_ext` methods for on-connect handling. [#1754] ### Changed -* Upgrade `base64` to `0.13`. [#1744] -* Upgrade `pin-project` to `1.0`. [#1733] -* Deprecate `ResponseBuilder::{if_some, if_true}`. [#1760] +- Upgrade `base64` to `0.13`. [#1744] +- Upgrade `pin-project` to `1.0`. [#1733] +- Deprecate `ResponseBuilder::{if_some, if_true}`. [#1760] [#1760]: https://github.com/actix/actix-web/pull/1760 [#1754]: https://github.com/actix/actix-web/pull/1754 @@ -367,28 +412,28 @@ ## 2.0.0 - 2020-09-11 -* No significant changes from `2.0.0-beta.4`. +- No significant changes from `2.0.0-beta.4`. ## 2.0.0-beta.4 - 2020-09-09 ### Changed -* Update actix-codec and actix-utils dependencies. -* Update actix-connect and actix-tls dependencies. +- Update actix-codec and actix-utils dependencies. +- Update actix-connect and actix-tls dependencies. ## 2.0.0-beta.3 - 2020-08-14 ### Fixed -* Memory leak of `client::pool::ConnectorPoolSupport`. [#1626] +- Memory leak of `client::pool::ConnectorPoolSupport`. [#1626] [#1626]: https://github.com/actix/actix-web/pull/1626 ## 2.0.0-beta.2 - 2020-07-21 ### Fixed -* Potential UB in h1 decoder using uninitialized memory. [#1614] +- Potential UB in h1 decoder using uninitialized memory. [#1614] ### Changed -* Fix illegal chunked encoding. [#1615] +- Fix illegal chunked encoding. [#1615] [#1614]: https://github.com/actix/actix-web/pull/1614 [#1615]: https://github.com/actix/actix-web/pull/1615 @@ -396,10 +441,10 @@ ## 2.0.0-beta.1 - 2020-07-11 ### Changed -* Migrate cookie handling to `cookie` crate. [#1558] -* Update `sha-1` to 0.9. [#1586] -* Fix leak in client pool. [#1580] -* MSRV is now 1.41.1. +- Migrate cookie handling to `cookie` crate. [#1558] +- Update `sha-1` to 0.9. [#1586] +- Fix leak in client pool. [#1580] +- MSRV is now 1.41.1. [#1558]: https://github.com/actix/actix-web/pull/1558 [#1586]: https://github.com/actix/actix-web/pull/1586 @@ -408,15 +453,15 @@ ## 2.0.0-alpha.4 - 2020-05-21 ### Changed -* Bump minimum supported Rust version to 1.40 -* content_length function is removed, and you can set Content-Length by calling +- Bump minimum supported Rust version to 1.40 +- content_length function is removed, and you can set Content-Length by calling no_chunking function [#1439] -* `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a +- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a `u64` instead of a `usize`. -* Update `base64` dependency to 0.12 +- Update `base64` dependency to 0.12 ### Fixed -* Support parsing of `SameSite=None` [#1503] +- Support parsing of `SameSite=None` [#1503] [#1439]: https://github.com/actix/actix-web/pull/1439 [#1503]: https://github.com/actix/actix-web/pull/1503 @@ -424,13 +469,13 @@ ## 2.0.0-alpha.3 - 2020-05-08 ### Fixed -* Correct spelling of ConnectError::Unresolved [#1487] -* Fix a mistake in the encoding of websocket continuation messages wherein +- Correct spelling of ConnectError::Unresolved [#1487] +- Fix a mistake in the encoding of websocket continuation messages wherein Item::FirstText and Item::FirstBinary are each encoded as the other. ### Changed -* Implement `std::error::Error` for our custom errors [#1422] -* Remove `failure` support for `ResponseError` since that crate +- Implement `std::error::Error` for our custom errors [#1422] +- Remove `failure` support for `ResponseError` since that crate will be deprecated in the near future. [#1422]: https://github.com/actix/actix-web/pull/1422 @@ -439,12 +484,12 @@ ## 2.0.0-alpha.2 - 2020-03-07 ### Changed -* Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395] -* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB +- Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395] +- Change default initial window size and connection window size for HTTP2 to 2MB and 1MB respectively to improve download speed for awc when downloading large objects. [#1394] -* client::Connector accepts initial_window_size and initial_connection_window_size +- client::Connector accepts initial_window_size and initial_connection_window_size HTTP2 configuration. [#1394] -* client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394] +- client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394] [#1394]: https://github.com/actix/actix-web/pull/1394 [#1395]: https://github.com/actix/actix-web/pull/1395 @@ -452,61 +497,61 @@ ## 2.0.0-alpha.1 - 2020-02-27 ### Changed -* Update the `time` dependency to 0.2.7. -* Moved actors messages support from actix crate, enabled with feature `actors`. -* Breaking change: trait MessageBody requires Unpin and accepting `Pin<&mut Self>` instead of +- Update the `time` dependency to 0.2.7. +- Moved actors messages support from actix crate, enabled with feature `actors`. +- Breaking change: trait MessageBody requires Unpin and accepting `Pin<&mut Self>` instead of `&mut self` in the poll_next(). -* MessageBody is not implemented for &'static [u8] anymore. +- MessageBody is not implemented for &'static [u8] anymore. ### Fixed -* Allow `SameSite=None` cookies to be sent in a response. +- Allow `SameSite=None` cookies to be sent in a response. ## 1.0.1 - 2019-12-20 ### Fixed -* Poll upgrade service's readiness from HTTP service handlers -* Replace brotli with brotli2 #1224 +- Poll upgrade service's readiness from HTTP service handlers +- Replace brotli with brotli2 #1224 ## 1.0.0 - 2019-12-13 ### Added -* Add websockets continuation frame support +- Add websockets continuation frame support ### Changed -* Replace `flate2-xxx` features with `compress` +- Replace `flate2-xxx` features with `compress` ## 1.0.0-alpha.5 - 2019-12-09 ### Fixed -* Check `Upgrade` service readiness before calling it -* Fix buffer remaining capacity calculation +- Check `Upgrade` service readiness before calling it +- Fix buffer remaining capacity calculation ### Changed -* Websockets: Ping and Pong should have binary data #1049 +- Websockets: Ping and Pong should have binary data #1049 ## 1.0.0-alpha.4 - 2019-12-08 ### Added -* Add impl ResponseBuilder for Error +- Add impl ResponseBuilder for Error ### Changed -* Use rust based brotli compression library +- Use rust based brotli compression library ## 1.0.0-alpha.3 - 2019-12-07 ### Changed -* Migrate to tokio 0.2 -* Migrate to `std::future` +- Migrate to tokio 0.2 +- Migrate to `std::future` ## 0.2.11 - 2019-11-06 ### Added -* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body() -* Add an additional `filename*` param in the `Content-Disposition` header of +- Add support for serde_json::Value to be passed as argument to ResponseBuilder.body() +- Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151) -* Allow to use `std::convert::Infallible` as `actix_http::error::Error` +- Allow to use `std::convert::Infallible` as `actix_http::error::Error` ### Fixed -* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain; +- To be compatible with non-English error responses, `ResponseError` rendered with `text/plain; charset=utf-8` header [#1118] [#1878]: https://github.com/actix/actix-web/pull/1878 @@ -514,169 +559,169 @@ ## 0.2.10 - 2019-09-11 ### Added -* Add support for sending HTTP requests with `Rc` in addition to sending HTTP requests +- Add support for sending HTTP requests with `Rc` in addition to sending HTTP requests with `RequestHead` ### Fixed -* h2 will use error response #1080 -* on_connect result isn't added to request extensions for http2 requests #1009 +- h2 will use error response #1080 +- on_connect result isn't added to request extensions for http2 requests #1009 ## 0.2.9 - 2019-08-13 ### Changed -* Dropped the `byteorder`-dependency in favor of `stdlib`-implementation -* Update percent-encoding to 2.1 -* Update serde_urlencoded to 0.6.1 +- Dropped the `byteorder`-dependency in favor of `stdlib`-implementation +- Update percent-encoding to 2.1 +- Update serde_urlencoded to 0.6.1 ### Fixed -* Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031) +- Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031) ## 0.2.8 - 2019-08-01 ### Added -* Add `rustls` support -* Add `Clone` impl for `HeaderMap` +- Add `rustls` support +- Add `Clone` impl for `HeaderMap` ### Fixed -* awc client panic #1016 -* Invalid response with compression middleware enabled, but compression-related features +- awc client panic #1016 +- Invalid response with compression middleware enabled, but compression-related features disabled #997 ## 0.2.7 - 2019-07-18 ### Added -* Add support for downcasting response errors #986 +- Add support for downcasting response errors #986 ## 0.2.6 - 2019-07-17 ### Changed -* Replace `ClonableService` with local copy -* Upgrade `rand` dependency version to 0.7 +- Replace `ClonableService` with local copy +- Upgrade `rand` dependency version to 0.7 ## 0.2.5 - 2019-06-28 ### Added -* Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946 +- Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946 ### Changed -* Use `encoding_rs` crate instead of unmaintained `encoding` crate -* Add `Copy` and `Clone` impls for `ws::Codec` +- Use `encoding_rs` crate instead of unmaintained `encoding` crate +- Add `Copy` and `Clone` impls for `ws::Codec` ## 0.2.4 - 2019-06-16 ### Fixed -* Do not compress NoContent (204) responses #918 +- Do not compress NoContent (204) responses #918 ## 0.2.3 - 2019-06-02 ### Added -* Debug impl for ResponseBuilder -* From SizedStream and BodyStream for Body +- Debug impl for ResponseBuilder +- From SizedStream and BodyStream for Body ### Changed -* SizedStream uses u64 +- SizedStream uses u64 ## 0.2.2 - 2019-05-29 ### Fixed -* Parse incoming stream before closing stream on disconnect #868 +- Parse incoming stream before closing stream on disconnect #868 ## 0.2.1 - 2019-05-25 ### Fixed -* Handle socket read disconnect +- Handle socket read disconnect ## 0.2.0 - 2019-05-12 ### Changed -* Update actix-service to 0.4 -* Expect and upgrade services accept `ServerConfig` config. +- Update actix-service to 0.4 +- Expect and upgrade services accept `ServerConfig` config. ### Deleted -* `OneRequest` service +- `OneRequest` service ## 0.1.5 - 2019-05-04 ### Fixed -* Clean up response extensions in response pool #817 +- Clean up response extensions in response pool #817 ## 0.1.4 - 2019-04-24 ### Added -* Allow to render h1 request headers in `Camel-Case` +- Allow to render h1 request headers in `Camel-Case` ### Fixed -* Read until eof for http/1.0 responses #771 +- Read until eof for http/1.0 responses #771 ## 0.1.3 - 2019-04-23 ### Fixed -* Fix http client pool management -* Fix http client wait queue management #794 +- Fix http client pool management +- Fix http client wait queue management #794 ## 0.1.2 - 2019-04-23 ### Fixed -* Fix BorrowMutError panic in client connector #793 +- Fix BorrowMutError panic in client connector #793 ## 0.1.1 - 2019-04-19 ### Changed -* Cookie::max_age() accepts value in seconds -* Cookie::max_age_time() accepts value in time::Duration -* Allow to specify server address for client connector +- Cookie::max_age() accepts value in seconds +- Cookie::max_age_time() accepts value in time::Duration +- Allow to specify server address for client connector ## 0.1.0 - 2019-04-16 ### Added -* Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr` +- Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr` ### Changed -* `actix_http::encoding` always available -* use trust-dns-resolver 0.11.0 +- `actix_http::encoding` always available +- use trust-dns-resolver 0.11.0 ## 0.1.0-alpha.5 - 2019-04-12 ### Added -* Allow to use custom service for upgrade requests -* Added `h1::SendResponse` future. +- Allow to use custom service for upgrade requests +- Added `h1::SendResponse` future. ### Changed -* MessageBody::length() renamed to MessageBody::size() for consistency -* ws handshake verification functions take RequestHead instead of Request +- MessageBody::length() renamed to MessageBody::size() for consistency +- ws handshake verification functions take RequestHead instead of Request ## 0.1.0-alpha.4 - 2019-04-08 ### Added -* Allow to use custom `Expect` handler -* Add minimal `std::error::Error` impl for `Error` +- Allow to use custom `Expect` handler +- Add minimal `std::error::Error` impl for `Error` ### Changed -* Export IntoHeaderValue -* Render error and return as response body -* Use thread pool for response body compression +- Export IntoHeaderValue +- Render error and return as response body +- Use thread pool for response body compression ### Deleted -* Removed PayloadBuffer +- Removed PayloadBuffer ## 0.1.0-alpha.3 - 2019-04-02 ### Added -* Warn when an unsealed private cookie isn't valid UTF-8 +- Warn when an unsealed private cookie isn't valid UTF-8 ### Fixed -* Rust 1.31.0 compatibility -* Preallocate read buffer for h1 codec -* Detect socket disconnection during protocol selection +- Rust 1.31.0 compatibility +- Preallocate read buffer for h1 codec +- Detect socket disconnection during protocol selection ## 0.1.0-alpha.2 - 2019-03-29 ### Added -* Added ws::Message::Nop, no-op websockets message +- Added ws::Message::Nop, no-op websockets message ### Changed -* Do not use thread pool for decompression if chunk size is smaller than 2048. +- Do not use thread pool for decompression if chunk size is smaller than 2048. ## 0.1.0-alpha.1 - 2019-03-28 -* Initial impl +- Initial impl diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 87669aeb1..9575f55e7 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.0.0-beta.14" +version = "3.0.0-beta.17" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" keywords = ["actix", "http", "framework", "async", "futures"] @@ -45,7 +45,7 @@ __compress = [] actix-service = "2.0.0" actix-codec = "0.4.1" actix-utils = "3.0.0" -actix-rt = "2.2" +actix-rt = { version = "2.2", default-features = false } ahash = "0.7" base64 = "0.13" @@ -55,25 +55,23 @@ bytestring = "1" derive_more = "0.99.5" encoding_rs = "0.8" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } -futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] } -h2 = "0.3.1" +h2 = "0.3.9" http = "0.2.5" httparse = "1.5.1" httpdate = "1.0.1" -itoa = "0.4" +itoa = "1" language-tags = "0.3" local-channel = "0.1" log = "0.4" mime = "0.3" percent-encoding = "2.1" -pin-project = "1.0.0" pin-project-lite = "0.2" rand = "0.8" -sha-1 = "0.9" +sha-1 = "0.10" smallvec = "1.6.1" # tls -actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true } +actix-tls = { version = "3.0.0", default-features = false, optional = true } # compression brotli2 = { version="0.3.2", optional = true } @@ -81,12 +79,15 @@ flate2 = { version = "1.0.13", optional = true } zstd = { version = "0.9", optional = true } [dev-dependencies] -actix-server = "2.0.0-rc.1" -actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] } -actix-tls = { version = "3.0.0-rc.1", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] } +actix-server = "2.0.0-rc.2" +actix-tls = { version = "3.0.0", features = ["openssl"] } +actix-web = "4.0.0-beta.17" + 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"] } rcgen = "0.8" regex = "1.3" rustls-pemfile = "0.2" @@ -95,7 +96,7 @@ serde_json = "1.0" static_assertions = "1" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } -tokio = { version = "1.2", features = ["net", "rt"] } +tokio = { version = "1.8.4", features = ["net", "rt", "macros"] } [[example]] name = "ws" diff --git a/actix-http/README.md b/actix-http/README.md index 92b86d2a3..223e18ceb 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -3,11 +3,11 @@ > HTTP primitives for the Actix ecosystem. [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) -[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.14)](https://docs.rs/actix-http/3.0.0-beta.14) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.17)](https://docs.rs/actix-http/3.0.0-beta.17) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
-[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.14/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.14) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.17/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.17) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) @@ -54,8 +54,8 @@ async fn main() -> io::Result<()> { 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](http://www.apache.org/licenses/LICENSE-2.0)) -* MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) +- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) at your option. diff --git a/actix-http/benches/status-line.rs b/actix-http/benches/status-line.rs index f62d18ed8..9fe099478 100644 --- a/actix-http/benches/status-line.rs +++ b/actix-http/benches/status-line.rs @@ -189,11 +189,7 @@ mod _original { n /= 100; curr -= 2; unsafe { - ptr::copy_nonoverlapping( - lut_ptr.offset(d1 as isize), - buf_ptr.offset(curr), - 2, - ); + ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2); } // decode last 1 or 2 chars @@ -206,11 +202,7 @@ mod _original { let d1 = n << 1; curr -= 2; unsafe { - ptr::copy_nonoverlapping( - lut_ptr.offset(d1 as isize), - buf_ptr.offset(curr), - 2, - ); + ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2); } } diff --git a/actix-http/benches/uninit-headers.rs b/actix-http/benches/uninit-headers.rs index 53a2528ab..5dfd3bc11 100644 --- a/actix-http/benches/uninit-headers.rs +++ b/actix-http/benches/uninit-headers.rs @@ -54,15 +54,10 @@ const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex { value: (0, 0), }; -const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] = - [EMPTY_HEADER_INDEX; MAX_HEADERS]; +const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] = [EMPTY_HEADER_INDEX; MAX_HEADERS]; impl HeaderIndex { - fn record( - bytes: &[u8], - headers: &[httparse::Header<'_>], - indices: &mut [HeaderIndex], - ) { + fn record(bytes: &[u8], headers: &[httparse::Header<'_>], indices: &mut [HeaderIndex]) { let bytes_ptr = bytes.as_ptr() as usize; for (header, indices) in headers.iter().zip(indices.iter_mut()) { let name_start = header.name.as_ptr() as usize - bytes_ptr; diff --git a/actix-http/examples/actix-web.rs b/actix-http/examples/actix-web.rs new file mode 100644 index 000000000..f8226507f --- /dev/null +++ b/actix-http/examples/actix-web.rs @@ -0,0 +1,26 @@ +use actix_http::HttpService; +use actix_server::Server; +use actix_service::map_config; +use actix_web::{dev::AppConfig, get, App}; + +#[get("/")] +async fn index() -> &'static str { + "Hello, world. From Actix Web!" +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> std::io::Result<()> { + Server::build() + .bind("hello-world", "127.0.0.1:8080", || { + // construct actix-web app + let app = App::new().service(index); + + 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())) + .tcp() + })? + .run() + .await +} diff --git a/actix-http/examples/echo.rs b/actix-http/examples/echo.rs index 5ff2bcc89..22f553f38 100644 --- a/actix-http/examples/echo.rs +++ b/actix-http/examples/echo.rs @@ -25,10 +25,7 @@ async fn main() -> io::Result<()> { Ok::<_, Error>( Response::build(StatusCode::OK) - .insert_header(( - "x-head", - HeaderValue::from_static("dummy value!"), - )) + .insert_header(("x-head", HeaderValue::from_static("dummy value!"))) .body(body), ) }) diff --git a/actix-http/examples/echo2.rs b/actix-http/examples/echo2.rs index 487b8d8d1..e3b915e05 100644 --- a/actix-http/examples/echo2.rs +++ b/actix-http/examples/echo2.rs @@ -1,8 +1,7 @@ use std::io; use actix_http::{ - body::MessageBody, header::HeaderValue, Error, HttpService, Request, Response, - StatusCode, + body::MessageBody, header::HeaderValue, Error, HttpService, Request, Response, StatusCode, }; use actix_server::Server; use bytes::BytesMut; diff --git a/actix-http/examples/hello-world.rs b/actix-http/examples/hello-world.rs index 3678774b8..a29903cc4 100644 --- a/actix-http/examples/hello-world.rs +++ b/actix-http/examples/hello-world.rs @@ -1,8 +1,9 @@ use std::{convert::Infallible, io}; -use actix_http::{HttpService, Response, StatusCode}; +use actix_http::{ + header::HeaderValue, HttpMessage, HttpService, Request, Response, StatusCode, +}; use actix_server::Server; -use http::header::HeaderValue; #[actix_rt::main] async fn main() -> io::Result<()> { @@ -13,13 +14,19 @@ async fn main() -> io::Result<()> { HttpService::build() .client_timeout(1000) .client_disconnect(1000) - .finish(|req| async move { + .on_connect_ext(|_, ext| { + ext.insert(42u32); + }) + .finish(|req: Request| async move { log::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(); res.insert_header(( - "x-head", - HeaderValue::from_static("dummy value!"), + "x-forty-two", + HeaderValue::from_str(&forty_two).unwrap(), )); Ok::<_, Infallible>(res.body("Hello world!")) diff --git a/actix-http/examples/ws.rs b/actix-http/examples/ws.rs index b6be4d2f1..d70e43314 100644 --- a/actix-http/examples/ws.rs +++ b/actix-http/examples/ws.rs @@ -60,10 +60,7 @@ impl Heartbeat { impl Stream for Heartbeat { type Item = Result; - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { log::trace!("poll"); ready!(self.as_mut().interval.poll_tick(cx)); diff --git a/actix-http/rustfmt.toml b/actix-http/rustfmt.toml deleted file mode 100644 index 5fcaaca0f..000000000 --- a/actix-http/rustfmt.toml +++ /dev/null @@ -1,5 +0,0 @@ -max_width = 89 -reorder_imports = true -#wrap_comments = true -#fn_args_density = "Compressed" -#use_small_heuristics = false diff --git a/actix-http/src/body/body_stream.rs b/actix-http/src/body/body_stream.rs index 1da7a848a..cf4f488b2 100644 --- a/actix-http/src/body/body_stream.rs +++ b/actix-http/src/body/body_stream.rs @@ -27,6 +27,7 @@ where S: Stream>, E: Into> + 'static, { + #[inline] pub fn new(stream: S) -> Self { BodyStream { stream } } @@ -39,6 +40,7 @@ where { type Error = E; + #[inline] fn size(&self) -> BodySize { BodySize::Stream } @@ -165,8 +167,7 @@ mod tests { #[actix_rt::test] async fn stream_delayed_error() { - let body = - BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)])); + let body = BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)])); assert!(matches!(to_bytes(body).await, Err(StreamErr))); pin_project! { diff --git a/actix-http/src/body/boxed.rs b/actix-http/src/body/boxed.rs index 9442bd1df..d109a6a74 100644 --- a/actix-http/src/body/boxed.rs +++ b/actix-http/src/body/boxed.rs @@ -8,50 +8,97 @@ use std::{ use bytes::Bytes; use super::{BodySize, MessageBody, MessageBodyMapErr}; -use crate::Error; +use crate::body; /// A boxed message body with boxed errors. -pub struct BoxBody(Pin>>>); +#[derive(Debug)] +pub struct BoxBody(BoxBodyInner); + +enum BoxBodyInner { + None(body::None), + Bytes(Bytes), + Stream(Pin>>>), +} + +impl fmt::Debug for BoxBodyInner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None(arg0) => f.debug_tuple("None").field(arg0).finish(), + Self::Bytes(arg0) => f.debug_tuple("Bytes").field(arg0).finish(), + Self::Stream(_) => f.debug_tuple("Stream").field(&"dyn MessageBody").finish(), + } + } +} impl BoxBody { - /// Boxes a `MessageBody` and any errors it generates. + /// Same as `MessageBody::boxed`. + /// + /// If the body type to wrap is unknown or generic it is better to use [`MessageBody::boxed`] to + /// avoid double boxing. + #[inline] pub fn new(body: B) -> Self where B: MessageBody + 'static, { - let body = MessageBodyMapErr::new(body, Into::into); - Self(Box::pin(body)) + match body.size() { + BodySize::None => Self(BoxBodyInner::None(body::None)), + _ => match body.try_into_bytes() { + Ok(bytes) => Self(BoxBodyInner::Bytes(bytes)), + Err(body) => { + let body = MessageBodyMapErr::new(body, Into::into); + Self(BoxBodyInner::Stream(Box::pin(body))) + } + }, + } } /// Returns a mutable pinned reference to the inner message body type. - pub fn as_pin_mut( - &mut self, - ) -> Pin<&mut (dyn MessageBody>)> { - self.0.as_mut() - } -} - -impl fmt::Debug for BoxBody { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("BoxBody(dyn MessageBody)") + #[inline] + pub fn as_pin_mut(&mut self) -> Pin<&mut Self> { + Pin::new(self) } } impl MessageBody for BoxBody { - type Error = Error; + type Error = Box; + #[inline] fn size(&self) -> BodySize { - self.0.size() + match &self.0 { + BoxBodyInner::None(none) => none.size(), + BoxBodyInner::Bytes(bytes) => bytes.size(), + BoxBodyInner::Stream(stream) => stream.size(), + } } + #[inline] fn poll_next( mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll>> { - self.0 - .as_mut() - .poll_next(cx) - .map_err(|err| Error::new_body().with_cause(err)) + match &mut self.0 { + BoxBodyInner::None(body) => { + Pin::new(body).poll_next(cx).map_err(|err| match err {}) + } + BoxBodyInner::Bytes(body) => { + Pin::new(body).poll_next(cx).map_err(|err| match err {}) + } + BoxBodyInner::Stream(body) => Pin::new(body).poll_next(cx), + } + } + + #[inline] + fn try_into_bytes(self) -> Result { + match self.0 { + BoxBodyInner::None(body) => Ok(body.try_into_bytes().unwrap()), + BoxBodyInner::Bytes(body) => Ok(body.try_into_bytes().unwrap()), + _ => Err(self), + } + } + + #[inline] + fn boxed(self) -> BoxBody { + self } } diff --git a/actix-http/src/body/either.rs b/actix-http/src/body/either.rs index 6169ee627..add1eab7c 100644 --- a/actix-http/src/body/either.rs +++ b/actix-http/src/body/either.rs @@ -23,6 +23,7 @@ pin_project! { impl EitherBody { /// Creates new `EitherBody` using left variant and boxed right variant. + #[inline] pub fn new(body: L) -> Self { Self::Left { body } } @@ -30,11 +31,13 @@ impl EitherBody { impl EitherBody { /// Creates new `EitherBody` using left variant. + #[inline] pub fn left(body: L) -> Self { Self::Left { body } } /// Creates new `EitherBody` using right variant. + #[inline] pub fn right(body: R) -> Self { Self::Right { body } } @@ -47,6 +50,7 @@ where { type Error = Error; + #[inline] fn size(&self) -> BodySize { match self { EitherBody::Left { body } => body.size(), @@ -54,6 +58,7 @@ where } } + #[inline] fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_>, @@ -67,6 +72,26 @@ where .map_err(|err| Error::new_body().with_cause(err)), } } + + #[inline] + fn try_into_bytes(self) -> Result { + match self { + EitherBody::Left { body } => body + .try_into_bytes() + .map_err(|body| EitherBody::Left { body }), + EitherBody::Right { body } => body + .try_into_bytes() + .map_err(|body| EitherBody::Right { body }), + } + } + + #[inline] + fn boxed(self) -> BoxBody { + match self { + EitherBody::Left { body } => body.boxed(), + EitherBody::Right { body } => body.boxed(), + } + } } #[cfg(test)] diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs index 053b6f286..0a605a69a 100644 --- a/actix-http/src/body/message_body.rs +++ b/actix-http/src/body/message_body.rs @@ -12,23 +12,56 @@ use bytes::{Bytes, BytesMut}; use futures_core::ready; use pin_project_lite::pin_project; -use super::BodySize; +use super::{BodySize, BoxBody}; /// An interface types that can converted to bytes and used as response bodies. // TODO: examples pub trait MessageBody { - // TODO: consider this bound to only fmt::Display since the error type is not really used - // and there is an impl for Into> on String + /// The type of error that will be returned if streaming body fails. + /// + /// Since it is not appropriate to generate a response mid-stream, it only requires `Error` for + /// internal use and logging. type Error: Into>; /// Body size hint. + /// + /// If [`BodySize::None`] is returned, optimizations that skip reading the body are allowed. fn size(&self) -> BodySize; /// Attempt to pull out the next chunk of body bytes. + // TODO: expand documentation fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll>>; + + /// Try to convert into the complete chunk of body bytes. + /// + /// Implement this method if the entire body can be trivially extracted. This is useful for + /// optimizations where `poll_next` calls can be avoided. + /// + /// Body types with [`BodySize::None`] are allowed to return empty `Bytes`. Although, if calling + /// this method, it is recommended to check `size` first and return early. + /// + /// # Errors + /// The default implementation will error and return the original type back to the caller for + /// further use. + #[inline] + fn try_into_bytes(self) -> Result + where + Self: Sized, + { + Err(self) + } + + /// Converts this body into `BoxBody`. + #[inline] + fn boxed(self) -> BoxBody + where + Self: Sized + 'static, + { + BoxBody::new(self) + } } mod foreign_impls { @@ -37,12 +70,10 @@ mod foreign_impls { impl MessageBody for Infallible { type Error = Infallible; - #[inline] fn size(&self) -> BodySize { match *self {} } - #[inline] fn poll_next( self: Pin<&mut Self>, _cx: &mut Context<'_>, @@ -66,11 +97,16 @@ mod foreign_impls { ) -> Poll>> { Poll::Ready(None) } + + #[inline] + fn try_into_bytes(self) -> Result { + Ok(Bytes::new()) + } } impl MessageBody for Box where - B: MessageBody + Unpin, + B: MessageBody + Unpin + ?Sized, { type Error = B::Error; @@ -90,7 +126,7 @@ mod foreign_impls { impl MessageBody for Pin> where - B: MessageBody, + B: MessageBody + ?Sized, { type Error = B::Error; @@ -101,20 +137,22 @@ mod foreign_impls { #[inline] fn poll_next( - mut self: Pin<&mut Self>, + self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll>> { - self.as_mut().poll_next(cx) + self.get_mut().as_mut().poll_next(cx) } } impl MessageBody for &'static [u8] { type Error = Infallible; + #[inline] fn size(&self) -> BodySize { BodySize::Sized(self.len() as u64) } + #[inline] fn poll_next( self: Pin<&mut Self>, _cx: &mut Context<'_>, @@ -122,20 +160,25 @@ mod foreign_impls { if self.is_empty() { Poll::Ready(None) } else { - let bytes = mem::take(self.get_mut()); - let bytes = Bytes::from_static(bytes); - Poll::Ready(Some(Ok(bytes))) + Poll::Ready(Some(Ok(Bytes::from_static(mem::take(self.get_mut()))))) } } + + #[inline] + fn try_into_bytes(self) -> Result { + Ok(Bytes::from_static(self)) + } } impl MessageBody for Bytes { type Error = Infallible; + #[inline] fn size(&self) -> BodySize { BodySize::Sized(self.len() as u64) } + #[inline] fn poll_next( self: Pin<&mut Self>, _cx: &mut Context<'_>, @@ -143,19 +186,25 @@ mod foreign_impls { if self.is_empty() { Poll::Ready(None) } else { - let bytes = mem::take(self.get_mut()); - Poll::Ready(Some(Ok(bytes))) + Poll::Ready(Some(Ok(mem::take(self.get_mut())))) } } + + #[inline] + fn try_into_bytes(self) -> Result { + Ok(self) + } } impl MessageBody for BytesMut { type Error = Infallible; + #[inline] fn size(&self) -> BodySize { BodySize::Sized(self.len() as u64) } + #[inline] fn poll_next( self: Pin<&mut Self>, _cx: &mut Context<'_>, @@ -163,19 +212,25 @@ mod foreign_impls { if self.is_empty() { Poll::Ready(None) } else { - let bytes = mem::take(self.get_mut()).freeze(); - Poll::Ready(Some(Ok(bytes))) + Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze()))) } } + + #[inline] + fn try_into_bytes(self) -> Result { + Ok(self.freeze()) + } } impl MessageBody for Vec { type Error = Infallible; + #[inline] fn size(&self) -> BodySize { BodySize::Sized(self.len() as u64) } + #[inline] fn poll_next( self: Pin<&mut Self>, _cx: &mut Context<'_>, @@ -183,19 +238,25 @@ mod foreign_impls { if self.is_empty() { Poll::Ready(None) } else { - let bytes = mem::take(self.get_mut()); - Poll::Ready(Some(Ok(Bytes::from(bytes)))) + Poll::Ready(Some(Ok(mem::take(self.get_mut()).into()))) } } + + #[inline] + fn try_into_bytes(self) -> Result { + Ok(Bytes::from(self)) + } } impl MessageBody for &'static str { type Error = Infallible; + #[inline] fn size(&self) -> BodySize { BodySize::Sized(self.len() as u64) } + #[inline] fn poll_next( self: Pin<&mut Self>, _cx: &mut Context<'_>, @@ -208,15 +269,22 @@ mod foreign_impls { Poll::Ready(Some(Ok(bytes))) } } + + #[inline] + fn try_into_bytes(self) -> Result { + Ok(Bytes::from_static(self.as_bytes())) + } } impl MessageBody for String { type Error = Infallible; + #[inline] fn size(&self) -> BodySize { BodySize::Sized(self.len() as u64) } + #[inline] fn poll_next( self: Pin<&mut Self>, _cx: &mut Context<'_>, @@ -228,15 +296,22 @@ mod foreign_impls { Poll::Ready(Some(Ok(Bytes::from(string)))) } } + + #[inline] + fn try_into_bytes(self) -> Result { + Ok(Bytes::from(self)) + } } impl MessageBody for bytestring::ByteString { type Error = Infallible; + #[inline] fn size(&self) -> BodySize { BodySize::Sized(self.len() as u64) } + #[inline] fn poll_next( self: Pin<&mut Self>, _cx: &mut Context<'_>, @@ -244,6 +319,11 @@ mod foreign_impls { let string = mem::take(self.get_mut()); Poll::Ready(Some(Ok(string.into_bytes()))) } + + #[inline] + fn try_into_bytes(self) -> Result { + Ok(self.into_bytes()) + } } } @@ -276,6 +356,7 @@ where { type Error = E; + #[inline] fn size(&self) -> BodySize { self.body.size() } @@ -296,6 +377,12 @@ where None => Poll::Ready(None), } } + + #[inline] + fn try_into_bytes(self) -> Result { + let Self { body, mapper } = self; + body.try_into_bytes().map_err(|body| Self { body, mapper }) + } } #[cfg(test)] @@ -305,6 +392,7 @@ mod tests { use bytes::{Bytes, BytesMut}; use super::*; + use crate::body::{self, EitherBody}; macro_rules! assert_poll_next { ($pin:expr, $exp:expr) => { @@ -406,6 +494,47 @@ mod tests { assert_poll_next!(pl, Bytes::from("test")); } + #[actix_rt::test] + async fn complete_body_combinators() { + let body = Bytes::from_static(b"test"); + let body = BoxBody::new(body); + let body = EitherBody::<_, ()>::left(body); + let body = EitherBody::<(), _>::right(body); + // Do not support try_into_bytes: + // let body = Box::new(body); + // let body = Box::pin(body); + + assert_eq!(body.try_into_bytes().unwrap(), Bytes::from("test")); + } + + #[actix_rt::test] + async fn complete_body_combinators_poll() { + let body = Bytes::from_static(b"test"); + let body = BoxBody::new(body); + let body = EitherBody::<_, ()>::left(body); + let body = EitherBody::<(), _>::right(body); + let mut body = body; + + assert_eq!(body.size(), BodySize::Sized(4)); + assert_poll_next!(Pin::new(&mut body), Bytes::from("test")); + assert_poll_next_none!(Pin::new(&mut body)); + } + + #[actix_rt::test] + async fn none_body_combinators() { + fn none_body() -> BoxBody { + let body = body::None; + let body = BoxBody::new(body); + let body = EitherBody::<_, ()>::left(body); + let body = EitherBody::<(), _>::right(body); + body.boxed() + } + + assert_eq!(none_body().size(), BodySize::None); + assert_eq!(none_body().try_into_bytes().unwrap(), Bytes::new()); + assert_poll_next_none!(Pin::new(&mut none_body())); + } + // down-casting used to be done with a method on MessageBody trait // test is kept to demonstrate equivalence of Any trait #[actix_rt::test] diff --git a/actix-http/src/body/none.rs b/actix-http/src/body/none.rs index 0fc7c8c9f..0e7bbe5a9 100644 --- a/actix-http/src/body/none.rs +++ b/actix-http/src/body/none.rs @@ -40,4 +40,9 @@ impl MessageBody for None { ) -> Poll>> { Poll::Ready(Option::None) } + + #[inline] + fn try_into_bytes(self) -> Result { + Ok(Bytes::new()) + } } diff --git a/actix-http/src/body/size.rs b/actix-http/src/body/size.rs index d64af9d44..ec7873ca5 100644 --- a/actix-http/src/body/size.rs +++ b/actix-http/src/body/size.rs @@ -1,9 +1,11 @@ /// Body size hint. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BodySize { - /// Absence of body can be assumed from method or status code. + /// Implicitly empty body. /// - /// Will skip writing Content-Length header. + /// Will omit the Content-Length header. Used for responses to certain methods (e.g., `HEAD`) or + /// with particular status codes (e.g., 204 No Content). Consumers that read this as a body size + /// hint are allowed to make optimizations that skip reading or writing the payload. None, /// Known size body. @@ -18,6 +20,9 @@ pub enum BodySize { } impl BodySize { + /// Equivalent to `BodySize::Sized(0)`; + pub const ZERO: Self = Self::Sized(0); + /// Returns true if size hint indicates omitted or empty body. /// /// Streams will return false because it cannot be known without reading the stream. diff --git a/actix-http/src/body/sized_stream.rs b/actix-http/src/body/sized_stream.rs index c8606897d..9c1727246 100644 --- a/actix-http/src/body/sized_stream.rs +++ b/actix-http/src/body/sized_stream.rs @@ -27,6 +27,7 @@ where S: Stream>, E: Into> + 'static, { + #[inline] pub fn new(size: u64, stream: S) -> Self { SizedStream { size, stream } } @@ -41,6 +42,7 @@ where { type Error = E; + #[inline] fn size(&self) -> BodySize { BodySize::Sized(self.size as u64) } diff --git a/actix-http/src/body/utils.rs b/actix-http/src/body/utils.rs index a421ffd76..194af47f8 100644 --- a/actix-http/src/body/utils.rs +++ b/actix-http/src/body/utils.rs @@ -68,9 +68,8 @@ mod test { let bytes = to_bytes(body).await.unwrap(); assert_eq!(bytes, b"123"[..]); - let stream = - stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")]) - .map(Ok::<_, Error>); + let stream = stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")]) + .map(Ok::<_, Error>); let body = BodyStream::new(stream); let bytes = to_bytes(body).await.unwrap(); assert_eq!(bytes, b"123abc"[..]); diff --git a/actix-http/src/builder.rs b/actix-http/src/builder.rs index ca821f1d9..408ee7924 100644 --- a/actix-http/src/builder.rs +++ b/actix-http/src/builder.rs @@ -36,6 +36,7 @@ where >::Future: 'static, { /// Create instance of `ServiceConfigBuilder` + #[allow(clippy::new_without_default)] pub fn new() -> Self { HttpServiceBuilder { keep_alive: KeepAlive::Timeout(5), @@ -214,8 +215,7 @@ where self.local_addr, ); - H2Service::with_config(cfg, service.into_factory()) - .on_connect_ext(self.on_connect_ext) + H2Service::with_config(cfg, service.into_factory()).on_connect_ext(self.on_connect_ext) } /// Finish service configuration and create `HttpService` instance. diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index afe4c6e13..0f519637a 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -28,11 +28,14 @@ use crate::{ const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049; -pub struct Decoder { - decoder: Option, - stream: S, - eof: bool, - fut: Option, ContentDecoder), io::Error>>>, +pin_project_lite::pin_project! { + pub struct Decoder { + decoder: Option, + #[pin] + stream: S, + eof: bool, + fut: Option, ContentDecoder), io::Error>>>, + } } impl Decoder @@ -44,17 +47,17 @@ where pub fn new(stream: S, encoding: ContentEncoding) -> Decoder { let decoder = match encoding { #[cfg(feature = "compress-brotli")] - ContentEncoding::Br => Some(ContentDecoder::Br(Box::new( - BrotliDecoder::new(Writer::new()), - ))), + ContentEncoding::Br => Some(ContentDecoder::Br(Box::new(BrotliDecoder::new( + Writer::new(), + )))), #[cfg(feature = "compress-gzip")] ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( ZlibDecoder::new(Writer::new()), ))), #[cfg(feature = "compress-gzip")] - ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new( - GzDecoder::new(Writer::new()), - ))), + 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( @@ -89,45 +92,44 @@ where impl Stream for Decoder where - S: Stream> + Unpin, + S: Stream>, { type Item = Result; - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.project(); + loop { - if let Some(ref mut fut) = self.fut { + if let Some(ref mut fut) = this.fut { let (chunk, decoder) = ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??; - self.decoder = Some(decoder); - self.fut.take(); + *this.decoder = Some(decoder); + this.fut.take(); if let Some(chunk) = chunk { return Poll::Ready(Some(Ok(chunk))); } } - if self.eof { + if *this.eof { return Poll::Ready(None); } - match ready!(Pin::new(&mut self.stream).poll_next(cx)) { + match ready!(this.stream.as_mut().poll_next(cx)) { Some(Err(err)) => return Poll::Ready(Some(Err(err))), Some(Ok(chunk)) => { - if let Some(mut decoder) = self.decoder.take() { + if let Some(mut decoder) = this.decoder.take() { if chunk.len() < MAX_CHUNK_SIZE_DECODE_IN_PLACE { let chunk = decoder.feed_data(chunk)?; - self.decoder = Some(decoder); + *this.decoder = Some(decoder); if let Some(chunk) = chunk { return Poll::Ready(Some(Ok(chunk))); } } else { - self.fut = Some(spawn_blocking(move || { + *this.fut = Some(spawn_blocking(move || { let chunk = decoder.feed_data(chunk)?; Ok((chunk, decoder)) })); @@ -140,9 +142,9 @@ where } None => { - self.eof = true; + *this.eof = true; - return if let Some(mut decoder) = self.decoder.take() { + return if let Some(mut decoder) = this.decoder.take() { match decoder.feed_eof() { Ok(Some(res)) => Poll::Ready(Some(Ok(res))), Ok(None) => Poll::Ready(None), diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 350e7f062..b565bb2b5 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -25,7 +25,7 @@ use zstd::stream::write::Encoder as ZstdEncoder; use super::Writer; use crate::{ - body::{BodySize, MessageBody}, + body::{self, BodySize, MessageBody}, error::BlockingError, header::{self, ContentEncoding, HeaderValue, CONTENT_ENCODING}, ResponseHead, StatusCode, @@ -46,43 +46,39 @@ pin_project! { impl Encoder { fn none() -> Self { Encoder { - body: EncoderBody::None, + body: EncoderBody::None { + body: body::None::new(), + }, encoder: None, fut: None, eof: true, } } - pub fn response( - encoding: ContentEncoding, - head: &mut ResponseHead, - body: B, - ) -> Self { + pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self { let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING) || head.status == StatusCode::SWITCHING_PROTOCOLS || head.status == StatusCode::NO_CONTENT || encoding == ContentEncoding::Identity || encoding == ContentEncoding::Auto); - match body.size() { - // no need to compress an empty body - BodySize::None => return Self::none(), - - // we cannot assume that Sized is not a stream - BodySize::Sized(_) | BodySize::Stream => {} + // no need to compress an empty body + if matches!(body.size(), BodySize::None) { + return Self::none(); } - // TODO potentially some optimisation for single-chunk responses here by trying to read the - // payload eagerly, stopping after 2 polls if the first is a chunk and the second is None + let body = match body.try_into_bytes() { + Ok(body) => EncoderBody::Full { body }, + Err(body) => EncoderBody::Stream { body }, + }; if can_encode { // Modify response body only if encoder is set if let Some(enc) = ContentEncoder::encoder(encoding) { update_head(encoding, head); - head.no_chunking(false); return Encoder { - body: EncoderBody::Stream { body }, + body, encoder: Some(enc), fut: None, eof: false, @@ -91,7 +87,7 @@ impl Encoder { } Encoder { - body: EncoderBody::Stream { body }, + body, encoder: None, fut: None, eof: false, @@ -102,7 +98,8 @@ impl Encoder { pin_project! { #[project = EncoderBodyProj] enum EncoderBody { - None, + None { body: body::None }, + Full { body: Bytes }, Stream { #[pin] body: B }, } } @@ -113,9 +110,11 @@ where { type Error = EncoderError; + #[inline] fn size(&self) -> BodySize { match self { - EncoderBody::None => BodySize::None, + EncoderBody::None { body } => body.size(), + EncoderBody::Full { body } => body.size(), EncoderBody::Stream { body } => body.size(), } } @@ -125,13 +124,29 @@ where cx: &mut Context<'_>, ) -> Poll>> { match self.project() { - EncoderBodyProj::None => Poll::Ready(None), - + EncoderBodyProj::None { body } => { + Pin::new(body).poll_next(cx).map_err(|err| match err {}) + } + EncoderBodyProj::Full { body } => { + Pin::new(body).poll_next(cx).map_err(|err| match err {}) + } EncoderBodyProj::Stream { body } => body .poll_next(cx) .map_err(|err| EncoderError::Body(err.into())), } } + + #[inline] + fn try_into_bytes(self) -> Result + where + Self: Sized, + { + match self { + EncoderBody::None { body } => Ok(body.try_into_bytes().unwrap()), + EncoderBody::Full { body } => Ok(body.try_into_bytes().unwrap()), + _ => Err(self), + } + } } impl MessageBody for Encoder @@ -140,11 +155,12 @@ where { type Error = EncoderError; + #[inline] fn size(&self) -> BodySize { - if self.encoder.is_none() { - self.body.size() - } else { + if self.encoder.is_some() { BodySize::Stream + } else { + self.body.size() } } @@ -215,6 +231,24 @@ where } } } + + #[inline] + fn try_into_bytes(mut self) -> Result + where + Self: Sized, + { + if self.encoder.is_some() { + Err(self) + } else { + match self.body.try_into_bytes() { + Ok(body) => Ok(body), + Err(body) => { + self.body = body; + Err(self) + } + } + } + } } fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { @@ -222,6 +256,8 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { header::CONTENT_ENCODING, HeaderValue::from_static(encoding.as_str()), ); + + head.no_chunking(false); } enum ContentEncoder { diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 231e90e57..3d2a918f4 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -332,31 +332,28 @@ impl From for Error { } /// A set of errors that can occur during dispatching HTTP requests. -#[derive(Debug, Display, Error, From)] -#[non_exhaustive] +#[derive(Debug, Display, From)] pub enum DispatchError { - /// Service error - // FIXME: display and error type + /// Service error. #[display(fmt = "Service Error")] - Service(#[error(not(source))] Response), + Service(Response), - /// Body error - // FIXME: display and error type - #[display(fmt = "Body Error")] - Body(#[error(not(source))] Box), + /// Body streaming error. + #[display(fmt = "Body error: {}", _0)] + Body(Box), - /// Upgrade service error + /// Upgrade service error. Upgrade, /// An `io::Error` that occurred while trying to read or write to a network stream. #[display(fmt = "IO error: {}", _0)] Io(io::Error), - /// Http request parse error. - #[display(fmt = "Parse error: {}", _0)] + /// Request parse error. + #[display(fmt = "Request parse error: {}", _0)] Parse(ParseError), - /// Http/2 error + /// HTTP/2 error. #[display(fmt = "{}", _0)] H2(h2::Error), @@ -368,21 +365,23 @@ pub enum DispatchError { #[display(fmt = "Connection shutdown timeout")] DisconnectTimeout, - /// Payload is not consumed - #[display(fmt = "Task is completed but request's payload is not consumed")] - PayloadIsNotConsumed, - - /// Malformed request - #[display(fmt = "Malformed request")] - MalformedRequest, - - /// Internal error + /// Internal error. #[display(fmt = "Internal error")] InternalError, +} - /// Unknown error - #[display(fmt = "Unknown error")] - Unknown, +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), + DispatchError::H2(err) => Some(err), + _ => None, + } + } } /// A set of error that can occur during parsing content type. @@ -457,8 +456,7 @@ mod tests { #[test] fn test_payload_error() { - let err: PayloadError = - io::Error::new(io::ErrorKind::Other, "ParseError").into(); + let err: PayloadError = io::Error::new(io::ErrorKind::Other, "ParseError").into(); assert!(err.to_string().contains("ParseError")); let err = PayloadError::Incomplete(None); diff --git a/actix-http/src/extensions.rs b/actix-http/src/extensions.rs index 5fdcefd6d..60b769d13 100644 --- a/actix-http/src/extensions.rs +++ b/actix-http/src/extensions.rs @@ -1,6 +1,6 @@ use std::{ any::{Any, TypeId}, - fmt, mem, + fmt, }; use ahash::AHashMap; @@ -10,8 +10,7 @@ use ahash::AHashMap; /// All entries into this map must be owned types (or static references). #[derive(Default)] pub struct Extensions { - /// Use FxHasher with a std HashMap with for faster - /// lookups on the small `TypeId` (u64 equivalent) keys. + /// Use AHasher with a std HashMap with for faster lookups on the small `TypeId` keys. map: AHashMap>, } @@ -20,7 +19,7 @@ impl Extensions { #[inline] pub fn new() -> Extensions { Extensions { - map: AHashMap::default(), + map: AHashMap::new(), } } @@ -123,11 +122,6 @@ impl Extensions { pub fn extend(&mut self, other: Extensions) { self.map.extend(other.map); } - - /// Sets (or overrides) items from `other` into this map. - pub(crate) fn drain_from(&mut self, other: &mut Self) { - self.map.extend(mem::take(&mut other.map)); - } } impl fmt::Debug for Extensions { @@ -179,6 +173,8 @@ mod tests { #[test] fn test_integers() { + static A: u32 = 8; + let mut map = Extensions::new(); map.insert::(8); @@ -191,6 +187,7 @@ mod tests { map.insert::(32); map.insert::(64); map.insert::(128); + map.insert::<&'static u32>(&A); assert!(map.get::().is_some()); assert!(map.get::().is_some()); assert!(map.get::().is_some()); @@ -201,6 +198,7 @@ mod tests { assert!(map.get::().is_some()); assert!(map.get::().is_some()); assert!(map.get::().is_some()); + assert!(map.get::<&'static u32>().is_some()); } #[test] @@ -279,27 +277,4 @@ mod tests { assert_eq!(extensions.get(), Some(&20u8)); assert_eq!(extensions.get_mut(), Some(&mut 20u8)); } - - #[test] - fn test_drain_from() { - let mut ext = Extensions::new(); - ext.insert(2isize); - - let mut more_ext = Extensions::new(); - - more_ext.insert(5isize); - more_ext.insert(5usize); - - assert_eq!(ext.get::(), Some(&2isize)); - assert_eq!(ext.get::(), None); - assert_eq!(more_ext.get::(), Some(&5isize)); - assert_eq!(more_ext.get::(), Some(&5usize)); - - ext.drain_from(&mut more_ext); - - assert_eq!(ext.get::(), Some(&5isize)); - assert_eq!(ext.get::(), Some(&5usize)); - assert_eq!(more_ext.get::(), None); - assert_eq!(more_ext.get::(), None); - } } diff --git a/actix-http/src/h1/chunked.rs b/actix-http/src/h1/chunked.rs index e5b734fff..7d0532fcd 100644 --- a/actix-http/src/h1/chunked.rs +++ b/actix-http/src/h1/chunked.rs @@ -50,10 +50,7 @@ impl ChunkedState { } } - fn read_size( - rdr: &mut BytesMut, - size: &mut u64, - ) -> Poll> { + fn read_size(rdr: &mut BytesMut, size: &mut u64) -> Poll> { let radix = 16; let rem = match byte!(rdr) { @@ -111,10 +108,7 @@ impl ChunkedState { _ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions } } - fn read_size_lf( - rdr: &mut BytesMut, - size: u64, - ) -> Poll> { + fn read_size_lf(rdr: &mut BytesMut, size: u64) -> Poll> { match byte!(rdr) { b'\n' if size > 0 => Poll::Ready(Ok(ChunkedState::Body)), b'\n' if size == 0 => Poll::Ready(Ok(ChunkedState::EndCr)), diff --git a/actix-http/src/h1/client.rs b/actix-http/src/h1/client.rs index bec167971..9bd896ae0 100644 --- a/actix-http/src/h1/client.rs +++ b/actix-http/src/h1/client.rs @@ -5,13 +5,15 @@ use bitflags::bitflags; use bytes::{Bytes, BytesMut}; use http::{Method, Version}; -use super::decoder::{PayloadDecoder, PayloadItem, PayloadType}; -use super::{decoder, encoder, reserve_readbuf}; -use super::{Message, MessageType}; -use crate::body::BodySize; -use crate::config::ServiceConfig; -use crate::error::{ParseError, PayloadError}; -use crate::message::{ConnectionType, RequestHeadType, ResponseHead}; +use super::{ + decoder::{self, PayloadDecoder, PayloadItem, PayloadType}, + encoder, reserve_readbuf, Message, MessageType, +}; +use crate::{ + body::BodySize, + error::{ParseError, PayloadError}, + ConnectionType, RequestHeadType, ResponseHead, ServiceConfig, +}; bitflags! { struct Flags: u8 { diff --git a/actix-http/src/h1/codec.rs b/actix-http/src/h1/codec.rs index 29f6f4170..9a8907579 100644 --- a/actix-http/src/h1/codec.rs +++ b/actix-http/src/h1/codec.rs @@ -5,15 +5,13 @@ use bitflags::bitflags; use bytes::BytesMut; use http::{Method, Version}; -use super::decoder::{PayloadDecoder, PayloadItem, PayloadType}; -use super::{decoder, encoder}; -use super::{Message, MessageType}; -use crate::body::BodySize; -use crate::config::ServiceConfig; -use crate::error::ParseError; -use crate::message::ConnectionType; -use crate::request::Request; -use crate::response::Response; +use super::{ + decoder::{self, PayloadDecoder, PayloadItem, PayloadType}, + encoder, Message, MessageType, +}; +use crate::{ + body::BodySize, error::ParseError, ConnectionType, Request, Response, ServiceConfig, +}; bitflags! { struct Flags: u8 { @@ -199,7 +197,7 @@ mod tests { use http::Method; use super::*; - use crate::HttpMessage; + use crate::HttpMessage as _; #[actix_rt::test] async fn test_http_request_chunked_payload_and_next_message() { diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index a4db19669..3d3a3ceac 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -2,17 +2,14 @@ use std::{convert::TryFrom, io, marker::PhantomData, mem::MaybeUninit, task::Pol use actix_codec::Decoder; use bytes::{Bytes, BytesMut}; -use http::header::{HeaderName, HeaderValue}; -use http::{header, Method, StatusCode, Uri, Version}; +use http::{ + header::{self, HeaderName, HeaderValue}, + Method, StatusCode, Uri, Version, +}; use log::{debug, error, trace}; use super::chunked::ChunkedState; -use crate::{ - error::ParseError, - header::HeaderMap, - message::{ConnectionType, ResponseHead}, - request::Request, -}; +use crate::{error::ParseError, header::HeaderMap, ConnectionType, Request, ResponseHead}; pub(crate) const MAX_BUFFER_SIZE: usize = 131_072; const MAX_HEADERS: usize = 96; @@ -50,7 +47,7 @@ pub(crate) enum PayloadLength { } pub(crate) trait MessageType: Sized { - fn set_connection_type(&mut self, ctype: Option); + fn set_connection_type(&mut self, conn_type: Option); fn set_expect(&mut self); @@ -74,8 +71,7 @@ pub(crate) trait MessageType: Sized { let headers = self.headers_mut(); for idx in raw_headers.iter() { - let name = - HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]).unwrap(); + let name = HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]).unwrap(); // SAFETY: httparse already checks header value is only visible ASCII bytes // from_maybe_shared_unchecked contains debug assertions so they are omitted here @@ -194,8 +190,8 @@ pub(crate) trait MessageType: Sized { } impl MessageType for Request { - fn set_connection_type(&mut self, ctype: Option) { - if let Some(ctype) = ctype { + fn set_connection_type(&mut self, conn_type: Option) { + if let Some(ctype) = conn_type { self.head_mut().set_connection_type(ctype); } } @@ -279,8 +275,8 @@ impl MessageType for Request { } impl MessageType for ResponseHead { - fn set_connection_type(&mut self, ctype: Option) { - if let Some(ctype) = ctype { + fn set_connection_type(&mut self, conn_type: Option) { + if let Some(ctype) = conn_type { ResponseHead::set_connection_type(self, ctype); } } @@ -605,8 +601,7 @@ mod tests { #[test] fn test_parse_body() { - let mut buf = - BytesMut::from("GET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); + let mut buf = BytesMut::from("GET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); let mut reader = MessageDecoder::::default(); let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); @@ -622,8 +617,7 @@ mod tests { #[test] fn test_parse_body_crlf() { - let mut buf = - BytesMut::from("\r\nGET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); + let mut buf = BytesMut::from("\r\nGET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); let mut reader = MessageDecoder::::default(); let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 3c36e7367..13055f08a 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -15,18 +15,19 @@ use bitflags::bitflags; use bytes::{Buf, BytesMut}; use futures_core::ready; use log::{error, trace}; -use pin_project::pin_project; +use pin_project_lite::pin_project; use crate::{ body::{BodySize, BoxBody, MessageBody}, config::ServiceConfig, error::{DispatchError, ParseError, PayloadError}, service::HttpFlow, - OnConnectData, Request, Response, StatusCode, + Error, Extensions, OnConnectData, Request, Response, StatusCode, }; use super::{ codec::Codec, + decoder::MAX_BUFFER_SIZE, payload::{Payload, PayloadSender, PayloadStatus}, Message, MessageType, }; @@ -45,79 +46,111 @@ bitflags! { } } -#[pin_project] -/// Dispatcher for HTTP/1.1 protocol -pub struct Dispatcher -where - S: Service, - S::Error: Into>, +// there's 2 versions of Dispatcher state because of: +// https://github.com/taiki-e/pin-project-lite/issues/3 +// +// tl;dr: pin-project-lite doesn't play well with other attribute macros - B: MessageBody, +#[cfg(not(test))] +pin_project! { + /// Dispatcher for HTTP/1.1 protocol + pub struct Dispatcher + where + S: Service, + S::Error: Into>, - X: Service, - X::Error: Into>, + B: MessageBody, - U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display, -{ - #[pin] - inner: DispatcherState, + X: Service, + X::Error: Into>, - #[cfg(test)] - poll_count: u64, + U: Service<(Request, Framed), Response = ()>, + U::Error: fmt::Display, + { + #[pin] + inner: DispatcherState, + } } -#[pin_project(project = DispatcherStateProj)] -enum DispatcherState -where - S: Service, - S::Error: Into>, +#[cfg(test)] +pin_project! { + /// Dispatcher for HTTP/1.1 protocol + pub struct Dispatcher + where + S: Service, + S::Error: Into>, - B: MessageBody, + B: MessageBody, - X: Service, - X::Error: Into>, + X: Service, + X::Error: Into>, - U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display, -{ - Normal(#[pin] InnerDispatcher), - Upgrade(#[pin] U::Future), + U: Service<(Request, Framed), Response = ()>, + U::Error: fmt::Display, + { + #[pin] + inner: DispatcherState, + + // used in tests + poll_count: u64, + } } -#[pin_project(project = InnerDispatcherProj)] -struct InnerDispatcher -where - S: Service, - S::Error: Into>, +pin_project! { + #[project = DispatcherStateProj] + enum DispatcherState + where + S: Service, + S::Error: Into>, - B: MessageBody, + B: MessageBody, - X: Service, - X::Error: Into>, + X: Service, + X::Error: Into>, - U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display, -{ - flow: Rc>, - on_connect_data: OnConnectData, - flags: Flags, - peer_addr: Option, - error: Option, + U: Service<(Request, Framed), Response = ()>, + U::Error: fmt::Display, + { + Normal { #[pin] inner: InnerDispatcher }, + Upgrade { #[pin] fut: U::Future }, + } +} - #[pin] - state: State, - payload: Option, - messages: VecDeque, +pin_project! { + #[project = InnerDispatcherProj] + struct InnerDispatcher + where + S: Service, + S::Error: Into>, - ka_expire: Instant, - #[pin] - ka_timer: Option, + B: MessageBody, - io: Option, - read_buf: BytesMut, - write_buf: BytesMut, - codec: Codec, + X: Service, + X::Error: Into>, + + U: Service<(Request, Framed), Response = ()>, + U::Error: fmt::Display, + { + flow: Rc>, + flags: Flags, + peer_addr: Option, + conn_data: Option>, + error: Option, + + #[pin] + state: State, + payload: Option, + messages: VecDeque, + + ka_expire: Instant, + #[pin] + ka_timer: Option, + + io: Option, + read_buf: BytesMut, + write_buf: BytesMut, + codec: Codec, + } } enum DispatcherMessage { @@ -126,19 +159,21 @@ enum DispatcherMessage { Error(Response<()>), } -#[pin_project(project = StateProj)] -enum State -where - S: Service, - X: Service, +pin_project! { + #[project = StateProj] + enum State + where + S: Service, + X: Service, - B: MessageBody, -{ - None, - ExpectCall(#[pin] X::Future), - ServiceCall(#[pin] S::Future), - SendPayload(#[pin] B), - SendErrorPayload(#[pin] BoxBody), + B: MessageBody, + { + None, + ExpectCall { #[pin] fut: X::Future }, + ServiceCall { #[pin] fut: S::Future }, + SendPayload { #[pin] body: B }, + SendErrorPayload { #[pin] body: BoxBody }, + } } impl State @@ -179,10 +214,10 @@ where /// Create HTTP/1 dispatcher. pub(crate) fn new( io: T, - config: ServiceConfig, flow: Rc>, - on_connect_data: OnConnectData, + config: ServiceConfig, peer_addr: Option, + conn_data: OnConnectData, ) -> Self { let flags = if config.keep_alive_enabled() { Flags::KEEPALIVE @@ -197,22 +232,27 @@ where }; Dispatcher { - inner: DispatcherState::Normal(InnerDispatcher { - read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), - write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), - payload: None, - state: State::None, - error: None, - messages: VecDeque::new(), - io: Some(io), - codec: Codec::new(config), - flow, - on_connect_data, - flags, - peer_addr, - ka_expire, - ka_timer, - }), + inner: DispatcherState::Normal { + inner: InnerDispatcher { + flow, + flags, + peer_addr, + conn_data: conn_data.0.map(Rc::new), + error: None, + + state: State::None, + payload: None, + messages: VecDeque::new(), + + ka_expire, + ka_timer, + + io: Some(io), + read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), + write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), + codec: Codec::new(config), + }, + }, #[cfg(test)] poll_count: 0, @@ -256,10 +296,7 @@ where } } - fn poll_flush( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let InnerDispatcherProj { io, write_buf, .. } = self.project(); let mut io = Pin::new(io.as_mut().unwrap()); @@ -269,10 +306,7 @@ 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, - "", - ))) + return Poll::Ready(Err(io::Error::new(io::ErrorKind::WriteZero, ""))) } Poll::Ready(n) => written += n, Poll::Pending => { @@ -318,7 +352,7 @@ where let size = self.as_mut().send_response_inner(message, &body)?; let state = match size { BodySize::None | BodySize::Sized(0) => State::None, - _ => State::SendPayload(body), + _ => State::SendPayload { body }, }; self.project().state.set(state); Ok(()) @@ -332,7 +366,7 @@ where let size = self.as_mut().send_response_inner(message, &body)?; let state = match size { BodySize::None | BodySize::Sized(0) => State::None, - _ => State::SendErrorPayload(body), + _ => State::SendErrorPayload { body }, }; self.project().state.set(state); Ok(()) @@ -358,12 +392,12 @@ where // Handle `EXPECT: 100-Continue` header if req.head().expect() { // set InnerDispatcher state and continue loop to poll it. - let task = this.flow.expect.call(req); - this.state.set(State::ExpectCall(task)); + let fut = this.flow.expect.call(req); + this.state.set(State::ExpectCall { fut }); } else { // the same as expect call. - let task = this.flow.service.call(req); - this.state.set(State::ServiceCall(task)); + let fut = this.flow.service.call(req); + this.state.set(State::ServiceCall { fut }); }; } @@ -383,7 +417,7 @@ where // all messages are dealt with. None => return Ok(PollResponse::DoNothing), }, - StateProj::ServiceCall(fut) => match fut.poll(cx) { + StateProj::ServiceCall { fut } => match fut.poll(cx) { // service call resolved. send response. Poll::Ready(Ok(res)) => { let (res, body) = res.into().replace_body(()); @@ -409,21 +443,18 @@ where } }, - StateProj::SendPayload(mut stream) => { + StateProj::SendPayload { mut body } => { // keep populate writer buffer until buffer size limit hit, // get blocked or finished. while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE { - match stream.as_mut().poll_next(cx) { + match body.as_mut().poll_next(cx) { Poll::Ready(Some(Ok(item))) => { - this.codec.encode( - Message::Chunk(Some(item)), - this.write_buf, - )?; + this.codec + .encode(Message::Chunk(Some(item)), this.write_buf)?; } Poll::Ready(None) => { - this.codec - .encode(Message::Chunk(None), this.write_buf)?; + 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); @@ -442,23 +473,20 @@ where return Ok(PollResponse::DrainWriteBuf); } - StateProj::SendErrorPayload(mut stream) => { + StateProj::SendErrorPayload { mut body } => { // TODO: de-dupe impl with SendPayload // keep populate writer buffer until buffer size limit hit, // get blocked or finished. while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE { - match stream.as_mut().poll_next(cx) { + match body.as_mut().poll_next(cx) { Poll::Ready(Some(Ok(item))) => { - this.codec.encode( - Message::Chunk(Some(item)), - this.write_buf, - )?; + this.codec + .encode(Message::Chunk(Some(item)), this.write_buf)?; } Poll::Ready(None) => { - this.codec - .encode(Message::Chunk(None), this.write_buf)?; + 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); @@ -466,7 +494,9 @@ where } Poll::Ready(Some(Err(err))) => { - return Err(DispatchError::Service(err.into())) + return Err(DispatchError::Body( + Error::new_body().with_cause(err).into(), + )) } Poll::Pending => return Ok(PollResponse::DoNothing), @@ -477,14 +507,14 @@ where return Ok(PollResponse::DrainWriteBuf); } - StateProj::ExpectCall(fut) => match fut.poll(cx) { + 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)); + this.state.set(State::ServiceCall { fut }); } // send expect error as response @@ -510,25 +540,25 @@ where let mut this = self.as_mut().project(); if req.head().expect() { // set dispatcher state so the future is pinned. - let task = this.flow.expect.call(req); - this.state.set(State::ExpectCall(task)); + let fut = this.flow.expect.call(req); + this.state.set(State::ExpectCall { fut }); } else { // the same as above. - let task = this.flow.service.call(req); - this.state.set(State::ServiceCall(task)); + 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). loop { match self.as_mut().project().state.project() { - StateProj::ExpectCall(fut) => { + StateProj::ExpectCall { fut } => { match fut.poll(cx) { // 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 task = this.flow.service.call(req); - this.state.set(State::ServiceCall(task)); + 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 @@ -544,7 +574,7 @@ where } } } - StateProj::ServiceCall(fut) => { + 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 @@ -564,9 +594,11 @@ where } }; } - _ => unreachable!( - "State must be set to ServiceCall or ExceptCall in handle_request" - ), + _ => { + unreachable!( + "State must be set to ServiceCall or ExceptCall in handle_request" + ) + } } } } @@ -593,16 +625,14 @@ where Message::Item(mut req) => { req.head_mut().peer_addr = *this.peer_addr; - // merge on_connect_ext data into request extensions - this.on_connect_data.merge_into(&mut req); + 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 // upgraded Request. MessageType::Stream if this.flow.upgrade.is_some() => { - this.messages - .push_back(DispatcherMessage::Upgrade(req)); + this.messages.push_back(DispatcherMessage::Upgrade(req)); break; } @@ -616,11 +646,11 @@ where Payload is attached to Request and passed to Service::call where the state can be collected and consumed. */ - let (ps, pl) = Payload::create(false); + let (sender, payload) = Payload::create(false); let (req1, _) = - req.replace_payload(crate::Payload::H1(pl)); + req.replace_payload(crate::Payload::H1 { payload }); req = req1; - *this.payload = Some(ps); + *this.payload = Some(sender); } // Request has no payload. @@ -639,9 +669,7 @@ where if let Some(ref mut payload) = this.payload { payload.feed_data(chunk); } else { - error!( - "Internal server error: unexpected payload chunk" - ); + error!("Internal server error: unexpected payload chunk"); this.flags.insert(Flags::READ_DISCONNECT); this.messages.push_back(DispatcherMessage::Error( Response::internal_server_error().drop_body(), @@ -679,12 +707,11 @@ where payload.set_error(PayloadError::Overflow); } // Requests overflow buffer size should be responded with 431 - this.messages.push_back(DispatcherMessage::Error( - Response::with_body( + 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; @@ -726,8 +753,7 @@ where None => { // conditionally go into shutdown timeout if this.flags.contains(Flags::SHUTDOWN) { - if let Some(deadline) = this.codec.config().client_disconnect_timer() - { + 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))); @@ -770,9 +796,7 @@ where 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() - { + } else if let Some(deadline) = this.codec.config().keep_alive_expire() { timer.as_mut().reset(deadline); let _ = timer.poll(cx); } @@ -791,7 +815,6 @@ where /// Returns true when io stream can be disconnected after write to it. /// /// It covers these conditions: - /// /// - `std::io::ErrorKind::ConnectionReset` after partial read. /// - all data read done. #[inline(always)] @@ -811,46 +834,39 @@ where loop { // Return early when read buf exceed decoder's max buffer size. - if this.read_buf.len() >= super::decoder::MAX_BUFFER_SIZE { - /* - At this point it's not known IO stream is still scheduled - to be waked up. so force wake up dispatcher just in case. + if this.read_buf.len() >= MAX_BUFFER_SIZE { + // At this point it's not known IO stream is still scheduled to be waked up so + // force wake up dispatcher just in case. + // + // Reason: + // AsyncRead mostly would only have guarantee wake up when the poll_read + // return Poll::Pending. + // + // Case: + // 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) + // + // 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. - Reason: - AsyncRead mostly would only have guarantee wake up - when the poll_read return Poll::Pending. - - Case: - 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) - - 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. - */ if this.payload.is_none() { - /* - When dispatcher has a payload the responsibility of - wake up it would be shift to h1::payload::Payload. - - Reason: - Self wake up when there is payload would waste poll - and/or result in over read. - - Case: - When payload is (partial) dropped by user there is - no need to do read anymore. - At this case read_buf could always remain beyond - MAX_BUFFER_SIZE and self wake up would be busy poll - dispatcher and waste resource. - - */ + // When dispatcher has a payload the responsibility of wake up it would be shift + // to h1::payload::Payload. + // + // Reason: + // Self wake up when there is payload would waste poll and/or result in + // over read. + // + // Case: + // When payload is (partial) dropped by user there is no need to do + // read anymore. At this case read_buf could always remain beyond + // MAX_BUFFER_SIZE and self wake up would be busy poll dispatcher and + // waste resources. cx.waker().wake_by_ref(); } @@ -924,7 +940,7 @@ where } match this.inner.project() { - DispatcherStateProj::Normal(mut inner) => { + DispatcherStateProj::Normal { mut inner } => { inner.as_mut().poll_keepalive(cx)?; if inner.flags.contains(Flags::SHUTDOWN) { @@ -964,7 +980,7 @@ where self.as_mut() .project() .inner - .set(DispatcherState::Upgrade(upgrade)); + .set(DispatcherState::Upgrade { fut: upgrade }); return self.poll(cx); } }; @@ -1016,8 +1032,8 @@ where } } } - DispatcherStateProj::Upgrade(fut) => fut.poll(cx).map_err(|e| { - error!("Upgrade handler error: {}", e); + DispatcherStateProj::Upgrade { fut: upgrade } => upgrade.poll(cx).map_err(|err| { + error!("Upgrade handler error: {}", err); DispatchError::Upgrade }), } @@ -1058,14 +1074,12 @@ mod tests { } fn ok_service( - ) -> impl Service, Error = Error> - { + ) -> impl Service, Error = Error> { fn_service(|_req: Request| ready(Ok::<_, Error>(Response::ok()))) } fn echo_path_service( - ) -> impl Service, Error = Error> - { + ) -> impl Service, Error = Error> { fn_service(|req: Request| { let path = req.path().as_bytes(); ready(Ok::<_, Error>( @@ -1074,8 +1088,8 @@ mod tests { }) } - fn echo_payload_service( - ) -> impl Service, Error = Error> { + fn echo_payload_service() -> impl Service, Error = Error> + { fn_service(|mut req: Request| { Box::pin(async move { use futures_util::stream::StreamExt as _; @@ -1100,10 +1114,10 @@ mod tests { let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( buf, - ServiceConfig::default(), services, - OnConnectData::default(), + ServiceConfig::default(), None, + OnConnectData::default(), ); actix_rt::pin!(h1); @@ -1113,7 +1127,7 @@ mod tests { Poll::Ready(res) => assert!(res.is_err()), } - if let DispatcherStateProj::Normal(inner) = h1.project().inner.project() { + 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], @@ -1140,15 +1154,15 @@ mod tests { let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( buf, - cfg, services, - OnConnectData::default(), + cfg, None, + OnConnectData::default(), ); actix_rt::pin!(h1); - assert!(matches!(&h1.inner, DispatcherState::Normal(_))); + assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); match h1.as_mut().poll(cx) { Poll::Pending => panic!("first poll should not be pending"), @@ -1158,7 +1172,7 @@ mod tests { // polls: initial => shutdown assert_eq!(h1.poll_count, 2); - if let DispatcherStateProj::Normal(inner) = h1.project().inner.project() { + if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() { let res = &mut inner.project().io.take().unwrap().write_buf[..]; stabilize_date_header(res); @@ -1194,15 +1208,15 @@ mod tests { let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( buf, - cfg, services, - OnConnectData::default(), + cfg, None, + OnConnectData::default(), ); actix_rt::pin!(h1); - assert!(matches!(&h1.inner, DispatcherState::Normal(_))); + assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); match h1.as_mut().poll(cx) { Poll::Pending => panic!("first poll should not be pending"), @@ -1212,7 +1226,7 @@ mod tests { // polls: initial => shutdown assert_eq!(h1.poll_count, 1); - if let DispatcherStateProj::Normal(inner) = h1.project().inner.project() { + if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() { let res = &mut inner.project().io.take().unwrap().write_buf[..]; stabilize_date_header(res); @@ -1244,10 +1258,10 @@ mod tests { let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( buf.clone(), - cfg, services, - OnConnectData::default(), + cfg, None, + OnConnectData::default(), ); buf.extend_read_buf( @@ -1262,13 +1276,13 @@ mod tests { actix_rt::pin!(h1); assert!(h1.as_mut().poll(cx).is_pending()); - assert!(matches!(&h1.inner, DispatcherState::Normal(_))); + 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 { + if let DispatcherState::Normal { ref inner } = h1.inner { let io = inner.io.as_ref().unwrap(); let res = &io.write_buf()[..]; assert_eq!( @@ -1283,7 +1297,7 @@ mod tests { // polls: manual manual shutdown assert_eq!(h1.poll_count, 3); - if let DispatcherState::Normal(ref inner) = h1.inner { + 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); @@ -1316,10 +1330,10 @@ mod tests { let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( buf.clone(), - cfg, services, - OnConnectData::default(), + cfg, None, + OnConnectData::default(), ); buf.extend_read_buf( @@ -1334,12 +1348,12 @@ mod tests { actix_rt::pin!(h1); assert!(h1.as_mut().poll(cx).is_ready()); - assert!(matches!(&h1.inner, DispatcherState::Normal(_))); + assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); // polls: manual shutdown assert_eq!(h1.poll_count, 2); - if let DispatcherState::Normal(ref inner) = h1.inner { + 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); @@ -1393,10 +1407,10 @@ mod tests { let h1 = Dispatcher::<_, _, _, _, TestUpgrade>::new( buf.clone(), - cfg, services, - OnConnectData::default(), + cfg, None, + OnConnectData::default(), ); buf.extend_read_buf( @@ -1411,7 +1425,7 @@ mod tests { actix_rt::pin!(h1); assert!(h1.as_mut().poll(cx).is_ready()); - assert!(matches!(&h1.inner, DispatcherState::Upgrade(_))); + assert!(matches!(&h1.inner, DispatcherState::Upgrade { .. })); // polls: manual shutdown assert_eq!(h1.poll_count, 2); diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index fccd5da46..f2a862278 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -1,19 +1,19 @@ -use std::io::Write; -use std::marker::PhantomData; -use std::ptr::copy_nonoverlapping; -use std::slice::from_raw_parts_mut; -use std::{cmp, io}; +use std::{ + cmp, + io::{self, Write as _}, + marker::PhantomData, + ptr::copy_nonoverlapping, + slice::from_raw_parts_mut, +}; use bytes::{BufMut, BytesMut}; use crate::{ body::BodySize, - config::ServiceConfig, - header::{map::Value, HeaderMap, HeaderName}, - header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}, - helpers, - message::{ConnectionType, RequestHeadType}, - Response, StatusCode, Version, + header::{ + map::Value, HeaderMap, HeaderName, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, + }, + helpers, ConnectionType, RequestHeadType, Response, ServiceConfig, StatusCode, Version, }; const AVERAGE_HEADER_SIZE: usize = 30; @@ -103,9 +103,7 @@ pub(crate) trait MessageType: Sized { dst.put_slice(b"\r\n"); } } - BodySize::Sized(0) if camel_case => { - dst.put_slice(b"\r\nContent-Length: 0\r\n") - } + BodySize::Sized(0) if camel_case => dst.put_slice(b"\r\nContent-Length: 0\r\n"), BodySize::Sized(0) => dst.put_slice(b"\r\ncontent-length: 0\r\n"), BodySize::Sized(len) => helpers::write_content_length(len, dst), BodySize::None => dst.put_slice(b"\r\n"), @@ -307,11 +305,7 @@ impl MessageType for RequestHeadType { Version::HTTP_11 => "HTTP/1.1", Version::HTTP_2 => "HTTP/2.0", Version::HTTP_3 => "HTTP/3.0", - _ => - return Err(io::Error::new( - io::ErrorKind::Other, - "unsupported version" - )), + _ => return Err(io::Error::new(io::ErrorKind::Other, "unsupported version")), } ) .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) @@ -568,8 +562,7 @@ mod tests { ConnectionType::Close, &ServiceConfig::default(), ); - let data = - String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap(); + let data = String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap(); assert!(data.contains("Content-Length: 0\r\n")); assert!(data.contains("Connection: close\r\n")); @@ -583,8 +576,7 @@ mod tests { ConnectionType::KeepAlive, &ServiceConfig::default(), ); - let data = - String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap(); + let data = String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap(); assert!(data.contains("Transfer-Encoding: chunked\r\n")); assert!(data.contains("Content-Type: plain/text\r\n")); assert!(data.contains("Date: date\r\n")); @@ -605,8 +597,7 @@ mod tests { ConnectionType::KeepAlive, &ServiceConfig::default(), ); - let data = - String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap(); + let data = String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap(); assert!(data.contains("transfer-encoding: chunked\r\n")); assert!(data.contains("content-type: xml\r\n")); assert!(data.contains("content-type: plain/text\r\n")); @@ -639,8 +630,7 @@ mod tests { ConnectionType::Close, &ServiceConfig::default(), ); - let data = - String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap(); + let data = String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap(); assert!(data.contains("content-length: 0\r\n")); assert!(data.contains("connection: close\r\n")); assert!(data.contains("authorization: another authorization\r\n")); @@ -663,8 +653,7 @@ mod tests { ConnectionType::Upgrade, &ServiceConfig::default(), ); - let data = - String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap(); + let data = String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap(); assert!(!data.contains("content-length: 0\r\n")); assert!(!data.contains("transfer-encoding: chunked\r\n")); } diff --git a/actix-http/src/h1/expect.rs b/actix-http/src/h1/expect.rs index bb8e28e95..bef281e59 100644 --- a/actix-http/src/h1/expect.rs +++ b/actix-http/src/h1/expect.rs @@ -1,8 +1,7 @@ use actix_service::{Service, ServiceFactory}; use actix_utils::future::{ready, Ready}; -use crate::error::Error; -use crate::request::Request; +use crate::{Error, Request}; pub struct ExpectHandler; diff --git a/actix-http/src/h1/mod.rs b/actix-http/src/h1/mod.rs index 17cbfb90f..64586a2dc 100644 --- a/actix-http/src/h1/mod.rs +++ b/actix-http/src/h1/mod.rs @@ -59,7 +59,7 @@ pub(crate) fn reserve_readbuf(src: &mut BytesMut) { #[cfg(test)] mod tests { use super::*; - use crate::request::Request; + use crate::Request; impl Message { pub fn message(self) -> Request { diff --git a/actix-http/src/h1/payload.rs b/actix-http/src/h1/payload.rs index cc771f28a..4d031c15a 100644 --- a/actix-http/src/h1/payload.rs +++ b/actix-http/src/h1/payload.rs @@ -1,9 +1,12 @@ //! Payload stream -use std::cell::RefCell; -use std::collections::VecDeque; -use std::pin::Pin; -use std::rc::{Rc, Weak}; -use std::task::{Context, Poll, Waker}; + +use std::{ + cell::RefCell, + collections::VecDeque, + pin::Pin, + rc::{Rc, Weak}, + task::{Context, Poll, Waker}, +}; use bytes::Bytes; use futures_core::Stream; @@ -22,39 +25,32 @@ pub enum PayloadStatus { /// Buffered stream of bytes chunks /// -/// Payload stores chunks in a vector. First chunk can be received with -/// `.readany()` method. Payload stream is not thread safe. Payload does not -/// notify current task when new data is available. +/// Payload stores chunks in a vector. First chunk can be received with `poll_next`. Payload does +/// not notify current task when new data is available. /// -/// Payload stream can be used as `Response` body stream. +/// Payload can be used as `Response` body stream. #[derive(Debug)] pub struct Payload { inner: Rc>, } impl Payload { - /// Create payload stream. + /// Creates a payload stream. /// - /// This method construct two objects responsible for bytes stream - /// generation. - /// - /// * `PayloadSender` - *Sender* side of the stream - /// - /// * `Payload` - *Receiver* side of the stream + /// This method construct two objects responsible for bytes stream generation: + /// - `PayloadSender` - *Sender* side of the stream + /// - `Payload` - *Receiver* side of the stream pub fn create(eof: bool) -> (PayloadSender, Payload) { let shared = Rc::new(RefCell::new(Inner::new(eof))); ( - PayloadSender { - inner: Rc::downgrade(&shared), - }, + PayloadSender::new(Rc::downgrade(&shared)), Payload { inner: shared }, ) } - /// Create empty payload - #[doc(hidden)] - pub fn empty() -> Payload { + /// Creates an empty payload. + pub(crate) fn empty() -> Payload { Payload { inner: Rc::new(RefCell::new(Inner::new(true))), } @@ -77,14 +73,6 @@ impl Payload { pub fn unread_data(&mut self, data: Bytes) { self.inner.borrow_mut().unread_data(data); } - - #[inline] - pub fn readany( - &mut self, - cx: &mut Context<'_>, - ) -> Poll>> { - self.inner.borrow_mut().readany(cx) - } } impl Stream for Payload { @@ -94,7 +82,7 @@ impl Stream for Payload { self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll>> { - self.inner.borrow_mut().readany(cx) + Pin::new(&mut *self.inner.borrow_mut()).poll_next(cx) } } @@ -104,6 +92,10 @@ pub struct PayloadSender { } impl PayloadSender { + fn new(inner: Weak>) -> Self { + Self { inner } + } + #[inline] pub fn set_error(&mut self, err: PayloadError) { if let Some(shared) = self.inner.upgrade() { @@ -227,8 +219,8 @@ impl Inner { self.len } - fn readany( - &mut self, + fn poll_next( + mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll>> { if let Some(data) = self.items.pop_front() { @@ -260,8 +252,18 @@ impl Inner { #[cfg(test)] mod tests { - use super::*; + use std::panic::{RefUnwindSafe, UnwindSafe}; + use actix_utils::future::poll_fn; + use static_assertions::{assert_impl_all, assert_not_impl_any}; + + use super::*; + + assert_impl_all!(Payload: Unpin); + assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe); + + assert_impl_all!(Inner: Unpin, Send, Sync); + assert_not_impl_any!(Inner: UnwindSafe, RefUnwindSafe); #[actix_rt::test] async fn test_unread_data() { @@ -273,7 +275,10 @@ mod tests { assert_eq!( Bytes::from("data"), - poll_fn(|cx| payload.readany(cx)).await.unwrap().unwrap() + poll_fn(|cx| Pin::new(&mut payload).poll_next(cx)) + .await + .unwrap() + .unwrap() ); } } diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index 70e83901c..43b7919a7 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -266,8 +266,7 @@ where } } -impl ServiceFactory<(T, Option)> - for H1Service +impl ServiceFactory<(T, Option)> for H1Service where T: AsyncRead + AsyncWrite + Unpin + 'static, @@ -310,9 +309,9 @@ where let upgrade = match upgrade { Some(upgrade) => { - let upgrade = upgrade.await.map_err(|e| { - log::error!("Init http upgrade service error: {:?}", e) - })?; + let upgrade = upgrade + .await + .map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?; Some(upgrade) } None => None, @@ -336,8 +335,7 @@ where /// `Service` implementation for HTTP/1 transport pub type H1ServiceHandler = HttpServiceHandler; -impl Service<(T, Option)> - for HttpServiceHandler +impl Service<(T, Option)> for HttpServiceHandler where T: AsyncRead + AsyncWrite + Unpin, @@ -358,22 +356,14 @@ where type Future = Dispatcher; fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { - self._poll_ready(cx).map_err(|e| { - log::error!("HTTP/1 service readiness error: {:?}", e); - DispatchError::Service(e) + self._poll_ready(cx).map_err(|err| { + log::error!("HTTP/1 service readiness error: {:?}", err); + DispatchError::Service(err) }) } fn call(&self, (io, addr): (T, Option)) -> Self::Future { - let on_connect_data = - OnConnectData::from_io(&io, self.on_connect_ext.as_deref()); - - Dispatcher::new( - io, - self.cfg.clone(), - self.flow.clone(), - on_connect_data, - addr, - ) + let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref()); + Dispatcher::new(io, self.flow.clone(), self.cfg.clone(), addr, conn_data) } } diff --git a/actix-http/src/h1/upgrade.rs b/actix-http/src/h1/upgrade.rs index e57ea8ae9..f25b0718b 100644 --- a/actix-http/src/h1/upgrade.rs +++ b/actix-http/src/h1/upgrade.rs @@ -2,9 +2,7 @@ use actix_codec::Framed; use actix_service::{Service, ServiceFactory}; use futures_core::future::LocalBoxFuture; -use crate::error::Error; -use crate::h1::Codec; -use crate::request::Request; +use crate::{h1::Codec, Error, Request}; pub struct UpgradeHandler; diff --git a/actix-http/src/h1/utils.rs b/actix-http/src/h1/utils.rs index 905585a32..5c11b1dab 100644 --- a/actix-http/src/h1/utils.rs +++ b/actix-http/src/h1/utils.rs @@ -9,9 +9,8 @@ use pin_project_lite::pin_project; use crate::{ body::{BodySize, MessageBody}, - error::Error, h1::{Codec, Message}, - response::Response, + Error, Response, }; pin_project! { @@ -46,7 +45,7 @@ where impl Future for SendResponse where T: AsyncRead + AsyncWrite + Unpin, - B: MessageBody + Unpin, + B: MessageBody, B::Error: Into, { type Output = Result, Error>; @@ -70,27 +69,24 @@ where .unwrap() .is_write_buf_full() { - let next = - match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx) { - Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(item)), - Poll::Ready(Some(Err(err))) => { - return Poll::Ready(Err(err.into())) - } - Poll::Ready(None) => Poll::Ready(None), - Poll::Pending => Poll::Pending, - }; + let next = match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx) { + Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(item)), + Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err.into())), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + }; match next { Poll::Ready(item) => { // body is done when item is None body_done = item.is_none(); if body_done { - let _ = this.body.take(); + this.body.set(None); } let framed = this.framed.as_mut().as_pin_mut().unwrap(); - framed.write(Message::Chunk(item)).map_err(|err| { - Error::new_send_response().with_cause(err) - })?; + framed + .write(Message::Chunk(item)) + .map_err(|err| Error::new_send_response().with_cause(err))?; } Poll::Pending => body_ready = false, } diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 6d2f4579a..a90eb3466 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -19,15 +19,15 @@ use h2::{ server::{Connection, SendResponse}, Ping, PingPong, }; -use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use log::{error, trace}; use pin_project_lite::pin_project; use crate::{ body::{BodySize, BoxBody, MessageBody}, config::ServiceConfig, + header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}, service::HttpFlow, - OnConnectData, Payload, Request, Response, ResponseHead, + Extensions, OnConnectData, Payload, Request, Response, ResponseHead, }; const CHUNK_SIZE: usize = 16_384; @@ -37,7 +37,7 @@ pin_project! { pub struct Dispatcher { flow: Rc>, connection: Connection, - on_connect_data: OnConnectData, + conn_data: Option>, config: ServiceConfig, peer_addr: Option, ping_pong: Option, @@ -50,11 +50,11 @@ where T: AsyncRead + AsyncWrite + Unpin, { pub(crate) fn new( - flow: Rc>, mut conn: Connection, - on_connect_data: OnConnectData, + flow: Rc>, config: ServiceConfig, peer_addr: Option, + conn_data: OnConnectData, timer: Option>>, ) -> Self { let ping_pong = config.keep_alive().map(|dur| H2PingPong { @@ -74,7 +74,7 @@ where config, peer_addr, connection: conn, - on_connect_data, + conn_data: conn_data.0.map(Rc::new), ping_pong, _phantom: PhantomData, } @@ -108,8 +108,8 @@ where match Pin::new(&mut this.connection).poll_accept(cx)? { Poll::Ready(Some((req, tx))) => { let (parts, body) = req.into_parts(); - let pl = crate::h2::Payload::new(body); - let pl = Payload::::H2(pl); + let payload = crate::h2::Payload::new(body); + let pl = Payload::H2 { payload }; let mut req = Request::with_payload(pl); let head = req.head_mut(); @@ -119,8 +119,7 @@ where head.headers = parts.headers.into(); head.peer_addr = this.peer_addr; - // merge on_connect_ext data into request extensions - this.on_connect_data.merge_into(&mut req); + req.conn_data = this.conn_data.as_ref().map(Rc::clone); let fut = this.flow.service.call(req); let config = this.config.clone(); @@ -161,16 +160,11 @@ where Poll::Ready(_) => { ping_pong.on_flight = false; - let dead_line = - this.config.keep_alive_expire().unwrap(); + let dead_line = this.config.keep_alive_expire().unwrap(); ping_pong.timer.as_mut().reset(dead_line); } Poll::Pending => { - return ping_pong - .timer - .as_mut() - .poll(cx) - .map(|_| Ok(())) + return ping_pong.timer.as_mut().poll(cx).map(|_| Ok(())) } } } else { @@ -223,25 +217,28 @@ where return Ok(()); } - // poll response body and send chunks to client. + // poll response body and send chunks to client actix_rt::pin!(body); while let Some(res) = poll_fn(|cx| body.as_mut().poll_next(cx)).await { let mut chunk = res.map_err(|err| DispatchError::ResponseBody(err.into()))?; 'send: loop { + let chunk_size = cmp::min(chunk.len(), CHUNK_SIZE); + // reserve enough space and wait for stream ready. - stream.reserve_capacity(cmp::min(chunk.len(), CHUNK_SIZE)); + stream.reserve_capacity(chunk_size); match poll_fn(|cx| stream.poll_capacity(cx)).await { // No capacity left. drop body and return. None => return Ok(()), - Some(res) => { - // Split chuck to writeable size and send to client. - let cap = res.map_err(DispatchError::SendData)?; + Some(Err(err)) => return Err(DispatchError::SendData(err)), + + Some(Ok(cap)) => { + // split chunk to writeable size and send to client let len = chunk.len(); - let bytes = chunk.split_to(cmp::min(cap, len)); + let bytes = chunk.split_to(cmp::min(len, cap)); stream .send_data(bytes, false) @@ -291,9 +288,11 @@ fn prepare_response( let _ = match size { BodySize::None | BodySize::Stream => None, - BodySize::Sized(0) => res - .headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), + BodySize::Sized(0) => { + #[allow(clippy::declare_interior_mutable_const)] + const HV_ZERO: HeaderValue = HeaderValue::from_static("0"); + res.headers_mut().insert(CONTENT_LENGTH, HV_ZERO) + } BodySize::Sized(len) => { let mut buf = itoa::Buffer::new(); diff --git a/actix-http/src/h2/mod.rs b/actix-http/src/h2/mod.rs index 25d53403e..47d51b420 100644 --- a/actix-http/src/h2/mod.rs +++ b/actix-http/src/h2/mod.rs @@ -40,10 +40,7 @@ impl Payload { impl Stream for Payload { type Item = Result; - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); match ready!(Pin::new(&mut this.stream).poll_data(cx)) { @@ -101,3 +98,14 @@ where } } } + +#[cfg(test)] +mod tests { + use std::panic::{RefUnwindSafe, UnwindSafe}; + + use static_assertions::assert_impl_all; + + use super::*; + + assert_impl_all!(Payload: Unpin, Send, Sync, UnwindSafe, RefUnwindSafe); +} diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 8a9061b94..469648054 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -1,7 +1,7 @@ use std::{ future::Future, marker::PhantomData, - net, + mem, net, pin::Pin, rc::Rc, task::{Context, Poll}, @@ -10,8 +10,7 @@ use std::{ use actix_codec::{AsyncRead, AsyncWrite}; use actix_rt::net::TcpStream; use actix_service::{ - fn_factory, fn_service, IntoServiceFactory, Service, ServiceFactory, - ServiceFactoryExt as _, + fn_factory, fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _, }; use actix_utils::future::ready; use futures_core::{future::LocalBoxFuture, ready}; @@ -271,16 +270,15 @@ where type Future = H2ServiceHandlerResponse; fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { - self.flow.service.poll_ready(cx).map_err(|e| { - let e = e.into(); - error!("Service readiness error: {:?}", e); - DispatchError::Service(e) + self.flow.service.poll_ready(cx).map_err(|err| { + let err = err.into(); + error!("Service readiness error: {:?}", err); + DispatchError::Service(err) }) } fn call(&self, (io, addr): (T, Option)) -> Self::Future { - let on_connect_data = - OnConnectData::from_io(&io, self.on_connect_ext.as_deref()); + let on_connect_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref()); H2ServiceHandlerResponse { state: State::Handshake( @@ -299,7 +297,6 @@ where T: AsyncRead + AsyncWrite + Unpin, S::Future: 'static, { - Incoming(Dispatcher), Handshake( Option>>, Option, @@ -307,6 +304,7 @@ where OnConnectData, HandshakeWithTimeout, ), + Established(Dispatcher), } pub struct H2ServiceHandlerResponse @@ -334,31 +332,35 @@ where fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match self.state { - State::Incoming(ref mut disp) => Pin::new(disp).poll(cx), State::Handshake( ref mut srv, ref mut config, ref peer_addr, - ref mut on_connect_data, + ref mut conn_data, ref mut handshake, ) => match ready!(Pin::new(handshake).poll(cx)) { Ok((conn, timer)) => { - let on_connect_data = std::mem::take(on_connect_data); - self.state = State::Incoming(Dispatcher::new( - srv.take().unwrap(), + let on_connect_data = mem::take(conn_data); + + self.state = State::Established(Dispatcher::new( conn, - on_connect_data, + srv.take().unwrap(), config.take().unwrap(), *peer_addr, + on_connect_data, timer, )); + self.poll(cx) } + Err(err) => { trace!("H2 handshake error: {}", err); Poll::Ready(Err(err)) } }, + + State::Established(ref mut disp) => Pin::new(disp).poll(cx), } } } diff --git a/actix-http/src/header/as_name.rs b/actix-http/src/header/as_name.rs index 04d32c41d..a895010b1 100644 --- a/actix-http/src/header/as_name.rs +++ b/actix-http/src/header/as_name.rs @@ -6,7 +6,7 @@ use http::header::{HeaderName, InvalidHeaderName}; /// Sealed trait implemented for types that can be effectively borrowed as a [`HeaderValue`]. /// -/// [`HeaderValue`]: crate::http::HeaderValue +/// [`HeaderValue`]: super::HeaderValue pub trait AsHeaderName: Sealed {} pub struct Seal; @@ -16,6 +16,7 @@ pub trait Sealed { } impl Sealed for HeaderName { + #[inline] fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { Ok(Cow::Borrowed(self)) } @@ -23,6 +24,7 @@ impl Sealed for HeaderName { impl AsHeaderName for HeaderName {} impl Sealed for &HeaderName { + #[inline] fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { Ok(Cow::Borrowed(*self)) } @@ -30,6 +32,7 @@ impl Sealed for &HeaderName { impl AsHeaderName for &HeaderName {} impl Sealed for &str { + #[inline] fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { HeaderName::from_str(self).map(Cow::Owned) } @@ -37,6 +40,7 @@ impl Sealed for &str { impl AsHeaderName for &str {} impl Sealed for String { + #[inline] fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { HeaderName::from_str(self).map(Cow::Owned) } @@ -44,6 +48,7 @@ impl Sealed for String { impl AsHeaderName for String {} impl Sealed for &String { + #[inline] fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { HeaderName::from_str(self).map(Cow::Owned) } diff --git a/actix-http/src/header/into_pair.rs b/actix-http/src/header/into_pair.rs index 472700548..91c3e6640 100644 --- a/actix-http/src/header/into_pair.rs +++ b/actix-http/src/header/into_pair.rs @@ -1,22 +1,20 @@ -//! [`IntoHeaderPair`] trait and implementations. +//! [`TryIntoHeaderPair`] trait and implementations. use std::convert::TryFrom as _; -use http::{ - header::{HeaderName, InvalidHeaderName, InvalidHeaderValue}, - Error as HttpError, HeaderValue, +use super::{ + Header, HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, TryIntoHeaderValue, }; +use crate::error::HttpError; -use super::{Header, IntoHeaderValue}; - -/// An interface for types that can be converted into a [`HeaderName`]/[`HeaderValue`] pair for +/// An interface for types that can be converted into a [`HeaderName`] + [`HeaderValue`] pair for /// insertion into a [`HeaderMap`]. /// -/// [`HeaderMap`]: crate::http::HeaderMap -pub trait IntoHeaderPair: Sized { +/// [`HeaderMap`]: super::HeaderMap +pub trait TryIntoHeaderPair: Sized { type Error: Into; - fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error>; + fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error>; } #[derive(Debug)] @@ -34,14 +32,14 @@ impl From for HttpError { } } -impl IntoHeaderPair for (HeaderName, V) +impl TryIntoHeaderPair for (HeaderName, V) where - V: IntoHeaderValue, + V: TryIntoHeaderValue, V::Error: Into, { type Error = InvalidHeaderPart; - fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { + fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { let (name, value) = self; let value = value .try_into_value() @@ -50,14 +48,14 @@ where } } -impl IntoHeaderPair for (&HeaderName, V) +impl TryIntoHeaderPair for (&HeaderName, V) where - V: IntoHeaderValue, + V: TryIntoHeaderValue, V::Error: Into, { type Error = InvalidHeaderPart; - fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { + fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { let (name, value) = self; let value = value .try_into_value() @@ -66,14 +64,14 @@ where } } -impl IntoHeaderPair for (&[u8], V) +impl TryIntoHeaderPair for (&[u8], V) where - V: IntoHeaderValue, + V: TryIntoHeaderValue, V::Error: Into, { type Error = InvalidHeaderPart; - fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { + fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { let (name, value) = self; let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?; let value = value @@ -83,14 +81,14 @@ where } } -impl IntoHeaderPair for (&str, V) +impl TryIntoHeaderPair for (&str, V) where - V: IntoHeaderValue, + V: TryIntoHeaderValue, V::Error: Into, { type Error = InvalidHeaderPart; - fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { + fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { let (name, value) = self; let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?; let value = value @@ -100,23 +98,25 @@ where } } -impl IntoHeaderPair for (String, V) +impl TryIntoHeaderPair for (String, V) where - V: IntoHeaderValue, + V: TryIntoHeaderValue, V::Error: Into, { type Error = InvalidHeaderPart; - fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { + #[inline] + fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { let (name, value) = self; - (name.as_str(), value).try_into_header_pair() + (name.as_str(), value).try_into_pair() } } -impl IntoHeaderPair for T { - type Error = ::Error; +impl TryIntoHeaderPair for T { + type Error = ::Error; - fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { + #[inline] + fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { Ok((T::name(), self.try_into_value()?)) } } diff --git a/actix-http/src/header/into_value.rs b/actix-http/src/header/into_value.rs index bad05db64..6d369ee65 100644 --- a/actix-http/src/header/into_value.rs +++ b/actix-http/src/header/into_value.rs @@ -1,4 +1,4 @@ -//! [`IntoHeaderValue`] trait and implementations. +//! [`TryIntoHeaderValue`] trait and implementations. use std::convert::TryFrom as _; @@ -7,7 +7,7 @@ use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue}; use mime::Mime; /// An interface for types that can be converted into a [`HeaderValue`]. -pub trait IntoHeaderValue: Sized { +pub trait TryIntoHeaderValue: Sized { /// The type returned in the event of a conversion error. type Error: Into; @@ -15,7 +15,7 @@ pub trait IntoHeaderValue: Sized { fn try_into_value(self) -> Result; } -impl IntoHeaderValue for HeaderValue { +impl TryIntoHeaderValue for HeaderValue { type Error = InvalidHeaderValue; #[inline] @@ -24,7 +24,7 @@ impl IntoHeaderValue for HeaderValue { } } -impl IntoHeaderValue for &HeaderValue { +impl TryIntoHeaderValue for &HeaderValue { type Error = InvalidHeaderValue; #[inline] @@ -33,7 +33,7 @@ impl IntoHeaderValue for &HeaderValue { } } -impl IntoHeaderValue for &str { +impl TryIntoHeaderValue for &str { type Error = InvalidHeaderValue; #[inline] @@ -42,7 +42,7 @@ impl IntoHeaderValue for &str { } } -impl IntoHeaderValue for &[u8] { +impl TryIntoHeaderValue for &[u8] { type Error = InvalidHeaderValue; #[inline] @@ -51,7 +51,7 @@ impl IntoHeaderValue for &[u8] { } } -impl IntoHeaderValue for Bytes { +impl TryIntoHeaderValue for Bytes { type Error = InvalidHeaderValue; #[inline] @@ -60,7 +60,7 @@ impl IntoHeaderValue for Bytes { } } -impl IntoHeaderValue for Vec { +impl TryIntoHeaderValue for Vec { type Error = InvalidHeaderValue; #[inline] @@ -69,7 +69,7 @@ impl IntoHeaderValue for Vec { } } -impl IntoHeaderValue for String { +impl TryIntoHeaderValue for String { type Error = InvalidHeaderValue; #[inline] @@ -78,7 +78,7 @@ impl IntoHeaderValue for String { } } -impl IntoHeaderValue for usize { +impl TryIntoHeaderValue for usize { type Error = InvalidHeaderValue; #[inline] @@ -87,7 +87,7 @@ impl IntoHeaderValue for usize { } } -impl IntoHeaderValue for i64 { +impl TryIntoHeaderValue for i64 { type Error = InvalidHeaderValue; #[inline] @@ -96,7 +96,7 @@ impl IntoHeaderValue for i64 { } } -impl IntoHeaderValue for u64 { +impl TryIntoHeaderValue for u64 { type Error = InvalidHeaderValue; #[inline] @@ -105,7 +105,7 @@ impl IntoHeaderValue for u64 { } } -impl IntoHeaderValue for i32 { +impl TryIntoHeaderValue for i32 { type Error = InvalidHeaderValue; #[inline] @@ -114,7 +114,7 @@ impl IntoHeaderValue for i32 { } } -impl IntoHeaderValue for u32 { +impl TryIntoHeaderValue for u32 { type Error = InvalidHeaderValue; #[inline] @@ -123,7 +123,7 @@ impl IntoHeaderValue for u32 { } } -impl IntoHeaderValue for Mime { +impl TryIntoHeaderValue for Mime { type Error = InvalidHeaderValue; #[inline] diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index 7b18be991..478867ed0 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -123,12 +123,11 @@ impl HeaderMap { let mut map = HeaderMap::with_capacity(capacity); map.append(first_name.clone(), first_value); - let (map, _) = - drain.fold((map, first_name), |(mut map, prev_name), (name, value)| { - let name = name.unwrap_or(prev_name); - map.append(name.clone(), value); - (map, name) - }); + let (map, _) = drain.fold((map, first_name), |(mut map, prev_name), (name, value)| { + let name = name.unwrap_or(prev_name); + map.append(name.clone(), value); + (map, name) + }); map } @@ -307,8 +306,11 @@ impl HeaderMap { /// assert_eq!(set_cookies_iter.next().unwrap(), "two=2"); /// assert!(set_cookies_iter.next().is_none()); /// ``` - pub fn get_all(&self, key: impl AsHeaderName) -> GetAll<'_> { - GetAll::new(self.get_value(key)) + pub fn get_all(&self, key: impl AsHeaderName) -> std::slice::Iter<'_, HeaderValue> { + match self.get_value(key) { + Some(value) => value.iter(), + None => (&[]).iter(), + } } // TODO: get_all_mut ? @@ -334,7 +336,7 @@ impl HeaderMap { } } - /// Inserts a name-value pair into the map. + /// Inserts (overrides) a name-value pair in the map. /// /// If the map already contained this key, the new value is associated with the key and all /// previous values are removed and returned as a `Removed` iterator. The key is not updated; @@ -373,7 +375,7 @@ impl HeaderMap { Removed::new(value) } - /// Inserts a name-value pair into the map. + /// Appends a name-value pair to the map. /// /// If the map already contained this key, the new value is added to the list of values /// currently associated with the key. The key is not updated; this matters for types that can @@ -603,52 +605,6 @@ impl<'a> IntoIterator for &'a HeaderMap { } } -/// Iterator over borrowed values with the same associated name. -/// -/// See [`HeaderMap::get_all`]. -#[derive(Debug)] -pub struct GetAll<'a> { - idx: usize, - value: Option<&'a Value>, -} - -impl<'a> GetAll<'a> { - fn new(value: Option<&'a Value>) -> Self { - Self { idx: 0, value } - } -} - -impl<'a> Iterator for GetAll<'a> { - type Item = &'a HeaderValue; - - fn next(&mut self) -> Option { - let val = self.value?; - - match val.get(self.idx) { - Some(val) => { - self.idx += 1; - Some(val) - } - None => { - // current index is none; remove value to fast-path future next calls - self.value = None; - None - } - } - } - - fn size_hint(&self) -> (usize, Option) { - match self.value { - Some(val) => (val.len(), Some(val.len())), - None => (0, Some(0)), - } - } -} - -impl ExactSizeIterator for GetAll<'_> {} - -impl iter::FusedIterator for GetAll<'_> {} - /// Iterator over removed, owned values with the same associated name. /// /// Returned from methods that remove or replace items. See [`HeaderMap::insert`] @@ -896,7 +852,7 @@ mod tests { assert_impl_all!(HeaderMap: IntoIterator); assert_impl_all!(Keys<'_>: Iterator, ExactSizeIterator, FusedIterator); - assert_impl_all!(GetAll<'_>: Iterator, ExactSizeIterator, FusedIterator); + assert_impl_all!(std::slice::Iter<'_, HeaderValue>: Iterator, ExactSizeIterator, FusedIterator); assert_impl_all!(Removed: Iterator, ExactSizeIterator, FusedIterator); assert_impl_all!(Iter<'_>: Iterator, ExactSizeIterator, FusedIterator); assert_impl_all!(IntoIter: Iterator, ExactSizeIterator, FusedIterator); diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index 381842e74..dd4f06106 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -11,22 +11,20 @@ pub use http::header::{ pub use http::header::{ ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES, ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, - ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, - ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_MAX_AGE, - ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, AGE, ALLOW, ALT_SVC, - AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_DISPOSITION, CONTENT_ENCODING, - CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_RANGE, - CONTENT_SECURITY_POLICY, CONTENT_SECURITY_POLICY_REPORT_ONLY, CONTENT_TYPE, COOKIE, - DATE, DNT, ETAG, EXPECT, EXPIRES, FORWARDED, FROM, HOST, IF_MATCH, - IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_RANGE, IF_UNMODIFIED_SINCE, LAST_MODIFIED, - LINK, LOCATION, MAX_FORWARDS, ORIGIN, PRAGMA, PROXY_AUTHENTICATE, - PROXY_AUTHORIZATION, PUBLIC_KEY_PINS, PUBLIC_KEY_PINS_REPORT_ONLY, RANGE, REFERER, - REFERRER_POLICY, REFRESH, RETRY_AFTER, SEC_WEBSOCKET_ACCEPT, - SEC_WEBSOCKET_EXTENSIONS, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_PROTOCOL, + ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS, + ACCESS_CONTROL_MAX_AGE, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, AGE, + ALLOW, ALT_SVC, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_DISPOSITION, + CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_RANGE, + CONTENT_SECURITY_POLICY, CONTENT_SECURITY_POLICY_REPORT_ONLY, CONTENT_TYPE, COOKIE, DATE, + DNT, ETAG, EXPECT, EXPIRES, FORWARDED, FROM, HOST, IF_MATCH, IF_MODIFIED_SINCE, + IF_NONE_MATCH, IF_RANGE, IF_UNMODIFIED_SINCE, LAST_MODIFIED, LINK, LOCATION, MAX_FORWARDS, + ORIGIN, PRAGMA, PROXY_AUTHENTICATE, PROXY_AUTHORIZATION, PUBLIC_KEY_PINS, + PUBLIC_KEY_PINS_REPORT_ONLY, RANGE, REFERER, REFERRER_POLICY, REFRESH, RETRY_AFTER, + SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_EXTENSIONS, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_PROTOCOL, SEC_WEBSOCKET_VERSION, SERVER, SET_COOKIE, STRICT_TRANSPORT_SECURITY, TE, TRAILER, - TRANSFER_ENCODING, UPGRADE, UPGRADE_INSECURE_REQUESTS, USER_AGENT, VARY, VIA, - WARNING, WWW_AUTHENTICATE, X_CONTENT_TYPE_OPTIONS, X_DNS_PREFETCH_CONTROL, - X_FRAME_OPTIONS, X_XSS_PROTECTION, + TRANSFER_ENCODING, UPGRADE, UPGRADE_INSECURE_REQUESTS, USER_AGENT, VARY, VIA, WARNING, + WWW_AUTHENTICATE, X_CONTENT_TYPE_OPTIONS, X_DNS_PREFETCH_CONTROL, X_FRAME_OPTIONS, + X_XSS_PROTECTION, }; use crate::{error::ParseError, HttpMessage}; @@ -39,19 +37,19 @@ mod shared; mod utils; pub use self::as_name::AsHeaderName; -pub use self::into_pair::IntoHeaderPair; -pub use self::into_value::IntoHeaderValue; +pub use self::into_pair::TryIntoHeaderPair; +pub use self::into_value::TryIntoHeaderValue; pub use self::map::HeaderMap; pub use self::shared::{ - parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, - LanguageTag, Quality, QualityItem, + parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, LanguageTag, + Quality, QualityItem, }; pub use self::utils::{ fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode, }; /// An interface for types that already represent a valid header. -pub trait Header: IntoHeaderValue { +pub trait Header: TryIntoHeaderValue { /// Returns the name of the header field fn name() -> HeaderName; diff --git a/actix-http/src/header/shared/content_encoding.rs b/actix-http/src/header/shared/content_encoding.rs index 073d90dce..68511a8ee 100644 --- a/actix-http/src/header/shared/content_encoding.rs +++ b/actix-http/src/header/shared/content_encoding.rs @@ -5,7 +5,7 @@ use http::header::InvalidHeaderValue; use crate::{ error::ParseError, - header::{self, from_one_raw_str, Header, HeaderName, HeaderValue, IntoHeaderValue}, + header::{self, from_one_raw_str, Header, HeaderName, HeaderValue, TryIntoHeaderValue}, HttpMessage, }; @@ -45,13 +45,13 @@ pub enum ContentEncoding { impl ContentEncoding { /// Is the content compressed? #[inline] - pub fn is_compression(self) -> bool { + pub const fn is_compression(self) -> bool { matches!(self, ContentEncoding::Identity | ContentEncoding::Auto) } /// Convert content encoding to string. #[inline] - pub fn as_str(self) -> &'static str { + pub const fn as_str(self) -> &'static str { match self { ContentEncoding::Br => "br", ContentEncoding::Gzip => "gzip", @@ -96,7 +96,7 @@ impl TryFrom<&str> for ContentEncoding { } } -impl IntoHeaderValue for ContentEncoding { +impl TryIntoHeaderValue for ContentEncoding { type Error = InvalidHeaderValue; fn try_into_value(self) -> Result { diff --git a/actix-http/src/header/shared/extended.rs b/actix-http/src/header/shared/extended.rs index 60f2d359e..1af9ca20e 100644 --- a/actix-http/src/header/shared/extended.rs +++ b/actix-http/src/header/shared/extended.rs @@ -63,9 +63,7 @@ pub struct ExtendedValue { /// [RFC 2231 §7]: https://datatracker.ietf.org/doc/html/rfc2231#section-7 /// [RFC 2978 §2.3]: https://datatracker.ietf.org/doc/html/rfc2978#section-2.3 /// [RFC 3986 §2.1]: https://datatracker.ietf.org/doc/html/rfc5646#section-2.1 -pub fn parse_extended_value( - val: &str, -) -> Result { +pub fn parse_extended_value(val: &str) -> Result { // Break into three pieces separated by the single-quote character let mut parts = val.splitn(3, '\''); @@ -100,8 +98,7 @@ pub fn parse_extended_value( impl fmt::Display for ExtendedValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let encoded_value = - percent_encoding::percent_encode(&self.value[..], HTTP_VALUE); + let encoded_value = percent_encoding::percent_encode(&self.value[..], HTTP_VALUE); if let Some(ref lang) = self.language_tag { write!(f, "{}'{}'{}", self.charset, lang, encoded_value) } else { @@ -143,8 +140,8 @@ mod tests { assert!(extended_value.language_tag.is_none()); assert_eq!( vec![ - 194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', - b't', b'e', b's', + 194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', b't', + b'e', b's', ], extended_value.value ); @@ -185,8 +182,8 @@ mod tests { charset: Charset::Ext("UTF-8".to_string()), language_tag: None, value: vec![ - 194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', - b't', b'e', b's', + 194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', b't', + b'e', b's', ], }; assert_eq!( diff --git a/actix-http/src/header/shared/http_date.rs b/actix-http/src/header/shared/http_date.rs index 8dbdf4a62..473d6cad0 100644 --- a/actix-http/src/header/shared/http_date.rs +++ b/actix-http/src/header/shared/http_date.rs @@ -4,7 +4,7 @@ use bytes::BytesMut; use http::header::{HeaderValue, InvalidHeaderValue}; use crate::{ - config::DATE_VALUE_LENGTH, error::ParseError, header::IntoHeaderValue, + config::DATE_VALUE_LENGTH, error::ParseError, header::TryIntoHeaderValue, helpers::MutWriter, }; @@ -30,7 +30,7 @@ impl fmt::Display for HttpDate { } } -impl IntoHeaderValue for HttpDate { +impl TryIntoHeaderValue for HttpDate { type Error = InvalidHeaderValue; fn try_into_value(self) -> Result { diff --git a/actix-http/src/header/shared/quality.rs b/actix-http/src/header/shared/quality.rs index 5321c754d..c2f08edc2 100644 --- a/actix-http/src/header/shared/quality.rs +++ b/actix-http/src/header/shared/quality.rs @@ -87,7 +87,7 @@ impl fmt::Display for Quality { // 0 is already handled so it's not possible to have a trailing 0 in this range // we can just write the integer - itoa::fmt(f, x) + itoa_fmt(f, x) } else if x < 100 { // x in is range 10–99 @@ -95,21 +95,21 @@ impl fmt::Display for Quality { if x % 10 == 0 { // trailing 0, divide by 10 and write - itoa::fmt(f, x / 10) + itoa_fmt(f, x / 10) } else { - itoa::fmt(f, x) + itoa_fmt(f, x) } } else { // x is in range 100–999 if x % 100 == 0 { // two trailing 0s, divide by 100 and write - itoa::fmt(f, x / 100) + itoa_fmt(f, x / 100) } else if x % 10 == 0 { // one trailing 0, divide by 10 and write - itoa::fmt(f, x / 10) + itoa_fmt(f, x / 10) } else { - itoa::fmt(f, x) + itoa_fmt(f, x) } } } @@ -117,6 +117,12 @@ impl fmt::Display for Quality { } } +/// Write integer to a `fmt::Write`. +pub fn itoa_fmt(mut wr: W, value: V) -> fmt::Result { + let mut buf = itoa::Buffer::new(); + wr.write_str(buf.format(value)) +} + #[derive(Debug, Clone, Display, Error)] #[display(fmt = "quality out of bounds")] #[non_exhaustive] diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index 9354915ad..c9eee7d9d 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -120,8 +120,7 @@ impl str::FromStr for QualityItem { } let q_value = q_val.parse::().map_err(|_| ParseError::Header)?; - let q_value = - Quality::try_from(q_value).map_err(|_| ParseError::Header)?; + let q_value = Quality::try_from(q_value).map_err(|_| ParseError::Header)?; quality = q_value; raw_item = val; diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index aeba3da36..f2b415790 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -14,11 +14,11 @@ //! [rustls]: https://crates.io/crates/rustls //! [trust-dns]: https://crates.io/crates/trust-dns -#![deny(rust_2018_idioms, nonstandard_style, clippy::uninit_assumed_init)] +#![deny(rust_2018_idioms, nonstandard_style)] +#![warn(future_incompatible)] #![allow( clippy::type_complexity, clippy::too_many_arguments, - clippy::new_without_default, clippy::borrow_interior_mutable_const )] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] @@ -27,26 +27,26 @@ #[macro_use] extern crate log; +pub use ::http::{uri, uri::Uri}; +pub use ::http::{Method, StatusCode, Version}; + pub mod body; mod builder; mod config; - #[cfg(feature = "__compress")] pub mod encoding; +pub mod error; mod extensions; +pub mod h1; +pub mod h2; pub mod header; mod helpers; mod http_message; mod message; mod payload; -mod request; -mod response; -mod response_builder; +mod requests; +mod responses; mod service; - -pub mod error; -pub mod h1; -pub mod h2; pub mod test; pub mod ws; @@ -57,16 +57,13 @@ pub use self::extensions::Extensions; pub use self::header::ContentEncoding; pub use self::http_message::HttpMessage; pub use self::message::ConnectionType; -pub use self::message::{Message, RequestHead, RequestHeadType, ResponseHead}; -pub use self::payload::{Payload, PayloadStream}; -pub use self::request::Request; -pub use self::response::Response; -pub use self::response_builder::ResponseBuilder; +pub use self::message::Message; +#[allow(deprecated)] +pub use self::payload::{BoxedPayloadStream, Payload, PayloadStream}; +pub use self::requests::{Request, RequestHead, RequestHeadType}; +pub use self::responses::{Response, ResponseBuilder, ResponseHead}; pub use self::service::HttpService; -pub use ::http::{uri, uri::Uri}; -pub use ::http::{Method, StatusCode, Version}; - /// A major HTTP protocol version. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[non_exhaustive] @@ -87,24 +84,13 @@ pub(crate) struct OnConnectData(Option); impl OnConnectData { /// Construct by calling the on-connect callback with the underlying transport I/O. - pub(crate) fn from_io( - io: &T, - on_connect_ext: Option<&ConnectCallback>, - ) -> Self { + pub(crate) fn from_io(io: &T, on_connect_ext: Option<&ConnectCallback>) -> Self { let ext = on_connect_ext.map(|handler| { - let mut extensions = Extensions::new(); + let mut extensions = Extensions::default(); handler(io, &mut extensions); extensions }); Self(ext) } - - /// Merge self into given request's extensions. - #[inline] - pub(crate) fn merge_into(&mut self, req: &mut Request) { - if let Some(ref mut ext) = self.0 { - req.head.extensions.get_mut().drain_from(ext); - } - } } diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index c8e1ce6db..ecd08fbb3 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -1,16 +1,7 @@ -use std::{ - cell::{Ref, RefCell, RefMut}, - net, - rc::Rc, -}; +use std::{cell::RefCell, ops, rc::Rc}; use bitflags::bitflags; -use crate::{ - header::{self, HeaderMap}, - Extensions, Method, StatusCode, Uri, Version, -}; - /// Represents various types of connection #[derive(Copy, Clone, PartialEq, Debug)] pub enum ConnectionType { @@ -44,309 +35,6 @@ pub trait Head: Default + 'static { F: FnOnce(&MessagePool) -> R; } -#[derive(Debug)] -pub struct RequestHead { - pub method: Method, - pub uri: Uri, - pub version: Version, - pub headers: HeaderMap, - pub extensions: RefCell, - pub peer_addr: Option, - flags: Flags, -} - -impl Default for RequestHead { - fn default() -> RequestHead { - RequestHead { - method: Method::default(), - uri: Uri::default(), - version: Version::HTTP_11, - headers: HeaderMap::with_capacity(16), - extensions: RefCell::new(Extensions::new()), - peer_addr: None, - flags: Flags::empty(), - } - } -} - -impl Head for RequestHead { - fn clear(&mut self) { - self.flags = Flags::empty(); - self.headers.clear(); - self.extensions.get_mut().clear(); - } - - fn with_pool(f: F) -> R - where - F: FnOnce(&MessagePool) -> R, - { - REQUEST_POOL.with(|p| f(p)) - } -} - -impl RequestHead { - /// Message extensions - #[inline] - pub fn extensions(&self) -> Ref<'_, Extensions> { - self.extensions.borrow() - } - - /// Mutable reference to a the message's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut<'_, Extensions> { - self.extensions.borrow_mut() - } - - /// Read the message headers. - pub fn headers(&self) -> &HeaderMap { - &self.headers - } - - /// Mutable reference to the message headers. - pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.headers - } - - /// Is to uppercase headers with Camel-Case. - /// Default is `false` - #[inline] - pub fn camel_case_headers(&self) -> bool { - self.flags.contains(Flags::CAMEL_CASE) - } - - /// Set `true` to send headers which are formatted as Camel-Case. - #[inline] - pub fn set_camel_case_headers(&mut self, val: bool) { - if val { - self.flags.insert(Flags::CAMEL_CASE); - } else { - self.flags.remove(Flags::CAMEL_CASE); - } - } - - #[inline] - /// Set connection type of the message - pub fn set_connection_type(&mut self, ctype: ConnectionType) { - match ctype { - ConnectionType::Close => self.flags.insert(Flags::CLOSE), - ConnectionType::KeepAlive => self.flags.insert(Flags::KEEP_ALIVE), - ConnectionType::Upgrade => self.flags.insert(Flags::UPGRADE), - } - } - - #[inline] - /// Connection type - pub fn connection_type(&self) -> ConnectionType { - if self.flags.contains(Flags::CLOSE) { - ConnectionType::Close - } else if self.flags.contains(Flags::KEEP_ALIVE) { - ConnectionType::KeepAlive - } else if self.flags.contains(Flags::UPGRADE) { - ConnectionType::Upgrade - } else if self.version < Version::HTTP_11 { - ConnectionType::Close - } else { - ConnectionType::KeepAlive - } - } - - /// Connection upgrade status - pub fn upgrade(&self) -> bool { - self.headers() - .get(header::CONNECTION) - .map(|hdr| { - if let Ok(s) = hdr.to_str() { - s.to_ascii_lowercase().contains("upgrade") - } else { - false - } - }) - .unwrap_or(false) - } - - #[inline] - /// Get response body chunking state - pub fn chunked(&self) -> bool { - !self.flags.contains(Flags::NO_CHUNKING) - } - - #[inline] - pub fn no_chunking(&mut self, val: bool) { - if val { - self.flags.insert(Flags::NO_CHUNKING); - } else { - self.flags.remove(Flags::NO_CHUNKING); - } - } - - #[inline] - /// Request contains `EXPECT` header - pub fn expect(&self) -> bool { - self.flags.contains(Flags::EXPECT) - } - - #[inline] - pub(crate) fn set_expect(&mut self) { - self.flags.insert(Flags::EXPECT); - } -} - -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub enum RequestHeadType { - Owned(RequestHead), - Rc(Rc, Option), -} - -impl RequestHeadType { - pub fn extra_headers(&self) -> Option<&HeaderMap> { - match self { - RequestHeadType::Owned(_) => None, - RequestHeadType::Rc(_, headers) => headers.as_ref(), - } - } -} - -impl AsRef for RequestHeadType { - fn as_ref(&self) -> &RequestHead { - match self { - RequestHeadType::Owned(head) => head, - RequestHeadType::Rc(head, _) => head.as_ref(), - } - } -} - -impl From for RequestHeadType { - fn from(head: RequestHead) -> Self { - RequestHeadType::Owned(head) - } -} - -#[derive(Debug)] -pub struct ResponseHead { - pub version: Version, - pub status: StatusCode, - pub headers: HeaderMap, - pub reason: Option<&'static str>, - pub(crate) extensions: RefCell, - flags: Flags, -} - -impl ResponseHead { - /// Create new instance of `ResponseHead` type - #[inline] - pub fn new(status: StatusCode) -> ResponseHead { - ResponseHead { - status, - version: Version::default(), - headers: HeaderMap::with_capacity(12), - reason: None, - flags: Flags::empty(), - extensions: RefCell::new(Extensions::new()), - } - } - - /// Message extensions - #[inline] - pub fn extensions(&self) -> Ref<'_, Extensions> { - self.extensions.borrow() - } - - /// Mutable reference to a the message's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut<'_, Extensions> { - self.extensions.borrow_mut() - } - - #[inline] - /// Read the message headers. - pub fn headers(&self) -> &HeaderMap { - &self.headers - } - - #[inline] - /// Mutable reference to the message headers. - pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.headers - } - - #[inline] - /// Set connection type of the message - pub fn set_connection_type(&mut self, ctype: ConnectionType) { - match ctype { - ConnectionType::Close => self.flags.insert(Flags::CLOSE), - ConnectionType::KeepAlive => self.flags.insert(Flags::KEEP_ALIVE), - ConnectionType::Upgrade => self.flags.insert(Flags::UPGRADE), - } - } - - #[inline] - pub fn connection_type(&self) -> ConnectionType { - if self.flags.contains(Flags::CLOSE) { - ConnectionType::Close - } else if self.flags.contains(Flags::KEEP_ALIVE) { - ConnectionType::KeepAlive - } else if self.flags.contains(Flags::UPGRADE) { - ConnectionType::Upgrade - } else if self.version < Version::HTTP_11 { - ConnectionType::Close - } else { - ConnectionType::KeepAlive - } - } - - /// Check if keep-alive is enabled - #[inline] - pub fn keep_alive(&self) -> bool { - self.connection_type() == ConnectionType::KeepAlive - } - - /// Check upgrade status of this message - #[inline] - pub fn upgrade(&self) -> bool { - self.connection_type() == ConnectionType::Upgrade - } - - /// Get custom reason for the response - #[inline] - pub fn reason(&self) -> &str { - self.reason.unwrap_or_else(|| { - self.status - .canonical_reason() - .unwrap_or("") - }) - } - - #[inline] - pub(crate) fn conn_type(&self) -> Option { - if self.flags.contains(Flags::CLOSE) { - Some(ConnectionType::Close) - } else if self.flags.contains(Flags::KEEP_ALIVE) { - Some(ConnectionType::KeepAlive) - } else if self.flags.contains(Flags::UPGRADE) { - Some(ConnectionType::Upgrade) - } else { - None - } - } - - #[inline] - /// Get response body chunking state - pub fn chunked(&self) -> bool { - !self.flags.contains(Flags::NO_CHUNKING) - } - - #[inline] - /// Set no chunking for payload - pub fn no_chunking(&mut self, val: bool) { - if val { - self.flags.insert(Flags::NO_CHUNKING); - } else { - self.flags.remove(Flags::NO_CHUNKING); - } - } -} - pub struct Message { /// Rc here should not be cloned by anyone. /// It's used to reuse allocation of T and no shared ownership is allowed. @@ -355,12 +43,13 @@ pub struct Message { impl Message { /// Get new message from the pool of objects + #[allow(clippy::new_without_default)] pub fn new() -> Self { T::with_pool(MessagePool::get_message) } } -impl std::ops::Deref for Message { +impl ops::Deref for Message { type Target = T; fn deref(&self) -> &Self::Target { @@ -368,7 +57,7 @@ impl std::ops::Deref for Message { } } -impl std::ops::DerefMut for Message { +impl ops::DerefMut for Message { fn deref_mut(&mut self) -> &mut Self::Target { Rc::get_mut(&mut self.head).expect("Multiple copies exist") } @@ -380,53 +69,12 @@ impl Drop for Message { } } -pub(crate) struct BoxedResponseHead { - head: Option>, -} - -impl BoxedResponseHead { - /// Get new message from the pool of objects - pub fn new(status: StatusCode) -> Self { - RESPONSE_POOL.with(|p| p.get_message(status)) - } -} - -impl std::ops::Deref for BoxedResponseHead { - type Target = ResponseHead; - - fn deref(&self) -> &Self::Target { - self.head.as_ref().unwrap() - } -} - -impl std::ops::DerefMut for BoxedResponseHead { - fn deref_mut(&mut self) -> &mut Self::Target { - self.head.as_mut().unwrap() - } -} - -impl Drop for BoxedResponseHead { - fn drop(&mut self) { - if let Some(head) = self.head.take() { - RESPONSE_POOL.with(move |p| p.release(head)) - } - } -} - #[doc(hidden)] /// Request's objects pool pub struct MessagePool(RefCell>>); -#[doc(hidden)] -#[allow(clippy::vec_box)] -/// Request's objects pool -pub struct BoxedResponsePool(RefCell>>); - -thread_local!(static REQUEST_POOL: MessagePool = MessagePool::::create()); -thread_local!(static RESPONSE_POOL: BoxedResponsePool = BoxedResponsePool::create()); - impl MessagePool { - fn create() -> MessagePool { + pub(crate) fn create() -> MessagePool { MessagePool(RefCell::new(Vec::with_capacity(128))) } @@ -448,43 +96,11 @@ impl MessagePool { } #[inline] - /// Release request instance + /// Release message instance fn release(&self, msg: Rc) { - let v = &mut self.0.borrow_mut(); - if v.len() < 128 { - v.push(msg); - } - } -} - -impl BoxedResponsePool { - fn create() -> BoxedResponsePool { - BoxedResponsePool(RefCell::new(Vec::with_capacity(128))) - } - - /// Get message from the pool - #[inline] - fn get_message(&self, status: StatusCode) -> BoxedResponseHead { - if let Some(mut head) = self.0.borrow_mut().pop() { - head.reason = None; - head.status = status; - head.headers.clear(); - head.flags = Flags::empty(); - BoxedResponseHead { head: Some(head) } - } else { - BoxedResponseHead { - head: Some(Box::new(ResponseHead::new(status))), - } - } - } - - #[inline] - /// Release request instance - fn release(&self, mut msg: Box) { - let v = &mut self.0.borrow_mut(); - if v.len() < 128 { - msg.extensions.get_mut().clear(); - v.push(msg); + let pool = &mut self.0.borrow_mut(); + if pool.len() < 128 { + pool.push(msg); } } } diff --git a/actix-http/src/payload.rs b/actix-http/src/payload.rs index 54de6ed93..c9f338c7d 100644 --- a/actix-http/src/payload.rs +++ b/actix-http/src/payload.rs @@ -1,70 +1,89 @@ -use std::pin::Pin; -use std::task::{Context, Poll}; +use std::{ + mem, + pin::Pin, + task::{Context, Poll}, +}; use bytes::Bytes; use futures_core::Stream; -use h2::RecvStream; use crate::error::PayloadError; -/// Type represent boxed payload -pub type PayloadStream = Pin>>>; +/// A boxed payload stream. +pub type BoxedPayloadStream = Pin>>>; -/// Type represent streaming payload -pub enum Payload { - None, - H1(crate::h1::Payload), - H2(crate::h2::Payload), - Stream(S), +#[deprecated(since = "4.0.0", note = "Renamed to `BoxedPayloadStream`.")] +pub type PayloadStream = BoxedPayloadStream; + +pin_project_lite::pin_project! { + /// A streaming payload. + #[project = PayloadProj] + pub enum Payload { + None, + H1 { payload: crate::h1::Payload }, + H2 { payload: crate::h2::Payload }, + Stream { #[pin] payload: S }, + } } impl From for Payload { - fn from(v: crate::h1::Payload) -> Self { - Payload::H1(v) + fn from(payload: crate::h1::Payload) -> Self { + Payload::H1 { payload } } } impl From for Payload { - fn from(v: crate::h2::Payload) -> Self { - Payload::H2(v) + fn from(payload: crate::h2::Payload) -> Self { + Payload::H2 { payload } } } -impl From for Payload { - fn from(v: RecvStream) -> Self { - Payload::H2(crate::h2::Payload::new(v)) +impl From for Payload { + fn from(stream: h2::RecvStream) -> Self { + Payload::H2 { + payload: crate::h2::Payload::new(stream), + } } } -impl From for Payload { - fn from(pl: PayloadStream) -> Self { - Payload::Stream(pl) +impl From for Payload { + fn from(payload: BoxedPayloadStream) -> Self { + Payload::Stream { payload } } } impl Payload { /// Takes current payload and replaces it with `None` value pub fn take(&mut self) -> Payload { - std::mem::replace(self, Payload::None) + mem::replace(self, Payload::None) } } impl Stream for Payload where - S: Stream> + Unpin, + S: Stream>, { type Item = Result; #[inline] - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - match self.get_mut() { - Payload::None => Poll::Ready(None), - Payload::H1(ref mut pl) => pl.readany(cx), - Payload::H2(ref mut pl) => Pin::new(pl).poll_next(cx), - Payload::Stream(ref mut pl) => Pin::new(pl).poll_next(cx), + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.project() { + PayloadProj::None => Poll::Ready(None), + PayloadProj::H1 { payload } => Pin::new(payload).poll_next(cx), + PayloadProj::H2 { payload } => Pin::new(payload).poll_next(cx), + PayloadProj::Stream { payload } => payload.poll_next(cx), } } } + +#[cfg(test)] +mod tests { + use std::panic::{RefUnwindSafe, UnwindSafe}; + + use static_assertions::{assert_impl_all, assert_not_impl_any}; + + use super::*; + + assert_impl_all!(Payload: Unpin); + assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe); +} diff --git a/actix-http/src/requests/head.rs b/actix-http/src/requests/head.rs new file mode 100644 index 000000000..524075b61 --- /dev/null +++ b/actix-http/src/requests/head.rs @@ -0,0 +1,174 @@ +use std::{net, rc::Rc}; + +use crate::{ + header::{self, HeaderMap}, + message::{Flags, Head, MessagePool}, + ConnectionType, Method, Uri, Version, +}; + +thread_local! { + static REQUEST_POOL: MessagePool = MessagePool::::create() +} + +#[derive(Debug, Clone)] +pub struct RequestHead { + pub method: Method, + pub uri: Uri, + pub version: Version, + pub headers: HeaderMap, + pub peer_addr: Option, + flags: Flags, +} + +impl Default for RequestHead { + fn default() -> RequestHead { + RequestHead { + method: Method::default(), + uri: Uri::default(), + version: Version::HTTP_11, + headers: HeaderMap::with_capacity(16), + peer_addr: None, + flags: Flags::empty(), + } + } +} + +impl Head for RequestHead { + fn clear(&mut self) { + self.flags = Flags::empty(); + self.headers.clear(); + } + + fn with_pool(f: F) -> R + where + F: FnOnce(&MessagePool) -> R, + { + REQUEST_POOL.with(|p| f(p)) + } +} + +impl RequestHead { + /// Read the message headers. + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + /// Mutable reference to the message headers. + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.headers + } + + /// Is to uppercase headers with Camel-Case. + /// Default is `false` + #[inline] + pub fn camel_case_headers(&self) -> bool { + self.flags.contains(Flags::CAMEL_CASE) + } + + /// Set `true` to send headers which are formatted as Camel-Case. + #[inline] + pub fn set_camel_case_headers(&mut self, val: bool) { + if val { + self.flags.insert(Flags::CAMEL_CASE); + } else { + self.flags.remove(Flags::CAMEL_CASE); + } + } + + #[inline] + /// Set connection type of the message + pub fn set_connection_type(&mut self, ctype: ConnectionType) { + match ctype { + ConnectionType::Close => self.flags.insert(Flags::CLOSE), + ConnectionType::KeepAlive => self.flags.insert(Flags::KEEP_ALIVE), + ConnectionType::Upgrade => self.flags.insert(Flags::UPGRADE), + } + } + + #[inline] + /// Connection type + pub fn connection_type(&self) -> ConnectionType { + if self.flags.contains(Flags::CLOSE) { + ConnectionType::Close + } else if self.flags.contains(Flags::KEEP_ALIVE) { + ConnectionType::KeepAlive + } else if self.flags.contains(Flags::UPGRADE) { + ConnectionType::Upgrade + } else if self.version < Version::HTTP_11 { + ConnectionType::Close + } else { + ConnectionType::KeepAlive + } + } + + /// Connection upgrade status + pub fn upgrade(&self) -> bool { + self.headers() + .get(header::CONNECTION) + .map(|hdr| { + if let Ok(s) = hdr.to_str() { + s.to_ascii_lowercase().contains("upgrade") + } else { + false + } + }) + .unwrap_or(false) + } + + #[inline] + /// Get response body chunking state + pub fn chunked(&self) -> bool { + !self.flags.contains(Flags::NO_CHUNKING) + } + + #[inline] + pub fn no_chunking(&mut self, val: bool) { + if val { + self.flags.insert(Flags::NO_CHUNKING); + } else { + self.flags.remove(Flags::NO_CHUNKING); + } + } + + #[inline] + /// Request contains `EXPECT` header + pub fn expect(&self) -> bool { + self.flags.contains(Flags::EXPECT) + } + + #[inline] + pub(crate) fn set_expect(&mut self) { + self.flags.insert(Flags::EXPECT); + } +} + +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub enum RequestHeadType { + Owned(RequestHead), + Rc(Rc, Option), +} + +impl RequestHeadType { + pub fn extra_headers(&self) -> Option<&HeaderMap> { + match self { + RequestHeadType::Owned(_) => None, + RequestHeadType::Rc(_, headers) => headers.as_ref(), + } + } +} + +impl AsRef for RequestHeadType { + fn as_ref(&self) -> &RequestHead { + match self { + RequestHeadType::Owned(head) => head, + RequestHeadType::Rc(head, _) => head.as_ref(), + } + } +} + +impl From for RequestHeadType { + fn from(head: RequestHead) -> Self { + RequestHeadType::Owned(head) + } +} diff --git a/actix-http/src/requests/mod.rs b/actix-http/src/requests/mod.rs new file mode 100644 index 000000000..fc35da65a --- /dev/null +++ b/actix-http/src/requests/mod.rs @@ -0,0 +1,7 @@ +//! HTTP requests. + +mod head; +mod request; + +pub use self::head::{RequestHead, RequestHeadType}; +pub use self::request::Request; diff --git a/actix-http/src/request.rs b/actix-http/src/requests/request.rs similarity index 70% rename from actix-http/src/request.rs rename to actix-http/src/requests/request.rs index 401e9745c..4eaaba8e1 100644 --- a/actix-http/src/request.rs +++ b/actix-http/src/requests/request.rs @@ -1,24 +1,25 @@ //! HTTP requests. use std::{ - cell::{Ref, RefMut}, - fmt, net, str, + cell::{Ref, RefCell, RefMut}, + fmt, mem, net, + rc::Rc, + str, }; use http::{header, Method, Uri, Version}; use crate::{ - extensions::Extensions, - header::HeaderMap, - message::{Message, RequestHead}, - payload::{Payload, PayloadStream}, - HttpMessage, + header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, Message, Payload, + RequestHead, }; /// An HTTP request. -pub struct Request

{ +pub struct Request

{ pub(crate) payload: Payload

, pub(crate) head: Message, + pub(crate) conn_data: Option>, + pub(crate) req_data: RefCell, } impl

HttpMessage for Request

{ @@ -30,37 +31,42 @@ impl

HttpMessage for Request

{ } fn take_payload(&mut self) -> Payload

{ - std::mem::replace(&mut self.payload, Payload::None) + mem::replace(&mut self.payload, Payload::None) } /// Request extensions #[inline] fn extensions(&self) -> Ref<'_, Extensions> { - self.head.extensions() + self.req_data.borrow() } /// Mutable reference to a the request's extensions #[inline] fn extensions_mut(&self) -> RefMut<'_, Extensions> { - self.head.extensions_mut() + self.req_data.borrow_mut() } } -impl From> for Request { +impl From> for Request { fn from(head: Message) -> Self { Request { head, payload: Payload::None, + req_data: RefCell::new(Extensions::default()), + conn_data: None, } } } -impl Request { +impl Request { /// Create new Request instance - pub fn new() -> Request { + #[allow(clippy::new_without_default)] + pub fn new() -> Request { Request { head: Message::new(), payload: Payload::None, + req_data: RefCell::new(Extensions::default()), + conn_data: None, } } } @@ -71,16 +77,21 @@ impl

Request

{ Request { payload, head: Message::new(), + req_data: RefCell::new(Extensions::default()), + conn_data: None, } } /// Create new Request instance pub fn replace_payload(self, payload: Payload) -> (Request, Payload

) { let pl = self.payload; + ( Request { payload, head: self.head, + req_data: self.req_data, + conn_data: self.conn_data, }, pl, ) @@ -93,7 +104,7 @@ impl

Request

{ /// Get request's payload pub fn take_payload(&mut self) -> Payload

{ - std::mem::replace(&mut self.payload, Payload::None) + mem::replace(&mut self.payload, Payload::None) } /// Split request into request head and payload @@ -116,7 +127,7 @@ impl

Request

{ /// Mutable reference to the message's headers. pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.head_mut().headers + &mut self.head.headers } /// Request's uri. @@ -128,7 +139,7 @@ impl

Request

{ /// Mutable reference to the request's uri. #[inline] pub fn uri_mut(&mut self) -> &mut Uri { - &mut self.head_mut().uri + &mut self.head.uri } /// Read the Request method. @@ -170,6 +181,31 @@ impl

Request

{ pub fn peer_addr(&self) -> Option { self.head().peer_addr } + + /// Returns a reference a piece of connection data set in an [on-connect] callback. + /// + /// ```ignore + /// let opt_t = req.conn_data::(); + /// ``` + /// + /// [on-connect]: crate::HttpServiceBuilder::on_connect_ext + pub fn conn_data(&self) -> Option<&T> { + self.conn_data + .as_deref() + .and_then(|container| container.get::()) + } + + /// Returns the connection data container if an [on-connect] callback was registered. + /// + /// [on-connect]: crate::HttpServiceBuilder::on_connect_ext + pub fn take_conn_data(&mut self) -> Option> { + self.conn_data.take() + } + + /// Returns the request data container, leaving an empty one in it's place. + pub fn take_req_data(&mut self) -> Extensions { + mem::take(self.req_data.get_mut()) + } } impl

fmt::Debug for Request

{ diff --git a/actix-http/src/response_builder.rs b/actix-http/src/responses/builder.rs similarity index 95% rename from actix-http/src/response_builder.rs rename to actix-http/src/responses/builder.rs index f11f89219..5854863de 100644 --- a/actix-http/src/response_builder.rs +++ b/actix-http/src/responses/builder.rs @@ -8,9 +8,9 @@ use std::{ use crate::{ body::{EitherBody, MessageBody}, error::{Error, HttpError}, - header::{self, IntoHeaderPair, IntoHeaderValue}, - message::{BoxedResponseHead, ConnectionType, ResponseHead}, - Extensions, Response, StatusCode, + header::{self, TryIntoHeaderPair, TryIntoHeaderValue}, + responses::{BoxedResponseHead, ResponseHead}, + ConnectionType, Extensions, Response, StatusCode, }; /// An HTTP response builder. @@ -47,7 +47,8 @@ impl ResponseBuilder { /// Create response builder /// /// # Examples - // /// use actix_http::{Response, ResponseBuilder, StatusCode};, / `` + /// ``` + /// use actix_http::{Response, ResponseBuilder, StatusCode}; /// let res: Response<_> = ResponseBuilder::default().finish(); /// assert_eq!(res.status(), StatusCode::OK); /// ``` @@ -62,7 +63,8 @@ impl ResponseBuilder { /// Set HTTP status code of this response. /// /// # Examples - // /// use actix_http::{ResponseBuilder, StatusCode};, / `` + /// ``` + /// use actix_http::{ResponseBuilder, StatusCode}; /// let res = ResponseBuilder::default().status(StatusCode::NOT_FOUND).finish(); /// assert_eq!(res.status(), StatusCode::NOT_FOUND); /// ``` @@ -88,12 +90,9 @@ impl ResponseBuilder { /// assert!(res.headers().contains_key("content-type")); /// assert!(res.headers().contains_key("x-test")); /// ``` - pub fn insert_header(&mut self, header: H) -> &mut Self - where - H: IntoHeaderPair, - { + pub fn insert_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self { if let Some(parts) = self.inner() { - match header.try_into_header_pair() { + match header.try_into_pair() { Ok((key, value)) => { parts.headers.insert(key, value); } @@ -119,12 +118,9 @@ impl ResponseBuilder { /// assert_eq!(res.headers().get_all("content-type").count(), 1); /// assert_eq!(res.headers().get_all("x-test").count(), 2); /// ``` - pub fn append_header(&mut self, header: H) -> &mut Self - where - H: IntoHeaderPair, - { + pub fn append_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self { if let Some(parts) = self.inner() { - match header.try_into_header_pair() { + match header.try_into_pair() { Ok((key, value)) => parts.headers.append(key, value), Err(e) => self.err = Some(e.into()), }; @@ -155,7 +151,7 @@ impl ResponseBuilder { #[inline] pub fn upgrade(&mut self, value: V) -> &mut Self where - V: IntoHeaderValue, + V: TryIntoHeaderValue, { if let Some(parts) = self.inner() { parts.set_connection_type(ConnectionType::Upgrade); @@ -193,7 +189,7 @@ impl ResponseBuilder { #[inline] pub fn content_type(&mut self, value: V) -> &mut Self where - V: IntoHeaderValue, + V: TryIntoHeaderValue, { if let Some(parts) = self.inner() { match value.try_into_value() { diff --git a/actix-http/src/responses/head.rs b/actix-http/src/responses/head.rs new file mode 100644 index 000000000..78d9536e5 --- /dev/null +++ b/actix-http/src/responses/head.rs @@ -0,0 +1,208 @@ +//! Response head type and caching pool. + +use std::{ + cell::{Ref, RefCell, RefMut}, + ops, +}; + +use crate::{ + header::HeaderMap, message::Flags, ConnectionType, Extensions, StatusCode, Version, +}; + +thread_local! { + static RESPONSE_POOL: BoxedResponsePool = BoxedResponsePool::create(); +} + +#[derive(Debug)] +pub struct ResponseHead { + pub version: Version, + pub status: StatusCode, + pub headers: HeaderMap, + pub reason: Option<&'static str>, + pub(crate) extensions: RefCell, + flags: Flags, +} + +impl ResponseHead { + /// Create new instance of `ResponseHead` type + #[inline] + pub fn new(status: StatusCode) -> ResponseHead { + ResponseHead { + status, + version: Version::HTTP_11, + headers: HeaderMap::with_capacity(12), + reason: None, + flags: Flags::empty(), + extensions: RefCell::new(Extensions::new()), + } + } + + #[inline] + /// Read the message headers. + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + #[inline] + /// Mutable reference to the message headers. + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.headers + } + + /// Message extensions + #[inline] + pub fn extensions(&self) -> Ref<'_, Extensions> { + self.extensions.borrow() + } + + /// Mutable reference to a the message's extensions + #[inline] + pub fn extensions_mut(&self) -> RefMut<'_, Extensions> { + self.extensions.borrow_mut() + } + + #[inline] + /// Set connection type of the message + pub fn set_connection_type(&mut self, ctype: ConnectionType) { + match ctype { + ConnectionType::Close => self.flags.insert(Flags::CLOSE), + ConnectionType::KeepAlive => self.flags.insert(Flags::KEEP_ALIVE), + ConnectionType::Upgrade => self.flags.insert(Flags::UPGRADE), + } + } + + #[inline] + pub fn connection_type(&self) -> ConnectionType { + if self.flags.contains(Flags::CLOSE) { + ConnectionType::Close + } else if self.flags.contains(Flags::KEEP_ALIVE) { + ConnectionType::KeepAlive + } else if self.flags.contains(Flags::UPGRADE) { + ConnectionType::Upgrade + } else if self.version < Version::HTTP_11 { + ConnectionType::Close + } else { + ConnectionType::KeepAlive + } + } + + /// Check if keep-alive is enabled + #[inline] + pub fn keep_alive(&self) -> bool { + self.connection_type() == ConnectionType::KeepAlive + } + + /// Check upgrade status of this message + #[inline] + pub fn upgrade(&self) -> bool { + self.connection_type() == ConnectionType::Upgrade + } + + /// Get custom reason for the response + #[inline] + pub fn reason(&self) -> &str { + self.reason.unwrap_or_else(|| { + self.status + .canonical_reason() + .unwrap_or("") + }) + } + + #[inline] + pub(crate) fn conn_type(&self) -> Option { + if self.flags.contains(Flags::CLOSE) { + Some(ConnectionType::Close) + } else if self.flags.contains(Flags::KEEP_ALIVE) { + Some(ConnectionType::KeepAlive) + } else if self.flags.contains(Flags::UPGRADE) { + Some(ConnectionType::Upgrade) + } else { + None + } + } + + #[inline] + /// Get response body chunking state + pub fn chunked(&self) -> bool { + !self.flags.contains(Flags::NO_CHUNKING) + } + + #[inline] + /// Set no chunking for payload + pub fn no_chunking(&mut self, val: bool) { + if val { + self.flags.insert(Flags::NO_CHUNKING); + } else { + self.flags.remove(Flags::NO_CHUNKING); + } + } +} + +pub(crate) struct BoxedResponseHead { + head: Option>, +} + +impl BoxedResponseHead { + /// Get new message from the pool of objects + pub fn new(status: StatusCode) -> Self { + RESPONSE_POOL.with(|p| p.get_message(status)) + } +} + +impl ops::Deref for BoxedResponseHead { + type Target = ResponseHead; + + fn deref(&self) -> &Self::Target { + self.head.as_ref().unwrap() + } +} + +impl ops::DerefMut for BoxedResponseHead { + fn deref_mut(&mut self) -> &mut Self::Target { + self.head.as_mut().unwrap() + } +} + +impl Drop for BoxedResponseHead { + fn drop(&mut self) { + if let Some(head) = self.head.take() { + RESPONSE_POOL.with(move |p| p.release(head)) + } + } +} + +/// Request's objects pool +#[doc(hidden)] +pub struct BoxedResponsePool(#[allow(clippy::vec_box)] RefCell>>); + +impl BoxedResponsePool { + fn create() -> BoxedResponsePool { + BoxedResponsePool(RefCell::new(Vec::with_capacity(128))) + } + + /// Get message from the pool + #[inline] + fn get_message(&self, status: StatusCode) -> BoxedResponseHead { + if let Some(mut head) = self.0.borrow_mut().pop() { + head.reason = None; + head.status = status; + head.headers.clear(); + head.flags = Flags::empty(); + BoxedResponseHead { head: Some(head) } + } else { + BoxedResponseHead { + head: Some(Box::new(ResponseHead::new(status))), + } + } + } + + /// Release request instance + #[inline] + fn release(&self, mut msg: Box) { + let pool = &mut self.0.borrow_mut(); + if pool.len() < 128 { + msg.extensions.get_mut().clear(); + pool.push(msg); + } + } +} diff --git a/actix-http/src/responses/mod.rs b/actix-http/src/responses/mod.rs new file mode 100644 index 000000000..899232b9f --- /dev/null +++ b/actix-http/src/responses/mod.rs @@ -0,0 +1,11 @@ +//! HTTP response. + +mod builder; +mod head; +#[allow(clippy::module_inception)] +mod response; + +pub use self::builder::ResponseBuilder; +pub(crate) use self::head::BoxedResponseHead; +pub use self::head::ResponseHead; +pub use self::response::Response; diff --git a/actix-http/src/response.rs b/actix-http/src/responses/response.rs similarity index 97% rename from actix-http/src/response.rs rename to actix-http/src/responses/response.rs index ee7e38913..ec9157afb 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/responses/response.rs @@ -10,10 +10,9 @@ use bytestring::ByteString; use crate::{ body::{BoxBody, MessageBody}, - extensions::Extensions, - header::{self, HeaderMap, IntoHeaderValue}, - message::{BoxedResponseHead, ResponseHead}, - Error, ResponseBuilder, StatusCode, + header::{self, HeaderMap, TryIntoHeaderValue}, + responses::BoxedResponseHead, + Error, Extensions, ResponseBuilder, ResponseHead, StatusCode, }; /// An HTTP response. @@ -170,7 +169,7 @@ impl Response { /// Returns split head and body. /// /// # Implementation Notes - /// Due to internal performance optimisations, the first element of the returned tuple is a + /// Due to internal performance optimizations, the first element of the returned tuple is a /// `Response` as well but only contains the head of the response this was called on. pub fn into_parts(self) -> (Response<()>, B) { self.replace_body(()) @@ -194,7 +193,7 @@ impl Response { where B: MessageBody + 'static, { - self.map_body(|_, body| BoxBody::new(body)) + self.map_body(|_, body| body.boxed()) } /// Returns body, consuming this response. @@ -231,9 +230,7 @@ impl Default for Response { } } -impl>, E: Into> From> - for Response -{ +impl>, E: Into> From> for Response { fn from(res: Result) -> Self { match res { Ok(val) => val.into(), diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index 7af34ba05..cd2efe678 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -161,11 +161,7 @@ where X::Error: Into>, X::InitError: fmt::Debug, - U: ServiceFactory< - (Request, Framed), - Config = (), - Response = (), - >, + U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U::Future: 'static, U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, @@ -381,9 +377,9 @@ where let upgrade = match upgrade { Some(upgrade) => { - let upgrade = upgrade.await.map_err(|e| { - log::error!("Init http upgrade service error: {:?}", e) - })?; + let upgrade = upgrade + .await + .map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?; Some(upgrade) } None => None, @@ -497,9 +493,9 @@ where type Future = HttpServiceHandlerResponse; fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { - self._poll_ready(cx).map_err(|e| { - log::error!("HTTP service readiness error: {:?}", e); - DispatchError::Service(e) + self._poll_ready(cx).map_err(|err| { + log::error!("HTTP service readiness error: {:?}", err); + DispatchError::Service(err) }) } @@ -507,8 +503,7 @@ where &self, (io, proto, peer_addr): (T, Protocol, Option), ) -> Self::Future { - let on_connect_data = - OnConnectData::from_io(&io, self.on_connect_ext.as_deref()); + let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref()); match proto { Protocol::Http2 => HttpServiceHandlerResponse { @@ -517,7 +512,7 @@ where h2::handshake_with_timeout(io, &self.cfg), self.cfg.clone(), self.flow.clone(), - on_connect_data, + conn_data, peer_addr, )), }, @@ -527,10 +522,10 @@ where state: State::H1 { dispatcher: h1::Dispatcher::new( io, - self.cfg.clone(), self.flow.clone(), - on_connect_data, + self.cfg.clone(), peer_addr, + conn_data, ), }, }, @@ -627,17 +622,11 @@ where StateProj::H2Handshake { handshake: data } => { match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) { Ok((conn, timer)) => { - let (_, config, flow, on_connect_data, peer_addr) = - data.take().unwrap(); + let (_, config, flow, conn_data, peer_addr) = data.take().unwrap(); self.as_mut().project().state.set(State::H2 { dispatcher: h2::Dispatcher::new( - flow, - conn, - on_connect_data, - config, - peer_addr, - timer, + conn, flow, config, peer_addr, conn_data, timer, ), }); self.poll(cx) diff --git a/actix-http/src/test.rs b/actix-http/src/test.rs index ec781743d..1f76498ef 100644 --- a/actix-http/src/test.rs +++ b/actix-http/src/test.rs @@ -14,7 +14,7 @@ use bytes::{Bytes, BytesMut}; use http::{Method, Uri, Version}; use crate::{ - header::{HeaderMap, IntoHeaderPair}, + header::{HeaderMap, TryIntoHeaderPair}, payload::Payload, Request, }; @@ -92,11 +92,8 @@ impl TestRequest { } /// Insert a header, replacing any that were set with an equivalent field name. - pub fn insert_header(&mut self, header: H) -> &mut Self - where - H: IntoHeaderPair, - { - match header.try_into_header_pair() { + pub fn insert_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self { + match header.try_into_pair() { Ok((key, value)) => { parts(&mut self.0).headers.insert(key, value); } @@ -109,11 +106,8 @@ impl TestRequest { } /// Append a header, keeping any that were set with an equivalent field name. - pub fn append_header(&mut self, header: H) -> &mut Self - where - H: IntoHeaderPair, - { - match header.try_into_header_pair() { + pub fn append_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self { + match header.try_into_pair() { Ok((key, value)) => { parts(&mut self.0).headers.append(key, value); } @@ -126,7 +120,7 @@ impl TestRequest { } /// Set request payload. - pub fn set_payload>(&mut self, data: B) -> &mut Self { + pub fn set_payload(&mut self, data: impl Into) -> &mut Self { let mut payload = crate::h1::Payload::empty(); payload.unread_data(data.into()); parts(&mut self.0).payload = Some(payload.into()); @@ -270,7 +264,7 @@ impl TestSeqBuffer { /// Create new empty `TestBuffer` instance. pub fn empty() -> Self { - Self::new("") + Self::new(BytesMut::new()) } pub fn read_buf(&self) -> Ref<'_, BytesMut> { diff --git a/actix-http/src/ws/codec.rs b/actix-http/src/ws/codec.rs index d80613e5f..f5b755eec 100644 --- a/actix-http/src/ws/codec.rs +++ b/actix-http/src/ws/codec.rs @@ -224,9 +224,7 @@ impl Decoder for Codec { OpCode::Continue => { if self.flags.contains(Flags::CONTINUATION) { Ok(Some(Frame::Continuation(Item::Continue( - payload - .map(|pl| pl.freeze()) - .unwrap_or_else(Bytes::new), + payload.map(|pl| pl.freeze()).unwrap_or_else(Bytes::new), )))) } else { Err(ProtocolError::ContinuationNotStarted) @@ -236,9 +234,7 @@ impl Decoder for Codec { if !self.flags.contains(Flags::CONTINUATION) { self.flags.insert(Flags::CONTINUATION); Ok(Some(Frame::Continuation(Item::FirstBinary( - payload - .map(|pl| pl.freeze()) - .unwrap_or_else(Bytes::new), + payload.map(|pl| pl.freeze()).unwrap_or_else(Bytes::new), )))) } else { Err(ProtocolError::ContinuationStarted) @@ -248,9 +244,7 @@ impl Decoder for Codec { if !self.flags.contains(Flags::CONTINUATION) { self.flags.insert(Flags::CONTINUATION); Ok(Some(Frame::Continuation(Item::FirstText( - payload - .map(|pl| pl.freeze()) - .unwrap_or_else(Bytes::new), + payload.map(|pl| pl.freeze()).unwrap_or_else(Bytes::new), )))) } else { Err(ProtocolError::ContinuationStarted) diff --git a/actix-http/src/ws/dispatcher.rs b/actix-http/src/ws/dispatcher.rs index a3f766e9c..f12ae1b1a 100644 --- a/actix-http/src/ws/dispatcher.rs +++ b/actix-http/src/ws/dispatcher.rs @@ -304,8 +304,7 @@ mod inner { let item = match this.framed.next_item(cx) { Poll::Ready(Some(Ok(el))) => el, Poll::Ready(Some(Err(err))) => { - *this.state = - State::FramedError(DispatcherError::Decoder(err)); + *this.state = State::FramedError(DispatcherError::Decoder(err)); return true; } Poll::Pending => return false, @@ -348,8 +347,7 @@ mod inner { match Pin::new(&mut this.rx).poll_next(cx) { Poll::Ready(Some(Ok(Message::Item(msg)))) => { if let Err(err) = this.framed.as_mut().write(msg) { - *this.state = - State::FramedError(DispatcherError::Encoder(err)); + *this.state = State::FramedError(DispatcherError::Encoder(err)); return true; } } @@ -371,8 +369,7 @@ mod inner { Poll::Ready(Ok(_)) => {} Poll::Ready(Err(err)) => { debug!("Error sending data: {:?}", err); - *this.state = - State::FramedError(DispatcherError::Encoder(err)); + *this.state = State::FramedError(DispatcherError::Encoder(err)); return true; } } @@ -432,9 +429,7 @@ mod inner { Poll::Ready(Ok(())) } } - State::FramedError(_) => { - Poll::Ready(Err(this.state.take_framed_error())) - } + State::FramedError(_) => Poll::Ready(Err(this.state.take_framed_error())), State::Stopping => Poll::Ready(Ok(())), }; } diff --git a/actix-http/src/ws/frame.rs b/actix-http/src/ws/frame.rs index 46edf5d85..b58ef7362 100644 --- a/actix-http/src/ws/frame.rs +++ b/actix-http/src/ws/frame.rs @@ -16,8 +16,7 @@ impl Parser { src: &[u8], server: bool, max_size: usize, - ) -> Result)>, ProtocolError> - { + ) -> Result)>, ProtocolError> { let chunk_len = src.len(); let mut idx = 2; @@ -228,15 +227,11 @@ mod tests { payload: Bytes, } - fn is_none( - frm: &Result)>, ProtocolError>, - ) -> bool { + fn is_none(frm: &Result)>, ProtocolError>) -> bool { matches!(*frm, Ok(None)) } - fn extract( - frm: Result)>, ProtocolError>, - ) -> F { + fn extract(frm: Result)>, ProtocolError>) -> F { match frm { Ok(Some((finished, opcode, payload))) => F { finished, diff --git a/actix-http/src/ws/mask.rs b/actix-http/src/ws/mask.rs index 11a6ddc32..20b4372a0 100644 --- a/actix-http/src/ws/mask.rs +++ b/actix-http/src/ws/mask.rs @@ -54,8 +54,8 @@ mod tests { 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, + 0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, 0x74, 0xf9, + 0x12, 0x03, ]; // Check masking with proper alignment. @@ -85,8 +85,8 @@ mod tests { fn test_apply_mask() { let mask = [0x6d, 0xb6, 0xb2, 0x80]; let unmasked = vec![ - 0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, - 0x74, 0xf9, 0x12, 0x03, + 0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, 0x74, 0xf9, + 0x12, 0x03, ]; for data_len in 0..=unmasked.len() { diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index cb1aa6730..568d801a2 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -9,9 +9,7 @@ use derive_more::{Display, Error, From}; use http::{header, Method, StatusCode}; use crate::body::BoxBody; -use crate::{ - header::HeaderValue, message::RequestHead, response::Response, ResponseBuilder, -}; +use crate::{header::HeaderValue, RequestHead, Response, ResponseBuilder}; mod codec; mod dispatcher; @@ -101,8 +99,9 @@ impl From for Response { match err { HandshakeError::GetMethodRequired => { let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED); - res.headers_mut() - .insert(header::ALLOW, HeaderValue::from_static("GET")); + #[allow(clippy::declare_interior_mutable_const)] + const HV_GET: HeaderValue = HeaderValue::from_static("GET"); + res.headers_mut().insert(header::ALLOW, HV_GET); res } diff --git a/actix-http/tests/test_client.rs b/actix-http/tests/test_client.rs index acbdc8e83..a3adcdfd6 100644 --- a/actix-http/tests/test_client.rs +++ b/actix-http/tests/test_client.rs @@ -1,8 +1,6 @@ use std::convert::Infallible; -use actix_http::{ - body::BoxBody, HttpMessage, HttpService, Request, Response, StatusCode, -}; +use actix_http::{body::BoxBody, HttpMessage, HttpService, Request, Response, StatusCode}; use actix_http_test::test_server; use actix_service::ServiceFactoryExt; use actix_utils::future; diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index 6f68cc04d..1e371473f 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -8,7 +8,7 @@ use actix_http::{ body::{BodyStream, BoxBody, SizedStream}, error::PayloadError, header::{self, HeaderValue}, - Error, HttpMessage, HttpService, Method, Request, Response, StatusCode, Version, + Error, HttpService, Method, Request, Response, StatusCode, Version, }; use actix_http_test::test_server; use actix_service::{fn_service, ServiceFactoryExt}; @@ -101,7 +101,7 @@ async fn test_h2_1() -> io::Result<()> { #[actix_rt::test] async fn test_h2_body() -> io::Result<()> { - let data = "HELLOWORLD".to_owned().repeat(64 * 1024); + let data = "HELLOWORLD".to_owned().repeat(64 * 1024); // 640 KiB let mut srv = test_server(move || { HttpService::build() .h2(|mut req: Request<_>| async move { @@ -170,10 +170,11 @@ async fn test_h2_headers() { let mut srv = test_server(move || { let data = data.clone(); - HttpService::build().h2(move |_| { - let mut builder = Response::build(StatusCode::OK); - for idx in 0..90 { - builder.insert_header( + HttpService::build() + .h2(move |_| { + let mut builder = Response::build(StatusCode::OK); + for idx in 0..90 { + builder.insert_header( (format!("X-TEST-{}", idx).as_str(), "TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ @@ -189,12 +190,13 @@ async fn test_h2_headers() { TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", )); - } - ok::<_, Infallible>(builder.body(data.clone())) - }) + } + ok::<_, Infallible>(builder.body(data.clone())) + }) .openssl(tls_config()) - .map_err(|_| ()) - }).await; + .map_err(|_| ()) + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -315,9 +317,8 @@ async fn test_h2_body_length() { let mut srv = test_server(move || { HttpService::build() .h2(|_| async { - let body = once(async { - Ok::<_, Infallible>(Bytes::from_static(STR.as_ref())) - }); + let body = + once(async { Ok::<_, Infallible>(Bytes::from_static(STR.as_ref())) }); Ok::<_, Infallible>( Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), @@ -430,7 +431,7 @@ async fn test_h2_on_connect() { data.insert(20isize); }) .h2(|req: Request| { - assert!(req.extensions().contains::()); + assert!(req.conn_data::().is_some()); ok::<_, Infallible>(Response::ok()) }) .openssl(tls_config()) diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 1fc3bdf49..51fefae72 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -7,6 +7,7 @@ use std::{ io::{self, BufReader, Write}, net::{SocketAddr, TcpStream as StdTcpStream}, sync::Arc, + task::Poll, }; use actix_http::{ @@ -16,25 +17,37 @@ use actix_http::{ Error, HttpService, Method, Request, Response, StatusCode, Version, }; use actix_http_test::test_server; +use actix_rt::pin; use actix_service::{fn_factory_with_config, fn_service}; use actix_tls::connect::rustls::webpki_roots_cert_store; -use actix_utils::future::{err, ok}; +use actix_utils::future::{err, ok, poll_fn}; use bytes::{Bytes, BytesMut}; use derive_more::{Display, Error}; -use futures_core::Stream; -use futures_util::stream::{once, StreamExt as _}; +use futures_core::{ready, Stream}; +use futures_util::stream::once; use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig, ServerName}; use rustls_pemfile::{certs, pkcs8_private_keys}; -async fn load_body(mut stream: S) -> Result +async fn load_body(stream: S) -> Result where - S: Stream> + Unpin, + S: Stream>, { - let mut body = BytesMut::new(); - while let Some(item) = stream.next().await { - body.extend_from_slice(&item?) - } - Ok(body) + let mut buf = BytesMut::new(); + + pin!(stream); + + poll_fn(|cx| loop { + let body = stream.as_mut(); + + match ready!(body.poll_next(cx)) { + Some(Ok(bytes)) => buf.extend_from_slice(&*bytes), + None => return Poll::Ready(Ok(())), + Some(Err(err)) => return Poll::Ready(Err(err)), + } + }) + .await?; + + Ok(buf) } fn tls_config() -> RustlsServerConfig { @@ -238,10 +251,11 @@ async fn test_h2_headers() { let mut srv = test_server(move || { let data = data.clone(); - HttpService::build().h2(move |_| { - let mut config = Response::build(StatusCode::OK); - for idx in 0..90 { - config.insert_header(( + HttpService::build() + .h2(move |_| { + let mut config = Response::build(StatusCode::OK); + for idx in 0..90 { + config.insert_header(( format!("X-TEST-{}", idx).as_str(), "TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ @@ -257,11 +271,12 @@ async fn test_h2_headers() { TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", )); - } - ok::<_, Infallible>(config.body(data.clone())) - }) + } + ok::<_, Infallible>(config.body(data.clone())) + }) .rustls(tls_config()) - }).await; + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index e6733b29b..1bb574fd6 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -7,7 +7,7 @@ use std::{ use actix_http::{ body::{self, BodyStream, BoxBody, SizedStream}, - header, Error, HttpMessage, HttpService, KeepAlive, Request, Response, StatusCode, + header, Error, HttpService, KeepAlive, Request, Response, StatusCode, }; use actix_http_test::test_server; use actix_rt::time::sleep; @@ -154,9 +154,7 @@ async fn test_chunked_payload() { }) .fold(0usize, |acc, chunk| ready(acc + chunk.len())) .map(|req_size| { - Ok::<_, Error>( - Response::ok().set_body(format!("size={}", req_size)), - ) + Ok::<_, Error>(Response::ok().set_body(format!("size={}", req_size))) }) })) .tcp() @@ -165,8 +163,7 @@ async fn test_chunked_payload() { let returned_size = { let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); - let _ = stream - .write_all(b"POST /test HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"); + let _ = stream.write_all(b"POST /test HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"); for chunk_size in chunk_sizes.iter() { let mut bytes = Vec::new(); @@ -293,8 +290,7 @@ async fn test_http1_keepalive_close() { .await; let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); - let _ = - stream.write_all(b"GET /test/tests/test HTTP/1.1\r\nconnection: close\r\n\r\n"); + let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\nconnection: close\r\n\r\n"); let mut data = vec![0; 1024]; let _ = stream.read(&mut data); assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); @@ -338,8 +334,8 @@ async fn test_http10_keepalive() { .await; let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); - let _ = stream - .write_all(b"GET /test/tests/test HTTP/1.0\r\nconnection: keep-alive\r\n\r\n"); + let _ = + stream.write_all(b"GET /test/tests/test HTTP/1.0\r\nconnection: keep-alive\r\n\r\n"); let mut data = vec![0; 1024]; let _ = stream.read(&mut data); assert_eq!(&data[..17], b"HTTP/1.0 200 OK\r\n"); @@ -436,10 +432,11 @@ async fn test_h1_headers() { let mut srv = test_server(move || { let data = data.clone(); - HttpService::build().h1(move |_| { - let mut builder = Response::build(StatusCode::OK); - for idx in 0..90 { - builder.insert_header(( + HttpService::build() + .h1(move |_| { + let mut builder = Response::build(StatusCode::OK); + for idx in 0..90 { + builder.insert_header(( format!("X-TEST-{}", idx).as_str(), "TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ @@ -455,10 +452,12 @@ async fn test_h1_headers() { TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", )); - } - ok::<_, Infallible>(builder.body(data.clone())) - }).tcp() - }).await; + } + ok::<_, Infallible>(builder.body(data.clone())) + }) + .tcp() + }) + .await; let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -655,9 +654,7 @@ async fn test_h1_body_chunked_implicit() { HttpService::build() .h1(|_| { let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); - ok::<_, Infallible>( - Response::build(StatusCode::OK).body(BodyStream::new(body)), - ) + ok::<_, Infallible>(Response::build(StatusCode::OK).body(BodyStream::new(body))) }) .tcp() }) @@ -748,7 +745,7 @@ async fn test_h1_on_connect() { data.insert(20isize); }) .h1(|req: Request| { - assert!(req.extensions().contains::()); + assert!(req.conn_data::().is_some()); ok::<_, Infallible>(Response::ok()) }) .tcp() @@ -776,10 +773,8 @@ async fn test_not_modified_spec_h1() { .h1(|req: Request| { let res: Response = match req.path() { // with no content-length - "/none" => { - Response::with_body(StatusCode::NOT_MODIFIED, body::None::new()) - .map_into_boxed_body() - } + "/none" => Response::with_body(StatusCode::NOT_MODIFIED, body::None::new()) + .map_into_boxed_body(), // with no content-length "/body" => Response::with_body(StatusCode::NOT_MODIFIED, "1234") @@ -787,10 +782,8 @@ async fn test_not_modified_spec_h1() { // with manual content-length header and specific None body "/cl-none" => { - let mut res = Response::with_body( - StatusCode::NOT_MODIFIED, - body::None::new(), - ); + let mut res = + Response::with_body(StatusCode::NOT_MODIFIED, body::None::new()); res.headers_mut() .insert(CL.clone(), header::HeaderValue::from_static("24")); res.map_into_boxed_body() @@ -798,8 +791,7 @@ async fn test_not_modified_spec_h1() { // with manual content-length header and ignore-able body "/cl-body" => { - let mut res = - Response::with_body(StatusCode::NOT_MODIFIED, "1234"); + let mut res = Response::with_body(StatusCode::NOT_MODIFIED, "1234"); res.headers_mut() .insert(CL.clone(), header::HeaderValue::from_static("4")); res.map_into_boxed_body() diff --git a/actix-http/tests/test_ws.rs b/actix-http/tests/test_ws.rs index c91382013..ed8c61fd6 100644 --- a/actix-http/tests/test_ws.rs +++ b/actix-http/tests/test_ws.rs @@ -56,8 +56,9 @@ impl From for Response { WsServiceError::Http(err) => err.into(), WsServiceError::Ws(err) => err.into(), WsServiceError::Io(_err) => unreachable!(), - WsServiceError::Dispatcher => Response::internal_server_error() - .set_body(BoxBody::new(format!("{}", err))), + WsServiceError::Dispatcher => { + Response::internal_server_error().set_body(BoxBody::new(format!("{}", err))) + } } } } @@ -97,9 +98,7 @@ where async fn service(msg: Frame) -> Result { let msg = match msg { Frame::Ping(msg) => Message::Pong(msg), - Frame::Text(text) => { - Message::Text(String::from_utf8_lossy(&text).into_owned().into()) - } + Frame::Text(text) => Message::Text(String::from_utf8_lossy(&text).into_owned().into()), Frame::Binary(bin) => Message::Binary(bin), Frame::Continuation(item) => Message::Continuation(item), Frame::Close(reason) => Message::Close(reason), diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index d9ded57a4..a9a1e8784 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -3,116 +3,124 @@ ## Unreleased - 2021-xx-xx +## 0.4.0-beta.11 - 2021-12-27 +* No significant changes since `0.4.0-beta.10`. + + +## 0.4.0-beta.10 - 2021-12-11 +- No significant changes since `0.4.0-beta.9`. + + ## 0.4.0-beta.9 - 2021-12-01 -* Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463] +- Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463] [#2463]: https://github.com/actix/actix-web/pull/2463 ## 0.4.0-beta.8 - 2021-11-22 -* Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451] -* Added `MultipartError::NoContentDisposition` variant. [#2451] -* Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451] -* Added `Field::name` method for getting the field name. [#2451] -* `MultipartError` now marks variants with inner errors as the source. [#2451] -* `MultipartError` is now marked as non-exhaustive. [#2451] +- Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451] +- Added `MultipartError::NoContentDisposition` variant. [#2451] +- Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451] +- Added `Field::name` method for getting the field name. [#2451] +- `MultipartError` now marks variants with inner errors as the source. [#2451] +- `MultipartError` is now marked as non-exhaustive. [#2451] [#2451]: https://github.com/actix/actix-web/pull/2451 ## 0.4.0-beta.7 - 2021-10-20 -* Minimum supported Rust version (MSRV) is now 1.52. +- Minimum supported Rust version (MSRV) is now 1.52. ## 0.4.0-beta.6 - 2021-09-09 -* Minimum supported Rust version (MSRV) is now 1.51. +- Minimum supported Rust version (MSRV) is now 1.51. ## 0.4.0-beta.5 - 2021-06-17 -* No notable changes. +- No notable changes. ## 0.4.0-beta.4 - 2021-04-02 -* No notable changes. +- No notable changes. ## 0.4.0-beta.3 - 2021-03-09 -* No notable changes. +- No notable changes. ## 0.4.0-beta.2 - 2021-02-10 -* No notable changes. +- No notable changes. ## 0.4.0-beta.1 - 2021-01-07 -* Fix multipart consuming payload before header checks. [#1513] -* Update `bytes` to `1.0`. [#1813] +- Fix multipart consuming payload before header checks. [#1513] +- Update `bytes` to `1.0`. [#1813] [#1813]: https://github.com/actix/actix-web/pull/1813 [#1513]: https://github.com/actix/actix-web/pull/1513 ## 0.3.0 - 2020-09-11 -* No significant changes from `0.3.0-beta.2`. +- No significant changes from `0.3.0-beta.2`. ## 0.3.0-beta.2 - 2020-09-10 -* Update `actix-*` dependencies to latest versions. +- Update `actix-*` dependencies to latest versions. ## 0.3.0-beta.1 - 2020-07-15 -* Update `actix-web` to 3.0.0-beta.1 +- Update `actix-web` to 3.0.0-beta.1 ## 0.3.0-alpha.1 - 2020-05-25 -* Update `actix-web` to 3.0.0-alpha.3 -* Bump minimum supported Rust version to 1.40 -* Minimize `futures` dependencies -* Remove the unused `time` dependency -* Fix missing `std::error::Error` implement for `MultipartError`. +- Update `actix-web` to 3.0.0-alpha.3 +- Bump minimum supported Rust version to 1.40 +- Minimize `futures` dependencies +- Remove the unused `time` dependency +- Fix missing `std::error::Error` implement for `MultipartError`. ## [0.2.0] - 2019-12-20 -* Release +- Release ## [0.2.0-alpha.4] - 2019-12-xx -* Multipart handling now handles Pending during read of boundary #1205 +- Multipart handling now handles Pending during read of boundary #1205 ## [0.2.0-alpha.2] - 2019-12-03 -* Migrate to `std::future` +- Migrate to `std::future` ## [0.1.4] - 2019-09-12 -* Multipart handling now parses requests which do not end in CRLF #1038 +- Multipart handling now parses requests which do not end in CRLF #1038 ## [0.1.3] - 2019-08-18 -* Fix ring dependency from actix-web default features for #741. +- Fix ring dependency from actix-web default features for #741. ## [0.1.2] - 2019-06-02 -* Fix boundary parsing #876 +- Fix boundary parsing #876 ## [0.1.1] - 2019-05-25 -* Fix disconnect handling #834 +- Fix disconnect handling #834 ## [0.1.0] - 2019-05-18 -* Release +- Release ## [0.1.0-beta.4] - 2019-05-12 -* Handle cancellation of uploads #736 +- Handle cancellation of uploads #736 -* Upgrade to actix-web 1.0.0-beta.4 +- Upgrade to actix-web 1.0.0-beta.4 ## [0.1.0-beta.1] - 2019-04-21 -* Do not support nested multipart +- Do not support nested multipart -* Split multipart support to separate crate +- Split multipart support to separate crate -* Optimize multipart handling #634, #769 +- Optimize multipart handling #634, #769 diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 04a1d75ee..4beddd0b8 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.4.0-beta.9" +version = "0.4.0-beta.11" 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-web = { version = "4.0.0-beta.11", default-features = false } actix-utils = "3.0.0" +actix-web = { version = "4.0.0-beta.17", 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.14" +actix-http = "3.0.0-beta.17" futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } -tokio = { version = "1", features = ["sync"] } +tokio = { version = "1.8.4", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-multipart/README.md b/actix-multipart/README.md index 85c78c5f3..a9ee325ba 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -3,11 +3,11 @@ > Multipart form support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) -[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.9)](https://docs.rs/actix-multipart/0.4.0-beta.9) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.11)](https://docs.rs/actix-multipart/0.4.0-beta.11) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.9/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.9) +[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.11/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.11) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-multipart/src/lib.rs b/actix-multipart/src/lib.rs index 38a24e28f..3d536e08d 100644 --- a/actix-multipart/src/lib.rs +++ b/actix-multipart/src/lib.rs @@ -1,6 +1,7 @@ //! Multipart form support for Actix Web. -#![deny(rust_2018_idioms)] +#![deny(rust_2018_idioms, nonstandard_style)] +#![warn(future_incompatible)] #![allow(clippy::borrow_interior_mutable_const)] mod error; diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index 8eabcee10..239f7f905 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -1233,7 +1233,7 @@ mod tests { // and should not consume the payload match payload { - actix_web::dev::Payload::H1(_) => {} //expected + actix_web::dev::Payload::H1 { .. } => {} //expected _ => unreachable!(), } } diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index c2858f2ba..0a6a56359 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -1,20 +1,23 @@ # Changes ## Unreleased - 2021-xx-xx -* Minimum supported Rust version (MSRV) is now 1.52. + + +## 0.5.0-beta.3 - 2021-12-17 +- Minimum supported Rust version (MSRV) is now 1.52. ## 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] -* 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] -* `ResourceDef::pattern` now returns the first pattern in multi-pattern resources. [#2356] -* Support `build_resource_path` on multi-pattern resources. [#2356] -* Minimum supported Rust version (MSRV) is now 1.51. +- Introduce `ResourceDef::join`. [#380] +- Disallow prefix routes with tail segments. [#379] +- Enforce path separators on dynamic prefixes. [#378] +- Improve malformed path error message. [#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] +- `ResourceDef::pattern` now returns the first pattern in multi-pattern resources. [#2356] +- 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 @@ -25,23 +28,23 @@ ## 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] +- 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] [#368]: https://github.com/actix/actix-net/pull/368 [#366]: https://github.com/actix/actix-net/pull/366 @@ -53,10 +56,10 @@ ## 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] +- 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] [#345]: https://github.com/actix/actix-net/pull/345 [#357]: https://github.com/actix/actix-net/pull/357 @@ -65,68 +68,68 @@ ## 0.3.0 - 2019-12-31 -* Version was yanked previously. See https://crates.io/crates/actix-router/0.3.0 +- Version was yanked previously. See https://crates.io/crates/actix-router/0.3.0 ## 0.2.7 - 2021-02-06 -* Add `Router::recognize_checked` [#247] +- Add `Router::recognize_checked` [#247] [#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] [#246]: https://github.com/actix/actix-net/pull/246 ## 0.2.5 - 2020-09-20 -* Fix `from_hex()` method +- Fix `from_hex()` method ## 0.2.4 - 2019-12-31 -* Add `ResourceDef::resource_path_named()` path generation method +- Add `ResourceDef::resource_path_named()` path generation method ## 0.2.3 - 2019-12-25 -* Add impl `IntoPattern` for `&String` +- Add impl `IntoPattern` for `&String` ## 0.2.2 - 2019-12-25 -* Use `IntoPattern` for `RouterBuilder::path()` +- Use `IntoPattern` for `RouterBuilder::path()` ## 0.2.1 - 2019-12-25 -* Add `IntoPattern` trait -* Add multi-pattern resources +- Add `IntoPattern` trait +- Add multi-pattern resources ## 0.2.0 - 2019-12-07 -* Update http to 0.2 -* Update regex to 1.3 -* Use bytestring instead of string +- Update http to 0.2 +- Update regex to 1.3 +- Use bytestring instead of string ## 0.1.5 - 2019-05-15 -* Remove debug prints +- Remove debug prints ## 0.1.4 - 2019-05-15 -* Fix checked resource match +- Fix checked resource match ## 0.1.3 - 2019-04-22 -* Added support for `remainder match` (i.e "/path/{tail}*") +- Added support for `remainder match` (i.e "/path/{tail}*") ## 0.1.2 - 2019-04-07 -* Export `Quoter` type -* Allow to reset `Path` instance +- Export `Quoter` type +- Allow to reset `Path` instance ## 0.1.1 - 2019-04-03 -* Get dynamic segment by name instead of iterator. +- Get dynamic segment by name instead of iterator. ## 0.1.0 - 2019-03-09 -* Initial release +- Initial release diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml index b95bca505..c63448bc7 100644 --- a/actix-router/Cargo.toml +++ b/actix-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-router" -version = "0.5.0-beta.2" +version = "0.5.0-beta.3" authors = [ "Nikolay Kim ", "Ali MJ Al-Nasrawy ", @@ -21,7 +21,7 @@ default = ["http"] [dependencies] bytestring = ">=0.1.5, <2" -firestorm = "0.4" +firestorm = "0.5" http = { version = "0.2.3", optional = true } log = "0.4" regex = "1.5" @@ -29,7 +29,7 @@ serde = "1" [dev-dependencies] criterion = { version = "0.3", features = ["html_reports"] } -firestorm = { version = "0.4", features = ["enable_system_time"] } +firestorm = { version = "0.5", features = ["enable_system_time"] } http = "0.2.5" serde = { version = "1", features = ["derive"] } diff --git a/actix-router/src/lib.rs b/actix-router/src/lib.rs index 463e59e42..03f464626 100644 --- a/actix-router/src/lib.rs +++ b/actix-router/src/lib.rs @@ -1,149 +1,26 @@ //! Resource path matching and router. #![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")] mod de; mod path; +mod pattern; mod resource; +mod resource_path; mod router; -pub use self::de::PathDeserializer; -pub use self::path::Path; -pub use self::resource::ResourceDef; -pub use self::router::{ResourceInfo, Router, RouterBuilder}; - -// TODO: this trait is necessary, document it -// see impl Resource for ServiceRequest -pub trait Resource { - fn resource_path(&mut self) -> &mut Path; -} - -pub trait ResourcePath { - fn path(&self) -> &str; -} - -impl ResourcePath for String { - fn path(&self) -> &str { - self.as_str() - } -} - -impl<'a> ResourcePath for &'a str { - fn path(&self) -> &str { - self - } -} - -impl ResourcePath for bytestring::ByteString { - fn path(&self) -> &str { - &*self - } -} - -/// One or many patterns. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Patterns { - Single(String), - List(Vec), -} - -impl Patterns { - pub fn is_empty(&self) -> bool { - match self { - Patterns::Single(_) => false, - Patterns::List(pats) => pats.is_empty(), - } - } -} - -/// Helper trait for type that could be converted to one or more path pattern. -pub trait IntoPatterns { - fn patterns(&self) -> Patterns; -} - -impl IntoPatterns for String { - fn patterns(&self) -> Patterns { - Patterns::Single(self.clone()) - } -} - -impl<'a> IntoPatterns for &'a String { - fn patterns(&self) -> Patterns { - Patterns::Single((*self).clone()) - } -} - -impl<'a> IntoPatterns for &'a str { - fn patterns(&self) -> Patterns { - Patterns::Single((*self).to_owned()) - } -} - -impl IntoPatterns for bytestring::ByteString { - fn patterns(&self) -> Patterns { - Patterns::Single(self.to_string()) - } -} - -impl IntoPatterns for Patterns { - fn patterns(&self) -> Patterns { - self.clone() - } -} - -impl> IntoPatterns for Vec { - fn patterns(&self) -> Patterns { - let mut patterns = self.iter().map(|v| v.as_ref().to_owned()); - - match patterns.size_hint() { - (1, _) => Patterns::Single(patterns.next().unwrap()), - _ => Patterns::List(patterns.collect()), - } - } -} - -macro_rules! array_patterns_single (($tp:ty) => { - impl IntoPatterns for [$tp; 1] { - fn patterns(&self) -> Patterns { - Patterns::Single(self[0].to_owned()) - } - } -}); - -macro_rules! array_patterns_multiple (($tp:ty, $str_fn:expr, $($num:tt) +) => { - // for each array length specified in $num - $( - impl IntoPatterns for [$tp; $num] { - fn patterns(&self) -> Patterns { - Patterns::List(self.iter().map($str_fn).collect()) - } - } - )+ -}); - -array_patterns_single!(&str); -array_patterns_multiple!(&str, |&v| v.to_owned(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16); - -array_patterns_single!(String); -array_patterns_multiple!(String, |v| v.clone(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16); - #[cfg(feature = "http")] mod url; +pub use self::de::PathDeserializer; +pub use self::path::Path; +pub use self::pattern::{IntoPatterns, Patterns}; +pub use self::resource::ResourceDef; +pub use self::resource_path::{Resource, ResourcePath}; +pub use self::router::{ResourceInfo, Router, RouterBuilder}; + #[cfg(feature = "http")] pub use self::url::{Quoter, Url}; - -#[cfg(feature = "http")] -mod http_impls { - use http::Uri; - - use super::ResourcePath; - - impl ResourcePath for Uri { - fn path(&self) -> &str { - self.path() - } - } -} diff --git a/actix-router/src/pattern.rs b/actix-router/src/pattern.rs new file mode 100644 index 000000000..78a638a78 --- /dev/null +++ b/actix-router/src/pattern.rs @@ -0,0 +1,92 @@ +/// One or many patterns. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Patterns { + Single(String), + List(Vec), +} + +impl Patterns { + pub fn is_empty(&self) -> bool { + match self { + Patterns::Single(_) => false, + Patterns::List(pats) => pats.is_empty(), + } + } +} + +/// Helper trait for type that could be converted to one or more path patterns. +pub trait IntoPatterns { + fn patterns(&self) -> Patterns; +} + +impl IntoPatterns for String { + fn patterns(&self) -> Patterns { + Patterns::Single(self.clone()) + } +} + +impl IntoPatterns for &String { + fn patterns(&self) -> Patterns { + (*self).patterns() + } +} + +impl IntoPatterns for str { + fn patterns(&self) -> Patterns { + Patterns::Single(self.to_owned()) + } +} + +impl IntoPatterns for &str { + fn patterns(&self) -> Patterns { + (*self).patterns() + } +} + +impl IntoPatterns for bytestring::ByteString { + fn patterns(&self) -> Patterns { + Patterns::Single(self.to_string()) + } +} + +impl IntoPatterns for Patterns { + fn patterns(&self) -> Patterns { + self.clone() + } +} + +impl> IntoPatterns for Vec { + fn patterns(&self) -> Patterns { + let mut patterns = self.iter().map(|v| v.as_ref().to_owned()); + + match patterns.size_hint() { + (1, _) => Patterns::Single(patterns.next().unwrap()), + _ => Patterns::List(patterns.collect()), + } + } +} + +macro_rules! array_patterns_single (($tp:ty) => { + impl IntoPatterns for [$tp; 1] { + fn patterns(&self) -> Patterns { + Patterns::Single(self[0].to_owned()) + } + } +}); + +macro_rules! array_patterns_multiple (($tp:ty, $str_fn:expr, $($num:tt) +) => { + // for each array length specified in space-separated $num + $( + impl IntoPatterns for [$tp; $num] { + fn patterns(&self) -> Patterns { + Patterns::List(self.iter().map($str_fn).collect()) + } + } + )+ +}); + +array_patterns_single!(&str); +array_patterns_multiple!(&str, |&v| v.to_owned(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16); + +array_patterns_single!(String); +array_patterns_multiple!(String, |v| v.clone(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16); diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index d5f738a05..f1eb9caf5 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -29,26 +29,25 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// /// /// # Pattern Format and Matching Behavior -/// /// Resource pattern is defined as a string of zero or more _segments_ where each segment is /// preceded by a slash `/`. /// -/// This means that pattern string __must__ either be empty or begin with a slash (`/`). -/// This also implies that a trailing slash in pattern defines an empty segment. -/// For example, the pattern `"/user/"` has two segments: `["user", ""]` +/// This means that pattern string __must__ either be empty or begin with a slash (`/`). This also +/// implies that a trailing slash in pattern defines an empty segment. For example, the pattern +/// `"/user/"` has two segments: `["user", ""]` /// -/// A key point to underhand is that `ResourceDef` matches segments, not strings. -/// It matches segments individually. -/// For example, the pattern `/user/` is not considered a prefix for the path `/user/123/456`, -/// because the second segment doesn't match: `["user", ""]` vs `["user", "123", "456"]`. +/// A key point to understand is that `ResourceDef` matches segments, not strings. Segments are +/// matched individually. For example, the pattern `/user/` is not considered a prefix for the path +/// `/user/123/456`, because the second segment doesn't match: `["user", ""]` +/// vs `["user", "123", "456"]`. /// /// This definition is consistent with the definition of absolute URL path in -/// [RFC 3986 (section 3.3)](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3) +/// [RFC 3986 §3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3) /// /// /// # Static Resources -/// A static resource is the most basic type of definition. Pass a pattern to -/// [new][Self::new]. Conforming paths must match the pattern exactly. +/// A static resource is the most basic type of definition. Pass a pattern to [new][Self::new]. +/// Conforming paths must match the pattern exactly. /// /// ## Examples /// ``` @@ -63,7 +62,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(!resource.is_match("/search")); /// ``` /// -/// /// # Dynamic Segments /// Also known as "path parameters". Resources can define sections of a pattern that be extracted /// from a conforming path, if it conforms to (one of) the resource pattern(s). @@ -102,15 +100,15 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert_eq!(path.get("id").unwrap(), "123"); /// ``` /// -/// /// # Prefix Resources /// A prefix resource is defined as pattern that can match just the start of a path, up to a /// segment boundary. /// /// Prefix patterns with a trailing slash may have an unexpected, though correct, behavior. -/// They define and therefore require an empty segment in order to match. Examples are given below. +/// They define and therefore require an empty segment in order to match. It is easier to understand +/// this behavior after reading the [matching behavior section]. Examples are given below. /// -/// Empty pattern matches any path as a prefix. +/// The empty pattern (`""`), as a prefix, matches any path. /// /// Prefix resources can contain dynamic segments. /// @@ -130,7 +128,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(!resource.is_match("/user/123")); /// ``` /// -/// /// # Custom Regex Segments /// Dynamic segments can be customised to only match a specific regular expression. It can be /// helpful to do this if resource definitions would otherwise conflict and cause one to @@ -158,7 +155,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(!resource.is_match("/user/abc")); /// ``` /// -/// /// # Tail Segments /// As a shortcut to defining a custom regex for matching _all_ remaining characters (not just those /// up until a `/` character), there is a special pattern to match (and capture) the remaining @@ -168,7 +164,7 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// extracted in the same way as non-tail dynamic segments. /// /// ## Examples -/// ```rust +/// ``` /// # use actix_router::{Path, ResourceDef}; /// let resource = ResourceDef::new("/blob/{tail}*"); /// assert!(resource.is_match("/blob/HEAD/Cargo.toml")); @@ -179,7 +175,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert_eq!(path.get("tail").unwrap(), "main/LICENSE"); /// ``` /// -/// /// # Multi-Pattern Resources /// For resources that can map to multiple distinct paths, it may be suitable to use /// multi-pattern resources by passing an array/vec to [`new`][Self::new]. They will be combined @@ -191,14 +186,13 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// expectations in the router using these definitions and cause runtime panics. /// /// ## Examples -/// ```rust +/// ``` /// # use actix_router::ResourceDef; /// let resource = ResourceDef::new(["/home", "/index"]); /// assert!(resource.is_match("/home")); /// assert!(resource.is_match("/index")); /// ``` /// -/// /// # Trailing Slashes /// It should be noted that this library takes no steps to normalize intra-path or trailing slashes. /// As such, all resource definitions implicitly expect a pre-processing step to normalize paths if @@ -206,12 +200,14 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// resource-path pairs that would not be compatible. /// /// ## Examples -/// ```rust +/// ``` /// # use actix_router::ResourceDef; /// assert!(!ResourceDef::new("/root").is_match("/root/")); /// assert!(!ResourceDef::new("/root/").is_match("/root")); /// assert!(!ResourceDef::prefix("/root/").is_match("/root")); /// ``` +/// +/// [matching behavior section]: #pattern-format-and-matching-behavior #[derive(Clone, Debug)] pub struct ResourceDef { id: u16, @@ -279,7 +275,7 @@ impl ResourceDef { /// ``` pub fn new(paths: T) -> Self { profile_method!(new); - Self::new2(paths, false) + Self::construct(paths, false) } /// Constructs a new resource definition using a pattern that performs prefix matching. @@ -292,7 +288,7 @@ impl ResourceDef { /// resource definition with a tail segment; use [`new`][Self::new] in this case. /// /// # Panics - /// Panics if path regex pattern is malformed. + /// Panics if path pattern is malformed. /// /// # Examples /// ``` @@ -307,14 +303,14 @@ impl ResourceDef { /// ``` pub fn prefix(paths: T) -> Self { profile_method!(prefix); - ResourceDef::new2(paths, true) + ResourceDef::construct(paths, true) } /// Constructs a new resource definition using a string pattern that performs prefix matching, - /// inserting a `/` to beginning of the pattern if absent and pattern is not empty. + /// ensuring a leading `/` if pattern is not empty. /// /// # Panics - /// Panics if path regex pattern is malformed. + /// Panics if path pattern is malformed. /// /// # Examples /// ``` @@ -515,8 +511,8 @@ impl ResourceDef { .collect::>(); match patterns.len() { - 1 => ResourceDef::new2(&patterns[0], other.is_prefix()), - _ => ResourceDef::new2(patterns, other.is_prefix()), + 1 => ResourceDef::construct(&patterns[0], other.is_prefix()), + _ => ResourceDef::construct(patterns, other.is_prefix()), } } @@ -881,8 +877,8 @@ impl ResourceDef { } } - fn new2(paths: T, is_prefix: bool) -> Self { - profile_method!(new2); + fn construct(paths: T, is_prefix: bool) -> Self { + profile_method!(construct); let patterns = paths.patterns(); let (pat_type, segments) = match &patterns { @@ -1814,7 +1810,7 @@ mod tests { #[test] #[should_panic] - fn prefix_plus_tail_match_is_allowed() { + fn prefix_plus_tail_match_disallowed() { ResourceDef::prefix("/user/{id}*"); } } diff --git a/actix-router/src/resource_path.rs b/actix-router/src/resource_path.rs new file mode 100644 index 000000000..91a7f2f55 --- /dev/null +++ b/actix-router/src/resource_path.rs @@ -0,0 +1,36 @@ +use crate::Path; + +// TODO: this trait is necessary, document it +// see impl Resource for ServiceRequest +pub trait Resource { + fn resource_path(&mut self) -> &mut Path; +} + +pub trait ResourcePath { + fn path(&self) -> &str; +} + +impl ResourcePath for String { + fn path(&self) -> &str { + self.as_str() + } +} + +impl<'a> ResourcePath for &'a str { + fn path(&self) -> &str { + self + } +} + +impl ResourcePath for bytestring::ByteString { + fn path(&self) -> &str { + &*self + } +} + +#[cfg(feature = "http")] +impl ResourcePath for http::Uri { + fn path(&self) -> &str { + self.path() + } +} diff --git a/actix-router/src/url.rs b/actix-router/src/url.rs index e08a7171a..10193dde8 100644 --- a/actix-router/src/url.rs +++ b/actix-router/src/url.rs @@ -2,22 +2,28 @@ use crate::ResourcePath; #[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"; #[inline] @@ -34,19 +40,20 @@ thread_local! { static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+"); } -#[derive(Default, Clone, Debug)] +#[derive(Debug, Clone, Default)] pub struct Url { uri: http::Uri, path: Option, } impl Url { + #[inline] pub fn new(uri: http::Uri) -> Url { let path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes())); - Url { uri, path } } + #[inline] pub fn with_quoter(uri: http::Uri, quoter: &Quoter) -> Url { Url { path: quoter.requote(uri.path().as_bytes()), @@ -54,15 +61,16 @@ impl Url { } } + #[inline] pub fn uri(&self) -> &http::Uri { &self.uri } + #[inline] pub fn path(&self) -> &str { - if let Some(ref s) = self.path { - s - } else { - self.uri.path() + match self.path { + Some(ref path) => path, + _ => self.uri.path(), } } @@ -86,6 +94,7 @@ impl ResourcePath for Url { } } +/// A quoter pub struct Quoter { safe_table: [u8; 16], protected_table: [u8; 16], @@ -93,7 +102,7 @@ pub struct Quoter { impl Quoter { pub fn new(safe: &[u8], protected: &[u8]) -> Quoter { - let mut q = Quoter { + let mut quoter = Quoter { safe_table: [0; 16], protected_table: [0; 16], }; @@ -101,24 +110,24 @@ impl Quoter { // prepare safe table for i in 0..128 { if ALLOWED.contains(&i) { - set_bit(&mut q.safe_table, i); + set_bit(&mut quoter.safe_table, i); } if QS.contains(&i) { - set_bit(&mut q.safe_table, i); + set_bit(&mut quoter.safe_table, i); } } for ch in safe { - set_bit(&mut q.safe_table, *ch) + set_bit(&mut quoter.safe_table, *ch) } // prepare protected table for ch in protected { - set_bit(&mut q.safe_table, *ch); - set_bit(&mut q.protected_table, *ch); + set_bit(&mut quoter.safe_table, *ch); + set_bit(&mut quoter.protected_table, *ch); } - q + quoter } pub fn requote(&self, val: &[u8]) -> Option { @@ -215,7 +224,7 @@ mod tests { } #[test] - fn test_parse_url() { + fn parse_url() { let re = "/user/{id}/test"; let path = match_url(re, "/user/2345/test"); @@ -231,24 +240,24 @@ mod tests { } #[test] - fn test_protected_chars() { + fn protected_chars() { let encoded = percent_encode(PROTECTED); let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded)); assert_eq!(path.get("id").unwrap(), &encoded); } #[test] - fn test_non_protecteed_ascii() { - let nonprotected_ascii = ('\u{0}'..='\u{7F}') + fn non_protected_ascii() { + let non_protected_ascii = ('\u{0}'..='\u{7F}') .filter(|&c| c.is_ascii() && !PROTECTED.contains(&(c as u8))) .collect::(); - let encoded = percent_encode(nonprotected_ascii.as_bytes()); + let encoded = percent_encode(non_protected_ascii.as_bytes()); let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded)); - assert_eq!(path.get("id").unwrap(), &nonprotected_ascii); + assert_eq!(path.get("id").unwrap(), &non_protected_ascii); } #[test] - fn test_valid_utf8_multibyte() { + fn valid_utf8_multibyte() { let test = ('\u{FF00}'..='\u{FFFF}').collect::(); let encoded = percent_encode(test.as_bytes()); let path = match_url("/a/{id}/b", format!("/a/{}/b", &encoded)); @@ -256,7 +265,7 @@ mod tests { } #[test] - fn test_invalid_utf8() { + fn invalid_utf8() { let invalid_utf8 = percent_encode((0x80..=0xff).collect::>().as_slice()); let uri = Uri::try_from(format!("/{}", invalid_utf8)).unwrap(); let path = Path::new(Url::new(uri)); @@ -266,7 +275,7 @@ mod tests { } #[test] - fn test_from_hex() { + fn hex_encoding() { let hex = b"0123456789abcdefABCDEF"; for i in 0..256 { diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index b739011f0..2de0a69d6 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -3,36 +3,51 @@ ## Unreleased - 2021-xx-xx +## 0.1.0-beta.10 - 2021-12-27 +* No significant changes since `0.1.0-beta.9`. + + +## 0.1.0-beta.9 - 2021-12-17 +- Re-export `actix_http::body::to_bytes`. [#2518] +- Update `actix_web::test` re-exports. [#2518] + +[#2518]: https://github.com/actix/actix-web/pull/2518 + + +## 0.1.0-beta.8 - 2021-12-11 +- No significant changes since `0.1.0-beta.7`. + + ## 0.1.0-beta.7 - 2021-11-22 -* Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] +- Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] [#2408]: https://github.com/actix/actix-web/pull/2408 ## 0.1.0-beta.6 - 2021-11-15 -* No significant changes from `0.1.0-beta.5`. +- No significant changes from `0.1.0-beta.5`. ## 0.1.0-beta.5 - 2021-10-20 -* Updated rustls to v0.20. [#2414] -* Minimum supported Rust version (MSRV) is now 1.52. +- Updated rustls to v0.20. [#2414] +- Minimum supported Rust version (MSRV) is now 1.52. [#2414]: https://github.com/actix/actix-web/pull/2414 ## 0.1.0-beta.4 - 2021-09-09 -* Minimum supported Rust version (MSRV) is now 1.51. +- Minimum supported Rust version (MSRV) is now 1.51. ## 0.1.0-beta.3 - 2021-06-20 -* No significant changes from `0.1.0-beta.2`. +- No significant changes from `0.1.0-beta.2`. ## 0.1.0-beta.2 - 2021-04-17 -* No significant changes from `0.1.0-beta.1`. +- No significant changes from `0.1.0-beta.1`. ## 0.1.0-beta.1 - 2021-04-02 -* Move integration testing structs from `actix-web`. [#2112] +- Move integration testing structs from `actix-web`. [#2112] [#2112]: https://github.com/actix/actix-web/pull/2112 diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index dcaa3e9a3..c523a6566 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-test" -version = "0.1.0-beta.7" +version = "0.1.0-beta.10" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] actix-codec = "0.4.1" -actix-http = "3.0.0-beta.14" -actix-http-test = "3.0.0-beta.7" +actix-http = "3.0.0-beta.17" +actix-http-test = "3.0.0-beta.10" +actix-rt = "2.1" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] } -actix-rt = "2.1" -awc = { version = "3.0.0-beta.11", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.17", default-features = false, features = ["cookies"] } +awc = { version = "3.0.0-beta.16", default-features = false, features = ["cookies"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false, features = [] } @@ -45,4 +45,4 @@ serde_json = "1" serde_urlencoded = "0.7" tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-rustls = { package = "rustls", version = "0.20.0", optional = true } -tokio = { version = "1.2", features = ["sync"] } +tokio = { version = "1.8.4", features = ["sync"] } diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs index 7e493ce71..f86120f2f 100644 --- a/actix-test/src/lib.rs +++ b/actix-test/src/lib.rs @@ -26,6 +26,9 @@ //! } //! ``` +#![deny(rust_2018_idioms, nonstandard_style)] +#![warn(future_incompatible)] + #[cfg(feature = "openssl")] extern crate tls_openssl as openssl; #[cfg(feature = "rustls")] @@ -34,9 +37,14 @@ extern crate tls_rustls as rustls; use std::{fmt, net, thread, time::Duration}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; -pub use actix_http::test::TestBuffer; +pub use actix_http::{body::to_bytes, test::TestBuffer}; use actix_http::{header::HeaderMap, ws, HttpService, Method, Request, Response}; +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, +}; use actix_web::{ body::MessageBody, dev::{AppConfig, Server, ServerHandle, Service}, @@ -45,12 +53,6 @@ use actix_web::{ }; use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector}; use futures_core::Stream; - -pub use actix_http_test::unused_addr; -pub use actix_web::test::{ - call_service, default_service, init_service, load_stream, ok_service, read_body, - read_body_json, read_response, read_response_json, TestRequest, -}; use tokio::sync::mpsc; /// Start default [`TestServer`]. @@ -338,7 +340,7 @@ where Connector::new() .conn_lifetime(Duration::from_secs(0)) .timeout(Duration::from_millis(30000)) - .ssl(builder.build()) + .openssl(builder.build()) } #[cfg(not(feature = "openssl"))] { diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 898098ed8..2fbbe7444 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,105 +1,112 @@ # Changes ## Unreleased - 2021-xx-xx -* Add `ws:WsResponseBuilder` for building WebSocket session response. [#1920] -* Deprecate `ws::{start_with_addr, start_with_protocols}`. [#1920] -* Minimum supported Rust version (MSRV) is now 1.52. + + +## 4.0.0-beta.9 - 2021-12-27 +* No significant changes since `4.0.0-beta.8`. + + +## 4.0.0-beta.8 - 2021-12-11 +- Add `ws:WsResponseBuilder` for building WebSocket session response. [#1920] +- Deprecate `ws::{start_with_addr, start_with_protocols}`. [#1920] +- Minimum supported Rust version (MSRV) is now 1.52. [#1920]: https://github.com/actix/actix-web/pull/1920 ## 4.0.0-beta.7 - 2021-09-09 -* Minimum supported Rust version (MSRV) is now 1.51. +- Minimum supported Rust version (MSRV) is now 1.51. ## 4.0.0-beta.6 - 2021-06-26 -* Update `actix` to `0.12`. [#2277] +- Update `actix` to `0.12`. [#2277] [#2277]: https://github.com/actix/actix-web/pull/2277 ## 4.0.0-beta.5 - 2021-06-17 -* No notable changes. +- No notable changes. ## 4.0.0-beta.4 - 2021-04-02 -* No notable changes. +- No notable changes. ## 4.0.0-beta.3 - 2021-03-09 -* No notable changes. +- No notable changes. ## 4.0.0-beta.2 - 2021-02-10 -* No notable changes. +- No notable changes. ## 4.0.0-beta.1 - 2021-01-07 -* Update `pin-project` to `1.0`. -* Update `bytes` to `1.0`. [#1813] -* `WebsocketContext::text` now takes an `Into`. [#1864] +- Update `pin-project` to `1.0`. +- Update `bytes` to `1.0`. [#1813] +- `WebsocketContext::text` now takes an `Into`. [#1864] [#1813]: https://github.com/actix/actix-web/pull/1813 [#1864]: https://github.com/actix/actix-web/pull/1864 ## 3.0.0 - 2020-09-11 -* No significant changes from `3.0.0-beta.2`. +- No significant changes from `3.0.0-beta.2`. ## 3.0.0-beta.2 - 2020-09-10 -* Update `actix-*` dependencies to latest versions. +- Update `actix-*` dependencies to latest versions. ## [3.0.0-beta.1] - 2020-xx-xx -* Update `actix-web` & `actix-http` dependencies to beta.1 -* Bump minimum supported Rust version to 1.40 +- Update `actix-web` & `actix-http` dependencies to beta.1 +- Bump minimum supported Rust version to 1.40 ## [3.0.0-alpha.1] - 2020-05-08 -* Update the actix-web dependency to 3.0.0-alpha.1 -* Update the actix dependency to 0.10.0-alpha.2 -* Update the actix-http dependency to 2.0.0-alpha.3 +- Update the actix-web dependency to 3.0.0-alpha.1 +- Update the actix dependency to 0.10.0-alpha.2 +- Update the actix-http dependency to 2.0.0-alpha.3 ## [2.0.0] - 2019-12-20 -* Release +- Release ## [2.0.0-alpha.1] - 2019-12-15 -* Migrate to actix-web 2.0.0 +- Migrate to actix-web 2.0.0 ## [1.0.4] - 2019-12-07 -* Allow comma-separated websocket subprotocols without spaces (#1172) +- Allow comma-separated websocket subprotocols without spaces (#1172) ## [1.0.3] - 2019-11-14 -* Update actix-web and actix-http dependencies +- Update actix-web and actix-http dependencies ## [1.0.2] - 2019-07-20 -* Add `ws::start_with_addr()`, returning the address of the created actor, along +- Add `ws::start_with_addr()`, returning the address of the created actor, along with the `HttpResponse`. -* Add support for specifying protocols on websocket handshake #835 +- Add support for specifying protocols on websocket handshake #835 ## [1.0.1] - 2019-06-28 -* Allow to use custom ws codec with `WebsocketContext` #925 +- Allow to use custom ws codec with `WebsocketContext` #925 ## [1.0.0] - 2019-05-29 -* Update actix-http and actix-web +- Update actix-http and actix-web ## [0.1.0-alpha.3] - 2019-04-02 -* Update actix-http and actix-web +- Update actix-http and actix-web ## [0.1.0-alpha.2] - 2019-03-29 -* Update actix-http and actix-web +- Update actix-http and actix-web ## [0.1.0-alpha.1] - 2019-03-28 -* Initial impl +- Initial impl diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 28b5b29ea..719c563cb 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.7" +version = "4.0.0-beta.9" authors = ["Nikolay Kim "] description = "Actix actors support for Actix Web" keywords = ["actix", "http", "web", "framework", "async"] @@ -16,19 +16,19 @@ path = "src/lib.rs" [dependencies] actix = { version = "0.12.0", default-features = false } actix-codec = "0.4.1" -actix-http = "3.0.0-beta.14" -actix-web = { version = "4.0.0-beta.11", default-features = false } +actix-http = "3.0.0-beta.17" +actix-web = { version = "4.0.0-beta.17", default-features = false } bytes = "1" bytestring = "1" futures-core = { version = "0.3.7", default-features = false } pin-project-lite = "0.2" -tokio = { version = "1", features = ["sync"] } +tokio = { version = "1.8.4", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.7" +actix-test = "0.1.0-beta.10" +awc = { version = "3.0.0-beta.16", default-features = false } -awc = { version = "3.0.0-beta.11", default-features = false } env_logger = "0.9" futures-util = { version = "0.3.7", default-features = false } diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index 2c29dedf2..232c81eac 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -3,11 +3,11 @@ > Actix actors support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) -[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.7)](https://docs.rs/actix-web-actors/4.0.0-beta.7) +[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.9)](https://docs.rs/actix-web-actors/4.0.0-beta.9) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
-[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.7) +[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.9) [![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs index 7a4823d91..70c957020 100644 --- a/actix-web-actors/src/lib.rs +++ b/actix-web-actors/src/lib.rs @@ -1,7 +1,7 @@ //! Actix actors support for Actix Web. -#![deny(rust_2018_idioms)] -#![allow(clippy::borrow_interior_mutable_const)] +#![deny(rust_2018_idioms, nonstandard_style)] +#![warn(future_incompatible)] mod context; pub mod ws; diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 3811ef030..0d881d303 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -3,98 +3,102 @@ ## Unreleased - 2021-xx-xx +## 0.5.0-beta.6 - 2021-12-11 +- No significant changes since `0.5.0-beta.5`. + + ## 0.5.0-beta.5 - 2021-10-20 -* Improve error recovery potential when macro input is invalid. [#2410] -* Add `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] -* Minimum supported Rust version (MSRV) is now 1.52. +- Improve error recovery potential when macro input is invalid. [#2410] +- Add `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] +- Minimum supported Rust version (MSRV) is now 1.52. [#2410]: https://github.com/actix/actix-web/pull/2410 [#2409]: https://github.com/actix/actix-web/pull/2409 ## 0.5.0-beta.4 - 2021-09-09 -* In routing macros, paths are now validated at compile time. [#2350] -* Minimum supported Rust version (MSRV) is now 1.51. +- In routing macros, paths are now validated at compile time. [#2350] +- Minimum supported Rust version (MSRV) is now 1.51. [#2350]: https://github.com/actix/actix-web/pull/2350 ## 0.5.0-beta.3 - 2021-06-17 -* No notable changes. +- No notable changes. ## 0.5.0-beta.2 - 2021-03-09 -* Preserve doc comments when using route macros. [#2022] -* Add `name` attribute to `route` macro. [#1934] +- Preserve doc comments when using route macros. [#2022] +- Add `name` attribute to `route` macro. [#1934] [#2022]: https://github.com/actix/actix-web/pull/2022 [#1934]: https://github.com/actix/actix-web/pull/1934 ## 0.5.0-beta.1 - 2021-02-10 -* Use new call signature for `System::new`. +- Use new call signature for `System::new`. ## 0.4.0 - 2020-09-20 -* Added compile success and failure testing. [#1677] -* Add `route` macro for supporting multiple HTTP methods guards. [#1674] +- Added compile success and failure testing. [#1677] +- Add `route` macro for supporting multiple HTTP methods guards. [#1674] [#1677]: https://github.com/actix/actix-web/pull/1677 [#1674]: https://github.com/actix/actix-web/pull/1674 ## 0.3.0 - 2020-09-11 -* No significant changes from `0.3.0-beta.1`. +- No significant changes from `0.3.0-beta.1`. ## 0.3.0-beta.1 - 2020-07-14 -* Add main entry-point macro that uses re-exported runtime. [#1559] +- Add main entry-point macro that uses re-exported runtime. [#1559] [#1559]: https://github.com/actix/actix-web/pull/1559 ## 0.2.2 - 2020-05-23 -* Add resource middleware on actix-web-codegen [#1467] +- Add resource middleware on actix-web-codegen [#1467] [#1467]: https://github.com/actix/actix-web/pull/1467 ## 0.2.1 - 2020-02-25 -* Add `#[allow(missing_docs)]` attribute to generated structs [#1368] -* Allow the handler function to be named as `config` [#1290] +- Add `#[allow(missing_docs)]` attribute to generated structs [#1368] +- Allow the handler function to be named as `config` [#1290] [#1368]: https://github.com/actix/actix-web/issues/1368 [#1290]: https://github.com/actix/actix-web/issues/1290 ## 0.2.0 - 2019-12-13 -* Generate code for actix-web 2.0 +- Generate code for actix-web 2.0 ## 0.1.3 - 2019-10-14 -* Bump up `syn` & `quote` to 1.0 -* Provide better error message +- Bump up `syn` & `quote` to 1.0 +- Provide better error message ## 0.1.2 - 2019-06-04 -* Add macros for head, options, trace, connect and patch http methods +- Add macros for head, options, trace, connect and patch http methods ## 0.1.1 - 2019-06-01 -* Add syn "extra-traits" feature +- Add syn "extra-traits" feature ## 0.1.0 - 2019-05-18 -* Release +- Release ## 0.1.0-beta.1 - 2019-04-20 -* Gen code for actix-web 1.0.0-beta.1 +- Gen code for actix-web 1.0.0-beta.1 ## 0.1.0-alpha.6 - 2019-04-14 -* Gen code for actix-web 1.0.0-alpha.6 +- Gen code for actix-web 1.0.0-alpha.6 ## 0.1.0-alpha.1 - 2019-03-28 -* Initial impl +- Initial impl diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 8497f0b23..b014a47ae 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-codegen" -version = "0.5.0-beta.5" +version = "0.5.0-beta.6" description = "Routing and runtime macros for Actix Web" homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" @@ -18,14 +18,14 @@ proc-macro = true quote = "1" syn = { version = "1", features = ["full", "parsing"] } proc-macro2 = "1" -actix-router = "0.5.0-beta.2" +actix-router = "0.5.0-beta.3" [dev-dependencies] -actix-rt = "2.2" actix-macros = "0.2.3" -actix-test = "0.1.0-beta.7" +actix-rt = "2.2" +actix-test = "0.1.0-beta.10" actix-utils = "3.0.0" -actix-web = "4.0.0-beta.11" +actix-web = "4.0.0-beta.17" 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 2ffd5b31c..f05d3f22c 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-beta.5)](https://docs.rs/actix-web-codegen/0.5.0-beta.5) +[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.6)](https://docs.rs/actix-web-codegen/0.5.0-beta.6) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
-[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.5/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.5) +[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.6/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.6) [![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 cebf9e5fb..52cfc0d8f 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -57,6 +57,8 @@ //! [DELETE]: macro@delete #![recursion_limit = "512"] +#![deny(rust_2018_idioms, nonstandard_style)] +#![warn(future_incompatible)] use proc_macro::TokenStream; use quote::quote; diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index eac1948a7..a4472efd2 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -1,7 +1,4 @@ -extern crate proc_macro; - -use std::collections::HashSet; -use std::convert::TryFrom; +use std::{collections::HashSet, convert::TryFrom}; use actix_router::ResourceDef; use proc_macro::TokenStream; diff --git a/awc/CHANGES.md b/awc/CHANGES.md index ab3362b72..212469873 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -3,60 +3,91 @@ ## Unreleased - 2021-xx-xx +## 3.0.0-beta.16 - 2021-12-29 +- `*::send_json` and `*::send_form` methods now receive `impl Serialize`. [#2553] +- `FrozenClientRequest::extra_header` now uses receives an `impl TryIntoHeaderPair`. [#2553] +- Remove unnecessary `Unpin` bounds on `*::send_stream`. [#2553] + +[#2553]: https://github.com/actix/actix-web/pull/2553 + + +## 3.0.0-beta.15 - 2021-12-27 +- Rename `Connector::{ssl => openssl}`. [#2503] +- Improve `Client` instantiation efficiency when using `openssl` by only building connectors once. [#2503] +- `ClientRequest::send_body` now takes an `impl MessageBody`. [#2546] +- Rename `MessageBody => ResponseBody` to avoid conflicts with `MessageBody` trait. [#2546] +- `impl Future` for `ResponseBody` no longer requires the body type be `Unpin`. [#2546] +- `impl Future` for `JsonBody` no longer requires the body type be `Unpin`. [#2546] +- `impl Stream` for `ClientResponse` no longer requires the body type be `Unpin`. [#2546] + +[#2503]: https://github.com/actix/actix-web/pull/2503 +[#2546]: https://github.com/actix/actix-web/pull/2546 + + +## 3.0.0-beta.14 - 2021-12-17 +- Add `ClientBuilder::add_default_header` and deprecate `ClientBuilder::header`. [#2510] + +[#2510]: https://github.com/actix/actix-web/pull/2510 + + +## 3.0.0-beta.13 - 2021-12-11 +- No significant changes since `3.0.0-beta.12`. + + ## 3.0.0-beta.12 - 2021-11-30 -* Update `actix-tls` to `3.0.0-rc.1`. [#2474] +- Update `actix-tls` to `3.0.0-rc.1`. [#2474] [#2474]: https://github.com/actix/actix-web/pull/2474 ## 3.0.0-beta.11 - 2021-11-22 -* No significant changes from `3.0.0-beta.10`. +- No significant changes from `3.0.0-beta.10`. ## 3.0.0-beta.10 - 2021-11-15 -* No significant changes from `3.0.0-beta.9`. +- No significant changes from `3.0.0-beta.9`. ## 3.0.0-beta.9 - 2021-10-20 -* Updated rustls to v0.20. [#2414] +- Updated rustls to v0.20. [#2414] [#2414]: https://github.com/actix/actix-web/pull/2414 ## 3.0.0-beta.8 - 2021-09-09 ### Changed -* Send headers within the redirect requests. [#2310] +- Send headers within the redirect requests. [#2310] [#2310]: https://github.com/actix/actix-web/pull/2310 ## 3.0.0-beta.7 - 2021-06-26 ### Changed -* Change compression algorithm features flags. [#2250] +- Change compression algorithm features flags. [#2250] [#2250]: https://github.com/actix/actix-web/pull/2250 ## 3.0.0-beta.6 - 2021-06-17 -* No significant changes since 3.0.0-beta.5. +- No significant changes since 3.0.0-beta.5. ## 3.0.0-beta.5 - 2021-04-17 ### Removed -* Deprecated methods on `ClientRequest`: `if_true`, `if_some`. [#2148] +- Deprecated methods on `ClientRequest`: `if_true`, `if_some`. [#2148] [#2148]: https://github.com/actix/actix-web/pull/2148 ## 3.0.0-beta.4 - 2021-04-02 ### Added -* Add `Client::headers` to get default mut reference of `HeaderMap` of client object. [#2114] +- Add `Client::headers` to get default mut reference of `HeaderMap` of client object. [#2114] ### Changed -* `ConnectorService` type is renamed to `BoxConnectorService`. [#2081] -* Fix http/https encoding when enabling `compress` feature. [#2116] -* Rename `TestResponse::header` to `append_header`, `set` to `insert_header`. `TestResponse` header - methods now take `IntoHeaderPair` tuples. [#2094] +- `ConnectorService` type is renamed to `BoxConnectorService`. [#2081] +- Fix http/https encoding when enabling `compress` feature. [#2116] +- Rename `TestResponse::header` to `append_header`, `set` to `insert_header`. `TestResponse` header + methods now take `TryIntoHeaderPair` tuples. [#2094] [#2081]: https://github.com/actix/actix-web/pull/2081 [#2094]: https://github.com/actix/actix-web/pull/2094 @@ -66,16 +97,16 @@ ## 3.0.0-beta.3 - 2021-03-08 ### Added -* `ClientResponse::timeout` for set the timeout of collecting response body. [#1931] -* `ClientBuilder::local_address` for bind to a local ip address for this client. [#2024] +- `ClientResponse::timeout` for set the timeout of collecting response body. [#1931] +- `ClientBuilder::local_address` for bind to a local ip address for this client. [#2024] ### Changed -* Feature `cookies` is now optional and enabled by default. [#1981] -* `ClientBuilder::connector` method would take `actix_http::client::Connector` type. [#2008] -* Basic auth password now takes blank passwords as an empty string instead of Option. [#2050] +- Feature `cookies` is now optional and enabled by default. [#1981] +- `ClientBuilder::connector` method would take `actix_http::client::Connector` type. [#2008] +- Basic auth password now takes blank passwords as an empty string instead of Option. [#2050] ### Removed -* `ClientBuilder::default` function [#2008] +- `ClientBuilder::default` function [#2008] [#1931]: https://github.com/actix/actix-web/pull/1931 [#1981]: https://github.com/actix/actix-web/pull/1981 @@ -86,18 +117,18 @@ ## 3.0.0-beta.2 - 2021-02-10 ### Added -* `ClientRequest::insert_header` method which allows using typed headers. [#1869] -* `ClientRequest::append_header` method which allows using typed headers. [#1869] -* `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969] +- `ClientRequest::insert_header` method which allows using typed headers. [#1869] +- `ClientRequest::append_header` method which allows using typed headers. [#1869] +- `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969] ### Changed -* Relax default timeout for `Connector` to 5 seconds(original 1 second). [#1905] +- Relax default timeout for `Connector` to 5 seconds(original 1 second). [#1905] ### Removed -* `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] +- `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] [#1869]: https://github.com/actix/actix-web/pull/1869 [#1905]: https://github.com/actix/actix-web/pull/1905 @@ -106,32 +137,32 @@ ## 3.0.0-beta.1 - 2021-01-07 ### Changed -* Update `rand` to `0.8` -* Update `bytes` to `1.0`. [#1813] -* Update `rust-tls` to `0.19`. [#1813] +- Update `rand` to `0.8` +- Update `bytes` to `1.0`. [#1813] +- Update `rust-tls` to `0.19`. [#1813] [#1813]: https://github.com/actix/actix-web/pull/1813 ## 2.0.3 - 2020-11-29 ### Fixed -* Ensure `actix-http` dependency uses same `serde_urlencoded`. +- Ensure `actix-http` dependency uses same `serde_urlencoded`. ## 2.0.2 - 2020-11-25 ### Changed -* Upgrade `serde_urlencoded` to `0.7`. [#1773] +- Upgrade `serde_urlencoded` to `0.7`. [#1773] [#1773]: https://github.com/actix/actix-web/pull/1773 ## 2.0.1 - 2020-10-30 ### Changed -* Upgrade `base64` to `0.13`. [#1744] -* Deprecate `ClientRequest::{if_some, if_true}`. [#1760] +- Upgrade `base64` to `0.13`. [#1744] +- Deprecate `ClientRequest::{if_some, if_true}`. [#1760] ### Fixed -* Use `Accept-Encoding: identity` instead of `Accept-Encoding: br` when no compression feature +- Use `Accept-Encoding: identity` instead of `Accept-Encoding: br` when no compression feature is enabled [#1737] [#1737]: https://github.com/actix/actix-web/pull/1737 @@ -141,209 +172,209 @@ ## 2.0.0 - 2020-09-11 ### Changed -* `Client::build` was renamed to `Client::builder`. +- `Client::build` was renamed to `Client::builder`. ## 2.0.0-beta.4 - 2020-09-09 ### Changed -* Update actix-codec & actix-tls dependencies. +- Update actix-codec & actix-tls dependencies. ## 2.0.0-beta.3 - 2020-08-17 ### Changed -* Update `rustls` to 0.18 +- Update `rustls` to 0.18 ## 2.0.0-beta.2 - 2020-07-21 ### Changed -* Update `actix-http` dependency to 2.0.0-beta.2 +- Update `actix-http` dependency to 2.0.0-beta.2 ## [2.0.0-beta.1] - 2020-07-14 ### Changed -* Update `actix-http` dependency to 2.0.0-beta.1 +- Update `actix-http` dependency to 2.0.0-beta.1 ## [2.0.0-alpha.2] - 2020-05-21 ### Changed -* Implement `std::error::Error` for our custom errors [#1422] -* Bump minimum supported Rust version to 1.40 -* Update `base64` dependency to 0.12 +- Implement `std::error::Error` for our custom errors [#1422] +- Bump minimum supported Rust version to 1.40 +- Update `base64` dependency to 0.12 [#1422]: https://github.com/actix/actix-web/pull/1422 ## [2.0.0-alpha.1] - 2020-03-11 -* Update `actix-http` dependency to 2.0.0-alpha.2 -* Update `rustls` dependency to 0.17 -* ClientBuilder accepts initial_window_size and initial_connection_window_size HTTP2 configuration -* ClientBuilder allowing to set max_http_version to limit HTTP version to be used +- Update `actix-http` dependency to 2.0.0-alpha.2 +- Update `rustls` dependency to 0.17 +- ClientBuilder accepts initial_window_size and initial_connection_window_size HTTP2 configuration +- ClientBuilder allowing to set max_http_version to limit HTTP version to be used ## [1.0.1] - 2019-12-15 -* Fix compilation with default features off +- Fix compilation with default features off ## [1.0.0] - 2019-12-13 -* Release +- Release ## [1.0.0-alpha.3] -* Migrate to `std::future` +- Migrate to `std::future` ## [0.2.8] - 2019-11-06 -* Add support for setting query from Serialize type for client request. +- Add support for setting query from Serialize type for client request. ## [0.2.7] - 2019-09-25 ### Added -* Remaining getter methods for `ClientRequest`'s private `head` field #1101 +- Remaining getter methods for `ClientRequest`'s private `head` field #1101 ## [0.2.6] - 2019-09-12 ### Added -* Export frozen request related types. +- Export frozen request related types. ## [0.2.5] - 2019-09-11 ### Added -* Add `FrozenClientRequest` to support retries for sending HTTP requests +- Add `FrozenClientRequest` to support retries for sending HTTP requests ### Changed -* Ensure that the `Host` header is set when initiating a WebSocket client connection. +- Ensure that the `Host` header is set when initiating a WebSocket client connection. ## [0.2.4] - 2019-08-13 ### Changed -* Update percent-encoding to "2.1" +- Update percent-encoding to "2.1" -* Update serde_urlencoded to "0.6.1" +- Update serde_urlencoded to "0.6.1" ## [0.2.3] - 2019-08-01 ### Added -* Add `rustls` support +- Add `rustls` support ## [0.2.2] - 2019-07-01 ### Changed -* Always append a colon after username in basic auth +- Always append a colon after username in basic auth -* Upgrade `rand` dependency version to 0.7 +- Upgrade `rand` dependency version to 0.7 ## [0.2.1] - 2019-06-05 ### Added -* Add license files +- Add license files ## [0.2.0] - 2019-05-12 ### Added -* Allow to send headers in `Camel-Case` form. +- Allow to send headers in `Camel-Case` form. ### Changed -* Upgrade actix-http dependency. +- Upgrade actix-http dependency. ## [0.1.1] - 2019-04-19 ### Added -* Allow to specify server address for http and ws requests. +- Allow to specify server address for http and ws requests. ### Changed -* `ClientRequest::if_true()` and `ClientRequest::if_some()` use instance instead of ref +- `ClientRequest::if_true()` and `ClientRequest::if_some()` use instance instead of ref ## [0.1.0] - 2019-04-16 -* No changes +- No changes ## [0.1.0-alpha.6] - 2019-04-14 ### Changed -* Do not set default headers for websocket request +- Do not set default headers for websocket request ## [0.1.0-alpha.5] - 2019-04-12 ### Changed -* Do not set any default headers +- Do not set any default headers ### Added -* Add Debug impl for BoxedSocket +- Add Debug impl for BoxedSocket ## [0.1.0-alpha.4] - 2019-04-08 ### Changed -* Update actix-http dependency +- Update actix-http dependency ## [0.1.0-alpha.3] - 2019-04-02 ### Added -* Export `MessageBody` type +- Export `MessageBody` type -* `ClientResponse::json()` - Loads and parse `application/json` encoded body +- `ClientResponse::json()` - Loads and parse `application/json` encoded body ### Changed -* `ClientRequest::json()` accepts reference instead of object. +- `ClientRequest::json()` accepts reference instead of object. -* `ClientResponse::body()` does not consume response object. +- `ClientResponse::body()` does not consume response object. -* Renamed `ClientRequest::close_connection()` to `ClientRequest::force_close()` +- Renamed `ClientRequest::close_connection()` to `ClientRequest::force_close()` ## [0.1.0-alpha.2] - 2019-03-29 ### Added -* Per request and session wide request timeout. +- Per request and session wide request timeout. -* Session wide headers. +- Session wide headers. -* Session wide basic and bearer auth. +- Session wide basic and bearer auth. -* Re-export `actix_http::client::Connector`. +- Re-export `actix_http::client::Connector`. ### Changed -* Allow to override request's uri +- Allow to override request's uri -* Export `ws` sub-module with websockets related types +- Export `ws` sub-module with websockets related types ## [0.1.0-alpha.1] - 2019-03-28 -* Initial impl +- Initial impl diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 836241d46..676a10895 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.12" +version = "3.0.0-beta.16" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -60,9 +60,9 @@ dangerous-h2c = [] [dependencies] actix-codec = "0.4.1" actix-service = "2.0.0" -actix-http = "3.0.0-beta.14" +actix-http = "3.0.0-beta.17" actix-rt = { version = "2.1", default-features = false } -actix-tls = { version = "3.0.0-rc.1", features = ["connect", "uri"] } +actix-tls = { version = "3.0.0", features = ["connect", "uri"] } actix-utils = "3.0.0" ahash = "0.7" @@ -70,11 +70,11 @@ base64 = "0.13" bytes = "1" cfg-if = "1" derive_more = "0.99.5" -futures-core = { version = "0.3.7", default-features = false } -futures-util = { version = "0.3.7", default-features = false } -h2 = "0.3" +futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } +futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] } +h2 = "0.3.9" http = "0.2.5" -itoa = "0.4" +itoa = "1" log =" 0.4" mime = "0.3" percent-encoding = "2.1" @@ -83,7 +83,7 @@ rand = "0.8" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7" -tokio = { version = "1", features = ["sync"] } +tokio = { version = "1.8.4", features = ["sync"] } cookie = { version = "0.15", features = ["percent-encode"], optional = true } @@ -93,13 +93,13 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] -actix-web = { version = "4.0.0-beta.11", features = ["openssl"] } -actix-http = { version = "3.0.0-beta.14", features = ["openssl"] } -actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] } +actix-http = { version = "3.0.0-beta.17", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] } +actix-server = "2.0.0-rc.2" +actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } +actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] } actix-utils = "3.0.0" -actix-server = "2.0.0-rc.1" -actix-tls = { version = "3.0.0-rc.1", features = ["openssl", "rustls"] } -actix-test = { version = "0.1.0-beta.7", features = ["openssl", "rustls"] } +actix-web = { version = "4.0.0-beta.17", features = ["openssl"] } brotli2 = "0.3.2" env_logger = "0.9" diff --git a/awc/README.md b/awc/README.md index b0faedc68..4916210e4 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,9 +3,9 @@ > Async HTTP and WebSocket client library. [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.12)](https://docs.rs/awc/3.0.0-beta.12) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.16)](https://docs.rs/awc/3.0.0-beta.16) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.12/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.12) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.16/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.16) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources diff --git a/awc/src/any_body.rs b/awc/src/any_body.rs index cb9038ff3..d09a943ab 100644 --- a/awc/src/any_body.rs +++ b/awc/src/any_body.rs @@ -1,17 +1,13 @@ use std::{ - borrow::Cow, fmt, mem, pin::Pin, task::{Context, Poll}, }; -use bytes::{Bytes, BytesMut}; -use futures_core::Stream; +use bytes::Bytes; use pin_project_lite::pin_project; -use actix_http::body::{BodySize, BodyStream, BoxBody, MessageBody, SizedStream}; - -use crate::BoxError; +use actix_http::body::{BodySize, BoxBody, MessageBody}; pin_project! { /// Represents various types of HTTP message body. @@ -45,9 +41,7 @@ impl AnyBody { where B: MessageBody + 'static, { - Self::Body { - body: BoxBody::new(body), - } + Self::Body { body: body.boxed() } } /// Constructs new `AnyBody` instance from a slice of bytes by copying it. @@ -79,10 +73,27 @@ impl AnyBody where B: MessageBody + 'static, { + /// Converts a [`MessageBody`] type into the best possible representation. + /// + /// Checks size for `None` and tries to convert to `Bytes`. Otherwise, uses the `Body` variant. + pub fn from_message_body(body: B) -> Self + where + B: MessageBody, + { + if matches!(body.size(), BodySize::None) { + return Self::None; + } + + match body.try_into_bytes() { + Ok(body) => Self::Bytes { body }, + Err(body) => Self::new(body), + } + } + pub fn into_boxed(self) -> AnyBody { match self { Self::None => AnyBody::None, - Self::Bytes { body: bytes } => AnyBody::Bytes { body: bytes }, + Self::Bytes { body } => AnyBody::Bytes { body }, Self::Body { body } => AnyBody::new_boxed(body), } } @@ -145,91 +156,6 @@ impl fmt::Debug for AnyBody { } } -impl From<&'static str> for AnyBody { - fn from(string: &'static str) -> Self { - Self::Bytes { - body: Bytes::from_static(string.as_ref()), - } - } -} - -impl From<&'static [u8]> for AnyBody { - fn from(bytes: &'static [u8]) -> Self { - Self::Bytes { - body: Bytes::from_static(bytes), - } - } -} - -impl From> for AnyBody { - fn from(vec: Vec) -> Self { - Self::Bytes { - body: Bytes::from(vec), - } - } -} - -impl From for AnyBody { - fn from(string: String) -> Self { - Self::Bytes { - body: Bytes::from(string), - } - } -} - -impl From<&'_ String> for AnyBody { - fn from(string: &String) -> Self { - Self::Bytes { - body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string)), - } - } -} - -impl From> for AnyBody { - fn from(string: Cow<'_, str>) -> Self { - match string { - Cow::Owned(s) => Self::from(s), - Cow::Borrowed(s) => Self::Bytes { - body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)), - }, - } - } -} - -impl From for AnyBody { - fn from(bytes: Bytes) -> Self { - Self::Bytes { body: bytes } - } -} - -impl From for AnyBody { - fn from(bytes: BytesMut) -> Self { - Self::Bytes { - body: bytes.freeze(), - } - } -} - -impl From> for AnyBody -where - S: Stream> + 'static, - E: Into + 'static, -{ - fn from(stream: SizedStream) -> Self { - AnyBody::new_boxed(stream) - } -} - -impl From> for AnyBody -where - S: Stream> + 'static, - E: Into + 'static, -{ - fn from(stream: BodyStream) -> Self { - AnyBody::new_boxed(stream) - } -} - #[cfg(test)] mod tests { use std::marker::PhantomPinned; diff --git a/awc/src/builder.rs b/awc/src/builder.rs index 43e5c0def..16a4e9cb5 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -2,18 +2,20 @@ use std::{convert::TryFrom, fmt, net::IpAddr, rc::Rc, time::Duration}; use actix_http::{ error::HttpError, - header::{self, HeaderMap, HeaderName}, + header::{self, HeaderMap, HeaderName, TryIntoHeaderPair}, Uri, }; use actix_rt::net::{ActixStream, TcpStream}; use actix_service::{boxed, Service}; use crate::{ - client::{ConnectInfo, Connector, ConnectorService, TcpConnectError, TcpConnection}, + client::{ + ClientConfig, ConnectInfo, Connector, ConnectorService, TcpConnectError, TcpConnection, + }, connect::DefaultConnector, error::SendRequestError, middleware::{NestTransform, Redirect, Transform}, - Client, ClientConfig, ConnectRequest, ConnectResponse, + Client, ConnectRequest, ConnectResponse, }; /// An HTTP Client builder @@ -21,11 +23,11 @@ use crate::{ /// This type can be used to construct an instance of `Client` through a /// builder-like pattern. pub struct ClientBuilder { - default_headers: bool, max_http_version: Option, stream_window_size: Option, conn_window_size: Option, - headers: HeaderMap, + fundamental_headers: bool, + default_headers: HeaderMap, timeout: Option, connector: Connector, middleware: M, @@ -44,15 +46,15 @@ impl ClientBuilder { (), > { ClientBuilder { - middleware: (), - default_headers: true, - headers: HeaderMap::new(), - timeout: Some(Duration::from_secs(5)), - local_address: None, - connector: Connector::new(), max_http_version: None, stream_window_size: None, conn_window_size: None, + fundamental_headers: true, + default_headers: HeaderMap::new(), + timeout: Some(Duration::from_secs(5)), + connector: Connector::new(), + middleware: (), + local_address: None, max_redirects: 10, } } @@ -78,8 +80,8 @@ where { ClientBuilder { middleware: self.middleware, + fundamental_headers: self.fundamental_headers, default_headers: self.default_headers, - headers: self.headers, timeout: self.timeout, local_address: self.local_address, connector, @@ -153,30 +155,46 @@ where self } - /// Do not add default request headers. + /// Do not add fundamental default request headers. + /// /// By default `Date` and `User-Agent` headers are set. pub fn no_default_headers(mut self) -> Self { - self.default_headers = false; + self.fundamental_headers = false; self } - /// Add default header. Headers added by this method - /// get added to every request. + /// Add default header. + /// + /// Headers added by this method get added to every request unless overriden by . + /// + /// # Panics + /// Panics if header name or value is invalid. + pub fn add_default_header(mut self, header: impl TryIntoHeaderPair) -> Self { + match header.try_into_pair() { + Ok((key, value)) => self.default_headers.append(key, value), + Err(err) => panic!("Header error: {:?}", err.into()), + } + + self + } + + #[doc(hidden)] + #[deprecated(since = "3.0.0", note = "Prefer `add_default_header((key, value))`.")] pub fn header(mut self, key: K, value: V) -> Self where HeaderName: TryFrom, >::Error: fmt::Debug + Into, - V: header::IntoHeaderValue, + V: header::TryIntoHeaderValue, V::Error: fmt::Debug, { match HeaderName::try_from(key) { Ok(key) => match value.try_into_value() { Ok(value) => { - self.headers.append(key, value); + self.default_headers.append(key, value); } - Err(e) => log::error!("Header value error: {:?}", e), + Err(err) => log::error!("Header value error: {:?}", err), }, - Err(e) => log::error!("Header name error: {:?}", e), + Err(err) => log::error!("Header name error: {:?}", err), } self } @@ -190,10 +208,10 @@ where Some(password) => format!("{}:{}", username, password), None => format!("{}:", username), }; - self.header( + self.add_default_header(( header::AUTHORIZATION, format!("Basic {}", base64::encode(&auth)), - ) + )) } /// Set client wide HTTP bearer authentication header @@ -201,13 +219,12 @@ where where T: fmt::Display, { - self.header(header::AUTHORIZATION, format!("Bearer {}", token)) + self.add_default_header((header::AUTHORIZATION, format!("Bearer {}", token))) } - /// 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 Client. + /// 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 `Client`. pub fn wrap( self, mw: M1, @@ -218,11 +235,11 @@ where { ClientBuilder { middleware: NestTransform::new(self.middleware, mw), - default_headers: self.default_headers, + fundamental_headers: self.fundamental_headers, max_http_version: self.max_http_version, stream_window_size: self.stream_window_size, conn_window_size: self.conn_window_size, - headers: self.headers, + default_headers: self.default_headers, timeout: self.timeout, connector: self.connector, local_address: self.local_address, @@ -237,10 +254,10 @@ where M::Transform: Service, { - let redirect_time = self.max_redirects; + let max_redirects = self.max_redirects; - if redirect_time > 0 { - self.wrap(Redirect::new().max_redirect_times(redirect_time)) + if max_redirects > 0 { + self.wrap(Redirect::new().max_redirect_times(max_redirects)) ._finish() } else { self._finish() @@ -272,7 +289,7 @@ where let connector = boxed::rc_service(self.middleware.new_transform(connector)); Client(ClientConfig { - headers: Rc::new(self.headers), + default_headers: Rc::new(self.default_headers), timeout: self.timeout, connector, }) @@ -288,7 +305,7 @@ mod tests { let client = ClientBuilder::new().basic_auth("username", Some("password")); assert_eq!( client - .headers + .default_headers .get(header::AUTHORIZATION) .unwrap() .to_str() @@ -299,7 +316,7 @@ mod tests { let client = ClientBuilder::new().basic_auth("username", None); assert_eq!( client - .headers + .default_headers .get(header::AUTHORIZATION) .unwrap() .to_str() @@ -313,7 +330,7 @@ mod tests { let client = ClientBuilder::new().bearer_auth("someS3cr3tAutht0k3n"); assert_eq!( client - .headers + .default_headers .get(header::AUTHORIZATION) .unwrap() .to_str() diff --git a/awc/src/client/connection.rs b/awc/src/client/connection.rs index 0e1f0bfec..456f119aa 100644 --- a/awc/src/client/connection.rs +++ b/awc/src/client/connection.rs @@ -267,7 +267,9 @@ where Connection::Tls(ConnectionType::H2(conn)) => { h2proto::send_request(conn, head.into(), body).await } - _ => unreachable!("Plain Tcp connection can be used only in Http1 protocol"), + _ => { + unreachable!("Plain TCP connection can be used only with HTTP/1.1 protocol") + } } }) } diff --git a/awc/src/client/connector.rs b/awc/src/client/connector.rs index 40b3c4d32..423f656a8 100644 --- a/awc/src/client/connector.rs +++ b/awc/src/client/connector.rs @@ -22,11 +22,13 @@ use futures_core::{future::LocalBoxFuture, ready}; use http::Uri; use pin_project_lite::pin_project; -use super::config::ConnectorConfig; -use super::connection::{Connection, ConnectionIo}; -use super::error::ConnectError; -use super::pool::ConnectionPool; -use super::Connect; +use super::{ + config::ConnectorConfig, + connection::{Connection, ConnectionIo}, + error::ConnectError, + pool::ConnectionPool, + Connect, +}; enum OurTlsConnector { #[allow(dead_code)] // only dead when no TLS feature is enabled @@ -35,6 +37,12 @@ enum OurTlsConnector { #[cfg(feature = "openssl")] Openssl(actix_tls::connect::openssl::reexports::SslConnector), + /// Provided because building the OpenSSL context on newer versions can be very slow. + /// This prevents unnecessary calls to `.build()` while constructing the client connector. + #[cfg(feature = "openssl")] + #[allow(dead_code)] // false positive; used in build_ssl + OpensslBuilder(actix_tls::connect::openssl::reexports::SslConnectorBuilder), + #[cfg(feature = "rustls")] Rustls(std::sync::Arc), } @@ -57,7 +65,7 @@ pub struct Connector { config: ConnectorConfig, #[allow(dead_code)] // only dead when no TLS feature is enabled - ssl: OurTlsConnector, + tls: OurTlsConnector, } impl Connector<()> { @@ -72,7 +80,7 @@ impl Connector<()> { Connector { connector: TcpConnector::new(resolver::resolver()).service(), config: ConnectorConfig::default(), - ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]), + tls: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]), } } @@ -116,7 +124,7 @@ impl Connector<()> { log::error!("Can not set ALPN protocol: {:?}", err); } - OurTlsConnector::Openssl(ssl.build()) + OurTlsConnector::OpensslBuilder(ssl) } } @@ -134,7 +142,7 @@ impl Connector { Connector { connector, config: self.config, - ssl: self.ssl, + tls: self.tls, } } } @@ -167,23 +175,35 @@ where self } + /// Use custom OpenSSL `SslConnector` instance. #[cfg(feature = "openssl")] - /// Use custom `SslConnector` instance. + pub fn openssl( + mut self, + connector: actix_tls::connect::openssl::reexports::SslConnector, + ) -> Self { + self.tls = OurTlsConnector::Openssl(connector); + self + } + + /// See docs for [`Connector::openssl`]. + #[doc(hidden)] + #[cfg(feature = "openssl")] + #[deprecated(since = "3.0.0", note = "Renamed to `Connector::openssl`.")] pub fn ssl( mut self, connector: actix_tls::connect::openssl::reexports::SslConnector, ) -> Self { - self.ssl = OurTlsConnector::Openssl(connector); + self.tls = OurTlsConnector::Openssl(connector); self } + /// Use custom Rustls `ClientConfig` instance. #[cfg(feature = "rustls")] - /// Use custom `ClientConfig` instance. pub fn rustls( mut self, connector: std::sync::Arc, ) -> Self { - self.ssl = OurTlsConnector::Rustls(connector); + self.tls = OurTlsConnector::Rustls(connector); self } @@ -198,7 +218,7 @@ where unimplemented!("actix-http client only supports versions http/1.1 & http/2") } }; - self.ssl = Connector::build_ssl(versions); + self.tls = Connector::build_ssl(versions); self } @@ -270,8 +290,8 @@ where } /// Finish configuration process and create connector service. - /// The Connector builder always concludes by calling `finish()` last in - /// its combinator chain. + /// + /// The `Connector` builder always concludes by calling `finish()` last in its combinator chain. pub fn finish(self) -> ConnectorService { let local_address = self.config.local_address; let timeout = self.config.timeout; @@ -284,7 +304,15 @@ where service: tcp_service_inner.clone(), }; - let tls_service = match self.ssl { + let tls = match self.tls { + #[cfg(feature = "openssl")] + OurTlsConnector::OpensslBuilder(builder) => { + OurTlsConnector::Openssl(builder.build()) + } + tls => tls, + }; + + let tls_service = match tls { OurTlsConnector::None => { #[cfg(not(feature = "dangerous-h2c"))] { @@ -374,6 +402,11 @@ where Some(actix_service::boxed::rc_service(tls_service)) } + #[cfg(feature = "openssl")] + OurTlsConnector::OpensslBuilder(_) => { + unreachable!("OpenSSL builder is built before this match."); + } + #[cfg(feature = "rustls")] OurTlsConnector::Rustls(tls) => { const H2: &[u8] = b"h2"; @@ -853,7 +886,7 @@ mod tests { let connector = Connector { connector: TcpConnector::new(resolver::resolver()).service(), config: ConnectorConfig::default(), - ssl: OurTlsConnector::None, + tls: OurTlsConnector::None, }; let client = Client::builder().connector(connector).finish(); diff --git a/awc/src/client/h1proto.rs b/awc/src/client/h1proto.rs index c8b9a3fae..cf716db72 100644 --- a/awc/src/client/h1proto.rs +++ b/awc/src/client/h1proto.rs @@ -9,20 +9,21 @@ use actix_http::{ body::{BodySize, MessageBody}, error::PayloadError, h1, - header::{HeaderMap, IntoHeaderValue, EXPECT, HOST}, + header::{HeaderMap, TryIntoHeaderValue, EXPECT, HOST}, Payload, RequestHeadType, ResponseHead, StatusCode, }; use actix_utils::future::poll_fn; -use bytes::buf::BufMut; -use bytes::{Bytes, BytesMut}; +use bytes::{buf::BufMut, Bytes, BytesMut}; use futures_core::{ready, Stream}; use futures_util::SinkExt as _; use pin_project_lite::pin_project; use crate::BoxError; -use super::connection::{ConnectionIo, H1Connection}; -use super::error::{ConnectError, SendRequestError}; +use super::{ + connection::{ConnectionIo, H1Connection}, + error::{ConnectError, SendRequestError}, +}; pub(crate) async fn send_request( io: H1Connection, @@ -123,7 +124,12 @@ where Ok((head, Payload::None)) } - _ => Ok((head, Payload::Stream(Box::pin(PlStream::new(framed))))), + _ => Ok(( + head, + Payload::Stream { + payload: Box::pin(PlStream::new(framed)), + }, + )), } } diff --git a/awc/src/client/h2proto.rs b/awc/src/client/h2proto.rs index 9ced5776b..709896ddd 100644 --- a/awc/src/client/h2proto.rs +++ b/awc/src/client/h2proto.rs @@ -52,9 +52,11 @@ where let _ = match length { BodySize::None => None, - BodySize::Sized(0) => req - .headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), + BodySize::Sized(0) => { + #[allow(clippy::declare_interior_mutable_const)] + const HV_ZERO: HeaderValue = HeaderValue::from_static("0"); + req.headers_mut().insert(CONTENT_LENGTH, HV_ZERO) + } BodySize::Sized(len) => { let mut buf = itoa::Buffer::new(); diff --git a/awc/src/client/mod.rs b/awc/src/client/mod.rs index 0d5c899bc..d5854d83e 100644 --- a/awc/src/client/mod.rs +++ b/awc/src/client/mod.rs @@ -1,6 +1,15 @@ //! HTTP client. -use http::Uri; +use std::{convert::TryFrom, rc::Rc, time::Duration}; + +use actix_http::{error::HttpError, header::HeaderMap, Method, RequestHead, Uri}; +use actix_rt::net::TcpStream; +use actix_service::Service; +pub use actix_tls::connect::{ + ConnectError as TcpConnectError, ConnectInfo, Connection as TcpConnection, +}; + +use crate::{ws, BoxConnectorService, ClientBuilder, ClientRequest}; mod config; mod connection; @@ -10,10 +19,6 @@ mod h1proto; mod h2proto; mod pool; -pub use actix_tls::connect::{ - ConnectError as TcpConnectError, ConnectInfo, Connection as TcpConnection, -}; - pub use self::connection::{Connection, ConnectionIo}; pub use self::connector::{Connector, ConnectorService}; pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; @@ -23,3 +28,176 @@ pub struct Connect { pub uri: Uri, pub addr: Option, } + +/// An asynchronous HTTP and WebSocket client. +/// +/// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU +/// and memory usage. +/// +/// # Examples +/// ``` +/// use awc::Client; +/// +/// #[actix_rt::main] +/// async fn main() { +/// let mut client = Client::default(); +/// +/// let res = client.get("http://www.rust-lang.org") +/// .insert_header(("User-Agent", "my-app/1.2")) +/// .send() +/// .await; +/// +/// println!("Response: {:?}", res); +/// } +/// ``` +#[derive(Clone)] +pub struct Client(pub(crate) ClientConfig); + +#[derive(Clone)] +pub(crate) struct ClientConfig { + pub(crate) connector: BoxConnectorService, + pub(crate) default_headers: Rc, + pub(crate) timeout: Option, +} + +impl Default for Client { + fn default() -> Self { + ClientBuilder::new().finish() + } +} + +impl Client { + /// Create new client instance with default settings. + pub fn new() -> Client { + Client::default() + } + + /// Create `Client` builder. + /// This function is equivalent of `ClientBuilder::new()`. + pub fn builder() -> ClientBuilder< + impl Service< + ConnectInfo, + Response = TcpConnection, + Error = TcpConnectError, + > + Clone, + > { + ClientBuilder::new() + } + + /// Construct HTTP request. + pub fn request(&self, method: Method, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + let mut req = ClientRequest::new(method, url, self.0.clone()); + + for header in self.0.default_headers.iter() { + // header map is empty + // TODO: probably append instead + req = req.insert_header_if_none(header); + } + req + } + + /// Create `ClientRequest` from `RequestHead` + /// + /// It is useful for proxy requests. This implementation + /// copies all headers and the method. + pub fn request_from(&self, url: U, head: &RequestHead) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + let mut req = self.request(head.method.clone(), url); + for header in head.headers.iter() { + req = req.insert_header_if_none(header); + } + req + } + + /// Construct HTTP *GET* request. + pub fn get(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::GET, url) + } + + /// Construct HTTP *HEAD* request. + pub fn head(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::HEAD, url) + } + + /// Construct HTTP *PUT* request. + pub fn put(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::PUT, url) + } + + /// Construct HTTP *POST* request. + pub fn post(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::POST, url) + } + + /// Construct HTTP *PATCH* request. + pub fn patch(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::PATCH, url) + } + + /// Construct HTTP *DELETE* request. + pub fn delete(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::DELETE, url) + } + + /// Construct HTTP *OPTIONS* request. + pub fn options(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::OPTIONS, url) + } + + /// Initialize a WebSocket connection. + /// Returns a WebSocket connection builder. + pub fn ws(&self, url: U) -> ws::WebsocketsRequest + where + Uri: TryFrom, + >::Error: Into, + { + let mut req = ws::WebsocketsRequest::new(url, self.0.clone()); + for (key, value) in self.0.default_headers.iter() { + req.head.headers.insert(key.clone(), value.clone()); + } + req + } + + /// Get default HeaderMap of Client. + /// + /// Returns Some(&mut HeaderMap) when Client object is unique + /// (No other clone of client exists at the same time). + pub fn headers(&mut self) -> Option<&mut HeaderMap> { + Rc::get_mut(&mut self.0.default_headers) + } +} diff --git a/awc/src/connect.rs b/awc/src/connect.rs index 19870b069..f93014a67 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -16,7 +16,7 @@ use crate::{ client::{ Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError, }, - response::ClientResponse, + ClientResponse, }; pub type BoxConnectorService = Rc< diff --git a/awc/src/error.rs b/awc/src/error.rs index c1d855053..aa9dc4d99 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -1,5 +1,6 @@ //! HTTP client errors +// TODO: figure out how best to expose http::Error vs actix_http::Error pub use actix_http::{ error::{HttpError, PayloadError}, header::HeaderValue, diff --git a/awc/src/frozen.rs b/awc/src/frozen.rs index 7497f85c8..4023bd1c8 100644 --- a/awc/src/frozen.rs +++ b/awc/src/frozen.rs @@ -1,22 +1,24 @@ -use std::{convert::TryFrom, net, rc::Rc, time::Duration}; +use std::{net, rc::Rc, time::Duration}; use bytes::Bytes; use futures_core::Stream; use serde::Serialize; use actix_http::{ + body::MessageBody, error::HttpError, - header::{HeaderMap, HeaderName, IntoHeaderValue}, + header::{HeaderMap, TryIntoHeaderPair}, Method, RequestHead, Uri, }; use crate::{ - any_body::AnyBody, + client::ClientConfig, sender::{RequestSender, SendClientRequest}, - BoxError, ClientConfig, + BoxError, }; /// `FrozenClientRequest` struct represents cloneable client request. +/// /// It could be used to send same request multiple times. #[derive(Clone)] pub struct FrozenClientRequest { @@ -46,7 +48,7 @@ impl FrozenClientRequest { /// Send a body. pub fn send_body(&self, body: B) -> SendClientRequest where - B: Into, + B: MessageBody + 'static, { RequestSender::Rc(self.head.clone(), None).send_body( self.addr, @@ -82,7 +84,7 @@ impl FrozenClientRequest { /// Send a streaming body. pub fn send_stream(&self, stream: S) -> SendClientRequest where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, E: Into + 'static, { RequestSender::Rc(self.head.clone(), None).send_stream( @@ -104,20 +106,14 @@ impl FrozenClientRequest { ) } - /// Create a `FrozenSendBuilder` with extra headers + /// Clones this `FrozenClientRequest`, returning a new one with extra headers added. pub fn extra_headers(&self, extra_headers: HeaderMap) -> FrozenSendBuilder { FrozenSendBuilder::new(self.clone(), extra_headers) } - /// Create a `FrozenSendBuilder` with an extra header - pub fn extra_header(&self, key: K, value: V) -> FrozenSendBuilder - where - HeaderName: TryFrom, - >::Error: Into, - V: IntoHeaderValue, - { - self.extra_headers(HeaderMap::new()) - .extra_header(key, value) + /// Clones this `FrozenClientRequest`, returning a new one with the extra header added. + pub fn extra_header(&self, header: impl TryIntoHeaderPair) -> FrozenSendBuilder { + self.extra_headers(HeaderMap::new()).extra_header(header) } } @@ -138,29 +134,20 @@ impl FrozenSendBuilder { } /// Insert a header, it overrides existing header in `FrozenClientRequest`. - pub fn extra_header(mut self, key: K, value: V) -> Self - where - HeaderName: TryFrom, - >::Error: Into, - V: IntoHeaderValue, - { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into_value() { - Ok(value) => { - self.extra_headers.insert(key, value); - } - Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), + pub fn extra_header(mut self, header: impl TryIntoHeaderPair) -> Self { + match header.try_into_pair() { + Ok((key, value)) => { + self.extra_headers.insert(key, value); + } + + Err(err) => self.err = Some(err.into()), } + self } /// Complete request construction and send a body. - pub fn send_body(self, body: B) -> SendClientRequest - where - B: Into, - { + pub fn send_body(self, body: impl MessageBody + 'static) -> SendClientRequest { if let Some(e) = self.err { return e.into(); } @@ -175,9 +162,9 @@ impl FrozenSendBuilder { } /// Complete request construction and send a json body. - pub fn send_json(self, value: &T) -> SendClientRequest { - if let Some(e) = self.err { - return e.into(); + pub fn send_json(self, value: impl Serialize) -> SendClientRequest { + if let Some(err) = self.err { + return err.into(); } RequestSender::Rc(self.req.head, Some(self.extra_headers)).send_json( @@ -190,7 +177,7 @@ impl FrozenSendBuilder { } /// Complete request construction and send an urlencoded body. - pub fn send_form(self, value: &T) -> SendClientRequest { + pub fn send_form(self, value: impl Serialize) -> SendClientRequest { if let Some(e) = self.err { return e.into(); } @@ -207,7 +194,7 @@ impl FrozenSendBuilder { /// Complete request construction and send a streaming body. pub fn send_stream(self, stream: S) -> SendClientRequest where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, E: Into + 'static, { if let Some(e) = self.err { diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 0cb6c7f4f..970ca2d92 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -95,7 +95,8 @@ //! # } //! ``` -#![deny(rust_2018_idioms)] +#![deny(rust_2018_idioms, nonstandard_style)] +#![warn(future_incompatible)] #![allow( clippy::type_complexity, clippy::borrow_interior_mutable_const, @@ -104,6 +105,11 @@ #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] +pub use actix_http::body; + +#[cfg(feature = "cookies")] +pub use cookie; + mod any_body; mod builder; mod client; @@ -112,201 +118,27 @@ pub mod error; mod frozen; pub mod middleware; mod request; -mod response; +mod responses; mod sender; pub mod test; pub mod ws; -// TODO: hmmmmmm -pub use actix_http as http; -#[cfg(feature = "cookies")] -pub use cookie; +pub mod http { + //! Various HTTP related types. + + // TODO: figure out how best to expose http::Error vs actix_http::Error + pub use actix_http::{ + header, uri, ConnectionType, Error, Method, StatusCode, Uri, Version, + }; +} pub use self::builder::ClientBuilder; -pub use self::client::Connector; +pub use self::client::{Client, Connector}; pub use self::connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse}; pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder}; pub use self::request::ClientRequest; -pub use self::response::{ClientResponse, JsonBody, MessageBody}; +#[allow(deprecated)] +pub use self::responses::{ClientResponse, JsonBody, MessageBody, ResponseBody}; pub use self::sender::SendClientRequest; -use std::{convert::TryFrom, rc::Rc, time::Duration}; - -use actix_http::{error::HttpError, header::HeaderMap, Method, RequestHead, Uri}; -use actix_rt::net::TcpStream; -use actix_service::Service; - -use self::client::{ConnectInfo, TcpConnectError, TcpConnection}; - pub(crate) type BoxError = Box; - -/// An asynchronous HTTP and WebSocket client. -/// -/// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU -/// and memory usage. -/// -/// # Examples -/// ``` -/// use awc::Client; -/// -/// #[actix_rt::main] -/// async fn main() { -/// let mut client = Client::default(); -/// -/// let res = client.get("http://www.rust-lang.org") -/// .insert_header(("User-Agent", "my-app/1.2")) -/// .send() -/// .await; -/// -/// println!("Response: {:?}", res); -/// } -/// ``` -#[derive(Clone)] -pub struct Client(ClientConfig); - -#[derive(Clone)] -pub(crate) struct ClientConfig { - pub(crate) connector: BoxConnectorService, - pub(crate) headers: Rc, - pub(crate) timeout: Option, -} - -impl Default for Client { - fn default() -> Self { - ClientBuilder::new().finish() - } -} - -impl Client { - /// Create new client instance with default settings. - pub fn new() -> Client { - Client::default() - } - - /// Create `Client` builder. - /// This function is equivalent of `ClientBuilder::new()`. - pub fn builder() -> ClientBuilder< - impl Service< - ConnectInfo, - Response = TcpConnection, - Error = TcpConnectError, - > + Clone, - > { - ClientBuilder::new() - } - - /// Construct HTTP request. - pub fn request(&self, method: Method, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - let mut req = ClientRequest::new(method, url, self.0.clone()); - - for header in self.0.headers.iter() { - req = req.insert_header_if_none(header); - } - req - } - - /// Create `ClientRequest` from `RequestHead` - /// - /// It is useful for proxy requests. This implementation - /// copies all headers and the method. - pub fn request_from(&self, url: U, head: &RequestHead) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - let mut req = self.request(head.method.clone(), url); - for header in head.headers.iter() { - req = req.insert_header_if_none(header); - } - req - } - - /// Construct HTTP *GET* request. - pub fn get(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::GET, url) - } - - /// Construct HTTP *HEAD* request. - pub fn head(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::HEAD, url) - } - - /// Construct HTTP *PUT* request. - pub fn put(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::PUT, url) - } - - /// Construct HTTP *POST* request. - pub fn post(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::POST, url) - } - - /// Construct HTTP *PATCH* request. - pub fn patch(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::PATCH, url) - } - - /// Construct HTTP *DELETE* request. - pub fn delete(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::DELETE, url) - } - - /// Construct HTTP *OPTIONS* request. - pub fn options(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::OPTIONS, url) - } - - /// Initialize a WebSocket connection. - /// Returns a WebSocket connection builder. - pub fn ws(&self, url: U) -> ws::WebsocketsRequest - where - Uri: TryFrom, - >::Error: Into, - { - let mut req = ws::WebsocketsRequest::new(url, self.0.clone()); - for (key, value) in self.0.headers.iter() { - req.head.headers.insert(key.clone(), value.clone()); - } - req - } - - /// Get default HeaderMap of Client. - /// - /// Returns Some(&mut HeaderMap) when Client object is unique - /// (No other clone of client exists at the same time). - pub fn headers(&mut self) -> Option<&mut HeaderMap> { - Rc::get_mut(&mut self.0.headers) - } -} diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index 0fde48074..704d2d79d 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -442,13 +442,15 @@ mod tests { }); let client = ClientBuilder::new() - .header("custom", "value") + .add_default_header(("custom", "value")) .disable_redirects() .finish(); let res = client.get(srv.url("/")).send().await.unwrap(); assert_eq!(res.status().as_u16(), 302); - let client = ClientBuilder::new().header("custom", "value").finish(); + let client = ClientBuilder::new() + .add_default_header(("custom", "value")) + .finish(); let res = client.get(srv.url("/")).send().await.unwrap(); assert_eq!(res.status().as_u16(), 200); @@ -520,7 +522,7 @@ mod tests { // send a request to different origins, http://srv1/ then http://srv2/. So it should remove the header let client = ClientBuilder::new() - .header(header::AUTHORIZATION, "auth_key_value") + .add_default_header((header::AUTHORIZATION, "auth_key_value")) .finish(); let res = client.get(srv1.url("/")).send().await.unwrap(); assert_eq!(res.status().as_u16(), 200); diff --git a/awc/src/request.rs b/awc/src/request.rs index 3e1f83a82..8bcf1ee01 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -5,17 +5,18 @@ use futures_core::Stream; use serde::Serialize; use actix_http::{ + body::MessageBody, error::HttpError, - header::{self, HeaderMap, HeaderValue, IntoHeaderPair}, + header::{self, HeaderMap, HeaderValue, TryIntoHeaderPair}, ConnectionType, Method, RequestHead, Uri, Version, }; use crate::{ - any_body::AnyBody, + client::ClientConfig, error::{FreezeRequestError, InvalidUrl}, frozen::FrozenClientRequest, sender::{PrepForSendingError, RequestSender, SendClientRequest}, - BoxError, ClientConfig, + BoxError, }; #[cfg(feature = "cookies")] @@ -26,20 +27,20 @@ use crate::cookie::{Cookie, CookieJar}; /// This type can be used to construct an instance of `ClientRequest` through a /// builder-like pattern. /// -/// ``` -/// #[actix_rt::main] -/// async fn main() { -/// let response = awc::Client::new() -/// .get("http://www.rust-lang.org") // <- Create request builder -/// .insert_header(("User-Agent", "Actix-web")) -/// .send() // <- Send HTTP request -/// .await; +/// ```no_run +/// # #[actix_rt::main] +/// # async fn main() { +/// let response = awc::Client::new() +/// .get("http://www.rust-lang.org") // <- Create request builder +/// .insert_header(("User-Agent", "Actix-web")) +/// .send() // <- Send HTTP request +/// .await; /// -/// response.and_then(|response| { // <- server HTTP response -/// println!("Response: {:?}", response); -/// Ok(()) -/// }); -/// } +/// response.and_then(|response| { // <- server HTTP response +/// println!("Response: {:?}", response); +/// Ok(()) +/// }); +/// # } /// ``` pub struct ClientRequest { pub(crate) head: RequestHead, @@ -147,11 +148,8 @@ impl ClientRequest { } /// Insert a header, replacing any that were set with an equivalent field name. - pub fn insert_header(mut self, header: H) -> Self - where - H: IntoHeaderPair, - { - match header.try_into_header_pair() { + pub fn insert_header(mut self, header: impl TryIntoHeaderPair) -> Self { + match header.try_into_pair() { Ok((key, value)) => { self.head.headers.insert(key, value); } @@ -162,11 +160,8 @@ impl ClientRequest { } /// Insert a header only if it is not yet set. - pub fn insert_header_if_none(mut self, header: H) -> Self - where - H: IntoHeaderPair, - { - match header.try_into_header_pair() { + pub fn insert_header_if_none(mut self, header: impl TryIntoHeaderPair) -> Self { + match header.try_into_pair() { Ok((key, value)) => { if !self.head.headers.contains_key(&key) { self.head.headers.insert(key, value); @@ -180,23 +175,16 @@ impl ClientRequest { /// Append a header, keeping any that were set with an equivalent field name. /// - /// ``` - /// # #[actix_rt::main] - /// # async fn main() { - /// # use awc::Client; - /// use awc::http::header::CONTENT_TYPE; + /// ```no_run + /// use awc::{http::header, Client}; /// /// Client::new() /// .get("http://www.rust-lang.org") /// .insert_header(("X-TEST", "value")) - /// .insert_header((CONTENT_TYPE, mime::APPLICATION_JSON)); - /// # } + /// .insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)); /// ``` - pub fn append_header(mut self, header: H) -> Self - where - H: IntoHeaderPair, - { - match header.try_into_header_pair() { + pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self { + match header.try_into_pair() { Ok((key, value)) => self.head.headers.append(key, value), Err(e) => self.err = Some(e.into()), }; @@ -261,23 +249,18 @@ impl ClientRequest { /// Set a cookie /// - /// ``` - /// #[actix_rt::main] - /// async fn main() { - /// let resp = awc::Client::new().get("https://www.rust-lang.org") - /// .cookie( - /// awc::cookie::Cookie::build("name", "value") - /// .domain("www.rust-lang.org") - /// .path("/") - /// .secure(true) - /// .http_only(true) - /// .finish(), - /// ) - /// .send() - /// .await; + /// ```no_run + /// use awc::{cookie::Cookie, Client}; /// - /// println!("Response: {:?}", resp); - /// } + /// # #[actix_rt::main] + /// # async fn main() { + /// let res = Client::new().get("https://httpbin.org/cookies") + /// .cookie(Cookie::new("name", "value")) + /// .send() + /// .await; + /// + /// println!("Response: {:?}", res); + /// # } /// ``` #[cfg(feature = "cookies")] pub fn cookie(mut self, cookie: Cookie<'_>) -> Self { @@ -349,7 +332,7 @@ impl ClientRequest { /// Complete request construction and send body. pub fn send_body(self, body: B) -> SendClientRequest where - B: Into, + B: MessageBody + 'static, { let slf = match self.prep_for_sending() { Ok(slf) => slf, @@ -402,7 +385,7 @@ impl ClientRequest { /// Set an streaming body and generate `ClientRequest`. pub fn send_stream(self, stream: S) -> SendClientRequest where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, E: Into + 'static, { let slf = match self.prep_for_sending() { @@ -588,7 +571,7 @@ mod tests { #[actix_rt::test] async fn test_client_header() { let req = Client::builder() - .header(header::CONTENT_TYPE, "111") + .add_default_header((header::CONTENT_TYPE, "111")) .finish() .get("/"); @@ -606,7 +589,7 @@ mod tests { #[actix_rt::test] async fn test_client_header_override() { let req = Client::builder() - .header(header::CONTENT_TYPE, "111") + .add_default_header((header::CONTENT_TYPE, "111")) .finish() .get("/") .insert_header((header::CONTENT_TYPE, "222")); diff --git a/awc/src/response.rs b/awc/src/response.rs deleted file mode 100644 index fefebd0a0..000000000 --- a/awc/src/response.rs +++ /dev/null @@ -1,556 +0,0 @@ -use std::{ - cell::{Ref, RefMut}, - fmt, - future::Future, - io, - marker::PhantomData, - pin::Pin, - task::{Context, Poll}, - time::{Duration, Instant}, -}; - -use actix_http::{ - error::PayloadError, header, header::HeaderMap, Extensions, HttpMessage, Payload, - PayloadStream, ResponseHead, StatusCode, Version, -}; -use actix_rt::time::{sleep, Sleep}; -use bytes::{Bytes, BytesMut}; -use futures_core::{ready, Stream}; -use serde::de::DeserializeOwned; - -#[cfg(feature = "cookies")] -use crate::cookie::{Cookie, ParseError as CookieParseError}; -use crate::error::JsonPayloadError; - -/// Client Response -pub struct ClientResponse { - pub(crate) head: ResponseHead, - pub(crate) payload: Payload, - pub(crate) timeout: ResponseTimeout, -} - -/// helper enum with reusable sleep passed from `SendClientResponse`. -/// See `ClientResponse::_timeout` for reason. -pub(crate) enum ResponseTimeout { - Disabled(Option>>), - Enabled(Pin>), -} - -impl Default for ResponseTimeout { - fn default() -> Self { - Self::Disabled(None) - } -} - -impl ResponseTimeout { - fn poll_timeout(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> { - match *self { - Self::Enabled(ref mut timeout) => { - if timeout.as_mut().poll(cx).is_ready() { - Err(PayloadError::Io(io::Error::new( - io::ErrorKind::TimedOut, - "Response Payload IO timed out", - ))) - } else { - Ok(()) - } - } - Self::Disabled(_) => Ok(()), - } - } -} - -impl HttpMessage for ClientResponse { - type Stream = S; - - fn headers(&self) -> &HeaderMap { - &self.head.headers - } - - fn take_payload(&mut self) -> Payload { - std::mem::replace(&mut self.payload, Payload::None) - } - - fn extensions(&self) -> Ref<'_, Extensions> { - self.head.extensions() - } - - fn extensions_mut(&self) -> RefMut<'_, Extensions> { - self.head.extensions_mut() - } -} - -impl ClientResponse { - /// Create new Request instance - pub(crate) fn new(head: ResponseHead, payload: Payload) -> Self { - ClientResponse { - head, - payload, - timeout: ResponseTimeout::default(), - } - } - - #[inline] - pub(crate) fn head(&self) -> &ResponseHead { - &self.head - } - - /// Read the Request Version. - #[inline] - pub fn version(&self) -> Version { - self.head().version - } - - /// Get the status from the server. - #[inline] - pub fn status(&self) -> StatusCode { - self.head().status - } - - #[inline] - /// Returns request's headers. - pub fn headers(&self) -> &HeaderMap { - &self.head().headers - } - - /// Set a body and return previous body value - pub fn map_body(mut self, f: F) -> ClientResponse - where - F: FnOnce(&mut ResponseHead, Payload) -> Payload, - { - let payload = f(&mut self.head, self.payload); - - ClientResponse { - payload, - head: self.head, - timeout: self.timeout, - } - } - - /// Set a timeout duration for [`ClientResponse`](self::ClientResponse). - /// - /// This duration covers the duration of processing the response body stream - /// and would end it as timeout error when deadline met. - /// - /// Disabled by default. - pub fn timeout(self, dur: Duration) -> Self { - let timeout = match self.timeout { - ResponseTimeout::Disabled(Some(mut timeout)) - | ResponseTimeout::Enabled(mut timeout) => match Instant::now().checked_add(dur) { - Some(deadline) => { - timeout.as_mut().reset(deadline.into()); - ResponseTimeout::Enabled(timeout) - } - None => ResponseTimeout::Enabled(Box::pin(sleep(dur))), - }, - _ => ResponseTimeout::Enabled(Box::pin(sleep(dur))), - }; - - Self { - payload: self.payload, - head: self.head, - timeout, - } - } - - /// This method does not enable timeout. It's used to pass the boxed `Sleep` from - /// `SendClientRequest` and reuse it's heap allocation together with it's slot in - /// timer wheel. - pub(crate) fn _timeout(mut self, timeout: Option>>) -> Self { - self.timeout = ResponseTimeout::Disabled(timeout); - self - } - - /// Load request cookies. - #[cfg(feature = "cookies")] - pub fn cookies(&self) -> Result>>, CookieParseError> { - struct Cookies(Vec>); - - if self.extensions().get::().is_none() { - let mut cookies = Vec::new(); - for hdr in self.headers().get_all(&header::SET_COOKIE) { - let s = std::str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; - cookies.push(Cookie::parse_encoded(s)?.into_owned()); - } - self.extensions_mut().insert(Cookies(cookies)); - } - Ok(Ref::map(self.extensions(), |ext| { - &ext.get::().unwrap().0 - })) - } - - /// Return request cookie. - #[cfg(feature = "cookies")] - pub fn cookie(&self, name: &str) -> Option> { - if let Ok(cookies) = self.cookies() { - for cookie in cookies.iter() { - if cookie.name() == name { - return Some(cookie.to_owned()); - } - } - } - None - } -} - -impl ClientResponse -where - S: Stream>, -{ - /// Loads HTTP response's body. - pub fn body(&mut self) -> MessageBody { - MessageBody::new(self) - } - - /// Loads and parse `application/json` encoded body. - /// Return `JsonBody` future. It resolves to a `T` value. - /// - /// Returns error: - /// - /// * content type is not `application/json` - /// * content length is greater than 256k - pub fn json(&mut self) -> JsonBody { - JsonBody::new(self) - } -} - -impl Stream for ClientResponse -where - S: Stream> + Unpin, -{ - type Item = Result; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.get_mut(); - this.timeout.poll_timeout(cx)?; - - Pin::new(&mut this.payload).poll_next(cx) - } -} - -impl fmt::Debug for ClientResponse { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status(),)?; - writeln!(f, " headers:")?; - for (key, val) in self.headers().iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -const DEFAULT_BODY_LIMIT: usize = 2 * 1024 * 1024; - -/// Future that resolves to a complete HTTP message body. -pub struct MessageBody { - length: Option, - timeout: ResponseTimeout, - body: Result, Option>, -} - -impl MessageBody -where - S: Stream>, -{ - /// Create `MessageBody` for request. - pub fn new(res: &mut ClientResponse) -> MessageBody { - let length = match res.headers().get(&header::CONTENT_LENGTH) { - Some(value) => { - let len = value.to_str().ok().and_then(|s| s.parse::().ok()); - - match len { - None => return Self::err(PayloadError::UnknownLength), - len => len, - } - } - None => None, - }; - - MessageBody { - length, - timeout: std::mem::take(&mut res.timeout), - body: Ok(ReadBody::new(res.take_payload(), DEFAULT_BODY_LIMIT)), - } - } - - /// Change max size of payload. By default max size is 2048kB - pub fn limit(mut self, limit: usize) -> Self { - if let Ok(ref mut body) = self.body { - body.limit = limit; - } - self - } - - fn err(e: PayloadError) -> Self { - MessageBody { - length: None, - timeout: ResponseTimeout::default(), - body: Err(Some(e)), - } - } -} - -impl Future for MessageBody -where - S: Stream> + Unpin, -{ - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.get_mut(); - - match this.body { - Err(ref mut err) => Poll::Ready(Err(err.take().unwrap())), - Ok(ref mut body) => { - if let Some(len) = this.length.take() { - if len > body.limit { - return Poll::Ready(Err(PayloadError::Overflow)); - } - } - - this.timeout.poll_timeout(cx)?; - - Pin::new(body).poll(cx) - } - } - } -} - -/// Response's payload json parser, it resolves to a deserialized `T` value. -/// -/// Returns error: -/// -/// * content type is not `application/json` -/// * content length is greater than 64k -pub struct JsonBody { - length: Option, - err: Option, - timeout: ResponseTimeout, - fut: Option>, - _phantom: PhantomData, -} - -impl JsonBody -where - S: Stream>, - U: DeserializeOwned, -{ - /// Create `JsonBody` for request. - pub fn new(res: &mut ClientResponse) -> Self { - // check content-type - let json = if let Ok(Some(mime)) = res.mime_type() { - mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) - } else { - false - }; - if !json { - return JsonBody { - length: None, - fut: None, - timeout: ResponseTimeout::default(), - err: Some(JsonPayloadError::ContentType), - _phantom: PhantomData, - }; - } - - let mut len = None; - - if let Some(l) = res.headers().get(&header::CONTENT_LENGTH) { - if let Ok(s) = l.to_str() { - if let Ok(l) = s.parse::() { - len = Some(l) - } - } - } - - JsonBody { - length: len, - err: None, - timeout: std::mem::take(&mut res.timeout), - fut: Some(ReadBody::new(res.take_payload(), 65536)), - _phantom: PhantomData, - } - } - - /// Change max size of payload. By default max size is 64kB - pub fn limit(mut self, limit: usize) -> Self { - if let Some(ref mut fut) = self.fut { - fut.limit = limit; - } - self - } -} - -impl Unpin for JsonBody -where - T: Stream> + Unpin, - U: DeserializeOwned, -{ -} - -impl Future for JsonBody -where - T: Stream> + Unpin, - U: DeserializeOwned, -{ - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - if let Some(err) = self.err.take() { - return Poll::Ready(Err(err)); - } - - if let Some(len) = self.length.take() { - if len > self.fut.as_ref().unwrap().limit { - return Poll::Ready(Err(JsonPayloadError::Payload(PayloadError::Overflow))); - } - } - - self.timeout - .poll_timeout(cx) - .map_err(JsonPayloadError::Payload)?; - - let body = ready!(Pin::new(&mut self.get_mut().fut.as_mut().unwrap()).poll(cx))?; - Poll::Ready(serde_json::from_slice::(&body).map_err(JsonPayloadError::from)) - } -} - -struct ReadBody { - stream: Payload, - buf: BytesMut, - limit: usize, -} - -impl ReadBody { - fn new(stream: Payload, limit: usize) -> Self { - Self { - stream, - buf: BytesMut::new(), - limit, - } - } -} - -impl Future for ReadBody -where - S: Stream> + Unpin, -{ - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.get_mut(); - - while let Some(chunk) = ready!(Pin::new(&mut this.stream).poll_next(cx)?) { - if (this.buf.len() + chunk.len()) > this.limit { - return Poll::Ready(Err(PayloadError::Overflow)); - } - this.buf.extend_from_slice(&chunk); - } - - Poll::Ready(Ok(this.buf.split().freeze())) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde::{Deserialize, Serialize}; - - use crate::{http::header, test::TestResponse}; - - #[actix_rt::test] - async fn test_body() { - let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "xxxx")).finish(); - match req.body().await.err().unwrap() { - PayloadError::UnknownLength => {} - _ => unreachable!("error"), - } - - let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "10000000")).finish(); - match req.body().await.err().unwrap() { - PayloadError::Overflow => {} - _ => unreachable!("error"), - } - - let mut req = TestResponse::default() - .set_payload(Bytes::from_static(b"test")) - .finish(); - assert_eq!(req.body().await.ok().unwrap(), Bytes::from_static(b"test")); - - let mut req = TestResponse::default() - .set_payload(Bytes::from_static(b"11111111111111")) - .finish(); - match req.body().limit(5).await.err().unwrap() { - PayloadError::Overflow => {} - _ => unreachable!("error"), - } - } - - #[derive(Serialize, Deserialize, PartialEq, Debug)] - struct MyObject { - name: String, - } - - fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { - match err { - JsonPayloadError::Payload(PayloadError::Overflow) => { - matches!(other, JsonPayloadError::Payload(PayloadError::Overflow)) - } - JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), - _ => false, - } - } - - #[actix_rt::test] - async fn test_json_body() { - let mut req = TestResponse::default().finish(); - let json = JsonBody::<_, MyObject>::new(&mut req).await; - assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); - - let mut req = TestResponse::default() - .insert_header(( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/text"), - )) - .finish(); - let json = JsonBody::<_, MyObject>::new(&mut req).await; - assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); - - let mut req = TestResponse::default() - .insert_header(( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - )) - .insert_header(( - header::CONTENT_LENGTH, - header::HeaderValue::from_static("10000"), - )) - .finish(); - - let json = JsonBody::<_, MyObject>::new(&mut req).limit(100).await; - assert!(json_eq( - json.err().unwrap(), - JsonPayloadError::Payload(PayloadError::Overflow) - )); - - let mut req = TestResponse::default() - .insert_header(( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - )) - .insert_header(( - header::CONTENT_LENGTH, - header::HeaderValue::from_static("16"), - )) - .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) - .finish(); - - let json = JsonBody::<_, MyObject>::new(&mut req).await; - assert_eq!( - json.ok().unwrap(), - MyObject { - name: "test".to_owned() - } - ); - } -} diff --git a/awc/src/responses/json_body.rs b/awc/src/responses/json_body.rs new file mode 100644 index 000000000..3912324b6 --- /dev/null +++ b/awc/src/responses/json_body.rs @@ -0,0 +1,192 @@ +use std::{ + future::Future, + marker::PhantomData, + mem, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_http::{error::PayloadError, header, HttpMessage}; +use bytes::Bytes; +use futures_core::{ready, Stream}; +use pin_project_lite::pin_project; +use serde::de::DeserializeOwned; + +use super::{read_body::ReadBody, ResponseTimeout, DEFAULT_BODY_LIMIT}; +use crate::{error::JsonPayloadError, ClientResponse}; + +pin_project! { + /// A `Future` that reads a body stream, parses JSON, resolving to a deserialized `T`. + /// + /// # Errors + /// `Future` implementation returns error if: + /// - content type is not `application/json`; + /// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB). + pub struct JsonBody { + #[pin] + body: Option>, + length: Option, + timeout: ResponseTimeout, + err: Option, + _phantom: PhantomData, + } +} + +impl JsonBody +where + S: Stream>, + T: DeserializeOwned, +{ + /// Creates a JSON body stream reader from a response by taking its payload. + pub fn new(res: &mut ClientResponse) -> Self { + // check content-type + let json = if let Ok(Some(mime)) = res.mime_type() { + mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) + } else { + false + }; + + if !json { + return JsonBody { + length: None, + body: None, + timeout: ResponseTimeout::default(), + err: Some(JsonPayloadError::ContentType), + _phantom: PhantomData, + }; + } + + let length = res + .headers() + .get(&header::CONTENT_LENGTH) + .and_then(|len_hdr| len_hdr.to_str().ok()) + .and_then(|len_str| len_str.parse::().ok()); + + JsonBody { + body: Some(ReadBody::new(res.take_payload(), DEFAULT_BODY_LIMIT)), + length, + timeout: mem::take(&mut res.timeout), + err: None, + _phantom: PhantomData, + } + } + + /// Change max size of payload. Default limit is 2 MiB. + pub fn limit(mut self, limit: usize) -> Self { + if let Some(ref mut fut) = self.body { + fut.limit = limit; + } + + self + } +} + +impl Future for JsonBody +where + S: Stream>, + T: DeserializeOwned, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + + if let Some(err) = this.err.take() { + return Poll::Ready(Err(err)); + } + + if let Some(len) = this.length.take() { + let body = Option::as_ref(&this.body).unwrap(); + if len > body.limit { + return Poll::Ready(Err(JsonPayloadError::Payload(PayloadError::Overflow))); + } + } + + this.timeout + .poll_timeout(cx) + .map_err(JsonPayloadError::Payload)?; + + let body = ready!(this.body.as_pin_mut().unwrap().poll(cx))?; + Poll::Ready(serde_json::from_slice::(&body).map_err(JsonPayloadError::from)) + } +} + +#[cfg(test)] +mod tests { + use actix_http::BoxedPayloadStream; + use serde::{Deserialize, Serialize}; + use static_assertions::assert_impl_all; + + use super::*; + use crate::{http::header, test::TestResponse}; + + assert_impl_all!(JsonBody: Unpin); + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct MyObject { + name: String, + } + + fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { + match err { + JsonPayloadError::Payload(PayloadError::Overflow) => { + matches!(other, JsonPayloadError::Payload(PayloadError::Overflow)) + } + JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), + _ => false, + } + } + + #[actix_rt::test] + async fn read_json_body() { + let mut req = TestResponse::default().finish(); + let json = JsonBody::<_, MyObject>::new(&mut req).await; + assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); + + let mut req = TestResponse::default() + .insert_header(( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/text"), + )) + .finish(); + let json = JsonBody::<_, MyObject>::new(&mut req).await; + assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); + + let mut req = TestResponse::default() + .insert_header(( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + )) + .insert_header(( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("10000"), + )) + .finish(); + + let json = JsonBody::<_, MyObject>::new(&mut req).limit(100).await; + assert!(json_eq( + json.err().unwrap(), + JsonPayloadError::Payload(PayloadError::Overflow) + )); + + let mut req = TestResponse::default() + .insert_header(( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + )) + .insert_header(( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + )) + .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .finish(); + + let json = JsonBody::<_, MyObject>::new(&mut req).await; + assert_eq!( + json.ok().unwrap(), + MyObject { + name: "test".to_owned() + } + ); + } +} diff --git a/awc/src/responses/mod.rs b/awc/src/responses/mod.rs new file mode 100644 index 000000000..588ce014c --- /dev/null +++ b/awc/src/responses/mod.rs @@ -0,0 +1,49 @@ +use std::{future::Future, io, pin::Pin, task::Context}; + +use actix_http::error::PayloadError; +use actix_rt::time::Sleep; + +mod json_body; +mod read_body; +mod response; +mod response_body; + +pub use self::json_body::JsonBody; +pub use self::response::ClientResponse; +#[allow(deprecated)] +pub use self::response_body::{MessageBody, ResponseBody}; + +/// Default body size limit: 2 MiB +const DEFAULT_BODY_LIMIT: usize = 2 * 1024 * 1024; + +/// Helper enum with reusable sleep passed from `SendClientResponse`. +/// +/// See [`ClientResponse::_timeout`] for reason. +pub(crate) enum ResponseTimeout { + Disabled(Option>>), + Enabled(Pin>), +} + +impl Default for ResponseTimeout { + fn default() -> Self { + Self::Disabled(None) + } +} + +impl ResponseTimeout { + fn poll_timeout(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> { + match *self { + Self::Enabled(ref mut timeout) => { + if timeout.as_mut().poll(cx).is_ready() { + Err(PayloadError::Io(io::Error::new( + io::ErrorKind::TimedOut, + "Response Payload IO timed out", + ))) + } else { + Ok(()) + } + } + Self::Disabled(_) => Ok(()), + } + } +} diff --git a/awc/src/responses/read_body.rs b/awc/src/responses/read_body.rs new file mode 100644 index 000000000..a32bbb984 --- /dev/null +++ b/awc/src/responses/read_body.rs @@ -0,0 +1,61 @@ +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_http::{error::PayloadError, Payload}; +use bytes::{Bytes, BytesMut}; +use futures_core::{ready, Stream}; +use pin_project_lite::pin_project; + +pin_project! { + pub(crate) struct ReadBody { + #[pin] + pub(crate) stream: Payload, + pub(crate) buf: BytesMut, + pub(crate) limit: usize, + } +} + +impl ReadBody { + pub(crate) fn new(stream: Payload, limit: usize) -> Self { + Self { + stream, + buf: BytesMut::new(), + limit, + } + } +} + +impl Future for ReadBody +where + S: Stream>, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + + while let Some(chunk) = ready!(this.stream.as_mut().poll_next(cx)?) { + if (this.buf.len() + chunk.len()) > *this.limit { + return Poll::Ready(Err(PayloadError::Overflow)); + } + + this.buf.extend_from_slice(&chunk); + } + + Poll::Ready(Ok(this.buf.split().freeze())) + } +} + +#[cfg(test)] +mod tests { + use static_assertions::assert_impl_all; + + use super::*; + use crate::any_body::AnyBody; + + assert_impl_all!(ReadBody<()>: Unpin); + assert_impl_all!(ReadBody: Unpin); +} diff --git a/awc/src/responses/response.rs b/awc/src/responses/response.rs new file mode 100644 index 000000000..0197265f1 --- /dev/null +++ b/awc/src/responses/response.rs @@ -0,0 +1,258 @@ +use std::{ + cell::{Ref, RefMut}, + fmt, mem, + pin::Pin, + task::{Context, Poll}, + time::{Duration, Instant}, +}; + +use actix_http::{ + error::PayloadError, header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, + Payload, ResponseHead, StatusCode, Version, +}; +use actix_rt::time::{sleep, Sleep}; +use bytes::Bytes; +use futures_core::Stream; +use pin_project_lite::pin_project; +use serde::de::DeserializeOwned; + +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, ParseError as CookieParseError}; + +use super::{JsonBody, ResponseBody, ResponseTimeout}; + +pin_project! { + /// Client Response + pub struct ClientResponse { + pub(crate) head: ResponseHead, + #[pin] + pub(crate) payload: Payload, + pub(crate) timeout: ResponseTimeout, + } +} + +impl ClientResponse { + /// Create new Request instance + pub(crate) fn new(head: ResponseHead, payload: Payload) -> Self { + ClientResponse { + head, + payload, + timeout: ResponseTimeout::default(), + } + } + + #[inline] + pub(crate) fn head(&self) -> &ResponseHead { + &self.head + } + + /// Read the Request Version. + #[inline] + pub fn version(&self) -> Version { + self.head().version + } + + /// Get the status from the server. + #[inline] + pub fn status(&self) -> StatusCode { + self.head().status + } + + #[inline] + /// Returns request's headers. + pub fn headers(&self) -> &HeaderMap { + &self.head().headers + } + + /// Set a body and return previous body value + pub fn map_body(mut self, f: F) -> ClientResponse + where + F: FnOnce(&mut ResponseHead, Payload) -> Payload, + { + let payload = f(&mut self.head, self.payload); + + ClientResponse { + payload, + head: self.head, + timeout: self.timeout, + } + } + + /// Set a timeout duration for [`ClientResponse`](self::ClientResponse). + /// + /// This duration covers the duration of processing the response body stream + /// and would end it as timeout error when deadline met. + /// + /// Disabled by default. + pub fn timeout(self, dur: Duration) -> Self { + let timeout = match self.timeout { + ResponseTimeout::Disabled(Some(mut timeout)) + | ResponseTimeout::Enabled(mut timeout) => match Instant::now().checked_add(dur) { + Some(deadline) => { + timeout.as_mut().reset(deadline.into()); + ResponseTimeout::Enabled(timeout) + } + None => ResponseTimeout::Enabled(Box::pin(sleep(dur))), + }, + _ => ResponseTimeout::Enabled(Box::pin(sleep(dur))), + }; + + Self { + payload: self.payload, + head: self.head, + timeout, + } + } + + /// This method does not enable timeout. It's used to pass the boxed `Sleep` from + /// `SendClientRequest` and reuse it's heap allocation together with it's slot in + /// timer wheel. + pub(crate) fn _timeout(mut self, timeout: Option>>) -> Self { + self.timeout = ResponseTimeout::Disabled(timeout); + self + } + + /// Load request cookies. + #[cfg(feature = "cookies")] + pub fn cookies(&self) -> Result>>, CookieParseError> { + struct Cookies(Vec>); + + if self.extensions().get::().is_none() { + let mut cookies = Vec::new(); + for hdr in self.headers().get_all(&actix_http::header::SET_COOKIE) { + let s = std::str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; + cookies.push(Cookie::parse_encoded(s)?.into_owned()); + } + self.extensions_mut().insert(Cookies(cookies)); + } + + Ok(Ref::map(self.extensions(), |ext| { + &ext.get::().unwrap().0 + })) + } + + /// Return request cookie. + #[cfg(feature = "cookies")] + pub fn cookie(&self, name: &str) -> Option> { + if let Ok(cookies) = self.cookies() { + for cookie in cookies.iter() { + if cookie.name() == name { + return Some(cookie.to_owned()); + } + } + } + None + } +} + +impl ClientResponse +where + S: Stream>, +{ + /// Returns a [`Future`] that consumes the body stream and resolves to [`Bytes`]. + /// + /// # Errors + /// `Future` implementation returns error if: + /// - content type is not `application/json` + /// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB) + /// + /// # Examples + /// ```no_run + /// # use awc::Client; + /// # use bytes::Bytes; + /// # #[actix_rt::main] + /// # async fn async_ctx() -> Result<(), Box> { + /// let client = Client::default(); + /// let mut res = client.get("https://httpbin.org/robots.txt").send().await?; + /// let body: Bytes = res.body().await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Future`]: std::future::Future + pub fn body(&mut self) -> ResponseBody { + ResponseBody::new(self) + } + + /// Returns a [`Future`] consumes the body stream, parses JSON, and resolves to a deserialized + /// `T` value. + /// + /// # Errors + /// Future returns error if: + /// - content type is not `application/json`; + /// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB). + /// + /// # Examples + /// ```no_run + /// # use awc::Client; + /// # #[actix_rt::main] + /// # async fn async_ctx() -> Result<(), Box> { + /// let client = Client::default(); + /// let mut res = client.get("https://httpbin.org/json").send().await?; + /// let val = res.json::().await?; + /// assert!(val.is_object()); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Future`]: std::future::Future + pub fn json(&mut self) -> JsonBody { + JsonBody::new(self) + } +} + +impl fmt::Debug for ClientResponse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status(),)?; + writeln!(f, " headers:")?; + for (key, val) in self.headers().iter() { + writeln!(f, " {:?}: {:?}", key, val)?; + } + Ok(()) + } +} + +impl HttpMessage for ClientResponse { + type Stream = S; + + fn headers(&self) -> &HeaderMap { + &self.head.headers + } + + fn take_payload(&mut self) -> Payload { + mem::replace(&mut self.payload, Payload::None) + } + + fn extensions(&self) -> Ref<'_, Extensions> { + self.head.extensions() + } + + fn extensions_mut(&self) -> RefMut<'_, Extensions> { + self.head.extensions_mut() + } +} + +impl Stream for ClientResponse +where + S: Stream> + Unpin, +{ + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + this.timeout.poll_timeout(cx)?; + this.payload.poll_next(cx) + } +} + +#[cfg(test)] +mod tests { + use static_assertions::assert_impl_all; + + use super::*; + use crate::any_body::AnyBody; + + assert_impl_all!(ClientResponse: Unpin); + assert_impl_all!(ClientResponse<()>: Unpin); + assert_impl_all!(ClientResponse: Unpin); +} diff --git a/awc/src/responses/response_body.rs b/awc/src/responses/response_body.rs new file mode 100644 index 000000000..8d9d1274a --- /dev/null +++ b/awc/src/responses/response_body.rs @@ -0,0 +1,144 @@ +use std::{ + future::Future, + mem, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_http::{error::PayloadError, header, HttpMessage}; +use bytes::Bytes; +use futures_core::Stream; +use pin_project_lite::pin_project; + +use super::{read_body::ReadBody, ResponseTimeout, DEFAULT_BODY_LIMIT}; +use crate::ClientResponse; + +pin_project! { + /// A `Future` that reads a body stream, resolving as [`Bytes`]. + /// + /// # Errors + /// `Future` implementation returns error if: + /// - content type is not `application/json`; + /// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB). + pub struct ResponseBody { + #[pin] + body: Option>, + length: Option, + timeout: ResponseTimeout, + err: Option, + } +} + +#[deprecated(since = "3.0.0", note = "Renamed to `ResponseBody`.")] +pub type MessageBody = ResponseBody; + +impl ResponseBody +where + S: Stream>, +{ + /// Creates a body stream reader from a response by taking its payload. + pub fn new(res: &mut ClientResponse) -> ResponseBody { + let length = match res.headers().get(&header::CONTENT_LENGTH) { + Some(value) => { + let len = value.to_str().ok().and_then(|s| s.parse::().ok()); + + match len { + None => return Self::err(PayloadError::UnknownLength), + len => len, + } + } + None => None, + }; + + ResponseBody { + body: Some(ReadBody::new(res.take_payload(), DEFAULT_BODY_LIMIT)), + length, + timeout: mem::take(&mut res.timeout), + err: None, + } + } + + /// Change max size limit of payload. + /// + /// The default limit is 2 MiB. + pub fn limit(mut self, limit: usize) -> Self { + if let Some(ref mut body) = self.body { + body.limit = limit; + } + + self + } + + fn err(err: PayloadError) -> Self { + ResponseBody { + body: None, + length: None, + timeout: ResponseTimeout::default(), + err: Some(err), + } + } +} + +impl Future for ResponseBody +where + S: Stream>, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + + if let Some(err) = this.err.take() { + return Poll::Ready(Err(err)); + } + + if let Some(len) = this.length.take() { + let body = Option::as_ref(&this.body).unwrap(); + if len > body.limit { + return Poll::Ready(Err(PayloadError::Overflow)); + } + } + + this.timeout.poll_timeout(cx)?; + + this.body.as_pin_mut().unwrap().poll(cx) + } +} + +#[cfg(test)] +mod tests { + use static_assertions::assert_impl_all; + + use super::*; + use crate::{http::header, test::TestResponse}; + + assert_impl_all!(ResponseBody<()>: Unpin); + + #[actix_rt::test] + async fn read_body() { + let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "xxxx")).finish(); + match req.body().await.err().unwrap() { + PayloadError::UnknownLength => {} + _ => unreachable!("error"), + } + + let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "10000000")).finish(); + match req.body().await.err().unwrap() { + PayloadError::Overflow => {} + _ => unreachable!("error"), + } + + let mut req = TestResponse::default() + .set_payload(Bytes::from_static(b"test")) + .finish(); + assert_eq!(req.body().await.ok().unwrap(), Bytes::from_static(b"test")); + + let mut req = TestResponse::default() + .set_payload(Bytes::from_static(b"11111111111111")) + .finish(); + match req.body().limit(5).await.err().unwrap() { + PayloadError::Overflow => {} + _ => unreachable!("error"), + } + } +} diff --git a/awc/src/sender.rs b/awc/src/sender.rs index 1faf6140a..cd30e571d 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -8,9 +8,9 @@ use std::{ }; use actix_http::{ - body::BodyStream, + body::{BodyStream, MessageBody}, error::HttpError, - header::{self, HeaderMap, HeaderName, IntoHeaderValue}, + header::{self, HeaderMap, HeaderName, TryIntoHeaderValue}, RequestHead, RequestHeadType, }; use actix_rt::time::{sleep, Sleep}; @@ -20,12 +20,13 @@ use futures_core::Stream; use serde::Serialize; #[cfg(feature = "__compress")] -use actix_http::{encoding::Decoder, header::ContentEncoding, Payload, PayloadStream}; +use actix_http::{encoding::Decoder, header::ContentEncoding, Payload}; use crate::{ any_body::AnyBody, + client::ClientConfig, error::{FreezeRequestError, InvalidUrl, SendRequestError}, - BoxError, ClientConfig, ClientResponse, ConnectRequest, ConnectResponse, + BoxError, ClientResponse, ConnectRequest, ConnectResponse, }; #[derive(Debug, From)] @@ -91,7 +92,7 @@ impl SendClientRequest { #[cfg(feature = "__compress")] impl Future for SendClientRequest { - type Output = Result>>, SendRequestError>; + type Output = Result>, SendRequestError>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); @@ -108,12 +109,13 @@ impl Future for SendClientRequest { res.into_client_response()._timeout(delay.take()).map_body( |head, payload| { if *response_decompress { - Payload::Stream(Decoder::from_headers(payload, &head.headers)) + Payload::Stream { + payload: Decoder::from_headers(payload, &head.headers), + } } else { - Payload::Stream(Decoder::new( - payload, - ContentEncoding::Identity, - )) + Payload::Stream { + payload: Decoder::new(payload, ContentEncoding::Identity), + } } }, ) @@ -179,24 +181,23 @@ pub(crate) enum RequestSender { } impl RequestSender { - pub(crate) fn send_body( + pub(crate) fn send_body( self, addr: Option, response_decompress: bool, timeout: Option, config: &ClientConfig, - body: B, - ) -> SendClientRequest - where - B: Into, - { + body: impl MessageBody + 'static, + ) -> SendClientRequest { let req = match self { - RequestSender::Owned(head) => { - ConnectRequest::Client(RequestHeadType::Owned(head), body.into(), addr) - } + RequestSender::Owned(head) => ConnectRequest::Client( + RequestHeadType::Owned(head), + AnyBody::from_message_body(body).into_boxed(), + addr, + ), RequestSender::Rc(head, extra_headers) => ConnectRequest::Client( RequestHeadType::Rc(head, extra_headers), - body.into(), + AnyBody::from_message_body(body).into_boxed(), addr, ), }; @@ -206,15 +207,15 @@ impl RequestSender { SendClientRequest::new(fut, response_decompress, timeout.or(config.timeout)) } - pub(crate) fn send_json( + pub(crate) fn send_json( mut self, addr: Option, response_decompress: bool, timeout: Option, config: &ClientConfig, - value: &T, + value: impl Serialize, ) -> SendClientRequest { - let body = match serde_json::to_string(value) { + let body = match serde_json::to_string(&value) { Ok(body) => body, Err(err) => return PrepForSendingError::Json(err).into(), }; @@ -223,24 +224,16 @@ impl RequestSender { return e.into(); } - self.send_body( - addr, - response_decompress, - timeout, - config, - AnyBody::Bytes { - body: Bytes::from(body), - }, - ) + self.send_body(addr, response_decompress, timeout, config, body) } - pub(crate) fn send_form( + pub(crate) fn send_form( mut self, addr: Option, response_decompress: bool, timeout: Option, config: &ClientConfig, - value: &T, + value: impl Serialize, ) -> SendClientRequest { let body = match serde_urlencoded::to_string(value) { Ok(body) => body, @@ -248,21 +241,13 @@ impl RequestSender { }; // set content-type - if let Err(e) = + if let Err(err) = self.set_header_if_none(header::CONTENT_TYPE, "application/x-www-form-urlencoded") { - return e.into(); + return err.into(); } - self.send_body( - addr, - response_decompress, - timeout, - config, - AnyBody::Bytes { - body: Bytes::from(body), - }, - ) + self.send_body(addr, response_decompress, timeout, config, body) } pub(crate) fn send_stream( @@ -274,7 +259,7 @@ impl RequestSender { stream: S, ) -> SendClientRequest where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, E: Into + 'static, { self.send_body( @@ -282,7 +267,7 @@ impl RequestSender { response_decompress, timeout, config, - AnyBody::new_boxed(BodyStream::new(stream)), + BodyStream::new(stream), ) } @@ -293,12 +278,12 @@ impl RequestSender { timeout: Option, config: &ClientConfig, ) -> SendClientRequest { - self.send_body(addr, response_decompress, timeout, config, AnyBody::empty()) + self.send_body(addr, response_decompress, timeout, config, ()) } fn set_header_if_none(&mut self, key: HeaderName, value: V) -> Result<(), HttpError> where - V: IntoHeaderValue, + V: TryIntoHeaderValue, { match self { RequestSender::Owned(head) => { diff --git a/awc/src/test.rs b/awc/src/test.rs index 4a5c8e7ea..96ae1f0a1 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -1,6 +1,6 @@ //! Test helpers for actix http client to use during testing. -use actix_http::{h1, header::IntoHeaderPair, Payload, ResponseHead, StatusCode, Version}; +use actix_http::{h1, header::TryIntoHeaderPair, Payload, ResponseHead, StatusCode, Version}; use bytes::Bytes; #[cfg(feature = "cookies")] @@ -28,10 +28,7 @@ impl Default for TestResponse { impl TestResponse { /// Create TestResponse and set header - pub fn with_header(header: H) -> Self - where - H: IntoHeaderPair, - { + pub fn with_header(header: impl TryIntoHeaderPair) -> Self { Self::default().insert_header(header) } @@ -42,11 +39,8 @@ impl TestResponse { } /// Insert a header - pub fn insert_header(mut self, header: H) -> Self - where - H: IntoHeaderPair, - { - if let Ok((key, value)) = header.try_into_header_pair() { + pub fn insert_header(mut self, header: impl TryIntoHeaderPair) -> Self { + if let Ok((key, value)) = header.try_into_pair() { self.head.headers.insert(key, value); return self; } @@ -54,11 +48,8 @@ impl TestResponse { } /// Append a header - pub fn append_header(mut self, header: H) -> Self - where - H: IntoHeaderPair, - { - if let Ok((key, value)) = header.try_into_header_pair() { + pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self { + if let Ok((key, value)) = header.try_into_pair() { self.head.headers.append(key, value); return self; } @@ -74,7 +65,7 @@ impl TestResponse { /// Set response's payload pub fn set_payload>(mut self, data: B) -> Self { - let mut payload = h1::Payload::empty(); + let (_, mut payload) = h1::Payload::create(true); payload.unread_data(data.into()); self.payload = Some(payload.into()); self @@ -99,7 +90,8 @@ impl TestResponse { if let Some(pl) = self.payload { ClientResponse::new(head, pl) } else { - ClientResponse::new(head, h1::Payload::empty().into()) + let (_, payload) = h1::Payload::create(true); + ClientResponse::new(head, payload.into()) } } } diff --git a/awc/src/ws.rs b/awc/src/ws.rs index f0d421dbc..f3ee02d43 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -31,19 +31,19 @@ use std::{convert::TryFrom, fmt, net::SocketAddr, str}; use actix_codec::Framed; use actix_http::{ws, Payload, RequestHead}; use actix_rt::time::timeout; -use actix_service::Service; +use actix_service::Service as _; pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message}; use crate::{ + client::ClientConfig, connect::{BoxedSocket, ConnectRequest}, error::{HttpError, InvalidUrl, SendRequestError, WsClientError}, http::{ - header::{self, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION}, + header::{self, HeaderName, HeaderValue, TryIntoHeaderValue, AUTHORIZATION}, ConnectionType, Method, StatusCode, Uri, Version, }, - response::ClientResponse, - ClientConfig, + ClientResponse, }; #[cfg(feature = "cookies")] @@ -171,7 +171,7 @@ impl WebsocketsRequest { where HeaderName: TryFrom, >::Error: Into, - V: IntoHeaderValue, + V: TryIntoHeaderValue, { match HeaderName::try_from(key) { Ok(key) => match value.try_into_value() { @@ -190,7 +190,7 @@ impl WebsocketsRequest { where HeaderName: TryFrom, >::Error: Into, - V: IntoHeaderValue, + V: TryIntoHeaderValue, { match HeaderName::try_from(key) { Ok(key) => match value.try_into_value() { @@ -209,7 +209,7 @@ impl WebsocketsRequest { where HeaderName: TryFrom, >::Error: Into, - V: IntoHeaderValue, + V: TryIntoHeaderValue, { match HeaderName::try_from(key) { Ok(key) => { @@ -300,13 +300,16 @@ impl WebsocketsRequest { } self.head.set_connection_type(ConnectionType::Upgrade); + + #[allow(clippy::declare_interior_mutable_const)] + const HV_WEBSOCKET: HeaderValue = HeaderValue::from_static("websocket"); + self.head.headers.insert(header::UPGRADE, HV_WEBSOCKET); + + #[allow(clippy::declare_interior_mutable_const)] + const HV_THIRTEEN: HeaderValue = HeaderValue::from_static("13"); self.head .headers - .insert(header::UPGRADE, HeaderValue::from_static("websocket")); - self.head.headers.insert( - header::SEC_WEBSOCKET_VERSION, - HeaderValue::from_static("13"), - ); + .insert(header::SEC_WEBSOCKET_VERSION, HV_THIRTEEN); if let Some(protocols) = self.protocols.take() { self.head.headers.insert( @@ -445,7 +448,7 @@ mod tests { #[actix_rt::test] async fn test_header_override() { let req = Client::builder() - .header(header::CONTENT_TYPE, "111") + .add_default_header((header::CONTENT_TYPE, "111")) .finish() .ws("/") .set_header(header::CONTENT_TYPE, "222"); diff --git a/awc/tests/test_connector.rs b/awc/tests/test_connector.rs index 588c51463..0f0b81414 100644 --- a/awc/tests/test_connector.rs +++ b/awc/tests/test_connector.rs @@ -58,7 +58,7 @@ async fn test_connection_window_size() { .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); let client = awc::Client::builder() - .connector(awc::Connector::new().ssl(builder.build())) + .connector(awc::Connector::new().openssl(builder.build())) .initial_window_size(100) .initial_connection_window_size(100) .finish(); diff --git a/awc/tests/test_ssl_client.rs b/awc/tests/test_ssl_client.rs index 811efd4bc..40c9ab8f0 100644 --- a/awc/tests/test_ssl_client.rs +++ b/awc/tests/test_ssl_client.rs @@ -72,7 +72,7 @@ async fn test_connection_reuse_h2() { .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); let client = awc::Client::builder() - .connector(awc::Connector::new().ssl(builder.build())) + .connector(awc::Connector::new().openssl(builder.build())) .finish(); // req 1 diff --git a/examples/basic.rs b/examples/basic.rs index d29546129..598d13a40 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -22,14 +22,14 @@ async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() - .wrap(middleware::DefaultHeaders::new().header("X-Version", "0.2")) + .wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2"))) .wrap(middleware::Compress::default()) .wrap(middleware::Logger::default()) .service(index) .service(no_params) .service( web::resource("/resource2/index.html") - .wrap(middleware::DefaultHeaders::new().header("X-Version-R2", "0.3")) + .wrap(middleware::DefaultHeaders::new().add(("X-Version-R2", "0.3"))) .default_service(web::route().to(HttpResponse::MethodNotAllowed)) .route(web::get().to(index_async)), ) diff --git a/examples/on_connect.rs b/examples/on_connect.rs index 9709835e6..d76e9ce56 100644 --- a/examples/on_connect.rs +++ b/examples/on_connect.rs @@ -6,7 +6,10 @@ use std::{any::Any, io, net::SocketAddr}; -use actix_web::{dev::Extensions, rt::net::TcpStream, web, App, HttpServer}; +use actix_web::{ + dev::Extensions, rt::net::TcpStream, web, App, HttpRequest, HttpResponse, HttpServer, + Responder, +}; #[allow(dead_code)] #[derive(Debug, Clone)] @@ -16,11 +19,16 @@ struct ConnectionInfo { ttl: Option, } -async fn route_whoami(conn_info: web::ReqData) -> String { - format!( - "Here is some info about your connection:\n\n{:#?}", - conn_info - ) +async fn route_whoami(req: HttpRequest) -> impl Responder { + match req.conn_data::() { + Some(info) => HttpResponse::Ok().body(format!( + "Here is some info about your connection:\n\n{:#?}", + info + )), + None => { + HttpResponse::InternalServerError().body("Missing expected request extension data") + } + } } fn get_conn_info(connection: &dyn Any, data: &mut Extensions) { @@ -39,9 +47,12 @@ fn get_conn_info(connection: &dyn Any, data: &mut Extensions) { async fn main() -> io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + let bind = ("127.0.0.1", 8080); + log::info!("staring server at http://{}:{}", &bind.0, &bind.1); + HttpServer::new(|| App::new().default_service(web::to(route_whoami))) .on_connect(get_conn_info) - .bind(("127.0.0.1", 8080))? + .bind(bind)? .workers(1) .run() .await diff --git a/examples/uds.rs b/examples/uds.rs index 1db252fef..cf0ffebde 100644 --- a/examples/uds.rs +++ b/examples/uds.rs @@ -26,14 +26,14 @@ async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() - .wrap(middleware::DefaultHeaders::new().header("X-Version", "0.2")) + .wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2"))) .wrap(middleware::Compress::default()) .wrap(middleware::Logger::default()) .service(index) .service(no_params) .service( web::resource("/resource2/index.html") - .wrap(middleware::DefaultHeaders::new().header("X-Version-R2", "0.3")) + .wrap(middleware::DefaultHeaders::new().add(("X-Version-R2", "0.3"))) .default_service(web::route().to(HttpResponse::MethodNotAllowed)) .route(web::get().to(index_async)), ) diff --git a/scripts/bump b/scripts/bump index 8b6a3c424..43cd8b8c7 100755 --- a/scripts/bump +++ b/scripts/bump @@ -41,6 +41,8 @@ cat "$CHANGELOG_FILE" | # if word count of changelog chunk is 0 then insert filler changelog chunk if [ "$(wc -w "$CHANGE_CHUNK_FILE" | awk '{ print $1 }')" = "0" ]; then echo "* No significant changes since \`$CURRENT_VERSION\`." >"$CHANGE_CHUNK_FILE" + echo >>"$CHANGE_CHUNK_FILE" + echo >>"$CHANGE_CHUNK_FILE" fi if [ -n "${2-}" ]; then @@ -53,6 +55,11 @@ else read -p "Update version to: " NEW_VERSION fi +# strip leading v from input +if [ "${NEW_VERSION:0:1}" = "v" ]; then + NEW_VERSION="${NEW_VERSION:1}" +fi + DATE="$(date -u +"%Y-%m-%d")" echo "updating from $CURRENT_VERSION => $NEW_VERSION ($DATE)" @@ -82,8 +89,33 @@ rm -f $README_FILE.bak echo "manifest, changelog, and readme updated" echo echo "check other references:" -rg "$PACKAGE_NAME =" || true -rg "package = \"$PACKAGE_NAME\"" || true +rg --glob='**/Cargo.toml' "\ +${PACKAGE_NAME} ?= ?\"[^\"]+\"\ +|${PACKAGE_NAME} ?=.*version ?= ?\"([^\"]+)\"\ +|package ?= ?\"${PACKAGE_NAME}\".*version ?= ?\"([^\"]+)\"\ +|version ?= ?\"([^\"]+)\".*package ?= ?\"${PACKAGE_NAME}\"" || true + +echo +read -p "Update all references: (y/N) " UPDATE_REFERENCES +UPDATE_REFERENCES="${UPDATE_REFERENCES:-n}" + +if [ "$UPDATE_REFERENCES" = 'y' ] || [ "$UPDATE_REFERENCES" = 'Y' ]; then + + for f in $(fd Cargo.toml); do + sed -i.bak -E \ + "s/^(${PACKAGE_NAME} ?= ?\")[^\"]+(\")$/\1${NEW_VERSION}\2/g" $f + sed -i.bak -E \ + "s/^(${PACKAGE_NAME} ?=.*version ?= ?\")[^\"]+(\".*)$/\1${NEW_VERSION}\2/g" $f + sed -i.bak -E \ + "s/^(.*package ?= ?\"${PACKAGE_NAME}\".*version ?= ?\")[^\"]+(\".*)$/\1${NEW_VERSION}\2/g" $f + sed -i.bak -E \ + "s/^(.*version ?= ?\")[^\"]+(\".*package ?= ?\"${PACKAGE_NAME}\".*)$/\1${NEW_VERSION}\2/g" $f + + # remove backup file + rm -f $f.bak + done + +fi if [ $MACOS ]; then printf "prepare $PACKAGE_NAME release $NEW_VERSION" | pbcopy @@ -97,15 +129,20 @@ SHORT_PACKAGE_NAME="$(echo $PACKAGE_NAME | sed 's/^actix-web-//' | sed 's/^actix GIT_TAG="$(echo $SHORT_PACKAGE_NAME-v$NEW_VERSION)" RELEASE_TITLE="$(echo $PACKAGE_NAME: v$NEW_VERSION)" +if [ "$(echo $NEW_VERSION | grep beta)" ] || [ "$(echo $NEW_VERSION | grep rc)" ] || [ "$(echo $NEW_VERSION | grep alpha)" ]; then + PRERELEASE="--prerelease" +fi + echo echo "GitHub release command:" -echo "gh release create \"$GIT_TAG\" --draft --title \"$RELEASE_TITLE\" --notes-file \"$CHANGE_CHUNK_FILE\" --prerelease" +GH_CMD="gh release create \"$GIT_TAG\" --draft --title \"$RELEASE_TITLE\" --notes-file \"$CHANGE_CHUNK_FILE\" ${PRERELEASE:-}" +echo "$GH_CMD" read -p "Submit draft GH release: (y/N) " GH_RELEASE GH_RELEASE="${GH_RELEASE:-n}" if [ "$GH_RELEASE" = 'y' ] || [ "$GH_RELEASE" = 'Y' ]; then - gh release create "$GIT_TAG" --draft --title "$RELEASE_TITLE" --notes-file "$CHANGE_CHUNK_FILE" --prerelease + eval "$GH_CMD" fi echo diff --git a/scripts/ci-test b/scripts/ci-test index 98e13927d..3ab229665 100755 --- a/scripts/ci-test +++ b/scripts/ci-test @@ -4,15 +4,25 @@ set -x -cargo test --lib --tests -p=actix-router --all-features -cargo test --lib --tests -p=actix-http --all-features -cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls -cargo test --lib --tests -p=actix-web-codegen --all-features -cargo test --lib --tests -p=awc --all-features -cargo test --lib --tests -p=actix-http-test --all-features -cargo test --lib --tests -p=actix-test --all-features -cargo test --lib --tests -p=actix-files -cargo test --lib --tests -p=actix-multipart --all-features -cargo test --lib --tests -p=actix-web-actors --all-features +EXIT=0 -cargo test --workspace --doc +save_exit_code() { + eval $@ + local CMD_EXIT=$? + [ "$CMD_EXIT" = "0" ] || EXIT=$CMD_EXIT +} + +save_exit_code cargo test --lib --tests -p=actix-router --all-features +save_exit_code cargo test --lib --tests -p=actix-http --all-features +save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls +save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features +save_exit_code cargo test --lib --tests -p=awc --all-features +save_exit_code cargo test --lib --tests -p=actix-http-test --all-features +save_exit_code cargo test --lib --tests -p=actix-test --all-features +save_exit_code cargo test --lib --tests -p=actix-files +save_exit_code cargo test --lib --tests -p=actix-multipart --all-features +save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features + +save_exit_code cargo test --workspace --doc + +exit $EXIT diff --git a/scripts/unreleased b/scripts/unreleased new file mode 100755 index 000000000..4dfa2d9ae --- /dev/null +++ b/scripts/unreleased @@ -0,0 +1,41 @@ +#!/bin/sh + +set -euo pipefail + +bold="\033[1m" +reset="\033[0m" + +unreleased_for() { + DIR=$1 + + CARGO_MANIFEST=$DIR/Cargo.toml + CHANGELOG_FILE=$DIR/CHANGES.md + + # get current version + PACKAGE_NAME="$(sed -nE 's/^name ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST" | head -n 1)" + CURRENT_VERSION="$(sed -nE 's/^version ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST")" + + CHANGE_CHUNK_FILE="$(mktemp)" + + # get changelog chunk and save to temp file + cat "$CHANGELOG_FILE" | + # skip up to unreleased heading + sed '1,/Unreleased/ d' | + # take up to previous version heading + sed "/$CURRENT_VERSION/ q" | + # drop last line + sed '$d' \ + >"$CHANGE_CHUNK_FILE" + + # if word count of changelog chunk is 0 then exit + if [ "$(wc -w "$CHANGE_CHUNK_FILE" | awk '{ print $1 }')" = "0" ]; then + return 0; + fi + + echo "${bold}# ${PACKAGE_NAME}${reset} since ${bold}v$CURRENT_VERSION${reset}" + cat "$CHANGE_CHUNK_FILE" +} + +for f in $(fd --absolute-path CHANGES.md); do + unreleased_for $(dirname $f) +done diff --git a/src/app.rs b/src/app.rs index a27dd54a6..10868d18d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,9 +1,6 @@ -use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, rc::Rc}; +use std::{cell::RefCell, fmt, future::Future, rc::Rc}; -use actix_http::{ - body::{BoxBody, MessageBody}, - Extensions, Request, -}; +use actix_http::{body::MessageBody, Extensions, Request}; use actix_service::{ apply, apply_fn_factory, boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt, Transform, @@ -26,7 +23,7 @@ use crate::{ /// Application builder - structure that follows the builder pattern /// for building application instances. -pub struct App { +pub struct App { endpoint: T, services: Vec>, default: Option>, @@ -34,10 +31,9 @@ pub struct App { data_factories: Vec, external: Vec, extensions: Extensions, - _phantom: PhantomData, } -impl App { +impl App { /// Create application builder. Application can be configured with a builder-like pattern. #[allow(clippy::new_without_default)] pub fn new() -> Self { @@ -51,22 +47,11 @@ impl App { factory_ref, external: Vec::new(), extensions: Extensions::new(), - _phantom: PhantomData, } } } -impl App -where - B: MessageBody, - T: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - InitError = (), - >, -{ +impl App { /// Set application (root level) data. /// /// Application data stored with `App::app_data()` method is available through the @@ -137,9 +122,10 @@ where self.app_data(Data::new(data)) } - /// Add application data factory. This function is similar to `.data()` but it accepts a - /// "data factory". Data values are constructed asynchronously during application - /// initialization, before the server starts accepting requests. + /// Add application data factory that resolves asynchronously. + /// + /// Data items are constructed during application initialization, before the server starts + /// accepting requests. pub fn data_factory(mut self, data: F) -> Self where F: Fn() -> Out + 'static, @@ -165,6 +151,7 @@ where } .boxed_local() })); + self } @@ -215,11 +202,9 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new() - /// .route("/test1", web::get().to(index)) - /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())); - /// } + /// let app = App::new() + /// .route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())); /// ``` pub fn route(self, path: &str, mut route: Route) -> Self { self.service( @@ -258,13 +243,11 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new() - /// .service( - /// web::resource("/index.html").route(web::get().to(index))) - /// .default_service( - /// web::route().to(|| HttpResponse::NotFound())); - /// } + /// let app = App::new() + /// .service( + /// web::resource("/index.html").route(web::get().to(index))) + /// .default_service( + /// web::route().to(|| HttpResponse::NotFound())); /// ``` /// /// It is also possible to use static files as default service. @@ -272,14 +255,12 @@ where /// ``` /// use actix_web::{web, App, HttpResponse}; /// - /// fn main() { - /// let app = App::new() - /// .service( - /// web::resource("/index.html").to(|| HttpResponse::Ok())) - /// .default_service( - /// web::to(|| HttpResponse::NotFound()) - /// ); - /// } + /// let app = App::new() + /// .service( + /// web::resource("/index.html").to(|| HttpResponse::Ok())) + /// .default_service( + /// web::to(|| HttpResponse::NotFound()) + /// ); /// ``` pub fn default_service(mut self, svc: F) -> Self where @@ -365,7 +346,7 @@ where /// .route("/index.html", web::get().to(index)); /// } /// ``` - pub fn wrap( + pub fn wrap( self, mw: M, ) -> App< @@ -376,9 +357,16 @@ where Error = Error, InitError = (), >, - B1, > where + T: ServiceFactory< + ServiceRequest, + Response = ServiceResponse, + Error = Error, + Config = (), + InitError = (), + >, + B: MessageBody, M: Transform< T::Service, ServiceRequest, @@ -396,7 +384,6 @@ where factory_ref: self.factory_ref, external: self.external, extensions: self.extensions, - _phantom: PhantomData, } } @@ -431,7 +418,7 @@ where /// .route("/index.html", web::get().to(index)); /// } /// ``` - pub fn wrap_fn( + pub fn wrap_fn( self, mw: F, ) -> App< @@ -442,12 +429,19 @@ where Error = Error, InitError = (), >, - B1, > where - B1: MessageBody, + T: ServiceFactory< + ServiceRequest, + Response = ServiceResponse, + Error = Error, + Config = (), + InitError = (), + >, + B: MessageBody, F: Fn(ServiceRequest, &T::Service) -> R + Clone, R: Future, Error>>, + B1: MessageBody, { App { endpoint: apply_fn_factory(self.endpoint, mw), @@ -457,12 +451,11 @@ where factory_ref: self.factory_ref, external: self.external, extensions: self.extensions, - _phantom: PhantomData, } } } -impl IntoServiceFactory, Request> for App +impl IntoServiceFactory, Request> for App where B: MessageBody, T: ServiceFactory< @@ -489,19 +482,21 @@ where #[cfg(test)] mod tests { - use actix_service::Service; + use actix_service::Service as _; use actix_utils::future::{err, ok}; use bytes::Bytes; use super::*; - use crate::http::{ - header::{self, HeaderValue}, - Method, StatusCode, + use crate::{ + http::{ + header::{self, HeaderValue}, + Method, StatusCode, + }, + middleware::DefaultHeaders, + service::ServiceRequest, + test::{call_service, init_service, read_body, try_init_service, TestRequest}, + web, HttpRequest, HttpResponse, }; - use crate::middleware::DefaultHeaders; - use crate::service::ServiceRequest; - use crate::test::{call_service, init_service, read_body, try_init_service, TestRequest}; - use crate::{web, HttpRequest, HttpResponse}; #[actix_rt::test] async fn test_default_resource() { @@ -605,7 +600,7 @@ mod tests { App::new() .wrap( DefaultHeaders::new() - .header(header::CONTENT_TYPE, HeaderValue::from_static("0001")), + .add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))), ) .route("/test", web::get().to(HttpResponse::Ok)), ) @@ -626,7 +621,7 @@ mod tests { .route("/test", web::get().to(HttpResponse::Ok)) .wrap( DefaultHeaders::new() - .header(header::CONTENT_TYPE, HeaderValue::from_static("0001")), + .add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))), ), ) .await; @@ -709,4 +704,25 @@ mod tests { let body = read_body(resp).await; assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345")); } + + #[test] + fn can_be_returned_from_fn() { + /// compile-only test for returning app type from function + pub fn my_app() -> App< + impl ServiceFactory< + ServiceRequest, + Response = ServiceResponse, + Config = (), + InitError = (), + Error = Error, + >, + > { + App::new() + // logger can be removed without affecting the return type + .wrap(crate::middleware::Logger::default()) + .route("/", web::to(|| async { "hello" })) + } + + let _ = init_service(my_app()); + } } diff --git a/src/app_service.rs b/src/app_service.rs index bca8f2629..56b24f0d8 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -1,14 +1,16 @@ use std::{cell::RefCell, mem, rc::Rc}; -use actix_http::{Extensions, Request}; +use actix_http::Request; use actix_router::{Path, ResourceDef, Router, Url}; use actix_service::{boxed, fn_service, Service, ServiceFactory}; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; use crate::{ + body::BoxBody, config::{AppConfig, AppService}, data::FnDataFactory, + dev::Extensions, guard::Guard, request::{HttpRequest, HttpRequestPool}, rmap::ResourceMap, @@ -22,6 +24,7 @@ use crate::{ type Guards = Vec>; /// Service factory to convert `Request` to a `ServiceRequest`. +/// /// It also executes data factories. pub struct AppInit where @@ -197,7 +200,9 @@ where actix_service::forward_ready!(service); - fn call(&self, req: Request) -> Self::Future { + fn call(&self, mut req: Request) -> Self::Future { + let req_data = Rc::new(RefCell::new(req.take_req_data())); + let conn_data = req.take_conn_data(); let (head, payload) = req.into_parts(); let req = if let Some(mut req) = self.app_state.pool().pop() { @@ -205,6 +210,8 @@ where inner.path.get_mut().update(&head.uri); inner.path.reset(); inner.head = head; + inner.conn_data = conn_data; + inner.req_data = req_data; req } else { HttpRequest::new( @@ -212,8 +219,11 @@ where head, self.app_state.clone(), self.app_data.clone(), + conn_data, + req_data, ) }; + self.service.call(ServiceRequest::new(req, payload)) } } @@ -228,6 +238,7 @@ where } pub struct AppRoutingFactory { + #[allow(clippy::type_complexity)] services: Rc< [( ResourceDef, @@ -288,7 +299,7 @@ pub struct AppRouting { } impl Service for AppRouting { - type Response = ServiceResponse; + type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; @@ -297,12 +308,15 @@ impl Service for AppRouting { fn call(&self, mut req: ServiceRequest) -> Self::Future { let res = self.router.recognize_fn(&mut req, |req, guards| { if let Some(ref guards) = guards { - for f in guards { - if !f.check(req.head()) { + let guard_ctx = req.guard_ctx(); + + for guard in guards { + if !guard.check(&guard_ctx) { return false; } } } + true }); diff --git a/src/config.rs b/src/config.rs index 9e77c0f96..d68374387 100644 --- a/src/config.rs +++ b/src/config.rs @@ -24,6 +24,7 @@ pub struct AppService { config: AppConfig, root: bool, default: Rc, + #[allow(clippy::type_complexity)] services: Vec<( ResourceDef, HttpNewService, @@ -48,6 +49,7 @@ impl AppService { self.root } + #[allow(clippy::type_complexity)] pub(crate) fn into_services( self, ) -> ( @@ -126,7 +128,7 @@ impl AppConfig { /// Server host name. /// - /// Host name is used by application router as a hostname for url generation. + /// Host name is used by application router as a hostname for URL generation. /// Check [ConnectionInfo](super::dev::ConnectionInfo::host()) /// documentation for more information. /// @@ -135,7 +137,7 @@ impl AppConfig { &self.host } - /// Returns true if connection is secure(https) + /// Returns true if connection is secure (i.e., running over `https:`). pub fn secure(&self) -> bool { self.secure } diff --git a/src/data.rs b/src/data.rs index b29e4ecf4..ef077e87c 100644 --- a/src/data.rs +++ b/src/data.rs @@ -31,41 +31,53 @@ pub(crate) type FnDataFactory = /// server constructs an application instance for each thread, thus application data must be /// constructed multiple times. If you want to share data between different threads, a shareable /// object should be used, e.g. `Send + Sync`. Application data does not need to be `Send` -/// or `Sync`. Internally `Data` uses `Arc`. +/// or `Sync`. Internally `Data` contains an `Arc`. /// -/// If route data is not set for a handler, using `Data` extractor would cause *Internal -/// Server Error* response. +/// If route data is not set for a handler, using `Data` extractor would cause a `500 Internal +/// Server Error` response. /// -// TODO: document `dyn T` functionality through converting an Arc -// TODO: note equivalence of req.app_data> and Data extractor -// TODO: note that data must be inserted using Data in order to extract it +/// # Unsized Data +/// For types that are unsized, most commonly `dyn T`, `Data` can wrap these types by first +/// constructing an `Arc` and using the `From` implementation to convert it. +/// +/// ``` +/// # use std::{fmt::Display, sync::Arc}; +/// # use actix_web::web::Data; +/// let displayable_arc: Arc = Arc::new(42usize); +/// let displayable_data: Data = Data::from(displayable_arc); +/// ``` /// /// # Examples /// ``` /// use std::sync::Mutex; -/// use actix_web::{web, App, HttpResponse, Responder}; +/// use actix_web::{App, HttpRequest, HttpResponse, Responder, web::{self, Data}}; /// /// struct MyData { /// counter: usize, /// } /// /// /// Use the `Data` extractor to access data in a handler. -/// async fn index(data: web::Data>) -> impl Responder { -/// let mut data = data.lock().unwrap(); -/// data.counter += 1; +/// async fn index(data: Data>) -> impl Responder { +/// let mut my_data = data.lock().unwrap(); +/// my_data.counter += 1; /// HttpResponse::Ok() /// } /// -/// fn main() { -/// let data = web::Data::new(Mutex::new(MyData{ counter: 0 })); -/// -/// let app = App::new() -/// // Store `MyData` in application storage. -/// .app_data(data.clone()) -/// .service( -/// web::resource("/index.html").route( -/// web::get().to(index))); +/// /// Alteratively, use the `HttpRequest::app_data` method to access data in a handler. +/// async fn index_alt(req: HttpRequest) -> impl Responder { +/// let data = req.app_data::>>().unwrap(); +/// let mut my_data = data.lock().unwrap(); +/// my_data.counter += 1; +/// HttpResponse::Ok() /// } +/// +/// let data = Data::new(Mutex::new(MyData { counter: 0 })); +/// +/// let app = App::new() +/// // Store `MyData` in application storage. +/// .app_data(Data::clone(&data)) +/// .route("/index.html", web::get().to(index)) +/// .route("/index-alt.html", web::get().to(index_alt)); /// ``` #[derive(Debug)] pub struct Data(Arc); diff --git a/src/dev.rs b/src/dev.rs index d4a64985c..bb1385bde 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -3,6 +3,16 @@ //! Most users will not have to interact with the types in this module, but it is useful for those //! writing extractors, middleware, libraries, or interacting with the service API directly. +pub use actix_http::{Extensions, Payload, RequestHead, Response, ResponseHead}; +pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; +pub use actix_server::{Server, ServerHandle}; +pub use actix_service::{ + always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform, +}; + +#[cfg(feature = "__compress")] +pub use actix_http::encoding::Decoder as Decompress; + pub use crate::config::{AppConfig, AppService}; #[doc(hidden)] pub use crate::handler::Handler; @@ -14,16 +24,6 @@ pub use crate::types::form::UrlEncoded; pub use crate::types::json::JsonBody; pub use crate::types::readlines::Readlines; -pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, Response, ResponseHead}; -pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; -pub use actix_server::{Server, ServerHandle}; -pub use actix_service::{ - always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform, -}; - -#[cfg(feature = "__compress")] -pub use actix_http::encoding::Decoder as Decompress; - use crate::http::header::ContentEncoding; use actix_router::Patterns; @@ -46,7 +46,6 @@ pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns { patterns } -struct Enc(ContentEncoding); /// Helper trait that allows to set specific encoding for response. pub trait BodyEncoding { @@ -70,6 +69,8 @@ impl BodyEncoding for actix_http::ResponseBuilder { } } +struct Enc(ContentEncoding); + impl BodyEncoding for actix_http::Response { fn get_encoding(&self) -> Option { self.extensions().get::().map(|enc| enc.0) @@ -102,41 +103,3 @@ impl BodyEncoding for crate::HttpResponse { self } } - -// TODO: remove this if it doesn't appear to be needed - -#[allow(dead_code)] -#[derive(Debug)] -pub(crate) enum AnyBody { - None, - Full { body: crate::web::Bytes }, - Boxed { body: actix_http::body::BoxBody }, -} - -impl crate::body::MessageBody for AnyBody { - type Error = crate::BoxError; - - /// Body size hint. - fn size(&self) -> crate::body::BodySize { - match self { - AnyBody::None => crate::body::BodySize::None, - AnyBody::Full { body } => body.size(), - AnyBody::Boxed { body } => body.size(), - } - } - - /// Attempt to pull out the next chunk of body bytes. - fn poll_next( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll>> { - match self.get_mut() { - AnyBody::None => std::task::Poll::Ready(None), - AnyBody::Full { body } => { - let bytes = std::mem::take(body); - std::task::Poll::Ready(Some(Ok(bytes))) - } - AnyBody::Boxed { body } => body.as_pin_mut().poll_next(cx), - } - } -} diff --git a/src/error/internal.rs b/src/error/internal.rs index c766ba83e..37195dc2e 100644 --- a/src/error/internal.rs +++ b/src/error/internal.rs @@ -2,7 +2,7 @@ use std::{cell::RefCell, fmt, io::Write as _}; use actix_http::{ body::BoxBody, - header::{self, IntoHeaderValue as _}, + header::{self, TryIntoHeaderValue as _}, StatusCode, }; use bytes::{BufMut as _, BytesMut}; @@ -128,7 +128,7 @@ macro_rules! error_helper { InternalError::new(err, StatusCode::$status).into() } } - } + }; } error_helper!(ErrorBadRequest, BAD_REQUEST); diff --git a/src/error/mod.rs b/src/error/mod.rs index 90c2c9a61..64df9f553 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -1,8 +1,10 @@ //! Error and Result module -/// This is meant to be a glob import of the whole error module, but rustdoc can't handle -/// shadowing `Error` type, so it is expanded manually. -/// See https://github.com/rust-lang/rust/issues/83375 +// This is meant to be a glob import of the whole error module except for `Error`. Rustdoc can't yet +// correctly resolve the conflicting `Error` type defined in this module, so these re-exports are +// expanded manually. +// +// See pub use actix_http::error::{ BlockingError, ContentTypeError, DispatchError, HttpError, ParseError, PayloadError, }; diff --git a/src/error/response_error.rs b/src/error/response_error.rs index 7260efa1a..e0b4af44c 100644 --- a/src/error/response_error.rs +++ b/src/error/response_error.rs @@ -8,7 +8,7 @@ use std::{ use actix_http::{ body::BoxBody, - header::{self, IntoHeaderValue}, + header::{self, TryIntoHeaderValue}, Response, StatusCode, }; use bytes::BytesMut; diff --git a/src/guard.rs b/src/guard.rs index a5770df89..ebda69cb9 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -1,168 +1,232 @@ -//! Route match guards. +//! Route guards. //! -//! Guards are one of the ways how actix-web router chooses a -//! handler service. In essence it is just a function that accepts a -//! reference to a `RequestHead` instance and returns a boolean. -//! It is possible to add guards to *scopes*, *resources* -//! and *routes*. Actix provide several guards by default, like various -//! http methods, header, etc. To become a guard, type must implement `Guard` -//! trait. Simple functions could be guards as well. +//! Guards are used during routing to help select a matching service or handler using some aspect of +//! the request; though guards should not be used for path matching since it is a built-in function +//! of the Actix Web router. //! -//! Guards can not modify the request object. But it is possible -//! to store extra attributes on a request by using the `Extensions` container. -//! Extensions containers are available via the `RequestHead::extensions()` method. +//! Guards can be used on [`Scope`]s, [`Resource`]s, [`Route`]s, and other custom services. //! +//! Fundamentally, a guard is a predicate function that receives a reference to a request context +//! object and returns a boolean; true if the request _should_ be handled by the guarded service +//! or handler. This interface is defined by the [`Guard`] trait. +//! +//! Commonly-used guards are provided in this module as well as a way of creating a guard from a +//! closure ([`fn_guard`]). The [`Not`], [`Any`], and [`All`] guards are noteworthy, as they can be +//! used to compose other guards in a more flexible and semantic way than calling `.guard(...)` on +//! services multiple times (which might have different combining behavior than you want). +//! +//! There are shortcuts for routes with method guards in the [`web`](crate::web) module: +//! [`web::get()`](crate::web::get), [`web::post()`](crate::web::post), etc. The routes created by +//! the following calls are equivalent: +//! - `web::get()` (recommended form) +//! - `web::route().guard(guard::Get())` +//! +//! Guards can not modify anything about the request. However, it is possible to store extra +//! attributes in the request-local data container obtained with [`GuardContext::req_data_mut`]. +//! +//! Guards can prevent resource definitions from overlapping which, when only considering paths, +//! would result in inaccessible routes. See the [`Host`] guard for an example of virtual hosting. +//! +//! # Examples +//! In the following code, the `/guarded` resource has one defined route whose handler will only be +//! called if the request method is `POST` and there is a request header with name and value equal +//! to `x-guarded` and `secret`, respectively. //! ``` -//! use actix_web::{web, http, dev, guard, App, HttpResponse}; +//! use actix_web::{web, http::Method, guard, HttpResponse}; //! -//! fn main() { -//! App::new().service(web::resource("/index.html").route( -//! web::route() -//! .guard(guard::Post()) -//! .guard(guard::fn_guard(|head| head.method == http::Method::GET)) -//! .to(|| HttpResponse::MethodNotAllowed())) -//! ); -//! } +//! web::resource("/guarded").route( +//! web::route() +//! .guard(guard::Any(guard::Get()).or(guard::Post())) +//! .guard(guard::Header("x-guarded", "secret")) +//! .to(|| HttpResponse::Ok()) +//! ); //! ``` +//! +//! [`Scope`]: crate::Scope::guard() +//! [`Resource`]: crate::Resource::guard() +//! [`Route`]: crate::Route::guard() -#![allow(non_snake_case)] +use std::{ + cell::{Ref, RefMut}, + convert::TryFrom, + rc::Rc, +}; -use std::rc::Rc; -use std::{convert::TryFrom, ops::Deref}; +use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead}; -use actix_http::{header, uri::Uri, Method as HttpMethod, RequestHead}; +use crate::service::ServiceRequest; -/// Trait defines resource guards. Guards are used for route selection. -/// -/// Guards can not modify the request object. But it is possible -/// to store extra attributes on a request by using the `Extensions` container. -/// Extensions containers are available via the `RequestHead::extensions()` method. -pub trait Guard { - /// Check if request matches predicate - fn check(&self, request: &RequestHead) -> bool; +/// Provides access to request parts that are useful during routing. +#[derive(Debug)] +pub struct GuardContext<'a> { + pub(crate) req: &'a ServiceRequest, } -impl Guard for Rc { - fn check(&self, request: &RequestHead) -> bool { - self.deref().check(request) +impl<'a> GuardContext<'a> { + /// Returns reference to the request head. + #[inline] + pub fn head(&self) -> &RequestHead { + self.req.head() + } + + /// Returns reference to the request-local data container. + #[inline] + pub fn req_data(&self) -> Ref<'a, Extensions> { + self.req.req_data() + } + + /// Returns mutable reference to the request-local data container. + #[inline] + pub fn req_data_mut(&self) -> RefMut<'a, Extensions> { + self.req.req_data_mut() } } -/// Create guard object for supplied function. +/// Interface for routing guards. /// +/// See [module level documentation](self) for more. +pub trait Guard { + /// Returns true if predicate condition is met for a given request. + fn check(&self, ctx: &GuardContext<'_>) -> bool; +} + +impl Guard for Rc { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + (**self).check(ctx) + } +} + +/// Creates a guard using the given function. +/// +/// # Examples /// ``` -/// use actix_web::{guard, web, App, HttpResponse}; +/// use actix_web::{guard, web, HttpResponse}; /// -/// fn main() { -/// App::new().service(web::resource("/index.html").route( -/// web::route() -/// .guard( -/// guard::fn_guard( -/// |req| req.headers() -/// .contains_key("content-type"))) -/// .to(|| HttpResponse::MethodNotAllowed())) -/// ); -/// } +/// web::route() +/// .guard(guard::fn_guard(|ctx| { +/// ctx.head().headers().contains_key("content-type") +/// })) +/// .to(|| HttpResponse::Ok()); /// ``` pub fn fn_guard(f: F) -> impl Guard where - F: Fn(&RequestHead) -> bool, + F: Fn(&GuardContext<'_>) -> bool, { FnGuard(f) } -struct FnGuard bool>(F); +struct FnGuard) -> bool>(F); impl Guard for FnGuard where - F: Fn(&RequestHead) -> bool, + F: Fn(&GuardContext<'_>) -> bool, { - fn check(&self, head: &RequestHead) -> bool { - (self.0)(head) + fn check(&self, ctx: &GuardContext<'_>) -> bool { + (self.0)(ctx) } } impl Guard for F where - F: Fn(&RequestHead) -> bool, + F: Fn(&GuardContext<'_>) -> bool, { - fn check(&self, head: &RequestHead) -> bool { - (self)(head) + fn check(&self, ctx: &GuardContext<'_>) -> bool { + (self)(ctx) } } -/// Return guard that matches if any of supplied guards. +/// Creates a guard that matches if any added guards match. /// +/// # Examples +/// The handler below will be called for either request method `GET` or `POST`. /// ``` -/// use actix_web::{web, guard, App, HttpResponse}; +/// use actix_web::{web, guard, HttpResponse}; /// -/// fn main() { -/// App::new().service(web::resource("/index.html").route( -/// web::route() -/// .guard(guard::Any(guard::Get()).or(guard::Post())) -/// .to(|| HttpResponse::MethodNotAllowed())) -/// ); -/// } +/// web::route() +/// .guard( +/// guard::Any(guard::Get()) +/// .or(guard::Post())) +/// .to(|| HttpResponse::Ok()); /// ``` +#[allow(non_snake_case)] pub fn Any(guard: F) -> AnyGuard { - AnyGuard(vec![Box::new(guard)]) + AnyGuard { + guards: vec![Box::new(guard)], + } } -/// Matches any of supplied guards. -pub struct AnyGuard(Vec>); +/// A collection of guards that match if the disjunction of their `check` outcomes is true. +/// +/// That is, only one contained guard needs to match in order for the aggregate guard to match. +/// +/// Construct an `AnyGuard` using [`Any`]. +pub struct AnyGuard { + guards: Vec>, +} impl AnyGuard { - /// Add guard to a list of guards to check + /// Adds new guard to the collection of guards to check. pub fn or(mut self, guard: F) -> Self { - self.0.push(Box::new(guard)); + self.guards.push(Box::new(guard)); self } } impl Guard for AnyGuard { - fn check(&self, req: &RequestHead) -> bool { - for p in &self.0 { - if p.check(req) { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + for guard in &self.guards { + if guard.check(ctx) { return true; } } + false } } -/// Return guard that matches if all of the supplied guards. +/// Creates a guard that matches if all added guards match. /// +/// # Examples +/// The handler below will only be called if the request method is `GET` **and** the specified +/// header name and value match exactly. /// ``` -/// use actix_web::{guard, web, App, HttpResponse}; +/// use actix_web::{guard, web, HttpResponse}; /// -/// fn main() { -/// App::new().service(web::resource("/index.html").route( -/// web::route() -/// .guard( -/// guard::All(guard::Get()).and(guard::Header("content-type", "text/plain"))) -/// .to(|| HttpResponse::MethodNotAllowed())) -/// ); -/// } +/// web::route() +/// .guard( +/// guard::All(guard::Get()) +/// .and(guard::Header("accept", "text/plain")) +/// ) +/// .to(|| HttpResponse::Ok()); /// ``` +#[allow(non_snake_case)] pub fn All(guard: F) -> AllGuard { - AllGuard(vec![Box::new(guard)]) + AllGuard { + guards: vec![Box::new(guard)], + } } -/// Matches if all of supplied guards. -pub struct AllGuard(Vec>); +/// A collection of guards that match if the conjunction of their `check` outcomes is true. +/// +/// That is, **all** contained guard needs to match in order for the aggregate guard to match. +/// +/// Construct an `AllGuard` using [`All`]. +pub struct AllGuard { + guards: Vec>, +} impl AllGuard { - /// Add new guard to the list of guards to check + /// Adds new guard to the collection of guards to check. pub fn and(mut self, guard: F) -> Self { - self.0.push(Box::new(guard)); + self.guards.push(Box::new(guard)); self } } impl Guard for AllGuard { - fn check(&self, request: &RequestHead) -> bool { - for p in &self.0 { - if !p.check(request) { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + for guard in &self.guards { + if !guard.check(ctx) { return false; } } @@ -170,161 +234,212 @@ impl Guard for AllGuard { } } -/// Return guard that matches if supplied guard does not match. -pub fn Not(guard: F) -> NotGuard { - NotGuard(Box::new(guard)) -} +/// Wraps a guard and inverts the outcome of it's `Guard` implementation. +/// +/// # Examples +/// The handler below will be called for any request method apart from `GET`. +/// ``` +/// use actix_web::{guard, web, HttpResponse}; +/// +/// web::route() +/// .guard(guard::Not(guard::Get())) +/// .to(|| HttpResponse::Ok()); +/// ``` +pub struct Not(pub G); -#[doc(hidden)] -pub struct NotGuard(Box); - -impl Guard for NotGuard { - fn check(&self, request: &RequestHead) -> bool { - !self.0.check(request) +impl Guard for Not { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + !self.0.check(ctx) } } -/// HTTP method guard. -#[doc(hidden)] -pub struct MethodGuard(HttpMethod); - -impl Guard for MethodGuard { - fn check(&self, request: &RequestHead) -> bool { - request.method == self.0 - } -} - -/// Guard to match *GET* HTTP method. -pub fn Get() -> MethodGuard { - MethodGuard(HttpMethod::GET) -} - -/// Predicate to match *POST* HTTP method. -pub fn Post() -> MethodGuard { - MethodGuard(HttpMethod::POST) -} - -/// Predicate to match *PUT* HTTP method. -pub fn Put() -> MethodGuard { - MethodGuard(HttpMethod::PUT) -} - -/// Predicate to match *DELETE* HTTP method. -pub fn Delete() -> MethodGuard { - MethodGuard(HttpMethod::DELETE) -} - -/// Predicate to match *HEAD* HTTP method. -pub fn Head() -> MethodGuard { - MethodGuard(HttpMethod::HEAD) -} - -/// Predicate to match *OPTIONS* HTTP method. -pub fn Options() -> MethodGuard { - MethodGuard(HttpMethod::OPTIONS) -} - -/// Predicate to match *CONNECT* HTTP method. -pub fn Connect() -> MethodGuard { - MethodGuard(HttpMethod::CONNECT) -} - -/// Predicate to match *PATCH* HTTP method. -pub fn Patch() -> MethodGuard { - MethodGuard(HttpMethod::PATCH) -} - -/// Predicate to match *TRACE* HTTP method. -pub fn Trace() -> MethodGuard { - MethodGuard(HttpMethod::TRACE) -} - -/// Predicate to match specified HTTP method. -pub fn Method(method: HttpMethod) -> MethodGuard { +/// Creates a guard that matches a specified HTTP method. +#[allow(non_snake_case)] +pub fn Method(method: HttpMethod) -> impl Guard { MethodGuard(method) } -/// Return predicate that matches if request contains specified header and -/// value. -pub fn Header(name: &'static str, value: &'static str) -> HeaderGuard { +/// HTTP method guard. +struct MethodGuard(HttpMethod); + +impl Guard for MethodGuard { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + ctx.head().method == self.0 + } +} + +macro_rules! method_guard { + ($method_fn:ident, $method_const:ident) => { + paste::paste! { + #[doc = " Creates a guard that matches the `" $method_const "` request method."] + /// + /// # Examples + #[doc = " The route in this example will only respond to `" $method_const "` requests."] + /// ``` + /// use actix_web::{guard, web, HttpResponse}; + /// + /// web::route() + #[doc = " .guard(guard::" $method_fn "())"] + /// .to(|| HttpResponse::Ok()); + /// ``` + #[allow(non_snake_case)] + pub fn $method_fn() -> impl Guard { + MethodGuard(HttpMethod::$method_const) + } + } + }; +} + +method_guard!(Get, GET); +method_guard!(Post, POST); +method_guard!(Put, PUT); +method_guard!(Delete, DELETE); +method_guard!(Head, HEAD); +method_guard!(Options, OPTIONS); +method_guard!(Connect, CONNECT); +method_guard!(Patch, PATCH); +method_guard!(Trace, TRACE); + +/// Creates a guard that matches if request contains given header name and value. +/// +/// # Examples +/// The handler below will be called when the request contains an `x-guarded` header with value +/// equal to `secret`. +/// ``` +/// use actix_web::{guard, web, HttpResponse}; +/// +/// web::route() +/// .guard(guard::Header("x-guarded", "secret")) +/// .to(|| HttpResponse::Ok()); +/// ``` +#[allow(non_snake_case)] +pub fn Header(name: &'static str, value: &'static str) -> impl Guard { HeaderGuard( header::HeaderName::try_from(name).unwrap(), header::HeaderValue::from_static(value), ) } -#[doc(hidden)] -pub struct HeaderGuard(header::HeaderName, header::HeaderValue); +struct HeaderGuard(header::HeaderName, header::HeaderValue); impl Guard for HeaderGuard { - fn check(&self, req: &RequestHead) -> bool { - if let Some(val) = req.headers.get(&self.0) { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + if let Some(val) = ctx.head().headers.get(&self.0) { return val == self.1; } + false } } -/// Return predicate that matches if request contains specified Host name. +/// Creates a guard that matches requests targetting a specific host. /// +/// # Matching Host +/// This guard will: +/// - match against the `Host` header, if present; +/// - fall-back to matching against the request target's host, if present; +/// - return false if host cannot be determined; +/// +/// # Matching Scheme +/// Optionally, this guard can match against the host's scheme. Set the scheme for matching using +/// `Host(host).scheme(protocol)`. If the request's scheme cannot be determined, it will not prevent +/// the guard from matching successfully. +/// +/// # Examples +/// The [module-level documentation](self) has an example of virtual hosting using `Host` guards. +/// +/// The example below additionally guards on the host URI's scheme. This could allow routing to +/// different handlers for `http:` vs `https:` visitors; to redirect, for example. /// ``` -/// use actix_web::{web, guard::Host, App, HttpResponse}; +/// use actix_web::{web, guard::Host, HttpResponse}; /// -/// fn main() { -/// App::new().service( -/// web::resource("/index.html") -/// .guard(Host("www.rust-lang.org")) -/// .to(|| HttpResponse::MethodNotAllowed()) +/// web::scope("/admin") +/// .guard(Host("admin.rust-lang.org").scheme("https")) +/// .default_service(web::to(|| HttpResponse::Ok().body("admin connection is secure"))); +/// ``` +/// +/// The `Host` guard can be used to set up some form of [virtual hosting] within a single app. +/// Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard +/// definitions they become safe to use in this way. Without these host guards, only routes under +/// the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1` +/// and `localhost` as the `Host` guards. +/// ``` +/// use actix_web::{web, http::Method, guard, App, HttpResponse}; +/// +/// App::new() +/// .service( +/// web::scope("") +/// .guard(guard::Host("www.rust-lang.org")) +/// .default_service(web::to(|| HttpResponse::Ok().body("marketing site"))), +/// ) +/// .service( +/// web::scope("") +/// .guard(guard::Host("play.rust-lang.org")) +/// .default_service(web::to(|| HttpResponse::Ok().body("playground frontend"))), /// ); -/// } /// ``` -pub fn Host>(host: H) -> HostGuard { - HostGuard(host.as_ref().to_string(), None) +/// +/// [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting +#[allow(non_snake_case)] +pub fn Host(host: impl AsRef) -> HostGuard { + HostGuard { + host: host.as_ref().to_string(), + scheme: None, + } } fn get_host_uri(req: &RequestHead) -> Option { - use core::str::FromStr; req.headers .get(header::HOST) .and_then(|host_value| host_value.to_str().ok()) .or_else(|| req.uri.host()) - .map(|host: &str| Uri::from_str(host).ok()) - .and_then(|host_success| host_success) + .and_then(|host| host.parse().ok()) } #[doc(hidden)] -pub struct HostGuard(String, Option); +pub struct HostGuard { + host: String, + scheme: Option, +} impl HostGuard { /// Set request scheme to match pub fn scheme>(mut self, scheme: H) -> HostGuard { - self.1 = Some(scheme.as_ref().to_string()); + self.scheme = Some(scheme.as_ref().to_string()); self } } impl Guard for HostGuard { - fn check(&self, req: &RequestHead) -> bool { - let req_host_uri = if let Some(uri) = get_host_uri(req) { - uri - } else { - return false; + fn check(&self, ctx: &GuardContext<'_>) -> bool { + // parse host URI from header or request target + let req_host_uri = match get_host_uri(ctx.head()) { + Some(uri) => uri, + + // no match if host cannot be determined + None => return false, }; - if let Some(uri_host) = req_host_uri.host() { - if self.0 != uri_host { - return false; - } - } else { - return false; + match req_host_uri.host() { + // fall through to scheme checks + Some(uri_host) if self.host == uri_host => {} + + // Either: + // - request's host does not match guard's host; + // - It was possible that the parsed URI from request target did not contain a host. + _ => return false, } - if let Some(ref scheme) = self.1 { + if let Some(ref scheme) = self.scheme { if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() { return scheme == req_host_uri_scheme; } + + // TODO: is the the correct behavior? + // falls through if scheme cannot be determined } + // all conditions passed true } } @@ -337,171 +452,214 @@ mod tests { use crate::test::TestRequest; #[test] - fn test_header() { + fn header_match() { let req = TestRequest::default() .insert_header((header::TRANSFER_ENCODING, "chunked")) - .to_http_request(); + .to_srv_request(); - let pred = Header("transfer-encoding", "chunked"); - assert!(pred.check(req.head())); + let hdr = Header("transfer-encoding", "chunked"); + assert!(hdr.check(&req.guard_ctx())); - let pred = Header("transfer-encoding", "other"); - assert!(!pred.check(req.head())); + let hdr = Header("transfer-encoding", "other"); + assert!(!hdr.check(&req.guard_ctx())); - let pred = Header("content-type", "other"); - assert!(!pred.check(req.head())); + let hdr = Header("content-type", "chunked"); + assert!(!hdr.check(&req.guard_ctx())); + + let hdr = Header("content-type", "other"); + assert!(!hdr.check(&req.guard_ctx())); } #[test] - fn test_host() { + fn host_from_header() { let req = TestRequest::default() .insert_header(( header::HOST, header::HeaderValue::from_static("www.rust-lang.org"), )) - .to_http_request(); + .to_srv_request(); - let pred = Host("www.rust-lang.org"); - assert!(pred.check(req.head())); + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); - let pred = Host("www.rust-lang.org").scheme("https"); - assert!(pred.check(req.head())); + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); - let pred = Host("blog.rust-lang.org"); - assert!(!pred.check(req.head())); + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("blog.rust-lang.org").scheme("https"); - assert!(!pred.check(req.head())); + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("crates.io"); - assert!(!pred.check(req.head())); + let host = Host("crates.io"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("localhost"); - assert!(!pred.check(req.head())); + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); } #[test] - fn test_host_scheme() { + fn host_without_header() { + let req = TestRequest::default() + .uri("www.rust-lang.org") + .to_srv_request(); + + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("crates.io"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); + } + + #[test] + fn host_scheme() { let req = TestRequest::default() .insert_header(( header::HOST, header::HeaderValue::from_static("https://www.rust-lang.org"), )) - .to_http_request(); + .to_srv_request(); - let pred = Host("www.rust-lang.org").scheme("https"); - assert!(pred.check(req.head())); + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); - let pred = Host("www.rust-lang.org"); - assert!(pred.check(req.head())); + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); - let pred = Host("www.rust-lang.org").scheme("http"); - assert!(!pred.check(req.head())); + let host = Host("www.rust-lang.org").scheme("http"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("blog.rust-lang.org"); - assert!(!pred.check(req.head())); + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("blog.rust-lang.org").scheme("https"); - assert!(!pred.check(req.head())); + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("crates.io").scheme("https"); - assert!(!pred.check(req.head())); + let host = Host("crates.io").scheme("https"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("localhost"); - assert!(!pred.check(req.head())); + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); } #[test] - fn test_host_without_header() { + fn method_guards() { + let get_req = TestRequest::get().to_srv_request(); + let post_req = TestRequest::post().to_srv_request(); + + assert!(Get().check(&get_req.guard_ctx())); + assert!(!Get().check(&post_req.guard_ctx())); + + assert!(Post().check(&post_req.guard_ctx())); + assert!(!Post().check(&get_req.guard_ctx())); + + let req = TestRequest::put().to_srv_request(); + assert!(Put().check(&req.guard_ctx())); + assert!(!Put().check(&get_req.guard_ctx())); + + let req = TestRequest::patch().to_srv_request(); + assert!(Patch().check(&req.guard_ctx())); + assert!(!Patch().check(&get_req.guard_ctx())); + + let r = TestRequest::delete().to_srv_request(); + assert!(Delete().check(&r.guard_ctx())); + assert!(!Delete().check(&get_req.guard_ctx())); + + let req = TestRequest::default().method(Method::HEAD).to_srv_request(); + assert!(Head().check(&req.guard_ctx())); + assert!(!Head().check(&get_req.guard_ctx())); + let req = TestRequest::default() - .uri("www.rust-lang.org") - .to_http_request(); - - let pred = Host("www.rust-lang.org"); - assert!(pred.check(req.head())); - - let pred = Host("www.rust-lang.org").scheme("https"); - assert!(pred.check(req.head())); - - let pred = Host("blog.rust-lang.org"); - assert!(!pred.check(req.head())); - - let pred = Host("blog.rust-lang.org").scheme("https"); - assert!(!pred.check(req.head())); - - let pred = Host("crates.io"); - assert!(!pred.check(req.head())); - - let pred = Host("localhost"); - assert!(!pred.check(req.head())); - } - - #[test] - fn test_methods() { - let req = TestRequest::default().to_http_request(); - let req2 = TestRequest::default() - .method(Method::POST) - .to_http_request(); - - assert!(Get().check(req.head())); - assert!(!Get().check(req2.head())); - assert!(Post().check(req2.head())); - assert!(!Post().check(req.head())); - - let r = TestRequest::default().method(Method::PUT).to_http_request(); - assert!(Put().check(r.head())); - assert!(!Put().check(req.head())); - - let r = TestRequest::default() - .method(Method::DELETE) - .to_http_request(); - assert!(Delete().check(r.head())); - assert!(!Delete().check(req.head())); - - let r = TestRequest::default() - .method(Method::HEAD) - .to_http_request(); - assert!(Head().check(r.head())); - assert!(!Head().check(req.head())); - - let r = TestRequest::default() .method(Method::OPTIONS) - .to_http_request(); - assert!(Options().check(r.head())); - assert!(!Options().check(req.head())); + .to_srv_request(); + assert!(Options().check(&req.guard_ctx())); + assert!(!Options().check(&get_req.guard_ctx())); - let r = TestRequest::default() + let req = TestRequest::default() .method(Method::CONNECT) - .to_http_request(); - assert!(Connect().check(r.head())); - assert!(!Connect().check(req.head())); + .to_srv_request(); + assert!(Connect().check(&req.guard_ctx())); + assert!(!Connect().check(&get_req.guard_ctx())); - let r = TestRequest::default() - .method(Method::PATCH) - .to_http_request(); - assert!(Patch().check(r.head())); - assert!(!Patch().check(req.head())); - - let r = TestRequest::default() + let req = TestRequest::default() .method(Method::TRACE) - .to_http_request(); - assert!(Trace().check(r.head())); - assert!(!Trace().check(req.head())); + .to_srv_request(); + assert!(Trace().check(&req.guard_ctx())); + assert!(!Trace().check(&get_req.guard_ctx())); } #[test] - fn test_preds() { - let r = TestRequest::default() + fn aggregate_any() { + let req = TestRequest::default() .method(Method::TRACE) - .to_http_request(); + .to_srv_request(); - assert!(Not(Get()).check(r.head())); - assert!(!Not(Trace()).check(r.head())); + assert!(Any(Trace()).check(&req.guard_ctx())); + assert!(Any(Trace()).or(Get()).check(&req.guard_ctx())); + assert!(!Any(Get()).or(Get()).check(&req.guard_ctx())); + } - assert!(All(Trace()).and(Trace()).check(r.head())); - assert!(!All(Get()).and(Trace()).check(r.head())); + #[test] + fn aggregate_all() { + let req = TestRequest::default() + .method(Method::TRACE) + .to_srv_request(); - assert!(Any(Get()).or(Trace()).check(r.head())); - assert!(!Any(Get()).or(Get()).check(r.head())); + assert!(All(Trace()).check(&req.guard_ctx())); + assert!(All(Trace()).and(Trace()).check(&req.guard_ctx())); + assert!(!All(Trace()).and(Get()).check(&req.guard_ctx())); + } + + #[test] + fn nested_not() { + let req = TestRequest::default().to_srv_request(); + + let get = Get(); + assert!(get.check(&req.guard_ctx())); + + let not_get = Not(get); + assert!(!not_get.check(&req.guard_ctx())); + + let not_not_get = Not(not_get); + assert!(not_not_get.check(&req.guard_ctx())); + } + + #[test] + fn function_guard() { + let domain = "rust-lang.org".to_owned(); + let guard = fn_guard(|ctx| ctx.head().uri.host().unwrap().ends_with(&domain)); + + let req = TestRequest::default() + .uri("blog.rust-lang.org") + .to_srv_request(); + assert!(guard.check(&req.guard_ctx())); + + let req = TestRequest::default().uri("crates.io").to_srv_request(); + assert!(!guard.check(&req.guard_ctx())); + } + + #[test] + fn mega_nesting() { + let guard = fn_guard(|ctx| All(Not(Any(Not(Trace())))).check(ctx)); + + let req = TestRequest::default().to_srv_request(); + assert!(!guard.check(&req.guard_ctx())); + + let req = TestRequest::default() + .method(Method::TRACE) + .to_srv_request(); + assert!(guard.check(&req.guard_ctx())); } } diff --git a/src/handler.rs b/src/handler.rs index e543ecc7f..d458e22e1 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -3,35 +3,92 @@ use std::future::Future; use actix_service::{boxed, fn_service}; use crate::{ - body::MessageBody, service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse}, - BoxError, FromRequest, HttpResponse, Responder, + FromRequest, HttpResponse, Responder, }; -/// A request handler is an async function that accepts zero or more parameters that can be -/// extracted from a request (i.e., [`impl FromRequest`]) and returns a type that can be converted -/// into an [`HttpResponse`] (that is, it impls the [`Responder`] trait). +/// The interface for request handlers. /// -/// If you got the error `the trait Handler<_, _, _> is not implemented`, then your function is not -/// a valid handler. See for more information. +/// # What Is A Request Handler +/// A request handler has three requirements: +/// 1. It is an async function (or a function/closure that returns an appropriate future); +/// 1. The function accepts zero or more parameters that implement [`FromRequest`]; +/// 1. The async function (or future) resolves to a type that can be converted into an +/// [`HttpResponse`] (i.e., it implements the [`Responder`] trait). /// -/// [`impl FromRequest`]: crate::FromRequest -pub trait Handler: Clone + 'static -where - R: Future, - R::Output: Responder, -{ - fn call(&self, param: T) -> R; +/// # Compiler Errors +/// If you get the error `the trait Handler<_> is not implemented`, then your handler does not +/// fulfill one or more of the above requirements. +/// +/// Unfortunately we cannot provide a better compile error message (while keeping the trait's +/// flexibility) unless a stable alternative to [`#[rustc_on_unimplemented]`][on_unimpl] is added +/// to Rust. +/// +/// # How Do Handlers Receive Variable Numbers Of Arguments +/// Rest assured there is no macro magic here; it's just traits. +/// +/// The first thing to note is that [`FromRequest`] is implemented for tuples (up to 12 in length). +/// +/// Secondly, the `Handler` trait is implemented for functions (up to an [arity] of 12) in a way +/// that aligns their parameter positions with a corresponding tuple of types (becoming the `Args` +/// type parameter for this trait). +/// +/// Thanks to Rust's type system, Actix Web can infer the function parameter types. During the +/// extraction step, the parameter types are described as a tuple type, [`from_request`] is run on +/// that tuple, and the `Handler::call` implementation for that particular function arity +/// destructures the tuple into it's component types and calls your handler function with them. +/// +/// In pseudo-code the process looks something like this: +/// ```ignore +/// async fn my_handler(body: String, state: web::Data) -> impl Responder { +/// ... +/// } +/// +/// // the function params above described as a tuple, names do not matter, only position +/// type InferredMyHandlerArgs = (String, web::Data); +/// +/// // create tuple of arguments to be passed to handler +/// let args = InferredMyHandlerArgs::from_request(&request, &payload).await; +/// +/// // call handler with argument tuple +/// let response = Handler::call(&my_handler, args).await; +/// +/// // which is effectively... +/// +/// let (body, state) = args; +/// let response = my_handler(body, state).await; +/// ``` +/// +/// This is the source code for the 2-parameter implementation of `Handler` to help illustrate the +/// bounds of the handler call after argument extraction: +/// ```ignore +/// impl Handler<(Arg1, Arg2), R> for Func +/// where +/// Func: Fn(Arg1, Arg2) -> R + Clone + 'static, +/// R: Future, +/// R::Output: Responder, +/// { +/// fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> R { +/// (self)(arg1, arg2) +/// } +/// } +/// ``` +/// +/// [arity]: https://en.wikipedia.org/wiki/Arity +/// [`from_request`]: FromRequest::from_request +/// [on_unimpl]: https://github.com/rust-lang/rust/issues/29628 +pub trait Handler: Clone + 'static { + type Output; + type Future: Future; + + fn call(&self, args: Args) -> Self::Future; } -pub(crate) fn handler_service(handler: F) -> BoxedHttpServiceFactory +pub(crate) fn handler_service(handler: F) -> BoxedHttpServiceFactory where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, - ::Body: MessageBody, - <::Body as MessageBody>::Error: Into, + F: Handler, + Args: FromRequest, + F::Output: Responder, { boxed::factory(fn_service(move |req: ServiceRequest| { let handler = handler.clone(); @@ -39,7 +96,7 @@ where async move { let (req, mut payload) = req.into_parts(); - let res = match T::from_request(&req, &mut payload).await { + let res = match Args::from_request(&req, &mut payload).await { Err(err) => HttpResponse::from_error(err), Ok(data) => handler @@ -59,17 +116,20 @@ where /// /// # Examples /// ```ignore -/// factory_tuple! {} // implements Handler for types: fn() -> Res -/// factory_tuple! { A B C } // implements Handler for types: fn(A, B, C) -> Res +/// factory_tuple! {} // implements Handler for types: fn() -> R +/// factory_tuple! { A B C } // implements Handler for types: fn(A, B, C) -> R /// ``` macro_rules! factory_tuple ({ $($param:ident)* } => { - impl Handler<($($param,)*), Res> for Func - where Func: Fn($($param),*) -> Res + Clone + 'static, - Res: Future, - Res::Output: Responder, + impl Handler<($($param,)*)> for Func + where Func: Fn($($param),*) -> Fut + Clone + 'static, + Fut: Future, { + type Output = Fut::Output; + type Future = Fut; + + #[inline] #[allow(non_snake_case)] - fn call(&self, ($($param,)*): ($($param,)*)) -> Res { + fn call(&self, ($($param,)*): ($($param,)*)) -> Self::Future { (self)($($param,)*) } } diff --git a/src/http/header/content_disposition.rs b/src/http/header/content_disposition.rs index 945a58f7f..26a9d8e76 100644 --- a/src/http/header/content_disposition.rs +++ b/src/http/header/content_disposition.rs @@ -14,7 +14,7 @@ use once_cell::sync::Lazy; use regex::Regex; use std::fmt::{self, Write}; -use super::{ExtendedValue, Header, IntoHeaderValue, Writer}; +use super::{ExtendedValue, Header, TryIntoHeaderValue, Writer}; use crate::http::header; /// Split at the index of the first `needle` if it exists or at the end. @@ -454,7 +454,7 @@ impl ContentDisposition { } } -impl IntoHeaderValue for ContentDisposition { +impl TryIntoHeaderValue for ContentDisposition { type Error = header::InvalidHeaderValue; fn try_into_value(self) -> Result { diff --git a/src/http/header/content_range.rs b/src/http/header/content_range.rs index 90b3f7fe2..bcbe77e66 100644 --- a/src/http/header/content_range.rs +++ b/src/http/header/content_range.rs @@ -3,7 +3,7 @@ use std::{ str::FromStr, }; -use super::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer, CONTENT_RANGE}; +use super::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue, Writer, CONTENT_RANGE}; use crate::error::ParseError; crate::http::header::common_header! { @@ -196,7 +196,7 @@ impl Display for ContentRangeSpec { } } -impl IntoHeaderValue for ContentRangeSpec { +impl TryIntoHeaderValue for ContentRangeSpec { type Error = InvalidHeaderValue; fn try_into_value(self) -> Result { diff --git a/src/http/header/entity.rs b/src/http/header/entity.rs index 50b40b7b2..76fe39f23 100644 --- a/src/http/header/entity.rs +++ b/src/http/header/entity.rs @@ -3,7 +3,7 @@ use std::{ str::FromStr, }; -use super::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer}; +use super::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue, Writer}; /// check that each char in the slice is either: /// 1. `%x21`, or @@ -159,7 +159,7 @@ impl FromStr for EntityTag { } } -impl IntoHeaderValue for EntityTag { +impl TryIntoHeaderValue for EntityTag { type Error = InvalidHeaderValue; fn try_into_value(self) -> Result { diff --git a/src/http/header/if_range.rs b/src/http/header/if_range.rs index 5af9255f6..b845fb3bf 100644 --- a/src/http/header/if_range.rs +++ b/src/http/header/if_range.rs @@ -1,8 +1,8 @@ use std::fmt::{self, Display, Write}; use super::{ - from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, IntoHeaderValue, - InvalidHeaderValue, Writer, + from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, InvalidHeaderValue, + TryIntoHeaderValue, Writer, }; use crate::error::ParseError; use crate::http::header; @@ -96,7 +96,7 @@ impl Display for IfRange { } } -impl IntoHeaderValue for IfRange { +impl TryIntoHeaderValue for IfRange { type Error = InvalidHeaderValue; fn try_into_value(self) -> Result { diff --git a/src/http/header/macros.rs b/src/http/header/macros.rs index ca3792a37..25f40a52b 100644 --- a/src/http/header/macros.rs +++ b/src/http/header/macros.rs @@ -125,7 +125,7 @@ macro_rules! common_header { } } - impl $crate::http::header::IntoHeaderValue for $id { + impl $crate::http::header::TryIntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; #[inline] @@ -172,7 +172,7 @@ macro_rules! common_header { } } - impl $crate::http::header::IntoHeaderValue for $id { + impl $crate::http::header::TryIntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; #[inline] @@ -211,7 +211,7 @@ macro_rules! common_header { } } - impl $crate::http::header::IntoHeaderValue for $id { + impl $crate::http::header::TryIntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; #[inline] @@ -266,7 +266,7 @@ macro_rules! common_header { } } - impl $crate::http::header::IntoHeaderValue for $id { + impl $crate::http::header::TryIntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; #[inline] diff --git a/src/http/header/range.rs b/src/http/header/range.rs index c1d60f1ee..68028f53a 100644 --- a/src/http/header/range.rs +++ b/src/http/header/range.rs @@ -6,7 +6,7 @@ use std::{ use actix_http::{error::ParseError, header, HttpMessage}; -use super::{Header, HeaderName, HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer}; +use super::{Header, HeaderName, HeaderValue, InvalidHeaderValue, TryIntoHeaderValue, Writer}; /// `Range` header, defined /// in [RFC 7233 §3.1](https://datatracker.ietf.org/doc/html/rfc7233#section-3.1) @@ -274,7 +274,7 @@ impl Header for Range { } } -impl IntoHeaderValue for Range { +impl TryIntoHeaderValue for Range { type Error = InvalidHeaderValue; fn try_into_value(self) -> Result { diff --git a/src/http/mod.rs b/src/http/mod.rs index bbd94a60f..2581532cd 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -2,4 +2,5 @@ pub mod header; +// TODO: figure out how best to expose http::Error vs actix_http::Error pub use actix_http::{uri, ConnectionType, Error, Method, StatusCode, Uri, Version}; diff --git a/src/info.rs b/src/info.rs index d928a1e63..ce1ef97c6 100644 --- a/src/info.rs +++ b/src/info.rs @@ -1,4 +1,4 @@ -use std::{cell::Ref, convert::Infallible, net::SocketAddr}; +use std::{convert::Infallible, net::SocketAddr}; use actix_utils::future::{err, ok, Ready}; use derive_more::{Display, Error}; @@ -67,20 +67,12 @@ fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option< pub struct ConnectionInfo { host: String, scheme: String, - remote_addr: Option, + peer_addr: Option, realip_remote_addr: Option, } impl ConnectionInfo { - /// Create *ConnectionInfo* instance for a request. - pub fn get<'a>(req: &'a RequestHead, cfg: &AppConfig) -> Ref<'a, Self> { - if !req.extensions().contains::() { - req.extensions_mut().insert(ConnectionInfo::new(req, cfg)); - } - Ref::map(req.extensions(), |e| e.get().unwrap()) - } - - fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo { + pub(crate) fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo { let mut host = None; let mut scheme = None; let mut realip_remote_addr = None; @@ -142,67 +134,70 @@ impl ConnectionInfo { .or_else(|| first_header_value(req, &*X_FORWARDED_FOR)) .map(str::to_owned); - let remote_addr = req.peer_addr.map(|addr| addr.to_string()); + let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string()); ConnectionInfo { host, scheme, - remote_addr, + peer_addr, realip_remote_addr, } } + /// Real IP (remote address) of client that initiated request. + /// + /// The address is resolved through the following, in order: + /// - `Forwarded` header + /// - `X-Forwarded-For` header + /// - peer address of opened socket (same as [`remote_addr`](Self::remote_addr)) + /// + /// # Security + /// Do not use this function for security purposes unless you can be sure that the `Forwarded` + /// and `X-Forwarded-For` headers cannot be spoofed by the client. If you are running without a + /// proxy then [obtaining the peer address](Self::peer_addr) would be more appropriate. + #[inline] + pub fn realip_remote_addr(&self) -> Option<&str> { + self.realip_remote_addr + .as_deref() + .or_else(|| self.peer_addr.as_deref()) + } + + /// Returns serialized IP address of the peer connection. + /// + /// See [`HttpRequest::peer_addr`] for more details. + #[inline] + pub fn peer_addr(&self) -> Option<&str> { + self.peer_addr.as_deref() + } + + /// Hostname of the request. + /// + /// Hostname is resolved through the following, in order: + /// - `Forwarded` header + /// - `X-Forwarded-Host` header + /// - `Host` header + /// - request target / URI + /// - configured server hostname + #[inline] + pub fn host(&self) -> &str { + &self.host + } + /// Scheme of the request. /// - /// Scheme is resolved through the following headers, in this order: - /// - /// - Forwarded - /// - X-Forwarded-Proto - /// - Uri + /// Scheme is resolved through the following, in order: + /// - `Forwarded` header + /// - `X-Forwarded-Proto` header + /// - request target / URI #[inline] pub fn scheme(&self) -> &str { &self.scheme } - /// Hostname of the request. - /// - /// Hostname is resolved through the following headers, in this order: - /// - /// - Forwarded - /// - X-Forwarded-Host - /// - Host - /// - Uri - /// - Server hostname - pub fn host(&self) -> &str { - &self.host - } - - /// Remote address of the connection. - /// - /// Get remote_addr address from socket address. + #[doc(hidden)] + #[deprecated(since = "4.0.0", note = "Renamed to `peer_addr`.")] pub fn remote_addr(&self) -> Option<&str> { - self.remote_addr.as_deref() - } - - /// Real IP (remote address) of client that initiated request. - /// - /// The address is resolved through the following headers, in this order: - /// - /// - Forwarded - /// - X-Forwarded-For - /// - remote_addr name of opened socket - /// - /// # Security - /// Do not use this function for security purposes, unless you can ensure the Forwarded and - /// X-Forwarded-For headers cannot be spoofed by the client. If you want the client's socket - /// address explicitly, use [`HttpRequest::peer_addr()`][peer_addr] instead. - /// - /// [peer_addr]: crate::web::HttpRequest::peer_addr() - #[inline] - pub fn realip_remote_addr(&self) -> Option<&str> { - self.realip_remote_addr - .as_deref() - .or_else(|| self.remote_addr.as_deref()) + self.peer_addr() } } @@ -217,7 +212,7 @@ impl FromRequest for ConnectionInfo { /// Extractor for peer's socket address. /// -/// Also see [`HttpRequest::peer_addr`]. +/// Also see [`HttpRequest::peer_addr`] and [`ConnectionInfo::peer_addr`]. /// /// # Examples /// ``` @@ -440,13 +435,37 @@ mod tests { #[actix_rt::test] async fn peer_addr_extract() { + let req = TestRequest::default().to_http_request(); + let res = PeerAddr::extract(&req).await; + assert!(res.is_err()); + let addr = "127.0.0.1:8080".parse().unwrap(); let req = TestRequest::default().peer_addr(addr).to_http_request(); let peer_addr = PeerAddr::extract(&req).await.unwrap(); assert_eq!(peer_addr, PeerAddr(addr)); + } + #[actix_rt::test] + async fn remote_address() { let req = TestRequest::default().to_http_request(); - let res = PeerAddr::extract(&req).await; - assert!(res.is_err()); + let res = ConnectionInfo::extract(&req).await.unwrap(); + assert!(res.peer_addr().is_none()); + + let addr = "127.0.0.1:8080".parse().unwrap(); + let req = TestRequest::default().peer_addr(addr).to_http_request(); + let conn_info = ConnectionInfo::extract(&req).await.unwrap(); + assert_eq!(conn_info.peer_addr().unwrap(), "127.0.0.1"); + } + + #[actix_rt::test] + async fn real_ip_from_socket_addr() { + let req = TestRequest::default().to_http_request(); + let res = ConnectionInfo::extract(&req).await.unwrap(); + assert!(res.realip_remote_addr().is_none()); + + let addr = "127.0.0.1:8080".parse().unwrap(); + let req = TestRequest::default().peer_addr(addr).to_http_request(); + let conn_info = ConnectionInfo::extract(&req).await.unwrap(); + assert_eq!(conn_info.realip_remote_addr().unwrap(), "127.0.0.1"); } } diff --git a/src/lib.rs b/src/lib.rs index f6ec4082a..5f5b915b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,7 +65,7 @@ //! * `secure-cookies` - secure cookies support #![deny(rust_2018_idioms, nonstandard_style)] -#![allow(clippy::needless_doctest_main, clippy::type_complexity)] +#![warn(future_incompatible)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] @@ -85,7 +85,6 @@ pub mod middleware; mod request; mod request_data; mod resource; -mod responder; mod response; mod rmap; mod route; @@ -106,14 +105,13 @@ pub use cookie; pub use crate::app::App; pub use crate::error::{Error, ResponseError, Result}; pub use crate::extract::FromRequest; +pub use crate::handler::Handler; pub use crate::request::HttpRequest; pub use crate::resource::Resource; -pub use crate::responder::Responder; -pub use crate::response::{HttpResponse, HttpResponseBuilder}; +pub use crate::response::{CustomizeResponder, HttpResponse, HttpResponseBuilder, Responder}; pub use crate::route::Route; pub use crate::scope::Scope; pub use crate::server::HttpServer; -// TODO: is exposing the error directly really needed -pub use crate::types::{Either, EitherExtractError}; +pub use crate::types::Either; pub(crate) type BoxError = Box; diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs index e6ef1806f..18c9ff6a7 100644 --- a/src/middleware/compat.rs +++ b/src/middleware/compat.rs @@ -6,15 +6,18 @@ use std::{ task::{Context, Poll}, }; -use actix_http::body::MessageBody; -use actix_service::{Service, Transform}; use futures_core::{future::LocalBoxFuture, ready}; use pin_project_lite::pin_project; -use crate::{error::Error, service::ServiceResponse}; +use crate::{ + body::{BoxBody, MessageBody}, + dev::{Service, Transform}, + error::Error, + service::ServiceResponse, +}; /// Middleware for enabling any middleware to be used in [`Resource::wrap`](crate::Resource::wrap), -/// [`Scope::wrap`](crate::Scope::wrap) and [`Condition`](super::Condition). +/// and [`Condition`](super::Condition). /// /// # Examples /// ``` @@ -35,6 +38,15 @@ pub struct Compat { transform: T, } +#[cfg(test)] +impl Compat { + pub(crate) fn noop() -> Self { + Self { + transform: super::Noop, + } + } +} + impl Compat { /// Wrap a middleware to give it broader compatibility. pub fn new(middleware: T) -> Self { @@ -52,7 +64,7 @@ where T::Response: MapServiceResponseBody, T::Error: Into, { - type Response = ServiceResponse; + type Response = ServiceResponse; type Error = Error; type Transform = CompatMiddleware; type InitError = T::InitError; @@ -77,7 +89,7 @@ where S::Response: MapServiceResponseBody, S::Error: Into, { - type Response = ServiceResponse; + type Response = ServiceResponse; type Error = Error; type Future = CompatMiddlewareFuture; @@ -102,7 +114,7 @@ where T: MapServiceResponseBody, E: Into, { - type Output = Result; + type Output = Result, Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let res = match ready!(self.project().fut.poll(cx)) { @@ -116,14 +128,15 @@ where /// Convert `ServiceResponse`'s `ResponseBody` generic type to `ResponseBody`. pub trait MapServiceResponseBody { - fn map_body(self) -> ServiceResponse; + fn map_body(self) -> ServiceResponse; } impl MapServiceResponseBody for ServiceResponse where - B: MessageBody + Unpin + 'static, + B: MessageBody + 'static, { - fn map_body(self) -> ServiceResponse { + #[inline] + fn map_body(self) -> ServiceResponse { self.map_into_boxed_body() } } @@ -154,7 +167,7 @@ mod tests { let srv = init_service( App::new().service( web::scope("app") - .wrap(Compat::new(logger)) + .wrap(logger) .wrap(Compat::new(compress)) .service(web::resource("/test").route(web::get().to(HttpResponse::Ok))), ), diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index af4a107e3..d3cdf5763 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -113,6 +113,7 @@ where { type Response = ServiceResponse>>; type Error = Error; + #[allow(clippy::type_complexity)] type Future = Either, Ready>>; actix_service::forward_ready!(service); diff --git a/src/middleware/condition.rs b/src/middleware/condition.rs index a7777a96b..659f88bc9 100644 --- a/src/middleware/condition.rs +++ b/src/middleware/condition.rs @@ -106,7 +106,7 @@ mod tests { header::{HeaderValue, CONTENT_TYPE}, StatusCode, }, - middleware::err_handlers::*, + middleware::{err_handlers::*, Compat}, test::{self, TestRequest}, HttpResponse, }; @@ -116,7 +116,8 @@ mod tests { res.response_mut() .headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); - Ok(ErrorHandlerResponse::Response(res)) + + Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) } #[actix_rt::test] @@ -125,7 +126,9 @@ mod tests { ok(req.into_response(HttpResponse::InternalServerError().finish())) }; - let mw = ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); + let mw = Compat::new( + ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), + ); let mw = Condition::new(true, mw) .new_transform(srv.into_service()) @@ -141,7 +144,9 @@ mod tests { ok(req.into_response(HttpResponse::InternalServerError().finish())) }; - let mw = ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); + let mw = Compat::new( + ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), + ); let mw = Condition::new(false, mw) .new_transform(srv.into_service()) diff --git a/src/middleware/default_headers.rs b/src/middleware/default_headers.rs index dceca44c2..003abd40d 100644 --- a/src/middleware/default_headers.rs +++ b/src/middleware/default_headers.rs @@ -16,7 +16,7 @@ use pin_project_lite::pin_project; use crate::{ dev::{Service, Transform}, - http::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_TYPE}, + http::header::{HeaderMap, HeaderName, HeaderValue, TryIntoHeaderPair, CONTENT_TYPE}, service::{ServiceRequest, ServiceResponse}, Error, }; @@ -29,79 +29,80 @@ use crate::{ /// ``` /// use actix_web::{web, http, middleware, App, HttpResponse}; /// -/// fn main() { -/// let app = App::new() -/// .wrap(middleware::DefaultHeaders::new().header("X-Version", "0.2")) -/// .service( -/// web::resource("/test") -/// .route(web::get().to(|| HttpResponse::Ok())) -/// .route(web::method(http::Method::HEAD).to(|| HttpResponse::MethodNotAllowed())) -/// ); -/// } +/// let app = App::new() +/// .wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2"))) +/// .service( +/// web::resource("/test") +/// .route(web::get().to(|| HttpResponse::Ok())) +/// .route(web::method(http::Method::HEAD).to(|| HttpResponse::MethodNotAllowed())) +/// ); /// ``` -#[derive(Clone)] +#[derive(Debug, Clone, Default)] pub struct DefaultHeaders { inner: Rc, } +#[derive(Debug, Default)] struct Inner { headers: HeaderMap, } -impl Default for DefaultHeaders { - fn default() -> Self { - DefaultHeaders { - inner: Rc::new(Inner { - headers: HeaderMap::new(), - }), - } - } -} - impl DefaultHeaders { /// Constructs an empty `DefaultHeaders` middleware. + #[inline] pub fn new() -> DefaultHeaders { DefaultHeaders::default() } /// Adds a header to the default set. - #[inline] - pub fn header(mut self, key: K, value: V) -> Self + /// + /// # Panics + /// Panics when resolved header name or value is invalid. + #[allow(clippy::should_implement_trait)] + pub fn add(mut self, header: impl TryIntoHeaderPair) -> Self { + // standard header terminology `insert` or `append` for this method would make the behavior + // of this middleware less obvious since it only adds the headers if they are not present + + match header.try_into_pair() { + Ok((key, value)) => Rc::get_mut(&mut self.inner) + .expect("All default headers must be added before cloning.") + .headers + .append(key, value), + Err(err) => panic!("Invalid header: {}", err.into()), + } + + self + } + + #[doc(hidden)] + #[deprecated( + since = "4.0.0", + note = "Prefer `.add((key, value))`. Will be removed in v5." + )] + pub fn header(self, key: K, value: V) -> Self where HeaderName: TryFrom, >::Error: Into, HeaderValue: TryFrom, >::Error: Into, { - #[allow(clippy::match_wild_err_arm)] - match HeaderName::try_from(key) { - Ok(key) => match HeaderValue::try_from(value) { - Ok(value) => { - Rc::get_mut(&mut self.inner) - .expect("Multiple copies exist") - .headers - .append(key, value); - } - Err(_) => panic!("Can not create header value"), - }, - Err(_) => panic!("Can not create header name"), - } - self + self.add(( + HeaderName::try_from(key) + .map_err(Into::into) + .expect("Invalid header name"), + HeaderValue::try_from(value) + .map_err(Into::into) + .expect("Invalid header value"), + )) } /// Adds a default *Content-Type* header if response does not contain one. /// /// Default is `application/octet-stream`. - pub fn add_content_type(mut self) -> Self { - Rc::get_mut(&mut self.inner) - .expect("Multiple `Inner` copies exist.") - .headers - .insert( - CONTENT_TYPE, - HeaderValue::from_static("application/octet-stream"), - ); - - self + pub fn add_content_type(self) -> Self { + #[allow(clippy::declare_interior_mutable_const)] + const HV_MIME: HeaderValue = HeaderValue::from_static("application/octet-stream"); + self.add((CONTENT_TYPE, HV_MIME)) } } @@ -119,7 +120,7 @@ where fn new_transform(&self, service: S) -> Self::Future { ready(Ok(DefaultHeadersMiddleware { service, - inner: self.inner.clone(), + inner: Rc::clone(&self.inner), })) } } @@ -192,22 +193,27 @@ mod tests { use crate::{ dev::ServiceRequest, http::header::CONTENT_TYPE, - test::{ok_service, TestRequest}, + test::{self, TestRequest}, HttpResponse, }; #[actix_rt::test] - async fn test_default_headers() { + async fn adding_default_headers() { let mw = DefaultHeaders::new() - .header(CONTENT_TYPE, "0001") - .new_transform(ok_service()) + .add(("X-TEST", "0001")) + .add(("X-TEST-TWO", HeaderValue::from_static("123"))) + .new_transform(test::ok_service()) .await .unwrap(); let req = TestRequest::default().to_srv_request(); - let resp = mw.call(req).await.unwrap(); - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); + let res = mw.call(req).await.unwrap(); + assert_eq!(res.headers().get("x-test").unwrap(), "0001"); + assert_eq!(res.headers().get("x-test-two").unwrap(), "123"); + } + #[actix_rt::test] + async fn no_override_existing() { let req = TestRequest::default().to_srv_request(); let srv = |req: ServiceRequest| { ok(req.into_response( @@ -217,7 +223,7 @@ mod tests { )) }; let mw = DefaultHeaders::new() - .header(CONTENT_TYPE, "0001") + .add((CONTENT_TYPE, "0001")) .new_transform(srv.into_service()) .await .unwrap(); @@ -226,11 +232,10 @@ mod tests { } #[actix_rt::test] - async fn test_content_type() { - let srv = |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish())); + async fn adding_content_type() { let mw = DefaultHeaders::new() .add_content_type() - .new_transform(srv.into_service()) + .new_transform(test::ok_service()) .await .unwrap(); @@ -241,4 +246,16 @@ mod tests { "application/octet-stream" ); } + + #[test] + #[should_panic] + fn invalid_header_name() { + DefaultHeaders::new().add((":", "hello")); + } + + #[test] + #[should_panic] + fn invalid_header_value() { + DefaultHeaders::new().add(("x-test", "\n")); + } } diff --git a/src/middleware/err_handlers.rs b/src/middleware/err_handlers.rs index 756da30c3..bde054330 100644 --- a/src/middleware/err_handlers.rs +++ b/src/middleware/err_handlers.rs @@ -13,6 +13,7 @@ use futures_core::{future::LocalBoxFuture, ready}; use pin_project_lite::pin_project; use crate::{ + body::EitherBody, dev::{ServiceRequest, ServiceResponse}, http::StatusCode, Error, Result, @@ -21,10 +22,10 @@ use crate::{ /// Return type for [`ErrorHandlers`] custom handlers. pub enum ErrorHandlerResponse { /// Immediate HTTP response. - Response(ServiceResponse), + Response(ServiceResponse>), /// A future that resolves to an HTTP response. - Future(LocalBoxFuture<'static, Result, Error>>), + Future(LocalBoxFuture<'static, Result>, Error>>), } type ErrorHandler = dyn Fn(ServiceResponse) -> Result>; @@ -36,26 +37,21 @@ type ErrorHandler = dyn Fn(ServiceResponse) -> Result(mut res: dev::ServiceResponse) -> Result> { -/// res.response_mut() -/// .headers_mut() -/// .insert(header::CONTENT_TYPE, header::HeaderValue::from_static("Error")); -/// Ok(ErrorHandlerResponse::Response(res)) +/// fn add_error_header(mut res: dev::ServiceResponse) -> Result> { +/// res.response_mut().headers_mut().insert( +/// header::CONTENT_TYPE, +/// header::HeaderValue::from_static("Error"), +/// ); +/// Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) /// } /// /// let app = App::new() -/// .wrap( -/// ErrorHandlers::new() -/// .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), -/// ) -/// .service(web::resource("/test") -/// .route(web::get().to(|| HttpResponse::Ok())) -/// .route(web::head().to(|| HttpResponse::MethodNotAllowed()) -/// )); +/// .wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_header)) +/// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError))); /// ``` pub struct ErrorHandlers { handlers: Handlers, @@ -66,7 +62,7 @@ type Handlers = Rc>>>; impl Default for ErrorHandlers { fn default() -> Self { ErrorHandlers { - handlers: Rc::new(AHashMap::default()), + handlers: Default::default(), } } } @@ -95,7 +91,7 @@ where S::Future: 'static, B: 'static, { - type Response = ServiceResponse; + type Response = ServiceResponse>; type Error = Error; type Transform = ErrorHandlersMiddleware; type InitError = (); @@ -119,7 +115,7 @@ where S::Future: 'static, B: 'static, { - type Response = ServiceResponse; + type Response = ServiceResponse>; type Error = Error; type Future = ErrorHandlersFuture; @@ -143,8 +139,8 @@ pin_project! { fut: Fut, handlers: Handlers, }, - HandlerFuture { - fut: LocalBoxFuture<'static, Fut::Output>, + ErrorHandlerFuture { + fut: LocalBoxFuture<'static, Result>, Error>>, }, } } @@ -153,25 +149,29 @@ impl Future for ErrorHandlersFuture where Fut: Future, Error>>, { - type Output = Fut::Output; + type Output = Result>, Error>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match self.as_mut().project() { ErrorHandlersProj::ServiceFuture { fut, handlers } => { let res = ready!(fut.poll(cx))?; + match handlers.get(&res.status()) { Some(handler) => match handler(res)? { ErrorHandlerResponse::Response(res) => Poll::Ready(Ok(res)), ErrorHandlerResponse::Future(fut) => { self.as_mut() - .set(ErrorHandlersFuture::HandlerFuture { fut }); + .set(ErrorHandlersFuture::ErrorHandlerFuture { fut }); + self.poll(cx) } }, - None => Poll::Ready(Ok(res)), + + None => Poll::Ready(Ok(res.map_into_left_body())), } } - ErrorHandlersProj::HandlerFuture { fut } => fut.as_mut().poll(cx), + + ErrorHandlersProj::ErrorHandlerFuture { fut } => fut.as_mut().poll(cx), } } } @@ -180,32 +180,33 @@ where mod tests { use actix_service::IntoService; use actix_utils::future::ok; + use bytes::Bytes; use futures_util::future::FutureExt as _; use super::*; - use crate::http::{ - header::{HeaderValue, CONTENT_TYPE}, - StatusCode, + use crate::{ + http::{ + header::{HeaderValue, CONTENT_TYPE}, + StatusCode, + }, + test::{self, TestRequest}, }; - use crate::test::{self, TestRequest}; - use crate::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)) - } #[actix_rt::test] - async fn test_handler() { - let srv = |req: ServiceRequest| { - ok(req.into_response(HttpResponse::InternalServerError().finish())) - }; + async fn add_header_error_handler() { + #[allow(clippy::unnecessary_wraps)] + fn error_handler(mut res: ServiceResponse) -> Result> { + res.response_mut() + .headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); + + Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) + } + + let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR); let mw = ErrorHandlers::new() - .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500) + .handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler) .new_transform(srv.into_service()) .await .unwrap(); @@ -214,24 +215,25 @@ mod tests { assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); } - #[allow(clippy::unnecessary_wraps)] - fn render_500_async( - mut res: ServiceResponse, - ) -> Result> { - res.response_mut() - .headers_mut() - .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); - Ok(ErrorHandlerResponse::Future(ok(res).boxed_local())) - } - #[actix_rt::test] - async fn test_handler_async() { - let srv = |req: ServiceRequest| { - ok(req.into_response(HttpResponse::InternalServerError().finish())) - }; + async fn add_header_error_handler_async() { + #[allow(clippy::unnecessary_wraps)] + fn error_handler( + mut res: ServiceResponse, + ) -> Result> { + res.response_mut() + .headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); + + Ok(ErrorHandlerResponse::Future( + ok(res.map_into_left_body()).boxed_local(), + )) + } + + let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR); let mw = ErrorHandlers::new() - .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500_async) + .handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler) .new_transform(srv.into_service()) .await .unwrap(); @@ -239,4 +241,34 @@ mod tests { 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 changes_body_type() { + #[allow(clippy::unnecessary_wraps)] + fn error_handler( + res: ServiceResponse, + ) -> Result> { + let (req, res) = res.into_parts(); + let res = res.set_body(Bytes::from("sorry, that's no bueno")); + + let res = ServiceResponse::new(req, res) + .map_into_boxed_body() + .map_into_right_body(); + + Ok(ErrorHandlerResponse::Response(res)) + } + + let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR); + + let mw = ErrorHandlers::new() + .handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler) + .new_transform(srv.into_service()) + .await + .unwrap(); + + let res = test::call_service(&mw, TestRequest::default().to_srv_request()).await; + assert_eq!(test::read_body(res).await, "sorry, that's no bueno"); + } + + // TODO: test where error is thrown } diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 74daa26d5..969cb0c10 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -322,13 +322,10 @@ pin_project! { } } -impl MessageBody for StreamLog -where - B: MessageBody, - B::Error: Into, -{ - type Error = Error; +impl MessageBody for StreamLog { + type Error = B::Error; + #[inline] fn size(&self) -> BodySize { self.body.size() } @@ -344,7 +341,7 @@ where *this.size += chunk.len(); Poll::Ready(Some(Ok(chunk))) } - Some(Err(err)) => Poll::Ready(Some(Err(err.into()))), + Some(Err(err)) => Poll::Ready(Some(Err(err))), None => Poll::Ready(None), } } @@ -550,7 +547,7 @@ impl FormatText { *self = FormatText::Str(s.to_string()); } FormatText::RemoteAddr => { - let s = if let Some(peer) = req.connection_info().remote_addr() { + let s = if let Some(peer) = req.connection_info().peer_addr() { FormatText::Str((*peer).to_string()) } else { FormatText::Str("-".to_string()) diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index d19cb64e9..a781052a6 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -5,6 +5,8 @@ mod condition; mod default_headers; mod err_handlers; mod logger; +#[cfg(test)] +mod noop; mod normalize; pub use self::compat::Compat; @@ -12,6 +14,8 @@ pub use self::condition::Condition; pub use self::default_headers::DefaultHeaders; pub use self::err_handlers::{ErrorHandlerResponse, ErrorHandlers}; pub use self::logger::Logger; +#[cfg(test)] +pub(crate) use self::noop::Noop; pub use self::normalize::{NormalizePath, TrailingSlash}; #[cfg(feature = "__compress")] @@ -33,9 +37,9 @@ mod tests { let _ = App::new() .wrap(Compat::new(Logger::default())) .wrap(Condition::new(true, DefaultHeaders::new())) - .wrap(DefaultHeaders::new().header("X-Test2", "X-Value2")) + .wrap(DefaultHeaders::new().add(("X-Test2", "X-Value2"))) .wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| { - Ok(ErrorHandlerResponse::Response(res)) + Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) })) .wrap(Logger::default()) .wrap(NormalizePath::new(TrailingSlash::Trim)); @@ -44,9 +48,9 @@ mod tests { .wrap(NormalizePath::new(TrailingSlash::Trim)) .wrap(Logger::default()) .wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| { - Ok(ErrorHandlerResponse::Response(res)) + Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) })) - .wrap(DefaultHeaders::new().header("X-Test2", "X-Value2")) + .wrap(DefaultHeaders::new().add(("X-Test2", "X-Value2"))) .wrap(Condition::new(true, DefaultHeaders::new())) .wrap(Compat::new(Logger::default())); diff --git a/src/middleware/noop.rs b/src/middleware/noop.rs new file mode 100644 index 000000000..ae7da1d81 --- /dev/null +++ b/src/middleware/noop.rs @@ -0,0 +1,37 @@ +//! A no-op middleware. See [Noop] for docs. + +use actix_utils::future::{ready, Ready}; + +use crate::dev::{Service, Transform}; + +/// A no-op middleware that passes through request and response untouched. +pub(crate) struct Noop; + +impl, Req> Transform for Noop { + type Response = S::Response; + type Error = S::Error; + type Transform = NoopService; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(NoopService { service })) + } +} + +#[doc(hidden)] +pub(crate) struct NoopService { + service: S, +} + +impl, Req> Service for NoopService { + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + crate::dev::forward_ready!(service); + + fn call(&self, req: Req) -> Self::Future { + self.service.call(req) + } +} diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index 18dcaeefa..3ab908481 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -225,7 +225,7 @@ mod tests { .service(web::resource("/v1/something").to(HttpResponse::Ok)) .service( web::resource("/v2/something") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) @@ -261,7 +261,7 @@ mod tests { .service(web::resource("/v1/something").to(HttpResponse::Ok)) .service( web::resource("/v2/something") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) @@ -294,7 +294,7 @@ mod tests { let app = init_service( App::new().wrap(NormalizePath(TrailingSlash::Trim)).service( web::resource("/") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) @@ -318,7 +318,7 @@ mod tests { .service(web::resource("/v1/something/").to(HttpResponse::Ok)) .service( web::resource("/v2/something/") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) @@ -353,7 +353,7 @@ mod tests { .wrap(NormalizePath(TrailingSlash::Always)) .service( web::resource("/") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) @@ -378,7 +378,7 @@ mod tests { .service(web::resource("/v1/").to(HttpResponse::Ok)) .service( web::resource("/v2/something") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) diff --git a/src/request.rs b/src/request.rs index f04d47c6f..b59369317 100644 --- a/src/request.rs +++ b/src/request.rs @@ -37,6 +37,8 @@ pub(crate) struct HttpRequestInner { pub(crate) head: Message, pub(crate) path: Path, pub(crate) app_data: SmallVec<[Rc; 4]>, + pub(crate) conn_data: Option>, + pub(crate) req_data: Rc>, app_state: Rc, } @@ -47,6 +49,8 @@ impl HttpRequest { head: Message, app_state: Rc, app_data: Rc, + conn_data: Option>, + req_data: Rc>, ) -> HttpRequest { let mut data = SmallVec::<[Rc; 4]>::new(); data.push(app_data); @@ -57,6 +61,8 @@ impl HttpRequest { path, app_state, app_data: data, + conn_data, + req_data, }), } } @@ -153,16 +159,26 @@ impl HttpRequest { self.resource_map().match_name(self.path()) } - /// Request extensions - #[inline] - pub fn extensions(&self) -> Ref<'_, Extensions> { - self.head().extensions() + pub fn req_data(&self) -> Ref<'_, Extensions> { + self.inner.req_data.borrow() } - /// Mutable reference to a the request's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut<'_, Extensions> { - self.head().extensions_mut() + pub fn req_data_mut(&self) -> RefMut<'_, Extensions> { + self.inner.req_data.borrow_mut() + } + + /// Returns a reference a piece of connection data set in an [on-connect] callback. + /// + /// ```ignore + /// let opt_t = req.conn_data::(); + /// ``` + /// + /// [on-connect]: crate::HttpServer::on_connect + pub fn conn_data(&self) -> Option<&T> { + self.inner + .conn_data + .as_deref() + .and_then(|container| container.get::()) } /// Generates URL for a named resource. @@ -212,26 +228,36 @@ impl HttpRequest { self.app_state().rmap() } - /// Peer socket address. + /// Returns peer socket address. /// /// Peer address is the directly connected peer's socket address. If a proxy is used in front of /// the Actix Web server, then it would be address of this proxy. /// - /// To get client connection information `.connection_info()` should be used. + /// For expanded client connection information, use [`connection_info`] instead. /// - /// Will only return None when called in unit tests. + /// Will only return None when called in unit tests unless [`TestRequest::peer_addr`] is used. + /// + /// [`TestRequest::peer_addr`]: crate::test::TestRequest::peer_addr + /// [`connection_info`]: Self::connection_info #[inline] pub fn peer_addr(&self) -> Option { self.head().peer_addr } - /// Get *ConnectionInfo* for the current request. + /// Returns connection info for the current request. /// - /// This method panics if request's extensions container is already - /// borrowed. + /// The return type, [`ConnectionInfo`], can also be used as an extractor. + /// + /// # Panics + /// Panics if request's extensions container is already borrowed. #[inline] pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> { - ConnectionInfo::get(self.head(), self.app_config()) + if !self.extensions().contains::() { + let info = ConnectionInfo::new(self.head(), &*self.app_config()); + self.extensions_mut().insert(info); + } + + Ref::map(self.extensions(), |data| data.get().unwrap()) } /// App config @@ -304,21 +330,18 @@ impl HttpMessage for HttpRequest { type Stream = (); #[inline] - /// Returns Request's headers. fn headers(&self) -> &HeaderMap { &self.head().headers } - /// Request extensions #[inline] fn extensions(&self) -> Ref<'_, Extensions> { - self.inner.head.extensions() + self.req_data() } - /// Mutable reference to a the request's extensions #[inline] fn extensions_mut(&self) -> RefMut<'_, Extensions> { - self.inner.head.extensions_mut() + self.req_data_mut() } #[inline] @@ -331,17 +354,18 @@ impl Drop for HttpRequest { fn drop(&mut self) { // if possible, contribute to current worker's HttpRequest allocation pool - // This relies on no Weak exists anywhere.(There is none) + // This relies on no weak references to inner existing anywhere within the codebase. if let Some(inner) = Rc::get_mut(&mut self.inner) { if inner.app_state.pool().is_available() { // clear additional app_data and keep the root one for reuse. inner.app_data.truncate(1); - // inner is borrowed mut here. get head's Extension mutably - // to reduce borrow check - inner.head.extensions.get_mut().clear(); + + // 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. + Rc::get_mut(&mut inner.req_data).unwrap().get_mut().clear(); // a re-borrow of pool is necessary here. - let req = self.inner.clone(); + let req = Rc::clone(&self.inner); self.app_state().pool().push(req); } } diff --git a/src/request_data.rs b/src/request_data.rs index 575dc1eb3..b685fd0d6 100644 --- a/src/request_data.rs +++ b/src/request_data.rs @@ -17,7 +17,7 @@ use crate::{dev::Payload, error::ErrorInternalServerError, Error, FromRequest, H /// # Mutating Request Data /// Note that since extractors must output owned data, only types that `impl Clone` can use this /// extractor. A clone is taken of the required request data and can, therefore, not be directly -/// mutated in-place. To mutate request data, continue to use [`HttpRequest::extensions_mut`] or +/// mutated in-place. To mutate request data, continue to use [`HttpRequest::req_data_mut`] or /// re-insert the cloned data back into the extensions map. A `DerefMut` impl is intentionally not /// provided to make this potential foot-gun more obvious. /// @@ -33,12 +33,11 @@ use crate::{dev::Payload, error::ErrorInternalServerError, Error, FromRequest, H /// req: HttpRequest, /// opt_flag: Option>, /// ) -> impl Responder { -/// // use an optional extractor if the middleware is -/// // not guaranteed to add this type of requests data +/// // use an option extractor if middleware is not guaranteed to add this type of req data /// if let Some(flag) = opt_flag { -/// assert_eq!(&flag.into_inner(), req.extensions().get::().unwrap()); +/// assert_eq!(&flag.into_inner(), req.req_data().get::().unwrap()); /// } -/// +/// /// HttpResponse::Ok() /// } /// ``` @@ -68,7 +67,7 @@ impl FromRequest for ReqData { type Future = Ready>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - if let Some(st) = req.extensions().get::() { + if let Some(st) = req.req_data().get::() { ok(ReqData(st.clone())) } else { log::debug!( diff --git a/src/resource.rs b/src/resource.rs index 420374a86..564c4d3ef 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,6 +1,6 @@ -use std::{cell::RefCell, fmt, future::Future, rc::Rc}; +use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, rc::Rc}; -use actix_http::Extensions; +use actix_http::{body::BoxBody, Extensions}; use actix_router::{IntoPatterns, Patterns}; use actix_service::{ apply, apply_fn_factory, boxed, fn_service, IntoServiceFactory, Service, ServiceFactory, @@ -15,38 +15,34 @@ use crate::{ dev::{ensure_leading_slash, AppService, ResourceDef}, guard::Guard, handler::Handler, - responder::Responder, route::{Route, RouteService}, service::{ BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest, ServiceResponse, }, - BoxError, Error, FromRequest, HttpResponse, + Error, FromRequest, HttpResponse, Responder, }; -/// *Resource* is an entry in resources table which corresponds to requested URL. +/// A collection of [`Route`]s that respond to the same path pattern. /// -/// Resource in turn has at least one route. -/// Route consists of an handlers objects and list of guards -/// (objects that implement `Guard` trait). -/// Resources and routes uses builder-like pattern for configuration. -/// During request handling, resource object iterate through all routes -/// and check guards for specific route, if request matches all -/// guards, route considered matched and route handler get called. +/// Resource in turn has at least one route. Route consists of an handlers objects and list of +/// guards (objects that implement `Guard` trait). Resources and routes uses builder-like pattern +/// for configuration. During request handling, resource object iterate through all routes and check +/// guards for specific route, if request matches all guards, route considered matched and route +/// handler get called. /// +/// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/") -/// .route(web::get().to(|| HttpResponse::Ok()))); -/// } +/// let app = App::new().service( +/// web::resource("/") +/// .route(web::get().to(|| HttpResponse::Ok()))); /// ``` /// -/// If no matching route could be found, *405* response code get returned. -/// Default behavior could be overridden with `default_resource()` method. -pub struct Resource { +/// If no matching route could be found, *405* response code get returned. Default behavior could be +/// overridden with `default_resource()` method. +pub struct Resource { endpoint: T, rdef: Patterns, name: Option, @@ -55,6 +51,7 @@ pub struct Resource { guards: Vec>, default: BoxedHttpServiceFactory, factory_ref: Rc>>, + _phantom: PhantomData, } impl Resource { @@ -72,19 +69,21 @@ impl Resource { default: boxed::factory(fn_service(|req: ServiceRequest| async { Ok(req.into_response(HttpResponse::MethodNotAllowed())) })), + _phantom: PhantomData, } } } -impl Resource +impl Resource where T: ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, + B: MessageBody, { /// Set resource name. /// @@ -132,15 +131,13 @@ where /// ``` /// use actix_web::{web, guard, App, HttpResponse}; /// - /// fn main() { - /// let app = App::new().service( - /// web::resource("/").route( - /// web::route() - /// .guard(guard::Any(guard::Get()).or(guard::Put())) - /// .guard(guard::Header("Content-Type", "text/plain")) - /// .to(|| HttpResponse::Ok())) - /// ); - /// } + /// let app = App::new().service( + /// web::resource("/").route( + /// web::route() + /// .guard(guard::Any(guard::Get()).or(guard::Put())) + /// .guard(guard::Header("Content-Type", "text/plain")) + /// .to(|| HttpResponse::Ok())) + /// ); /// ``` /// /// Multiple routes could be added to a resource. Resource object uses @@ -233,14 +230,11 @@ where /// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } /// App::new().service(web::resource("/").route(web::route().to(index))); /// ``` - pub fn to(mut self, handler: F) -> Self + pub fn to(mut self, handler: F) -> Self where - F: Handler, - I: FromRequest + 'static, - R: Future + 'static, - R::Output: Responder + 'static, - ::Body: MessageBody, - <::Body as MessageBody>::Error: Into, + F: Handler, + Args: FromRequest + 'static, + F::Output: Responder + 'static, { self.routes.push(Route::new().to(handler)); self @@ -253,26 +247,28 @@ where /// type (i.e modify response's body). /// /// **Note**: middlewares get called in opposite order of middlewares registration. - pub fn wrap( + pub fn wrap( self, mw: M, ) -> Resource< impl ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, + B1, > where M: Transform< T::Service, ServiceRequest, - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, + B1: MessageBody, { Resource { endpoint: apply(mw, self.endpoint), @@ -283,6 +279,7 @@ where default: self.default, app_data: self.app_data, factory_ref: self.factory_ref, + _phantom: PhantomData, } } @@ -320,21 +317,23 @@ where /// .route(web::get().to(index))); /// } /// ``` - pub fn wrap_fn( + pub fn wrap_fn( self, mw: F, ) -> Resource< impl ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, + B1, > where F: Fn(ServiceRequest, &T::Service) -> R + Clone, - R: Future>, + R: Future, Error>>, + B1: MessageBody, { Resource { endpoint: apply_fn_factory(self.endpoint, mw), @@ -345,6 +344,7 @@ where default: self.default, app_data: self.app_data, factory_ref: self.factory_ref, + _phantom: PhantomData, } } @@ -372,15 +372,16 @@ where } } -impl HttpServiceFactory for Resource +impl HttpServiceFactory for Resource where T: ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), > + 'static, + B: MessageBody + 'static, { fn register(mut self, config: &mut AppService) { let guards = if self.guards.is_empty() { @@ -412,7 +413,9 @@ where req.add_data_container(Rc::clone(data)); } - srv.call(req) + let fut = srv.call(req); + + async { Ok(fut.await?.map_into_boxed_body()) } }); config.register_service(rdef, guards, endpoint, None) @@ -506,18 +509,48 @@ mod tests { use actix_service::Service; use actix_utils::future::ok; + use super::*; use crate::{ guard, http::{ header::{self, HeaderValue}, Method, StatusCode, }, - middleware::DefaultHeaders, + middleware::{Compat, DefaultHeaders}, service::{ServiceRequest, ServiceResponse}, test::{call_service, init_service, TestRequest}, web, App, Error, HttpMessage, HttpResponse, }; + #[test] + fn can_be_returned_from_fn() { + fn my_resource() -> Resource { + web::resource("/test").route(web::get().to(|| async { "hello" })) + } + + fn my_compat_resource() -> Resource< + impl ServiceFactory< + ServiceRequest, + Config = (), + Response = ServiceResponse, + Error = Error, + InitError = (), + >, + > { + web::resource("/test-compat") + .wrap_fn(|req, srv| { + let fut = srv.call(req); + async { Ok(fut.await?.map_into_right_body::<()>()) } + }) + .wrap(Compat::noop()) + .route(web::get().to(|| async { "hello" })) + } + + App::new() + .service(my_resource()) + .service(my_compat_resource()); + } + #[actix_rt::test] async fn test_middleware() { let srv = init_service( @@ -526,7 +559,7 @@ mod tests { .name("test") .wrap( DefaultHeaders::new() - .header(header::CONTENT_TYPE, HeaderValue::from_static("0001")), + .add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))), ) .route(web::get().to(HttpResponse::Ok)), ), @@ -772,4 +805,26 @@ mod tests { let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } + + #[actix_rt::test] + async fn test_middleware_body_type() { + let srv = init_service( + App::new().service( + web::resource("/test") + .wrap_fn(|req, srv| { + let fut = srv.call(req); + async { Ok(fut.await?.map_into_right_body::<()>()) } + }) + .route(web::get().to(|| async { "hello" })), + ), + ) + .await; + + // test if `try_into_bytes()` is preserved across scope layer + use actix_http::body::MessageBody as _; + let req = TestRequest::with_uri("/test").to_request(); + let resp = call_service(&srv, req).await; + let body = resp.into_body(); + assert_eq!(body.try_into_bytes().unwrap(), b"hello".as_ref()); + } } diff --git a/src/response/builder.rs b/src/response/builder.rs index 50e23f81b..93d8ab567 100644 --- a/src/response/builder.rs +++ b/src/response/builder.rs @@ -9,7 +9,7 @@ use std::{ use actix_http::{ body::{BodyStream, BoxBody, MessageBody}, error::HttpError, - header::{self, HeaderName, IntoHeaderPair, IntoHeaderValue}, + header::{self, HeaderName, TryIntoHeaderPair, TryIntoHeaderValue}, ConnectionType, Extensions, Response, ResponseHead, StatusCode, }; use bytes::Bytes; @@ -67,12 +67,9 @@ impl HttpResponseBuilder { /// .insert_header(("X-TEST", "value")) /// .finish(); /// ``` - pub fn insert_header(&mut self, header: H) -> &mut Self - where - H: IntoHeaderPair, - { + pub fn insert_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self { if let Some(parts) = self.inner() { - match header.try_into_header_pair() { + match header.try_into_pair() { Ok((key, value)) => { parts.headers.insert(key, value); } @@ -94,12 +91,9 @@ impl HttpResponseBuilder { /// .append_header(("X-TEST", "value2")) /// .finish(); /// ``` - pub fn append_header(&mut self, header: H) -> &mut Self - where - H: IntoHeaderPair, - { + pub fn append_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self { if let Some(parts) = self.inner() { - match header.try_into_header_pair() { + match header.try_into_pair() { Ok((key, value)) => parts.headers.append(key, value), Err(e) => self.err = Some(e.into()), }; @@ -109,6 +103,7 @@ impl HttpResponseBuilder { } /// Replaced with [`Self::insert_header()`]. + #[doc(hidden)] #[deprecated( since = "4.0.0", note = "Replaced with `insert_header((key, value))`. Will be removed in v5." @@ -117,7 +112,7 @@ impl HttpResponseBuilder { where K: TryInto, K::Error: Into, - V: IntoHeaderValue, + V: TryIntoHeaderValue, { if self.err.is_some() { return self; @@ -133,6 +128,7 @@ impl HttpResponseBuilder { } /// Replaced with [`Self::append_header()`]. + #[doc(hidden)] #[deprecated( since = "4.0.0", note = "Replaced with `append_header((key, value))`. Will be removed in v5." @@ -141,7 +137,7 @@ impl HttpResponseBuilder { where K: TryInto, K::Error: Into, - V: IntoHeaderValue, + V: TryIntoHeaderValue, { if self.err.is_some() { return self; @@ -178,7 +174,7 @@ impl HttpResponseBuilder { #[inline] pub fn upgrade(&mut self, value: V) -> &mut Self where - V: IntoHeaderValue, + V: TryIntoHeaderValue, { if let Some(parts) = self.inner() { parts.set_connection_type(ConnectionType::Upgrade); @@ -216,7 +212,7 @@ impl HttpResponseBuilder { #[inline] pub fn content_type(&mut self, value: V) -> &mut Self where - V: IntoHeaderValue, + V: TryIntoHeaderValue, { if let Some(parts) = self.inner() { match value.try_into_value() { @@ -433,9 +429,12 @@ mod tests { use actix_http::body; use super::*; - use crate::http::{ - header::{self, HeaderValue, CONTENT_TYPE}, - StatusCode, + use crate::{ + http::{ + header::{self, HeaderValue, CONTENT_TYPE}, + StatusCode, + }, + test::assert_body_eq, }; #[test] @@ -476,32 +475,23 @@ mod tests { #[actix_rt::test] async fn test_json() { - let resp = HttpResponse::Ok().json(vec!["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); + let res = HttpResponse::Ok().json(vec!["v1", "v2", "v3"]); + let ct = res.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("application/json")); - assert_eq!( - body::to_bytes(resp.into_body()).await.unwrap().as_ref(), - br#"["v1","v2","v3"]"# - ); + assert_body_eq!(res, br#"["v1","v2","v3"]"#); - let resp = HttpResponse::Ok().json(&["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); + let res = HttpResponse::Ok().json(&["v1", "v2", "v3"]); + let ct = res.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("application/json")); - assert_eq!( - body::to_bytes(resp.into_body()).await.unwrap().as_ref(), - br#"["v1","v2","v3"]"# - ); + assert_body_eq!(res, br#"["v1","v2","v3"]"#); // content type override - let resp = HttpResponse::Ok() + let res = HttpResponse::Ok() .insert_header((CONTENT_TYPE, "text/json")) .json(&vec!["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); + let ct = res.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("text/json")); - assert_eq!( - body::to_bytes(resp.into_body()).await.unwrap().as_ref(), - br#"["v1","v2","v3"]"# - ); + assert_body_eq!(res, br#"["v1","v2","v3"]"#); } #[actix_rt::test] diff --git a/src/response/customize_responder.rs b/src/response/customize_responder.rs new file mode 100644 index 000000000..11f6b2916 --- /dev/null +++ b/src/response/customize_responder.rs @@ -0,0 +1,245 @@ +use actix_http::{ + body::{EitherBody, MessageBody}, + error::HttpError, + header::HeaderMap, + header::TryIntoHeaderPair, + StatusCode, +}; + +use crate::{BoxError, HttpRequest, HttpResponse, Responder}; + +/// Allows overriding status code and headers for a [`Responder`]. +/// +/// Created by the [`Responder::customize`] method. +pub struct CustomizeResponder { + inner: CustomizeResponderInner, + error: Option, +} + +struct CustomizeResponderInner { + responder: R, + status: Option, + override_headers: HeaderMap, + append_headers: HeaderMap, +} + +impl CustomizeResponder { + pub(crate) fn new(responder: R) -> Self { + CustomizeResponder { + inner: CustomizeResponderInner { + responder, + status: None, + override_headers: HeaderMap::new(), + append_headers: HeaderMap::new(), + }, + error: None, + } + } + + /// Override a status code for the Responder's response. + /// + /// # Examples + /// ``` + /// use actix_web::{Responder, http::StatusCode, test::TestRequest}; + /// + /// let responder = "Welcome!".customize().with_status(StatusCode::ACCEPTED); + /// + /// let request = TestRequest::default().to_http_request(); + /// let response = responder.respond_to(&request); + /// assert_eq!(response.status(), StatusCode::ACCEPTED); + /// ``` + pub fn with_status(mut self, status: StatusCode) -> Self { + if let Some(inner) = self.inner() { + inner.status = Some(status); + } + + self + } + + /// Insert (override) header in the final response. + /// + /// Overrides other headers with the same name. + /// See [`HeaderMap::insert`](crate::http::header::HeaderMap::insert). + /// + /// Headers added with this method will be inserted before those added + /// with [`append_header`](Self::append_header). As such, header(s) can be overridden with more + /// than one new header by first calling `insert_header` followed by `append_header`. + /// + /// # Examples + /// ``` + /// use actix_web::{Responder, test::TestRequest}; + /// + /// let responder = "Hello world!" + /// .customize() + /// .insert_header(("x-version", "1.2.3")); + /// + /// let request = TestRequest::default().to_http_request(); + /// let response = responder.respond_to(&request); + /// assert_eq!(response.headers().get("x-version").unwrap(), "1.2.3"); + /// ``` + pub fn insert_header(mut self, header: impl TryIntoHeaderPair) -> Self { + if let Some(inner) = self.inner() { + match header.try_into_pair() { + Ok((key, value)) => { + inner.override_headers.insert(key, value); + } + Err(err) => self.error = Some(err.into()), + }; + } + + self + } + + /// Append header to the final response. + /// + /// Unlike [`insert_header`](Self::insert_header), this will not override existing headers. + /// See [`HeaderMap::append`](crate::http::header::HeaderMap::append). + /// + /// Headers added here are appended _after_ additions/overrides from `insert_header`. + /// + /// # Examples + /// ``` + /// use actix_web::{Responder, test::TestRequest}; + /// + /// let responder = "Hello world!" + /// .customize() + /// .append_header(("x-version", "1.2.3")); + /// + /// let request = TestRequest::default().to_http_request(); + /// let response = responder.respond_to(&request); + /// assert_eq!(response.headers().get("x-version").unwrap(), "1.2.3"); + /// ``` + pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self { + if let Some(inner) = self.inner() { + match header.try_into_pair() { + Ok((key, value)) => { + inner.append_headers.append(key, value); + } + Err(err) => self.error = Some(err.into()), + }; + } + + self + } + + #[doc(hidden)] + #[deprecated(since = "4.0.0", note = "Renamed to `insert_header`.")] + pub fn with_header(self, header: impl TryIntoHeaderPair) -> Self + where + Self: Sized, + { + self.insert_header(header) + } + + fn inner(&mut self) -> Option<&mut CustomizeResponderInner> { + if self.error.is_some() { + None + } else { + Some(&mut self.inner) + } + } +} + +impl Responder for CustomizeResponder +where + T: Responder, + ::Error: Into, +{ + type Body = EitherBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { + if let Some(err) = self.error { + return HttpResponse::from_error(err).map_into_right_body(); + } + + let mut res = self.inner.responder.respond_to(req); + + if let Some(status) = self.inner.status { + *res.status_mut() = status; + } + + for (k, v) in self.inner.override_headers { + res.headers_mut().insert(k, v); + } + + for (k, v) in self.inner.append_headers { + res.headers_mut().append(k, v); + } + + res.map_into_left_body() + } +} + +#[cfg(test)] +mod tests { + use bytes::Bytes; + + use actix_http::body::to_bytes; + + use super::*; + use crate::{ + http::{ + header::{HeaderValue, CONTENT_TYPE}, + StatusCode, + }, + test::TestRequest, + }; + + #[actix_rt::test] + async fn customize_responder() { + let req = TestRequest::default().to_http_request(); + let res = "test" + .to_string() + .customize() + .with_status(StatusCode::BAD_REQUEST) + .respond_to(&req); + + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = "test" + .to_string() + .customize() + .insert_header(("content-type", "json")) + .respond_to(&req); + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("json") + ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + } + + #[actix_rt::test] + async fn tuple_responder_with_status_code() { + let req = TestRequest::default().to_http_request(); + let res = ("test".to_string(), StatusCode::BAD_REQUEST).respond_to(&req); + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let req = TestRequest::default().to_http_request(); + let res = ("test".to_string(), StatusCode::OK) + .customize() + .insert_header((CONTENT_TYPE, mime::APPLICATION_JSON)) + .respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("application/json") + ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + } +} diff --git a/src/response/mod.rs b/src/response/mod.rs index 8401db9d2..977147104 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -1,9 +1,13 @@ mod builder; +mod customize_responder; mod http_codes; +mod responder; #[allow(clippy::module_inception)] mod response; pub use self::builder::HttpResponseBuilder; +pub use self::customize_responder::CustomizeResponder; +pub use self::responder::Responder; pub use self::response::HttpResponse; #[cfg(feature = "cookies")] diff --git a/src/responder.rs b/src/response/responder.rs similarity index 63% rename from src/responder.rs rename to src/response/responder.rs index e72739a71..319b824f1 100644 --- a/src/responder.rs +++ b/src/response/responder.rs @@ -2,64 +2,58 @@ use std::borrow::Cow; use actix_http::{ body::{BoxBody, EitherBody, MessageBody}, - error::HttpError, - header::HeaderMap, - header::IntoHeaderPair, + header::TryIntoHeaderPair, StatusCode, }; use bytes::{Bytes, BytesMut}; use crate::{BoxError, Error, HttpRequest, HttpResponse, HttpResponseBuilder}; +use super::CustomizeResponder; + /// 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 pub trait Responder { type Body: MessageBody + 'static; /// Convert self to `HttpResponse`. fn respond_to(self, req: &HttpRequest) -> HttpResponse; - /// Override a status code for a Responder. + /// Wraps responder to allow alteration of its response. /// - /// ``` - /// use actix_web::{http::StatusCode, HttpRequest, Responder}; + /// See [`CustomizeResponder`] docs for its capabilities. /// - /// fn index(req: HttpRequest) -> impl Responder { - /// "Welcome!".with_status(StatusCode::OK) - /// } + /// # Examples /// ``` - fn with_status(self, status: StatusCode) -> CustomResponder + /// use actix_web::{Responder, http::StatusCode, test::TestRequest}; + /// + /// let responder = "Hello world!" + /// .customize() + /// .with_status(StatusCode::BAD_REQUEST) + /// .insert_header(("x-hello", "world")); + /// + /// let request = TestRequest::default().to_http_request(); + /// let response = responder.respond_to(&request); + /// assert_eq!(response.status(), StatusCode::BAD_REQUEST); + /// assert_eq!(response.headers().get("x-hello").unwrap(), "world"); + /// ``` + #[inline] + fn customize(self) -> CustomizeResponder where Self: Sized, { - CustomResponder::new(self).with_status(status) + CustomizeResponder::new(self) } - /// Insert header to the final response. - /// - /// Overrides other headers with the same name. - /// - /// ``` - /// use actix_web::{web, HttpRequest, Responder}; - /// use serde::Serialize; - /// - /// #[derive(Serialize)] - /// struct MyObj { - /// name: String, - /// } - /// - /// fn index(req: HttpRequest) -> impl Responder { - /// web::Json(MyObj { name: "Name".to_owned() }) - /// .with_header(("x-version", "1.2.3")) - /// } - /// ``` - fn with_header(self, header: H) -> CustomResponder + #[doc(hidden)] + #[deprecated(since = "4.0.0", note = "Prefer `.customize().insert_header(header)`.")] + fn with_header(self, header: impl TryIntoHeaderPair) -> CustomizeResponder where Self: Sized, - H: IntoHeaderPair, { - CustomResponder::new(self).with_header(header) + self.customize().insert_header(header) } } @@ -181,98 +175,6 @@ macro_rules! impl_into_string_responder { impl_into_string_responder!(&'_ String); impl_into_string_responder!(Cow<'_, str>); -/// Allows overriding status code and headers for a responder. -pub struct CustomResponder { - responder: T, - status: Option, - headers: Result, -} - -impl CustomResponder { - fn new(responder: T) -> Self { - CustomResponder { - responder, - status: None, - headers: Ok(HeaderMap::new()), - } - } - - /// Override a status code for the Responder's response. - /// - /// ``` - /// use actix_web::{HttpRequest, Responder, http::StatusCode}; - /// - /// fn index(req: HttpRequest) -> impl Responder { - /// "Welcome!".with_status(StatusCode::OK) - /// } - /// ``` - pub fn with_status(mut self, status: StatusCode) -> Self { - self.status = Some(status); - self - } - - /// Insert header to the final response. - /// - /// Overrides other headers with the same name. - /// - /// ``` - /// use actix_web::{web, HttpRequest, Responder}; - /// use serde::Serialize; - /// - /// #[derive(Serialize)] - /// struct MyObj { - /// name: String, - /// } - /// - /// fn index(req: HttpRequest) -> impl Responder { - /// web::Json(MyObj { name: "Name".to_string() }) - /// .with_header(("x-version", "1.2.3")) - /// .with_header(("x-version", "1.2.3")) - /// } - /// ``` - pub fn with_header(mut self, header: H) -> Self - where - H: IntoHeaderPair, - { - if let Ok(ref mut headers) = self.headers { - match header.try_into_header_pair() { - Ok((key, value)) => headers.append(key, value), - Err(e) => self.headers = Err(e.into()), - }; - } - - self - } -} - -impl Responder for CustomResponder -where - T: Responder, - ::Error: Into, -{ - type Body = EitherBody; - - fn respond_to(self, req: &HttpRequest) -> HttpResponse { - let headers = match self.headers { - Ok(headers) => headers, - Err(err) => return HttpResponse::from_error(err).map_into_right_body(), - }; - - let mut res = self.responder.respond_to(req); - - if let Some(status) = self.status { - *res.status_mut() = status; - } - - for (k, v) in headers { - // TODO: before v4, decide if this should be append instead - res.headers_mut().insert(k, v); - } - - res.map_into_left_body() - } -} - #[cfg(test)] pub(crate) mod tests { use actix_service::Service; @@ -440,59 +342,4 @@ pub(crate) mod tests { assert_eq!(res.status(), StatusCode::BAD_REQUEST); } - - #[actix_rt::test] - async fn test_custom_responder() { - let req = TestRequest::default().to_http_request(); - let res = "test" - .to_string() - .with_status(StatusCode::BAD_REQUEST) - .respond_to(&req); - - assert_eq!(res.status(), StatusCode::BAD_REQUEST); - assert_eq!( - to_bytes(res.into_body()).await.unwrap(), - Bytes::from_static(b"test"), - ); - - let res = "test" - .to_string() - .with_header(("content-type", "json")) - .respond_to(&req); - - assert_eq!(res.status(), StatusCode::OK); - assert_eq!( - res.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("json") - ); - assert_eq!( - to_bytes(res.into_body()).await.unwrap(), - Bytes::from_static(b"test"), - ); - } - - #[actix_rt::test] - async fn test_tuple_responder_with_status_code() { - let req = TestRequest::default().to_http_request(); - let res = ("test".to_string(), StatusCode::BAD_REQUEST).respond_to(&req); - assert_eq!(res.status(), StatusCode::BAD_REQUEST); - assert_eq!( - to_bytes(res.into_body()).await.unwrap(), - Bytes::from_static(b"test"), - ); - - let req = TestRequest::default().to_http_request(); - let res = ("test".to_string(), StatusCode::OK) - .with_header((CONTENT_TYPE, mime::APPLICATION_JSON)) - .respond_to(&req); - assert_eq!(res.status(), StatusCode::OK); - assert_eq!( - res.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/json") - ); - assert_eq!( - to_bytes(res.into_body()).await.unwrap(), - Bytes::from_static(b"test"), - ); - } } diff --git a/src/response/response.rs b/src/response/response.rs index 1900dd845..6fa2082e7 100644 --- a/src/response/response.rs +++ b/src/response/response.rs @@ -244,8 +244,7 @@ impl HttpResponse { where B: MessageBody + 'static, { - // TODO: avoid double boxing with down-casting, if it improves perf - self.map_body(|_, body| BoxBody::new(body)) + self.map_body(|_, body| body.boxed()) } /// Extract response body @@ -314,7 +313,7 @@ impl Future for HttpResponse { #[cfg(feature = "cookies")] pub struct CookieIter<'a> { - iter: header::map::GetAll<'a>, + iter: std::slice::Iter<'a, HeaderValue>, } #[cfg(feature = "cookies")] diff --git a/src/route.rs b/src/route.rs index 4447bff50..0410b99dd 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,4 +1,4 @@ -use std::{future::Future, mem, rc::Rc}; +use std::{mem, rc::Rc}; use actix_http::Method; use actix_service::{ @@ -8,17 +8,16 @@ use actix_service::{ use futures_core::future::LocalBoxFuture; use crate::{ - body::MessageBody, guard::{self, Guard}, handler::{handler_service, Handler}, service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse}, - BoxError, Error, FromRequest, HttpResponse, Responder, + Error, FromRequest, HttpResponse, Responder, }; -/// Resource route definition +/// A request handler with [guards](guard). /// -/// Route uses builder-like pattern for configuration. -/// If handler is not explicitly set, default *404 Not Found* handler is used. +/// Route uses a builder-like pattern for configuration. If handler is not set, a `404 Not Found` +/// handler is used. pub struct Route { service: BoxedHttpServiceFactory, guards: Rc>>, @@ -66,9 +65,12 @@ pub struct RouteService { } impl RouteService { + // TODO: does this need to take &mut ? pub fn check(&self, req: &mut ServiceRequest) -> bool { - for f in self.guards.iter() { - if !f.check(req.head()) { + let guard_ctx = req.guard_ctx(); + + for guard in self.guards.iter() { + if !guard.check(&guard_ctx) { return false; } } @@ -91,6 +93,7 @@ impl Service for RouteService { impl Route { /// Add method guard to the route. /// + /// # Examples /// ``` /// # use actix_web::*; /// # fn main() { @@ -111,6 +114,7 @@ impl Route { /// Add guard to the route. /// + /// # Examples /// ``` /// # use actix_web::*; /// # fn main() { @@ -144,16 +148,13 @@ impl Route { /// format!("Welcome {}!", info.username) /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::resource("/{username}/index.html") // <- define path parameters - /// .route(web::get().to(index)) // <- register handler - /// ); - /// } + /// let app = App::new().service( + /// web::resource("/{username}/index.html") // <- define path parameters + /// .route(web::get().to(index)) // <- register handler + /// ); /// ``` /// /// It is possible to use multiple extractors for one handler function. - /// /// ``` /// # use std::collections::HashMap; /// # use serde::Deserialize; @@ -165,25 +166,24 @@ impl Route { /// } /// /// /// extract path info using serde - /// async fn index(path: web::Path, query: web::Query>, body: web::Json) -> String { + /// async fn index( + /// path: web::Path, + /// query: web::Query>, + /// body: web::Json + /// ) -> String { /// format!("Welcome {}!", path.username) /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::resource("/{username}/index.html") // <- define path parameters - /// .route(web::get().to(index)) - /// ); - /// } + /// let app = App::new().service( + /// web::resource("/{username}/index.html") // <- define path parameters + /// .route(web::get().to(index)) + /// ); /// ``` - pub fn to(mut self, handler: F) -> Self + pub fn to(mut self, handler: F) -> Self where - F: Handler, - T: FromRequest + 'static, - R: Future + 'static, - R::Output: Responder + 'static, - ::Body: MessageBody, - <::Body as MessageBody>::Error: Into, + F: Handler, + Args: FromRequest + 'static, + F::Output: Responder + 'static, { self.service = handler_service(handler); self @@ -203,7 +203,7 @@ impl Route { /// type Error = Infallible; /// type Future = LocalBoxFuture<'static, Result>; /// - /// always_ready!(); + /// dev::always_ready!(); /// /// fn call(&self, req: ServiceRequest) -> Self::Future { /// let (req, _) = req.into_parts(); diff --git a/src/scope.rs b/src/scope.rs index ad102b66b..b4618bb6c 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,6 +1,9 @@ -use std::{cell::RefCell, fmt, future::Future, mem, rc::Rc}; +use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, mem, rc::Rc}; -use actix_http::Extensions; +use actix_http::{ + body::{BoxBody, MessageBody}, + Extensions, +}; use actix_router::{ResourceDef, Router}; use actix_service::{ apply, apply_fn_factory, boxed, IntoServiceFactory, Service, ServiceFactory, @@ -24,35 +27,37 @@ use crate::{ type Guards = Vec>; -/// Resources scope. +/// A collection of [`Route`]s, [`Resource`]s, or other services that share a common path prefix. /// -/// Scope is a set of resources with common root path. -/// Scopes collect multiple paths under a common path prefix. -/// Scope path can contain variable path segments as resources. -/// Scope prefix is always complete path segment, i.e `/app` would -/// be converted to a `/app/` and it would not match `/app` path. +/// The `Scope`'s path can contain [dynamic segments]. The dynamic segments can be extracted from +/// requests using the [`Path`](crate::web::Path) extractor or +/// with [`HttpRequest::match_info()`](crate::HttpRequest::match_info). /// -/// You can get variable path segments from `HttpRequest::match_info()`. -/// `Path` extractor also is able to extract scope level variable segments. +/// # Avoid Trailing Slashes +/// Avoid using trailing slashes in the scope prefix (e.g., `web::scope("/scope/")`). It will almost +/// certainly not have the expected behavior. See the [documentation on resource definitions][pat] +/// to understand why this is the case and how to correctly construct scope/prefix definitions. /// +/// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// -/// fn main() { -/// let app = App::new().service( -/// web::scope("/{project_id}/") -/// .service(web::resource("/path1").to(|| async { "OK" })) -/// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok()))) -/// .service(web::resource("/path3").route(web::head().to(HttpResponse::MethodNotAllowed))) -/// ); -/// } +/// let app = App::new().service( +/// web::scope("/{project_id}/") +/// .service(web::resource("/path1").to(|| async { "OK" })) +/// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok()))) +/// .service(web::resource("/path3").route(web::head().to(HttpResponse::MethodNotAllowed))) +/// ); /// ``` /// /// In the above example three routes get registered: -/// * /{project_id}/path1 - responds to all http method -/// * /{project_id}/path2 - `GET` requests -/// * /{project_id}/path3 - `HEAD` requests -pub struct Scope { +/// - /{project_id}/path1 - responds to all HTTP methods +/// - /{project_id}/path2 - responds to `GET` requests +/// - /{project_id}/path3 - responds to `HEAD` requests +/// +/// [pat]: crate::dev::ResourceDef#prefix-resources +/// [dynamic segments]: crate::dev::ResourceDef#dynamic-segments +pub struct Scope { endpoint: T, rdef: String, app_data: Option, @@ -61,6 +66,7 @@ pub struct Scope { default: Option>, external: Vec, factory_ref: Rc>>, + _phantom: PhantomData, } impl Scope { @@ -77,19 +83,21 @@ impl Scope { default: None, external: Vec::new(), factory_ref, + _phantom: Default::default(), } } } -impl Scope +impl Scope where T: ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, + B: 'static, { /// Add match guard to a scope. /// @@ -100,16 +108,14 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app") - /// .guard(guard::Header("content-type", "text/plain")) - /// .route("/test1", web::get().to(index)) - /// .route("/test2", web::post().to(|r: HttpRequest| { - /// HttpResponse::MethodNotAllowed() - /// })) - /// ); - /// } + /// let app = App::new().service( + /// web::scope("/app") + /// .guard(guard::Header("content-type", "text/plain")) + /// .route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|r: HttpRequest| { + /// HttpResponse::MethodNotAllowed() + /// })) + /// ); /// ``` pub fn guard(mut self, guard: G) -> Self { self.guards.push(Box::new(guard)); @@ -180,15 +186,13 @@ where /// ); /// } /// - /// fn main() { - /// let app = App::new() - /// .wrap(middleware::Logger::default()) - /// .service( - /// web::scope("/api") - /// .configure(config) - /// ) - /// .route("/index.html", web::get().to(|| HttpResponse::Ok())); - /// } + /// let app = App::new() + /// .wrap(middleware::Logger::default()) + /// .service( + /// web::scope("/api") + /// .configure(config) + /// ) + /// .route("/index.html", web::get().to(|| HttpResponse::Ok())); /// ``` pub fn configure(mut self, cfg_fn: F) -> Self where @@ -227,13 +231,11 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app").service( - /// web::scope("/v1") - /// .service(web::resource("/test1").to(index))) - /// ); - /// } + /// let app = App::new().service( + /// web::scope("/app").service( + /// web::scope("/v1") + /// .service(web::resource("/test1").to(index))) + /// ); /// ``` pub fn service(mut self, factory: F) -> Self where @@ -257,13 +259,11 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app") - /// .route("/test1", web::get().to(index)) - /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())) - /// ); - /// } + /// let app = App::new().service( + /// web::scope("/app") + /// .route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())) + /// ); /// ``` pub fn route(self, path: &str, mut route: Route) -> Self { self.service( @@ -295,32 +295,29 @@ where self } - /// Registers middleware, in the form of a middleware component (type), - /// that runs during inbound processing in the request - /// life-cycle (request -> response), modifying request as - /// necessary, across all requests managed by the *Scope*. Scope-level - /// middleware is more limited in what it can modify, relative to Route or - /// Application level middleware, in that Scope-level middleware can not modify - /// ServiceResponse. + /// Registers middleware, in the form of a middleware component (type), that runs during inbound + /// processing in the request life-cycle (request -> response), modifying request as necessary, + /// across all requests managed by the *Scope*. /// /// Use middleware when you need to read or modify *every* request in some way. - pub fn wrap( + pub fn wrap( self, mw: M, ) -> Scope< impl ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, + B1, > where M: Transform< T::Service, ServiceRequest, - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, @@ -334,16 +331,15 @@ where default: self.default, external: self.external, factory_ref: self.factory_ref, + _phantom: PhantomData, } } - /// Registers middleware, in the form of a closure, that runs during inbound - /// processing in the request life-cycle (request -> response), modifying - /// request as necessary, across all requests managed by the *Scope*. - /// Scope-level middleware is more limited in what it can modify, relative - /// to Route or Application level middleware, in that Scope-level middleware - /// can not modify ServiceResponse. + /// Registers middleware, in the form of a closure, that runs during inbound processing in the + /// request life-cycle (request -> response), modifying request as necessary, across all + /// requests managed by the *Scope*. /// + /// # Examples /// ``` /// use actix_service::Service; /// use actix_web::{web, App}; @@ -353,37 +349,36 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app") - /// .wrap_fn(|req, srv| { - /// let fut = srv.call(req); - /// async { - /// let mut res = fut.await?; - /// res.headers_mut().insert( - /// CONTENT_TYPE, HeaderValue::from_static("text/plain"), - /// ); - /// Ok(res) - /// } - /// }) - /// .route("/index.html", web::get().to(index))); - /// } + /// let app = App::new().service( + /// web::scope("/app") + /// .wrap_fn(|req, srv| { + /// let fut = srv.call(req); + /// async { + /// let mut res = fut.await?; + /// res.headers_mut().insert( + /// CONTENT_TYPE, HeaderValue::from_static("text/plain"), + /// ); + /// Ok(res) + /// } + /// }) + /// .route("/index.html", web::get().to(index))); /// ``` - pub fn wrap_fn( + pub fn wrap_fn( self, mw: F, ) -> Scope< impl ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, + B1, > where F: Fn(ServiceRequest, &T::Service) -> R + Clone, - R: Future>, + R: Future, Error>>, { Scope { endpoint: apply_fn_factory(self.endpoint, mw), @@ -394,19 +389,21 @@ where default: self.default, external: self.external, factory_ref: self.factory_ref, + _phantom: PhantomData, } } } -impl HttpServiceFactory for Scope +impl HttpServiceFactory for Scope where T: ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), > + 'static, + B: MessageBody + 'static, { fn register(mut self, config: &mut AppService) { // update default resource if needed @@ -456,7 +453,9 @@ where req.add_data_container(Rc::clone(data)); } - srv.call(req) + let fut = srv.call(req); + + async { Ok(fut.await?.map_into_boxed_body()) } }); // register final service @@ -470,6 +469,7 @@ where } pub struct ScopeFactory { + #[allow(clippy::type_complexity)] services: Rc< [( ResourceDef, @@ -538,12 +538,15 @@ impl Service for ScopeService { fn call(&self, mut req: ServiceRequest) -> Self::Future { let res = self.router.recognize_fn(&mut req, |req, guards| { if let Some(ref guards) = guards { - for f in guards { - if !f.check(req.head()) { + let guard_ctx = req.guard_ctx(); + + for guard in guards { + if !guard.check(&guard_ctx) { return false; } } } + true }); @@ -585,18 +588,47 @@ mod tests { use actix_utils::future::ok; use bytes::Bytes; + use super::*; use crate::{ guard, http::{ header::{self, HeaderValue}, Method, StatusCode, }, - middleware::DefaultHeaders, + middleware::{Compat, DefaultHeaders}, service::{ServiceRequest, ServiceResponse}, test::{assert_body_eq, call_service, init_service, read_body, TestRequest}, web, App, HttpMessage, HttpRequest, HttpResponse, }; + #[test] + fn can_be_returned_from_fn() { + fn my_scope() -> Scope { + web::scope("/test") + .service(web::resource("").route(web::get().to(|| async { "hello" }))) + } + + fn my_compat_scope() -> Scope< + impl ServiceFactory< + ServiceRequest, + Config = (), + Response = ServiceResponse, + Error = Error, + InitError = (), + >, + > { + web::scope("/test-compat") + .wrap_fn(|req, srv| { + let fut = srv.call(req); + async { Ok(fut.await?.map_into_right_body::<()>()) } + }) + .wrap(Compat::noop()) + .service(web::resource("").route(web::get().to(|| async { "hello" }))) + } + + App::new().service(my_scope()).service(my_compat_scope()); + } + #[actix_rt::test] async fn test_scope() { let srv = @@ -934,7 +966,7 @@ mod tests { web::scope("app") .wrap( DefaultHeaders::new() - .header(header::CONTENT_TYPE, HeaderValue::from_static("0001")), + .add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))), ) .service(web::resource("/test").route(web::get().to(HttpResponse::Ok))), ), @@ -950,6 +982,29 @@ mod tests { ); } + #[actix_rt::test] + async fn test_middleware_body_type() { + // Compile test that Scope accepts any body type; test for `EitherBody` + let srv = init_service( + App::new().service( + web::scope("app") + .wrap_fn(|req, srv| { + let fut = srv.call(req); + async { Ok(fut.await?.map_into_right_body::<()>()) } + }) + .service(web::resource("/test").route(web::get().to(|| async { "hello" }))), + ), + ) + .await; + + // test if `MessageBody::try_into_bytes()` is preserved across scope layer + use actix_http::body::MessageBody as _; + let req = TestRequest::with_uri("/app/test").to_request(); + let resp = call_service(&srv, req).await; + let body = resp.into_body(); + assert_eq!(body.try_into_bytes().unwrap(), b"hello".as_ref()); + } + #[actix_rt::test] async fn test_middleware_fn() { let srv = init_service( diff --git a/src/server.rs b/src/server.rs index 3db849410..ed0c965b3 100644 --- a/src/server.rs +++ b/src/server.rs @@ -63,6 +63,7 @@ where backlog: u32, sockets: Vec, builder: ServerBuilder, + #[allow(clippy::type_complexity)] on_connect_fn: Option>, _phantom: PhantomData<(S, B)>, } @@ -101,9 +102,9 @@ where /// Sets function that will be called once before each connection is handled. /// It will receive a `&std::any::Any`, which contains underlying connection type and an - /// [Extensions] container so that request-local data can be passed to middleware and handlers. + /// [Extensions] container so that connection data can be accessed in middleware and handlers. /// - /// For example: + /// # Connection Types /// - `actix_tls::accept::openssl::TlsStream` when using openssl. /// - `actix_tls::accept::rustls::TlsStream` when using rustls. /// - `actix_web::rt::net::TcpStream` when no encryption is used. diff --git a/src/service.rs b/src/service.rs index 4185d6018..975556197 100644 --- a/src/service.rs +++ b/src/service.rs @@ -7,7 +7,7 @@ use std::{ use actix_http::{ body::{BoxBody, EitherBody, MessageBody}, header::HeaderMap, - Extensions, HttpMessage, Method, Payload, PayloadStream, RequestHead, Response, + BoxedPayloadStream, Extensions, HttpMessage, Method, Payload, RequestHead, Response, ResponseHead, StatusCode, Uri, Version, }; use actix_router::{IntoPatterns, Path, Patterns, Resource, ResourceDef, Url}; @@ -21,7 +21,7 @@ use cookie::{Cookie, ParseError as CookieParseError}; use crate::{ config::{AppConfig, AppService}, dev::ensure_leading_slash, - guard::Guard, + guard::{Guard, GuardContext}, info::ConnectionInfo, rmap::ResourceMap, Error, HttpRequest, HttpResponse, @@ -172,12 +172,10 @@ impl ServiceRequest { self.head().uri.path() } - /// The query string in the URL. - /// - /// E.g., id=10 + /// Counterpart to [`HttpRequest::query_string`]. #[inline] pub fn query_string(&self) -> &str { - self.uri().query().unwrap_or_default() + self.req.query_string() } /// Peer socket address. @@ -196,7 +194,7 @@ impl ServiceRequest { /// Get *ConnectionInfo* for the current request. #[inline] pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> { - ConnectionInfo::get(self.head(), &*self.app_config()) + self.req.connection_info() } /// Get a reference to the Path parameters. @@ -210,13 +208,13 @@ impl ServiceRequest { self.req.match_info() } - /// Counterpart to [`HttpRequest::match_name`](super::HttpRequest::match_name()). + /// Counterpart to [`HttpRequest::match_name`]. #[inline] pub fn match_name(&self) -> Option<&str> { self.req.match_name() } - /// Counterpart to [`HttpRequest::match_pattern`](super::HttpRequest::match_pattern()). + /// Counterpart to [`HttpRequest::match_pattern`]. #[inline] pub fn match_pattern(&self) -> Option { self.req.match_pattern() @@ -240,7 +238,8 @@ impl ServiceRequest { self.req.app_config() } - /// Counterpart to [`HttpRequest::app_data`](super::HttpRequest::app_data()). + /// Counterpart to [`HttpRequest::app_data`]. + #[inline] pub fn app_data(&self) -> Option<&T> { for container in self.req.inner.app_data.iter().rev() { if let Some(data) = container.get::() { @@ -251,18 +250,39 @@ impl ServiceRequest { None } + /// Counterpart to [`HttpRequest::conn_data`]. + #[inline] + pub fn conn_data(&self) -> Option<&T> { + self.req.conn_data() + } + + /// Counterpart to [`HttpRequest::req_data`]. + #[inline] + pub fn req_data(&self) -> Ref<'_, Extensions> { + self.req.req_data() + } + + /// Counterpart to [`HttpRequest::req_data_mut`]. + #[inline] + pub fn req_data_mut(&self) -> RefMut<'_, Extensions> { + self.req.req_data_mut() + } + #[cfg(feature = "cookies")] + #[inline] pub fn cookies(&self) -> Result>>, CookieParseError> { self.req.cookies() } /// Return request cookie. #[cfg(feature = "cookies")] + #[inline] pub fn cookie(&self, name: &str) -> Option> { self.req.cookie(name) } /// Set request payload. + #[inline] pub fn set_payload(&mut self, payload: Payload) { self.payload = payload; } @@ -277,16 +297,25 @@ impl ServiceRequest { .app_data .push(extensions); } + + /// Creates a context object for use with a [guard](crate::guard). + /// + /// Useful if you are implementing + #[inline] + pub fn guard_ctx(&self) -> GuardContext<'_> { + GuardContext { req: self } + } } impl Resource for ServiceRequest { + #[inline] fn resource_path(&mut self) -> &mut Path { self.match_info_mut() } } impl HttpMessage for ServiceRequest { - type Stream = PayloadStream; + type Stream = BoxedPayloadStream; #[inline] /// Returns Request's headers. @@ -403,13 +432,18 @@ impl ServiceResponse { self.response.headers_mut() } + /// Destructures `ServiceResponse` into request and response components. + #[inline] + pub fn into_parts(self) -> (HttpRequest, HttpResponse) { + (self.request, self.response) + } + /// Extract response body + #[inline] pub fn into_body(self) -> B { self.response.into_body() } -} -impl ServiceResponse { /// Set a new body #[inline] pub fn map_body(self, f: F) -> ServiceResponse @@ -439,7 +473,7 @@ impl ServiceResponse { where B: MessageBody + 'static, { - self.map_body(|_, body| BoxBody::new(body)) + self.map_body(|_, body| body.boxed()) } } diff --git a/src/test.rs b/src/test.rs deleted file mode 100644 index 07d2d16b6..000000000 --- a/src/test.rs +++ /dev/null @@ -1,894 +0,0 @@ -//! Various helpers for Actix applications to use during testing. - -use std::{borrow::Cow, net::SocketAddr, rc::Rc}; - -pub use actix_http::test::TestBuffer; -use actix_http::{ - header::IntoHeaderPair, test::TestRequest as HttpTestRequest, Extensions, Method, Request, - StatusCode, Uri, Version, -}; -use actix_router::{Path, ResourceDef, Url}; -use actix_service::{IntoService, IntoServiceFactory, Service, ServiceFactory}; -use actix_utils::future::{ok, poll_fn}; -use futures_core::Stream; -use futures_util::StreamExt as _; -use serde::{de::DeserializeOwned, Serialize}; - -#[cfg(feature = "cookies")] -use crate::cookie::{Cookie, CookieJar}; -use crate::{ - app_service::AppInitServiceState, - body::{self, BoxBody, MessageBody}, - config::AppConfig, - data::Data, - dev::Payload, - http::header::ContentType, - rmap::ResourceMap, - service::{ServiceRequest, ServiceResponse}, - web::{Bytes, BytesMut}, - Error, HttpRequest, HttpResponse, HttpResponseBuilder, -}; - -/// Create service that always responds with `HttpResponse::Ok()` and no body. -pub fn ok_service( -) -> impl Service, Error = Error> { - default_service(StatusCode::OK) -} - -/// Create service that always responds with given status code and no body. -pub fn default_service( - status_code: StatusCode, -) -> impl Service, Error = Error> { - (move |req: ServiceRequest| { - ok(req.into_response(HttpResponseBuilder::new(status_code).finish())) - }) - .into_service() -} - -/// Initialize service from application builder instance. -/// -/// ``` -/// use actix_service::Service; -/// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; -/// -/// #[actix_web::test] -/// async fn test_init_service() { -/// let app = test::init_service( -/// App::new() -/// .service(web::resource("/test").to(|| async { "OK" })) -/// ).await; -/// -/// // Create request object -/// let req = test::TestRequest::with_uri("/test").to_request(); -/// -/// // Execute application -/// let resp = app.call(req).await.unwrap(); -/// assert_eq!(resp.status(), StatusCode::OK); -/// } -/// ``` -pub async fn init_service( - app: R, -) -> impl Service, Error = E> -where - R: IntoServiceFactory, - S: ServiceFactory, Error = E>, - S::InitError: std::fmt::Debug, -{ - try_init_service(app) - .await - .expect("service initialization failed") -} - -/// Fallible version of [`init_service`] that allows testing initialization errors. -pub(crate) async fn try_init_service( - app: R, -) -> Result, Error = E>, S::InitError> -where - R: IntoServiceFactory, - S: ServiceFactory, Error = E>, - S::InitError: std::fmt::Debug, -{ - let srv = app.into_factory(); - srv.new_service(AppConfig::default()).await -} - -/// Calls service and waits for response future completion. -/// -/// ``` -/// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; -/// -/// #[actix_web::test] -/// async fn test_response() { -/// let app = test::init_service( -/// App::new() -/// .service(web::resource("/test").to(|| async { -/// HttpResponse::Ok() -/// })) -/// ).await; -/// -/// // Create request object -/// let req = test::TestRequest::with_uri("/test").to_request(); -/// -/// // Call application -/// let resp = test::call_service(&app, req).await; -/// assert_eq!(resp.status(), StatusCode::OK); -/// } -/// ``` -pub async fn call_service(app: &S, req: R) -> S::Response -where - S: Service, Error = E>, - E: std::fmt::Debug, -{ - app.call(req).await.unwrap() -} - -/// Helper function that returns a response body of a TestRequest -/// -/// ``` -/// use actix_web::{test, web, App, HttpResponse, http::header}; -/// use bytes::Bytes; -/// -/// #[actix_web::test] -/// async fn test_index() { -/// let app = test::init_service( -/// App::new().service( -/// web::resource("/index.html") -/// .route(web::post().to(|| async { -/// HttpResponse::Ok().body("welcome!") -/// }))) -/// ).await; -/// -/// let req = test::TestRequest::post() -/// .uri("/index.html") -/// .header(header::CONTENT_TYPE, "application/json") -/// .to_request(); -/// -/// let result = test::read_response(&app, req).await; -/// assert_eq!(result, Bytes::from_static(b"welcome!")); -/// } -/// ``` -pub async fn read_response(app: &S, req: Request) -> Bytes -where - S: Service, Error = Error>, - B: MessageBody + Unpin, - B::Error: Into, -{ - let resp = app - .call(req) - .await - .unwrap_or_else(|e| panic!("read_response failed at application call: {}", e)); - - let body = resp.into_body(); - let mut bytes = BytesMut::new(); - - actix_rt::pin!(body); - while let Some(item) = poll_fn(|cx| body.as_mut().poll_next(cx)).await { - bytes.extend_from_slice(&item.map_err(Into::into).unwrap()); - } - - bytes.freeze() -} - -/// Helper function that returns a response body of a ServiceResponse. -/// -/// ``` -/// use actix_web::{test, web, App, HttpResponse, http::header}; -/// use bytes::Bytes; -/// -/// #[actix_web::test] -/// async fn test_index() { -/// let app = test::init_service( -/// App::new().service( -/// web::resource("/index.html") -/// .route(web::post().to(|| async { -/// HttpResponse::Ok().body("welcome!") -/// }))) -/// ).await; -/// -/// let req = test::TestRequest::post() -/// .uri("/index.html") -/// .header(header::CONTENT_TYPE, "application/json") -/// .to_request(); -/// -/// let resp = test::call_service(&app, req).await; -/// let result = test::read_body(resp).await; -/// assert_eq!(result, Bytes::from_static(b"welcome!")); -/// } -/// ``` -pub async fn read_body(res: ServiceResponse) -> Bytes -where - B: MessageBody + Unpin, - B::Error: Into, -{ - let body = res.into_body(); - let mut bytes = BytesMut::new(); - - actix_rt::pin!(body); - while let Some(item) = poll_fn(|cx| body.as_mut().poll_next(cx)).await { - bytes.extend_from_slice(&item.map_err(Into::into).unwrap()); - } - - bytes.freeze() -} - -/// Helper function that returns a deserialized response body of a ServiceResponse. -/// -/// ``` -/// use actix_web::{App, test, web, HttpResponse, http::header}; -/// use serde::{Serialize, Deserialize}; -/// -/// #[derive(Serialize, Deserialize)] -/// pub struct Person { -/// id: String, -/// name: String, -/// } -/// -/// #[actix_web::test] -/// async fn test_post_person() { -/// let app = test::init_service( -/// App::new().service( -/// web::resource("/people") -/// .route(web::post().to(|person: web::Json| async { -/// HttpResponse::Ok() -/// .json(person)}) -/// )) -/// ).await; -/// -/// let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); -/// -/// let resp = test::TestRequest::post() -/// .uri("/people") -/// .header(header::CONTENT_TYPE, "application/json") -/// .set_payload(payload) -/// .send_request(&mut app) -/// .await; -/// -/// assert!(resp.status().is_success()); -/// -/// let result: Person = test::read_body_json(resp).await; -/// } -/// ``` -pub async fn read_body_json(res: ServiceResponse) -> T -where - B: MessageBody + Unpin, - B::Error: Into, - T: DeserializeOwned, -{ - let body = read_body(res).await; - - serde_json::from_slice(&body).unwrap_or_else(|e| { - panic!( - "read_response_json failed during deserialization of body: {:?}, {}", - body, e - ) - }) -} - -pub async fn load_stream(mut stream: S) -> Result -where - S: Stream> + Unpin, -{ - let mut data = BytesMut::new(); - while let Some(item) = stream.next().await { - data.extend_from_slice(&item?); - } - Ok(data.freeze()) -} - -pub async fn load_body(body: B) -> Result -where - B: MessageBody + Unpin, - B::Error: Into, -{ - body::to_bytes(body).await.map_err(Into::into) -} - -/// Helper function that returns a deserialized response body of a TestRequest -/// -/// ``` -/// use actix_web::{App, test, web, HttpResponse, http::header}; -/// use serde::{Serialize, Deserialize}; -/// -/// #[derive(Serialize, Deserialize)] -/// pub struct Person { -/// id: String, -/// name: String -/// } -/// -/// #[actix_web::test] -/// async fn test_add_person() { -/// let app = test::init_service( -/// App::new().service( -/// web::resource("/people") -/// .route(web::post().to(|person: web::Json| async { -/// HttpResponse::Ok() -/// .json(person)}) -/// )) -/// ).await; -/// -/// let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); -/// -/// let req = test::TestRequest::post() -/// .uri("/people") -/// .header(header::CONTENT_TYPE, "application/json") -/// .set_payload(payload) -/// .to_request(); -/// -/// let result: Person = test::read_response_json(&mut app, req).await; -/// } -/// ``` -pub async fn read_response_json(app: &S, req: Request) -> T -where - S: Service, Error = Error>, - B: MessageBody + Unpin, - B::Error: Into, - T: DeserializeOwned, -{ - let body = read_response(app, req).await; - - serde_json::from_slice(&body).unwrap_or_else(|_| { - panic!( - "read_response_json failed during deserialization of body: {:?}", - body - ) - }) -} - -/// Test `Request` builder. -/// -/// 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. -/// -/// ``` -/// use actix_web::{test, HttpRequest, HttpResponse, HttpMessage}; -/// use actix_web::http::{header, StatusCode}; -/// -/// async fn index(req: HttpRequest) -> HttpResponse { -/// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { -/// HttpResponse::Ok().into() -/// } else { -/// HttpResponse::BadRequest().into() -/// } -/// } -/// -/// #[actix_web::test] -/// async fn test_index() { -/// let req = test::TestRequest::default().insert_header("content-type", "text/plain") -/// .to_http_request(); -/// -/// let resp = index(req).await.unwrap(); -/// assert_eq!(resp.status(), StatusCode::OK); -/// -/// let req = test::TestRequest::default().to_http_request(); -/// let resp = index(req).await.unwrap(); -/// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); -/// } -/// ``` -pub struct TestRequest { - req: HttpTestRequest, - rmap: ResourceMap, - config: AppConfig, - path: Path, - peer_addr: Option, - app_data: Extensions, - #[cfg(feature = "cookies")] - cookies: CookieJar, -} - -impl Default for TestRequest { - fn default() -> TestRequest { - TestRequest { - req: HttpTestRequest::default(), - rmap: ResourceMap::new(ResourceDef::new("")), - config: AppConfig::default(), - path: Path::new(Url::new(Uri::default())), - peer_addr: None, - app_data: Extensions::new(), - #[cfg(feature = "cookies")] - cookies: CookieJar::new(), - } - } -} - -#[allow(clippy::wrong_self_convention)] -impl TestRequest { - /// Create TestRequest and set request uri - pub fn with_uri(path: &str) -> TestRequest { - TestRequest::default().uri(path) - } - - /// Create TestRequest and set method to `Method::GET` - pub fn get() -> TestRequest { - TestRequest::default().method(Method::GET) - } - - /// Create TestRequest and set method to `Method::POST` - pub fn post() -> TestRequest { - TestRequest::default().method(Method::POST) - } - - /// Create TestRequest and set method to `Method::PUT` - pub fn put() -> TestRequest { - TestRequest::default().method(Method::PUT) - } - - /// Create TestRequest and set method to `Method::PATCH` - pub fn patch() -> TestRequest { - TestRequest::default().method(Method::PATCH) - } - - /// Create TestRequest and set method to `Method::DELETE` - pub fn delete() -> TestRequest { - TestRequest::default().method(Method::DELETE) - } - - /// Set HTTP version of this request - pub fn version(mut self, ver: Version) -> Self { - self.req.version(ver); - self - } - - /// Set HTTP method of this request - pub fn method(mut self, meth: Method) -> Self { - self.req.method(meth); - self - } - - /// Set HTTP Uri of this request - pub fn uri(mut self, path: &str) -> Self { - self.req.uri(path); - self - } - - /// Insert a header, replacing any that were set with an equivalent field name. - pub fn insert_header(mut self, header: H) -> Self - where - H: IntoHeaderPair, - { - self.req.insert_header(header); - self - } - - /// Append a header, keeping any that were set with an equivalent field name. - pub fn append_header(mut self, header: H) -> Self - where - H: IntoHeaderPair, - { - self.req.append_header(header); - self - } - - /// Set cookie for this request. - #[cfg(feature = "cookies")] - pub fn cookie(mut self, cookie: Cookie<'_>) -> Self { - self.cookies.add(cookie.into_owned()); - self - } - - /// Set request path pattern parameter. - /// - /// # Examples - /// ``` - /// use actix_web::test::TestRequest; - /// - /// let req = TestRequest::default().param("foo", "bar"); - /// let req = TestRequest::default().param("foo".to_owned(), "bar".to_owned()); - /// ``` - pub fn param( - mut self, - name: impl Into>, - value: impl Into>, - ) -> Self { - self.path.add_static(name, value); - self - } - - /// Set peer addr. - pub fn peer_addr(mut self, addr: SocketAddr) -> Self { - self.peer_addr = Some(addr); - self - } - - /// Set request payload. - pub fn set_payload>(mut self, data: B) -> Self { - self.req.set_payload(data); - self - } - - /// Serialize `data` to a URL encoded form and set it as the request payload. The `Content-Type` - /// header is set to `application/x-www-form-urlencoded`. - pub fn set_form(mut self, data: &T) -> Self { - let bytes = serde_urlencoded::to_string(data) - .expect("Failed to serialize test data as a urlencoded form"); - self.req.set_payload(bytes); - self.req.insert_header(ContentType::form_url_encoded()); - self - } - - /// Serialize `data` to JSON and set it as the request payload. The `Content-Type` header is - /// set to `application/json`. - pub fn set_json(mut self, data: &T) -> Self { - let bytes = serde_json::to_string(data).expect("Failed to serialize test data to json"); - self.req.set_payload(bytes); - self.req.insert_header(ContentType::json()); - self - } - - /// Set application data. This is equivalent of `App::data()` method - /// for testing purpose. - pub fn data(mut self, data: T) -> Self { - self.app_data.insert(Data::new(data)); - self - } - - /// Set application data. This is equivalent of `App::app_data()` method - /// for testing purpose. - pub fn app_data(mut self, data: T) -> Self { - self.app_data.insert(data); - self - } - - #[cfg(test)] - /// Set request config - pub(crate) fn rmap(mut self, rmap: ResourceMap) -> Self { - self.rmap = rmap; - self - } - - fn finish(&mut self) -> Request { - // mut used when cookie feature is enabled - #[allow(unused_mut)] - let mut req = self.req.finish(); - - #[cfg(feature = "cookies")] - { - use actix_http::header::{HeaderValue, COOKIE}; - - let cookie: String = self - .cookies - .delta() - // ensure only name=value is written to cookie header - .map(|c| c.stripped().encoded().to_string()) - .collect::>() - .join("; "); - - if !cookie.is_empty() { - req.headers_mut() - .insert(COOKIE, HeaderValue::from_str(&cookie).unwrap()); - } - } - - req - } - - /// Complete request creation and generate `Request` instance - pub fn to_request(mut self) -> Request { - let mut req = self.finish(); - req.head_mut().peer_addr = self.peer_addr; - req - } - - /// Complete request creation and generate `ServiceRequest` instance - pub fn to_srv_request(mut self) -> ServiceRequest { - let (mut head, payload) = self.finish().into_parts(); - head.peer_addr = self.peer_addr; - self.path.get_mut().update(&head.uri); - - let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); - - ServiceRequest::new( - HttpRequest::new(self.path, head, app_state, Rc::new(self.app_data)), - payload, - ) - } - - /// Complete request creation and generate `ServiceResponse` instance - pub fn to_srv_response(self, res: HttpResponse) -> ServiceResponse { - self.to_srv_request().into_response(res) - } - - /// Complete request creation and generate `HttpRequest` instance - pub fn to_http_request(mut self) -> HttpRequest { - let (mut head, _) = self.finish().into_parts(); - head.peer_addr = self.peer_addr; - self.path.get_mut().update(&head.uri); - - let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); - - HttpRequest::new(self.path, head, app_state, Rc::new(self.app_data)) - } - - /// Complete request creation and generate `HttpRequest` and `Payload` instances - pub fn to_http_parts(mut self) -> (HttpRequest, Payload) { - let (mut head, payload) = self.finish().into_parts(); - head.peer_addr = self.peer_addr; - self.path.get_mut().update(&head.uri); - - let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); - - let req = HttpRequest::new(self.path, head, app_state, Rc::new(self.app_data)); - - (req, payload) - } - - /// Complete request creation, calls service and waits for response future completion. - pub async fn send_request(self, app: &S) -> S::Response - where - S: Service, Error = E>, - E: std::fmt::Debug, - { - let req = self.to_request(); - call_service(app, req).await - } - - #[cfg(test)] - pub fn set_server_hostname(&mut self, host: &str) { - self.config.set_host(host) - } -} - -/// Reduces boilerplate code when testing expected response payloads. -#[cfg(test)] -macro_rules! assert_body_eq { - ($res:ident, $expected:expr) => { - assert_eq!( - ::actix_http::body::to_bytes($res.into_body()) - .await - .expect("body read should have succeeded"), - Bytes::from_static($expected), - ) - }; -} - -#[cfg(test)] -pub(crate) use assert_body_eq; - -#[cfg(test)] -mod tests { - use std::time::SystemTime; - - use actix_http::HttpMessage; - use serde::{Deserialize, Serialize}; - - use super::*; - use crate::{http::header, web, App, HttpResponse, Responder}; - - #[actix_rt::test] - async fn test_basics() { - let req = TestRequest::default() - .version(Version::HTTP_2) - .insert_header(header::ContentType::json()) - .insert_header(header::Date(SystemTime::now().into())) - .param("test", "123") - .data(10u32) - .app_data(20u64) - .peer_addr("127.0.0.1:8081".parse().unwrap()) - .to_http_request(); - assert!(req.headers().contains_key(header::CONTENT_TYPE)); - assert!(req.headers().contains_key(header::DATE)); - assert_eq!( - req.head().peer_addr, - Some("127.0.0.1:8081".parse().unwrap()) - ); - assert_eq!(&req.match_info()["test"], "123"); - assert_eq!(req.version(), Version::HTTP_2); - let data = req.app_data::>().unwrap(); - assert!(req.app_data::>().is_none()); - assert_eq!(*data.get_ref(), 10); - - assert!(req.app_data::().is_none()); - let data = req.app_data::().unwrap(); - assert_eq!(*data, 20); - } - - #[actix_rt::test] - async fn test_request_methods() { - let app = init_service( - App::new().service( - web::resource("/index.html") - .route(web::put().to(|| HttpResponse::Ok().body("put!"))) - .route(web::patch().to(|| HttpResponse::Ok().body("patch!"))) - .route(web::delete().to(|| HttpResponse::Ok().body("delete!"))), - ), - ) - .await; - - let put_req = TestRequest::put() - .uri("/index.html") - .insert_header((header::CONTENT_TYPE, "application/json")) - .to_request(); - - let result = read_response(&app, put_req).await; - assert_eq!(result, Bytes::from_static(b"put!")); - - let patch_req = TestRequest::patch() - .uri("/index.html") - .insert_header((header::CONTENT_TYPE, "application/json")) - .to_request(); - - let result = read_response(&app, patch_req).await; - assert_eq!(result, Bytes::from_static(b"patch!")); - - let delete_req = TestRequest::delete().uri("/index.html").to_request(); - let result = read_response(&app, delete_req).await; - assert_eq!(result, Bytes::from_static(b"delete!")); - } - - #[actix_rt::test] - async fn test_response() { - let app = init_service( - App::new().service( - web::resource("/index.html") - .route(web::post().to(|| HttpResponse::Ok().body("welcome!"))), - ), - ) - .await; - - let req = TestRequest::post() - .uri("/index.html") - .insert_header((header::CONTENT_TYPE, "application/json")) - .to_request(); - - let result = read_response(&app, req).await; - assert_eq!(result, Bytes::from_static(b"welcome!")); - } - - #[actix_rt::test] - async fn test_send_request() { - let app = init_service( - App::new().service( - web::resource("/index.html") - .route(web::get().to(|| HttpResponse::Ok().body("welcome!"))), - ), - ) - .await; - - let resp = TestRequest::get() - .uri("/index.html") - .send_request(&app) - .await; - - let result = read_body(resp).await; - assert_eq!(result, Bytes::from_static(b"welcome!")); - } - - #[derive(Serialize, Deserialize)] - pub struct Person { - id: String, - name: String, - } - - #[actix_rt::test] - async fn test_response_json() { - let app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), - ))) - .await; - - let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); - - let req = TestRequest::post() - .uri("/people") - .insert_header((header::CONTENT_TYPE, "application/json")) - .set_payload(payload) - .to_request(); - - let result: Person = read_response_json(&app, req).await; - assert_eq!(&result.id, "12345"); - } - - #[actix_rt::test] - async fn test_body_json() { - let app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), - ))) - .await; - - let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); - - let resp = TestRequest::post() - .uri("/people") - .insert_header((header::CONTENT_TYPE, "application/json")) - .set_payload(payload) - .send_request(&app) - .await; - - let result: Person = read_body_json(resp).await; - assert_eq!(&result.name, "User name"); - } - - #[actix_rt::test] - async fn test_request_response_form() { - let app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Form| HttpResponse::Ok().json(person)), - ))) - .await; - - let payload = Person { - id: "12345".to_string(), - name: "User name".to_string(), - }; - - let req = TestRequest::post() - .uri("/people") - .set_form(&payload) - .to_request(); - - assert_eq!(req.content_type(), "application/x-www-form-urlencoded"); - - let result: Person = read_response_json(&app, req).await; - assert_eq!(&result.id, "12345"); - assert_eq!(&result.name, "User name"); - } - - #[actix_rt::test] - async fn test_request_response_json() { - let app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), - ))) - .await; - - let payload = Person { - id: "12345".to_string(), - name: "User name".to_string(), - }; - - let req = TestRequest::post() - .uri("/people") - .set_json(&payload) - .to_request(); - - assert_eq!(req.content_type(), "application/json"); - - let result: Person = read_response_json(&app, req).await; - assert_eq!(&result.id, "12345"); - assert_eq!(&result.name, "User name"); - } - - #[actix_rt::test] - async fn test_async_with_block() { - async fn async_with_block() -> Result { - let res = web::block(move || Some(4usize).ok_or("wrong")).await; - - match res { - Ok(value) => Ok(HttpResponse::Ok() - .content_type("text/plain") - .body(format!("Async with block value: {:?}", value))), - Err(_) => panic!("Unexpected"), - } - } - - let app = - init_service(App::new().service(web::resource("/index.html").to(async_with_block))) - .await; - - let req = TestRequest::post().uri("/index.html").to_request(); - let res = app.call(req).await.unwrap(); - assert!(res.status().is_success()); - } - - // allow deprecated App::data - #[allow(deprecated)] - #[actix_rt::test] - async fn test_server_data() { - async fn handler(data: web::Data) -> impl Responder { - assert_eq!(**data, 10); - HttpResponse::Ok() - } - - let app = init_service( - App::new() - .data(10usize) - .service(web::resource("/index.html").to(handler)), - ) - .await; - - let req = TestRequest::post().uri("/index.html").to_request(); - let res = app.call(req).await.unwrap(); - assert!(res.status().is_success()); - } -} diff --git a/src/test/mod.rs b/src/test/mod.rs new file mode 100644 index 000000000..a29dfc437 --- /dev/null +++ b/src/test/mod.rs @@ -0,0 +1,81 @@ +//! Various helpers for Actix applications to use during testing. +//! +//! # Creating A Test Service +//! - [`init_service`] +//! +//! # Off-The-Shelf Test Services +//! - [`ok_service`] +//! - [`simple_service`] +//! +//! # Calling Test Service +//! - [`TestRequest`] +//! - [`call_service`] +//! - [`call_and_read_body`] +//! - [`call_and_read_body_json`] +//! +//! # Reading Response Payloads +//! - [`read_body`] +//! - [`read_body_json`] + +// TODO: more docs on generally how testing works with these parts + +pub use actix_http::test::TestBuffer; + +mod test_request; +mod test_services; +mod test_utils; + +pub use self::test_request::TestRequest; +#[allow(deprecated)] +pub use self::test_services::{default_service, ok_service, simple_service}; +#[allow(deprecated)] +pub use self::test_utils::{ + call_and_read_body, call_and_read_body_json, call_service, init_service, read_body, + read_body_json, read_response, read_response_json, +}; + +#[cfg(test)] +pub(crate) use self::test_utils::try_init_service; + +/// Reduces boilerplate code when testing expected response payloads. +/// +/// Must be used inside an async test. Works for both `ServiceRequest` and `HttpRequest`. +/// +/// # Examples +/// ``` +/// use actix_web::{http::StatusCode, HttpResponse}; +/// +/// let res = HttpResponse::with_body(StatusCode::OK, "http response"); +/// assert_body_eq!(res, b"http response"); +/// ``` +#[cfg(test)] +macro_rules! assert_body_eq { + ($res:ident, $expected:expr) => { + assert_eq!( + ::actix_http::body::to_bytes($res.into_body()) + .await + .expect("error reading test response body"), + ::bytes::Bytes::from_static($expected), + ) + }; +} + +#[cfg(test)] +pub(crate) use assert_body_eq; + +#[cfg(test)] +mod tests { + use super::*; + use crate::{http::StatusCode, service::ServiceResponse, HttpResponse}; + + #[actix_rt::test] + async fn assert_body_works_for_service_and_regular_response() { + let res = HttpResponse::with_body(StatusCode::OK, "http response"); + assert_body_eq!(res, b"http response"); + + let req = TestRequest::default().to_http_request(); + let res = HttpResponse::with_body(StatusCode::OK, "service response"); + let res = ServiceResponse::new(req, res); + assert_body_eq!(res, b"service response"); + } +} diff --git a/src/test/test_request.rs b/src/test/test_request.rs new file mode 100644 index 000000000..fc42253d7 --- /dev/null +++ b/src/test/test_request.rs @@ -0,0 +1,434 @@ +use std::{borrow::Cow, net::SocketAddr, rc::Rc}; + +use actix_http::{test::TestRequest as HttpTestRequest, Request}; +use serde::Serialize; + +use crate::{ + app_service::AppInitServiceState, + config::AppConfig, + data::Data, + dev::{Extensions, Path, Payload, ResourceDef, Service, Url}, + http::header::ContentType, + http::{header::TryIntoHeaderPair, Method, Uri, Version}, + rmap::ResourceMap, + service::{ServiceRequest, ServiceResponse}, + test, + web::Bytes, + HttpRequest, HttpResponse, +}; + +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, CookieJar}; + +/// Test `Request` builder. +/// +/// 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. +/// +/// ``` +/// use actix_web::{test, HttpRequest, HttpResponse, HttpMessage}; +/// use actix_web::http::{header, StatusCode}; +/// +/// async fn index(req: HttpRequest) -> HttpResponse { +/// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { +/// HttpResponse::Ok().into() +/// } else { +/// HttpResponse::BadRequest().into() +/// } +/// } +/// +/// #[actix_web::test] +/// async fn test_index() { +/// let req = test::TestRequest::default().insert_header("content-type", "text/plain") +/// .to_http_request(); +/// +/// let resp = index(req).await.unwrap(); +/// assert_eq!(resp.status(), StatusCode::OK); +/// +/// let req = test::TestRequest::default().to_http_request(); +/// let resp = index(req).await.unwrap(); +/// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); +/// } +/// ``` +pub struct TestRequest { + req: HttpTestRequest, + rmap: ResourceMap, + config: AppConfig, + path: Path, + peer_addr: Option, + app_data: Extensions, + #[cfg(feature = "cookies")] + cookies: CookieJar, +} + +impl Default for TestRequest { + fn default() -> TestRequest { + TestRequest { + req: HttpTestRequest::default(), + rmap: ResourceMap::new(ResourceDef::new("")), + config: AppConfig::default(), + path: Path::new(Url::new(Uri::default())), + peer_addr: None, + app_data: Extensions::new(), + #[cfg(feature = "cookies")] + cookies: CookieJar::new(), + } + } +} + +#[allow(clippy::wrong_self_convention)] +impl TestRequest { + /// Create TestRequest and set request uri + pub fn with_uri(path: &str) -> TestRequest { + TestRequest::default().uri(path) + } + + /// Create TestRequest and set method to `Method::GET` + pub fn get() -> TestRequest { + TestRequest::default().method(Method::GET) + } + + /// Create TestRequest and set method to `Method::POST` + pub fn post() -> TestRequest { + TestRequest::default().method(Method::POST) + } + + /// Create TestRequest and set method to `Method::PUT` + pub fn put() -> TestRequest { + TestRequest::default().method(Method::PUT) + } + + /// Create TestRequest and set method to `Method::PATCH` + pub fn patch() -> TestRequest { + TestRequest::default().method(Method::PATCH) + } + + /// Create TestRequest and set method to `Method::DELETE` + pub fn delete() -> TestRequest { + TestRequest::default().method(Method::DELETE) + } + + /// Set HTTP version of this request + pub fn version(mut self, ver: Version) -> Self { + self.req.version(ver); + self + } + + /// Set HTTP method of this request + pub fn method(mut self, meth: Method) -> Self { + self.req.method(meth); + self + } + + /// Set HTTP URI of this request + pub fn uri(mut self, path: &str) -> Self { + self.req.uri(path); + self + } + + /// Insert a header, replacing any that were set with an equivalent field name. + pub fn insert_header(mut self, header: impl TryIntoHeaderPair) -> Self { + self.req.insert_header(header); + self + } + + /// Append a header, keeping any that were set with an equivalent field name. + pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self { + self.req.append_header(header); + self + } + + /// Set cookie for this request. + #[cfg(feature = "cookies")] + pub fn cookie(mut self, cookie: Cookie<'_>) -> Self { + self.cookies.add(cookie.into_owned()); + self + } + + /// Set request path pattern parameter. + /// + /// # Examples + /// ``` + /// use actix_web::test::TestRequest; + /// + /// let req = TestRequest::default().param("foo", "bar"); + /// let req = TestRequest::default().param("foo".to_owned(), "bar".to_owned()); + /// ``` + pub fn param( + mut self, + name: impl Into>, + value: impl Into>, + ) -> Self { + self.path.add_static(name, value); + self + } + + /// Set peer addr. + pub fn peer_addr(mut self, addr: SocketAddr) -> Self { + self.peer_addr = Some(addr); + self + } + + /// Set request payload. + pub fn set_payload(mut self, data: impl Into) -> Self { + self.req.set_payload(data); + self + } + + /// Serialize `data` to a URL encoded form and set it as the request payload. + /// + /// The `Content-Type` header is set to `application/x-www-form-urlencoded`. + pub fn set_form(mut self, data: impl Serialize) -> Self { + let bytes = serde_urlencoded::to_string(&data) + .expect("Failed to serialize test data as a urlencoded form"); + self.req.set_payload(bytes); + self.req.insert_header(ContentType::form_url_encoded()); + self + } + + /// Serialize `data` to JSON and set it as the request payload. + /// + /// The `Content-Type` header is set to `application/json`. + pub fn set_json(mut self, data: impl Serialize) -> Self { + let bytes = + serde_json::to_string(&data).expect("Failed to serialize test data to json"); + self.req.set_payload(bytes); + self.req.insert_header(ContentType::json()); + self + } + + /// Set application data. This is equivalent of `App::data()` method + /// for testing purpose. + pub fn data(mut self, data: T) -> Self { + self.app_data.insert(Data::new(data)); + self + } + + /// Set application data. This is equivalent of `App::app_data()` method + /// for testing purpose. + pub fn app_data(mut self, data: T) -> Self { + self.app_data.insert(data); + self + } + + #[cfg(test)] + /// Set request config + pub(crate) fn rmap(mut self, rmap: ResourceMap) -> Self { + self.rmap = rmap; + self + } + + fn finish(&mut self) -> Request { + // mut used when cookie feature is enabled + #[allow(unused_mut)] + let mut req = self.req.finish(); + + #[cfg(feature = "cookies")] + { + use actix_http::header::{HeaderValue, COOKIE}; + + let cookie: String = self + .cookies + .delta() + // ensure only name=value is written to cookie header + .map(|c| c.stripped().encoded().to_string()) + .collect::>() + .join("; "); + + if !cookie.is_empty() { + req.headers_mut() + .insert(COOKIE, HeaderValue::from_str(&cookie).unwrap()); + } + } + + req + } + + /// Complete request creation and generate `Request` instance + pub fn to_request(mut self) -> Request { + let mut req = self.finish(); + req.head_mut().peer_addr = self.peer_addr; + req + } + + /// Complete request creation and generate `ServiceRequest` instance + pub fn to_srv_request(mut self) -> ServiceRequest { + let (mut head, payload) = self.finish().into_parts(); + head.peer_addr = self.peer_addr; + self.path.get_mut().update(&head.uri); + + let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); + + ServiceRequest::new( + HttpRequest::new( + self.path, + head, + app_state, + Rc::new(self.app_data), + None, + Default::default(), + ), + payload, + ) + } + + /// Complete request creation and generate `ServiceResponse` instance + pub fn to_srv_response(self, res: HttpResponse) -> ServiceResponse { + self.to_srv_request().into_response(res) + } + + /// Complete request creation and generate `HttpRequest` instance + pub fn to_http_request(mut self) -> HttpRequest { + let (mut head, _) = self.finish().into_parts(); + head.peer_addr = self.peer_addr; + self.path.get_mut().update(&head.uri); + + let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); + + HttpRequest::new( + self.path, + head, + app_state, + Rc::new(self.app_data), + None, + Default::default(), + ) + } + + /// Complete request creation and generate `HttpRequest` and `Payload` instances + pub fn to_http_parts(mut self) -> (HttpRequest, Payload) { + let (mut head, payload) = self.finish().into_parts(); + head.peer_addr = self.peer_addr; + self.path.get_mut().update(&head.uri); + + let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); + + let req = HttpRequest::new( + self.path, + head, + app_state, + Rc::new(self.app_data), + None, + Default::default(), + ); + + (req, payload) + } + + /// Complete request creation, calls service and waits for response future completion. + pub async fn send_request(self, app: &S) -> S::Response + where + S: Service, Error = E>, + E: std::fmt::Debug, + { + let req = self.to_request(); + test::call_service(app, req).await + } + + #[cfg(test)] + pub fn set_server_hostname(&mut self, host: &str) { + self.config.set_host(host) + } +} + +#[cfg(test)] +mod tests { + use std::time::SystemTime; + + use super::*; + use crate::{http::header, test::init_service, web, App, Error, HttpResponse, Responder}; + + #[actix_rt::test] + async fn test_basics() { + let req = TestRequest::default() + .version(Version::HTTP_2) + .insert_header(header::ContentType::json()) + .insert_header(header::Date(SystemTime::now().into())) + .param("test", "123") + .data(10u32) + .app_data(20u64) + .peer_addr("127.0.0.1:8081".parse().unwrap()) + .to_http_request(); + assert!(req.headers().contains_key(header::CONTENT_TYPE)); + assert!(req.headers().contains_key(header::DATE)); + assert_eq!( + req.head().peer_addr, + Some("127.0.0.1:8081".parse().unwrap()) + ); + assert_eq!(&req.match_info()["test"], "123"); + assert_eq!(req.version(), Version::HTTP_2); + let data = req.app_data::>().unwrap(); + assert!(req.app_data::>().is_none()); + assert_eq!(*data.get_ref(), 10); + + assert!(req.app_data::().is_none()); + let data = req.app_data::().unwrap(); + assert_eq!(*data, 20); + } + + #[actix_rt::test] + async fn test_send_request() { + let app = init_service( + App::new().service( + web::resource("/index.html") + .route(web::get().to(|| HttpResponse::Ok().body("welcome!"))), + ), + ) + .await; + + let resp = TestRequest::get() + .uri("/index.html") + .send_request(&app) + .await; + + let result = test::read_body(resp).await; + assert_eq!(result, Bytes::from_static(b"welcome!")); + } + + #[actix_rt::test] + async fn test_async_with_block() { + async fn async_with_block() -> Result { + let res = web::block(move || Some(4usize).ok_or("wrong")).await; + + match res { + Ok(value) => Ok(HttpResponse::Ok() + .content_type("text/plain") + .body(format!("Async with block value: {:?}", value))), + Err(_) => panic!("Unexpected"), + } + } + + let app = + init_service(App::new().service(web::resource("/index.html").to(async_with_block))) + .await; + + let req = TestRequest::post().uri("/index.html").to_request(); + let res = app.call(req).await.unwrap(); + assert!(res.status().is_success()); + } + + // allow deprecated App::data + #[allow(deprecated)] + #[actix_rt::test] + async fn test_server_data() { + async fn handler(data: web::Data) -> impl Responder { + assert_eq!(**data, 10); + HttpResponse::Ok() + } + + let app = init_service( + App::new() + .data(10usize) + .service(web::resource("/index.html").to(handler)), + ) + .await; + + let req = TestRequest::post().uri("/index.html").to_request(); + let res = app.call(req).await.unwrap(); + assert!(res.status().is_success()); + } +} diff --git a/src/test/test_services.rs b/src/test/test_services.rs new file mode 100644 index 000000000..b4810cfd8 --- /dev/null +++ b/src/test/test_services.rs @@ -0,0 +1,31 @@ +use actix_utils::future::ok; + +use crate::{ + body::BoxBody, + dev::{fn_service, Service, ServiceRequest, ServiceResponse}, + http::StatusCode, + Error, HttpResponseBuilder, +}; + +/// Creates service that always responds with `200 OK` and no body. +pub fn ok_service( +) -> impl Service, Error = Error> { + simple_service(StatusCode::OK) +} + +/// Creates service that always responds with given status code and no body. +pub fn simple_service( + status_code: StatusCode, +) -> impl Service, Error = Error> { + fn_service(move |req: ServiceRequest| { + ok(req.into_response(HttpResponseBuilder::new(status_code).finish())) + }) +} + +#[doc(hidden)] +#[deprecated(since = "4.0.0", note = "Renamed to `simple_service`.")] +pub fn default_service( + status_code: StatusCode, +) -> impl Service, Error = Error> { + simple_service(status_code) +} diff --git a/src/test/test_utils.rs b/src/test/test_utils.rs new file mode 100644 index 000000000..02d4c9bf3 --- /dev/null +++ b/src/test/test_utils.rs @@ -0,0 +1,474 @@ +use std::fmt; + +use actix_http::Request; +use actix_service::IntoServiceFactory; +use serde::de::DeserializeOwned; + +use crate::{ + body::{self, MessageBody}, + config::AppConfig, + dev::{Service, ServiceFactory}, + service::ServiceResponse, + web::Bytes, + Error, +}; + +/// Initialize service from application builder instance. +/// +/// # Examples +/// ``` +/// use actix_service::Service; +/// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; +/// +/// #[actix_web::test] +/// async fn test_init_service() { +/// let app = test::init_service( +/// App::new() +/// .service(web::resource("/test").to(|| async { "OK" })) +/// ).await; +/// +/// // Create request object +/// let req = test::TestRequest::with_uri("/test").to_request(); +/// +/// // Execute application +/// let res = app.call(req).await.unwrap(); +/// assert_eq!(res.status(), StatusCode::OK); +/// } +/// ``` +/// +/// # Panics +/// Panics if service initialization returns an error. +pub async fn init_service( + app: R, +) -> impl Service, Error = E> +where + R: IntoServiceFactory, + S: ServiceFactory, Error = E>, + S::InitError: std::fmt::Debug, +{ + try_init_service(app) + .await + .expect("service initialization failed") +} + +/// Fallible version of [`init_service`] that allows testing initialization errors. +pub(crate) async fn try_init_service( + app: R, +) -> Result, Error = E>, S::InitError> +where + R: IntoServiceFactory, + S: ServiceFactory, Error = E>, + S::InitError: std::fmt::Debug, +{ + let srv = app.into_factory(); + srv.new_service(AppConfig::default()).await +} + +/// Calls service and waits for response future completion. +/// +/// # Examples +/// ``` +/// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; +/// +/// #[actix_web::test] +/// async fn test_response() { +/// let app = test::init_service( +/// App::new() +/// .service(web::resource("/test").to(|| async { +/// HttpResponse::Ok() +/// })) +/// ).await; +/// +/// // Create request object +/// let req = test::TestRequest::with_uri("/test").to_request(); +/// +/// // Call application +/// let res = test::call_service(&app, req).await; +/// assert_eq!(res.status(), StatusCode::OK); +/// } +/// ``` +/// +/// # Panics +/// Panics if service call returns error. +pub async fn call_service(app: &S, req: R) -> S::Response +where + S: Service, Error = E>, + E: std::fmt::Debug, +{ + app.call(req) + .await + .expect("test service call returned error") +} + +/// Helper function that returns a response body of a TestRequest +/// +/// # Examples +/// ``` +/// use actix_web::{test, web, App, HttpResponse, http::header}; +/// use bytes::Bytes; +/// +/// #[actix_web::test] +/// async fn test_index() { +/// let app = test::init_service( +/// App::new().service( +/// web::resource("/index.html") +/// .route(web::post().to(|| async { +/// HttpResponse::Ok().body("welcome!") +/// }))) +/// ).await; +/// +/// let req = test::TestRequest::post() +/// .uri("/index.html") +/// .header(header::CONTENT_TYPE, "application/json") +/// .to_request(); +/// +/// let result = test::call_and_read_body(&app, req).await; +/// assert_eq!(result, Bytes::from_static(b"welcome!")); +/// } +/// ``` +/// +/// # Panics +/// Panics if: +/// - service call returns error; +/// - body yields an error while it is being read. +pub async fn call_and_read_body(app: &S, req: Request) -> Bytes +where + S: Service, Error = Error>, + B: MessageBody, + B::Error: fmt::Debug, +{ + let res = call_service(app, req).await; + read_body(res).await +} + +#[doc(hidden)] +#[deprecated(since = "4.0.0", note = "Renamed to `call_and_read_body`.")] +pub async fn read_response(app: &S, req: Request) -> Bytes +where + S: Service, Error = Error>, + B: MessageBody, + B::Error: fmt::Debug, +{ + let res = call_service(app, req).await; + read_body(res).await +} + +/// Helper function that returns a response body of a ServiceResponse. +/// +/// # Examples +/// ``` +/// use actix_web::{test, web, App, HttpResponse, http::header}; +/// use bytes::Bytes; +/// +/// #[actix_web::test] +/// async fn test_index() { +/// let app = test::init_service( +/// App::new().service( +/// web::resource("/index.html") +/// .route(web::post().to(|| async { +/// HttpResponse::Ok().body("welcome!") +/// }))) +/// ).await; +/// +/// let req = test::TestRequest::post() +/// .uri("/index.html") +/// .header(header::CONTENT_TYPE, "application/json") +/// .to_request(); +/// +/// let res = test::call_service(&app, req).await; +/// let result = test::read_body(res).await; +/// assert_eq!(result, Bytes::from_static(b"welcome!")); +/// } +/// ``` +/// +/// # Panics +/// Panics if body yields an error while it is being read. +pub async fn read_body(res: ServiceResponse) -> Bytes +where + B: MessageBody, + B::Error: fmt::Debug, +{ + let body = res.into_body(); + body::to_bytes(body) + .await + .expect("error reading test response body") +} + +/// Helper function that returns a deserialized response body of a ServiceResponse. +/// +/// # Examples +/// ``` +/// use actix_web::{App, test, web, HttpResponse, http::header}; +/// use serde::{Serialize, Deserialize}; +/// +/// #[derive(Serialize, Deserialize)] +/// pub struct Person { +/// id: String, +/// name: String, +/// } +/// +/// #[actix_web::test] +/// async fn test_post_person() { +/// let app = test::init_service( +/// App::new().service( +/// web::resource("/people") +/// .route(web::post().to(|person: web::Json| async { +/// HttpResponse::Ok() +/// .json(person)}) +/// )) +/// ).await; +/// +/// let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); +/// +/// let res = test::TestRequest::post() +/// .uri("/people") +/// .header(header::CONTENT_TYPE, "application/json") +/// .set_payload(payload) +/// .send_request(&mut app) +/// .await; +/// +/// assert!(res.status().is_success()); +/// +/// let result: Person = test::read_body_json(res).await; +/// } +/// ``` +/// +/// # Panics +/// Panics if: +/// - body yields an error while it is being read; +/// - received body is not a valid JSON representation of `T`. +pub async fn read_body_json(res: ServiceResponse) -> T +where + B: MessageBody, + B::Error: fmt::Debug, + T: DeserializeOwned, +{ + let body = read_body(res).await; + + serde_json::from_slice(&body).unwrap_or_else(|err| { + panic!( + "could not deserialize body into a {}\nerr: {}\nbody: {:?}", + std::any::type_name::(), + err, + body, + ) + }) +} + +/// Helper function that returns a deserialized response body of a TestRequest +/// +/// # Examples +/// ``` +/// use actix_web::{App, test, web, HttpResponse, http::header}; +/// use serde::{Serialize, Deserialize}; +/// +/// #[derive(Serialize, Deserialize)] +/// pub struct Person { +/// id: String, +/// name: String +/// } +/// +/// #[actix_web::test] +/// async fn test_add_person() { +/// let app = test::init_service( +/// App::new().service( +/// web::resource("/people") +/// .route(web::post().to(|person: web::Json| async { +/// HttpResponse::Ok() +/// .json(person)}) +/// )) +/// ).await; +/// +/// let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); +/// +/// let req = test::TestRequest::post() +/// .uri("/people") +/// .header(header::CONTENT_TYPE, "application/json") +/// .set_payload(payload) +/// .to_request(); +/// +/// let result: Person = test::call_and_read_body_json(&mut app, req).await; +/// } +/// ``` +/// +/// # Panics +/// Panics if: +/// - service call returns an error body yields an error while it is being read; +/// - body yields an error while it is being read; +/// - received body is not a valid JSON representation of `T`. +pub async fn call_and_read_body_json(app: &S, req: Request) -> T +where + S: Service, Error = Error>, + B: MessageBody, + B::Error: fmt::Debug, + T: DeserializeOwned, +{ + let res = call_service(app, req).await; + read_body_json(res).await +} + +#[doc(hidden)] +#[deprecated(since = "4.0.0", note = "Renamed to `call_and_read_body_json`.")] +pub async fn read_response_json(app: &S, req: Request) -> T +where + S: Service, Error = Error>, + B: MessageBody, + B::Error: fmt::Debug, + T: DeserializeOwned, +{ + call_and_read_body_json(app, req).await +} + +#[cfg(test)] +mod tests { + + use serde::{Deserialize, Serialize}; + + use super::*; + use crate::{http::header, test::TestRequest, web, App, HttpMessage, HttpResponse}; + + #[actix_rt::test] + async fn test_request_methods() { + let app = init_service( + App::new().service( + web::resource("/index.html") + .route(web::put().to(|| HttpResponse::Ok().body("put!"))) + .route(web::patch().to(|| HttpResponse::Ok().body("patch!"))) + .route(web::delete().to(|| HttpResponse::Ok().body("delete!"))), + ), + ) + .await; + + let put_req = TestRequest::put() + .uri("/index.html") + .insert_header((header::CONTENT_TYPE, "application/json")) + .to_request(); + + let result = call_and_read_body(&app, put_req).await; + assert_eq!(result, Bytes::from_static(b"put!")); + + let patch_req = TestRequest::patch() + .uri("/index.html") + .insert_header((header::CONTENT_TYPE, "application/json")) + .to_request(); + + let result = call_and_read_body(&app, patch_req).await; + assert_eq!(result, Bytes::from_static(b"patch!")); + + let delete_req = TestRequest::delete().uri("/index.html").to_request(); + let result = call_and_read_body(&app, delete_req).await; + assert_eq!(result, Bytes::from_static(b"delete!")); + } + + #[derive(Serialize, Deserialize)] + pub struct Person { + id: String, + name: String, + } + + #[actix_rt::test] + async fn test_response_json() { + let app = init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), + ))) + .await; + + let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); + + let req = TestRequest::post() + .uri("/people") + .insert_header((header::CONTENT_TYPE, "application/json")) + .set_payload(payload) + .to_request(); + + let result: Person = call_and_read_body_json(&app, req).await; + assert_eq!(&result.id, "12345"); + } + + #[actix_rt::test] + async fn test_body_json() { + let app = init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), + ))) + .await; + + let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); + + let res = TestRequest::post() + .uri("/people") + .insert_header((header::CONTENT_TYPE, "application/json")) + .set_payload(payload) + .send_request(&app) + .await; + + let result: Person = read_body_json(res).await; + assert_eq!(&result.name, "User name"); + } + + #[actix_rt::test] + async fn test_request_response_form() { + let app = init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Form| HttpResponse::Ok().json(person)), + ))) + .await; + + let payload = Person { + id: "12345".to_string(), + name: "User name".to_string(), + }; + + let req = TestRequest::post() + .uri("/people") + .set_form(&payload) + .to_request(); + + assert_eq!(req.content_type(), "application/x-www-form-urlencoded"); + + let result: Person = call_and_read_body_json(&app, req).await; + assert_eq!(&result.id, "12345"); + assert_eq!(&result.name, "User name"); + } + + #[actix_rt::test] + async fn test_response() { + let app = init_service( + App::new().service( + web::resource("/index.html") + .route(web::post().to(|| HttpResponse::Ok().body("welcome!"))), + ), + ) + .await; + + let req = TestRequest::post() + .uri("/index.html") + .insert_header((header::CONTENT_TYPE, "application/json")) + .to_request(); + + let result = call_and_read_body(&app, req).await; + assert_eq!(result, Bytes::from_static(b"welcome!")); + } + + #[actix_rt::test] + async fn test_request_response_json() { + let app = init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), + ))) + .await; + + let payload = Person { + id: "12345".to_string(), + name: "User name".to_string(), + }; + + let req = TestRequest::post() + .uri("/people") + .set_json(&payload) + .to_request(); + + assert_eq!(req.content_type(), "application/json"); + + let result: Person = call_and_read_body_json(&app, req).await; + assert_eq!(&result.id, "12345"); + assert_eq!(&result.name, "User name"); + } +} diff --git a/src/types/either.rs b/src/types/either.rs index 3c759736e..0eafb9e43 100644 --- a/src/types/either.rs +++ b/src/types/either.rs @@ -12,15 +12,14 @@ use futures_core::ready; use pin_project_lite::pin_project; use crate::{ - body, dev, + body::EitherBody, + dev, web::{Form, Json}, Error, FromRequest, HttpRequest, HttpResponse, Responder, }; /// Combines two extractor or responder types into a single type. /// -/// Can be converted to and from an [`either::Either`]. -/// /// # Extractor /// Provides a mechanism for trying two extractors, a primary and a fallback. Useful for /// "polymorphic payloads" where, for example, a form might be JSON or URL encoded. @@ -101,24 +100,6 @@ impl Either, Form> { } } -impl From> for Either { - fn from(val: either::Either) -> Self { - match val { - either::Either::Left(l) => Either::Left(l), - either::Either::Right(r) => Either::Right(r), - } - } -} - -impl From> for either::Either { - fn from(val: Either) -> Self { - match val { - Either::Left(l) => either::Either::Left(l), - Either::Right(r) => either::Either::Right(r), - } - } -} - #[cfg(test)] impl Either { pub(self) fn unwrap_left(self) -> L { @@ -146,7 +127,7 @@ where L: Responder, R: Responder, { - type Body = body::EitherBody; + type Body = EitherBody; fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self { @@ -165,7 +146,7 @@ pub enum EitherExtractError { /// Error from payload buffering, such as exceeding payload max size limit. Bytes(Error), - /// Error from primary extractor. + /// Error from primary and fallback extractors. Extract(L, R), } diff --git a/src/types/json.rs b/src/types/json.rs index 2b4d220e2..be6078b2b 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -449,12 +449,13 @@ mod tests { use super::*; use crate::{ + body, error::InternalError, http::{ header::{self, CONTENT_LENGTH, CONTENT_TYPE}, StatusCode, }, - test::{assert_body_eq, load_body, TestRequest}, + test::{assert_body_eq, TestRequest}, }; #[derive(Serialize, Deserialize, PartialEq, Debug)] @@ -517,7 +518,7 @@ mod tests { let resp = HttpResponse::from_error(s.err().unwrap()); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let body = load_body(resp.into_body()).await.unwrap(); + let body = body::to_bytes(resp.into_body()).await.unwrap(); let msg: MyObject = serde_json::from_slice(&body).unwrap(); assert_eq!(msg.name, "invalid request"); } diff --git a/src/web.rs b/src/web.rs index 16dbace60..47bff36a3 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,14 +1,14 @@ //! Essentials helper functions and types for application registration. -use std::{error::Error as StdError, future::Future}; +use std::future::Future; use actix_http::Method; use actix_router::IntoPatterns; pub use bytes::{Buf, BufMut, Bytes, BytesMut}; use crate::{ - body::MessageBody, error::BlockingError, extract::FromRequest, handler::Handler, - resource::Resource, responder::Responder, route::Route, scope::Scope, service::WebService, + error::BlockingError, extract::FromRequest, handler::Handler, resource::Resource, + route::Route, scope::Scope, service::WebService, Responder, }; pub use crate::config::ServiceConfig; @@ -52,11 +52,16 @@ pub fn resource(path: T) -> Resource { /// Scopes collect multiple paths under a common path prefix. The scope's path can contain dynamic /// path segments. /// +/// # Avoid Trailing Slashes +/// Avoid using trailing slashes in the scope prefix (e.g., `web::scope("/scope/")`). It will almost +/// certainly not have the expected behavior. See the [documentation on resource definitions][pat] +/// to understand why this is the case and how to correctly construct scope/prefix definitions. +/// /// # Examples /// In this example, three routes are set up (and will handle any method): -/// * `/{project_id}/path1` -/// * `/{project_id}/path2` -/// * `/{project_id}/path3` +/// - `/{project_id}/path1` +/// - `/{project_id}/path2` +/// - `/{project_id}/path3` /// /// ``` /// use actix_web::{web, App, HttpResponse}; @@ -68,6 +73,8 @@ pub fn resource(path: T) -> Resource { /// .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed())) /// ); /// ``` +/// +/// [pat]: crate::dev::ResourceDef#prefix-resources pub fn scope(path: &str) -> Scope { Scope::new(path) } @@ -139,14 +146,11 @@ pub fn method(method: Method) -> Route { /// web::to(index)) /// ); /// ``` -pub fn to(handler: F) -> Route +pub fn to(handler: F) -> Route where - F: Handler, - I: FromRequest + 'static, - R: Future + 'static, - R::Output: Responder + 'static, - ::Body: MessageBody + 'static, - <::Body as MessageBody>::Error: Into>, + F: Handler, + Args: FromRequest + 'static, + F::Output: Responder + 'static, { Route::new().to(handler) } diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs index 887b51d41..464a650a2 100644 --- a/tests/test_httpserver.rs +++ b/tests/test_httpserver.rs @@ -121,7 +121,7 @@ async fn test_start_ssl() { let client = awc::Client::builder() .connector( awc::Connector::new() - .ssl(builder.build()) + .openssl(builder.build()) .timeout(Duration::from_millis(100)), ) .finish(); diff --git a/tests/test_server.rs b/tests/test_server.rs index 51a78eb28..9b7ef6e1b 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -10,8 +10,13 @@ use std::{ task::{Context, Poll}, }; -use actix_http::header::{ - ContentEncoding, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING, +use actix_web::{ + dev::BodyEncoding, + http::header::{ + ContentEncoding, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING, + }, + middleware::{Compress, NormalizePath, TrailingSlash}, + web, App, Error, HttpResponse, }; use brotli2::write::{BrotliDecoder, BrotliEncoder}; use bytes::Bytes; @@ -31,10 +36,6 @@ use openssl::{ use rand::{distributions::Alphanumeric, Rng}; use zstd::stream::{read::Decoder as ZstdDecoder, write::Encoder as ZstdEncoder}; -use actix_web::dev::BodyEncoding; -use actix_web::middleware::{Compress, NormalizePath, TrailingSlash}; -use actix_web::{web, App, Error, HttpResponse}; - const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \