Merge branch 'actix:master' into fix/http-v3.2.1

This commit is contained in:
simorex80 2022-07-26 21:04:23 +02:00 committed by GitHub
commit 4c65879a94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 656 additions and 353 deletions

View File

@ -45,18 +45,15 @@ jobs:
profile: minimal profile: minimal
override: true override: true
- name: Install cargo-hack
uses: taiki-e/install-action@cargo-hack
- name: Generate Cargo.lock - name: Generate Cargo.lock
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: generate-lockfile } with: { command: generate-lockfile }
- name: Cache Dependencies - name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0 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 - name: check minimal
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: ci-check-min } with: { command: ci-check-min }
@ -81,7 +78,7 @@ jobs:
- name: Clear the cargo caches - name: Clear the cargo caches
run: | 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 cargo-cache
ci_feature_powerset_check: ci_feature_powerset_check:
@ -102,18 +99,15 @@ jobs:
profile: minimal profile: minimal
override: true override: true
- name: Install cargo-hack
uses: taiki-e/install-action@cargo-hack
- name: Generate Cargo.lock - name: Generate Cargo.lock
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: generate-lockfile } with: { command: generate-lockfile }
- name: Cache Dependencies - name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0 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 - name: check feature combinations
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: ci-check-all-feature-powerset } with: { command: ci-check-all-feature-powerset }
@ -140,18 +134,15 @@ jobs:
profile: minimal profile: minimal
override: true override: true
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Generate Cargo.lock - name: Generate Cargo.lock
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: generate-lockfile } with: { command: generate-lockfile }
- name: Cache Dependencies - name: Cache Dependencies
uses: Swatinem/rust-cache@v1.3.0 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 - name: Test with cargo-nextest
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:

View File

@ -47,18 +47,22 @@ jobs:
profile: minimal profile: minimal
override: true 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 - name: Generate Cargo.lock
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: generate-lockfile } with: { command: generate-lockfile }
- name: Cache Dependencies - name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0 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 - name: check minimal
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: ci-check-min } with: { command: ci-check-min }
@ -83,7 +87,7 @@ jobs:
- name: Clear the cargo caches - name: Clear the cargo caches
run: | 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 cargo-cache
io-uring: io-uring:

View File

@ -1,8 +1,14 @@
# Changes # Changes
## Unreleased - 2022-xx-xx ## 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. - 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 ## 0.6.1 - 2022-06-11
- Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021] - Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.1" version = "0.6.2"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>", "fakeshadow <24548779@qq.com>",
@ -46,6 +46,6 @@ actix-server = { version = "2.1", optional = true } # ensure matching tokio-urin
[dev-dependencies] [dev-dependencies]
actix-rt = "2.7" actix-rt = "2.7"
actix-test = "0.1.0-beta.13" actix-test = "0.1"
actix-web = "4" actix-web = "4"
tempfile = "3.2" tempfile = "3.2"

View File

