Merge branch 'main' into feat-files-array-support

This commit is contained in:
LBS 2026-03-01 15:00:03 +08:00 committed by GitHub
commit c55a37a6da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 599 additions and 178 deletions

View File

@ -49,7 +49,7 @@ jobs:
toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25
uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -83,7 +83,7 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2
- name: Install just, cargo-hack
uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25
uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7
with:
tool: just,cargo-hack

View File

@ -64,7 +64,7 @@ jobs:
toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25
uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -79,7 +79,7 @@ jobs:
run: just check-default
- name: tests
timeout-minutes: 60
timeout-minutes: 30
run: just test
- name: CI cache clean
@ -101,7 +101,7 @@ jobs:
toolchain: nightly
- name: tests (io-uring)
timeout-minutes: 60
timeout-minutes: 30
run: >
sudo bash -c "ulimit -Sl 512 && ulimit -Hl 512 && PATH=$PATH:/usr/share/rust/.cargo/bin && RUSTUP_TOOLCHAIN=stable cargo test --lib --tests -p=actix-files --all-features"
@ -117,7 +117,7 @@ jobs:
toolchain: nightly
- name: Install just
uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25
uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7
with:
tool: just

View File

@ -24,7 +24,7 @@ jobs:
components: llvm-tools
- name: Install just, cargo-llvm-cov, cargo-nextest
uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25
uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7
with:
tool: just,cargo-llvm-cov,cargo-nextest

View File

@ -77,12 +77,12 @@ jobs:
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
- name: Install just
uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25
uses: taiki-e/install-action@f92912fad184299a31e22ad070a5059fd07d4f59 # v2.68.7
with:
tool: just
- name: Install cargo-check-external-types
uses: taiki-e/cache-cargo-install-action@34ce5120836e5f9f1508d8713d7fdea0e8facd6f # v3.0.1
uses: taiki-e/cache-cargo-install-action@2bfc3cedaf2ee5e7fa5d0ae034ccd5fb50cf8e1f # v3.0.2
with:
tool: cargo-check-external-types

143
Cargo.lock generated
View File

