Merge branch 'main' into feat/http-response-take-error

This commit is contained in:
FAIZAL KHAN 2026-04-23 01:05:20 +05:30 committed by GitHub
commit 206262c571
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 1656 additions and 316 deletions

View File

@ -44,12 +44,12 @@ 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 }}
- 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
@ -80,10 +80,10 @@ 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
uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18
with:
tool: just,cargo-hack

View File

@ -59,12 +59,12 @@ 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 }}
- 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
@ -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
@ -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,12 +112,12 @@ 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
- name: Install just
uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7
uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18
with:
tool: just

View File

@ -18,13 +18,13 @@ 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
- 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

View File

@ -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,12 +72,12 @@ 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 }}
- name: Install just
uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2.75.7
uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18
with:
tool: just

View File

@ -15,12 +15,12 @@ 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
- 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

326
Cargo.lock generated
View File

@ -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",
@ -49,7 +49,7 @@ dependencies = [
[[package]]
name = "actix-http"
version = "3.12.0"
version = "3.12.1"
dependencies = [
"actix-codec",
"actix-http-test",
@ -62,7 +62,7 @@ dependencies = [
"async-stream",
"awc",
"base64 0.22.1",
"bitflags 2.11.0",
"bitflags 2.11.1",
"brotli",
"bytes",
"bytestring",
@ -72,7 +72,7 @@ dependencies = [
"encoding_rs",
"env_logger",
"flate2",
"foldhash",
"foldhash 0.2.0",
"futures-core",
"futures-util",
"h2",
@ -88,10 +88,10 @@ dependencies = [
"openssl",
"percent-encoding",
"pin-project-lite",
"rand 0.9.3",
"rand 0.10.1",
"rcgen",
"regex",
"rustls 0.23.37",
"rustls 0.23.38",
"rustls-pki-types",
"rustversion",
"serde",
@ -142,7 +142,7 @@ dependencies = [
[[package]]
name = "actix-multipart"
version = "0.7.2"
version = "0.8.0"
dependencies = [
"actix-http",
"actix-multipart-derive",
@ -164,7 +164,7 @@ dependencies = [
"memchr",
"mime",
"multer",
"rand 0.9.3",
"rand 0.10.1",
"serde",
"serde_json",
"serde_plain",
@ -175,7 +175,7 @@ dependencies = [
[[package]]
name = "actix-multipart-derive"
version = "0.7.0"
version = "0.8.0"
dependencies = [
"actix-multipart",
"actix-web",
@ -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",
@ -351,7 +351,7 @@ dependencies = [
"encoding_rs",
"env_logger",
"flate2",
"foldhash",
"foldhash 0.2.0",
"futures-core",
"futures-util",
"impl-more",
@ -362,11 +362,11 @@ dependencies = [
"once_cell",
"openssl",
"pin-project-lite",
"rand 0.9.3",
"rand 0.10.1",
"rcgen",
"regex",
"regex-lite",
"rustls 0.23.37",
"rustls 0.23.38",
"rustls-pki-types",
"serde",
"serde_json",
@ -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",
]
@ -424,7 +424,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
@ -606,12 +606,12 @@ dependencies = [
"openssl",
"percent-encoding",
"pin-project-lite",
"rand 0.9.3",
"rand 0.10.1",
"rcgen",
"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",
@ -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",
@ -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"
@ -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"
@ -732,9 +741,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",
@ -748,6 +757,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"
@ -781,15 +801,15 @@ 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",
]
[[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",
]
@ -838,7 +858,7 @@ dependencies = [
"http 1.4.0",
"mime",
"mime_guess",
"rand 0.8.5",
"rand 0.8.6",
"thiserror 1.0.69",
]
@ -849,10 +869,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af"
[[package]]
name = "const-str"
version = "0.5.7"
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "const-str"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f"
[[package]]
name = "convert_case"
@ -874,7 +900,7 @@ dependencies = [
"hkdf",
"hmac",
"percent-encoding",
"rand 0.8.5",
"rand 0.8.6",
"sha2",
"subtle",
"time",
@ -917,6 +943,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"
@ -1019,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"
@ -1107,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"
@ -1265,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"
@ -1423,6 +1484,7 @@ dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.1",
"wasip2",
"wasip3",
]
@ -1479,14 +1541,14 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
"foldhash 0.1.5",
]
[[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"
@ -1516,7 +1578,7 @@ dependencies = [
"idna",
"ipnet",
"once_cell",
"rand 0.9.3",
"rand 0.9.4",
"ring 0.17.14",
"thiserror 2.0.18",
"tinyvec",
@ -1538,7 +1600,7 @@ dependencies = [
"moka",
"once_cell",
"parking_lot",
"rand 0.9.3",
"rand 0.9.4",
"resolv-conf",
"smallvec",
"thiserror 2.0.18",
@ -1561,7 +1623,7 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
"digest 0.10.7",
]
[[package]]
@ -1603,6 +1665,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"
@ -1726,12 +1797,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",
]
@ -1842,9 +1913,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",
@ -1864,20 +1935,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]]
@ -2057,11 +2128,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl"
version = "0.10.76"
version = "0.10.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf"
checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.11.1",
"cfg-if",
"foreign-types",
"libc",
@ -2089,9 +2160,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-sys"
version = "0.9.112"
version = "0.9.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb"
checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6"
dependencies = [
"cc",
"libc",
@ -2166,9 +2237,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"
@ -2211,7 +2282,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",
]
@ -2224,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",
]
@ -2297,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",
@ -2308,14 +2379,25 @@ 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",
]
[[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"
@ -2355,10 +2437,16 @@ dependencies = [
]
[[package]]
name = "rayon"
version = "1.11.0"
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
@ -2393,16 +2481,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]]
@ -2490,7 +2578,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",
@ -2537,15 +2625,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.13",
"subtle",
"zeroize",
]
@ -2594,9 +2682,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.103.10"
version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"aws-lc-rs",
"ring 0.17.14",
@ -2662,7 +2750,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",
@ -2760,13 +2848,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",
"digest",
"cpufeatures 0.3.0",
"digest 0.11.2",
]
[[package]]
@ -2776,8 +2864,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
[[package]]
@ -3054,9 +3142,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.51.1"
version = "1.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c"
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
dependencies = [
"bytes",
"libc",
@ -3129,7 +3217,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",
]
@ -3260,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"
@ -3294,7 +3382,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",
]
@ -3336,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",
@ -3381,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]]
@ -3394,14 +3482,14 @@ 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]]
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",
@ -3412,9 +3500,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",
@ -3422,9 +3510,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",
@ -3435,9 +3523,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",
]
@ -3470,7 +3558,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",
@ -3478,9 +3566,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",
@ -3517,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",
]
@ -3685,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"
@ -3698,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"
@ -3747,7 +3841,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",