@ -3,11 +3,11 @@
> Static file serving for Actix Web > Static file serving for Actix Web
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![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) ![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-files.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
<br /> <br />
[![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) [![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) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -2,7 +2,7 @@ use actix_web::{http::StatusCode, ResponseError};
use derive_more::Display; use derive_more::Display;
/// Errors which can occur when serving static files. /// Errors which can occur when serving static files.
#[derive(Display, Debug, PartialEq)] #[derive(Debug, PartialEq, Eq, Display)]
pub enum FilesError { pub enum FilesError {
/// Path is not a directory /// Path is not a directory
#[allow(dead_code)] #[allow(dead_code)]
@ -22,7 +22,7 @@ impl ResponseError for FilesError {
} }
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
#[derive(Display, Debug, PartialEq)] #[derive(Debug, PartialEq, Eq, Display)]
#[non_exhaustive] #[non_exhaustive]
pub enum UriSegmentError { pub enum UriSegmentError {
/// The segment started with the wrapped invalid character. /// The segment started with the wrapped invalid character.

View File

@ -528,11 +528,26 @@ impl NamedFile {
length = ranges[0].length; length = ranges[0].length;
offset = ranges[0].start; offset = ranges[0].start;
// don't allow compression middleware to modify partial content // When a Content-Encoding header is present in a 206 partial content response
res.insert_header(( // for video content, it prevents browser video players from starting playback
header::CONTENT_ENCODING, // before loading the whole video and also prevents seeking.
HeaderValue::from_static("identity"), //
)); // 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(( res.insert_header((
header::CONTENT_RANGE, header::CONTENT_RANGE,

View File

@ -1,11 +1,11 @@
use actix_files::Files; use actix_files::{Files, NamedFile};
use actix_web::{ use actix_web::{
http::{ http::{
header::{self, HeaderValue}, header::{self, HeaderValue},
StatusCode, StatusCode,
}, },
test::{self, TestRequest}, test::{self, TestRequest},
App, web, App,
}; };
#[actix_web::test] #[actix_web::test]
@ -36,3 +36,31 @@ async fn test_utf8_file_contents() {
Some(&HeaderValue::from_static("text/plain")), 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"
);
}

View File

@ -1,9 +1,24 @@
# Changes # Changes
## Unreleased - 2022-xx-xx ## 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
<details>
<summary>3.0.0 Pre-Releases</summary>
## 3.0.0-beta.13 - 2022-02-16 ## 3.0.0-beta.13 - 2022-02-16
- No significant changes since `3.0.0-beta.12`. - No significant changes since `3.0.0-beta.12`.
@ -69,6 +84,7 @@
[#1813]: https://github.com/actix/actix-web/pull/1813 [#1813]: https://github.com/actix/actix-web/pull/1813
</details>
## 2.1.0 - 2020-11-25 ## 2.1.0 - 2020-11-25
- Add ability to set address for `TestServer`. [#1645] - Add ability to set address for `TestServer`. [#1645]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http-test" name = "actix-http-test"
version = "3.0.0-beta.13" version = "3.0.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Various helpers for Actix applications to use during testing" description = "Various helpers for Actix applications to use during testing"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]

View File

@ -3,11 +3,11 @@
> Various helpers for Actix applications to use during testing. > 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) [![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) ![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) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
<br> <br>
[![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) [![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) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -1,6 +1,10 @@
# Changes # Changes
## Unreleased - 2022-xx-xx ## 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 ## 3.2.1 - 2022-07-02
@ -29,9 +33,9 @@
### Fixed ### Fixed
- Revert broken fix in [#2624] that caused erroneous 500 error responses. Temporarily re-introduces [#2357] bug. [#2779] - 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 [#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/pull/2779
[#2779]: https://github.com/actix/actix-web/issues/2779
## 3.0.4 - 2022-03-09 ## 3.0.4 - 2022-03-09
@ -43,14 +47,14 @@
### Fixed ### Fixed
- Allow spaces between header name and colon when parsing responses. [#2684] - 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 ## 3.0.2 - 2022-03-05
### Fixed ### Fixed
- Fix encoding camel-case header names with more than one hyphen. [#2683] - 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 ## 3.0.1 - 2022-03-04

View File

@ -97,7 +97,7 @@ flate2 = { version = "1.0.13", optional = true }
zstd = { version = "0.11", optional = true } zstd = { version = "0.11", optional = true }
[dev-dependencies] [dev-dependencies]
actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] } actix-http-test = { version = "3", features = ["openssl"] }
actix-server = "2" actix-server = "2"
actix-tls = { version = "3", features = ["openssl"] } actix-tls = { version = "3", features = ["openssl"] }
actix-web = "4" actix-web = "4"

View File

@ -35,7 +35,7 @@ impl Default for ServiceConfig {
} }
impl ServiceConfig { impl ServiceConfig {
/// Create instance of `ServiceConfig` /// Create instance of `ServiceConfig`.
pub fn new( pub fn new(
keep_alive: KeepAlive, keep_alive: KeepAlive,
client_request_timeout: Duration, client_request_timeout: Duration,

View File

@ -388,7 +388,7 @@ impl StdError for DispatchError {
/// A set of error that can occur during parsing content type. /// A set of error that can occur during parsing content type.
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq, Eq))]
#[non_exhaustive] #[non_exhaustive]
pub enum ContentTypeError { pub enum ContentTypeError {
/// Can not parse content type /// Can not parse content type

View File

@ -15,7 +15,7 @@ macro_rules! byte (
}) })
); );
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum ChunkedState { pub(super) enum ChunkedState {
Size, Size,
SizeLws, SizeLws,

View File

@ -440,7 +440,7 @@ impl HeaderIndex {
} }
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
/// Chunk type yielded while decoding a payload. /// Chunk type yielded while decoding a payload.
pub enum PayloadItem { pub enum PayloadItem {
Chunk(Bytes), Chunk(Bytes),
@ -450,7 +450,7 @@ pub enum PayloadItem {
/// Decoder that can handle different payload types. /// Decoder that can handle different payload types.
/// ///
/// If a message body does not use `Transfer-Encoding`, it should include a `Content-Length`. /// 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 { pub struct PayloadDecoder {
kind: Kind, kind: Kind,
} }
@ -476,7 +476,7 @@ impl PayloadDecoder {
} }
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
enum Kind { enum Kind {
/// A reader used when a `Content-Length` header is passed with a positive integer. /// A reader used when a `Content-Length` header is passed with a positive integer.
Length(u64), Length(u64),
@ -844,121 +844,98 @@ mod tests {
#[test] #[test]
fn test_conn_default_1_0() { 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 BytesMut::from("GET /test HTTP/1.0\r\n\r\n"));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::Close); assert_eq!(req.head().connection_type(), ConnectionType::Close);
} }
#[test] #[test]
fn test_conn_default_1_1() { 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 BytesMut::from("GET /test HTTP/1.1\r\n\r\n"));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
} }
#[test] #[test]
fn test_conn_close() { fn test_conn_close() {
let mut buf = BytesMut::from( let req = parse_ready!(&mut BytesMut::from(
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
connection: close\r\n\r\n", connection: close\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::Close); 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\ "GET /test HTTP/1.1\r\n\
connection: Close\r\n\r\n", connection: Close\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::Close); assert_eq!(req.head().connection_type(), ConnectionType::Close);
} }
#[test] #[test]
fn test_conn_close_1_0() { 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\ "GET /test HTTP/1.0\r\n\
connection: close\r\n\r\n", connection: close\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::Close); assert_eq!(req.head().connection_type(), ConnectionType::Close);
} }
#[test] #[test]
fn test_conn_keep_alive_1_0() { 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\ "GET /test HTTP/1.0\r\n\
connection: keep-alive\r\n\r\n", connection: keep-alive\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); 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\ "GET /test HTTP/1.0\r\n\
connection: Keep-Alive\r\n\r\n", connection: Keep-Alive\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
} }
#[test] #[test]
fn test_conn_keep_alive_1_1() { 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\ "GET /test HTTP/1.1\r\n\
connection: keep-alive\r\n\r\n", connection: keep-alive\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
} }
#[test] #[test]
fn test_conn_other_1_0() { 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\ "GET /test HTTP/1.0\r\n\
connection: other\r\n\r\n", connection: other\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::Close); assert_eq!(req.head().connection_type(), ConnectionType::Close);
} }
#[test] #[test]
fn test_conn_other_1_1() { 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\ "GET /test HTTP/1.1\r\n\
connection: other\r\n\r\n", connection: other\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
} }
#[test] #[test]
fn test_conn_upgrade() { fn test_conn_upgrade() {
let mut buf = BytesMut::from( let req = parse_ready!(&mut BytesMut::from(
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
upgrade: websockets\r\n\ upgrade: websockets\r\n\
connection: upgrade\r\n\r\n", connection: upgrade\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert!(req.upgrade()); assert!(req.upgrade());
assert_eq!(req.head().connection_type(), ConnectionType::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\ "GET /test HTTP/1.1\r\n\
upgrade: Websockets\r\n\ upgrade: Websockets\r\n\
connection: Upgrade\r\n\r\n", connection: Upgrade\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert!(req.upgrade()); assert!(req.upgrade());
assert_eq!(req.head().connection_type(), ConnectionType::Upgrade); assert_eq!(req.head().connection_type(), ConnectionType::Upgrade);
@ -966,59 +943,62 @@ mod tests {
#[test] #[test]
fn test_conn_upgrade_connect_method() { 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\ "CONNECT /test HTTP/1.1\r\n\
content-type: text/plain\r\n\r\n", content-type: text/plain\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert!(req.upgrade()); assert!(req.upgrade());
} }
#[test] #[test]
fn test_headers_content_length_err_1() { fn test_headers_bad_content_length() {
let mut buf = BytesMut::from( // string CL
expect_parse_err!(&mut BytesMut::from(
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
content-length: line\r\n\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] #[test]
fn test_headers_content_length_err_2() { fn octal_ish_cl_parsed_as_decimal() {
let mut buf = BytesMut::from( let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\ "POST /test HTTP/1.1\r\n\
content-length: -1\r\n\r\n", content-length: 011\r\n\r\n",
); );
let mut reader = MessageDecoder::<Request>::default();
expect_parse_err!(&mut buf); let (_req, pl) = reader.decode(&mut buf).unwrap().unwrap();
assert!(matches!(
pl,
PayloadType::Payload(pl) if pl == PayloadDecoder::length(11)
));
} }
#[test] #[test]
fn test_invalid_header() { fn test_invalid_header() {
let mut buf = BytesMut::from( expect_parse_err!(&mut BytesMut::from(
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
test line\r\n\r\n", test line\r\n\r\n",
); ));
expect_parse_err!(&mut buf);
} }
#[test] #[test]
fn test_invalid_name() { fn test_invalid_name() {
let mut buf = BytesMut::from( expect_parse_err!(&mut BytesMut::from(
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
test[]: line\r\n\r\n", test[]: line\r\n\r\n",
); ));
expect_parse_err!(&mut buf);
} }
#[test] #[test]
fn test_http_request_bad_status_line() { fn test_http_request_bad_status_line() {
let mut buf = BytesMut::from("getpath \r\n\r\n"); expect_parse_err!(&mut BytesMut::from("getpath \r\n\r\n"));
expect_parse_err!(&mut buf);
} }
#[test] #[test]
@ -1058,11 +1038,10 @@ mod tests {
#[test] #[test]
fn test_http_request_parser_utf8() { 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\ "GET /test HTTP/1.1\r\n\
x-test: тест\r\n\r\n", x-test: тест\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!( assert_eq!(
req.headers().get("x-test").unwrap().as_bytes(), req.headers().get("x-test").unwrap().as_bytes(),
@ -1072,24 +1051,18 @@ mod tests {
#[test] #[test]
fn test_http_request_parser_two_slashes() { 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 BytesMut::from("GET //path HTTP/1.1\r\n\r\n"));
let req = parse_ready!(&mut buf);
assert_eq!(req.path(), "//path"); assert_eq!(req.path(), "//path");
} }
#[test] #[test]
fn test_http_request_parser_bad_method() { 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 BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n"));
expect_parse_err!(&mut buf);
} }
#[test] #[test]
fn test_http_request_parser_bad_version() { fn test_http_request_parser_bad_version() {
let mut buf = BytesMut::from("GET //get HT/11\r\n\r\n"); expect_parse_err!(&mut BytesMut::from("GET //get HT/11\r\n\r\n"));
expect_parse_err!(&mut buf);
} }
#[test] #[test]
@ -1106,47 +1079,41 @@ mod tests {
#[test] #[test]
fn hrs_multiple_content_length() { fn hrs_multiple_content_length() {
let mut buf = BytesMut::from( expect_parse_err!(&mut BytesMut::from(
"GET / HTTP/1.1\r\n\ "GET / HTTP/1.1\r\n\
Host: example.com\r\n\ Host: example.com\r\n\
Content-Length: 4\r\n\ Content-Length: 4\r\n\
Content-Length: 2\r\n\ Content-Length: 2\r\n\
\r\n\ \r\n\
abcd", abcd",
); ));
expect_parse_err!(&mut buf); expect_parse_err!(&mut BytesMut::from(
let mut buf = BytesMut::from(
"GET / HTTP/1.1\r\n\ "GET / HTTP/1.1\r\n\
Host: example.com\r\n\ Host: example.com\r\n\
Content-Length: 0\r\n\ Content-Length: 0\r\n\
Content-Length: 2\r\n\ Content-Length: 2\r\n\
\r\n\ \r\n\
ab", ab",
); ));
expect_parse_err!(&mut buf);
} }
#[test] #[test]
fn hrs_content_length_plus() { fn hrs_content_length_plus() {
let mut buf = BytesMut::from( expect_parse_err!(&mut BytesMut::from(
"GET / HTTP/1.1\r\n\ "GET / HTTP/1.1\r\n\
Host: example.com\r\n\ Host: example.com\r\n\
Content-Length: +3\r\n\ Content-Length: +3\r\n\
\r\n\ \r\n\
000", 000",
); ));
expect_parse_err!(&mut buf);
} }
#[test] #[test]
fn hrs_te_http10() { fn hrs_te_http10() {
// in HTTP/1.0 transfer encoding is ignored and must therefore contain a CL header // 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\ "POST / HTTP/1.0\r\n\
Host: example.com\r\n\ Host: example.com\r\n\
Transfer-Encoding: chunked\r\n\ Transfer-Encoding: chunked\r\n\
@ -1155,9 +1122,7 @@ mod tests {
aaa\r\n\ aaa\r\n\
0\r\n\ 0\r\n\
", ",
); ));
expect_parse_err!(&mut buf);
} }
#[test] #[test]

View File

@ -976,9 +976,11 @@ where
// //
// A Request head too large to parse is only checked on `httparse::Status::Partial`. // A Request head too large to parse is only checked on `httparse::Status::Partial`.
if this.payload.is_none() { match this.payload {
// When dispatcher has a payload the responsibility of wake up it would be shift // When dispatcher has a payload the responsibility of wake ups is shifted to
// to h1::payload::Payload. // `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: // Reason:
// Self wake up when there is payload would waste poll and/or result in // 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 // read anymore. At this case read_buf could always remain beyond
// MAX_BUFFER_SIZE and self wake up would be busy poll dispatcher and // MAX_BUFFER_SIZE and self wake up would be busy poll dispatcher and
// waste resources. // 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); return Ok(false);

View File

@ -16,7 +16,7 @@ use crate::error::PayloadError;
/// max buffer size 32k /// max buffer size 32k
pub(crate) const MAX_BUFFER_SIZE: usize = 32_768; pub(crate) const MAX_BUFFER_SIZE: usize = 32_768;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Eq)]
pub enum PayloadStatus { pub enum PayloadStatus {
Read, Read,
Pause, Pause,
@ -252,19 +252,15 @@ impl Inner {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::panic::{RefUnwindSafe, UnwindSafe};
use actix_utils::future::poll_fn; use actix_utils::future::poll_fn;
use static_assertions::{assert_impl_all, assert_not_impl_any}; use static_assertions::{assert_impl_all, assert_not_impl_any};
use super::*; use super::*;
assert_impl_all!(Payload: Unpin); 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); assert_impl_all!(Inner: Unpin, Send, Sync);
// assertion not stable wrt rustc versions yet
// assert_impl_all!(Inner: UnwindSafe, RefUnwindSafe);
#[actix_rt::test] #[actix_rt::test]
async fn test_unread_data() { async fn test_unread_data() {

View File

@ -67,7 +67,7 @@ where
timer timer
}) })
.unwrap_or_else(|| Box::pin(sleep(dur))), .unwrap_or_else(|| Box::pin(sleep(dur))),
on_flight: false, in_flight: false,
ping_pong: conn.ping_pong().unwrap(), ping_pong: conn.ping_pong().unwrap(),
}); });
@ -84,9 +84,14 @@ where
} }
struct H2PingPong { struct H2PingPong {
timer: Pin<Box<Sleep>>, /// Handle to send ping frames from the peer.
on_flight: bool,
ping_pong: PingPong, 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<Box<Sleep>>,
} }
impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U> impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U>
@ -152,26 +157,28 @@ where
}); });
} }
Poll::Ready(None) => return Poll::Ready(Ok(())), Poll::Ready(None) => return Poll::Ready(Ok(())),
Poll::Pending => match this.ping_pong.as_mut() { Poll::Pending => match this.ping_pong.as_mut() {
Some(ping_pong) => loop { Some(ping_pong) => loop {
if ping_pong.on_flight { if ping_pong.in_flight {
// When have on flight ping pong. poll pong and and keep alive timer. // When there is an in-flight ping-pong, poll pong and and keep-alive
// on success pong received update keep alive timer to determine the next timing of // timer. On successful pong received, update keep-alive timer to
// ping pong. // determine the next timing of ping pong.
match ping_pong.ping_pong.poll_pong(cx)? { match ping_pong.ping_pong.poll_pong(cx)? {
Poll::Ready(_) => { Poll::Ready(_) => {
ping_pong.on_flight = false; ping_pong.in_flight = false;
let dead_line = this.config.keep_alive_deadline().unwrap(); let dead_line = this.config.keep_alive_deadline().unwrap();
ping_pong.timer.as_mut().reset(dead_line.into()); ping_pong.timer.as_mut().reset(dead_line.into());
} }
Poll::Pending => { Poll::Pending => {
return ping_pong.timer.as_mut().poll(cx).map(|_| Ok(())) return ping_pong.timer.as_mut().poll(cx).map(|_| Ok(()));
} }
} }
} else { } else {
// When there is no on flight ping pong. keep alive timer is used to wait for next // When there is no in-flight ping-pong, keep-alive timer is used to
// timing of ping pong. Therefore at this point it serves as an interval instead. // 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)); ready!(ping_pong.timer.as_mut().poll(cx));
ping_pong.ping_pong.send_ping(Ping::opaque())?; ping_pong.ping_pong.send_ping(Ping::opaque())?;
@ -179,7 +186,7 @@ where
let dead_line = this.config.keep_alive_deadline().unwrap(); let dead_line = this.config.keep_alive_deadline().unwrap();
ping_pong.timer.as_mut().reset(dead_line.into()); ping_pong.timer.as_mut().reset(dead_line.into());
ping_pong.on_flight = true; ping_pong.in_flight = true;
} }
}, },
None => return Poll::Pending, None => return Poll::Pending,
@ -287,13 +294,13 @@ fn prepare_response(
_ => {} _ => {}
} }
let _ = match size { match size {
BodySize::None | BodySize::Stream => None, BodySize::None | BodySize::Stream => {}
BodySize::Sized(0) => { BodySize::Sized(0) => {
#[allow(clippy::declare_interior_mutable_const)] #[allow(clippy::declare_interior_mutable_const)]
const HV_ZERO: HeaderValue = HeaderValue::from_static("0"); 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) => { BodySize::Sized(len) => {
@ -302,7 +309,7 @@ fn prepare_response(
res.headers_mut().insert( res.headers_mut().insert(
CONTENT_LENGTH, CONTENT_LENGTH,
HeaderValue::from_str(buf.format(*len)).unwrap(), HeaderValue::from_str(buf.format(*len)).unwrap(),
) );
} }
}; };

View File

@ -103,11 +103,9 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::panic::{RefUnwindSafe, UnwindSafe};
use static_assertions::assert_impl_all; use static_assertions::assert_impl_all;
use super::*; use super::*;
assert_impl_all!(Payload: Unpin, Send, Sync, UnwindSafe, RefUnwindSafe); assert_impl_all!(Payload: Unpin, Send, Sync);
} }

View File

@ -12,7 +12,7 @@ use crate::header::{Charset, HTTP_VALUE};
/// - A character sequence representing the actual value (`value`), separated by single quotes. /// - 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). /// 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 { pub struct ExtendedValue {
/// The character set that is used to encode the `value` to a string. /// The character set that is used to encode the `value` to a string.
pub charset: Charset, pub charset: Charset,

View File

@ -147,7 +147,7 @@ mod tests {
// copy of encoding from actix-web headers // copy of encoding from actix-web headers
#[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt #[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt
#[derive(Clone, PartialEq, Debug)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Encoding { pub enum Encoding {
Chunked, Chunked,
Brotli, Brotli,

View File

@ -3,7 +3,7 @@ use std::{cell::RefCell, ops, rc::Rc};
use bitflags::bitflags; use bitflags::bitflags;
/// Represents various types of connection /// Represents various types of connection
#[derive(Copy, Clone, PartialEq, Debug)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionType { pub enum ConnectionType {
/// Close connection after response. /// Close connection after response.
Close, Close,

View File

@ -97,12 +97,10 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::panic::{RefUnwindSafe, UnwindSafe};
use static_assertions::{assert_impl_all, assert_not_impl_any}; use static_assertions::{assert_impl_all, assert_not_impl_any};
use super::*; use super::*;
assert_impl_all!(Payload: Unpin); assert_impl_all!(Payload: Unpin);
assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe); assert_not_impl_any!(Payload: Send, Sync);
} }

