diff --git a/.github/workflows/ci-post-merge.yml b/.github/workflows/ci-post-merge.yml index d8752d8aa..1ee97b591 100644 --- a/.github/workflows/ci-post-merge.yml +++ b/.github/workflows/ci-post-merge.yml @@ -45,18 +45,15 @@ jobs: profile: minimal override: true + - name: Install cargo-hack + uses: taiki-e/install-action@cargo-hack + - name: Generate Cargo.lock uses: actions-rs/cargo@v1 with: { command: generate-lockfile } - name: Cache Dependencies uses: Swatinem/rust-cache@v1.2.0 - - name: Install cargo-hack - uses: actions-rs/cargo@v1 - with: - command: install - args: cargo-hack - - name: check minimal uses: actions-rs/cargo@v1 with: { command: ci-check-min } @@ -81,7 +78,7 @@ jobs: - name: Clear the cargo caches run: | - cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean + cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean cargo-cache ci_feature_powerset_check: @@ -102,18 +99,15 @@ jobs: profile: minimal override: true + - name: Install cargo-hack + uses: taiki-e/install-action@cargo-hack + - name: Generate Cargo.lock uses: actions-rs/cargo@v1 with: { command: generate-lockfile } - name: Cache Dependencies uses: Swatinem/rust-cache@v1.2.0 - - name: Install cargo-hack - uses: actions-rs/cargo@v1 - with: - command: install - args: cargo-hack - - name: check feature combinations uses: actions-rs/cargo@v1 with: { command: ci-check-all-feature-powerset } @@ -140,18 +134,15 @@ jobs: profile: minimal override: true + - name: Install nextest + uses: taiki-e/install-action@nextest + - name: Generate Cargo.lock uses: actions-rs/cargo@v1 with: { command: generate-lockfile } - name: Cache Dependencies uses: Swatinem/rust-cache@v1.3.0 - - name: Install cargo-nextest - uses: actions-rs/cargo@v1 - with: - command: install - args: cargo-nextest - - name: Test with cargo-nextest uses: actions-rs/cargo@v1 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49ad25ccf..2ea920808 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,18 +47,22 @@ jobs: profile: minimal override: true + - name: Install cargo-hack + uses: taiki-e/install-action@cargo-hack + + - name: workaround MSRV issues + if: matrix.version != 'stable' + run: | + cargo install cargo-edit --version=0.8.0 + cargo add const-str@0.3 --dev -p=actix-web + cargo add const-str@0.3 --dev -p=awc + - name: Generate Cargo.lock uses: actions-rs/cargo@v1 with: { command: generate-lockfile } - name: Cache Dependencies uses: Swatinem/rust-cache@v1.2.0 - - name: Install cargo-hack - uses: actions-rs/cargo@v1 - with: - command: install - args: cargo-hack - - name: check minimal uses: actions-rs/cargo@v1 with: { command: ci-check-min } @@ -83,7 +87,7 @@ jobs: - name: Clear the cargo caches run: | - cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean + cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean cargo-cache io-uring: diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index b127cd9ea..5c0a48024 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,8 +1,14 @@ # Changes ## Unreleased - 2022-xx-xx + + +## 0.6.2 - 2022-07-23 +- Allow partial range responses for video content to start streaming sooner. [#2817] - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. +[#2817]: https://github.com/actix/actix-web/pull/2817 + ## 0.6.1 - 2022-06-11 - Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021] diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 02543095f..30356d81a 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.1" +version = "0.6.2" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -46,6 +46,6 @@ actix-server = { version = "2.1", optional = true } # ensure matching tokio-urin [dev-dependencies] actix-rt = "2.7" -actix-test = "0.1.0-beta.13" +actix-test = "0.1" actix-web = "4" tempfile = "3.2" diff --git a/actix-files/README.md b/actix-files/README.md index 35db41c9a..c3204a68c 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.1)](https://docs.rs/actix-files/0.6.1) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.2)](https://docs.rs/actix-files/0.6.2) ![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.1/status.svg)](https://deps.rs/crate/actix-files/0.6.1) +[![dependency status](https://deps.rs/crate/actix-files/0.6.2/status.svg)](https://deps.rs/crate/actix-files/0.6.2) [![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/error.rs b/actix-files/src/error.rs index 6682529f8..2f3a36cd1 100644 --- a/actix-files/src/error.rs +++ b/actix-files/src/error.rs @@ -2,7 +2,7 @@ use actix_web::{http::StatusCode, ResponseError}; use derive_more::Display; /// Errors which can occur when serving static files. -#[derive(Display, Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq, Display)] pub enum FilesError { /// Path is not a directory #[allow(dead_code)] @@ -22,7 +22,7 @@ impl ResponseError for FilesError { } #[allow(clippy::enum_variant_names)] -#[derive(Display, Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq, Display)] #[non_exhaustive] pub enum UriSegmentError { /// The segment started with the wrapped invalid character. diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 5580e6f7e..1213534c2 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -528,11 +528,26 @@ impl NamedFile { length = ranges[0].length; offset = ranges[0].start; - // don't allow compression middleware to modify partial content - res.insert_header(( - header::CONTENT_ENCODING, - HeaderValue::from_static("identity"), - )); + // When a Content-Encoding header is present in a 206 partial content response + // for video content, it prevents browser video players from starting playback + // before loading the whole video and also prevents seeking. + // + // See: https://github.com/actix/actix-web/issues/2815 + // + // The assumption of this fix is that the video player knows to not send an + // Accept-Encoding header for this request and that downstream middleware will + // not attempt compression for requests without it. + // + // TODO: Solve question around what to do if self.encoding is set and partial + // range is requested. Reject request? Ignoring self.encoding seems wrong, too. + // In practice, it should not come up. + if req.headers().contains_key(&header::ACCEPT_ENCODING) { + // don't allow compression middleware to modify partial content + res.insert_header(( + header::CONTENT_ENCODING, + HeaderValue::from_static("identity"), + )); + } res.insert_header(( header::CONTENT_RANGE, diff --git a/actix-files/tests/encoding.rs b/actix-files/tests/encoding.rs index 080292af5..7aec25ff9 100644 --- a/actix-files/tests/encoding.rs +++ b/actix-files/tests/encoding.rs @@ -1,11 +1,11 @@ -use actix_files::Files; +use actix_files::{Files, NamedFile}; use actix_web::{ http::{ header::{self, HeaderValue}, StatusCode, }, test::{self, TestRequest}, - App, + web, App, }; #[actix_web::test] @@ -36,3 +36,31 @@ async fn test_utf8_file_contents() { Some(&HeaderValue::from_static("text/plain")), ); } + +#[actix_web::test] +async fn partial_range_response_encoding() { + let srv = test::init_service(App::new().default_service(web::to(|| async { + NamedFile::open_async("./tests/test.binary").await.unwrap() + }))) + .await; + + // range request without accept-encoding returns no content-encoding header + let req = TestRequest::with_uri("/") + .append_header((header::RANGE, "bytes=10-20")) + .to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::PARTIAL_CONTENT); + assert!(!res.headers().contains_key(header::CONTENT_ENCODING)); + + // range request with accept-encoding returns a content-encoding header + let req = TestRequest::with_uri("/") + .append_header((header::RANGE, "bytes=10-20")) + .append_header((header::ACCEPT_ENCODING, "identity")) + .to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::PARTIAL_CONTENT); + assert_eq!( + res.headers().get(header::CONTENT_ENCODING).unwrap(), + "identity" + ); +} diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index f91ef4081..9aad2e4ba 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -1,9 +1,24 @@ # Changes ## Unreleased - 2022-xx-xx -- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. +## 3.0.0 - 2022-07-24 +- `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442] +- Added `TestServer::client_headers` method. [#2097] +- Update `actix-server` dependency to `2`. +- Update `actix-tls` dependency to `3`. +- Update `bytes` to `1.0`. [#1813] +- Minimum supported Rust version (MSRV) is now 1.57. + +[#2442]: https://github.com/actix/actix-web/pull/2442 +[#2097]: https://github.com/actix/actix-web/pull/2097 +[#1813]: https://github.com/actix/actix-web/pull/1813 + + +
+3.0.0 Pre-Releases + ## 3.0.0-beta.13 - 2022-02-16 - No significant changes since `3.0.0-beta.12`. @@ -69,6 +84,7 @@ [#1813]: https://github.com/actix/actix-web/pull/1813 +
## 2.1.0 - 2020-11-25 - Add ability to set address for `TestServer`. [#1645] diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 6f7563ffa..0a9ddf947 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.13" +version = "3.0.0" authors = ["Nikolay Kim "] description = "Various helpers for Actix applications to use during testing" keywords = ["http", "web", "framework", "async", "futures"] diff --git a/actix-http-test/README.md b/actix-http-test/README.md index 9429bb760..ec2bd769c 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.13)](https://docs.rs/actix-http-test/3.0.0-beta.13) +[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0)](https://docs.rs/actix-http-test/3.0.0) ![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
-[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.13/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.13) +[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 7e6604046..785a1b13f 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,10 @@ # Changes ## Unreleased - 2022-xx-xx +### Fixed +- Avoid possibility of dispatcher getting stuck while back-pressuring I/O. [#2369] + +[#2369]: https://github.com/actix/actix-web/pull/2369 ## 3.2.1 - 2022-07-02 @@ -29,9 +33,9 @@ ### Fixed - Revert broken fix in [#2624] that caused erroneous 500 error responses. Temporarily re-introduces [#2357] bug. [#2779] +[#2624]: https://github.com/actix/actix-web/pull/2624 [#2357]: https://github.com/actix/actix-web/issues/2357 -[#2624]: https://github.com/actix/actix-web/issues/2624 -[#2779]: https://github.com/actix/actix-web/issues/2779 +[#2779]: https://github.com/actix/actix-web/pull/2779 ## 3.0.4 - 2022-03-09 @@ -43,14 +47,14 @@ ### Fixed - Allow spaces between header name and colon when parsing responses. [#2684] -[#2684]: https://github.com/actix/actix-web/issues/2684 +[#2684]: https://github.com/actix/actix-web/pull/2684 ## 3.0.2 - 2022-03-05 ### Fixed - Fix encoding camel-case header names with more than one hyphen. [#2683] -[#2683]: https://github.com/actix/actix-web/issues/2683 +[#2683]: https://github.com/actix/actix-web/pull/2683 ## 3.0.1 - 2022-03-04 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 03767ca4e..ba8c80e64 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -97,7 +97,7 @@ flate2 = { version = "1.0.13", optional = true } zstd = { version = "0.11", optional = true } [dev-dependencies] -actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] } +actix-http-test = { version = "3", features = ["openssl"] } actix-server = "2" actix-tls = { version = "3", features = ["openssl"] } actix-web = "4" diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index ac95a2802..c0d297a20 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -35,7 +35,7 @@ impl Default for ServiceConfig { } impl ServiceConfig { - /// Create instance of `ServiceConfig` + /// Create instance of `ServiceConfig`. pub fn new( keep_alive: KeepAlive, client_request_timeout: Duration, diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 3fce0a60b..41522a254 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -388,7 +388,7 @@ impl StdError for DispatchError { /// A set of error that can occur during parsing content type. #[derive(Debug, Display, Error)] -#[cfg_attr(test, derive(PartialEq))] +#[cfg_attr(test, derive(PartialEq, Eq))] #[non_exhaustive] pub enum ContentTypeError { /// Can not parse content type diff --git a/actix-http/src/h1/chunked.rs b/actix-http/src/h1/chunked.rs index c1dd4b283..4005ed892 100644 --- a/actix-http/src/h1/chunked.rs +++ b/actix-http/src/h1/chunked.rs @@ -15,7 +15,7 @@ macro_rules! byte ( }) ); -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub(super) enum ChunkedState { Size, SizeLws, diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index edfc00fd6..0b06bfe24 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -440,7 +440,7 @@ impl HeaderIndex { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] /// Chunk type yielded while decoding a payload. pub enum PayloadItem { Chunk(Bytes), @@ -450,7 +450,7 @@ pub enum PayloadItem { /// Decoder that can handle different payload types. /// /// If a message body does not use `Transfer-Encoding`, it should include a `Content-Length`. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct PayloadDecoder { kind: Kind, } @@ -476,7 +476,7 @@ impl PayloadDecoder { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] enum Kind { /// A reader used when a `Content-Length` header is passed with a positive integer. Length(u64), @@ -844,121 +844,98 @@ mod tests { #[test] fn test_conn_default_1_0() { - let mut buf = BytesMut::from("GET /test HTTP/1.0\r\n\r\n"); - let req = parse_ready!(&mut buf); - + let req = parse_ready!(&mut BytesMut::from("GET /test HTTP/1.0\r\n\r\n")); assert_eq!(req.head().connection_type(), ConnectionType::Close); } #[test] fn test_conn_default_1_1() { - let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n"); - let req = parse_ready!(&mut buf); - + let req = parse_ready!(&mut BytesMut::from("GET /test HTTP/1.1\r\n\r\n")); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); } #[test] fn test_conn_close() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ connection: close\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - + )); assert_eq!(req.head().connection_type(), ConnectionType::Close); - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ connection: Close\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - + )); assert_eq!(req.head().connection_type(), ConnectionType::Close); } #[test] fn test_conn_close_1_0() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.0\r\n\ connection: close\r\n\r\n", - ); - - let req = parse_ready!(&mut buf); - + )); assert_eq!(req.head().connection_type(), ConnectionType::Close); } #[test] fn test_conn_keep_alive_1_0() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.0\r\n\ connection: keep-alive\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - + )); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.0\r\n\ connection: Keep-Alive\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - + )); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); } #[test] fn test_conn_keep_alive_1_1() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ connection: keep-alive\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - + )); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); } #[test] fn test_conn_other_1_0() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.0\r\n\ connection: other\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - + )); assert_eq!(req.head().connection_type(), ConnectionType::Close); } #[test] fn test_conn_other_1_1() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ connection: other\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - + )); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); } #[test] fn test_conn_upgrade() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ upgrade: websockets\r\n\ connection: upgrade\r\n\r\n", - ); - let req = parse_ready!(&mut buf); + )); assert!(req.upgrade()); assert_eq!(req.head().connection_type(), ConnectionType::Upgrade); - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ upgrade: Websockets\r\n\ connection: Upgrade\r\n\r\n", - ); - let req = parse_ready!(&mut buf); + )); assert!(req.upgrade()); assert_eq!(req.head().connection_type(), ConnectionType::Upgrade); @@ -966,59 +943,62 @@ mod tests { #[test] fn test_conn_upgrade_connect_method() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "CONNECT /test HTTP/1.1\r\n\ content-type: text/plain\r\n\r\n", - ); - let req = parse_ready!(&mut buf); + )); assert!(req.upgrade()); } #[test] - fn test_headers_content_length_err_1() { - let mut buf = BytesMut::from( + fn test_headers_bad_content_length() { + // string CL + expect_parse_err!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ content-length: line\r\n\r\n", - ); + )); - expect_parse_err!(&mut buf) + // negative CL + expect_parse_err!(&mut BytesMut::from( + "GET /test HTTP/1.1\r\n\ + content-length: -1\r\n\r\n", + )); } #[test] - fn test_headers_content_length_err_2() { + fn octal_ish_cl_parsed_as_decimal() { let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - content-length: -1\r\n\r\n", + "POST /test HTTP/1.1\r\n\ + content-length: 011\r\n\r\n", ); - - expect_parse_err!(&mut buf); + let mut reader = MessageDecoder::::default(); + let (_req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + assert!(matches!( + pl, + PayloadType::Payload(pl) if pl == PayloadDecoder::length(11) + )); } #[test] fn test_invalid_header() { - let mut buf = BytesMut::from( + expect_parse_err!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ test line\r\n\r\n", - ); - - expect_parse_err!(&mut buf); + )); } #[test] fn test_invalid_name() { - let mut buf = BytesMut::from( + expect_parse_err!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ test[]: line\r\n\r\n", - ); - - expect_parse_err!(&mut buf); + )); } #[test] fn test_http_request_bad_status_line() { - let mut buf = BytesMut::from("getpath \r\n\r\n"); - expect_parse_err!(&mut buf); + expect_parse_err!(&mut BytesMut::from("getpath \r\n\r\n")); } #[test] @@ -1058,11 +1038,10 @@ mod tests { #[test] fn test_http_request_parser_utf8() { - let mut buf = BytesMut::from( + let req = parse_ready!(&mut BytesMut::from( "GET /test HTTP/1.1\r\n\ x-test: тест\r\n\r\n", - ); - let req = parse_ready!(&mut buf); + )); assert_eq!( req.headers().get("x-test").unwrap().as_bytes(), @@ -1072,24 +1051,18 @@ mod tests { #[test] fn test_http_request_parser_two_slashes() { - let mut buf = BytesMut::from("GET //path HTTP/1.1\r\n\r\n"); - let req = parse_ready!(&mut buf); - + let req = parse_ready!(&mut BytesMut::from("GET //path HTTP/1.1\r\n\r\n")); assert_eq!(req.path(), "//path"); } #[test] fn test_http_request_parser_bad_method() { - let mut buf = BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n"); - - expect_parse_err!(&mut buf); + expect_parse_err!(&mut BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n")); } #[test] fn test_http_request_parser_bad_version() { - let mut buf = BytesMut::from("GET //get HT/11\r\n\r\n"); - - expect_parse_err!(&mut buf); + expect_parse_err!(&mut BytesMut::from("GET //get HT/11\r\n\r\n")); } #[test] @@ -1106,47 +1079,41 @@ mod tests { #[test] fn hrs_multiple_content_length() { - let mut buf = BytesMut::from( + expect_parse_err!(&mut BytesMut::from( "GET / HTTP/1.1\r\n\ Host: example.com\r\n\ Content-Length: 4\r\n\ Content-Length: 2\r\n\ \r\n\ abcd", - ); + )); - expect_parse_err!(&mut buf); - - let mut buf = BytesMut::from( + expect_parse_err!(&mut BytesMut::from( "GET / HTTP/1.1\r\n\ Host: example.com\r\n\ Content-Length: 0\r\n\ Content-Length: 2\r\n\ \r\n\ ab", - ); - - expect_parse_err!(&mut buf); + )); } #[test] fn hrs_content_length_plus() { - let mut buf = BytesMut::from( + expect_parse_err!(&mut BytesMut::from( "GET / HTTP/1.1\r\n\ Host: example.com\r\n\ Content-Length: +3\r\n\ \r\n\ 000", - ); - - expect_parse_err!(&mut buf); + )); } #[test] fn hrs_te_http10() { // in HTTP/1.0 transfer encoding is ignored and must therefore contain a CL header - let mut buf = BytesMut::from( + expect_parse_err!(&mut BytesMut::from( "POST / HTTP/1.0\r\n\ Host: example.com\r\n\ Transfer-Encoding: chunked\r\n\ @@ -1155,9 +1122,7 @@ mod tests { aaa\r\n\ 0\r\n\ ", - ); - - expect_parse_err!(&mut buf); + )); } #[test] diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 72cbfbee5..3267c2aed 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -976,9 +976,11 @@ where // // A Request head too large to parse is only checked on `httparse::Status::Partial`. - if this.payload.is_none() { - // When dispatcher has a payload the responsibility of wake up it would be shift - // to h1::payload::Payload. + match this.payload { + // When dispatcher has a payload the responsibility of wake ups is shifted to + // `h1::payload::Payload` unless the payload is needing a read, in which case it + // might not have access to the waker and could result in the dispatcher + // getting stuck until timeout. // // Reason: // Self wake up when there is payload would waste poll and/or result in @@ -989,7 +991,8 @@ where // 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(); + Some(ref p) if p.need_read(cx) != PayloadStatus::Read => {} + _ => cx.waker().wake_by_ref(), } return Ok(false); diff --git a/actix-http/src/h1/payload.rs b/actix-http/src/h1/payload.rs index a8c632396..1ed785a1b 100644 --- a/actix-http/src/h1/payload.rs +++ b/actix-http/src/h1/payload.rs @@ -16,7 +16,7 @@ use crate::error::PayloadError; /// max buffer size 32k pub(crate) const MAX_BUFFER_SIZE: usize = 32_768; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum PayloadStatus { Read, Pause, @@ -252,19 +252,15 @@ impl Inner { #[cfg(test)] mod tests { - 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_not_impl_any!(Payload: Send, Sync); assert_impl_all!(Inner: Unpin, Send, Sync); - // assertion not stable wrt rustc versions yet - // assert_impl_all!(Inner: UnwindSafe, RefUnwindSafe); #[actix_rt::test] async fn test_unread_data() { diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 85516cccc..680936f0f 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -67,7 +67,7 @@ where timer }) .unwrap_or_else(|| Box::pin(sleep(dur))), - on_flight: false, + in_flight: false, ping_pong: conn.ping_pong().unwrap(), }); @@ -84,9 +84,14 @@ where } struct H2PingPong { - timer: Pin>, - on_flight: bool, + /// Handle to send ping frames from the peer. ping_pong: PingPong, + + /// True when a ping has been sent and is waiting for a reply. + in_flight: bool, + + /// Timeout for pong response. + timer: Pin>, } impl Future for Dispatcher @@ -152,26 +157,28 @@ where }); } Poll::Ready(None) => return Poll::Ready(Ok(())), + Poll::Pending => match this.ping_pong.as_mut() { Some(ping_pong) => loop { - if ping_pong.on_flight { - // When have on flight ping pong. poll pong and and keep alive timer. - // on success pong received update keep alive timer to determine the next timing of - // ping pong. + if ping_pong.in_flight { + // When there is an in-flight ping-pong, poll pong and and keep-alive + // timer. On successful pong received, update keep-alive timer to + // determine the next timing of ping pong. match ping_pong.ping_pong.poll_pong(cx)? { Poll::Ready(_) => { - ping_pong.on_flight = false; + ping_pong.in_flight = false; let dead_line = this.config.keep_alive_deadline().unwrap(); ping_pong.timer.as_mut().reset(dead_line.into()); } Poll::Pending => { - return ping_pong.timer.as_mut().poll(cx).map(|_| Ok(())) + return ping_pong.timer.as_mut().poll(cx).map(|_| Ok(())); } } } else { - // When there is no on flight ping pong. keep alive timer is used to wait for next - // timing of ping pong. Therefore at this point it serves as an interval instead. + // When there is no in-flight ping-pong, keep-alive timer is used to + // wait for next timing of ping-pong. Therefore, at this point it serves + // as an interval instead. ready!(ping_pong.timer.as_mut().poll(cx)); ping_pong.ping_pong.send_ping(Ping::opaque())?; @@ -179,7 +186,7 @@ where let dead_line = this.config.keep_alive_deadline().unwrap(); ping_pong.timer.as_mut().reset(dead_line.into()); - ping_pong.on_flight = true; + ping_pong.in_flight = true; } }, None => return Poll::Pending, @@ -287,13 +294,13 @@ fn prepare_response( _ => {} } - let _ = match size { - BodySize::None | BodySize::Stream => None, + match size { + BodySize::None | BodySize::Stream => {} 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) + res.headers_mut().insert(CONTENT_LENGTH, HV_ZERO); } BodySize::Sized(len) => { @@ -302,7 +309,7 @@ fn prepare_response( res.headers_mut().insert( CONTENT_LENGTH, HeaderValue::from_str(buf.format(*len)).unwrap(), - ) + ); } }; diff --git a/actix-http/src/h2/mod.rs b/actix-http/src/h2/mod.rs index c8aaaaa5f..39198e0fe 100644 --- a/actix-http/src/h2/mod.rs +++ b/actix-http/src/h2/mod.rs @@ -103,11 +103,9 @@ 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); + assert_impl_all!(Payload: Unpin, Send, Sync); } diff --git a/actix-http/src/header/shared/extended.rs b/actix-http/src/header/shared/extended.rs index 1af9ca20e..cd8adb2bd 100644 --- a/actix-http/src/header/shared/extended.rs +++ b/actix-http/src/header/shared/extended.rs @@ -12,7 +12,7 @@ use crate::header::{Charset, HTTP_VALUE}; /// - A character sequence representing the actual value (`value`), separated by single quotes. /// /// It is defined in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2). -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ExtendedValue { /// The character set that is used to encode the `value` to a string. pub charset: Charset, diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index 71a3bdd53..0b35b5401 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -147,7 +147,7 @@ mod tests { // copy of encoding from actix-web headers #[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt - #[derive(Clone, PartialEq, Debug)] + #[derive(Debug, Clone, PartialEq, Eq)] pub enum Encoding { Chunked, Brotli, diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index 5616a4762..7469d74ee 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -3,7 +3,7 @@ use std::{cell::RefCell, ops, rc::Rc}; use bitflags::bitflags; /// Represents various types of connection -#[derive(Copy, Clone, PartialEq, Debug)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConnectionType { /// Close connection after response. Close, diff --git a/actix-http/src/payload.rs b/actix-http/src/payload.rs index ee0128af4..7d476c55f 100644 --- a/actix-http/src/payload.rs +++ b/actix-http/src/payload.rs @@ -97,12 +97,10 @@ where #[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); + assert_not_impl_any!(Payload: Send, Sync); } diff --git a/actix-http/src/ws/codec.rs b/actix-http/src/ws/codec.rs index 3aa325d6a..4a2e741b6 100644 --- a/actix-http/src/ws/codec.rs +++ b/actix-http/src/ws/codec.rs @@ -11,7 +11,7 @@ use super::{ }; /// A WebSocket message. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum Message { /// Text message. Text(ByteString), @@ -36,7 +36,7 @@ pub enum Message { } /// A WebSocket frame. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum Frame { /// Text frame. Note that the codec does not validate UTF-8 encoding. Text(Bytes), @@ -58,7 +58,7 @@ pub enum Frame { } /// A WebSocket continuation item. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum Item { FirstText(Bytes), FirstBinary(Bytes), diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index 568d801a2..75d4ca628 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -67,7 +67,7 @@ pub enum ProtocolError { } /// WebSocket handshake errors -#[derive(Debug, Clone, Copy, PartialEq, Display, Error)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)] pub enum HandshakeError { /// Only get method is allowed. #[display(fmt = "Method not allowed.")] diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml index ceb5b14dc..141b2d39e 100644 --- a/actix-router/Cargo.toml +++ b/actix-router/Cargo.toml @@ -21,7 +21,7 @@ default = ["http"] [dependencies] bytestring = ">=0.1.5, <2" -http = { version = "0.2.3", optional = true } +http = { version = "0.2.5", optional = true } regex = "1.5" serde = "1" tracing = { version = "0.1.30", default-features = false, features = ["log"] } diff --git a/actix-router/src/resource_path.rs b/actix-router/src/resource_path.rs index 00eb0927c..45948aa2a 100644 --- a/actix-router/src/resource_path.rs +++ b/actix-router/src/resource_path.rs @@ -27,7 +27,7 @@ impl<'a> ResourcePath for &'a str { impl ResourcePath for bytestring::ByteString { fn path(&self) -> &str { - &*self + self } } diff --git a/actix-router/src/router.rs b/actix-router/src/router.rs index 8ed966b59..064c5e904 100644 --- a/actix-router/src/router.rs +++ b/actix-router/src/router.rs @@ -1,6 +1,6 @@ use crate::{IntoPatterns, Resource, ResourceDef}; -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct ResourceId(pub u16); /// Resource router. diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index 43e306bb1..bf5d9324f 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2022-xx-xx + + +## 0.1.0 - 2022-07-24 - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 9938be67d..eaea15d47 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-test" -version = "0.1.0-beta.13" +version = "0.1.0" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -30,7 +30,7 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] actix-codec = "0.5" actix-http = "3" -actix-http-test = "3.0.0-beta.13" +actix-http-test = "3" actix-rt = "2.1" actix-service = "2" actix-utils = "3" diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 284351ed3..8222fc864 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -27,7 +27,7 @@ tokio = { version = "1.13.1", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.13" +actix-test = "0.1" awc = { version = "3", default-features = false } actix-web = { version = "4", features = ["macros"] } diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index a85d6c454..6b525a441 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,8 +1,11 @@ # Changes ## Unreleased - 2022-xx-xx +- Add `#[routes]` macro to support multiple paths for one handler. [#2718] - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. +[#2718]: https://github.com/actix/actix-web/pull/2718 + ## 4.0.1 - 2022-06-11 - Fix support for guard paths in route handler macros. [#2771] diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 52094443b..0c3b70589 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -18,12 +18,12 @@ proc-macro = true actix-router = "0.5.0" proc-macro2 = "1" quote = "1" -syn = { version = "1", features = ["full", "parsing"] } +syn = { version = "1", features = ["full", "extra-traits"] } [dev-dependencies] actix-macros = "0.2.3" actix-rt = "2.2" -actix-test = "0.1.0-beta.13" +actix-test = "0.1" actix-utils = "3.0.0" actix-web = "4.0.0" diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 5ca5616b6..4b6dc43c5 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -46,9 +46,20 @@ //! ``` //! //! # Multiple Path Handlers -//! There are no macros to generate multi-path handlers. Let us know in [this issue]. +//! Acts as a wrapper for multiple single method handler macros. It takes no arguments and +//! delegates those to the macros for the individual methods. See [macro@routes] macro docs. //! -//! [this issue]: https://github.com/actix/actix-web/issues/1709 +//! ``` +//! # use actix_web::HttpResponse; +//! # use actix_web_codegen::routes; +//! #[routes] +//! #[get("/test")] +//! #[get("/test2")] +//! #[delete("/test")] +//! async fn example() -> HttpResponse { +//! HttpResponse::Ok().finish() +//! } +//! ``` //! //! [actix-web attributes docs]: https://docs.rs/actix-web/latest/actix_web/#attributes //! [GET]: macro@get @@ -104,6 +115,39 @@ pub fn route(args: TokenStream, input: TokenStream) -> TokenStream { route::with_method(None, args, input) } +/// Creates resource handler, allowing multiple HTTP methods and paths. +/// +/// # Syntax +/// ```plain +/// #[routes] +/// #[("path", ...)] +/// #[("path", ...)] +/// ... +/// ``` +/// +/// # Attributes +/// The `routes` macro itself has no parameters, but allows specifying the attribute macros for +/// the multiple paths and/or methods, e.g. [`GET`](macro@get) and [`POST`](macro@post). +/// +/// These helper attributes take the same parameters as the [single method handlers](crate#single-method-handler). +/// +/// # Examples +/// ``` +/// # use actix_web::HttpResponse; +/// # use actix_web_codegen::routes; +/// #[routes] +/// #[get("/test")] +/// #[get("/test2")] +/// #[delete("/test")] +/// async fn example() -> HttpResponse { +/// HttpResponse::Ok().finish() +/// } +/// ``` +#[proc_macro_attribute] +pub fn routes(_: TokenStream, input: TokenStream) -> TokenStream { + route::with_methods(input) +} + macro_rules! method_macro { ($variant:ident, $method:ident) => { #[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")] diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index cae3cbd55..7a0658468 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -3,24 +3,12 @@ use std::{collections::HashSet, convert::TryFrom}; use actix_router::ResourceDef; use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::{format_ident, quote, ToTokens, TokenStreamExt}; -use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, NestedMeta, Path}; - -enum ResourceType { - Async, - Sync, -} - -impl ToTokens for ResourceType { - fn to_tokens(&self, stream: &mut TokenStream2) { - let ident = format_ident!("to"); - stream.append(ident); - } -} +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path}; macro_rules! method_type { ( - $($variant:ident, $upper:ident,)+ + $($variant:ident, $upper:ident, $lower:ident,)+ ) => { #[derive(Debug, PartialEq, Eq, Hash)] pub enum MethodType { @@ -42,20 +30,27 @@ macro_rules! method_type { _ => Err(format!("Unexpected HTTP method: `{}`", method)), } } + + fn from_path(method: &Path) -> Result { + match () { + $(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+ + _ => Err(()), + } + } } }; } method_type! { - Get, GET, - Post, POST, - Put, PUT, - Delete, DELETE, - Head, HEAD, - Connect, CONNECT, - Options, OPTIONS, - Trace, TRACE, - Patch, PATCH, + Get, GET, get, + Post, POST, post, + Put, PUT, put, + Delete, DELETE, delete, + Head, HEAD, head, + Connect, CONNECT, connect, + Options, OPTIONS, options, + Trace, TRACE, trace, + Patch, PATCH, patch, } impl ToTokens for MethodType { @@ -90,6 +85,18 @@ impl Args { let mut wrappers = Vec::new(); let mut methods = HashSet::new(); + if args.is_empty() { + return Err(syn::Error::new( + Span::call_site(), + format!( + r#"invalid service definition, expected #[{}("")]"#, + method + .map_or("route", |it| it.as_str()) + .to_ascii_lowercase() + ), + )); + } + let is_route_macro = method.is_none(); if let Some(method) = method { methods.insert(method); @@ -183,55 +190,27 @@ impl Args { } pub struct Route { + /// Name of the handler function being annotated. name: syn::Ident, - args: Args, + + /// Args passed to routing macro. + /// + /// When using `#[routes]`, this will contain args for each specific routing macro. + args: Vec, + + /// AST of the handler function being annotated. ast: syn::ItemFn, - resource_type: ResourceType, /// The doc comment attributes to copy to generated struct, if any. doc_attributes: Vec, } -fn guess_resource_type(typ: &syn::Type) -> ResourceType { - let mut guess = ResourceType::Sync; - - if let syn::Type::ImplTrait(typ) = typ { - for bound in typ.bounds.iter() { - if let syn::TypeParamBound::Trait(bound) = bound { - for bound in bound.path.segments.iter() { - if bound.ident == "Future" { - guess = ResourceType::Async; - break; - } else if bound.ident == "Responder" { - guess = ResourceType::Sync; - break; - } - } - } - } - } - - guess -} - impl Route { pub fn new( args: AttributeArgs, ast: syn::ItemFn, method: Option, ) -> syn::Result { - if args.is_empty() { - return Err(syn::Error::new( - Span::call_site(), - format!( - r#"invalid service definition, expected #[{}("")]"#, - method - .map_or("route", |it| it.as_str()) - .to_ascii_lowercase() - ), - )); - } - let name = ast.sig.ident.clone(); // Try and pull out the doc comments so that we can reapply them to the generated struct. @@ -244,6 +223,7 @@ impl Route { .collect(); let args = Args::new(args, method)?; + if args.methods.is_empty() { return Err(syn::Error::new( Span::call_site(), @@ -251,25 +231,44 @@ impl Route { )); } - let resource_type = if ast.sig.asyncness.is_some() { - ResourceType::Async - } else { - match ast.sig.output { - syn::ReturnType::Default => { - return Err(syn::Error::new_spanned( - ast, - "Function has no return type. Cannot be used as handler", - )); - } - syn::ReturnType::Type(_, ref typ) => guess_resource_type(typ.as_ref()), - } - }; + if matches!(ast.sig.output, syn::ReturnType::Default) { + return Err(syn::Error::new_spanned( + ast, + "Function has no return type. Cannot be used as handler", + )); + } + + Ok(Self { + name, + args: vec![args], + ast, + doc_attributes, + }) + } + + fn multiple(args: Vec, ast: syn::ItemFn) -> syn::Result { + let name = ast.sig.ident.clone(); + + // Try and pull out the doc comments so that we can reapply them to the generated struct. + // Note that multi line doc comments are converted to multiple doc attributes. + let doc_attributes = ast + .attrs + .iter() + .filter(|attr| attr.path.is_ident("doc")) + .cloned() + .collect(); + + if matches!(ast.sig.output, syn::ReturnType::Default) { + return Err(syn::Error::new_spanned( + ast, + "Function has no return type. Cannot be used as handler", + )); + } Ok(Self { name, args, ast, - resource_type, doc_attributes, }) } @@ -280,38 +279,57 @@ impl ToTokens for Route { let Self { name, ast, - args: - Args { + args, + doc_attributes, + } = self; + + let registrations: TokenStream2 = args + .iter() + .map(|args| { + let Args { path, resource_name, guards, wrappers, methods, - }, - resource_type, - doc_attributes, - } = self; - let resource_name = resource_name - .as_ref() - .map_or_else(|| name.to_string(), LitStr::value); - let method_guards = { - let mut others = methods.iter(); - // unwrapping since length is checked to be at least one - let first = others.next().unwrap(); + } = args; + + let resource_name = resource_name + .as_ref() + .map_or_else(|| name.to_string(), LitStr::value); + + let method_guards = { + let mut others = methods.iter(); + + // unwrapping since length is checked to be at least one + let first = others.next().unwrap(); + + if methods.len() > 1 { + quote! { + .guard( + ::actix_web::guard::Any(::actix_web::guard::#first()) + #(.or(::actix_web::guard::#others()))* + ) + } + } else { + quote! { + .guard(::actix_web::guard::#first()) + } + } + }; - if methods.len() > 1 { quote! { - .guard( - ::actix_web::guard::Any(::actix_web::guard::#first()) - #(.or(::actix_web::guard::#others()))* - ) + let __resource = ::actix_web::Resource::new(#path) + .name(#resource_name) + #method_guards + #(.guard(::actix_web::guard::fn_guard(#guards)))* + #(.wrap(#wrappers))* + .to(#name); + + ::actix_web::dev::HttpServiceFactory::register(__resource, __config); } - } else { - quote! { - .guard(::actix_web::guard::#first()) - } - } - }; + }) + .collect(); let stream = quote! { #(#doc_attributes)* @@ -321,14 +339,7 @@ impl ToTokens for Route { impl ::actix_web::dev::HttpServiceFactory for #name { fn register(self, __config: &mut actix_web::dev::AppService) { #ast - let __resource = ::actix_web::Resource::new(#path) - .name(#resource_name) - #method_guards - #(.guard(::actix_web::guard::fn_guard(#guards)))* - #(.wrap(#wrappers))* - .#resource_type(#name); - - ::actix_web::dev::HttpServiceFactory::register(__resource, __config) + #registrations } } }; @@ -357,6 +368,57 @@ pub(crate) fn with_method( } } +pub(crate) fn with_methods(input: TokenStream) -> TokenStream { + let mut ast = match syn::parse::(input.clone()) { + Ok(ast) => ast, + // on parse error, make IDEs happy; see fn docs + Err(err) => return input_and_compile_error(input, err), + }; + + let (methods, others) = ast + .attrs + .into_iter() + .map(|attr| match MethodType::from_path(&attr.path) { + Ok(method) => Ok((method, attr)), + Err(_) => Err(attr), + }) + .partition::, _>(Result::is_ok); + + ast.attrs = others.into_iter().map(Result::unwrap_err).collect(); + + let methods = + match methods + .into_iter() + .map(Result::unwrap) + .map(|(method, attr)| { + attr.parse_meta().and_then(|args| { + if let Meta::List(args) = args { + Args::new(args.nested.into_iter().collect(), Some(method)) + } else { + Err(syn::Error::new_spanned(attr, "Invalid input for macro")) + } + }) + }) + .collect::, _>>() + { + Ok(methods) if methods.is_empty() => return input_and_compile_error( + input, + syn::Error::new( + Span::call_site(), + "The #[routes] macro requires at least one `#[(..)]` attribute.", + ), + ), + Ok(methods) => methods, + Err(err) => return input_and_compile_error(input, err), + }; + + match Route::multiple(methods, ast) { + Ok(route) => route.into_token_stream().into(), + // on macro related error, make IDEs happy; see fn docs + Err(err) => input_and_compile_error(input, err), + } +} + /// Converts the error to a token stream and appends it to the original input. /// /// Returning the original input in addition to the error is good for IDEs which can gracefully diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 55c2417b2..10e906967 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -8,9 +8,11 @@ use actix_web::{ header::{HeaderName, HeaderValue}, StatusCode, }, - web, App, Error, HttpResponse, Responder, + web, App, Error, HttpRequest, HttpResponse, Responder, +}; +use actix_web_codegen::{ + connect, delete, get, head, options, patch, post, put, route, routes, trace, }; -use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, route, trace}; use futures_core::future::LocalBoxFuture; // Make sure that we can name function as 'config' @@ -89,8 +91,41 @@ async fn route_test() -> impl Responder { HttpResponse::Ok() } +#[routes] +#[get("/routes/test")] +#[get("/routes/test2")] +#[post("/routes/test")] +async fn routes_test() -> impl Responder { + HttpResponse::Ok() +} + +// routes overlap with the more specific route first, therefore accessible +#[routes] +#[get("/routes/overlap/test")] +#[get("/routes/overlap/{foo}")] +async fn routes_overlapping_test(req: HttpRequest) -> impl Responder { + // foo is only populated when route is not /routes/overlap/test + match req.match_info().get("foo") { + None => assert!(req.uri() == "/routes/overlap/test"), + Some(_) => assert!(req.uri() != "/routes/overlap/test"), + } + + HttpResponse::Ok() +} + +// routes overlap with the more specific route last, therefore inaccessible +#[routes] +#[get("/routes/overlap2/{foo}")] +#[get("/routes/overlap2/test")] +async fn routes_overlapping_inaccessible_test(req: HttpRequest) -> impl Responder { + // foo is always populated even when path is /routes/overlap2/test + assert!(req.match_info().get("foo").is_some()); + + HttpResponse::Ok() +} + #[get("/custom_resource_name", name = "custom")] -async fn custom_resource_name_test<'a>(req: actix_web::HttpRequest) -> impl Responder { +async fn custom_resource_name_test<'a>(req: HttpRequest) -> impl Responder { assert!(req.url_for_static("custom").is_ok()); assert!(req.url_for_static("custom_resource_name_test").is_err()); HttpResponse::Ok() @@ -201,6 +236,9 @@ async fn test_body() { .service(patch_test) .service(test_handler) .service(route_test) + .service(routes_overlapping_test) + .service(routes_overlapping_inaccessible_test) + .service(routes_test) .service(custom_resource_name_test) .service(guard_test) }); @@ -258,6 +296,38 @@ async fn test_body() { let response = request.send().await.unwrap(); assert!(!response.status().is_success()); + let request = srv.request(http::Method::GET, srv.url("/routes/test")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::GET, srv.url("/routes/test2")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::POST, srv.url("/routes/test")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::GET, srv.url("/routes/not-set")); + let response = request.send().await.unwrap(); + assert!(response.status().is_client_error()); + + let request = srv.request(http::Method::GET, srv.url("/routes/overlap/test")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::GET, srv.url("/routes/overlap/bar")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::GET, srv.url("/routes/overlap2/test")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::GET, srv.url("/routes/overlap2/bar")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + let request = srv.request(http::Method::GET, srv.url("/custom_resource_name")); let response = request.send().await.unwrap(); assert!(response.status().is_success()); diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index 976b3d52c..1f7996fd0 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -12,6 +12,10 @@ fn compile_macros() { t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs"); t.compile_fail("tests/trybuild/route-malformed-path-fail.rs"); + t.pass("tests/trybuild/routes-ok.rs"); + t.compile_fail("tests/trybuild/routes-missing-method-fail.rs"); + t.compile_fail("tests/trybuild/routes-missing-args-fail.rs"); + t.pass("tests/trybuild/docstring-ok.rs"); t.pass("tests/trybuild/test-runtime.rs"); diff --git a/actix-web-codegen/tests/trybuild/routes-missing-args-fail.rs b/actix-web-codegen/tests/trybuild/routes-missing-args-fail.rs new file mode 100644 index 000000000..65573cf79 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/routes-missing-args-fail.rs @@ -0,0 +1,14 @@ +use actix_web_codegen::*; + +#[routes] +#[get] +async fn index() -> String { + "Hello World!".to_owned() +} + +#[actix_web::main] +async fn main() { + use actix_web::App; + + let srv = actix_test::start(|| App::new().service(index)); +} diff --git a/actix-web-codegen/tests/trybuild/routes-missing-args-fail.stderr b/actix-web-codegen/tests/trybuild/routes-missing-args-fail.stderr new file mode 100644 index 000000000..8efe0682b --- /dev/null +++ b/actix-web-codegen/tests/trybuild/routes-missing-args-fail.stderr @@ -0,0 +1,21 @@ +error: invalid service definition, expected #[get("")] + --> tests/trybuild/routes-missing-args-fail.rs:4:1 + | +4 | #[get] + | ^^^^^^ + | + = note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: Invalid input for macro + --> tests/trybuild/routes-missing-args-fail.rs:4:1 + | +4 | #[get] + | ^^^^^^ + +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied + --> tests/trybuild/routes-missing-args-fail.rs:13:55 + | +13 | let srv = actix_test::start(|| App::new().service(index)); + | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` + | | + | required by a bound introduced by this call diff --git a/actix-web-codegen/tests/trybuild/routes-missing-method-fail.rs b/actix-web-codegen/tests/trybuild/routes-missing-method-fail.rs new file mode 100644 index 000000000..f0271ef44 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/routes-missing-method-fail.rs @@ -0,0 +1,13 @@ +use actix_web_codegen::*; + +#[routes] +async fn index() -> String { + "Hello World!".to_owned() +} + +#[actix_web::main] +async fn main() { + use actix_web::App; + + let srv = actix_test::start(|| App::new().service(index)); +} diff --git a/actix-web-codegen/tests/trybuild/routes-missing-method-fail.stderr b/actix-web-codegen/tests/trybuild/routes-missing-method-fail.stderr new file mode 100644 index 000000000..b3795d74a --- /dev/null +++ b/actix-web-codegen/tests/trybuild/routes-missing-method-fail.stderr @@ -0,0 +1,15 @@ +error: The #[routes] macro requires at least one `#[(..)]` attribute. + --> tests/trybuild/routes-missing-method-fail.rs:3:1 + | +3 | #[routes] + | ^^^^^^^^^ + | + = note: this error originates in the attribute macro `routes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied + --> tests/trybuild/routes-missing-method-fail.rs:12:55 + | +12 | let srv = actix_test::start(|| App::new().service(index)); + | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` + | | + | required by a bound introduced by this call diff --git a/actix-web-codegen/tests/trybuild/routes-ok.rs b/actix-web-codegen/tests/trybuild/routes-ok.rs new file mode 100644 index 000000000..98b5e553e --- /dev/null +++ b/actix-web-codegen/tests/trybuild/routes-ok.rs @@ -0,0 +1,23 @@ +use actix_web_codegen::*; + +#[routes] +#[get("/")] +#[post("/")] +async fn index() -> String { + "Hello World!".to_owned() +} + +#[actix_web::main] +async fn main() { + use actix_web::App; + + let srv = actix_test::start(|| App::new().service(index)); + + let request = srv.get("/"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.post("/"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index 0144cb912..f38282b41 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -2,12 +2,14 @@ ## Unreleased - 2022-xx-xx ### Added +- Add `#[routes]` macro to support multiple paths for one handler. [#2718] - Add `ServiceRequest::{parts, request}()` getter methods. [#2786] - Add configuration options for TLS handshake timeout via `HttpServer::{rustls, openssl}_with_config` methods. [#2752] ### Changed - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. +[#2718]: https://github.com/actix/actix-web/pull/2718 [#2752]: https://github.com/actix/actix-web/pull/2752 [#2786]: https://github.com/actix/actix-web/pull/2786 diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index c58e1604b..12806e686 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -84,11 +84,12 @@ derive_more = "0.99.5" encoding_rs = "0.8" futures-core = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false } +http = "0.2.8" itoa = "1" language-tags = "0.3" -once_cell = "1.5" log = "0.4" mime = "0.3" +once_cell = "1.5" pin-project-lite = "0.2.7" regex = "1.5.5" serde = "1.0" @@ -101,7 +102,7 @@ url = "2.1" [dev-dependencies] actix-files = "0.6" -actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] } +actix-test = { version = "0.1", features = ["openssl", "rustls"] } awc = { version = "3", features = ["openssl"] } brotli = "3.3.3" diff --git a/actix-web/src/data.rs b/actix-web/src/data.rs index a689d13e0..89104a1ac 100644 --- a/actix-web/src/data.rs +++ b/actix-web/src/data.rs @@ -118,7 +118,7 @@ impl Deref for Data { impl Clone for Data { fn clone(&self) -> Data { - Data(self.0.clone()) + Data(Arc::clone(&self.0)) } } diff --git a/actix-web/src/error/mod.rs b/actix-web/src/error/mod.rs index 6095cd5d2..604c539f3 100644 --- a/actix-web/src/error/mod.rs +++ b/actix-web/src/error/mod.rs @@ -42,7 +42,7 @@ pub struct BlockingError; impl ResponseError for crate::error::BlockingError {} /// Errors which can occur when attempting to generate resource uri. -#[derive(Debug, PartialEq, Display, Error, From)] +#[derive(Debug, PartialEq, Eq, Display, Error, From)] #[non_exhaustive] pub enum UrlGenerationError { /// Resource not found. diff --git a/actix-web/src/http/header/content_disposition.rs b/actix-web/src/http/header/content_disposition.rs index 8b7101aa1..0bb459193 100644 --- a/actix-web/src/http/header/content_disposition.rs +++ b/actix-web/src/http/header/content_disposition.rs @@ -10,9 +10,10 @@ //! - Browser conformance tests at: //! - IANA assignment: +use std::fmt::{self, Write}; + use once_cell::sync::Lazy; use regex::Regex; -use std::fmt::{self, Write}; use super::{ExtendedValue, Header, TryIntoHeaderValue, Writer}; use crate::http::header; @@ -36,7 +37,7 @@ fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) { } /// The implied disposition of the content of the HTTP body. -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum DispositionType { /// Inline implies default processing. Inline, @@ -78,7 +79,7 @@ impl<'a> From<&'a str> for DispositionType { /// assert!(param.is_filename()); /// assert_eq!(param.as_filename().unwrap(), "sample.txt"); /// ``` -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] #[allow(clippy::large_enum_variant)] pub enum DispositionParam { /// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from @@ -301,7 +302,7 @@ impl DispositionParam { /// change to match local file system conventions if applicable, and do not use directory path /// information that may be present. /// See [RFC 2183 §2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3). -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ContentDisposition { /// The disposition type pub disposition: DispositionType, diff --git a/actix-web/src/http/header/if_range.rs b/actix-web/src/http/header/if_range.rs index b845fb3bf..eb3632a4d 100644 --- a/actix-web/src/http/header/if_range.rs +++ b/actix-web/src/http/header/if_range.rs @@ -57,7 +57,7 @@ use crate::HttpMessage; /// IfRange::Date(fetched.into()) /// ); /// ``` -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum IfRange { /// The entity-tag the client has of the resource. EntityTag(EntityTag), diff --git a/actix-web/src/http/header/macros.rs b/actix-web/src/http/header/macros.rs index 25f40a52b..b40eca03b 100644 --- a/actix-web/src/http/header/macros.rs +++ b/actix-web/src/http/header/macros.rs @@ -224,10 +224,11 @@ macro_rules! common_header { // List header, one or more items with "*" option ($(#[$attrs:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => { $(#[$attrs])* - #[derive(Clone, Debug, PartialEq)] + #[derive(Clone, Debug, PartialEq, Eq)] pub enum $id { /// Any value is a match Any, + /// Only the listed items are a match Items(Vec<$item>), } diff --git a/actix-web/src/http/header/range.rs b/actix-web/src/http/header/range.rs index 68028f53a..2326bb19c 100644 --- a/actix-web/src/http/header/range.rs +++ b/actix-web/src/http/header/range.rs @@ -53,7 +53,7 @@ use super::{Header, HeaderName, HeaderValue, InvalidHeaderValue, TryIntoHeaderVa /// builder.insert_header(Range::bytes(1, 100)); /// builder.insert_header(Range::bytes_multi(vec![(1, 100), (200, 300)])); /// ``` -#[derive(PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Range { /// Byte range. Bytes(Vec), diff --git a/actix-web/src/info.rs b/actix-web/src/info.rs index 77b98110e..7c685406e 100644 --- a/actix-web/src/info.rs +++ b/actix-web/src/info.rs @@ -2,7 +2,6 @@ use std::{convert::Infallible, net::SocketAddr}; use actix_utils::future::{err, ok, Ready}; use derive_more::{Display, Error}; -use once_cell::sync::Lazy; use crate::{ dev::{AppConfig, Payload, RequestHead}, @@ -13,12 +12,9 @@ use crate::{ FromRequest, HttpRequest, ResponseError, }; -static X_FORWARDED_FOR: Lazy = - Lazy::new(|| HeaderName::from_static("x-forwarded-for")); -static X_FORWARDED_HOST: Lazy = - Lazy::new(|| HeaderName::from_static("x-forwarded-host")); -static X_FORWARDED_PROTO: Lazy = - Lazy::new(|| HeaderName::from_static("x-forwarded-proto")); +static X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for"); +static X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host"); +static X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto"); /// Trim whitespace then any quote marks. fn unquote(val: &str) -> &str { @@ -117,21 +113,21 @@ impl ConnectionInfo { } let scheme = scheme - .or_else(|| first_header_value(req, &*X_FORWARDED_PROTO)) + .or_else(|| first_header_value(req, &X_FORWARDED_PROTO)) .or_else(|| req.uri.scheme().map(Scheme::as_str)) .or_else(|| Some("https").filter(|_| cfg.secure())) .unwrap_or("http") .to_owned(); let host = host - .or_else(|| first_header_value(req, &*X_FORWARDED_HOST)) + .or_else(|| first_header_value(req, &X_FORWARDED_HOST)) .or_else(|| req.headers.get(&header::HOST)?.to_str().ok()) .or_else(|| req.uri.authority().map(Authority::as_str)) .unwrap_or_else(|| cfg.host()) .to_owned(); let realip_remote_addr = realip_remote_addr - .or_else(|| first_header_value(req, &*X_FORWARDED_FOR)) + .or_else(|| first_header_value(req, &X_FORWARDED_FOR)) .map(str::to_owned); let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string()); diff --git a/actix-web/src/lib.rs b/actix-web/src/lib.rs index 4eab24cec..8d9e2dbcd 100644 --- a/actix-web/src/lib.rs +++ b/actix-web/src/lib.rs @@ -132,6 +132,7 @@ macro_rules! codegen_reexport { codegen_reexport!(main); codegen_reexport!(test); codegen_reexport!(route); +codegen_reexport!(routes); codegen_reexport!(head); codegen_reexport!(get); codegen_reexport!(post); diff --git a/actix-web/src/middleware/compress.rs b/actix-web/src/middleware/compress.rs index ed4291bed..51b44c6ef 100644 --- a/actix-web/src/middleware/compress.rs +++ b/actix-web/src/middleware/compress.rs @@ -220,32 +220,25 @@ static SUPPORTED_ENCODINGS_STRING: Lazy = Lazy::new(|| { encoding.join(", ") }); -static SUPPORTED_ENCODINGS: Lazy> = Lazy::new(|| { - let mut encodings = vec![Encoding::identity()]; - +static SUPPORTED_ENCODINGS: &[Encoding] = &[ + Encoding::identity(), #[cfg(feature = "compress-brotli")] { - encodings.push(Encoding::brotli()); - } - + Encoding::brotli() + }, #[cfg(feature = "compress-gzip")] { - encodings.push(Encoding::gzip()); - encodings.push(Encoding::deflate()); - } - + Encoding::gzip() + }, + #[cfg(feature = "compress-gzip")] + { + Encoding::deflate() + }, #[cfg(feature = "compress-zstd")] { - encodings.push(Encoding::zstd()); - } - - assert!( - !encodings.is_empty(), - "encodings can not be empty unless __compress feature has been explicitly enabled by itself" - ); - - encodings -}); + Encoding::zstd() + }, +]; // move cfg(feature) to prevents_double_compressing if more tests are added #[cfg(feature = "compress-gzip")] @@ -326,6 +319,7 @@ mod tests { .to_request(); let res = test::call_service(&app, req).await; assert_eq!(res.status(), StatusCode::OK); + #[allow(clippy::mutable_key_type)] let vary_headers = res.headers().get_all(header::VARY).collect::>(); assert!(vary_headers.contains(&HeaderValue::from_static("x-test"))); assert!(vary_headers.contains(&HeaderValue::from_static("accept-encoding"))); diff --git a/actix-web/src/request.rs b/actix-web/src/request.rs index d26488e2b..2998c7010 100644 --- a/actix-web/src/request.rs +++ b/actix-web/src/request.rs @@ -253,7 +253,7 @@ impl HttpRequest { #[inline] pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> { if !self.extensions().contains::() { - let info = ConnectionInfo::new(self.head(), &*self.app_config()); + let info = ConnectionInfo::new(self.head(), self.app_config()); self.extensions_mut().insert(info); } diff --git a/actix-web/src/server.rs b/actix-web/src/server.rs index 169eafab0..9c5076d36 100644 --- a/actix-web/src/server.rs +++ b/actix-web/src/server.rs @@ -401,6 +401,8 @@ where .into_factory() .map_err(|err| err.into().error_response()); + // false positive lint (?) + #[allow(clippy::significant_drop_in_scrutinee)] let acceptor_config = match c.tls_handshake_timeout { Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur), None => TlsAcceptorConfig::default(), diff --git a/actix-web/src/types/either.rs b/actix-web/src/types/either.rs index c0faf04b1..119dd0d62 100644 --- a/actix-web/src/types/either.rs +++ b/actix-web/src/types/either.rs @@ -73,7 +73,7 @@ use crate::{ /// } /// } /// ``` -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum Either { /// A value of type `L`. Left(L), diff --git a/actix-web/src/types/path.rs b/actix-web/src/types/path.rs index 34c335ba9..a90c912f6 100644 --- a/actix-web/src/types/path.rs +++ b/actix-web/src/types/path.rs @@ -134,6 +134,7 @@ where /// ``` #[derive(Clone, Default)] pub struct PathConfig { + #[allow(clippy::type_complexity)] err_handler: Option Error + Send + Sync>>, } diff --git a/actix-web/src/types/query.rs b/actix-web/src/types/query.rs index 97d17123d..e71b886f2 100644 --- a/actix-web/src/types/query.rs +++ b/actix-web/src/types/query.rs @@ -169,6 +169,7 @@ impl FromRequest for Query { /// ``` #[derive(Clone, Default)] pub struct QueryConfig { + #[allow(clippy::type_complexity)] err_handler: Option Error + Send + Sync>>, } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 9da103cb0..0250091bf 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -94,9 +94,9 @@ trust-dns-resolver = { version = "0.21", optional = true } [dev-dependencies] actix-http = { version = "3", features = ["openssl"] } -actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] } +actix-http-test = { version = "3", features = ["openssl"] } actix-server = "2" -actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] } +actix-test = { version = "0.1", features = ["openssl", "rustls"] } actix-tls = { version = "3", features = ["openssl", "rustls"] } actix-utils = "3" actix-web = { version = "4", features = ["openssl"] } diff --git a/scripts/bump b/scripts/bump index 209e2281d..33ea52010 100755 --- a/scripts/bump +++ b/scripts/bump @@ -98,7 +98,7 @@ rm -f $README_FILE.bak echo "manifest, changelog, and readme updated" echo echo "check other references:" -rg --glob='**/Cargo.toml' "\ +rg --glob='**/{Cargo.toml,README.md}' "\ ${PACKAGE_NAME} ?= ?\"[^\"]+\"\ |${PACKAGE_NAME} ?=.*version ?= ?\"([^\"]+)\"\ |package ?= ?\"${PACKAGE_NAME}\".*version ?= ?\"([^\"]+)\"\