View File

@ -2,8 +2,21 @@
## 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
## 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`.
- 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

View File

@ -1,6 +1,6 @@
[package]
name = "actix-http"
version = "3.12.0"
version = "3.12.1"
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
description = "HTTP types and services for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"]
@ -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"
@ -123,8 +123,8 @@ 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 }
sha1 = { version = "0.10", optional = true }
rand = { version = "0.10.1", optional = true }
sha1 = { version = "0.11", optional = true }
# openssl/rustls
actix-tls = { version = "3.4", default-features = false, optional = true }

View File

@ -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<T, S, X = ExpectHandler, U = UpgradeHandler> {
secure: bool,
local_addr: Option<net::SocketAddr>,
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();

View File

@ -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<bool>,
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<bool> {
self.0.tcp_nodelay

View File

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

View File

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

View File

@ -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;
}
}
@ -168,6 +171,7 @@ pin_project! {
pub(super) io: Option<T>,
read_buf: BytesMut,
write_buf: BytesMut,
h1_write_buffer_size: usize,
codec: Codec,
}
}
@ -281,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),
},
},
@ -361,6 +366,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<Poll<()>, 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 +449,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(())
}
@ -520,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
@ -534,18 +634,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);
}
@ -574,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
@ -588,18 +696,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 +1076,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 +1255,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 +1411,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 +1425,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,

View File