View File

@ -11,7 +11,7 @@ use super::{
}; };
/// A WebSocket message. /// A WebSocket message.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Eq)]
pub enum Message { pub enum Message {
/// Text message. /// Text message.
Text(ByteString), Text(ByteString),
@ -36,7 +36,7 @@ pub enum Message {
} }
/// A WebSocket frame. /// A WebSocket frame.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Eq)]
pub enum Frame { pub enum Frame {
/// Text frame. Note that the codec does not validate UTF-8 encoding. /// Text frame. Note that the codec does not validate UTF-8 encoding.
Text(Bytes), Text(Bytes),
@ -58,7 +58,7 @@ pub enum Frame {
} }
/// A WebSocket continuation item. /// A WebSocket continuation item.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Eq)]
pub enum Item { pub enum Item {
FirstText(Bytes), FirstText(Bytes),
FirstBinary(Bytes), FirstBinary(Bytes),

View File

@ -67,7 +67,7 @@ pub enum ProtocolError {
} }
/// WebSocket handshake errors /// WebSocket handshake errors
#[derive(Debug, Clone, Copy, PartialEq, Display, Error)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)]
pub enum HandshakeError { pub enum HandshakeError {
/// Only get method is allowed. /// Only get method is allowed.
#[display(fmt = "Method not allowed.")] #[display(fmt = "Method not allowed.")]

View File

@ -21,7 +21,7 @@ default = ["http"]
[dependencies] [dependencies]
bytestring = ">=0.1.5, <2" bytestring = ">=0.1.5, <2"
http = { version = "0.2.3", optional = true } http = { version = "0.2.5", optional = true }
regex = "1.5" regex = "1.5"
serde = "1" serde = "1"
tracing = { version = "0.1.30", default-features = false, features = ["log"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] }

View File

@ -27,7 +27,7 @@ impl<'a> ResourcePath for &'a str {
impl ResourcePath for bytestring::ByteString { impl ResourcePath for bytestring::ByteString {
fn path(&self) -> &str { fn path(&self) -> &str {
&*self self
} }
} }

View File

@ -1,6 +1,6 @@
use crate::{IntoPatterns, Resource, ResourceDef}; use crate::{IntoPatterns, Resource, ResourceDef};
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct ResourceId(pub u16); pub struct ResourceId(pub u16);
/// Resource router. /// Resource router.

View File

@ -1,6 +1,9 @@
# Changes # Changes
## Unreleased - 2022-xx-xx ## Unreleased - 2022-xx-xx
## 0.1.0 - 2022-07-24
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-test" name = "actix-test"
version = "0.1.0-beta.13" version = "0.1.0"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -30,7 +30,7 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-codec = "0.5" actix-codec = "0.5"
actix-http = "3" actix-http = "3"
actix-http-test = "3.0.0-beta.13" actix-http-test = "3"
actix-rt = "2.1" actix-rt = "2.1"
actix-service = "2" actix-service = "2"
actix-utils = "3" actix-utils = "3"

View File

@ -27,7 +27,7 @@ tokio = { version = "1.13.1", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.13" actix-test = "0.1"
awc = { version = "3", default-features = false } awc = { version = "3", default-features = false }
actix-web = { version = "4", features = ["macros"] } actix-web = { version = "4", features = ["macros"] }

View File

@ -1,8 +1,11 @@
# Changes # Changes
## Unreleased - 2022-xx-xx ## Unreleased - 2022-xx-xx
- Add `#[routes]` macro to support multiple paths for one handler. [#2718]
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. - 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 ## 4.0.1 - 2022-06-11
- Fix support for guard paths in route handler macros. [#2771] - Fix support for guard paths in route handler macros. [#2771]

View File

@ -18,12 +18,12 @@ proc-macro = true
actix-router = "0.5.0" actix-router = "0.5.0"
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
syn = { version = "1", features = ["full", "parsing"] } syn = { version = "1", features = ["full", "extra-traits"] }
[dev-dependencies] [dev-dependencies]
actix-macros = "0.2.3" actix-macros = "0.2.3"
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.13" actix-test = "0.1"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = "4.0.0" actix-web = "4.0.0"

View File

@ -46,9 +46,20 @@
//! ``` //! ```
//! //!
//! # Multiple Path Handlers //! # 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 //! [actix-web attributes docs]: https://docs.rs/actix-web/latest/actix_web/#attributes
//! [GET]: macro@get //! [GET]: macro@get
@ -104,6 +115,39 @@ pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(None, args, input) route::with_method(None, args, input)
} }
/// Creates resource handler, allowing multiple HTTP methods and paths.
///
/// # Syntax
/// ```plain
/// #[routes]
/// #[<method>("path", ...)]
/// #[<method>("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 { macro_rules! method_macro {
($variant:ident, $method:ident) => { ($variant:ident, $method:ident) => {
#[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")] #[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")]

View File

@ -3,24 +3,12 @@ use std::{collections::HashSet, convert::TryFrom};
use actix_router::ResourceDef; use actix_router::ResourceDef;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2}; use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote, ToTokens, TokenStreamExt}; use quote::{quote, ToTokens, TokenStreamExt};
use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, NestedMeta, Path}; use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, Meta, 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);
}
}
macro_rules! method_type { macro_rules! method_type {
( (
$($variant:ident, $upper:ident,)+ $($variant:ident, $upper:ident, $lower:ident,)+
) => { ) => {
#[derive(Debug, PartialEq, Eq, Hash)] #[derive(Debug, PartialEq, Eq, Hash)]
pub enum MethodType { pub enum MethodType {
@ -42,20 +30,27 @@ macro_rules! method_type {
_ => Err(format!("Unexpected HTTP method: `{}`", method)), _ => Err(format!("Unexpected HTTP method: `{}`", method)),
} }
} }
fn from_path(method: &Path) -> Result<Self, ()> {
match () {
$(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+
_ => Err(()),
}
}
} }
}; };
} }
method_type! { method_type! {
Get, GET, Get, GET, get,
Post, POST, Post, POST, post,
Put, PUT, Put, PUT, put,
Delete, DELETE, Delete, DELETE, delete,
Head, HEAD, Head, HEAD, head,
Connect, CONNECT, Connect, CONNECT, connect,
Options, OPTIONS, Options, OPTIONS, options,
Trace, TRACE, Trace, TRACE, trace,
Patch, PATCH, Patch, PATCH, patch,
} }
impl ToTokens for MethodType { impl ToTokens for MethodType {
@ -90,6 +85,18 @@ impl Args {
let mut wrappers = Vec::new(); let mut wrappers = Vec::new();
let mut methods = HashSet::new(); let mut methods = HashSet::new();
if args.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
format!(
r#"invalid service definition, expected #[{}("<path>")]"#,
method
.map_or("route", |it| it.as_str())
.to_ascii_lowercase()
),
));
}
let is_route_macro = method.is_none(); let is_route_macro = method.is_none();
if let Some(method) = method { if let Some(method) = method {
methods.insert(method); methods.insert(method);
@ -183,55 +190,27 @@ impl Args {
} }
pub struct Route { pub struct Route {
/// Name of the handler function being annotated.
name: syn::Ident, name: syn::Ident,
args: Args,
/// Args passed to routing macro.
///
/// When using `#[routes]`, this will contain args for each specific routing macro.
args: Vec<Args>,
/// AST of the handler function being annotated.
ast: syn::ItemFn, ast: syn::ItemFn,
resource_type: ResourceType,
/// The doc comment attributes to copy to generated struct, if any. /// The doc comment attributes to copy to generated struct, if any.
doc_attributes: Vec<syn::Attribute>, doc_attributes: Vec<syn::Attribute>,
} }
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 { impl Route {
pub fn new( pub fn new(
args: AttributeArgs, args: AttributeArgs,
ast: syn::ItemFn, ast: syn::ItemFn,
method: Option<MethodType>, method: Option<MethodType>,
) -> syn::Result<Self> { ) -> syn::Result<Self> {
if args.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
format!(
r#"invalid service definition, expected #[{}("<some path>")]"#,
method
.map_or("route", |it| it.as_str())
.to_ascii_lowercase()
),
));
}
let name = ast.sig.ident.clone(); let name = ast.sig.ident.clone();
// Try and pull out the doc comments so that we can reapply them to the generated struct. // Try and pull out the doc comments so that we can reapply them to the generated struct.
@ -244,6 +223,7 @@ impl Route {
.collect(); .collect();
let args = Args::new(args, method)?; let args = Args::new(args, method)?;
if args.methods.is_empty() { if args.methods.is_empty() {
return Err(syn::Error::new( return Err(syn::Error::new(
Span::call_site(), Span::call_site(),
@ -251,25 +231,44 @@ impl Route {
)); ));
} }
let resource_type = if ast.sig.asyncness.is_some() { if matches!(ast.sig.output, syn::ReturnType::Default) {
ResourceType::Async return Err(syn::Error::new_spanned(
} else { ast,
match ast.sig.output { "Function has no return type. Cannot be used as handler",
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],
syn::ReturnType::Type(_, ref typ) => guess_resource_type(typ.as_ref()), ast,
} doc_attributes,
}; })
}
fn multiple(args: Vec<Args>, ast: syn::ItemFn) -> syn::Result<Self> {
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 { Ok(Self {
name, name,
args, args,
ast, ast,
resource_type,
doc_attributes, doc_attributes,
}) })
} }
@ -280,38 +279,57 @@ impl ToTokens for Route {
let Self { let Self {
name, name,
ast, ast,
args: args,
Args { doc_attributes,
} = self;
let registrations: TokenStream2 = args
.iter()
.map(|args| {
let Args {
path, path,
resource_name, resource_name,
guards, guards,
wrappers, wrappers,
methods, methods,
}, } = args;
resource_type,
doc_attributes, let resource_name = resource_name
} = self; .as_ref()
let resource_name = resource_name .map_or_else(|| name.to_string(), LitStr::value);
.as_ref()
.map_or_else(|| name.to_string(), LitStr::value); let method_guards = {
let method_guards = { let mut others = methods.iter();
let mut others = methods.iter();
// unwrapping since length is checked to be at least one // unwrapping since length is checked to be at least one
let first = others.next().unwrap(); 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! { quote! {
.guard( let __resource = ::actix_web::Resource::new(#path)
::actix_web::guard::Any(::actix_web::guard::#first()) .name(#resource_name)
#(.or(::actix_web::guard::#others()))* #method_guards
) #(.guard(::actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))*
.to(#name);
::actix_web::dev::HttpServiceFactory::register(__resource, __config);
} }
} else { })
quote! { .collect();
.guard(::actix_web::guard::#first())
}
}
};
let stream = quote! { let stream = quote! {
#(#doc_attributes)* #(#doc_attributes)*
@ -321,14 +339,7 @@ impl ToTokens for Route {
impl ::actix_web::dev::HttpServiceFactory for #name { impl ::actix_web::dev::HttpServiceFactory for #name {
fn register(self, __config: &mut actix_web::dev::AppService) { fn register(self, __config: &mut actix_web::dev::AppService) {
#ast #ast
let __resource = ::actix_web::Resource::new(#path) #registrations
.name(#resource_name)
#method_guards
#(.guard(::actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))*
.#resource_type(#name);
::actix_web::dev::HttpServiceFactory::register(__resource, __config)
} }
} }
}; };
@ -357,6 +368,57 @@ pub(crate) fn with_method(
} }
} }
pub(crate) fn with_methods(input: TokenStream) -> TokenStream {
let mut ast = match syn::parse::<syn::ItemFn>(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::<Vec<_>, _>(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::<Result<Vec<_>, _>>()
{
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 `#[<method>(..)]` 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. /// 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 /// Returning the original input in addition to the error is good for IDEs which can gracefully

View File

@ -8,9 +8,11 @@ use actix_web::{
header::{HeaderName, HeaderValue}, header::{HeaderName, HeaderValue},
StatusCode, 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; use futures_core::future::LocalBoxFuture;
// Make sure that we can name function as 'config' // Make sure that we can name function as 'config'
@ -89,8 +91,41 @@ async fn route_test() -> impl Responder {
HttpResponse::Ok() 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")] #[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").is_ok());
assert!(req.url_for_static("custom_resource_name_test").is_err()); assert!(req.url_for_static("custom_resource_name_test").is_err());
HttpResponse::Ok() HttpResponse::Ok()
@ -201,6 +236,9 @@ async fn test_body() {
.service(patch_test) .service(patch_test)
.service(test_handler) .service(test_handler)
.service(route_test) .service(route_test)
.service(routes_overlapping_test)
.service(routes_overlapping_inaccessible_test)
.service(routes_test)
.service(custom_resource_name_test) .service(custom_resource_name_test)
.service(guard_test) .service(guard_test)
}); });
@ -258,6 +296,38 @@ async fn test_body() {
let response = request.send().await.unwrap(); let response = request.send().await.unwrap();
assert!(!response.status().is_success()); 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 request = srv.request(http::Method::GET, srv.url("/custom_resource_name"));
let response = request.send().await.unwrap(); let response = request.send().await.unwrap();
assert!(response.status().is_success()); assert!(response.status().is_success());

View File

@ -12,6 +12,10 @@ fn compile_macros() {
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs"); t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
t.compile_fail("tests/trybuild/route-malformed-path-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/docstring-ok.rs");
t.pass("tests/trybuild/test-runtime.rs"); t.pass("tests/trybuild/test-runtime.rs");

View File

@ -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));
}

View File

@ -0,0 +1,21 @@
error: invalid service definition, expected #[get("<path>")]
--> 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

View File

@ -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));
}

View File

@ -0,0 +1,15 @@
error: The #[routes] macro requires at least one `#[<method>(..)]` 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

View File

@ -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());
}

