mirror of https://github.com/fafhrd91/actix-web
Merge branch 'main' into feat/http-response-take-error
This commit is contained in:
commit
206262c571
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
## 0.8.0
|
||||
|
||||
- Minimum supported Rust version (MSRV) is now 1.88.
|
||||
|
||||
## 0.7.0
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@
|
|||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-multipart-derive)
|
||||
[](https://docs.rs/actix-multipart-derive/0.7.0)
|
||||
[](https://docs.rs/actix-multipart-derive/0.8.0)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-multipart-derive/0.7.0)
|
||||
[](https://deps.rs/crate/actix-multipart-derive/0.8.0)
|
||||
[](https://crates.io/crates/actix-multipart-derive)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-multipart)
|
||||
[](https://docs.rs/actix-multipart/0.7.2)
|
||||
[](https://docs.rs/actix-multipart/0.8.0)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-multipart/0.7.2)
|
||||
[](https://deps.rs/crate/actix-multipart/0.8.0)
|
||||
[](https://crates.io/crates/actix-multipart)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -82,5 +82,5 @@ pub mod test;
|
|||
pub use self::{
|
||||
error::Error as MultipartError,
|
||||
field::{Field, LimitExceeded},
|
||||
multipart::Multipart,
|
||||
multipart::{Multipart, MultipartConfig},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
Loading…
Reference in New Issue