@ -1,13 +1,19 @@
use std::{
cell::Cell,
future::Future,
io,
pin::Pin,
rc::Rc,
str,
task::{Context, Poll},
time::Duration,
};
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};
@ -43,6 +49,111 @@ impl Service<Request> for YieldService {
}
}
struct ReadyChunkBody {
chunk_polls: Rc<Cell<usize>>,
remaining: usize,
chunk_len: usize,
}
impl ReadyChunkBody {
fn new(chunk_polls: Rc<Cell<usize>>, 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<Option<Result<Bytes, Self::Error>>> {
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<T>(data: T) -> Self
where
T: Into<BytesMut>,
{
Self {
io: TestBuffer::new(data),
block_next_write: true,
}
}
}
impl io::Read for PendingOnceWriteBuf {
fn read(&mut self, dst: &mut [u8]) -> Result<usize, io::Error> {
self.io.read(dst)
}
}
impl io::Write for PendingOnceWriteBuf {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
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<io::Result<()>> {
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<io::Result<usize>> {
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<io::Result<()>> {
Pin::new(&mut self.io).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.io).poll_shutdown(cx)
}
}
fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
memchr::memmem::find(&haystack[from..], needle)
}
@ -84,6 +195,11 @@ fn drop_payload_service() -> impl Service<Request, Response = Response<&'static
})
}
fn ignore_payload_service(
) -> impl Service<Request, Response = Response<&'static str>, Error = Error> {
fn_service(|_req: Request| ready(Ok::<_, Error>(Response::with_body(StatusCode::OK, "ok"))))
}
fn echo_payload_service() -> impl Service<Request, Response = Response<Bytes>, Error = Error> {
fn_service(|mut req: Request| {
Box::pin(async move {
@ -100,6 +216,18 @@ fn echo_payload_service() -> impl Service<Request, Response = Response<Bytes>, E
})
}
fn ready_chunk_body_service(
chunk_polls: Rc<Cell<usize>>,
chunk_count: usize,
chunk_len: usize,
) -> impl Service<Request, Response = Response<ReadyChunkBody>, 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();
@ -536,15 +664,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 +696,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| {
@ -1224,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::<UpgradeHandler>,
);
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::<UpgradeHandler>,
);
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<str>) -> BytesMut {
let mut msg = msg
.as_ref()

View File

@ -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]

View File

@ -2,6 +2,8 @@
## Unreleased
## 0.8.0
- Minimum supported Rust version (MSRV) is now 1.88.
## 0.7.0

View File

@ -1,6 +1,6 @@
[package]
name = "actix-multipart-derive"
version = "0.7.0"
version = "0.8.0"
authors = ["Jacob Halsey <jacob@jhalsey.com>"]
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"

View File

@ -5,11 +5,11 @@
<!-- prettier-ignore-start -->
[![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)
<br />
[![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)

View File

@ -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<dyn ::std::future::Future<Output = ::std::result::Result<(), ::actix_multipart::MultipartError>> + '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 }),
}
}

View File