View File

@ -2,12 +2,14 @@
## Unreleased - 2022-xx-xx ## Unreleased - 2022-xx-xx
### Added ### Added
- Add `#[routes]` macro to support multiple paths for one handler. [#2718]
- Add `ServiceRequest::{parts, request}()` getter methods. [#2786] - Add `ServiceRequest::{parts, request}()` getter methods. [#2786]
- Add configuration options for TLS handshake timeout via `HttpServer::{rustls, openssl}_with_config` methods. [#2752] - Add configuration options for TLS handshake timeout via `HttpServer::{rustls, openssl}_with_config` methods. [#2752]
### Changed ### Changed
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. - 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 [#2752]: https://github.com/actix/actix-web/pull/2752
[#2786]: https://github.com/actix/actix-web/pull/2786 [#2786]: https://github.com/actix/actix-web/pull/2786

View File

@ -84,11 +84,12 @@ derive_more = "0.99.5"
encoding_rs = "0.8" encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }
http = "0.2.8"
itoa = "1" itoa = "1"
language-tags = "0.3" language-tags = "0.3"
once_cell = "1.5"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
once_cell = "1.5"
pin-project-lite = "0.2.7" pin-project-lite = "0.2.7"
regex = "1.5.5" regex = "1.5.5"
serde = "1.0" serde = "1.0"
@ -101,7 +102,7 @@ url = "2.1"
[dev-dependencies] [dev-dependencies]
actix-files = "0.6" 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"] } awc = { version = "3", features = ["openssl"] }
brotli = "3.3.3" brotli = "3.3.3"

View File

@ -118,7 +118,7 @@ impl<T: ?Sized> Deref for Data<T> {
impl<T: ?Sized> Clone for Data<T> { impl<T: ?Sized> Clone for Data<T> {
fn clone(&self) -> Data<T> { fn clone(&self) -> Data<T> {
Data(self.0.clone()) Data(Arc::clone(&self.0))
} }
} }

View File

@ -42,7 +42,7 @@ pub struct BlockingError;
impl ResponseError for crate::error::BlockingError {} impl ResponseError for crate::error::BlockingError {}
/// Errors which can occur when attempting to generate resource uri. /// 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] #[non_exhaustive]
pub enum UrlGenerationError { pub enum UrlGenerationError {
/// Resource not found. /// Resource not found.

View File

@ -10,9 +10,10 @@
//! - Browser conformance tests at: <http://greenbytes.de/tech/tc2231/> //! - Browser conformance tests at: <http://greenbytes.de/tech/tc2231/>
//! - IANA assignment: <http://www.iana.org/assignments/cont-disp/cont-disp.xhtml> //! - IANA assignment: <http://www.iana.org/assignments/cont-disp/cont-disp.xhtml>
use std::fmt::{self, Write};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use std::fmt::{self, Write};
use super::{ExtendedValue, Header, TryIntoHeaderValue, Writer}; use super::{ExtendedValue, Header, TryIntoHeaderValue, Writer};
use crate::http::header; 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. /// The implied disposition of the content of the HTTP body.
#[derive(Clone, Debug, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum DispositionType { pub enum DispositionType {
/// Inline implies default processing. /// Inline implies default processing.
Inline, Inline,
@ -78,7 +79,7 @@ impl<'a> From<&'a str> for DispositionType {
/// assert!(param.is_filename()); /// assert!(param.is_filename());
/// assert_eq!(param.as_filename().unwrap(), "sample.txt"); /// assert_eq!(param.as_filename().unwrap(), "sample.txt");
/// ``` /// ```
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, Eq)]
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
pub enum DispositionParam { pub enum DispositionParam {
/// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from /// 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 /// change to match local file system conventions if applicable, and do not use directory path
/// information that may be present. /// information that may be present.
/// See [RFC 2183 §2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3). /// 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 { pub struct ContentDisposition {
/// The disposition type /// The disposition type
pub disposition: DispositionType, pub disposition: DispositionType,

View File

@ -57,7 +57,7 @@ use crate::HttpMessage;
/// IfRange::Date(fetched.into()) /// IfRange::Date(fetched.into())
/// ); /// );
/// ``` /// ```
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum IfRange { pub enum IfRange {
/// The entity-tag the client has of the resource. /// The entity-tag the client has of the resource.
EntityTag(EntityTag), EntityTag(EntityTag),

View File

@ -224,10 +224,11 @@ macro_rules! common_header {
// List header, one or more items with "*" option // List header, one or more items with "*" option
($(#[$attrs:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => { ($(#[$attrs:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => {
$(#[$attrs])* $(#[$attrs])*
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum $id { pub enum $id {
/// Any value is a match /// Any value is a match
Any, Any,
/// Only the listed items are a match /// Only the listed items are a match
Items(Vec<$item>), Items(Vec<$item>),
} }

View File

@ -53,7 +53,7 @@ use super::{Header, HeaderName, HeaderValue, InvalidHeaderValue, TryIntoHeaderVa
/// builder.insert_header(Range::bytes(1, 100)); /// builder.insert_header(Range::bytes(1, 100));
/// builder.insert_header(Range::bytes_multi(vec![(1, 100), (200, 300)])); /// builder.insert_header(Range::bytes_multi(vec![(1, 100), (200, 300)]));
/// ``` /// ```
#[derive(PartialEq, Clone, Debug)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Range { pub enum Range {
/// Byte range. /// Byte range.
Bytes(Vec<ByteRangeSpec>), Bytes(Vec<ByteRangeSpec>),

View File

@ -2,7 +2,6 @@ use std::{convert::Infallible, net::SocketAddr};
use actix_utils::future::{err, ok, Ready}; use actix_utils::future::{err, ok, Ready};
use derive_more::{Display, Error}; use derive_more::{Display, Error};
use once_cell::sync::Lazy;
use crate::{ use crate::{
dev::{AppConfig, Payload, RequestHead}, dev::{AppConfig, Payload, RequestHead},
@ -13,12 +12,9 @@ use crate::{
FromRequest, HttpRequest, ResponseError, FromRequest, HttpRequest, ResponseError,
}; };
static X_FORWARDED_FOR: Lazy<HeaderName> = static X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
Lazy::new(|| HeaderName::from_static("x-forwarded-for")); static X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
static X_FORWARDED_HOST: Lazy<HeaderName> = static X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
Lazy::new(|| HeaderName::from_static("x-forwarded-host"));
static X_FORWARDED_PROTO: Lazy<HeaderName> =
Lazy::new(|| HeaderName::from_static("x-forwarded-proto"));
/// Trim whitespace then any quote marks. /// Trim whitespace then any quote marks.
fn unquote(val: &str) -> &str { fn unquote(val: &str) -> &str {
@ -117,21 +113,21 @@ impl ConnectionInfo {
} }
let scheme = scheme 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(|| req.uri.scheme().map(Scheme::as_str))
.or_else(|| Some("https").filter(|_| cfg.secure())) .or_else(|| Some("https").filter(|_| cfg.secure()))
.unwrap_or("http") .unwrap_or("http")
.to_owned(); .to_owned();
let host = host 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.headers.get(&header::HOST)?.to_str().ok())
.or_else(|| req.uri.authority().map(Authority::as_str)) .or_else(|| req.uri.authority().map(Authority::as_str))
.unwrap_or_else(|| cfg.host()) .unwrap_or_else(|| cfg.host())
.to_owned(); .to_owned();
let realip_remote_addr = realip_remote_addr 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); .map(str::to_owned);
let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string()); let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string());

View File

@ -132,6 +132,7 @@ macro_rules! codegen_reexport {
codegen_reexport!(main); codegen_reexport!(main);
codegen_reexport!(test); codegen_reexport!(test);
codegen_reexport!(route); codegen_reexport!(route);
codegen_reexport!(routes);
codegen_reexport!(head); codegen_reexport!(head);
codegen_reexport!(get); codegen_reexport!(get);
codegen_reexport!(post); codegen_reexport!(post);

View File

@ -220,32 +220,25 @@ static SUPPORTED_ENCODINGS_STRING: Lazy<String> = Lazy::new(|| {
encoding.join(", ") encoding.join(", ")
}); });
static SUPPORTED_ENCODINGS: Lazy<Vec<Encoding>> = Lazy::new(|| { static SUPPORTED_ENCODINGS: &[Encoding] = &[
let mut encodings = vec![Encoding::identity()]; Encoding::identity(),
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
{ {
encodings.push(Encoding::brotli()); Encoding::brotli()
} },
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
{ {
encodings.push(Encoding::gzip()); Encoding::gzip()
encodings.push(Encoding::deflate()); },
} #[cfg(feature = "compress-gzip")]
{
Encoding::deflate()
},
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
{ {
encodings.push(Encoding::zstd()); Encoding::zstd()
} },
];
assert!(
!encodings.is_empty(),
"encodings can not be empty unless __compress feature has been explicitly enabled by itself"
);
encodings
});
// move cfg(feature) to prevents_double_compressing if more tests are added // move cfg(feature) to prevents_double_compressing if more tests are added
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
@ -326,6 +319,7 @@ mod tests {
.to_request(); .to_request();
let res = test::call_service(&app, req).await; let res = test::call_service(&app, req).await;
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
#[allow(clippy::mutable_key_type)]
let vary_headers = res.headers().get_all(header::VARY).collect::<HashSet<_>>(); let vary_headers = res.headers().get_all(header::VARY).collect::<HashSet<_>>();
assert!(vary_headers.contains(&HeaderValue::from_static("x-test"))); assert!(vary_headers.contains(&HeaderValue::from_static("x-test")));
assert!(vary_headers.contains(&HeaderValue::from_static("accept-encoding"))); assert!(vary_headers.contains(&HeaderValue::from_static("accept-encoding")));

View File

@ -253,7 +253,7 @@ impl HttpRequest {
#[inline] #[inline]
pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> { pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> {
if !self.extensions().contains::<ConnectionInfo>() { if !self.extensions().contains::<ConnectionInfo>() {
let info = ConnectionInfo::new(self.head(), &*self.app_config()); let info = ConnectionInfo::new(self.head(), self.app_config());
self.extensions_mut().insert(info); self.extensions_mut().insert(info);
} }

View File

@ -401,6 +401,8 @@ where
.into_factory() .into_factory()
.map_err(|err| err.into().error_response()); .map_err(|err| err.into().error_response());
// false positive lint (?)
#[allow(clippy::significant_drop_in_scrutinee)]
let acceptor_config = match c.tls_handshake_timeout { let acceptor_config = match c.tls_handshake_timeout {
Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur), Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur),
None => TlsAcceptorConfig::default(), None => TlsAcceptorConfig::default(),

View File

@ -73,7 +73,7 @@ use crate::{
/// } /// }
/// } /// }
/// ``` /// ```
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Eq)]
pub enum Either<L, R> { pub enum Either<L, R> {
/// A value of type `L`. /// A value of type `L`.
Left(L), Left(L),

View File

@ -134,6 +134,7 @@ where
/// ``` /// ```
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct PathConfig { pub struct PathConfig {
#[allow(clippy::type_complexity)]
err_handler: Option<Arc<dyn Fn(PathError, &HttpRequest) -> Error + Send + Sync>>, err_handler: Option<Arc<dyn Fn(PathError, &HttpRequest) -> Error + Send + Sync>>,
} }

View File

@ -169,6 +169,7 @@ impl<T: DeserializeOwned> FromRequest for Query<T> {
/// ``` /// ```
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct QueryConfig { pub struct QueryConfig {
#[allow(clippy::type_complexity)]
err_handler: Option<Arc<dyn Fn(QueryPayloadError, &HttpRequest) -> Error + Send + Sync>>, err_handler: Option<Arc<dyn Fn(QueryPayloadError, &HttpRequest) -> Error + Send + Sync>>,
} }

View File

@ -94,9 +94,9 @@ trust-dns-resolver = { version = "0.21", optional = true }
[dev-dependencies] [dev-dependencies]
actix-http = { version = "3", features = ["openssl"] } 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-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-tls = { version = "3", features = ["openssl", "rustls"] }
actix-utils = "3" actix-utils = "3"
actix-web = { version = "4", features = ["openssl"] } actix-web = { version = "4", features = ["openssl"] }

View File

@ -98,7 +98,7 @@ rm -f $README_FILE.bak
echo "manifest, changelog, and readme updated" echo "manifest, changelog, and readme updated"
echo echo
echo "check other references:" echo "check other references:"
rg --glob='**/Cargo.toml' "\ rg --glob='**/{Cargo.toml,README.md}' "\
${PACKAGE_NAME} ?= ?\"[^\"]+\"\ ${PACKAGE_NAME} ?= ?\"[^\"]+\"\
|${PACKAGE_NAME} ?=.*version ?= ?\"([^\"]+)\"\ |${PACKAGE_NAME} ?=.*version ?= ?\"([^\"]+)\"\
|package ?= ?\"${PACKAGE_NAME}\".*version ?= ?\"([^\"]+)\"\ |package ?= ?\"${PACKAGE_NAME}\".*version ?= ?\"([^\"]+)\"\