@ -2,36 +2,13 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "actix"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b"
dependencies = [
"actix-rt",
"bitflags 2.10.0",
"bytes",
"crossbeam-channel",
"futures-core",
"futures-sink",
"futures-task",
"futures-util",
"log",
"once_cell",
"parking_lot",
"pin-project-lite",
"smallvec",
"tokio",
"tokio-util",
]
[[package]]
name = "actix-codec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"bytes",
"futures-core",
"futures-sink",
@ -53,7 +30,7 @@ dependencies = [
"actix-test",
"actix-utils",
"actix-web",
"bitflags 2.10.0",
"bitflags 2.11.0",
"bytes",
"derive_more",
"env_logger",
@ -72,7 +49,7 @@ dependencies = [
[[package]]
name = "actix-http"
version = "3.11.2"
version = "3.12.0"
dependencies = [
"actix-codec",
"actix-http-test",
@ -83,8 +60,9 @@ dependencies = [
"actix-utils",
"actix-web",
"async-stream",
"awc",
"base64 0.22.1",
"bitflags 2.10.0",
"bitflags 2.11.0",
"brotli",
"bytes",
"bytestring",
@ -225,7 +203,7 @@ dependencies = [
[[package]]
name = "actix-router"
version = "0.5.3"
version = "0.5.4"
dependencies = [
"bytestring",
"cfg-if",
@ -346,7 +324,7 @@ dependencies = [
[[package]]
name = "actix-web"
version = "4.12.1"
version = "4.13.0"
dependencies = [
"actix-codec",
"actix-files",
@ -404,28 +382,6 @@ dependencies = [
"zstd",
]
[[package]]
name = "actix-web-actors"
version = "4.3.1+deprecated"
dependencies = [
"actix",
"actix-codec",
"actix-http",
"actix-rt",
"actix-test",
"actix-web",
"awc",
"bytes",
"bytestring",
"env_logger",
"futures-core",
"futures-util",
"mime",
"pin-project-lite",
"tokio",
"tokio-util",
]
[[package]]
name = "actix-web-codegen"
version = "4.3.0"
@ -612,7 +568,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "awc"
version = "3.8.1"
version = "3.8.2"
dependencies = [
"actix-codec",
"actix-http",
@ -701,9 +657,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "block-buffer"
@ -1221,9 +1177,9 @@ dependencies = [
[[package]]
name = "env_filter"
version = "0.1.4"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f"
dependencies = [
"log",
"regex",
@ -1231,9 +1187,9 @@ dependencies = [
[[package]]
name = "env_logger"
version = "0.11.8"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d"
dependencies = [
"anstream",
"anstyle",
@ -1344,15 +1300,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-executor"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
dependencies = [
"futures-core",
"futures-task",
@ -1361,15 +1317,15 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-macro"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
@ -1378,21 +1334,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]]
name = "futures-task"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-test"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5961fb6311645f46e2cdc2964a8bfae6743fd72315eaec181a71ae3eb2467113"
checksum = "32d24b40cb9018c6b0f9d891b74a86a777d5db37972a115016d1150257b1c793"
dependencies = [
"futures-core",
"futures-executor",
@ -1406,9 +1362,9 @@ dependencies = [
[[package]]
name = "futures-util"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-io",
@ -1416,7 +1372,6 @@ dependencies = [
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
@ -1875,7 +1830,7 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"libc",
"redox_syscall 0.7.1",
]
@ -2061,7 +2016,7 @@ version = "0.10.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"cfg-if",
"foreign-types",
"libc",
@ -2164,12 +2119,6 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.32"
@ -2377,7 +2326,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
]
[[package]]
@ -2386,7 +2335,7 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
]
[[package]]
@ -2474,7 +2423,7 @@ version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"errno",
"libc",
"linux-raw-sys",
@ -2646,7 +2595,7 @@ version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"core-foundation",
"core-foundation-sys",
"libc",
@ -2900,9 +2849,9 @@ checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b"
[[package]]
name = "tempfile"
version = "3.24.0"
version = "3.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
dependencies = [
"fastrand",
"getrandom 0.3.4",
@ -3158,9 +3107,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.11+spec-1.1.0"
version = "1.0.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
checksum = "bbe30f93627849fa362d4a602212d41bb237dc2bd0f8ba0b2ce785012e124220"
dependencies = [
"indexmap",
"serde_core",
@ -3173,18 +3122,18 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.7.5+spec-1.1.0"
version = "1.0.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.0.6+spec-1.1.0"
version = "1.0.8+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc"
dependencies = [
"winnow",
]
@ -3229,9 +3178,9 @@ dependencies = [
[[package]]
name = "trybuild"
version = "1.0.115"
version = "1.0.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f614c21bd3a61bad9501d75cbb7686f00386c806d7f456778432c25cf86948a"
checksum = "47c635f0191bd3a2941013e5062667100969f8c4e9cd787c14f977265d73616e"
dependencies = [
"glob",
"serde",

View File

@ -8,7 +8,6 @@ members = [
"actix-multipart-derive",
"actix-router",
"actix-test",
"actix-web-actors",
"actix-web-codegen",
"actix-web",
"awc",
@ -39,7 +38,6 @@ actix-multipart-derive = { path = "actix-multipart-derive" }
actix-router = { path = "actix-router" }
actix-test = { path = "actix-test" }
actix-web = { path = "actix-web" }
actix-web-actors = { path = "actix-web-actors" }
actix-web-codegen = { path = "actix-web-codegen" }
awc = { path = "awc" }

View File

@ -45,7 +45,7 @@ pub struct HttpRange {
impl HttpRange {
/// Parses Range HTTP header string as per RFC 2616.
///
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
/// `header` is HTTP Range header (e.g. `bytes=0-9`).
/// `size` is full size of response (file).
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> {
let ranges =

View File

@ -2,17 +2,25 @@
## Unreleased
- Encode the HTTP/1 `Connection: Upgrade` header in Camel-Case when camel-case header formatting is enabled.[#3953]
[#3953]: https://github.com/actix/actix-web/pull/3953
## 3.12.0
- Minimum supported Rust version (MSRV) is now 1.88.
- Increase default HTTP/2 flow control window sizes. [#3638]
- Expose configuration methods to improve upload throughput. [#3638]
- Fix truncated body ending without error when connection closed abnormally. [#3067]
- Add config/method for `TCP_NODELAY`. [#3918]
- Do not compress 206 Partial Content responses. [#3191]
- Fix lingering sockets and client stalls when responding early to dropped chunked request payloads. [#2972]
[#3638]: https://github.com/actix/actix-web/issues/3638
[#3067]: https://github.com/actix/actix-web/pull/3067
[#3918]: https://github.com/actix/actix-web/pull/3918
[#3191]: https://github.com/actix/actix-web/issues/3191
[#2972]: https://github.com/actix/actix-web/issues/2972
## 3.11.2

View File

@ -1,6 +1,6 @@
[package]
name = "actix-http"
version = "3.11.2"
version = "3.12.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
description = "HTTP types and services for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"]
@ -139,6 +139,7 @@ actix-http-test = { version = "3", features = ["openssl"] }
actix-server = "2"
actix-tls = { version = "3.4", features = ["openssl", "rustls-0_23-webpki-roots"] }
actix-web = "4"
awc = { version = "3", default-features = false, features = ["openssl"] }
async-stream = "0.3"
criterion = { version = "0.5", features = ["html_reports"] }

View File

@ -5,11 +5,11 @@
<!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.11.2)](https://docs.rs/actix-http/3.11.2)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.12.0)](https://docs.rs/actix-http/3.12.0)
![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-http/3.11.2/status.svg)](https://deps.rs/crate/actix-http/3.11.2)
[![dependency status](https://deps.rs/crate/actix-http/3.12.0/status.svg)](https://deps.rs/crate/actix-http/3.12.0)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -31,7 +31,7 @@ use crate::{
config::ServiceConfig,
error::{DispatchError, ParseError, PayloadError},
service::HttpFlow,
Error, Extensions, OnConnectData, Request, Response, StatusCode,
Error, Extensions, HttpMessage, OnConnectData, Request, Response, StatusCode,
};
const LW_BUFFER_SIZE: usize = 1024;
@ -157,6 +157,8 @@ pin_project! {
pub(super) state: State<S, B, X>,
// when Some(_) dispatcher is in state of receiving request payload
payload: Option<PayloadSender>,
// true when current request uses chunked transfer encoding (drainable when payload is dropped)
payload_drainable: bool,
messages: VecDeque<DispatcherMessage>,
head_timer: TimerState,
@ -269,6 +271,7 @@ where
state: State::None,
payload: None,
payload_drainable: false,
messages: VecDeque::new(),
head_timer: TimerState::new(config.client_request_deadline().is_some()),
@ -308,7 +311,10 @@ where
if self.flags.contains(Flags::READ_DISCONNECT) {
false
} else if let Some(ref info) = self.payload {
info.need_read(cx) == PayloadStatus::Read
matches!(
info.need_read(cx),
PayloadStatus::Read | PayloadStatus::Dropped
)
} else {
true
}
@ -387,8 +393,10 @@ where
this.state.set(match size {
BodySize::None | BodySize::Sized(0) => {
let payload_unfinished = this.payload.is_some();
let drain_payload = this.payload.as_ref().is_some_and(|pl| pl.is_dropped())
&& *this.payload_drainable;
if payload_unfinished {
if payload_unfinished && !drain_payload {
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
} else {
this.flags.insert(Flags::FINISHED);
@ -412,8 +420,10 @@ where
this.state.set(match size {
BodySize::None | BodySize::Sized(0) => {
let payload_unfinished = this.payload.is_some();
let drain_payload = this.payload.as_ref().is_some_and(|pl| pl.is_dropped())
&& *this.payload_drainable;
if payload_unfinished {
if payload_unfinished && !drain_payload {
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
} else {
this.flags.insert(Flags::FINISHED);
@ -469,8 +479,11 @@ where
// all messages are dealt with
None => {
// start keep-alive if last request allowed it
this.flags.set(Flags::KEEP_ALIVE, this.codec.keep_alive());
// start keep-alive only if request payload is fully read/drained
this.flags.set(
Flags::KEEP_ALIVE,
this.payload.is_none() && this.codec.keep_alive(),
);
return Ok(PollResponse::DoNothing);
}
@ -522,13 +535,16 @@ where
// responding to. We can check to see if we finished reading it
// yet, and if not, shutdown the connection.
let payload_unfinished = this.payload.is_some();
let drain_payload =
this.payload.as_ref().is_some_and(|pl| pl.is_dropped())
&& *this.payload_drainable;
let not_pipelined = this.messages.is_empty();
// payload stream finished.
// set state to None and handle next message
this.state.set(State::None);
if not_pipelined && payload_unfinished {
if not_pipelined && payload_unfinished && !drain_payload {
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
} else {
this.flags.insert(Flags::FINISHED);
@ -573,13 +589,16 @@ where
// responding to. We can check to see if we finished reading it
// yet, and if not, shutdown the connection.
let payload_unfinished = this.payload.is_some();
let drain_payload =
this.payload.as_ref().is_some_and(|pl| pl.is_dropped())
&& *this.payload_drainable;
let not_pipelined = this.messages.is_empty();
// payload stream finished.
// set state to None and handle next message
this.state.set(State::None);
if not_pipelined && payload_unfinished {
if not_pipelined && payload_unfinished && !drain_payload {
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
} else {
this.flags.insert(Flags::FINISHED);
@ -748,12 +767,13 @@ where
match this.codec.message_type() {
// request has no payload
MessageType::None => {}
MessageType::None => *this.payload_drainable = false,
// Request is upgradable. Add upgrade message and break.
// Everything remaining in read buffer will be handed to
// upgraded Request.
MessageType::Stream if this.flow.upgrade.is_some() => {
*this.payload_drainable = false;
this.messages.push_back(DispatcherMessage::Upgrade(req));
break;
}
@ -768,6 +788,7 @@ where
let (sender, payload) = Payload::create(false);
*req.payload() = crate::Payload::H1 { payload };
*this.payload = Some(sender);
*this.payload_drainable = req.chunked().unwrap_or(false);
}
}
@ -797,6 +818,7 @@ where
Message::Chunk(None) => {
if let Some(mut payload) = this.payload.take() {
payload.feed_eof();
*this.payload_drainable = false;
} else {
error!("Internal server error: unexpected eof");
this.flags.insert(Flags::READ_DISCONNECT);
@ -999,23 +1021,14 @@ where
//
// A Request head too large to parse is only checked on `httparse::Status::Partial`.
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
// over read.
//
// Case:
// When payload is (partial) dropped by user there is no need to do
// read anymore. At this case read_buf could always remain beyond
// MAX_BUFFER_SIZE and self wake up would be busy poll dispatcher and
// waste resources.
Some(ref p) if p.need_read(cx) != PayloadStatus::Read => {}
_ => cx.waker().wake_by_ref(),
match this.payload.as_ref().map(|p| p.need_read(cx)) {
// Payload consumer is alive but applying backpressure. Wait for its waker.
Some(PayloadStatus::Pause) => {}
// Consumer dropped means drain/discard mode; keep polling to make progress.
Some(PayloadStatus::Dropped) | Some(PayloadStatus::Read) | None => {
cx.waker().wake_by_ref()
}
}
return Ok(false);
@ -1029,7 +1042,11 @@ where
match tokio_util::io::poll_read_buf(io.as_mut(), cx, this.read_buf) {
Poll::Ready(Ok(n)) => {
this.flags.remove(Flags::FINISHED);
// When draining a dropped request payload, keep FINISHED set so the
// disconnect/keep-alive decision can be made once the payload is fully drained.
if !this.payload.as_ref().is_some_and(|pl| pl.is_dropped()) {
this.flags.remove(Flags::FINISHED);
}
if n == 0 {
return Ok(true);
@ -1244,6 +1261,7 @@ where
// disconnect if keep-alive is not enabled
if inner_p.flags.contains(Flags::FINISHED)
&& !inner_p.flags.contains(Flags::KEEP_ALIVE)
&& inner_p.payload.is_none()
{
inner_p.flags.remove(Flags::FINISHED);
inner_p.flags.insert(Flags::SHUTDOWN);

View File

@ -1017,6 +1017,128 @@ async fn handler_drop_payload() {
.await;
}
// Handler drops request payload without reading it. Server should keep reading and discarding the
// rest of the request body so clients that do not read the response until they've finished
// writing the request (like `requests` in Python) do not deadlock.
// ref. https://github.com/actix/actix-web/issues/2972
#[actix_rt::test]
async fn handler_drop_payload_drains_body() {
let _ = env_logger::try_init();
let mut buf = TestSeqBuffer::new(http_msg(
r"
POST /drop-payload HTTP/1.1
Transfer-Encoding: chunked
",
));
let services = HttpFlow::new(
drop_payload_service(),
ExpectHandler,
None::<UpgradeHandler>,
);
let h1 = Dispatcher::new(
buf.clone(),
services,
ServiceConfig::default(),
None,
OnConnectData::default(),
);
pin!(h1);
lazy(|cx| {
assert!(h1.as_mut().poll(cx).is_pending());
let mut res = BytesMut::from(buf.take_write_buf().as_ref());
stabilize_date_header(&mut res);
let res = &res[..];
let exp = http_msg(
r"
HTTP/1.1 200 OK
content-length: 15
date: Thu, 01 Jan 1970 12:34:56 UTC
payload dropped
",
);
assert_eq!(
res,
exp,
"\nexpected response not in write buffer:\n\
response: {:?}\n\
expected: {:?}",
String::from_utf8_lossy(res),
String::from_utf8_lossy(&exp)
);
})
.await;
// stream a body larger than the dispatcher read buffer limit; it should still be drained
// (read + decoded + discarded) without stalling.
for _ in 0..32 {
let data = vec![b'a'; 8192];
let mut chunk = BytesMut::new();
chunk.extend_from_slice(format!("{:x}\r\n", data.len()).as_bytes());
chunk.extend_from_slice(&data);
chunk.extend_from_slice(b"\r\n");
buf.extend_read_buf(chunk);
lazy(|cx| {
assert!(h1.as_mut().poll(cx).is_pending());
assert!(buf.take_write_buf().is_empty());
assert!(buf.read_buf().is_empty());
})
.await;
}
// terminating chunk
buf.extend_read_buf(b"0\r\n\r\n");
lazy(|cx| {
assert!(h1.as_mut().poll(cx).is_pending());
assert!(buf.take_write_buf().is_empty());
assert!(buf.read_buf().is_empty());
})
.await;
// connection should be able to accept another request after draining the previous body
buf.extend_read_buf(http_msg("GET /drop-payload HTTP/1.1"));
lazy(|cx| {
assert!(h1.as_mut().poll(cx).is_pending());
let mut res = BytesMut::from(buf.take_write_buf().as_ref());
stabilize_date_header(&mut res);
let res = &res[..];
let exp = http_msg(
r"
HTTP/1.1 200 OK
content-length: 15
date: Thu, 01 Jan 1970 12:34:56 UTC
payload dropped
",
);
assert_eq!(
res,
exp,
"\nexpected response not in write buffer:\n\
response: {:?}\n\
expected: {:?}",
String::from_utf8_lossy(res),
String::from_utf8_lossy(&exp)
);
})
.await;
}
#[actix_rt::test]
async fn allow_half_closed() {
let buf = TestSeqBuffer::new(http_msg("GET / HTTP/1.1"));

View File

@ -111,7 +111,13 @@ pub(crate) trait MessageType: Sized {
// Connection
match conn_type {
ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"),
ConnectionType::Upgrade => {
if camel_case {
dst.put_slice(b"Connection: Upgrade\r\n")
} else {
dst.put_slice(b"connection: upgrade\r\n")
}
}
ConnectionType::KeepAlive if version < Version::HTTP_11 => {
if camel_case {
dst.put_slice(b"Connection: keep-alive\r\n")
@ -580,6 +586,16 @@ mod tests {
assert!(data.contains("Date: date\r\n"));
assert!(data.contains("Upgrade-Insecure-Requests: 1\r\n"));
let _ = head.encode_headers(
&mut bytes,
Version::HTTP_11,
BodySize::None,
ConnectionType::Upgrade,
&ServiceConfig::default(),
);
let data = String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
assert!(data.contains("Connection: Upgrade\r\n"));
let _ = head.encode_headers(
&mut bytes,
Version::HTTP_11,

View File

@ -133,6 +133,11 @@ impl PayloadSender {
PayloadStatus::Dropped
}
}
#[inline]
pub fn is_dropped(&self) -> bool {
self.inner.strong_count() == 0
}
}
#[derive(Debug)]

View File

@ -61,14 +61,15 @@ impl RequestHead {
&mut self.headers
}
/// Is to uppercase headers with Camel-Case.
/// Default is `false`
/// Returns whether headers should be sent in Camel-Case.
///
/// Default is `false`.
#[inline]
pub fn camel_case_headers(&self) -> bool {
self.flags.contains(Flags::CAMEL_CASE)
}
/// Set `true` to send headers which are formatted as Camel-Case.
/// Sets whether to send headers formatted as Camel-Case.
#[inline]
pub fn set_camel_case_headers(&mut self, val: bool) {
if val {

View File

@ -1,5 +1,6 @@
#![cfg(feature = "rustls-0_23")]
extern crate tls_openssl as openssl;
extern crate tls_rustls_023 as rustls;
use std::{
@ -22,6 +23,7 @@ use actix_rt::{net::TcpStream as RtTcpStream, pin};
use actix_service::{fn_factory_with_config, fn_service};
use actix_tls::{accept::rustls_0_23::TlsStream, connect::rustls_0_23::webpki_roots_cert_store};
use actix_utils::future::{err, ok, poll_fn};
use awc::{Client, Connector};
use bytes::{Bytes, BytesMut};
use derive_more::{Display, Error};
use futures_core::{ready, Stream};
@ -79,6 +81,21 @@ fn tls_config_h2() -> RustlsServerConfig {
tls_config_with_alpn(&[H2_ALPN_PROTOCOL])
}
fn h1_client() -> Client {
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
builder.set_verify(SslVerifyMode::NONE);
builder.set_alpn_protos(b"\x08http/1.1").unwrap();
let connector = Connector::new()
.conn_lifetime(Duration::from_secs(0))
.timeout(Duration::from_millis(30_000))
.openssl(builder.build());
Client::builder().connector(connector).finish()
}
pub fn get_negotiated_alpn_protocol(
addr: SocketAddr,
client_alpn_protocol: &[u8],
@ -106,21 +123,22 @@ pub fn get_negotiated_alpn_protocol(
#[actix_rt::test]
async fn h1() -> io::Result<()> {
let srv = test_server(move || {
let mut srv = test_server(move || {
HttpService::build()
.h1(|_| ok::<_, Error>(Response::ok()))
.rustls_0_23(tls_config_h1())
})
.await;
let response = srv.sget("/").send().await.unwrap();
let response = h1_client().get(srv.surl("/")).send().await.unwrap();
assert!(response.status().is_success());
srv.stop().await;
Ok(())
}
#[actix_rt::test]
async fn h2() -> io::Result<()> {
let srv = test_server(move || {
let mut srv = test_server(move || {
HttpService::build()
.h2(|_| ok::<_, Error>(Response::ok()))
.rustls_0_23(tls_config_h2())
@ -129,12 +147,13 @@ async fn h2() -> io::Result<()> {
let response = srv.sget("/").send().await.unwrap();
assert!(response.status().is_success());
srv.stop().await;
Ok(())
}
#[actix_rt::test]
async fn h1_1() -> io::Result<()> {
let srv = test_server(move || {
let mut srv = test_server(move || {
HttpService::build()
.h1(|req: Request| {
assert!(req.peer_addr().is_some());
@ -145,14 +164,15 @@ async fn h1_1() -> io::Result<()> {
})
.await;
let response = srv.sget("/").send().await.unwrap();
let response = h1_client().get(srv.surl("/")).send().await.unwrap();
assert!(response.status().is_success());
srv.stop().await;
Ok(())
}
#[actix_rt::test]
async fn h2_1() -> io::Result<()> {
let srv = test_server(move || {
let mut srv = test_server(move || {
HttpService::build()
.finish(|req: Request| {
assert!(req.peer_addr().is_some());
@ -168,12 +188,13 @@ async fn h2_1() -> io::Result<()> {
let response = srv.sget("/").send().await.unwrap();
assert!(response.status().is_success());
srv.stop().await;
Ok(())
}
#[actix_rt::test]
async fn h2_tcp_nodelay_override_true() -> io::Result<()> {
let srv = test_server(move || {
let mut srv = test_server(move || {
HttpService::build()
.tcp_nodelay(true)
.on_connect_ext(|io: &TlsStream<RtTcpStream>, data| {
@ -189,12 +210,13 @@ async fn h2_tcp_nodelay_override_true() -> io::Result<()> {
let response = srv.sget("/").send().await.unwrap();
assert!(response.status().is_success());
srv.stop().await;
Ok(())
}
#[actix_rt::test]
async fn h2_tcp_nodelay_override_false() -> io::Result<()> {
let srv = test_server(move || {
let mut srv = test_server(move || {
HttpService::build()
.tcp_nodelay(false)
.on_connect_ext(|io: &TlsStream<RtTcpStream>, data| {
@ -210,6 +232,7 @@ async fn h2_tcp_nodelay_override_false() -> io::Result<()> {
let response = srv.sget("/").send().await.unwrap();
assert!(response.status().is_success());
srv.stop().await;
Ok(())
}
@ -231,12 +254,13 @@ async fn h2_body1() -> io::Result<()> {
let body = srv.load_body(response).await.unwrap();
assert_eq!(&body, data.as_bytes());
srv.stop().await;
Ok(())
}
#[actix_rt::test]
async fn h2_content_length() {
let srv = test_server(move || {
let mut srv = test_server(move || {
HttpService::build()
.h2(|req: Request| {
let indx: usize = req.uri().path()[1..].parse().unwrap();
@ -294,6 +318,8 @@ async fn h2_content_length() {
assert_eq!(response.headers().get(&header), Some(&value));
}
}
srv.stop().await;
}
#[actix_rt::test]
@ -336,6 +362,7 @@ async fn h2_headers() {
// read response
let bytes = srv.load_body(response).await.unwrap();
assert_eq!(bytes, Bytes::from(data2));
srv.stop().await;
}
const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
@ -375,6 +402,7 @@ async fn h2_body2() {
// read response
let bytes = srv.load_body(response).await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await;
}
#[actix_rt::test]
@ -401,6 +429,7 @@ async fn h2_head_empty() {
// read response
let bytes = srv.load_body(response).await.unwrap();
assert!(bytes.is_empty());
srv.stop().await;
}
#[actix_rt::test]
@ -426,11 +455,12 @@ async fn h2_head_binary() {
// read response
let bytes = srv.load_body(response).await.unwrap();
assert!(bytes.is_empty());
srv.stop().await;
}
#[actix_rt::test]
async fn h2_head_binary2() {
let srv = test_server(move || {
let mut srv = test_server(move || {
HttpService::build()
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.rustls_0_23(tls_config_h2())
@ -447,6 +477,8 @@ async fn h2_head_binary2() {
.unwrap();
assert_eq!(format!("{}", STR.len()), len.to_str().unwrap());
}
srv.stop().await;
}
#[actix_rt::test]
@ -469,6 +501,7 @@ async fn h2_body_length() {
// read response
let bytes = srv.load_body(response).await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await;
}
#[actix_rt::test]
@ -496,6 +529,7 @@ async fn h2_body_chunked_explicit() {
// decode
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await;
}
#[actix_rt::test]
@ -525,6 +559,7 @@ async fn h2_response_http_error_handling() {
bytes,
Bytes::from_static(b"error processing HTTP: failed to parse header value")
);
srv.stop().await;
}
#[derive(Debug, Display, Error)]
@ -552,6 +587,7 @@ async fn h2_service_error() {
// read response
let bytes = srv.load_body(response).await.unwrap();
assert_eq!(bytes, Bytes::from_static(b"error"));
srv.stop().await;
}
#[actix_rt::test]
@ -563,12 +599,13 @@ async fn h1_service_error() {
})
.await;
let response = srv.sget("/").send().await.unwrap();
let response = h1_client().get(srv.surl("/")).send().await.unwrap();
assert_eq!(response.status(), http::StatusCode::BAD_REQUEST);
// read response
let bytes = srv.load_body(response).await.unwrap();
assert_eq!(bytes, Bytes::from_static(b"error"));
srv.stop().await;
}
const H2_ALPN_PROTOCOL: &[u8] = b"h2";
@ -577,7 +614,7 @@ const CUSTOM_ALPN_PROTOCOL: &[u8] = b"custom";
#[actix_rt::test]
async fn alpn_h1() -> io::Result<()> {
let srv = test_server(move || {
let mut srv = test_server(move || {
let mut config = tls_config_h1();
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
@ -591,15 +628,16 @@ async fn alpn_h1() -> io::Result<()> {
Some(CUSTOM_ALPN_PROTOCOL.to_vec())
);
let response = srv.sget("/").send().await.unwrap();
let response = h1_client().get(srv.surl("/")).send().await.unwrap();
assert!(response.status().is_success());
srv.stop().await;
Ok(())
}
#[actix_rt::test]
async fn alpn_h2() -> io::Result<()> {
let srv = test_server(move || {
let mut srv = test_server(move || {
let mut config = tls_config_h2();
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
@ -620,12 +658,13 @@ async fn alpn_h2() -> io::Result<()> {
let response = srv.sget("/").send().await.unwrap();
assert!(response.status().is_success());
srv.stop().await;
Ok(())
}
#[actix_rt::test]
async fn alpn_h2_1() -> io::Result<()> {
let srv = test_server(move || {
let mut srv = test_server(move || {
let mut config = tls_config();
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
@ -650,5 +689,6 @@ async fn alpn_h2_1() -> io::Result<()> {
let response = srv.sget("/").send().await.unwrap();
assert!(response.status().is_success());
srv.stop().await;
Ok(())
}

View File

@ -962,7 +962,13 @@ async fn h2_flow_control_window_sizes() {
let mut srv = test_server(|| {
HttpService::build()
.keep_alive(KeepAlive::Disabled)
.finish(|_req: Request| ok::<_, Infallible>(Response::ok()))
.finish(|mut req: Request| async move {
while let Some(item) = req.take_payload().next().await {
item?;
}
Ok::<_, Error>(Response::ok())
})
.tcp_auto_h2c()
})
.await;
@ -992,8 +998,8 @@ async fn h2_flow_control_window_sizes() {
loop {
let cap = std::future::poll_fn(|cx| send.poll_capacity(cx))
.await
.unwrap()
.unwrap();
.expect("request stream closed before flow control capacity became available")
.expect("failed polling flow control capacity");
if cap >= 1024 * 1024 {
break cap;
@ -1001,7 +1007,7 @@ async fn h2_flow_control_window_sizes() {
}
})
.await
.unwrap();
.expect("timed out waiting for flow control capacity");
assert!(
cap >= 1024 * 1024,

View File

@ -2,6 +2,8 @@
## Unreleased
## 0.5.4
- Minimum supported Rust version (MSRV) is now 1.88.
- Support `deserialize_any` in `PathDeserializer` (enables derived `#[serde(untagged)]` enums in path segments). [#2881]
- Fix stale path segment indices after path rewrites, preventing out-of-bounds access during extraction. [#3562]

View File

@ -1,6 +1,6 @@
[package]
name = "actix-router"
version = "0.5.3"
version = "0.5.4"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",

View File

@ -1,11 +1,16 @@
use crate::Path;
// TODO: this trait is necessary, document it
// see impl Resource for ServiceRequest
/// Abstraction over types that can provide a mutable [`Path`] for routing.
///
/// This trait is used by the router to extract the request path in a uniform way across different
/// request types (e.g., Actix Web's `ServiceRequest`). Implementors return a mutable [`Path`]
/// wrapper so routing can read and potentially normalize/parse the path without requiring the
/// original request type.
pub trait Resource {
/// Type of resource's path returned in `resource_path`.
type Path: ResourcePath;
/// Returns a mutable reference to the path wrapper used by the router.
fn resource_path(&mut self) -> &mut Path<Self::Path>;
}

View File

@ -2,6 +2,14 @@
## Unreleased
- Panic when calling `Route::to()` or `Route::service()` after `Route::wrap()` to prevent silently dropping route middleware. [#3944]
- Fix `HttpRequest::{match_pattern,match_name}` reporting path-only matches when route guards disambiguate overlapping resources. [#3346]
[#3944]: https://github.com/actix/actix-web/pull/3944
[#3346]: https://github.com/actix/actix-web/issues/3346
## 4.13.0
- Minimum supported Rust version (MSRV) is now 1.88.
- Improve HTTP/2 upload throughput by increasing default flow control window sizes. [#3638]
- Add `HttpServer::{h2_initial_window_size, h2_initial_connection_window_size}` methods for tuning. [#3638]

View File

@ -1,6 +1,6 @@
[package]
name = "actix-web"
version = "4.12.1"
version = "4.13.0"
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
keywords = ["actix", "http", "web", "framework", "async"]
@ -137,8 +137,8 @@ actix-service = "2"
actix-tls = { version = "3.4", default-features = false, optional = true }
actix-utils = "3"
actix-http = "3.11.2"
actix-router = { version = "0.5.3", default-features = false, features = ["http"] }
actix-http = "3.12.0"
actix-router = { version = "0.5.4", default-features = false, features = ["http"] }
actix-web-codegen = { version = "4.3", optional = true, default-features = false }
bytes = "1"

View File

@ -8,10 +8,10 @@
<!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.12.1)](https://docs.rs/actix-web/4.12.1)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.13.0)](https://docs.rs/actix-web/4.13.0)
![MSRV](https://img.shields.io/badge/rustc-1.88+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/4.12.1/status.svg)](https://deps.rs/crate/actix-web/4.12.1)
[![Dependency Status](https://deps.rs/crate/actix-web/4.13.0/status.svg)](https://deps.rs/crate/actix-web/4.13.0)
<br />
[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/actix/actix-web/graph/badge.svg?token=dSwOnp9QCv)](https://codecov.io/gh/actix/actix-web)
@ -45,6 +45,10 @@ To enable faster release iterations, we mark some features as experimental.
These features are prefixed with `experimental` and a breaking change may happen at any release.
Please use them in a production environment at your own risk.
- `experimental-introspection`: exposes route and method reporting helpers for local diagnostics
and tooling. See [`examples/introspection.rs`](examples/introspection.rs) and
[`examples/introspection_multi_servers.rs`](examples/introspection_multi_servers.rs).
## Documentation
- [Website & User Guide](https://actix.rs)

View File

@ -228,6 +228,8 @@ where
let inner = Rc::get_mut(&mut req.inner).unwrap();
inner.path.get_mut().update(&head.uri);
inner.path.reset();
inner.resource_path.clear();
inner.resource_path_matched = false;
inner.head = head;
inner.conn_data = conn_data;
inner.extensions = extensions;
@ -332,7 +334,15 @@ impl Service<ServiceRequest> for AppRouting {
guards.iter().all(|guard| guard.check(&guard_ctx))
});
if let Some((srv, _info)) = res {
if let Some((srv, info)) = res {
req.push_resource_id(info.0);
let matched = req
.resource_map()
.is_resource_path_match(req.resource_id_path());
req.mark_resource_path(matched);
srv.call(req)
} else {
self.default.call(req)

View File

@ -74,6 +74,9 @@
//! To enable faster release iterations, we mark some features as experimental.
//! These features are prefixed with `experimental` and a breaking change may happen at any release.
//! Please use them in a production environment at your own risk.
//!
//! - `experimental-introspection` - route and method reporting utilities for local diagnostics
//! and tooling. See `examples/introspection.rs` and `examples/introspection_multi_servers.rs`.
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]

View File

@ -42,6 +42,8 @@ pub struct HttpRequest {
pub(crate) struct HttpRequestInner {
pub(crate) head: Message<RequestHead>,
pub(crate) path: Path<Url>,
pub(crate) resource_path: SmallVec<[u16; 4]>,
pub(crate) resource_path_matched: bool,
pub(crate) app_data: SmallVec<[Rc<Extensions>; 4]>,
pub(crate) conn_data: Option<Rc<Extensions>>,
pub(crate) extensions: Rc<RefCell<Extensions>>,
@ -65,6 +67,8 @@ impl HttpRequest {
inner: Rc::new(HttpRequestInner {
head,
path,
resource_path: SmallVec::new(),
resource_path_matched: false,
app_state,
app_data: data,
conn_data,
@ -180,6 +184,26 @@ impl HttpRequest {
&mut Rc::get_mut(&mut self.inner).unwrap().path
}
#[inline]
pub(crate) fn push_resource_id(&mut self, id: u16) {
Rc::get_mut(&mut self.inner).unwrap().resource_path.push(id);
}
#[inline]
pub(crate) fn mark_resource_path(&mut self, is_matched: bool) {
Rc::get_mut(&mut self.inner).unwrap().resource_path_matched = is_matched;
}
#[inline]
pub(crate) fn resource_path(&self) -> &[u16] {
&self.inner.resource_path
}
#[inline]
pub(crate) fn is_resource_path_matched(&self) -> bool {
self.inner.resource_path_matched
}
/// The resource definition pattern that matched the path. Useful for logging and metrics.
///
/// For example, when a resource with pattern `/user/{id}/profile` is defined and a call is made
@ -188,6 +212,15 @@ impl HttpRequest {
/// Returns a None when no resource is fully matched, including default services.
#[inline]
pub fn match_pattern(&self) -> Option<String> {
if self.is_resource_path_matched() {
if let Some(pattern) = self
.resource_map()
.match_pattern_by_resource_path(self.resource_path())
{
return Some(pattern);
}
}
self.resource_map().match_pattern(self.path())
}
@ -196,6 +229,15 @@ impl HttpRequest {
/// Returns a None when no resource is fully matched, including default services.
#[inline]
pub fn match_name(&self) -> Option<&str> {
if self.is_resource_path_matched() {
if let Some(name) = self
.resource_map()
.match_name_by_resource_path(self.resource_path())
{
return Some(name);
}
}
self.resource_map().match_name(self.path())
}
@ -633,6 +675,7 @@ mod tests {
use super::*;
use crate::{
dev::{ResourceDef, Service},
guard,
http::{header, StatusCode},
test::{self, call_service, init_service, read_body, TestRequest},
web, App, HttpResponse,
@ -1019,6 +1062,44 @@ mod tests {
assert_eq!(res.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn extract_path_pattern_with_guards() {
let srv = init_service(
App::new().service(
web::scope("/widgets")
.service(
web::resource("/{id}")
.name("get_widget")
.guard(guard::Get())
.to(|req: HttpRequest| {
assert_eq!(req.match_pattern(), Some("/widgets/{id}".to_owned()));
assert_eq!(req.match_name(), Some("get_widget"));
HttpResponse::Ok().finish()
}),
)
.service(
web::resource("/action")
.name("widget_action")
.guard(guard::Post())
.to(|req: HttpRequest| {
assert_eq!(req.match_pattern(), Some("/widgets/action".to_owned()));
assert_eq!(req.match_name(), Some("widget_action"));
HttpResponse::Ok().finish()
}),
),
),
)
.await;
let req = TestRequest::get().uri("/widgets/42").to_request();
let res = call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
let req = TestRequest::post().uri("/widgets/action").to_request();
let res = call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn extract_path_pattern_complex() {
let srv = init_service(

View File

@ -240,10 +240,40 @@ impl ResourceMap {
)
}
pub(crate) fn is_resource_path_match(&self, resource_path: &[u16]) -> bool {
self.find_node_by_resource_path(resource_path)
.is_some_and(|node| node.nodes.is_none())
}
pub(crate) fn match_name_by_resource_path(&self, resource_path: &[u16]) -> Option<&str> {
self.find_node_by_resource_path(resource_path)?
.pattern
.name()
}
pub(crate) fn match_pattern_by_resource_path(&self, resource_path: &[u16]) -> Option<String> {
self.find_node_by_resource_path(resource_path)?
.root_rmap_fn(String::with_capacity(AVG_PATH_LEN), |mut acc, node| {
let pattern = node.pattern.pattern()?;
acc.push_str(pattern);
Some(acc)
})
}
fn find_matching_node(&self, path: &str) -> Option<&ResourceMap> {
self._find_matching_node(path).flatten()
}
fn find_node_by_resource_path(&self, resource_path: &[u16]) -> Option<&ResourceMap> {
let mut node = self;
for id in resource_path {
node = node.nodes.as_ref()?.get(*id as usize)?;
}
Some(node)
}
/// Returns `None` if root pattern doesn't match;
/// `Some(None)` if root pattern matches but there is no matching child pattern.
/// Don't search sideways when `Some(none)` is returned.

View File

@ -23,6 +23,7 @@ use crate::{
pub struct Route {
service: BoxedHttpServiceFactory,
guards: Rc<Vec<Box<dyn Guard>>>,
wrapped: bool,
}
impl Route {
@ -34,6 +35,7 @@ impl Route {
Ok(req.into_response(HttpResponse::NotFound()))
})),
guards: Rc::new(Vec::new()),
wrapped: false,
}
}
@ -42,6 +44,17 @@ impl Route {
/// `mw` is a middleware component (type), that can modify the requests and responses handled by
/// this `Route`.
///
/// This middleware wraps the currently configured route service. Call this method after
/// [`Route::to`] or [`Route::service`] so the middleware is applied to the final handler.
///
/// # Examples
/// ```
/// # use actix_web::{web, HttpResponse, middleware};
/// web::get()
/// .to(|| async { HttpResponse::Ok() })
/// .wrap(middleware::Logger::default());
/// ```
///
/// See [`App::wrap`](crate::App::wrap) for more details.
#[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
@ -59,12 +72,24 @@ impl Route {
Route {
service: boxed::factory(apply(Compat::new(mw), self.service)),
guards: self.guards,
wrapped: true,
}
}
pub(crate) fn take_guards(&mut self) -> Vec<Box<dyn Guard>> {
mem::take(Rc::get_mut(&mut self.guards).unwrap())
}
#[cold]
#[inline(never)]
#[track_caller]
fn panic_after_wrap(replaced: &str, example: &str) -> ! {
panic!(
"Route middleware was already registered with `.wrap()`. \
Calling `.{replaced}()` now would replace the wrapped service and silently drop middleware. \
Call `.{replaced}()` before `.wrap()` (for example: `{example}`)."
);
}
}
impl ServiceFactory<ServiceRequest> for Route {
@ -212,12 +237,21 @@ impl Route {
/// .route(web::get().to(index))
/// );
/// ```
///
/// # Panics
/// Panics if called after [`Route::wrap`], since this would replace the wrapped service and
/// silently discard middleware.
#[track_caller]
pub fn to<F, Args>(mut self, handler: F) -> Self
where
F: Handler<Args>,
Args: FromRequest + 'static,
F::Output: Responder + 'static,
{
if self.wrapped {
Self::panic_after_wrap("to", "web::get().to(handler).wrap(mw)");
}
self.service = handler_service(handler);
self
}
@ -254,6 +288,11 @@ impl Route {
/// web::get().service(fn_factory(|| async { Ok(HelloWorld) })),
/// );
/// ```
///
/// # Panics
/// Panics if called after [`Route::wrap`], since this would replace the wrapped service and
/// silently discard middleware.
#[track_caller]
pub fn service<S, E>(mut self, service_factory: S) -> Self
where
S: ServiceFactory<
@ -265,6 +304,10 @@ impl Route {
> + 'static,
E: Into<Error> + 'static,
{
if self.wrapped {
Self::panic_after_wrap("service", "web::get().service(factory).wrap(mw)");
}
self.service = boxed::factory(service_factory.map_err(Into::into));
self
}
@ -459,4 +502,25 @@ mod tests {
Bytes::from_static(b"Goodbye, and thanks for all the fish!")
);
}
#[test]
#[should_panic(expected = "Route middleware was already registered with `.wrap()`")]
fn wrap_before_to_panics() {
web::get()
.wrap(DefaultHeaders::new().add(("x-test", "x-value")))
.to(HttpResponse::Ok);
}
#[test]
#[should_panic(expected = "Route middleware was already registered with `.wrap()`")]
fn wrap_before_service_panics() {
web::get()
.wrap(DefaultHeaders::new().add(("x-test", "x-value")))
.service(fn_factory(|| async {
Ok::<_, ()>(fn_service(|req: ServiceRequest| async {
let (req, _) = req.into_parts();
Ok::<_, Infallible>(ServiceResponse::new(req, HttpResponse::Ok().finish()))
}))
}));
}
}

View File

@ -533,7 +533,15 @@ impl Service<ServiceRequest> for ScopeService {
guards.iter().all(|guard| guard.check(&guard_ctx))
});
if let Some((srv, _info)) = res {
if let Some((srv, info)) = res {
req.push_resource_id(info.0);
let matched = req
.resource_map()
.is_resource_path_match(req.resource_id_path());
req.mark_resource_path(matched);
srv.call(req)
} else {
self.default.call(req)

View File

@ -321,6 +321,21 @@ impl ServiceRequest {
.push(extensions);
}
#[inline]
pub(crate) fn push_resource_id(&mut self, id: u16) {
self.req.push_resource_id(id);
}
#[inline]
pub(crate) fn mark_resource_path(&mut self, is_matched: bool) {
self.req.mark_resource_path(is_matched);
}
#[inline]
pub(crate) fn resource_id_path(&self) -> &[u16] {
self.req.resource_path()
}
/// Creates a context object for use with a routing [guard](crate::guard).
#[inline]
pub fn guard_ctx(&self) -> GuardContext<'_> {

View File

@ -2,6 +2,12 @@
## Unreleased
- Add camel-case header controls to `WebsocketsRequest` via `camel_case_headers()` and `set_camel_case_headers()`. [#3953]
[#3953]: https://github.com/actix/actix-web/pull/3953
## 3.8.2
- Minimum supported Rust version (MSRV) is now 1.88.
- Fix empty streaming request bodies being sent with chunked transfer encoding.

View File

@ -1,6 +1,6 @@
[package]
name = "awc"
version = "3.8.1"
version = "3.8.2"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Async HTTP and WebSocket client library"
keywords = ["actix", "http", "framework", "async", "web"]
@ -98,7 +98,7 @@ dangerous-h2c = []
[dependencies]
actix-codec = "0.5"
actix-http = { version = "3.10", features = ["http2", "ws"] }
actix-http = { version = "3.12.0", features = ["http2", "ws"] }
actix-rt = { version = "2.1", default-features = false }
actix-service = "2"
actix-tls = { version = "3.4", features = ["connect", "uri"] }
@ -134,13 +134,13 @@ tls-rustls-0_23 = { package = "rustls", version = "0.23", optional = true, defau
hickory-resolver = { version = "0.25", optional = true, features = ["system-config", "tokio"] }
[dev-dependencies]
actix-http = { version = "3.7", features = ["openssl"] }
actix-http = { version = "3.12", features = ["openssl"] }
actix-http-test = { version = "3", features = ["openssl"] }
actix-server = "2"
actix-test = { version = "0.1", features = ["openssl", "rustls-0_23"] }
actix-tls = { version = "3.4", features = ["openssl", "rustls-0_23"] }
actix-utils = "3"
actix-web = { version = "4", features = ["openssl"] }
actix-web = { version = "4.13", features = ["openssl"] }
brotli = "8"
const-str = "0.5" # TODO(MSRV 1.77): update to 0.6

View File

@ -5,9 +5,9 @@
<!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc)
[![Documentation](https://docs.rs/awc/badge.svg?version=3.8.1)](https://docs.rs/awc/3.8.1)
[![Documentation](https://docs.rs/awc/badge.svg?version=3.8.2)](https://docs.rs/awc/3.8.2)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc)
[![Dependency Status](https://deps.rs/crate/awc/3.8.1/status.svg)](https://deps.rs/crate/awc/3.8.1)
[![Dependency Status](https://deps.rs/crate/awc/3.8.2/status.svg)](https://deps.rs/crate/awc/3.8.2)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
<!-- prettier-ignore-end -->

View File

@ -245,6 +245,21 @@ impl WebsocketsRequest {
self.header(AUTHORIZATION, format!("Bearer {}", token))
}
/// Returns whether headers should be sent in Camel-Case.
///
/// Default is `false`.
#[inline]
pub fn camel_case_headers(&self) -> bool {
self.head.camel_case_headers()
}
/// Sets whether to send headers formatted as Camel-Case.
#[inline]
pub fn set_camel_case_headers(mut self, val: bool) -> Self {
self.head.set_camel_case_headers(val);
self
}
/// Complete request construction and connect to a WebSocket server.
pub async fn connect(
mut self,
@ -529,6 +544,12 @@ mod tests {
let _ = req.connect();
}
#[actix_rt::test]
async fn camel_case_headers() {
let req = Client::new().ws("/").set_camel_case_headers(true);
assert!(req.camel_case_headers());
}
#[actix_rt::test]
async fn basics() {
let req = Client::new()