@ -2,9 +2,15 @@
## Unreleased
## 0.8.0
- Add multi-field multipart payload builders to `actix_multipart::test`. [#3575]
- Add `MultipartForm` support for `Option<Vec<T>>` 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`.
[#3577]: https://github.com/actix/actix-web/pull/3577
[#3575]: https://github.com/actix/actix-web/issues/3575

View File

@ -1,6 +1,6 @@
[package]
name = "actix-multipart"
version = "0.7.2"
version = "0.8.0"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Jacob Halsey <jacob@jhalsey.com>",
@ -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 }
@ -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"

View File

@ -3,11 +3,11 @@
<!-- prettier-ignore-start -->
[![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)
<br />
[![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)

View File

@ -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 {

View File

@ -82,5 +82,5 @@ pub mod test;
pub use self::{
error::Error as MultipartError,
field::{Field, LimitExceeded},
multipart::Multipart,
multipart::{Multipart, MultipartConfig},
};

View File

@ -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<Error>),
}
/// [`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<T>`, in that order, and
/// fall back to the default multipart config.
fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>()
.or_else(|| req.app_data::<web::Data<Self>>().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<S>(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),
}
}
@ -84,18 +131,39 @@ impl Multipart {
.as_str()
.to_owned();
if boundary.is_empty() {
return Err(Error::BoundaryMissing);
}
Ok((content_type, boundary))
}
/// Constructs a new multipart reader from given Content-Type, boundary, and stream.
pub(crate) fn from_ct_and_boundary<S>(ct: Mime, boundary: String, stream: S) -> Multipart
where
S: Stream<Item = Result<Bytes, PayloadError>> + 'static,
{
Self::from_ct_and_boundary_with_buffer_limit(
ct,
boundary,
stream,
DEFAULT_CONFIG.buffer_limit,
)
}
fn from_ct_and_boundary_with_buffer_limit<S>(
ct: Mime,
boundary: String,
stream: S,
buffer_limit: usize,
) -> Multipart
where
S: Stream<Item = Result<Bytes, PayloadError>> + '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,
@ -239,6 +307,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<Option<bool>, 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 +321,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 +346,12 @@ impl Inner {
payload: &mut PayloadBuffer,
boundary: &str,
) -> Result<Option<bool>, Error> {
if boundary.is_empty() {
return Err(Error::BoundaryMissing);
}
let mut eof = false;
let boundary = boundary.as_bytes();
loop {
match payload.readline()? {
@ -295,19 +359,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 +651,226 @@ 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
}
fn create_multipart_with_buffer_limit(
body: impl Stream<Item = Result<Bytes, PayloadError>> + '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")) });
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 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();
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

View File

@ -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<RefCell<PayloadBuffer>>,
}
@ -45,31 +48,64 @@ impl Clone for PayloadRef {
/// Payload buffer.
pub(crate) struct PayloadBuffer {
pub(crate) stream: LocalBoxStream<'static, Result<Bytes, PayloadError>>,
pending: Option<Bytes>,
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<S>(stream: S) -> Self
pub(crate) fn new_with_limit<S>(stream: S, buffer_limit: usize) -> Self
where
S: Stream<Item = Result<Bytes, PayloadError>> + '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<bool, PayloadError> {
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);
}
}

View File

@ -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.

View File

@ -399,11 +399,25 @@ impl<'de> Deserializer<'de> for Value<'de> {
visitor.visit_newtype_struct(self)
}
fn deserialize_tuple<V>(self, _: usize, _: V) -> Result<V::Value, Self::Error>
fn deserialize_tuple<V>(self, len: usize, visitor: V) -> Result<V::Value, Self::Error>
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<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_seq(ValueSeq::new(self.value))
}
fn deserialize_struct<V>(
@ -421,13 +435,13 @@ impl<'de> Deserializer<'de> for Value<'de> {
fn deserialize_tuple_struct<V>(
self,
_: &'static str,
_: usize,
_: V,
len: usize,
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
Err(de::value::Error::custom("unsupported type: tuple struct"))
self.deserialize_tuple(len, visitor)
}
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
@ -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<T>(&mut self, seed: T) -> Result<Option<T::Value>, 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<usize> {
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<String>,
}
#[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::<String>().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();

View File

@ -7,6 +7,9 @@
- 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()`.
[#3944]: https://github.com/actix/actix-web/pull/3944
[#3346]: https://github.com/actix/actix-web/issues/3346

View File

@ -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 }
@ -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"
@ -174,13 +174,13 @@ 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"
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"] }

View File

@ -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")

View File

@ -33,6 +33,7 @@ struct Config {
client_request_timeout: Duration,
client_disconnect_timeout: Duration,
h1_allow_half_closed: bool,
h1_write_buffer_size: Option<usize>,
h2_initial_window_size: Option<u32>,
h2_initial_connection_window_size: Option<u32>,
#[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,
@ -245,7 +247,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
@ -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());

View File

@ -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<String>,
/// }
///
/// // extract `Tail` from a path using serde
/// #[get("/path/to/{tail}*")]
/// async fn index(info: web::Path<Tail>) -> String {
/// format!("Navigating to {}!", info.tail.join(" :: "))
/// }
/// ```
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut, AsRef, Display, From)]
pub struct Path<T>(T);

View File

@ -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

View File

@ -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"] }
@ -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"
@ -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 }

View File

@ -42,7 +42,8 @@ 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",
"RUSTSEC-2026-0104",
]