From e766ca61f14a5614aebe58d164df49d8de118606 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Wed, 15 Apr 2026 15:20:52 +0900 Subject: [PATCH 01/19] chore: upgrade rand to 0.10.1 (#4021) --- Cargo.lock | 54 ++++++++++++++++++++++++++++----- actix-http/CHANGES.md | 1 + actix-http/Cargo.toml | 2 +- actix-http/tests/test_server.rs | 2 +- actix-multipart/CHANGES.md | 1 + actix-multipart/Cargo.toml | 2 +- actix-web/CHANGES.md | 1 + actix-web/Cargo.toml | 2 +- awc/CHANGES.md | 1 + awc/Cargo.toml | 2 +- 10 files changed, 55 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2a92839e2..26ee4ff0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ dependencies = [ "openssl", "percent-encoding", "pin-project-lite", - "rand 0.9.3", + "rand 0.10.1", "rcgen", "regex", "rustls 0.23.37", @@ -164,7 +164,7 @@ dependencies = [ "memchr", "mime", "multer", - "rand 0.9.3", + "rand 0.10.1", "serde", "serde_json", "serde_plain", @@ -362,7 +362,7 @@ dependencies = [ "once_cell", "openssl", "pin-project-lite", - "rand 0.9.3", + "rand 0.10.1", "rcgen", "regex", "regex-lite", @@ -424,7 +424,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -606,7 +606,7 @@ dependencies = [ "openssl", "percent-encoding", "pin-project-lite", - "rand 0.9.3", + "rand 0.10.1", "rcgen", "rustls 0.20.9", "rustls 0.21.12", @@ -748,6 +748,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -917,6 +928,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1423,6 +1443,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -2211,7 +2232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -2316,6 +2337,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -2354,6 +2386,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rayon" version = "1.11.0" @@ -2765,7 +2803,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -2776,7 +2814,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index a627718b0..32a997db9 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -4,6 +4,7 @@ - Encode the HTTP/1 `Connection: Upgrade` header in Camel-Case when camel-case header formatting is enabled.[#3953] - Fix `HeaderMap` iterators' `len()` and `size_hint()` implementations for multi-value headers. +- Update `rand` dependency to `0.10`. [#3953]: https://github.com/actix/actix-web/pull/3953 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 6495ebbf1..aee75a0a6 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -123,7 +123,7 @@ h2 = { version = "0.3.27", optional = true } # websockets base64 = { version = "0.22", optional = true } local-channel = { version = "0.1", optional = true } -rand = { version = "0.9", optional = true } +rand = { version = "0.10.1", optional = true } sha1 = { version = "0.10", optional = true } # openssl/rustls diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index 4b4e435bd..6cbb680d5 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -19,7 +19,7 @@ use actix_utils::future::{err, ok, ready}; use bytes::Bytes; use derive_more::{Display, Error}; use futures_util::{stream::once, FutureExt as _, StreamExt as _}; -use rand::Rng as _; +use rand::RngExt as _; use regex::Regex; #[actix_rt::test] diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 13c18038b..5e614a81e 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -5,6 +5,7 @@ - Add multi-field multipart payload builders to `actix_multipart::test`. [#3575] - Add `MultipartForm` support for `Option>` fields. [#3577] - Minimum supported Rust version (MSRV) is now 1.88. +- Update `rand` dependency to `0.10`. [#3577]: https://github.com/actix/actix-web/pull/3577 [#3575]: https://github.com/actix/actix-web/issues/3575 diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 384430f06..29c30df7b 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -49,7 +49,7 @@ local-waker = "0.1" log = "0.4" memchr = "2.5" mime = "0.3" -rand = "0.9" +rand = "0.10.1" serde = "1" serde_json = "1" serde_plain = "1" diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index b5a91cc95..1c48a27ce 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -7,6 +7,7 @@ - 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] - Fix `Readlines` handling of lines split across payload chunks so combined line limits are enforced and complete lines are yielded. +- Update `rand` dependency to `0.10`. [#3944]: https://github.com/actix/actix-web/pull/3944 [#3346]: https://github.com/actix/actix-web/issues/3346 diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index 13b5615f7..bb24ab175 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -180,7 +180,7 @@ criterion = { version = "0.5", features = ["html_reports"] } env_logger = "0.11" flate2 = "1.0.13" futures-util = { version = "0.3.17", default-features = false, features = ["std"] } -rand = "0.9" +rand = "0.10.1" rcgen = "0.13" rustls-pki-types = "1.13.1" serde = { version = "1", features = ["derive"] } diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 38a9f8b09..e523c0fc7 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -3,6 +3,7 @@ ## Unreleased - Add camel-case header controls to `WebsocketsRequest` via `camel_case_headers()` and `set_camel_case_headers()`. [#3953] +- Update `rand` dependency to `0.10`. [#3953]: https://github.com/actix/actix-web/pull/3953 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index b5b9396b0..86b319e42 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -117,7 +117,7 @@ log = "0.4" mime = "0.3" percent-encoding = "2.1" pin-project-lite = "0.2" -rand = "0.9" +rand = "0.10.1" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7" From 253cd4f9771c3094b6cdb4350b9e8e7ae53fad75 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Wed, 15 Apr 2026 22:49:34 +0900 Subject: [PATCH 02/19] chore: address new advisories (#4023) --- Cargo.lock | 126 ++++++++++++++++++++++++++--------------------------- deny.toml | 6 +-- 2 files changed, 66 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 26ee4ff0e..c3a6c679f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", "futures-core", "futures-sink", @@ -30,7 +30,7 @@ dependencies = [ "actix-test", "actix-utils", "actix-web", - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", "derive_more", "env_logger", @@ -62,7 +62,7 @@ dependencies = [ "async-stream", "awc", "base64 0.22.1", - "bitflags 2.11.0", + "bitflags 2.11.1", "brotli", "bytes", "bytestring", @@ -91,7 +91,7 @@ dependencies = [ "rand 0.10.1", "rcgen", "regex", - "rustls 0.23.37", + "rustls 0.23.38", "rustls-pki-types", "rustversion", "serde", @@ -275,7 +275,7 @@ dependencies = [ "rustls 0.20.9", "rustls 0.21.12", "rustls 0.22.4", - "rustls 0.23.37", + "rustls 0.23.38", "serde", "serde_json", "serde_urlencoded", @@ -366,7 +366,7 @@ dependencies = [ "rcgen", "regex", "regex-lite", - "rustls 0.23.37", + "rustls 0.23.38", "rustls-pki-types", "serde", "serde_json", @@ -611,7 +611,7 @@ dependencies = [ "rustls 0.20.9", "rustls 0.21.12", "rustls 0.22.4", - "rustls 0.23.37", + "rustls 0.23.38", "rustls-pki-types", "serde", "serde_json", @@ -663,9 +663,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -732,9 +732,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.59" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "jobserver", @@ -1505,9 +1505,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -1537,7 +1537,7 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.9.3", + "rand 0.9.4", "ring 0.17.14", "thiserror 2.0.18", "tinyvec", @@ -1559,7 +1559,7 @@ dependencies = [ "moka", "once_cell", "parking_lot", - "rand 0.9.3", + "rand 0.9.4", "resolv-conf", "smallvec", "thiserror 2.0.18", @@ -1747,12 +1747,12 @@ checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] name = "indexmap" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -1863,9 +1863,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "once_cell", "wasm-bindgen", @@ -1885,20 +1885,20 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libredox" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "libc", "plain", - "redox_syscall 0.7.3", + "redox_syscall 0.7.4", ] [[package]] @@ -2078,11 +2078,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.76" +version = "0.10.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +checksum = "bfe4646e360ec77dff7dde40ed3d6c5fee52d156ef4a62f53973d38294dad87f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "foreign-types", "libc", @@ -2110,9 +2110,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "ad2f2c0eba47118757e4c6d2bff2838f3e0523380021356e7875e858372ce644" dependencies = [ "cc", "libc", @@ -2187,9 +2187,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -2329,9 +2329,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -2394,9 +2394,9 @@ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -2431,16 +2431,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] name = "redox_syscall" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -2528,7 +2528,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -2575,15 +2575,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ "aws-lc-rs", "log", "once_cell", "rustls-pki-types", - "rustls-webpki 0.103.10", + "rustls-webpki 0.103.12", "subtle", "zeroize", ] @@ -2632,9 +2632,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" dependencies = [ "aws-lc-rs", "ring 0.17.14", @@ -2700,7 +2700,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "core-foundation", "core-foundation-sys", "libc", @@ -3092,9 +3092,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.51.1" +version = "1.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" dependencies = [ "bytes", "libc", @@ -3167,7 +3167,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.37", + "rustls 0.23.38", "tokio", ] @@ -3437,9 +3437,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -3450,9 +3450,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3460,9 +3460,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -3473,9 +3473,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -3508,7 +3508,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap", "semver", @@ -3516,9 +3516,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -3785,7 +3785,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags 2.11.1", "indexmap", "log", "serde", diff --git a/deny.toml b/deny.toml index b8dcd8f38..c0dd7ad78 100644 --- a/deny.toml +++ b/deny.toml @@ -42,7 +42,7 @@ ignore = [ "RUSTSEC-2024-0336", "RUSTSEC-2025-0009", "RUSTSEC-2025-0010", - "RUSTSEC-2026-0044", - "RUSTSEC-2026-0048", - "RUSTSEC-2026-0049" + "RUSTSEC-2026-0049", + "RUSTSEC-2026-0098", + "RUSTSEC-2026-0099", ] From 6d2c2f44622aef2ad5e115a3dba3bb80f12e020e Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Thu, 16 Apr 2026 00:38:01 +0900 Subject: [PATCH 03/19] chore(http): upgrade `sha1` to 0.11 (#4022) Co-authored-by: Rob Ede --- Cargo.lock | 66 +++++++++++++++++++++++++++++++++++-------- actix-http/CHANGES.md | 1 + actix-http/Cargo.toml | 2 +- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3a6c679f..de37efdc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -412,7 +412,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array", ] @@ -676,6 +676,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "brotli" version = "8.0.2" @@ -792,7 +801,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", ] @@ -859,6 +868,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-str" version = "0.5.7" @@ -1039,6 +1054,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctr" version = "0.9.2" @@ -1127,11 +1151,22 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.1", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1582,7 +1617,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1624,6 +1659,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -2798,13 +2842,13 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", - "digest", + "cpufeatures 0.3.0", + "digest 0.11.2", ] [[package]] @@ -2815,7 +2859,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -3332,7 +3376,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 32a997db9..ba4d0aa9d 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -5,6 +5,7 @@ - Encode the HTTP/1 `Connection: Upgrade` header in Camel-Case when camel-case header formatting is enabled.[#3953] - Fix `HeaderMap` iterators' `len()` and `size_hint()` implementations for multi-value headers. - Update `rand` dependency to `0.10`. +- Update `sha1` dependency to `0.11`. [#3953]: https://github.com/actix/actix-web/pull/3953 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index aee75a0a6..a617459b8 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -124,7 +124,7 @@ h2 = { version = "0.3.27", optional = true } base64 = { version = "0.22", optional = true } local-channel = { version = "0.1", optional = true } rand = { version = "0.10.1", optional = true } -sha1 = { version = "0.10", optional = true } +sha1 = { version = "0.11", optional = true } # openssl/rustls actix-tls = { version = "3.4", default-features = false, optional = true } From be4566d669a8e3900526616f9df6ec0ee71856c2 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Fri, 17 Apr 2026 00:32:45 +0900 Subject: [PATCH 04/19] fix(multipart): do not parse with fixed index not to panic (#4024) --- actix-multipart/CHANGES.md | 1 + actix-multipart/src/multipart.rs | 202 ++++++++++++++++++++++++++----- 2 files changed, 171 insertions(+), 32 deletions(-) diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 5e614a81e..4445bcd1e 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -4,6 +4,7 @@ - Add multi-field multipart payload builders to `actix_multipart::test`. [#3575] - Add `MultipartForm` support for `Option>` fields. [#3577] +- Fix user-triggerable panic when parsing multipart boundaries. - Minimum supported Rust version (MSRV) is now 1.88. - Update `rand` dependency to `0.10`. diff --git a/actix-multipart/src/multipart.rs b/actix-multipart/src/multipart.rs index e38fbde9e..be0bc59f0 100644 --- a/actix-multipart/src/multipart.rs +++ b/actix-multipart/src/multipart.rs @@ -84,6 +84,10 @@ impl Multipart { .as_str() .to_owned(); + if boundary.is_empty() { + return Err(Error::BoundaryMissing); + } + Ok((content_type, boundary)) } @@ -239,6 +243,10 @@ impl Inner { /// - `Ok(None)` - boundary not found, more data needs reading /// - `Err(BoundaryMissing)` - multipart boundary is missing fn read_boundary(payload: &mut PayloadBuffer, boundary: &str) -> Result, Error> { + if boundary.is_empty() { + return Err(Error::BoundaryMissing); + } + // TODO: need to read epilogue let chunk = match payload.readline_or_eof()? { // TODO: this might be okay as a let Some() else return Ok(None) @@ -249,34 +257,21 @@ impl Inner { const BOUNDARY_MARKER: &[u8] = b"--"; const LINE_BREAK: &[u8] = b"\r\n"; - let boundary_len = boundary.len(); - - if chunk.len() < boundary_len + 2 + 2 - || !chunk.starts_with(BOUNDARY_MARKER) - || &chunk[2..boundary_len + 2] != boundary.as_bytes() - { + let Some(chunk) = chunk.as_ref().strip_prefix(BOUNDARY_MARKER) else { return Err(Error::BoundaryMissing); - } + }; - // chunk facts: - // - long enough to contain boundary + 2 markers or 1 marker and line-break - // - starts with boundary marker - // - chunk contains correct boundary + let Some(chunk) = chunk.strip_prefix(boundary.as_bytes()) else { + return Err(Error::BoundaryMissing); + }; - if &chunk[boundary_len + 2..] == LINE_BREAK { + if chunk == LINE_BREAK { // boundary is followed by line-break, indicating more fields to come return Ok(Some(false)); } // boundary is followed by marker - if &chunk[boundary_len + 2..boundary_len + 4] == BOUNDARY_MARKER - && ( - // chunk is exactly boundary len + 2 markers - chunk.len() == boundary_len + 2 + 2 - // final boundary is allowed to end with a line-break - || &chunk[boundary_len + 4..] == LINE_BREAK - ) - { + if chunk == BOUNDARY_MARKER || chunk == b"--\r\n" { return Ok(Some(true)); } @@ -287,7 +282,12 @@ impl Inner { payload: &mut PayloadBuffer, boundary: &str, ) -> Result, Error> { + if boundary.is_empty() { + return Err(Error::BoundaryMissing); + } + let mut eof = false; + let boundary = boundary.as_bytes(); loop { match payload.readline()? { @@ -295,19 +295,17 @@ impl Inner { if chunk.is_empty() { return Err(Error::BoundaryMissing); } - if chunk.len() < boundary.len() { + + let Some(line) = chunk.as_ref().strip_suffix(b"\r\n") else { continue; - } - if &chunk[..2] == b"--" && &chunk[2..chunk.len() - 2] == boundary.as_bytes() { - break; - } else { - if chunk.len() < boundary.len() + 2 { - continue; + }; + + if let Some(line) = line.strip_prefix(b"--") { + if line == boundary { + break; } - let b: &[u8] = boundary.as_ref(); - if &chunk[..boundary.len()] == b - && &chunk[boundary.len()..boundary.len() + 2] == b"--" - { + + if line.strip_suffix(b"--") == Some(boundary) { eof = true; break; } @@ -589,11 +587,151 @@ mod tests { (bytes, headers) } + fn create_header(content_type: &'static str) -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static(content_type), + ); + headers + } + + #[actix_rt::test] + async fn empty_boundary_does_not_panic() { + let payload = stream::once(async { Ok(Bytes::from_static(b"\n")) }); + let ct = "multipart/mixed; boundary=\"a\"".parse().unwrap(); + + let mut multipart = Multipart::from_ct_and_boundary(ct, String::new(), payload); + let res = multipart.next().await.unwrap(); + assert_matches!(res, Err(Error::BoundaryMissing)); + } + + #[actix_rt::test] + async fn short_line_with_one_byte_boundary_does_not_panic() { + let bytes = Bytes::from_static(b"\n"); + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("multipart/mixed; boundary=\"a\""), + ); + let payload = stream::once(async { Ok(bytes) }); + + let mut multipart = Multipart::new(&headers, payload); + let res = multipart.next().await.unwrap(); + assert_matches!(res, Err(Error::Incomplete)); + } + + #[actix_rt::test] + async fn short_final_boundary_with_one_byte_boundary_does_not_panic() { + let bytes = Bytes::from_static(b"--\n"); + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("multipart/mixed; boundary=\"a\""), + ); + let payload = stream::once(async { Ok(bytes) }); + + let mut multipart = Multipart::new(&headers, payload); + let res = multipart.next().await.unwrap(); + assert_matches!(res, Err(Error::Incomplete)); + } + + #[actix_rt::test] + async fn one_byte_boundary_parses_valid_body() { + let bytes = Bytes::from_static( + b"preamble\r\n\ + --a\r\n\ + Content-Type: text/plain\r\n\ + Content-Length: 3\r\n\ + \r\n\ + one\r\n\ + --a\r\n\ + Content-Type: text/plain\r\n\ + Content-Length: 3\r\n\ + \r\n\ + two\r\n\ + --a--\r\n", + ); + let headers = create_header("multipart/mixed; boundary=\"a\""); + let payload = stream::once(async { Ok(bytes) }); + + let mut multipart = Multipart::new(&headers, payload); + + let mut field = multipart.next().await.unwrap().unwrap(); + assert_eq!(get_whole_field(&mut field).await, "one"); + drop(field); + + let mut field = multipart.next().await.unwrap().unwrap(); + assert_eq!(get_whole_field(&mut field).await, "two"); + drop(field); + + assert!(multipart.next().await.is_none()); + } + + #[actix_rt::test] + async fn one_byte_boundary_parses_when_split_across_chunks() { + let bytes = Bytes::from_static( + b"x\r\n\ + --a\r\n\ + Content-Type: text/plain\r\n\ + Content-Length: 4\r\n\ + \r\n\ + data\r\n\ + --a--\r\n", + ); + let headers = create_header("multipart/mixed; boundary=\"a\""); + let payload = stream::iter(bytes) + .map(|byte| Ok(Bytes::copy_from_slice(&[byte]))) + .interleave_pending(); + + let mut multipart = Multipart::new(&headers, payload); + + let mut field = multipart.next().await.unwrap().unwrap(); + assert_eq!(get_whole_field(&mut field).await, "data"); + drop(field); + + assert!(multipart.next().await.is_none()); + } + + #[actix_rt::test] + async fn short_preamble_lines_before_boundary_are_skipped() { + let bytes = Bytes::from_static( + b"\n\ + -\r\n\ + --a\r\n\ + Content-Type: text/plain\r\n\ + Content-Length: 4\r\n\ + \r\n\ + data\r\n\ + --a--\r\n", + ); + let headers = create_header("multipart/mixed; boundary=\"a\""); + let payload = stream::once(async { Ok(bytes) }); + + let mut multipart = Multipart::new(&headers, payload); + + let mut field = multipart.next().await.unwrap().unwrap(); + assert_eq!(get_whole_field(&mut field).await, "data"); + drop(field); + + assert!(multipart.next().await.is_none()); + } + + #[actix_rt::test] + async fn first_boundary_can_be_final() { + let bytes = Bytes::from_static(b"--a--\r\n"); + let headers = create_header("multipart/mixed; boundary=\"a\""); + let payload = stream::once(async { Ok(bytes) }); + + let mut multipart = Multipart::new(&headers, payload); + assert!(multipart.next().await.is_none()); + } + #[actix_rt::test] async fn test_multipart_no_end_crlf() { let (sender, payload) = create_stream(); let (mut bytes, headers) = create_double_request_with_header(); - let bytes_stripped = bytes.split_to(bytes.len()); // strip crlf + let bytes_stripped = bytes.split_to(bytes.len() - 2); // strip crlf sender.send(Ok(bytes_stripped)).unwrap(); drop(sender); // eof From be62050f9d7f3aa92812870d87bd6635fae7fdc9 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sat, 18 Apr 2026 00:24:37 +0900 Subject: [PATCH 05/19] fix(multipart): set cap for parser buffering (#4025) --- actix-multipart/CHANGES.md | 2 + actix-multipart/src/lib.rs | 2 +- actix-multipart/src/multipart.rs | 147 ++++++++++++++++++++++++++++++- actix-multipart/src/payload.rs | 131 ++++++++++++++++++++++++--- 4 files changed, 266 insertions(+), 16 deletions(-) diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 4445bcd1e..1d5ddf55b 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -4,6 +4,8 @@ - Add multi-field multipart payload builders to `actix_multipart::test`. [#3575] - Add `MultipartForm` support for `Option>` fields. [#3577] +- Bound internal multipart parser buffering to prevent unbounded memory growth on malformed bodies. + - behavior change notice: There's now a cap for buffering (64KB). It can be changed with `MultipartConfig::buffer_limit`. - Fix user-triggerable panic when parsing multipart boundaries. - Minimum supported Rust version (MSRV) is now 1.88. - Update `rand` dependency to `0.10`. diff --git a/actix-multipart/src/lib.rs b/actix-multipart/src/lib.rs index ca5166d33..e7830b5e4 100644 --- a/actix-multipart/src/lib.rs +++ b/actix-multipart/src/lib.rs @@ -82,5 +82,5 @@ pub mod test; pub use self::{ error::Error as MultipartError, field::{Field, LimitExceeded}, - multipart::Multipart, + multipart::{Multipart, MultipartConfig}, }; diff --git a/actix-multipart/src/multipart.rs b/actix-multipart/src/multipart.rs index be0bc59f0..bde7d122f 100644 --- a/actix-multipart/src/multipart.rs +++ b/actix-multipart/src/multipart.rs @@ -11,7 +11,7 @@ use actix_web::{ dev, error::{ParseError, PayloadError}, http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}, - web::Bytes, + web::{self, Bytes}, HttpRequest, }; use futures_core::stream::Stream; @@ -20,7 +20,7 @@ use mime::Mime; use crate::{ error::Error, field::InnerField, - payload::{PayloadBuffer, PayloadRef}, + payload::{PayloadBuffer, PayloadRef, DEFAULT_BUFFER_LIMIT}, safety::Safety, Field, }; @@ -44,6 +44,46 @@ enum Flow { Error(Option), } +/// [`Multipart`] extractor configuration. +/// +/// Add to your app data to have it picked up by [`Multipart`] extractors. +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +pub struct MultipartConfig { + buffer_limit: usize, +} + +impl MultipartConfig { + /// Creates a default multipart extractor configuration. + pub fn new() -> Self { + DEFAULT_CONFIG + } + + /// Sets maximum internal parser buffer size. By default this limit is 64 KiB. + pub fn buffer_limit(mut self, buffer_limit: usize) -> Self { + self.buffer_limit = buffer_limit; + self + } + + /// Extracts multipart config from app data. Check both `T` and `Data`, in that order, and + /// fall back to the default multipart config. + fn from_req(req: &HttpRequest) -> &Self { + req.app_data::() + .or_else(|| req.app_data::>().map(|d| d.as_ref())) + .unwrap_or(&DEFAULT_CONFIG) + } +} + +static DEFAULT_CONFIG: MultipartConfig = MultipartConfig { + buffer_limit: DEFAULT_BUFFER_LIMIT, +}; + +impl Default for MultipartConfig { + fn default() -> Self { + Self::new() + } +} + impl Multipart { /// Creates multipart instance from parts. pub fn new(headers: &HeaderMap, stream: S) -> Self @@ -58,8 +98,15 @@ impl Multipart { /// Creates multipart instance from parts. pub(crate) fn from_req(req: &HttpRequest, payload: &mut dev::Payload) -> Self { + let config = MultipartConfig::from_req(req); + match Self::find_ct_and_boundary(req.headers()) { - Ok((ct, boundary)) => Self::from_ct_and_boundary(ct, boundary, payload.take()), + Ok((ct, boundary)) => Self::from_ct_and_boundary_with_buffer_limit( + ct, + boundary, + payload.take(), + config.buffer_limit, + ), Err(err) => Self::from_error(err), } } @@ -93,13 +140,30 @@ impl Multipart { /// Constructs a new multipart reader from given Content-Type, boundary, and stream. pub(crate) fn from_ct_and_boundary(ct: Mime, boundary: String, stream: S) -> Multipart + where + S: Stream> + 'static, + { + Self::from_ct_and_boundary_with_buffer_limit( + ct, + boundary, + stream, + DEFAULT_CONFIG.buffer_limit, + ) + } + + fn from_ct_and_boundary_with_buffer_limit( + ct: Mime, + boundary: String, + stream: S, + buffer_limit: usize, + ) -> Multipart where S: Stream> + 'static, { Multipart { safety: Safety::new(), flow: Flow::InFlight(Inner { - payload: PayloadRef::new(PayloadBuffer::new(stream)), + payload: PayloadRef::new(PayloadBuffer::new_with_limit(stream, buffer_limit)), content_type: ct, boundary, state: State::FirstBoundary, @@ -596,6 +660,18 @@ mod tests { headers } + fn create_multipart_with_buffer_limit( + body: impl Stream> + 'static, + buffer_limit: usize, + ) -> Multipart { + Multipart::from_ct_and_boundary_with_buffer_limit( + "multipart/mixed; boundary=\"a\"".parse().unwrap(), + "a".to_owned(), + body, + buffer_limit, + ) + } + #[actix_rt::test] async fn empty_boundary_does_not_panic() { let payload = stream::once(async { Ok(Bytes::from_static(b"\n")) }); @@ -727,6 +803,69 @@ mod tests { assert!(multipart.next().await.is_none()); } + #[actix_rt::test] + async fn malformed_preamble_over_buffer_limit_errors() { + let body = stream::iter( + [b"aaaaaaaa", b"bbbbbbbb", b"cccccccc"].map(|chunk| Ok(Bytes::from_static(chunk))), + ); + + let mut multipart = create_multipart_with_buffer_limit(body, 16); + let res = multipart.next().await.unwrap(); + + assert_matches!(res, Err(Error::Payload(PayloadError::Overflow))); + } + + #[actix_rt::test] + async fn malformed_headers_over_buffer_limit_errors() { + let body = stream::iter( + [ + Bytes::from_static(b"--a\r\n"), + Bytes::from_static(b"X-Long: 12345678"), + Bytes::from_static(b"9012345678901234"), + Bytes::from_static(b"5678901234567890"), + ] + .map(Ok), + ); + + let mut multipart = create_multipart_with_buffer_limit(body, 24); + let res = multipart.next().await.unwrap(); + + assert_matches!(res, Err(Error::Payload(PayloadError::Overflow))); + } + + #[actix_rt::test] + async fn raw_extractor_uses_configured_buffer_limit() { + let (req, mut payload) = TestRequest::default() + .insert_header((header::CONTENT_TYPE, "multipart/mixed; boundary=\"a\"")) + .app_data(MultipartConfig::default().buffer_limit(16)) + .set_payload(Bytes::from_static(b"aaaaaaaabbbbbbbbcccccccc")) + .to_http_parts(); + + let mut multipart = Multipart::from_request(&req, &mut payload).await.unwrap(); + let res = multipart.next().await.unwrap(); + + assert_matches!(res, Err(Error::Payload(PayloadError::Overflow))); + } + + #[actix_rt::test] + async fn valid_large_field_streams_through_small_parser_buffer() { + let mut bytes = BytesMut::new(); + bytes.put(&b"--a\r\nContent-Length: 100\r\n\r\n"[..]); + bytes.put(&[b'x'; 100][..]); + bytes.put(&b"\r\n--a--\r\n"[..]); + let body = stream::once(async { Ok(bytes.freeze()) }); + + let mut multipart = create_multipart_with_buffer_limit(body, 32); + let mut field = multipart.next().await.unwrap().unwrap(); + + assert_eq!( + get_whole_field(&mut field).await, + Bytes::from(vec![b'x'; 100]) + ); + drop(field); + assert!(multipart.next().await.is_none()); + } + #[actix_rt::test] async fn test_multipart_no_end_crlf() { let (sender, payload) = create_stream(); diff --git a/actix-multipart/src/payload.rs b/actix-multipart/src/payload.rs index 858634bc0..4c9929aed 100644 --- a/actix-multipart/src/payload.rs +++ b/actix-multipart/src/payload.rs @@ -14,6 +14,9 @@ use futures_core::stream::{LocalBoxStream, Stream}; use crate::{error::Error, safety::Safety}; +pub(crate) const DEFAULT_BUFFER_LIMIT: usize = 65_536; // 64 KiB +const MAX_READY_CHUNKS_PER_POLL: usize = 16; + pub(crate) struct PayloadRef { payload: Rc>, } @@ -45,31 +48,64 @@ impl Clone for PayloadRef { /// Payload buffer. pub(crate) struct PayloadBuffer { pub(crate) stream: LocalBoxStream<'static, Result>, + pending: Option, pub(crate) buf: BytesMut, + buffer_limit: usize, /// EOF flag. If true, no more payload reads will be attempted. pub(crate) eof: bool, } impl PayloadBuffer { /// Constructs new payload buffer. - pub(crate) fn new(stream: S) -> Self + pub(crate) fn new_with_limit(stream: S, buffer_limit: usize) -> Self where S: Stream> + 'static, { PayloadBuffer { stream: Box::pin(stream), + pending: None, buf: BytesMut::with_capacity(1_024), // pre-allocate 1KiB + buffer_limit, eof: false, } } + /// Polls a bounded amount of payload into the parser buffer. + /// + /// This does not drain the stream to EOF in one call. Callers must be prepared to poll again + /// after consuming buffered data. pub(crate) fn poll_stream(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> { - loop { + if self.buffer_limit == 0 { + return Err(PayloadError::Overflow); + } + + let mut appended = false; + + for _ in 0..MAX_READY_CHUNKS_PER_POLL { + if self.pending.is_some() { + appended |= self.append_pending()?; + + if self.pending.is_some() || self.buf.len() >= self.buffer_limit { + if appended { + cx.waker().wake_by_ref(); + } + return Ok(()); + } + + continue; + } + match Pin::new(&mut self.stream).poll_next(cx) { Poll::Ready(Some(Ok(data))) => { - self.buf.extend_from_slice(&data); - // try to read more data - continue; + self.pending = Some(data); + appended |= self.append_pending()?; + + if self.pending.is_some() || self.buf.len() >= self.buffer_limit { + if appended { + cx.waker().wake_by_ref(); + } + return Ok(()); + } } Poll::Ready(Some(Err(err))) => return Err(err), Poll::Ready(None) => { @@ -79,6 +115,40 @@ impl PayloadBuffer { Poll::Pending => return Ok(()), } } + + if appended { + cx.waker().wake_by_ref(); + } + + Ok(()) + } + + fn append_pending(&mut self) -> Result { + let Some(mut data) = self.pending.take() else { + return Ok(false); + }; + + if data.is_empty() { + return Ok(false); + } + + if self.buf.len() >= self.buffer_limit { + self.pending = Some(data); + return Err(PayloadError::Overflow); + } + + let available = self.buffer_limit - self.buf.len(); + let len = cmp::min(data.len(), available); + + if len == data.len() { + self.buf.extend_from_slice(&data); + } else { + let chunk = data.split_to(len); + self.buf.extend_from_slice(&chunk); + self.pending = Some(data); + } + + Ok(len != 0) } /// Reads exact number of bytes. @@ -162,7 +232,7 @@ mod tests { #[actix_rt::test] async fn basic() { let (_, payload) = h1::Payload::create(false); - let mut payload = PayloadBuffer::new(payload); + let mut payload = PayloadBuffer::new_with_limit(payload, DEFAULT_BUFFER_LIMIT); assert_eq!(payload.buf.len(), 0); lazy(|cx| payload.poll_stream(cx)).await.unwrap(); @@ -172,7 +242,7 @@ mod tests { #[actix_rt::test] async fn eof() { let (mut sender, payload) = h1::Payload::create(false); - let mut payload = PayloadBuffer::new(payload); + let mut payload = PayloadBuffer::new_with_limit(payload, DEFAULT_BUFFER_LIMIT); assert_eq!(None, payload.read_max(4).unwrap()); sender.feed_data(Bytes::from("data")); @@ -181,6 +251,8 @@ mod tests { assert_eq!(Some(Bytes::from("data")), payload.read_max(4).unwrap()); assert_eq!(payload.buf.len(), 0); + + lazy(|cx| payload.poll_stream(cx)).await.unwrap(); assert!(payload.read_max(1).is_err()); assert!(payload.eof); } @@ -188,7 +260,7 @@ mod tests { #[actix_rt::test] async fn err() { let (mut sender, payload) = h1::Payload::create(false); - let mut payload = PayloadBuffer::new(payload); + let mut payload = PayloadBuffer::new_with_limit(payload, DEFAULT_BUFFER_LIMIT); assert_eq!(None, payload.read_max(1).unwrap()); sender.set_error(PayloadError::Incomplete(None)); lazy(|cx| payload.poll_stream(cx)).await.err().unwrap(); @@ -197,11 +269,12 @@ mod tests { #[actix_rt::test] async fn read_max() { let (mut sender, payload) = h1::Payload::create(false); - let mut payload = PayloadBuffer::new(payload); + let mut payload = PayloadBuffer::new_with_limit(payload, DEFAULT_BUFFER_LIMIT); sender.feed_data(Bytes::from("line1")); sender.feed_data(Bytes::from("line2")); lazy(|cx| payload.poll_stream(cx)).await.unwrap(); + lazy(|cx| payload.poll_stream(cx)).await.unwrap(); assert_eq!(payload.buf.len(), 10); assert_eq!(Some(Bytes::from("line1")), payload.read_max(5).unwrap()); @@ -214,13 +287,14 @@ mod tests { #[actix_rt::test] async fn read_exactly() { let (mut sender, payload) = h1::Payload::create(false); - let mut payload = PayloadBuffer::new(payload); + let mut payload = PayloadBuffer::new_with_limit(payload, DEFAULT_BUFFER_LIMIT); assert_eq!(None, payload.read_exact(2)); sender.feed_data(Bytes::from("line1")); sender.feed_data(Bytes::from("line2")); lazy(|cx| payload.poll_stream(cx)).await.unwrap(); + lazy(|cx| payload.poll_stream(cx)).await.unwrap(); assert_eq!(Some(Bytes::from_static(b"li")), payload.read_exact(2)); assert_eq!(payload.buf.len(), 8); @@ -232,13 +306,14 @@ mod tests { #[actix_rt::test] async fn read_until() { let (mut sender, payload) = h1::Payload::create(false); - let mut payload = PayloadBuffer::new(payload); + let mut payload = PayloadBuffer::new_with_limit(payload, DEFAULT_BUFFER_LIMIT); assert_eq!(None, payload.read_until(b"ne").unwrap()); sender.feed_data(Bytes::from("line1")); sender.feed_data(Bytes::from("line2")); lazy(|cx| payload.poll_stream(cx)).await.unwrap(); + lazy(|cx| payload.poll_stream(cx)).await.unwrap(); assert_eq!( Some(Bytes::from("line")), @@ -252,4 +327,38 @@ mod tests { ); assert_eq!(payload.buf.len(), 0); } + + #[actix_rt::test] + async fn poll_stream_does_not_exceed_buffer_limit() { + let stream = futures_util::stream::iter([ + Ok(Bytes::from_static(b"12345678")), + Ok(Bytes::from_static(b"abcdefgh")), + Ok(Bytes::from_static(b"overflow")), + ]); + let mut payload = PayloadBuffer::new_with_limit(stream, 16); + + lazy(|cx| payload.poll_stream(cx)).await.unwrap(); + assert_eq!(payload.buf.len(), 16); + + let err = lazy(|cx| payload.poll_stream(cx)).await.unwrap_err(); + assert!(matches!(err, PayloadError::Overflow)); + assert_eq!(payload.buf.len(), 16); + } + + #[actix_rt::test] + async fn oversized_chunk_can_be_consumed_incrementally() { + let stream = futures_util::stream::once(async { Ok(Bytes::from_static(b"12345678")) }); + let mut payload = PayloadBuffer::new_with_limit(stream, 4); + + lazy(|cx| payload.poll_stream(cx)).await.unwrap(); + assert_eq!(payload.buf, Bytes::from_static(b"1234")); + assert_eq!(payload.read_max(4).unwrap().unwrap(), "1234"); + + lazy(|cx| payload.poll_stream(cx)).await.unwrap(); + assert_eq!(payload.buf, Bytes::from_static(b"5678")); + assert_eq!(payload.read_max(4).unwrap().unwrap(), "5678"); + + lazy(|cx| payload.poll_stream(cx)).await.unwrap(); + assert!(payload.eof); + } } From 4434a494eef7706aa3fbd82fe77e1be92e943f2e Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sat, 18 Apr 2026 00:57:50 +0900 Subject: [PATCH 06/19] =?UTF-8?q?fix(multipart):=20count=20ignored=20field?= =?UTF-8?q?s=20towards=20`MultipartFormConfig`=20li=E2=80=A6=20(#4026)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(multipart): count ignored fields towards `MultipartFormConfig` limits --- actix-multipart-derive/src/lib.rs | 4 +-- actix-multipart/src/form/mod.rs | 44 +++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/actix-multipart-derive/src/lib.rs b/actix-multipart-derive/src/lib.rs index 5b5e28254..161a9ba24 100644 --- a/actix-multipart-derive/src/lib.rs +++ b/actix-multipart-derive/src/lib.rs @@ -227,7 +227,7 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS ::actix_multipart::MultipartError::UnknownField(field.name().unwrap().to_string()) )) } else { - quote!(::std::result::Result::Ok(())) + quote!(::actix_multipart::form::discard_field(field, limits).await) }; // Value for duplicate action @@ -289,7 +289,7 @@ pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenS ) -> ::std::pin::Pin<::std::boxed::Box> + 't>> { match field.name().unwrap() { #handle_field_impl - _ => return ::std::boxed::Box::pin(::std::future::ready(#unknown_field_result)), + _ => return ::std::boxed::Box::pin(async move { #unknown_field_result }), } } diff --git a/actix-multipart/src/form/mod.rs b/actix-multipart/src/form/mod.rs index cb89b7cc1..de0eeecaa 100644 --- a/actix-multipart/src/form/mod.rs +++ b/actix-multipart/src/form/mod.rs @@ -82,7 +82,9 @@ where ) -> Self::Future { if state.contains_key(&field.form_field_name) { match duplicate_field { - DuplicateField::Ignore => return Box::pin(ready(Ok(()))), + DuplicateField::Ignore => { + return Box::pin(async move { discard_field(field, limits).await }); + } DuplicateField::Deny => { return Box::pin(ready(Err(MultipartError::DuplicateField( @@ -159,7 +161,9 @@ where ) -> Self::Future { if state.contains_key(&field.form_field_name) { match duplicate_field { - DuplicateField::Ignore => return Box::pin(ready(Ok(()))), + DuplicateField::Ignore => { + return Box::pin(async move { discard_field(field, limits).await }); + } DuplicateField::Deny => { return Box::pin(ready(Err(MultipartError::DuplicateField( @@ -312,6 +316,16 @@ impl Limits { } } +/// Drain a field that will not be retained while still accounting for form limits. +#[doc(hidden)] +pub async fn discard_field(mut field: Field, limits: &mut Limits) -> Result<(), MultipartError> { + while let Some(chunk) = field.try_next().await? { + limits.try_consume_limits(chunk.len(), false)?; + } + + Ok(()) +} + /// Typed `multipart/form-data` extractor. /// /// To extract typed data from a multipart stream, the inner type `T` must implement the @@ -710,6 +724,32 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); } + #[actix_rt::test] + async fn test_discarded_fields_count_towards_total_limit() { + let srv = actix_test::start(|| { + App::new() + .route("/unknown", web::post().to(test_upload_limits_memory)) + .route("/duplicate", web::post().to(test_duplicate_ignore_route)) + .app_data( + MultipartFormConfig::default() + .memory_limit(usize::MAX) + .total_limit(20), + ) + }); + + let mut form = multipart::Form::default(); + form.add_text("field", "7 bytes"); + form.add_text("unknown", "this string is 28 bytes long"); + let response = send_form(&srv, form, "/unknown").await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + let mut form = multipart::Form::default(); + form.add_text("field", "first_value"); + form.add_text("field", "this string is 28 bytes long"); + let response = send_form(&srv, form, "/duplicate").await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + } + /// Test the Limits. #[derive(MultipartForm)] struct TestMemoryUploadLimits { From e6d09913d9ebac53c8654a3574c19861900227f5 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sat, 18 Apr 2026 02:12:22 +0900 Subject: [PATCH 07/19] chore(multipart,derive): prepare 0.8.0 (#4027) --- Cargo.lock | 4 ++-- actix-multipart-derive/CHANGES.md | 2 ++ actix-multipart-derive/Cargo.toml | 4 ++-- actix-multipart-derive/README.md | 4 ++-- actix-multipart/CHANGES.md | 2 ++ actix-multipart/Cargo.toml | 4 ++-- actix-multipart/README.md | 4 ++-- 7 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de37efdc5..640e0844d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,7 +142,7 @@ dependencies = [ [[package]] name = "actix-multipart" -version = "0.7.2" +version = "0.8.0" dependencies = [ "actix-http", "actix-multipart-derive", @@ -175,7 +175,7 @@ dependencies = [ [[package]] name = "actix-multipart-derive" -version = "0.7.0" +version = "0.8.0" dependencies = [ "actix-multipart", "actix-web", diff --git a/actix-multipart-derive/CHANGES.md b/actix-multipart-derive/CHANGES.md index 44e0d8435..a5f30757c 100644 --- a/actix-multipart-derive/CHANGES.md +++ b/actix-multipart-derive/CHANGES.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.8.0 + - Minimum supported Rust version (MSRV) is now 1.88. ## 0.7.0 diff --git a/actix-multipart-derive/Cargo.toml b/actix-multipart-derive/Cargo.toml index 9859f6c8b..aee19b388 100644 --- a/actix-multipart-derive/Cargo.toml +++ b/actix-multipart-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart-derive" -version = "0.7.0" +version = "0.8.0" authors = ["Jacob Halsey "] description = "Multipart form derive macro for Actix Web" keywords = ["http", "web", "framework", "async", "futures"] @@ -24,7 +24,7 @@ quote = "1" syn = "2" [dev-dependencies] -actix-multipart = "0.7" +actix-multipart = "0.8" actix-web = "4" rustversion-msrv = "0.100" trybuild = "1" diff --git a/actix-multipart-derive/README.md b/actix-multipart-derive/README.md index 05b37dfe8..02a4f94e4 100644 --- a/actix-multipart-derive/README.md +++ b/actix-multipart-derive/README.md @@ -5,11 +5,11 @@ [![crates.io](https://img.shields.io/crates/v/actix-multipart-derive?label=latest)](https://crates.io/crates/actix-multipart-derive) -[![Documentation](https://docs.rs/actix-multipart-derive/badge.svg?version=0.7.0)](https://docs.rs/actix-multipart-derive/0.7.0) +[![Documentation](https://docs.rs/actix-multipart-derive/badge.svg?version=0.8.0)](https://docs.rs/actix-multipart-derive/0.8.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-multipart-derive.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart-derive/0.7.0/status.svg)](https://deps.rs/crate/actix-multipart-derive/0.7.0) +[![dependency status](https://deps.rs/crate/actix-multipart-derive/0.8.0/status.svg)](https://deps.rs/crate/actix-multipart-derive/0.8.0) [![Download](https://img.shields.io/crates/d/actix-multipart-derive.svg)](https://crates.io/crates/actix-multipart-derive) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 1d5ddf55b..62a78d3d3 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.8.0 + - Add multi-field multipart payload builders to `actix_multipart::test`. [#3575] - Add `MultipartForm` support for `Option>` fields. [#3577] - Bound internal multipart parser buffering to prevent unbounded memory growth on malformed bodies. diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 29c30df7b..317abaca4 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.7.2" +version = "0.8.0" authors = [ "Nikolay Kim ", "Jacob Halsey ", @@ -37,7 +37,7 @@ derive = ["actix-multipart-derive"] tempfile = ["dep:tempfile", "tokio/fs"] [dependencies] -actix-multipart-derive = { version = "=0.7.0", optional = true } +actix-multipart-derive = { version = "=0.8.0", optional = true } actix-utils = "3" actix-web = { version = "4", default-features = false } diff --git a/actix-multipart/README.md b/actix-multipart/README.md index 1faacd6a8..faebee04c 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -3,11 +3,11 @@ [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) -[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.7.2)](https://docs.rs/actix-multipart/0.7.2) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.8.0)](https://docs.rs/actix-multipart/0.8.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-multipart.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart/0.7.2/status.svg)](https://deps.rs/crate/actix-multipart/0.7.2) +[![dependency status](https://deps.rs/crate/actix-multipart/0.8.0/status.svg)](https://deps.rs/crate/actix-multipart/0.8.0) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) From 3c056bd36128d41ba130a8704ef5d648de3f2870 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sat, 18 Apr 2026 11:09:12 +0900 Subject: [PATCH 08/19] Merge commit from fork --- actix-http/CHANGES.md | 1 + actix-http/src/h1/codec.rs | 14 +++++++ actix-http/src/h1/decoder.rs | 78 +++++++++++++++++++++++++++++++----- 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index ba4d0aa9d..115774f6a 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -2,6 +2,7 @@ ## Unreleased +- Reject HTTP/1 requests with ambiguous request framing from `Content-Length` and `Transfer-Encoding` headers to prevent request smuggling. - Encode the HTTP/1 `Connection: Upgrade` header in Camel-Case when camel-case header formatting is enabled.[#3953] - Fix `HeaderMap` iterators' `len()` and `size_hint()` implementations for multi-value headers. - Update `rand` dependency to `0.10`. diff --git a/actix-http/src/h1/codec.rs b/actix-http/src/h1/codec.rs index 2b452f8f8..13fc7e6c5 100644 --- a/actix-http/src/h1/codec.rs +++ b/actix-http/src/h1/codec.rs @@ -237,4 +237,18 @@ mod tests { assert_eq!(*req.method(), Method::POST); assert!(req.chunked().unwrap()); } + + #[actix_rt::test] + async fn test_http_request_rejects_content_length_and_chunked() { + let mut codec = Codec::default(); + let mut buf = BytesMut::from( + "POST /test HTTP/1.1\r\n\ + content-length: 11\r\n\ + transfer-encoding: chunked\r\n\r\n\ + 0\r\n\r\n\ + GET /test2 HTTP/1.1\r\n\r\n", + ); + + assert!(codec.decode(&mut buf).is_err()); + } } diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index af64e8802..5170ea4a1 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -275,6 +275,23 @@ impl MessageType for Request { // convert headers let mut length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?; + if msg.head().headers.contains_key(header::TRANSFER_ENCODING) { + if ver == Version::HTTP_10 { + debug!("Transfer-Encoding is not allowed in HTTP/1.0 requests"); + return Err(ParseError::Header); + } + + if !crate::HttpMessage::chunked(&msg)? { + debug!("request Transfer-Encoding must be chunked"); + return Err(ParseError::Header); + } + + if msg.head().headers.contains_key(header::CONTENT_LENGTH) { + debug!("both Content-Length and Transfer-Encoding are set"); + return Err(ParseError::Header); + } + } + // disallow HTTP/1.0 POST requests that do not contain a Content-Length headers // see https://datatracker.ietf.org/doc/html/rfc1945#section-7.2.2 if ver == Version::HTTP_10 && method == Method::POST && length.is_none() { @@ -1116,18 +1133,57 @@ mod tests { #[test] fn hrs_cl_and_te_http10() { - // in HTTP/1.0 transfer encoding is simply ignored so it's fine to have both - - let mut buf = BytesMut::from( + expect_parse_err!(&mut BytesMut::from( "GET / HTTP/1.0\r\n\ Host: example.com\r\n\ Content-Length: 3\r\n\ Transfer-Encoding: chunked\r\n\ \r\n\ 000", - ); + )); + } - parse_ready!(&mut buf); + #[test] + fn hrs_cl_and_chunked_te_http11() { + expect_parse_err!(&mut BytesMut::from( + "POST / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Content-Length: 3\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 0\r\n\ + \r\n", + )); + + expect_parse_err!(&mut BytesMut::from( + "POST / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Transfer-Encoding: chunked\r\n\ + Content-Length: 3\r\n\ + \r\n\ + 0\r\n\ + \r\n", + )); + } + + #[test] + fn hrs_identity_te_http11() { + expect_parse_err!(&mut BytesMut::from( + "POST / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Transfer-Encoding: identity\r\n\ + \r\n\ + 0\r\n", + )); + + expect_parse_err!(&mut BytesMut::from( + "POST / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Content-Length: 3\r\n\ + Transfer-Encoding: identity\r\n\ + \r\n\ + 0\r\n", + )); } #[test] @@ -1165,14 +1221,16 @@ mod tests { } #[test] - fn transfer_encoding_agrees() { + fn hrs_chunked_te_http11() { let mut buf = BytesMut::from( "GET /test HTTP/1.1\r\n\ Host: example.com\r\n\ - Content-Length: 3\r\n\ - Transfer-Encoding: identity\r\n\ + Transfer-Encoding: chunked\r\n\ \r\n\ - 0\r\n", + 1\r\n\ + a\r\n\ + 0\r\n\ + \r\n", ); let mut reader = MessageDecoder::::default(); @@ -1180,6 +1238,6 @@ mod tests { let mut pl = pl.unwrap(); let chunk = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"0\r\n"))); + assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"a"))); } } From 0fb89457eda4a78a4cb7ccb3fdebe49a143ce2d5 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sat, 18 Apr 2026 11:41:32 +0900 Subject: [PATCH 09/19] chore(http): prepare v3.12.1 (#4029) --- Cargo.lock | 2 +- actix-http/CHANGES.md | 6 +++++- actix-http/Cargo.toml | 2 +- actix-web/Cargo.toml | 2 +- awc/Cargo.toml | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 640e0844d..08f1e7b1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,7 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.12.0" +version = "3.12.1" dependencies = [ "actix-codec", "actix-http-test", diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 115774f6a..c0ff503fd 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -2,7 +2,11 @@ ## Unreleased -- Reject HTTP/1 requests with ambiguous request framing from `Content-Length` and `Transfer-Encoding` headers to prevent request smuggling. +## 3.12.1 + +**Notice: This release contains a security fix. Users are encouraged to update to this version ASAP.** + +- SECURITY: Reject HTTP/1 requests with ambiguous request framing from `Content-Length` and `Transfer-Encoding` headers to prevent request smuggling. - Encode the HTTP/1 `Connection: Upgrade` header in Camel-Case when camel-case header formatting is enabled.[#3953] - Fix `HeaderMap` iterators' `len()` and `size_hint()` implementations for multi-value headers. - Update `rand` dependency to `0.10`. diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index a617459b8..07a4090c9 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.12.0" +version = "3.12.1" authors = ["Nikolay Kim ", "Rob Ede "] description = "HTTP types and services for the Actix ecosystem" keywords = ["actix", "http", "framework", "async", "futures"] diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index bb24ab175..e1cae53e3 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -137,7 +137,7 @@ actix-service = "2" actix-tls = { version = "3.4", default-features = false, optional = true } actix-utils = "3" -actix-http = "3.12.0" +actix-http = "3.12.1" actix-router = { version = "0.5.4", default-features = false, features = ["http"] } actix-web-codegen = { version = "4.3", optional = true, default-features = false } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 86b319e42..3fb11b584 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -98,7 +98,7 @@ dangerous-h2c = [] [dependencies] actix-codec = "0.5" -actix-http = { version = "3.12.0", features = ["http2", "ws"] } +actix-http = { version = "3.12.1", features = ["http2", "ws"] } actix-rt = { version = "2.1", default-features = false } actix-service = "2" actix-tls = { version = "3.4", features = ["connect", "uri"] } From 10609f749d619087bdc42810e536e619e49ca440 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sat, 18 Apr 2026 12:46:44 +0900 Subject: [PATCH 10/19] actix-http: linger after early responses (#3985) Co-authored-by: Ophir LOJKINE --- actix-http/CHANGES.md | 4 + actix-http/src/h1/dispatcher.rs | 227 ++++++++++++++++++++------ actix-http/src/h1/dispatcher_tests.rs | 206 +++++++++++++++++++---- actix-web/src/server.rs | 2 +- 4 files changed, 359 insertions(+), 80 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index c0ff503fd..e68acb254 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -2,6 +2,10 @@ ## Unreleased +- When configured, gracefully close HTTP/1 connections after early responses to unread request bodies. [#3967] + +[#3967]: https://github.com/actix/actix-web/issues/3967 + ## 3.12.1 **Notice: This release contains a security fix. Users are encouraged to update to this version ASAP.** diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 2ed78cfca..267b9dfb1 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -31,7 +31,7 @@ use crate::{ config::ServiceConfig, error::{DispatchError, ParseError, PayloadError}, service::HttpFlow, - Error, Extensions, HttpMessage, OnConnectData, Request, Response, StatusCode, + ConnectionType, Error, Extensions, HttpMessage, OnConnectData, Request, Response, StatusCode, }; const LW_BUFFER_SIZE: usize = 1024; @@ -58,6 +58,9 @@ bitflags! { /// Set if write-half is disconnected. const WRITE_DISCONNECT = 0b0010_0000; + + /// Set while gracefully closing a connection after an early response. + const LINGER = 0b0100_0000; } } @@ -361,6 +364,65 @@ where io.poll_flush(cx) } + fn enter_linger(mut self: Pin<&mut Self>) { + let this = self.as_mut().project(); + this.flags.remove(Flags::KEEP_ALIVE); + this.flags.insert(Flags::LINGER | Flags::FINISHED); + } + + fn ensure_linger_timer(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> bool { + let this = self.as_mut().project(); + + if matches!(this.shutdown_timer, TimerState::Active { .. }) { + return true; + } + + if let Some(deadline) = this.config.client_disconnect_deadline() { + this.shutdown_timer + .set_and_init(cx, sleep_until(deadline.into()), line!()); + true + } else { + false + } + } + + fn poll_linger( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Result, DispatchError> { + if self.as_mut().poll_flush(cx)?.is_pending() { + return Ok(Poll::Pending); + } + + if !self.as_mut().ensure_linger_timer(cx) { + let this = self.as_mut().project(); + this.flags.remove(Flags::LINGER); + this.flags.insert(Flags::SHUTDOWN); + return Ok(Poll::Ready(())); + } + + loop { + let should_disconnect = self.as_mut().read_available(cx)?; + let this = self.as_mut().project(); + let mut progressed = false; + + if !this.read_buf.is_empty() { + this.read_buf.clear(); + progressed = true; + } + + if should_disconnect { + this.flags.remove(Flags::LINGER); + this.flags.insert(Flags::READ_DISCONNECT | Flags::SHUTDOWN); + return Ok(Poll::Ready(())); + } + + if !progressed { + return Ok(Poll::Pending); + } + } + } + fn send_response_inner( self: Pin<&mut Self>, res: Response<()>, @@ -385,54 +447,90 @@ where fn send_response( mut self: Pin<&mut Self>, - res: Response<()>, + mut res: Response<()>, body: B, ) -> Result<(), DispatchError> { - let size = self.as_mut().send_response_inner(res, &body)?; - let mut this = self.project(); - this.state.set(match size { - BodySize::None | BodySize::Sized(0) => { - 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 close_after_response = { + let this = self.as_mut().project(); + should_close_after_response(this.payload.as_ref(), *this.payload_drainable) + }; - if payload_unfinished && !drain_payload { - this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED); + if close_after_response { + res.head_mut().set_connection_type(ConnectionType::Close); + } + + let size = self.as_mut().send_response_inner(res, &body)?; + match size { + BodySize::None | BodySize::Sized(0) => { + let this = self.as_mut().project(); + + if close_after_response { + if this.config.client_disconnect_deadline().is_some() { + drop(this); + self.as_mut().enter_linger(); + } else { + self.as_mut() + .project() + .flags + .insert(Flags::SHUTDOWN | Flags::FINISHED); + } } else { this.flags.insert(Flags::FINISHED); } - State::None + self.as_mut().project().state.set(State::None); } - _ => State::SendPayload { body }, - }); + _ => self + .as_mut() + .project() + .state + .set(State::SendPayload { body }), + } Ok(()) } fn send_error_response( mut self: Pin<&mut Self>, - res: Response<()>, + mut res: Response<()>, body: BoxBody, ) -> Result<(), DispatchError> { - let size = self.as_mut().send_response_inner(res, &body)?; - let mut this = self.project(); - this.state.set(match size { - BodySize::None | BodySize::Sized(0) => { - 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 close_after_response = { + let this = self.as_mut().project(); + should_close_after_response(this.payload.as_ref(), *this.payload_drainable) + }; - if payload_unfinished && !drain_payload { - this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED); + if close_after_response { + res.head_mut().set_connection_type(ConnectionType::Close); + } + + let size = self.as_mut().send_response_inner(res, &body)?; + match size { + BodySize::None | BodySize::Sized(0) => { + let this = self.as_mut().project(); + + if close_after_response { + if this.config.client_disconnect_deadline().is_some() { + drop(this); + self.as_mut().enter_linger(); + } else { + self.as_mut() + .project() + .flags + .insert(Flags::SHUTDOWN | Flags::FINISHED); + } } else { this.flags.insert(Flags::FINISHED); } - State::None + self.as_mut().project().state.set(State::None); } - _ => State::SendErrorPayload { body }, - }); + _ => self + .as_mut() + .project() + .state + .set(State::SendErrorPayload { body }), + } Ok(()) } @@ -534,18 +632,26 @@ where // this.payload was the payload for the request we just finished // 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 close_after_response = should_close_after_response( + this.payload.as_ref(), + *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 && !drain_payload { - this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED); + if not_pipelined && close_after_response { + if this.config.client_disconnect_deadline().is_some() { + drop(this); + self.as_mut().enter_linger(); + } else { + self.as_mut() + .project() + .flags + .insert(Flags::SHUTDOWN | Flags::FINISHED); + } } else { this.flags.insert(Flags::FINISHED); } @@ -588,18 +694,26 @@ where // this.payload was the payload for the request we just finished // 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 close_after_response = should_close_after_response( + this.payload.as_ref(), + *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 && !drain_payload { - this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED); + if not_pipelined && close_after_response { + if this.config.client_disconnect_deadline().is_some() { + drop(this); + self.as_mut().enter_linger(); + } else { + self.as_mut() + .project() + .flags + .insert(Flags::SHUTDOWN | Flags::FINISHED); + } } else { this.flags.insert(Flags::FINISHED); } @@ -960,14 +1074,20 @@ where let this = self.as_mut().project(); if let TimerState::Active { timer } = this.shutdown_timer { debug_assert!( - this.flags.contains(Flags::SHUTDOWN), - "shutdown flag should be set when timer is active", + this.flags.intersects(Flags::LINGER | Flags::SHUTDOWN), + "shutdown or linger flag should be set when timer is active", ); - // timed-out during shutdown; drop connection if timer.as_mut().poll(cx).is_ready() { - trace!("timed-out during shutdown"); - return Err(DispatchError::DisconnectTimeout); + if this.flags.contains(Flags::LINGER) { + trace!("timed-out during linger; shutting down connection"); + this.flags.remove(Flags::LINGER); + this.flags.insert(Flags::SHUTDOWN); + this.shutdown_timer.clear(line!()); + } else { + trace!("timed-out during shutdown"); + return Err(DispatchError::DisconnectTimeout); + } } } @@ -1133,7 +1253,15 @@ where inner.as_mut().poll_timers(cx)?; - let poll = if inner.flags.contains(Flags::SHUTDOWN) { + let poll = if inner.flags.contains(Flags::LINGER) { + match inner.as_mut().poll_linger(cx)? { + Poll::Ready(()) => { + cx.waker().wake_by_ref(); + Poll::Pending + } + Poll::Pending => Poll::Pending, + } + } else if inner.flags.contains(Flags::SHUTDOWN) { if inner.flags.contains(Flags::WRITE_DISCONNECT) { Poll::Ready(Ok(())) } else { @@ -1281,7 +1409,7 @@ where inner_p.shutdown_timer, ); - if inner_p.flags.contains(Flags::SHUTDOWN) { + if inner_p.flags.intersects(Flags::LINGER | Flags::SHUTDOWN) { cx.waker().wake_by_ref(); } Poll::Pending @@ -1295,6 +1423,13 @@ where } } +fn should_close_after_response(payload: Option<&PayloadSender>, payload_drainable: bool) -> bool { + let payload_unfinished = payload.is_some(); + let drain_payload = payload.is_some_and(|pl| pl.is_dropped()) && payload_drainable; + + payload_unfinished && !drain_payload +} + #[allow(dead_code)] fn trace_timer_states( label: &str, diff --git a/actix-http/src/h1/dispatcher_tests.rs b/actix-http/src/h1/dispatcher_tests.rs index e3a907e5c..fb9c1823b 100644 --- a/actix-http/src/h1/dispatcher_tests.rs +++ b/actix-http/src/h1/dispatcher_tests.rs @@ -7,7 +7,10 @@ use std::{ }; use actix_codec::Framed; -use actix_rt::{pin, time::sleep}; +use actix_rt::{ + pin, + time::{sleep, timeout}, +}; use actix_service::{fn_service, Service}; use actix_utils::future::{ready, Ready}; use bytes::{Buf, Bytes, BytesMut}; @@ -84,6 +87,11 @@ fn drop_payload_service() -> impl Service impl Service, Error = Error> { + fn_service(|_req: Request| ready(Ok::<_, Error>(Response::with_body(StatusCode::OK, "ok")))) +} + fn echo_payload_service() -> impl Service, Error = Error> { fn_service(|mut req: Request| { Box::pin(async move { @@ -536,15 +544,14 @@ async fn pipelining_ok_then_ok() { } #[actix_rt::test] -async fn early_response_with_payload_closes_connection() { +async fn early_response_with_payload_lingers_before_closing() { lazy(|cx| { - let buf = TestBuffer::new( - "\ - GET /unfinished HTTP/1.1\r\n\ - Content-Length: 2\r\n\ - \r\n\ - ", - ); + let buf = TestSeqBuffer::new(http_msg( + r" + GET /unfinished HTTP/1.1 + Content-Length: 2 + ", + )); let cfg = ServiceConfig::new( KeepAlive::Os, @@ -569,39 +576,172 @@ async fn early_response_with_payload_closes_connection() { assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); match h1.as_mut().poll(cx) { - Poll::Pending => panic!("Should have shut down"), - Poll::Ready(res) => assert!(res.is_ok()), + Poll::Pending => {} + Poll::Ready(res) => panic!("should still be lingering: {:?}", res), } - // polls: initial => shutdown - assert_eq!(h1.poll_count, 2); + // polls: initial + assert_eq!(h1.poll_count, 1); - { - let mut res = buf.write_buf_slice_mut(); - stabilize_date_header(&mut res); - let res = &res[..]; + let mut res = buf.take_write_buf().to_vec(); + stabilize_date_header(&mut res); + let res = &res[..]; - let exp = b"\ - HTTP/1.1 200 OK\r\n\ - content-length: 11\r\n\ - date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\ - /unfinished\ - "; + let exp = b"\ + HTTP/1.1 200 OK\r\n\ + content-length: 11\r\n\ + connection: close\r\n\ + date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\ + /unfinished\ + "; - assert_eq!( - res, - exp, - "\nexpected response not in write buffer:\n\ - response: {:?}\n\ - expected: {:?}", - String::from_utf8_lossy(res), - String::from_utf8_lossy(exp) - ); - } + assert_eq!( + res, + exp, + "\nexpected response not in write buffer:\n\ + response: {:?}\n\ + expected: {:?}", + String::from_utf8_lossy(res), + String::from_utf8_lossy(exp) + ); + + buf.close_read(); + + assert!(h1.as_mut().poll(cx).is_pending()); + assert!(h1.as_mut().poll(cx).is_ready()); }) .await; } +#[actix_rt::test] +async fn buffered_upload_ignored_by_handler_should_not_shutdown_immediately() { + lazy(|cx| { + let buf = TestSeqBuffer::new(http_msg( + r" + POST / HTTP/1.1 + Content-Length: 8 + + ab + ", + )); + + let cfg = ServiceConfig::new( + KeepAlive::Os, + Duration::from_millis(1), + Duration::from_millis(1), + false, + None, + ); + + let services = HttpFlow::new(ignore_payload_service(), ExpectHandler, None); + + let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( + buf.clone(), + services, + cfg, + None, + OnConnectData::default(), + ); + + pin!(h1); + + assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); + + match h1.as_mut().poll(cx) { + Poll::Pending => {} + Poll::Ready(res) => panic!("closed connection early: {:?}", res), + } + + 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: 2 + connection: close + date: Thu, 01 Jan 1970 12:34:56 UTC + + ok + ", + ); + + assert_eq!( + res, + exp, + "\nexpected response not in write buffer:\n\ + response: {:?}\n\ + expected: {:?}", + String::from_utf8_lossy(res), + String::from_utf8_lossy(&exp) + ); + + buf.close_read(); + + assert!(h1.as_mut().poll(cx).is_pending()); + assert!(h1.as_mut().poll(cx).is_ready()); + }) + .await; +} + +#[actix_rt::test] +async fn lingering_timeout_uses_graceful_shutdown() { + let buf = TestSeqBuffer::new( + "\ + POST / HTTP/1.1\r\n\ + Content-Length: 8\r\n\ + \r\n\ + ab\ + ", + ); + + let cfg = ServiceConfig::new( + KeepAlive::Disabled, + Duration::ZERO, + Duration::from_millis(1), + false, + None, + ); + + let services = HttpFlow::new(ignore_payload_service(), ExpectHandler, None); + + let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( + buf.clone(), + services, + cfg, + None, + OnConnectData::default(), + ); + + assert!(matches!( + timeout(Duration::from_millis(100), h1).await, + Ok(Ok(())) + )); + + let mut res = buf.take_write_buf().to_vec(); + stabilize_date_header(&mut res); + let res = &res[..]; + + let exp = b"\ + HTTP/1.1 200 OK\r\n\ + content-length: 2\r\n\ + connection: close\r\n\ + date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\ + ok\ + "; + + assert_eq!( + res, + exp, + "\nexpected response not in write buffer:\n\ + response: {:?}\n\ + expected: {:?}", + String::from_utf8_lossy(res), + String::from_utf8_lossy(exp) + ); +} + #[actix_rt::test] async fn pipelining_ok_then_bad() { lazy(|cx| { diff --git a/actix-web/src/server.rs b/actix-web/src/server.rs index 2f52b21fb..1965d458e 100644 --- a/actix-web/src/server.rs +++ b/actix-web/src/server.rs @@ -245,7 +245,7 @@ where /// /// To disable timeout set value to 0. /// - /// By default client timeout is set to 5000 milliseconds. + /// By default client timeout is set to 1000 milliseconds. pub fn client_disconnect_timeout(self, dur: Duration) -> Self { self.config.lock().unwrap().client_disconnect_timeout = dur; self From cffe5d271e8b619078952b68860ae64e5d11732d Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sat, 18 Apr 2026 13:22:45 +0900 Subject: [PATCH 11/19] http: add h1 write buffer size setting (#3986) Co-authored-by: Nicolas Grondin --- actix-http/CHANGES.md | 1 + actix-http/src/builder.rs | 30 ++++- actix-http/src/config.rs | 30 +++++ actix-http/src/h1/dispatcher.rs | 6 +- actix-http/src/h1/dispatcher_tests.rs | 172 ++++++++++++++++++++++++++ actix-web/CHANGES.md | 1 + actix-web/src/server.rs | 68 +++++++++- 7 files changed, 300 insertions(+), 8 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index e68acb254..68a6188c4 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -15,6 +15,7 @@ - Fix `HeaderMap` iterators' `len()` and `size_hint()` implementations for multi-value headers. - Update `rand` dependency to `0.10`. - Update `sha1` dependency to `0.11`. +- Add `ServiceConfigBuilder::h1_write_buffer_size()` and `HttpServiceBuilder::h1_write_buffer_size()`. [#3953]: https://github.com/actix/actix-web/pull/3953 diff --git a/actix-http/src/builder.rs b/actix-http/src/builder.rs index fff7ceefe..f14db068d 100644 --- a/actix-http/src/builder.rs +++ b/actix-http/src/builder.rs @@ -5,7 +5,9 @@ use actix_service::{IntoServiceFactory, Service, ServiceFactory}; use crate::{ body::{BoxBody, MessageBody}, - config::{DEFAULT_H2_CONN_WINDOW_SIZE, DEFAULT_H2_STREAM_WINDOW_SIZE}, + config::{ + DEFAULT_H1_WRITE_BUFFER_SIZE, DEFAULT_H2_CONN_WINDOW_SIZE, DEFAULT_H2_STREAM_WINDOW_SIZE, + }, h1::{self, ExpectHandler, H1Service, UpgradeHandler}, service::HttpService, ConnectCallback, Extensions, KeepAlive, Request, Response, ServiceConfigBuilder, @@ -22,6 +24,7 @@ pub struct HttpServiceBuilder { secure: bool, local_addr: Option, h1_allow_half_closed: bool, + h1_write_buffer_size: usize, h2_conn_window_size: u32, h2_stream_window_size: u32, expect: X, @@ -47,6 +50,7 @@ where secure: false, local_addr: None, h1_allow_half_closed: true, + h1_write_buffer_size: DEFAULT_H1_WRITE_BUFFER_SIZE, h2_conn_window_size: DEFAULT_H2_CONN_WINDOW_SIZE, h2_stream_window_size: DEFAULT_H2_STREAM_WINDOW_SIZE, @@ -151,6 +155,25 @@ where self } + /// Sets the maximum response write buffer size for HTTP/1 connections. + /// + /// Once the response buffer reaches this size, the dispatcher flushes it to the I/O stream. + /// + /// The default value is 32 KiB. + /// + /// # Panics + /// + /// Panics if `size` is 0. + pub fn h1_write_buffer_size(mut self, size: usize) -> Self { + assert!( + size > 0, + "HTTP/1 write buffer size must be greater than zero" + ); + + self.h1_write_buffer_size = size; + self + } + /// Sets initial stream-level flow control window size for HTTP/2 connections. /// /// See [`ServiceConfigBuilder::h2_initial_window_size`] for more details. @@ -187,6 +210,7 @@ where secure: self.secure, local_addr: self.local_addr, h1_allow_half_closed: self.h1_allow_half_closed, + h1_write_buffer_size: self.h1_write_buffer_size, h2_conn_window_size: self.h2_conn_window_size, h2_stream_window_size: self.h2_stream_window_size, expect: expect.into_factory(), @@ -215,6 +239,7 @@ where secure: self.secure, local_addr: self.local_addr, h1_allow_half_closed: self.h1_allow_half_closed, + h1_write_buffer_size: self.h1_write_buffer_size, h2_conn_window_size: self.h2_conn_window_size, h2_stream_window_size: self.h2_stream_window_size, expect: self.expect, @@ -254,6 +279,7 @@ where .secure(self.secure) .local_addr(self.local_addr) .h1_allow_half_closed(self.h1_allow_half_closed) + .h1_write_buffer_size(self.h1_write_buffer_size) .h2_initial_window_size(self.h2_stream_window_size) .h2_initial_connection_window_size(self.h2_conn_window_size) .build(); @@ -283,6 +309,7 @@ where .secure(self.secure) .local_addr(self.local_addr) .h1_allow_half_closed(self.h1_allow_half_closed) + .h1_write_buffer_size(self.h1_write_buffer_size) .h2_initial_window_size(self.h2_stream_window_size) .h2_initial_connection_window_size(self.h2_conn_window_size) .build(); @@ -309,6 +336,7 @@ where .secure(self.secure) .local_addr(self.local_addr) .h1_allow_half_closed(self.h1_allow_half_closed) + .h1_write_buffer_size(self.h1_write_buffer_size) .h2_initial_window_size(self.h2_stream_window_size) .h2_initial_connection_window_size(self.h2_conn_window_size) .build(); diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index c0fbc7521..9c86b9d63 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -18,6 +18,9 @@ pub(crate) const DEFAULT_H2_CONN_WINDOW_SIZE: u32 = 1024 * 1024 * 2; // 2MiB /// Matches awc's defaults to avoid poor throughput on high-BDP links. pub(crate) const DEFAULT_H2_STREAM_WINDOW_SIZE: u32 = 1024 * 1024; // 1MiB +/// Default HTTP/1 response write buffer size. +pub(crate) const DEFAULT_H1_WRITE_BUFFER_SIZE: usize = 32_768; + /// A builder for creating a [`ServiceConfig`] #[derive(Default, Debug)] pub struct ServiceConfigBuilder { @@ -86,6 +89,25 @@ impl ServiceConfigBuilder { self } + /// Sets the maximum response write buffer size for HTTP/1 connections. + /// + /// Once the response buffer reaches this size, the dispatcher flushes it to the I/O stream. + /// + /// The default value is 32 KiB. + /// + /// # Panics + /// + /// Panics if `size` is 0. + pub fn h1_write_buffer_size(mut self, size: usize) -> Self { + assert!( + size > 0, + "HTTP/1 write buffer size must be greater than zero" + ); + + self.inner.h1_write_buffer_size = size; + self + } + /// Sets initial stream-level flow control window size for HTTP/2 connections. /// /// Higher values can improve upload performance on high-latency links at the cost of higher @@ -128,6 +150,7 @@ struct Inner { tcp_nodelay: Option, date_service: DateService, h1_allow_half_closed: bool, + h1_write_buffer_size: usize, h2_conn_window_size: u32, h2_stream_window_size: u32, } @@ -143,6 +166,7 @@ impl Default for Inner { tcp_nodelay: None, date_service: DateService::new(), h1_allow_half_closed: true, + h1_write_buffer_size: DEFAULT_H1_WRITE_BUFFER_SIZE, h2_conn_window_size: DEFAULT_H2_CONN_WINDOW_SIZE, h2_stream_window_size: DEFAULT_H2_STREAM_WINDOW_SIZE, } @@ -167,6 +191,7 @@ impl ServiceConfig { tcp_nodelay: None, date_service: DateService::new(), h1_allow_half_closed: true, + h1_write_buffer_size: DEFAULT_H1_WRITE_BUFFER_SIZE, h2_conn_window_size: DEFAULT_H2_CONN_WINDOW_SIZE, h2_stream_window_size: DEFAULT_H2_STREAM_WINDOW_SIZE, })) @@ -228,6 +253,11 @@ impl ServiceConfig { self.0.h1_allow_half_closed } + /// HTTP/1 response write buffer size (in bytes). + pub fn h1_write_buffer_size(&self) -> usize { + self.0.h1_write_buffer_size + } + /// Returns configured `TCP_NODELAY` setting for accepted TCP connections. pub fn tcp_nodelay(&self) -> Option { self.0.tcp_nodelay diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 267b9dfb1..6ef48b038 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -171,6 +171,7 @@ pin_project! { pub(super) io: Option, read_buf: BytesMut, write_buf: BytesMut, + h1_write_buffer_size: usize, codec: Codec, } } @@ -284,6 +285,7 @@ where io: Some(io), read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), + h1_write_buffer_size: config.h1_write_buffer_size(), codec: Codec::new(config), }, }, @@ -618,7 +620,7 @@ where StateProj::SendPayload { mut body } => { // keep populate writer buffer until buffer size limit hit, // get blocked or finished. - while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE { + while this.write_buf.len() < *this.h1_write_buffer_size { match body.as_mut().poll_next(cx) { Poll::Ready(Some(Ok(item))) => { this.codec @@ -680,7 +682,7 @@ where // keep populate writer buffer until buffer size limit hit, // get blocked or finished. - while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE { + while this.write_buf.len() < *this.h1_write_buffer_size { match body.as_mut().poll_next(cx) { Poll::Ready(Some(Ok(item))) => { this.codec diff --git a/actix-http/src/h1/dispatcher_tests.rs b/actix-http/src/h1/dispatcher_tests.rs index fb9c1823b..a9262a483 100644 --- a/actix-http/src/h1/dispatcher_tests.rs +++ b/actix-http/src/h1/dispatcher_tests.rs @@ -1,6 +1,9 @@ use std::{ + cell::Cell, future::Future, + io, pin::Pin, + rc::Rc, str, task::{Context, Poll}, time::Duration, @@ -46,6 +49,111 @@ impl Service for YieldService { } } +struct ReadyChunkBody { + chunk_polls: Rc>, + remaining: usize, + chunk_len: usize, +} + +impl ReadyChunkBody { + fn new(chunk_polls: Rc>, remaining: usize, chunk_len: usize) -> Self { + Self { + chunk_polls, + remaining, + chunk_len, + } + } +} + +impl MessageBody for ReadyChunkBody { + type Error = Error; + + fn size(&self) -> crate::body::BodySize { + crate::body::BodySize::Stream + } + + fn poll_next( + mut self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll>> { + if self.remaining == 0 { + return Poll::Ready(None); + } + + self.remaining -= 1; + self.chunk_polls.set(self.chunk_polls.get() + 1); + + Poll::Ready(Some(Ok(Bytes::from(vec![b'x'; self.chunk_len])))) + } +} + +struct PendingOnceWriteBuf { + io: TestBuffer, + block_next_write: bool, +} + +impl PendingOnceWriteBuf { + fn new(data: T) -> Self + where + T: Into, + { + Self { + io: TestBuffer::new(data), + block_next_write: true, + } + } +} + +impl io::Read for PendingOnceWriteBuf { + fn read(&mut self, dst: &mut [u8]) -> Result { + self.io.read(dst) + } +} + +impl io::Write for PendingOnceWriteBuf { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.io.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.io.flush() + } +} + +impl actix_codec::AsyncRead for PendingOnceWriteBuf { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut actix_codec::ReadBuf<'_>, + ) -> Poll> { + Pin::new(&mut self.io).poll_read(cx, buf) + } +} + +impl actix_codec::AsyncWrite for PendingOnceWriteBuf { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + if self.block_next_write { + self.block_next_write = false; + cx.waker().wake_by_ref(); + return Poll::Pending; + } + + Pin::new(&mut self.io).poll_write(cx, buf) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.io).poll_flush(cx) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.io).poll_shutdown(cx) + } +} + fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option { memchr::memmem::find(&haystack[from..], needle) } @@ -108,6 +216,18 @@ fn echo_payload_service() -> impl Service, E }) } +fn ready_chunk_body_service( + chunk_polls: Rc>, + chunk_count: usize, + chunk_len: usize, +) -> impl Service, Error = Error> { + fn_service(move |_req: Request| { + ready(Ok::<_, Error>(Response::ok().set_body( + ReadyChunkBody::new(chunk_polls.clone(), chunk_count, chunk_len), + ))) + }) +} + #[actix_rt::test] async fn late_request() { let mut buf = TestBuffer::empty(); @@ -1364,6 +1484,58 @@ async fn disallow_half_closed() { assert!(matches!(inner.state, State::ServiceCall { .. })) } +#[actix_rt::test] +async fn h1_write_buffer_size_limits_buffering() { + let request = "GET /stream HTTP/1.1\r\nConnection: close\r\n\r\n"; + + let default_polls = Rc::new(Cell::new(0)); + let default_services = HttpFlow::new( + ready_chunk_body_service(default_polls.clone(), 8, 1024), + ExpectHandler, + None::, + ); + let default_io = PendingOnceWriteBuf::new(request); + let default_dispatcher = Dispatcher::new( + default_io, + default_services, + ServiceConfig::default(), + None, + OnConnectData::default(), + ); + pin!(default_dispatcher); + + let mut cx = Context::from_waker(futures_util::task::noop_waker_ref()); + assert!(default_dispatcher.as_mut().poll(&mut cx).is_pending()); + assert_eq!(default_polls.get(), 8); + + let custom_polls = Rc::new(Cell::new(0)); + let custom_services = HttpFlow::new( + ready_chunk_body_service(custom_polls.clone(), 8, 1024), + ExpectHandler, + None::, + ); + let custom_io = PendingOnceWriteBuf::new(request); + let custom_dispatcher = Dispatcher::new( + custom_io, + custom_services, + crate::config::ServiceConfigBuilder::new() + .h1_write_buffer_size(1024) + .build(), + None, + OnConnectData::default(), + ); + pin!(custom_dispatcher); + + assert!(custom_dispatcher.as_mut().poll(&mut cx).is_pending()); + assert_eq!(custom_polls.get(), 1); +} + +#[actix_rt::test] +#[should_panic(expected = "HTTP/1 write buffer size must be greater than zero")] +async fn h1_write_buffer_size_rejects_zero() { + let _ = crate::config::ServiceConfigBuilder::new().h1_write_buffer_size(0); +} + fn http_msg(msg: impl AsRef) -> BytesMut { let mut msg = msg .as_ref() diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index 1c48a27ce..884017a47 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -8,6 +8,7 @@ - Fix `HttpRequest::{match_pattern,match_name}` reporting path-only matches when route guards disambiguate overlapping resources. [#3346] - Fix `Readlines` handling of lines split across payload chunks so combined line limits are enforced and complete lines are yielded. - Update `rand` dependency to `0.10`. +- Add `HttpServer::h1_write_buffer_size()`. [#3944]: https://github.com/actix/actix-web/pull/3944 [#3346]: https://github.com/actix/actix-web/issues/3346 diff --git a/actix-web/src/server.rs b/actix-web/src/server.rs index 1965d458e..3f5899b50 100644 --- a/actix-web/src/server.rs +++ b/actix-web/src/server.rs @@ -33,6 +33,7 @@ struct Config { client_request_timeout: Duration, client_disconnect_timeout: Duration, h1_allow_half_closed: bool, + h1_write_buffer_size: Option, h2_initial_window_size: Option, h2_initial_connection_window_size: Option, #[allow(dead_code)] // only dead when no TLS features are enabled @@ -122,6 +123,7 @@ where client_request_timeout: Duration::from_secs(5), client_disconnect_timeout: Duration::from_secs(1), h1_allow_half_closed: true, + h1_write_buffer_size: None, h2_initial_window_size: None, h2_initial_connection_window_size: None, tls_handshake_timeout: None, @@ -286,6 +288,25 @@ where self } + /// Sets the maximum response write buffer size for HTTP/1 connections. + /// + /// Once the response buffer reaches this size, the dispatcher flushes it to the I/O stream. + /// + /// The default value is 32 KiB. + /// + /// # Panics + /// + /// Panics if `size` is 0. + pub fn h1_write_buffer_size(self, size: usize) -> Self { + assert!( + size > 0, + "HTTP/1 write buffer size must be greater than zero" + ); + + self.config.lock().unwrap().h1_write_buffer_size = Some(size); + self + } + /// Sets initial stream-level flow control window size for HTTP/2 connections. /// /// Higher values can improve upload performance on high-latency links at the cost of higher @@ -629,6 +650,10 @@ where svc = svc.tcp_nodelay(enabled); } + if let Some(size) = cfg.h1_write_buffer_size { + svc = svc.h1_write_buffer_size(size); + } + if let Some(val) = cfg.h2_initial_window_size { svc = svc.h2_initial_window_size(val); } @@ -686,6 +711,10 @@ where svc = svc.tcp_nodelay(enabled); } + if let Some(size) = cfg.h1_write_buffer_size { + svc = svc.h1_write_buffer_size(size); + } + if let Some(val) = cfg.h2_initial_window_size { svc = svc.h2_initial_window_size(val); } @@ -774,6 +803,10 @@ where svc = svc.tcp_nodelay(enabled); } + if let Some(size) = c.h1_write_buffer_size { + svc = svc.h1_write_buffer_size(size); + } + if let Some(val) = c.h2_initial_window_size { svc = svc.h2_initial_window_size(val); } @@ -837,6 +870,10 @@ where svc = svc.tcp_nodelay(enabled); } + if let Some(size) = c.h1_write_buffer_size { + svc = svc.h1_write_buffer_size(size); + } + if let Some(val) = c.h2_initial_window_size { svc = svc.h2_initial_window_size(val); } @@ -915,6 +952,10 @@ where svc = svc.tcp_nodelay(enabled); } + if let Some(size) = c.h1_write_buffer_size { + svc = svc.h1_write_buffer_size(size); + } + if let Some(val) = c.h2_initial_window_size { svc = svc.h2_initial_window_size(val); } @@ -993,6 +1034,10 @@ where svc = svc.tcp_nodelay(enabled); } + if let Some(size) = c.h1_write_buffer_size { + svc = svc.h1_write_buffer_size(size); + } + if let Some(val) = c.h2_initial_window_size { svc = svc.h2_initial_window_size(val); } @@ -1072,6 +1117,10 @@ where svc = svc.tcp_nodelay(enabled); } + if let Some(size) = c.h1_write_buffer_size { + svc = svc.h1_write_buffer_size(size); + } + if let Some(val) = c.h2_initial_window_size { svc = svc.h2_initial_window_size(val); } @@ -1140,14 +1189,19 @@ where .into_factory() .map_err(|err| err.into().error_response()); - fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then( - HttpService::build() + fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then({ + let mut svc = HttpService::build() .keep_alive(c.keep_alive) .client_request_timeout(c.client_request_timeout) .client_disconnect_timeout(c.client_disconnect_timeout) - .h1_allow_half_closed(c.h1_allow_half_closed) - .finish(map_config(fac, move |_| config.clone())), - ) + .h1_allow_half_closed(c.h1_allow_half_closed); + + if let Some(size) = c.h1_write_buffer_size { + svc = svc.h1_write_buffer_size(size); + } + + svc.finish(map_config(fac, move |_| config.clone())) + }) }, )?; @@ -1194,6 +1248,10 @@ where svc = svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)); } + if let Some(size) = c.h1_write_buffer_size { + svc = svc.h1_write_buffer_size(size); + } + let fac = factory() .into_factory() .map_err(|err| err.into().error_response()); From 8c397f83a3c391aefbab5cba2deefdfe27ffcb03 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sat, 18 Apr 2026 14:22:44 +0900 Subject: [PATCH 12/19] chore(http,web): upgrade `foldhash` to 0.2 (#4030) --- Cargo.lock | 12 +++++++++--- actix-http/CHANGES.md | 1 + actix-http/Cargo.toml | 2 +- actix-web/CHANGES.md | 1 + actix-web/Cargo.toml | 2 +- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 08f1e7b1e..a520f1f6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,7 +72,7 @@ dependencies = [ "encoding_rs", "env_logger", "flate2", - "foldhash", + "foldhash 0.2.0", "futures-core", "futures-util", "h2", @@ -351,7 +351,7 @@ dependencies = [ "encoding_rs", "env_logger", "flate2", - "foldhash", + "foldhash 0.2.0", "futures-core", "futures-util", "impl-more", @@ -1320,6 +1320,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1535,7 +1541,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 68a6188c4..bcc13abe0 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -3,6 +3,7 @@ ## Unreleased - When configured, gracefully close HTTP/1 connections after early responses to unread request bodies. [#3967] +- Update `foldhash` dependency to `0.2`. [#3967]: https://github.com/actix/actix-web/issues/3967 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 07a4090c9..c76913cc5 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -102,7 +102,7 @@ bytes = "1" bytestring = "1" derive_more = { version = "2", features = ["as_ref", "deref", "deref_mut", "display", "error", "from"] } encoding_rs = "0.8" -foldhash = "0.1" +foldhash = "0.2" futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } http = "0.2.7" httparse = "1.5.1" diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index 884017a47..e0dffae8e 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -7,6 +7,7 @@ - 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] - Fix `Readlines` handling of lines split across payload chunks so combined line limits are enforced and complete lines are yielded. +- Update `foldhash` dependency to `0.2`. - Update `rand` dependency to `0.10`. - Add `HttpServer::h1_write_buffer_size()`. diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index e1cae53e3..bf788a89d 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -147,7 +147,7 @@ cfg-if = "1" cookie = { version = "0.16", features = ["percent-encode"], optional = true } derive_more = { version = "2", features = ["as_ref", "deref", "deref_mut", "display", "error", "from"] } encoding_rs = "0.8" -foldhash = "0.1" +foldhash = "0.2" futures-core = { version = "0.3.17", default-features = false } futures-util = { version = "0.3.17", default-features = false } impl-more = "0.1.4" From 71d6bdf03e6e8567f9cdfa4ccd2a9b9d3136d0ae Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sun, 19 Apr 2026 02:37:37 +0900 Subject: [PATCH 13/19] docs(web): tweak docs for `full_url` (#4032) --- actix-web/src/request.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/actix-web/src/request.rs b/actix-web/src/request.rs index 5e86f11a2..08be38ca6 100644 --- a/actix-web/src/request.rs +++ b/actix-web/src/request.rs @@ -89,7 +89,10 @@ impl HttpRequest { } /// This method returns mutable reference to the request head. - /// panics if multiple references of HTTP request exists. + /// + /// # Panics + /// + /// Panics if multiple references of HTTP request exists. #[inline] pub(crate) fn head_mut(&mut self) -> &mut RequestHead { &mut Rc::get_mut(&mut self.inner).unwrap().head @@ -106,6 +109,12 @@ impl HttpRequest { /// Reconstructed URL is best-effort, using [`connection_info`](HttpRequest::connection_info()) /// to get forwarded scheme & host. /// + /// # Panics + /// + /// Panics when the reconstructed URL cannot be parsed, such as when the host is malformed. + /// + /// # Examples + /// /// ``` /// use actix_web::test::TestRequest; /// let req = TestRequest::with_uri("http://10.1.2.3:8443/api?id=4&name=foo") From d604bcc85140e6abe2d4e5488a05116a394be0ae Mon Sep 17 00:00:00 2001 From: Maarten Deprez Date: Sat, 18 Apr 2026 19:40:25 +0200 Subject: [PATCH 14/19] Support deserializing paths to sequences for multi-component (eg. tail) matches (#3432) * Support deserializing paths to sequences for multi-component (eg. tail) matches * Support deserializing paths to sequences: use "/{tail}*" syntax * some fixes * make `size_hint` work properly * fix for tuple struct --------- Co-authored-by: Yuki Okushi --- actix-router/CHANGES.md | 4 ++ actix-router/src/de.rs | 136 ++++++++++++++++++++++++++++++++++-- actix-web/src/types/path.rs | 20 ++++++ 3 files changed, 154 insertions(+), 6 deletions(-) diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 30101e3ee..355f13fc8 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -2,6 +2,10 @@ ## Unreleased +- Add support for extracting multi-component path params into a sequence (Vec, tuple, ...). [#3432] + +[#3432]: https://github.com/actix/actix-web/pull/3432 + ## 0.5.4 - Minimum supported Rust version (MSRV) is now 1.88. diff --git a/actix-router/src/de.rs b/actix-router/src/de.rs index f06911b34..d255704fe 100644 --- a/actix-router/src/de.rs +++ b/actix-router/src/de.rs @@ -399,11 +399,25 @@ impl<'de> Deserializer<'de> for Value<'de> { visitor.visit_newtype_struct(self) } - fn deserialize_tuple(self, _: usize, _: V) -> Result + fn deserialize_tuple(self, len: usize, visitor: V) -> Result where V: Visitor<'de>, { - Err(de::value::Error::custom("unsupported type: tuple")) + let value_seq = ValueSeq::new(self.value); + if len == value_seq.len() { + visitor.visit_seq(value_seq) + } else { + Err(de::value::Error::custom( + "path and tuple lengths don't match", + )) + } + } + + fn deserialize_seq(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_seq(ValueSeq::new(self.value)) } fn deserialize_struct( @@ -421,13 +435,13 @@ impl<'de> Deserializer<'de> for Value<'de> { fn deserialize_tuple_struct( self, _: &'static str, - _: usize, - _: V, + len: usize, + visitor: V, ) -> Result where V: Visitor<'de>, { - Err(de::value::Error::custom("unsupported type: tuple struct")) + self.deserialize_tuple(len, visitor) } fn deserialize_any(self, visitor: V) -> Result @@ -463,7 +477,6 @@ impl<'de> Deserializer<'de> for Value<'de> { } } - unsupported_type!(deserialize_seq, "seq"); unsupported_type!(deserialize_map, "map"); unsupported_type!(deserialize_identifier, "identifier"); } @@ -533,6 +546,43 @@ impl<'de> de::VariantAccess<'de> for UnitVariant { } } +struct ValueSeq<'de> { + elems: std::str::Split<'de, char>, +} + +impl<'de> ValueSeq<'de> { + fn new(value: &'de str) -> Self { + Self { + elems: value.split('/'), + } + } + + fn len(&self) -> usize { + self.elems.clone().filter(|s| !s.is_empty()).count() + } +} + +impl<'de> de::SeqAccess<'de> for ValueSeq<'de> { + type Error = de::value::Error; + + fn next_element_seed(&mut self, seed: T) -> Result, Self::Error> + where + T: de::DeserializeSeed<'de>, + { + for elem in &mut self.elems { + if !elem.is_empty() { + return seed.deserialize(Value { value: elem }).map(Some); + } + } + + Ok(None) + } + + fn size_hint(&self) -> Option { + Some(self.len()) + } +} + #[cfg(test)] mod tests { use serde::Deserialize; @@ -567,6 +617,24 @@ mod tests { val: TestEnum, } + #[derive(Debug, Deserialize)] + struct TestSeq1 { + tail: Vec, + } + + #[derive(Debug, Deserialize)] + struct TestSeq2 { + tail: (String, String, String), + } + + #[derive(Debug, Deserialize)] + struct TestSeq3 { + tail: TestTupleStruct, + } + + #[derive(Debug, Deserialize, PartialEq)] + struct TestTupleStruct(String, String, String); + #[test] fn test_request_extract() { let mut router = Router::<()>::build(); @@ -662,6 +730,62 @@ mod tests { assert!(format!("{:?}", i).contains("unknown variant")); } + #[test] + fn test_extract_seq() { + let mut router = Router::<()>::build(); + router.path("/path/to/{tail}*", ()); + let router = router.finish(); + + let mut path = Path::new("/path/to/tail/with/slash%2fes"); + assert!(router.recognize(&mut path).is_some()); + + let i: (String,) = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(i.0, String::from("tail/with/slash/es")); + + let i: TestSeq1 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!( + i.tail, + vec![ + String::from("tail"), + String::from("with"), + String::from("slash/es") + ] + ); + + let i: TestSeq2 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!( + i.tail, + ( + String::from("tail"), + String::from("with"), + String::from("slash/es") + ) + ); + + let i: TestSeq3 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!( + i.tail, + TestTupleStruct( + String::from("tail"), + String::from("with"), + String::from("slash/es") + ) + ); + } + + #[test] + fn test_value_seq_size_hint_counts_remaining_elements() { + use serde::de::SeqAccess as _; + + let mut seq = ValueSeq::new("tail/with/slash"); + + assert_eq!(seq.size_hint(), Some(3)); + + let elem = seq.next_element::().unwrap(); + assert_eq!(elem.as_deref(), Some("tail")); + assert_eq!(seq.size_hint(), Some(2)); + } + #[test] fn test_extract_errors() { let mut router = Router::<()>::build(); diff --git a/actix-web/src/types/path.rs b/actix-web/src/types/path.rs index 5f22568cc..8bae70755 100644 --- a/actix-web/src/types/path.rs +++ b/actix-web/src/types/path.rs @@ -53,6 +53,26 @@ use crate::{ /// format!("Welcome {}!", info.name) /// } /// ``` +/// +/// Segments matching multiple path components can be deserialized +/// into a `Vec<_>` to percent-decode the components individually. Empty +/// path components are ignored. +/// +/// ``` +/// use actix_web::{get, web}; +/// use serde::Deserialize; +/// +/// #[derive(Deserialize)] +/// struct Tail { +/// tail: Vec, +/// } +/// +/// // extract `Tail` from a path using serde +/// #[get("/path/to/{tail}*")] +/// async fn index(info: web::Path) -> String { +/// format!("Navigating to {}!", info.tail.join(" :: ")) +/// } +/// ``` #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut, AsRef, Display, From)] pub struct Path(T); From 68f5bcf5fc90444cdb162f31d3903eeff6dd222f Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sun, 19 Apr 2026 14:17:41 +0900 Subject: [PATCH 15/19] chore(awc,web): upgrade const-str to v1.1 (#4034) --- Cargo.lock | 4 ++-- actix-web/Cargo.toml | 2 +- awc/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a520f1f6a..0d151fa90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -876,9 +876,9 @@ checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "const-str" -version = "0.5.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9" +checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f" [[package]] name = "convert_case" diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index bf788a89d..f6e05d325 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -174,7 +174,7 @@ actix-test = { version = "0.1", features = ["openssl", "rustls-0_23"] } awc = { version = "3", features = ["openssl"] } brotli = "8" -const-str = "0.5" # TODO(MSRV 1.77): update to 0.6 +const-str = "1.1" core_affinity = "0.8" criterion = { version = "0.5", features = ["html_reports"] } env_logger = "0.11" diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 3fb11b584..66bf24d9c 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -143,7 +143,7 @@ actix-utils = "3" actix-web = { version = "4.13", features = ["openssl"] } brotli = "8" -const-str = "0.5" # TODO(MSRV 1.77): update to 0.6 +const-str = "1.1" env_logger = "0.11" flate2 = "1.0.13" futures-util = { version = "0.3.17", default-features = false } From 98f26cc30f08dcb76f5f0ce352353b7ae9ba9b9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:26:08 +0100 Subject: [PATCH 16/19] build(deps): bump EmbarkStudios/cargo-deny-action from 2.0.16 to 2.0.17 (#4037) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aed186426..1886fbdd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: - name: deny check if: matrix.version.name == 'stable' && matrix.target.os == 'ubuntu-latest' - uses: EmbarkStudios/cargo-deny-action@175dc7fd4fb85ec8f46948fb98f44db001149081 # v2.0.16 + uses: EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb # v2.0.17 io-uring: name: io-uring tests From 15ce7b99843d5291edc09f70c3d5965f4eea822c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:26:44 +0100 Subject: [PATCH 17/19] build(deps): bump actions-rust-lang/setup-rust-toolchain from 1.15.4 to 1.16.0 (#4036) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-post-merge.yml | 4 ++-- .github/workflows/ci.yml | 6 +++--- .github/workflows/coverage.yml | 2 +- .github/workflows/lint.yml | 8 ++++---- .github/workflows/semver-checks.yml | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-post-merge.yml b/.github/workflows/ci-post-merge.yml index 578ca2794..4f8b58879 100644 --- a/.github/workflows/ci-post-merge.yml +++ b/.github/workflows/ci-post-merge.yml @@ -44,7 +44,7 @@ jobs: echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV - name: Install Rust (${{ matrix.version.name }}) - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 with: toolchain: ${{ matrix.version.version }} @@ -80,7 +80,7 @@ jobs: uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 - name: Install just, cargo-hack uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1886fbdd6..0865a9014 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - name: Install Rust (${{ matrix.version.name }}) - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 with: toolchain: ${{ matrix.version.version }} @@ -96,7 +96,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 with: toolchain: nightly @@ -112,7 +112,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rust (nightly) - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 with: toolchain: nightly diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 25bfa5b52..5a6abbbe6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rust (nightly) - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 with: toolchain: nightly components: llvm-tools diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 812abed11..d02bb8efa 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rust (nightly) - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 with: toolchain: nightly components: rustfmt @@ -36,7 +36,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 with: components: clippy @@ -55,7 +55,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rust (nightly) - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 with: toolchain: nightly components: rust-docs @@ -72,7 +72,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }}) - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 with: toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }} diff --git a/.github/workflows/semver-checks.yml b/.github/workflows/semver-checks.yml index dd5881815..618b28e6f 100644 --- a/.github/workflows/semver-checks.yml +++ b/.github/workflows/semver-checks.yml @@ -15,7 +15,7 @@ jobs: fetch-depth: 0 - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 with: toolchain: stable From 7ba139e8764749ac4924d9fc86d7d26c4af7e8b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:27:07 +0100 Subject: [PATCH 18/19] build(deps): bump taiki-e/install-action from 2.73.0 to 2.75.18 (#4035) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-post-merge.yml | 4 ++-- .github/workflows/ci.yml | 4 ++-- .github/workflows/coverage.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/semver-checks.yml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-post-merge.yml b/.github/workflows/ci-post-merge.yml index 4f8b58879..ee71934e8 100644 --- a/.github/workflows/ci-post-merge.yml +++ b/.github/workflows/ci-post-merge.yml @@ -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@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 + uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 with: tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean @@ -83,7 +83,7 @@ jobs: uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 - name: Install just, cargo-hack - uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 + uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 with: tool: just,cargo-hack diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0865a9014..48193b682 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 + uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 with: tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean @@ -117,7 +117,7 @@ jobs: toolchain: nightly - name: Install just - uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 + uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 with: tool: just diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 5a6abbbe6..f107793e6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -24,7 +24,7 @@ jobs: components: llvm-tools - name: Install just, cargo-llvm-cov, cargo-nextest - uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 + uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 with: tool: just,cargo-llvm-cov,cargo-nextest diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d02bb8efa..64d9c60c1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -77,7 +77,7 @@ jobs: toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }} - name: Install just - uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7 + uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 with: tool: just diff --git a/.github/workflows/semver-checks.yml b/.github/workflows/semver-checks.yml index 618b28e6f..3ac2675e1 100644 --- a/.github/workflows/semver-checks.yml +++ b/.github/workflows/semver-checks.yml @@ -20,7 +20,7 @@ jobs: toolchain: stable - name: Install cargo-semver-checks - uses: taiki-e/install-action@7a562dfa955aa2e4d5b0fd6ebd57ff9715c07b0b # v2.73.0 + uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 with: tool: cargo-semver-checks From 86eeea7f92cb14250a4529bb1938c1bfa26c0f27 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Wed, 22 Apr 2026 23:44:18 +0900 Subject: [PATCH 19/19] chore: address deny warning (#4040) --- Cargo.lock | 74 +++++++++++++++++++++++++++++------------------------- deny.toml | 1 + 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d151fa90..483994aa5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -623,9 +623,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ "aws-lc-sys", "zeroize", @@ -633,9 +633,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" dependencies = [ "cc", "cmake", @@ -807,9 +807,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", ] @@ -858,7 +858,7 @@ dependencies = [ "http 1.4.0", "mime", "mime_guess", - "rand 0.8.5", + "rand 0.8.6", "thiserror 1.0.69", ] @@ -900,7 +900,7 @@ dependencies = [ "hkdf", "hmac", "percent-encoding", - "rand 0.8.5", + "rand 0.8.6", "sha2", "subtle", "time", @@ -2128,9 +2128,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.77" +version = "0.10.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe4646e360ec77dff7dde40ed3d6c5fee52d156ef4a62f53973d38294dad87f" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" dependencies = [ "bitflags 2.11.1", "cfg-if", @@ -2160,9 +2160,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.113" +version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad2f2c0eba47118757e4c6d2bff2838f3e0523380021356e7875e858372ce644" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", @@ -2295,9 +2295,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] @@ -2368,9 +2368,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2633,7 +2633,7 @@ dependencies = [ "log", "once_cell", "rustls-pki-types", - "rustls-webpki 0.103.12", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] @@ -2682,9 +2682,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.12" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring 0.17.14", @@ -3142,9 +3142,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -3348,9 +3348,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicase" @@ -3424,9 +3424,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -3469,11 +3469,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -3482,7 +3482,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -3605,14 +3605,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -3773,9 +3773,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" [[package]] name = "wit-bindgen" @@ -3786,6 +3786,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" diff --git a/deny.toml b/deny.toml index c0dd7ad78..efbf62776 100644 --- a/deny.toml +++ b/deny.toml @@ -45,4 +45,5 @@ ignore = [ "RUSTSEC-2026-0049", "RUSTSEC-2026-0098", "RUSTSEC-2026-0099", + "RUSTSEC-2026-0104", ]