From 7e7c38f67d3e4ce81b80bd6a5ddde0de6cdc5562 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sun, 25 Jan 2026 16:38:33 +0900 Subject: [PATCH 01/32] fix: fix labeler config --- .github/workflows/labeler.yml | 1 + .github/workflows/semver-labeler.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 5db7c1ccb..1cab9a521 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -12,4 +12,5 @@ jobs: labeler: runs-on: ubuntu-latest steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 diff --git a/.github/workflows/semver-labeler.yml b/.github/workflows/semver-labeler.yml index efb7742e6..fdcaa0392 100644 --- a/.github/workflows/semver-labeler.yml +++ b/.github/workflows/semver-labeler.yml @@ -11,6 +11,8 @@ jobs: permissions: pull-requests: write contents: read + env: + ACTIONS_STEP_DEBUG: true steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: From 3551250e16eea9e7d23d1c9c7761a3b9883322d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 18:34:51 +0900 Subject: [PATCH 02/32] build(deps): bump quote from 1.0.43 to 1.0.44 (#3891) Bumps [quote](https://github.com/dtolnay/quote) from 1.0.43 to 1.0.44. - [Release notes](https://github.com/dtolnay/quote/releases) - [Commits](https://github.com/dtolnay/quote/compare/1.0.43...1.0.44) --- updated-dependencies: - dependency-name: quote dependency-version: 1.0.44 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0111f92a7..2320e5315 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "actix" @@ -2271,9 +2271,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] From 3d6e66b5b54a66a5e51a2fdf17063b67302f04e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 18:35:14 +0900 Subject: [PATCH 03/32] build(deps): bump proc-macro2 from 1.0.105 to 1.0.106 (#3889) Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.105 to 1.0.106. - [Release notes](https://github.com/dtolnay/proc-macro2/releases) - [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.105...1.0.106) --- updated-dependencies: - dependency-name: proc-macro2 dependency-version: 1.0.106 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2320e5315..33f60b63b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2262,9 +2262,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] From f6c1fe3d0f82dab056dfb0451843f160a57315fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 18:35:32 +0900 Subject: [PATCH 04/32] build(deps): bump socket2 from 0.6.1 to 0.6.2 (#3892) Bumps [socket2](https://github.com/rust-lang/socket2) from 0.6.1 to 0.6.2. - [Release notes](https://github.com/rust-lang/socket2/releases) - [Changelog](https://github.com/rust-lang/socket2/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/socket2/compare/v0.6.1...v0.6.2) --- updated-dependencies: - dependency-name: socket2 dependency-version: 0.6.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33f60b63b..b2c08267e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,7 +147,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "slab", - "socket2 0.6.1", + "socket2 0.6.2", "tokio", ] @@ -393,7 +393,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.1", + "socket2 0.6.2", "static_assertions", "time", "tokio", @@ -2815,9 +2815,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", @@ -3043,7 +3043,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.2", "tokio-macros", "windows-sys 0.61.2", ] From ae77e6bd3986116e02210b22f1867e3fdc7f84ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 18:35:44 +0900 Subject: [PATCH 05/32] build(deps): bump actions/checkout from 6.0.1 to 6.0.2 (#3888) Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.1 to 6.0.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/8e8c483db84b4bee98b60c0593521ed34d9990e8...de0fac2e4500dabe0009e67214ff5f5447ce83dd) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/bench.yml | 2 +- .github/workflows/ci-post-merge.yml | 4 ++-- .github/workflows/ci.yml | 6 +++--- .github/workflows/coverage.yml | 2 +- .github/workflows/labeler.yml | 2 +- .github/workflows/lint.yml | 8 ++++---- .github/workflows/semver-labeler.yml | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index a78d6784d..cabf1c267 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rust run: | diff --git a/.github/workflows/ci-post-merge.yml b/.github/workflows/ci-post-merge.yml index 05020a7f6..39ee9a136 100644 --- a/.github/workflows/ci-post-merge.yml +++ b/.github/workflows/ci-post-merge.yml @@ -28,7 +28,7 @@ jobs: runs-on: ${{ matrix.target.os }} steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install nasm if: matrix.target.os == 'windows-latest' @@ -71,7 +71,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Free Disk Space run: ./scripts/free-disk-space.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3fd6c7de..ded9bf725 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: runs-on: ${{ matrix.target.os }} steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install nasm if: matrix.target.os == 'windows-latest' @@ -93,7 +93,7 @@ jobs: name: io-uring tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rust uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2 @@ -109,7 +109,7 @@ jobs: name: doc tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rust (nightly) uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 08d7f43d9..42b5176c0 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -15,7 +15,7 @@ jobs: coverage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rust (nightly) uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 1cab9a521..91d234abe 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -12,5 +12,5 @@ jobs: labeler: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 62f89209c..1aeb396dc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,7 +15,7 @@ jobs: fmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rust (nightly) uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2 @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rust uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2 @@ -52,7 +52,7 @@ jobs: lint-docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rust (nightly) uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2 @@ -69,7 +69,7 @@ jobs: if: false # rustdoc mismatch currently runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }}) uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2 diff --git a/.github/workflows/semver-labeler.yml b/.github/workflows/semver-labeler.yml index fdcaa0392..79ffd621c 100644 --- a/.github/workflows/semver-labeler.yml +++ b/.github/workflows/semver-labeler.yml @@ -14,7 +14,7 @@ jobs: env: ACTIONS_STEP_DEBUG: true steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.workflow_run.head_sha }} From 45203e6cc562f611f6ee41d7441c86c47cfae800 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 06:51:34 +0900 Subject: [PATCH 06/32] build(deps): bump taiki-e/install-action from 2.66.6 to 2.67.10 (#3890) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.66.6 to 2.67.10. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/30eab0fabba9ea3f522099957e668b21876aa39e...81a2f66614862089b24532663f669a485d79c889) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.67.10 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-post-merge.yml | 4 ++-- .github/workflows/ci.yml | 4 ++-- .github/workflows/coverage.yml | 2 +- .github/workflows/lint.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-post-merge.yml b/.github/workflows/ci-post-merge.yml index 39ee9a136..3f4899352 100644 --- a/.github/workflows/ci-post-merge.yml +++ b/.github/workflows/ci-post-merge.yml @@ -49,7 +49,7 @@ jobs: toolchain: ${{ matrix.version.version }} - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - uses: taiki-e/install-action@30eab0fabba9ea3f522099957e668b21876aa39e # v2.66.6 + uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 with: tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean @@ -83,7 +83,7 @@ jobs: uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2 - name: Install just, cargo-hack - uses: taiki-e/install-action@30eab0fabba9ea3f522099957e668b21876aa39e # v2.66.6 + uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 with: tool: just,cargo-hack diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ded9bf725..557832dab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: toolchain: ${{ matrix.version.version }} - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - uses: taiki-e/install-action@30eab0fabba9ea3f522099957e668b21876aa39e # v2.66.6 + uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 with: tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean @@ -117,7 +117,7 @@ jobs: toolchain: nightly - name: Install just - uses: taiki-e/install-action@30eab0fabba9ea3f522099957e668b21876aa39e # v2.66.6 + uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 with: tool: just diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 42b5176c0..1ee0710b6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -24,7 +24,7 @@ jobs: components: llvm-tools - name: Install just, cargo-llvm-cov, cargo-nextest - uses: taiki-e/install-action@30eab0fabba9ea3f522099957e668b21876aa39e # v2.66.6 + uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 with: tool: just,cargo-llvm-cov,cargo-nextest diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1aeb396dc..f9de38600 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -77,7 +77,7 @@ jobs: toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }} - name: Install just - uses: taiki-e/install-action@30eab0fabba9ea3f522099957e668b21876aa39e # v2.66.6 + uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 with: tool: just From e62d84965688c18f4550cce55aaeb931282fce31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:10:27 +0900 Subject: [PATCH 07/32] build(deps): bump time from 0.3.45 to 0.3.46 (#3893) * build(deps): bump time from 0.3.45 to 0.3.46 Bumps [time](https://github.com/time-rs/time) from 0.3.45 to 0.3.46. - [Release notes](https://github.com/time-rs/time/releases) - [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md) - [Commits](https://github.com/time-rs/time/compare/v0.3.45...v0.3.46) --- updated-dependencies: - dependency-name: time dependency-version: 0.3.46 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * chore(*): bump MSRV to 1.88 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yuki Okushi --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 2 +- actix-files/CHANGES.md | 2 +- actix-files/README.md | 2 +- actix-http-test/CHANGES.md | 2 +- actix-http-test/README.md | 2 +- actix-http/CHANGES.md | 2 +- actix-http/README.md | 2 +- actix-multipart-derive/CHANGES.md | 2 +- actix-multipart-derive/README.md | 2 +- actix-multipart/CHANGES.md | 2 +- actix-multipart/README.md | 2 +- actix-router/CHANGES.md | 2 +- actix-router/README.md | 2 +- actix-test/CHANGES.md | 2 +- actix-test/README.md | 2 +- actix-web-actors/CHANGES.md | 2 +- actix-web-actors/README.md | 2 +- actix-web-codegen/CHANGES.md | 2 +- actix-web-codegen/README.md | 2 +- actix-web/CHANGES.md | 2 +- actix-web/README.md | 2 +- awc/CHANGES.md | 2 +- 23 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2c08267e..632cd063d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2018,9 +2018,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-traits" @@ -2967,9 +2967,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.45" +version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" dependencies = [ "deranged", "itoa", @@ -2982,15 +2982,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" dependencies = [ "num-conv", "time-core", diff --git a/Cargo.toml b/Cargo.toml index 79677d017..360239d33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web" license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.83" +rust-version = "1.88" [profile.dev] # Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index e366fbdd8..7c4d008d8 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -2,7 +2,7 @@ ## Unreleased -- Minimum supported Rust version (MSRV) is now 1.83. +- Minimum supported Rust version (MSRV) is now 1.88. ## 0.6.9 diff --git a/actix-files/README.md b/actix-files/README.md index e3c712e94..7bc0b90d8 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.9)](https://docs.rs/actix-files/0.6.9) -![Version](https://img.shields.io/badge/rustc-1.83+-ab6000.svg) +![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
[![dependency status](https://deps.rs/crate/actix-files/0.6.9/status.svg)](https://deps.rs/crate/actix-files/0.6.9) diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index d2e405fe9..be19d251a 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -2,7 +2,7 @@ ## Unreleased -- Minimum supported Rust version (MSRV) is now 1.83. +- Minimum supported Rust version (MSRV) is now 1.88. ## 3.2.0 diff --git a/actix-http-test/README.md b/actix-http-test/README.md index 617ef1b9b..c242c8ab2 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.2.0)](https://docs.rs/actix-http-test/3.2.0) -![Version](https://img.shields.io/badge/rustc-1.83+-ab6000.svg) +![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.2.0/status.svg)](https://deps.rs/crate/actix-http-test/3.2.0) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 4bbd7a880..197370bdf 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -2,7 +2,7 @@ ## Unreleased -- Minimum supported Rust version (MSRV) is now 1.83. +- Minimum supported Rust version (MSRV) is now 1.88. ## 3.11.2 diff --git a/actix-http/README.md b/actix-http/README.md index e94749d61..08dae437c 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -6,7 +6,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.11.2)](https://docs.rs/actix-http/3.11.2) -![Version](https://img.shields.io/badge/rustc-1.83+-ab6000.svg) +![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
[![dependency status](https://deps.rs/crate/actix-http/3.11.2/status.svg)](https://deps.rs/crate/actix-http/3.11.2) diff --git a/actix-multipart-derive/CHANGES.md b/actix-multipart-derive/CHANGES.md index e8f85bbe0..44e0d8435 100644 --- a/actix-multipart-derive/CHANGES.md +++ b/actix-multipart-derive/CHANGES.md @@ -2,7 +2,7 @@ ## Unreleased -- Minimum supported Rust version (MSRV) is now 1.83. +- Minimum supported Rust version (MSRV) is now 1.88. ## 0.7.0 diff --git a/actix-multipart-derive/README.md b/actix-multipart-derive/README.md index 82ef49257..05b37dfe8 100644 --- a/actix-multipart-derive/README.md +++ b/actix-multipart-derive/README.md @@ -6,7 +6,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-multipart-derive?label=latest)](https://crates.io/crates/actix-multipart-derive) [![Documentation](https://docs.rs/actix-multipart-derive/badge.svg?version=0.7.0)](https://docs.rs/actix-multipart-derive/0.7.0) -![Version](https://img.shields.io/badge/rustc-1.83+-ab6000.svg) +![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart-derive.svg)
[![dependency status](https://deps.rs/crate/actix-multipart-derive/0.7.0/status.svg)](https://deps.rs/crate/actix-multipart-derive/0.7.0) diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 206374bf0..57b84547d 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -2,7 +2,7 @@ ## Unreleased -- Minimum supported Rust version (MSRV) is now 1.83. +- Minimum supported Rust version (MSRV) is now 1.88. ## 0.7.2 diff --git a/actix-multipart/README.md b/actix-multipart/README.md index 1c1e90615..1faacd6a8 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.7.2)](https://docs.rs/actix-multipart/0.7.2) -![Version](https://img.shields.io/badge/rustc-1.83+-ab6000.svg) +![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
[![dependency status](https://deps.rs/crate/actix-multipart/0.7.2/status.svg)](https://deps.rs/crate/actix-multipart/0.7.2) diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 0a79111bd..781bdefe5 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -2,7 +2,7 @@ ## Unreleased -- Minimum supported Rust version (MSRV) is now 1.83. +- Minimum supported Rust version (MSRV) is now 1.88. ## 0.5.3 diff --git a/actix-router/README.md b/actix-router/README.md index 3f750b46a..20444904c 100644 --- a/actix-router/README.md +++ b/actix-router/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-router?label=latest)](https://crates.io/crates/actix-router) [![Documentation](https://docs.rs/actix-router/badge.svg?version=0.5.3)](https://docs.rs/actix-router/0.5.3) -![Version](https://img.shields.io/badge/rustc-1.83+-ab6000.svg) +![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-router.svg)
[![dependency status](https://deps.rs/crate/actix-router/0.5.3/status.svg)](https://deps.rs/crate/actix-router/0.5.3) diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index e368e2f7f..fc64cb7a3 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -2,7 +2,7 @@ ## Unreleased -- Minimum supported Rust version (MSRV) is now 1.83. +- Minimum supported Rust version (MSRV) is now 1.88. ## 0.1.5 diff --git a/actix-test/README.md b/actix-test/README.md index 3bd2d1550..3a0caf2ef 100644 --- a/actix-test/README.md +++ b/actix-test/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-test?label=latest)](https://crates.io/crates/actix-test) [![Documentation](https://docs.rs/actix-test/badge.svg?version=0.1.5)](https://docs.rs/actix-test/0.1.5) -![Version](https://img.shields.io/badge/rustc-1.83+-ab6000.svg) +![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-test.svg)
[![dependency status](https://deps.rs/crate/actix-test/0.1.5/status.svg)](https://deps.rs/crate/actix-test/0.1.5) diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index e85c62bd6..79fecaadf 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -2,7 +2,7 @@ ## Unreleased -- Minimum supported Rust version (MSRV) is now 1.83. +- Minimum supported Rust version (MSRV) is now 1.88. ## 4.3.1 diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index ff2a1af79..6a2e8f1a8 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -8,7 +8,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) [![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.3.1)](https://docs.rs/actix-web-actors/4.3.1) -![Version](https://img.shields.io/badge/rustc-1.83+-ab6000.svg) +![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
![maintenance-status](https://img.shields.io/badge/maintenance-deprecated-red.svg) diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 5b988d20a..6138892a1 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -2,7 +2,7 @@ ## Unreleased -- Minimum supported Rust version (MSRV) is now 1.83. +- Minimum supported Rust version (MSRV) is now 1.88. ## 4.3.0 diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index 1c2462f00..52d522b0a 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -6,7 +6,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) [![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=4.3.0)](https://docs.rs/actix-web-codegen/4.3.0) -![Version](https://img.shields.io/badge/rustc-1.83+-ab6000.svg) +![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
[![dependency status](https://deps.rs/crate/actix-web-codegen/4.3.0/status.svg)](https://deps.rs/crate/actix-web-codegen/4.3.0) diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index a9f417849..cf5971e9f 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -2,7 +2,7 @@ ## Unreleased -- Minimum supported Rust version (MSRV) is now 1.83. +- Minimum supported Rust version (MSRV) is now 1.88. ## 4.12.1 diff --git a/actix-web/README.md b/actix-web/README.md index df48fbce8..2704399d4 100644 --- a/actix-web/README.md +++ b/actix-web/README.md @@ -9,7 +9,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.12.1)](https://docs.rs/actix-web/4.12.1) -![MSRV](https://img.shields.io/badge/rustc-1.83+-ab6000.svg) +![MSRV](https://img.shields.io/badge/rustc-1.88+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) [![Dependency Status](https://deps.rs/crate/actix-web/4.12.1/status.svg)](https://deps.rs/crate/actix-web/4.12.1)
diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 22cf447f2..7d09212c2 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -2,7 +2,7 @@ ## Unreleased -- Minimum supported Rust version (MSRV) is now 1.83. +- Minimum supported Rust version (MSRV) is now 1.88. ## 3.8.1 From 2737e88973db26e6b6bf8d61767d38fe4bdac184 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Thu, 29 Jan 2026 21:19:25 +0900 Subject: [PATCH 08/32] chore(ci): pin semver-checks action (#3894) --- .github/workflows/semver-labeler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/semver-labeler.yml b/.github/workflows/semver-labeler.yml index 79ffd621c..0f96a0125 100644 --- a/.github/workflows/semver-labeler.yml +++ b/.github/workflows/semver-labeler.yml @@ -23,6 +23,6 @@ jobs: with: toolchain: stable - - uses: JohnTitor/cargo-semver-checks@main + - uses: JohnTitor/cargo-semver-checks@affdc88d7edadc20ad05cf7d901097bcd53ef29e # v0.2.0 with: label-prefix: B-semver- From aa8df45fce946cc4838a6605ddfb869aa961dd46 Mon Sep 17 00:00:00 2001 From: fasilmveloor <75309491+fasilmveloor@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:30:23 +0530 Subject: [PATCH 09/32] =?UTF-8?q?feat:=20implement=20FieldGroupReader=20fo?= =?UTF-8?q?r=20Option>=20in=20multipart=20form=E2=80=A6=20(#3577)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: implement FieldGroupReader for Option> in multipart form handling * add tests --------- Co-authored-by: Yuki Okushi --- actix-multipart/CHANGES.md | 3 ++ actix-multipart/src/form/mod.rs | 73 +++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 57b84547d..3dfca18b5 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -2,8 +2,11 @@ ## Unreleased +- Add `MultipartForm` support for `Option>` fields. [#3577] - Minimum supported Rust version (MSRV) is now 1.88. +[#3577]: https://github.com/actix/actix-web/pull/3577 + ## 0.7.2 - Fix re-exported version of `actix-multipart-derive`. diff --git a/actix-multipart/src/form/mod.rs b/actix-multipart/src/form/mod.rs index 693a45e8e..cb89b7cc1 100644 --- a/actix-multipart/src/form/mod.rs +++ b/actix-multipart/src/form/mod.rs @@ -187,6 +187,45 @@ where } } +impl<'t, T> FieldGroupReader<'t> for Option> +where + T: FieldReader<'t>, +{ + type Future = LocalBoxFuture<'t, Result<(), MultipartError>>; + + fn handle_field( + req: &'t HttpRequest, + field: Field, + limits: &'t mut Limits, + state: &'t mut State, + _duplicate_field: DuplicateField, + ) -> Self::Future { + let field_name = field.name().unwrap().to_string(); + + Box::pin(async move { + let vec = state + .entry(field_name) + .or_insert_with(|| Box::>::default()) + .downcast_mut::>() + .unwrap(); + + let item = T::read_field(req, field, limits).await?; + vec.push(item); + + Ok(()) + }) + } + + fn from_state(name: &str, state: &'t mut State) -> Result { + if let Some(boxed_vec) = state.remove(name) { + let vec = *boxed_vec.downcast::>().unwrap(); + Ok(Some(vec)) + } else { + Ok(None) + } + } +} + /// Trait that allows a type to be used in the [`struct@MultipartForm`] extractor. /// /// You should use the [`macro@MultipartForm`] macro to derive this for your struct. @@ -506,6 +545,40 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); } + /// Test `Option` fields. + #[derive(MultipartForm)] + struct TestOptionVec { + list1: Option>>, + list2: Option>>, + } + + async fn test_option_vec_route(form: MultipartForm) -> impl Responder { + let form = form.into_inner(); + let strings = form + .list1 + .unwrap() + .into_iter() + .map(|s| s.into_inner()) + .collect::>(); + assert_eq!(strings, vec!["value1", "value2", "value3"]); + assert!(form.list2.is_none()); + HttpResponse::Ok().finish() + } + + #[actix_rt::test] + async fn test_option_vec() { + let srv = + actix_test::start(|| App::new().route("/", web::post().to(test_option_vec_route))); + + let mut form = multipart::Form::default(); + form.add_text("list1", "value1"); + form.add_text("list1", "value2"); + form.add_text("list1", "value3"); + + let response = send_form(&srv, form, "/").await; + assert_eq!(response.status(), StatusCode::OK); + } + /// Test the `rename` field attribute. #[derive(MultipartForm)] struct TestFieldRenaming { From cf2b097de6b36ffc6a169cf1784647a70cff4bf7 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sun, 1 Feb 2026 16:17:59 +0900 Subject: [PATCH 10/32] feat(ci): skip semver-labeling if human added it (#3896) --- .github/workflows/semver-labeler.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/semver-labeler.yml b/.github/workflows/semver-labeler.yml index 0f96a0125..131bd81b9 100644 --- a/.github/workflows/semver-labeler.yml +++ b/.github/workflows/semver-labeler.yml @@ -23,6 +23,7 @@ jobs: with: toolchain: stable - - uses: JohnTitor/cargo-semver-checks@affdc88d7edadc20ad05cf7d901097bcd53ef29e # v0.2.0 + - uses: JohnTitor/cargo-semver-checks@3b76737b550e48ad0bd5912e2757e80eee6294b0 # v0.2.1 with: label-prefix: B-semver- + label-strategy: skip-if-human From 69edde96620981ddd00bca8e5c996a9675a19847 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sun, 1 Feb 2026 16:54:47 +0900 Subject: [PATCH 11/32] feat(web): implement `HttpRequest::url_for_iter`/`url_for_map` (#3895) --- actix-web/CHANGES.md | 3 + actix-web/src/request.rs | 130 ++++++++++++++++++++++++++++++++++++++- actix-web/src/rmap.rs | 54 +++++++++++++++- 3 files changed, 185 insertions(+), 2 deletions(-) diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index cf5971e9f..47624967d 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -3,6 +3,9 @@ ## Unreleased - Minimum supported Rust version (MSRV) is now 1.88. +- Add `HttpRequest::url_for_map` and `HttpRequest::url_for_iter` methods for named URL parameters. [#3895] + +[#3895]: https://github.com/actix/actix-web/pull/3895 ## 4.12.1 diff --git a/actix-web/src/request.rs b/actix-web/src/request.rs index a49a55bd0..3a3e539bf 100644 --- a/actix-web/src/request.rs +++ b/actix-web/src/request.rs @@ -1,6 +1,9 @@ use std::{ cell::{Ref, RefCell, RefMut}, - fmt, net, + collections::HashMap, + fmt, + hash::{BuildHasher, Hash}, + net, rc::Rc, str, }; @@ -242,6 +245,76 @@ impl HttpRequest { self.resource_map().url_for(self, name, elements) } + /// Generates URL for a named resource using a map of dynamic segment values. + /// + /// This substitutes URL parameters by name from `elements`, including parameters from parent + /// scopes. + /// + /// # Examples + /// ``` + /// # use std::collections::HashMap; + /// # use actix_web::{web, App, HttpRequest, HttpResponse}; + /// fn index(req: HttpRequest) -> HttpResponse { + /// let mut params = HashMap::new(); + /// params.insert("one", "1"); + /// params.insert("two", "2"); + /// let url = req.url_for_map("foo", ¶ms); // <- generate URL for "foo" resource + /// HttpResponse::Ok().into() + /// } + /// + /// let app = App::new() + /// .service(web::resource("/test/{one}/{two}") + /// .name("foo") // <- set resource name so it can be used in `url_for_map` + /// .route(web::get().to(|| HttpResponse::Ok())) + /// ); + /// ``` + pub fn url_for_map( + &self, + name: &str, + elements: &HashMap, + ) -> Result + where + K: std::borrow::Borrow + Eq + Hash, + V: AsRef, + S: BuildHasher, + { + self.resource_map().url_for_map(self, name, elements) + } + + /// Generates URL for a named resource using an iterator of key-value pairs. + /// + /// This is a convenience wrapper around [`HttpRequest::url_for_map`]. + /// + /// Note: passing a borrowed map (e.g. `&HashMap`) directly does not satisfy the + /// trait bounds because the iterator yields `(&String, &String)`. Prefer `url_for_map` for + /// borrowed maps, or map entries to `&str`: + /// + /// ``` + /// # use std::collections::HashMap; + /// # use actix_web::{web, App, HttpRequest, HttpResponse}; + /// fn index(req: HttpRequest) -> HttpResponse { + /// let mut params = HashMap::new(); + /// params.insert("one".to_string(), "1".to_string()); + /// params.insert("two".to_string(), "2".to_string()); + /// + /// let iter = params.iter().map(|(k, v)| (k.as_str(), v.as_str())); + /// let url = req.url_for_iter("foo", iter); + /// HttpResponse::Ok().into() + /// } + /// ``` + pub fn url_for_iter( + &self, + name: &str, + elements: I, + ) -> Result + where + I: IntoIterator, + K: std::borrow::Borrow + Eq + Hash, + V: AsRef, + { + self.resource_map().url_for_iter(self, name, elements) + } + /// Generate URL for named resource /// /// This method is similar to `HttpRequest::url_for()` but it can be used @@ -550,6 +623,8 @@ impl HttpRequestPool { #[cfg(test)] mod tests { + use std::collections::HashMap; + use bytes::Bytes; use super::*; @@ -638,6 +713,59 @@ mod tests { ); } + #[test] + fn test_url_for_map() { + let mut res = ResourceDef::new("/user/{name}.{ext}"); + res.set_name("index"); + + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); + rmap.add(&mut res, None); + + let req = TestRequest::default() + .insert_header((header::HOST, "www.actix.rs")) + .rmap(rmap) + .to_http_request(); + + let mut params = HashMap::new(); + params.insert("name", "test"); + params.insert("ext", "html"); + + let url = req.url_for_map("index", ¶ms); + assert_eq!( + url.ok().unwrap().as_str(), + "http://www.actix.rs/user/test.html" + ); + + params.remove("ext"); + assert_eq!( + req.url_for_map("index", ¶ms), + Err(UrlGenerationError::NotEnoughElements) + ); + } + + #[test] + fn test_url_for_iter() { + let mut res = ResourceDef::new("/user/{name}.{ext}"); + res.set_name("index"); + + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); + rmap.add(&mut res, None); + + let req = TestRequest::default() + .insert_header((header::HOST, "www.actix.rs")) + .rmap(rmap) + .to_http_request(); + + let url = req.url_for_iter("index", [("ext", "html"), ("name", "test")]); + assert_eq!( + url.ok().unwrap().as_str(), + "http://www.actix.rs/user/test.html" + ); + + let url = req.url_for_iter("index", [("name", "test")]); + assert_eq!(url, Err(UrlGenerationError::NotEnoughElements)); + } + #[test] fn test_url_for_static() { let mut rdef = ResourceDef::new("/index.html"); diff --git a/actix-web/src/rmap.rs b/actix-web/src/rmap.rs index b445687ac..ee86d271b 100644 --- a/actix-web/src/rmap.rs +++ b/actix-web/src/rmap.rs @@ -1,7 +1,9 @@ use std::{ - borrow::Cow, + borrow::{Borrow, Cow}, cell::RefCell, + collections::HashMap, fmt::Write as _, + hash::{BuildHasher, Hash}, rc::{Rc, Weak}, }; @@ -140,6 +142,56 @@ impl ResourceMap { }) .ok_or(UrlGenerationError::NotEnoughElements)?; + self.url_from_path(req, path) + } + + /// Generate URL for named resource using map of dynamic segment values. + /// + /// Check [`HttpRequest::url_for_map`] for detailed information. + pub fn url_for_map( + &self, + req: &HttpRequest, + name: &str, + elements: &HashMap, + ) -> Result + where + K: Borrow + Eq + Hash, + V: AsRef, + S: BuildHasher, + { + let path = self + .named + .get(name) + .ok_or(UrlGenerationError::ResourceNotFound)? + .root_rmap_fn(String::with_capacity(AVG_PATH_LEN), |mut acc, node| { + node.pattern + .resource_path_from_map(&mut acc, elements) + .then_some(acc) + }) + .ok_or(UrlGenerationError::NotEnoughElements)?; + + self.url_from_path(req, path) + } + + /// Generate URL for named resource using an iterator of key-value pairs. + /// + /// Check [`HttpRequest::url_for_iter`] for detailed information. + pub fn url_for_iter( + &self, + req: &HttpRequest, + name: &str, + elements: I, + ) -> Result + where + I: IntoIterator, + K: Borrow + Eq + Hash, + V: AsRef, + { + let elements = elements.into_iter().collect::>(); + self.url_for_map(req, name, &elements) + } + + fn url_from_path(&self, req: &HttpRequest, path: String) -> Result { let (base, path): (Cow<'_, _>, _) = if path.starts_with('/') { // build full URL from connection info parts and resource path let conn = req.connection_info(); From 0eef0601a1eb1d23dfed7b82bbaae0f87a89f879 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sun, 1 Feb 2026 22:10:47 +0900 Subject: [PATCH 12/32] docs(files): refine docs of `read_mode_threshold` (#3897) --- actix-files/src/files.rs | 8 ++++---- actix-files/src/named.rs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index 3491f59e2..afc852a18 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -220,11 +220,11 @@ impl Files { /// Sets the size threshold that determines file read mode (sync/async). /// - /// When a file is smaller than the threshold (bytes), the reader will switch from synchronous - /// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large - /// files. + /// When a file is smaller than the threshold (bytes), the reader will use synchronous + /// (blocking) file reads. For larger files, it switches to async reads to avoid blocking the + /// main thread. /// - /// Tweaking this value according to your expected usage may lead to signifiant performance + /// Tweaking this value according to your expected usage may lead to significant performance /// gains (or losses in other handlers, if `size` is too high). /// /// When the `experimental-io-uring` crate feature is enabled, file reads are always async. diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 23aa10d5c..7ff66e74d 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -357,11 +357,11 @@ impl NamedFile { /// Sets the size threshold that determines file read mode (sync/async). /// - /// When a file is smaller than the threshold (bytes), the reader will switch from synchronous - /// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large - /// files. + /// When a file is smaller than the threshold (bytes), the reader will use synchronous + /// (blocking) file reads. For larger files, it switches to async reads to avoid blocking the + /// main thread. /// - /// Tweaking this value according to your expected usage may lead to signifiant performance + /// Tweaking this value according to your expected usage may lead to significant performance /// gains (or losses in other handlers, if `size` is too high). /// /// When the `experimental-io-uring` crate feature is enabled, file reads are always async. From dfff65150333215abe3b73c5954ee12f6c2b4169 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:29:48 +0900 Subject: [PATCH 13/32] build(deps): bump trybuild from 1.0.114 to 1.0.115 (#3901) Bumps [trybuild](https://github.com/dtolnay/trybuild) from 1.0.114 to 1.0.115. - [Release notes](https://github.com/dtolnay/trybuild/releases) - [Commits](https://github.com/dtolnay/trybuild/compare/1.0.114...1.0.115) --- updated-dependencies: - dependency-name: trybuild dependency-version: 1.0.115 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 632cd063d..07c0ef1ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3224,9 +3224,9 @@ dependencies = [ [[package]] name = "trybuild" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17e807bff86d2a06b52bca4276746584a78375055b6e45843925ce2802b335" +checksum = "5f614c21bd3a61bad9501d75cbb7686f00386c806d7f456778432c25cf86948a" dependencies = [ "glob", "serde", From 85cc60708c95f6a82c26bb2c6f0b3f5e8dbe54db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:30:00 +0900 Subject: [PATCH 14/32] build(deps): bump taiki-e/install-action from 2.67.10 to 2.67.18 (#3899) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.67.10 to 2.67.18. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/81a2f66614862089b24532663f669a485d79c889...650c5ca14212efbbf3e580844b04bdccf68dac31) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.67.18 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-post-merge.yml | 4 ++-- .github/workflows/ci.yml | 4 ++-- .github/workflows/coverage.yml | 2 +- .github/workflows/lint.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-post-merge.yml b/.github/workflows/ci-post-merge.yml index 3f4899352..acf0e6135 100644 --- a/.github/workflows/ci-post-merge.yml +++ b/.github/workflows/ci-post-merge.yml @@ -49,7 +49,7 @@ jobs: toolchain: ${{ matrix.version.version }} - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 + uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 with: tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean @@ -83,7 +83,7 @@ jobs: uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2 - name: Install just, cargo-hack - uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 + uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 with: tool: just,cargo-hack diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 557832dab..db82b6e36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: toolchain: ${{ matrix.version.version }} - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 + uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 with: tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean @@ -117,7 +117,7 @@ jobs: toolchain: nightly - name: Install just - uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 + uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 with: tool: just diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 1ee0710b6..6fba6ad08 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -24,7 +24,7 @@ jobs: components: llvm-tools - name: Install just, cargo-llvm-cov, cargo-nextest - uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 + uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 with: tool: just,cargo-llvm-cov,cargo-nextest diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f9de38600..a630b9ba8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -77,7 +77,7 @@ jobs: toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }} - name: Install just - uses: taiki-e/install-action@81a2f66614862089b24532663f669a485d79c889 # v2.67.10 + uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 with: tool: just From 9021b82b91c8c6bc9dadd02574b1bfd2240be327 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:30:15 +0900 Subject: [PATCH 15/32] build(deps): bump slab from 0.4.11 to 0.4.12 (#3900) Bumps [slab](https://github.com/tokio-rs/slab) from 0.4.11 to 0.4.12. - [Release notes](https://github.com/tokio-rs/slab/releases) - [Changelog](https://github.com/tokio-rs/slab/blob/master/CHANGELOG.md) - [Commits](https://github.com/tokio-rs/slab/compare/v0.4.11...v0.4.12) --- updated-dependencies: - dependency-name: slab dependency-version: 0.4.12 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 07c0ef1ae..886aba632 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2783,9 +2783,9 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" From 5ee6b59b1e369d6ca01e80f73cbe82772132253d Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Mon, 2 Feb 2026 05:15:47 -0700 Subject: [PATCH 16/32] feat: expose PathBufWrap utility for public access (#3694) * feat: expose PathBufWrap utility for public access * fix: rename to `parse_unprocessed_req` --------- Co-authored-by: Yuki Okushi --- actix-files/CHANGES.md | 3 +++ actix-files/src/error.rs | 1 + actix-files/src/lib.rs | 5 ++--- actix-files/src/path_buf.rs | 37 ++++++++++++++++++++++++++++++++++++- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 7c4d008d8..307f5852d 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -3,6 +3,9 @@ ## Unreleased - Minimum supported Rust version (MSRV) is now 1.88. +- `PathBufWrap` & `UriSegmentError` made public. [#3694] + +[#3694]: https://github.com/actix/actix-web/pull/3694 ## 0.6.9 diff --git a/actix-files/src/error.rs b/actix-files/src/error.rs index e762116e6..1ba4ce67e 100644 --- a/actix-files/src/error.rs +++ b/actix-files/src/error.rs @@ -21,6 +21,7 @@ impl ResponseError for FilesError { } } +/// Error which can occur with parsing/validating a request-uri path #[derive(Debug, PartialEq, Eq, Display)] #[non_exhaustive] pub enum UriSegmentError { diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 9859db456..b0bb76ace 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -37,13 +37,12 @@ mod range; mod service; pub use self::{ - chunked::ChunkedReadFile, directory::Directory, files::Files, named::NamedFile, - range::HttpRange, service::FilesService, + chunked::ChunkedReadFile, directory::Directory, error::UriSegmentError, files::Files, + named::NamedFile, path_buf::PathBufWrap, range::HttpRange, service::FilesService, }; use self::{ directory::{directory_listing, DirectoryRenderer}, error::FilesError, - path_buf::PathBufWrap, }; type HttpService = BoxService; diff --git a/actix-files/src/path_buf.rs b/actix-files/src/path_buf.rs index c1983279b..0fe8493bf 100644 --- a/actix-files/src/path_buf.rs +++ b/actix-files/src/path_buf.rs @@ -8,8 +8,11 @@ use actix_web::{dev::Payload, FromRequest, HttpRequest}; use crate::error::UriSegmentError; +/// Secure Path Traversal Guard +/// +/// This struct parses a request-uri [`PathBuf`](std::path::PathBuf) #[derive(Debug, PartialEq, Eq)] -pub(crate) struct PathBufWrap(PathBuf); +pub struct PathBufWrap(PathBuf); impl FromStr for PathBufWrap { type Err = UriSegmentError; @@ -20,6 +23,37 @@ impl FromStr for PathBufWrap { } impl PathBufWrap { + /// Parse a safe path from the unprocessed tail of a supplied + /// [`HttpRequest`](actix_web::HttpRequest), given the choice of allowing hidden files to be + /// considered valid segments. + /// + /// This uses [`HttpRequest::match_info`](actix_web::HttpRequest::match_info) and + /// [`Path::unprocessed`](actix_web::dev::Path::unprocessed), which returns the part of the + /// path not matched by route patterns. This is useful for mounted services (eg. `Files`), + /// where only the tail should be parsed. + /// + /// Path traversal is guarded by this method. + #[inline] + pub fn parse_unprocessed_req( + req: &HttpRequest, + hidden_files: bool, + ) -> Result { + Self::parse_path(req.match_info().unprocessed(), hidden_files) + } + + /// Parse a safe path from the full request path of a supplied + /// [`HttpRequest`](actix_web::HttpRequest), given the choice of allowing hidden files to be + /// considered valid segments. + /// + /// This uses [`HttpRequest::path`](actix_web::HttpRequest::path), and is more appropriate + /// for non-mounted handlers that want the entire request path. + /// + /// Path traversal is guarded by this method. + #[inline] + pub fn parse_req_path(req: &HttpRequest, hidden_files: bool) -> Result { + Self::parse_path(req.path(), hidden_files) + } + /// Parse a path, giving the choice of allowing hidden files to be considered valid segments. /// /// Path traversal is guarded by this method. @@ -91,6 +125,7 @@ impl FromRequest for PathBufWrap { type Future = Ready>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + // Uses the unprocessed tail of the request path and disallows hidden files. ready(req.match_info().unprocessed().parse()) } } From aecc7ad09c5b79675940cf171c95dc6b52c198e2 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Tue, 3 Feb 2026 18:02:13 +0900 Subject: [PATCH 17/32] test(http-test): set ALPN explicitly (#3902) --- actix-http/tests/test_rustls.rs | 53 ++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 29e559666..b58c1138d 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -51,7 +51,7 @@ where Ok(buf) } -fn tls_config() -> RustlsServerConfig { +fn tls_config_with_alpn(protocols: &[&[u8]]) -> RustlsServerConfig { let rcgen::CertifiedKey { cert, key_pair } = rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); let cert_chain = vec![cert.der().clone()]; @@ -62,12 +62,23 @@ fn tls_config() -> RustlsServerConfig { .with_single_cert(cert_chain, key_der) .unwrap(); - config.alpn_protocols.push(HTTP1_1_ALPN_PROTOCOL.to_vec()); - config.alpn_protocols.push(H2_ALPN_PROTOCOL.to_vec()); + config.alpn_protocols = protocols.iter().map(|proto| proto.to_vec()).collect(); config } +fn tls_config() -> RustlsServerConfig { + tls_config_with_alpn(&[HTTP1_1_ALPN_PROTOCOL, H2_ALPN_PROTOCOL]) +} + +fn tls_config_h1() -> RustlsServerConfig { + tls_config_with_alpn(&[HTTP1_1_ALPN_PROTOCOL]) +} + +fn tls_config_h2() -> RustlsServerConfig { + tls_config_with_alpn(&[H2_ALPN_PROTOCOL]) +} + pub fn get_negotiated_alpn_protocol( addr: SocketAddr, client_alpn_protocol: &[u8], @@ -98,7 +109,7 @@ async fn h1() -> io::Result<()> { let srv = test_server(move || { HttpService::build() .h1(|_| ok::<_, Error>(Response::ok())) - .rustls_0_23(tls_config()) + .rustls_0_23(tls_config_h1()) }) .await; @@ -112,7 +123,7 @@ async fn h2() -> io::Result<()> { let srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Error>(Response::ok())) - .rustls_0_23(tls_config()) + .rustls_0_23(tls_config_h2()) }) .await; @@ -130,7 +141,7 @@ async fn h1_1() -> io::Result<()> { assert_eq!(req.version(), Version::HTTP_11); ok::<_, Error>(Response::ok()) }) - .rustls_0_23(tls_config()) + .rustls_0_23(tls_config_h1()) }) .await; @@ -149,7 +160,7 @@ async fn h2_1() -> io::Result<()> { ok::<_, Error>(Response::ok()) }) .rustls_0_23_with_config( - tls_config(), + tls_config_h2(), TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)), ) }) @@ -169,7 +180,7 @@ async fn h2_body1() -> io::Result<()> { let body = load_body(req.take_payload()).await?; Ok::<_, Error>(Response::ok().set_body(body)) }) - .rustls_0_23(tls_config()) + .rustls_0_23(tls_config_h2()) }) .await; @@ -195,7 +206,7 @@ async fn h2_content_length() { ]; ok::<_, Infallible>(Response::new(statuses[indx])) }) - .rustls_0_23(tls_config()) + .rustls_0_23(tls_config_h2()) }) .await; @@ -267,7 +278,7 @@ async fn h2_headers() { } ok::<_, Infallible>(config.body(data.clone())) }) - .rustls_0_23(tls_config()) + .rustls_0_23(tls_config_h2()) }) .await; @@ -306,7 +317,7 @@ async fn h2_body2() { let mut srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) - .rustls_0_23(tls_config()) + .rustls_0_23(tls_config_h2()) }) .await; @@ -323,7 +334,7 @@ async fn h2_head_empty() { let mut srv = test_server(move || { HttpService::build() .finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) - .rustls_0_23(tls_config()) + .rustls_0_23(tls_config_h2()) }) .await; @@ -349,7 +360,7 @@ async fn h2_head_binary() { let mut srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) - .rustls_0_23(tls_config()) + .rustls_0_23(tls_config_h2()) }) .await; @@ -374,7 +385,7 @@ async fn h2_head_binary2() { let srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) - .rustls_0_23(tls_config()) + .rustls_0_23(tls_config_h2()) }) .await; @@ -400,7 +411,7 @@ async fn h2_body_length() { Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), ) }) - .rustls_0_23(tls_config()) + .rustls_0_23(tls_config_h2()) }) .await; @@ -424,7 +435,7 @@ async fn h2_body_chunked_explicit() { .body(BodyStream::new(body)), ) }) - .rustls_0_23(tls_config()) + .rustls_0_23(tls_config_h2()) }) .await; @@ -453,7 +464,7 @@ async fn h2_response_http_error_handling() { ) })) })) - .rustls_0_23(tls_config()) + .rustls_0_23(tls_config_h2()) }) .await; @@ -483,7 +494,7 @@ async fn h2_service_error() { let mut srv = test_server(move || { HttpService::build() .h2(|_| err::, _>(BadRequest)) - .rustls_0_23(tls_config()) + .rustls_0_23(tls_config_h2()) }) .await; @@ -500,7 +511,7 @@ async fn h1_service_error() { let mut srv = test_server(move || { HttpService::build() .h1(|_| err::, _>(BadRequest)) - .rustls_0_23(tls_config()) + .rustls_0_23(tls_config_h1()) }) .await; @@ -519,7 +530,7 @@ const CUSTOM_ALPN_PROTOCOL: &[u8] = b"custom"; #[actix_rt::test] async fn alpn_h1() -> io::Result<()> { let srv = test_server(move || { - let mut config = tls_config(); + let mut config = tls_config_h1(); config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec()); HttpService::build() .h1(|_| ok::<_, Error>(Response::ok())) @@ -541,7 +552,7 @@ async fn alpn_h1() -> io::Result<()> { #[actix_rt::test] async fn alpn_h2() -> io::Result<()> { let srv = test_server(move || { - let mut config = tls_config(); + let mut config = tls_config_h2(); config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec()); HttpService::build() .h2(|_| ok::<_, Error>(Response::ok())) From 204a3e1384b94790bdd77ba8fb93477dcf8405e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ihc=E7=AB=A5=E9=9E=8B=40=E6=8F=90=E4=B8=8D=E8=B5=B7?= =?UTF-8?q?=E5=8A=B2?= Date: Tue, 3 Feb 2026 09:03:45 +0000 Subject: [PATCH 18/32] fix: set error to Payload before dispatcher disconnect (#3068) * fix: set error to Payload before dispatcher disconnect * align behavior --------- Co-authored-by: Yuki Okushi --- actix-http/CHANGES.md | 3 ++ actix-http/src/h1/dispatcher.rs | 1 + actix-http/tests/test_server.rs | 54 +++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 197370bdf..f052c6d22 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -3,6 +3,9 @@ ## Unreleased - Minimum supported Rust version (MSRV) is now 1.88. +- Fix truncated body ending without error when connection closed abnormally. [#3067] + +[#3067]: https://github.com/actix/actix-web/pull/3067 ## 3.11.2 diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 03851d0fb..c59be2d50 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -1156,6 +1156,7 @@ where let inner = inner.as_mut().project(); inner.flags.insert(Flags::READ_DISCONNECT); if let Some(mut payload) = inner.payload.take() { + payload.set_error(PayloadError::Incomplete(None)); payload.feed_eof(); } }; diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index aafcde19a..688fc9d0b 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -443,6 +443,60 @@ async fn content_length() { srv.stop().await; } +#[actix_rt::test] +async fn content_length_truncated() { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + + let mut srv = test_server(|| { + HttpService::build() + .h1(|mut req: Request| async move { + let expected_length: usize = req.uri().path()[1..].parse().unwrap(); + let mut payload = req.take_payload(); + + let mut length = 0; + let mut seen_error = false; + while let Some(chunk) = payload.next().await { + match chunk { + Ok(b) => length += b.len(), + Err(_) => { + seen_error = true; + break; + } + } + } + if seen_error { + return Result::<_, Infallible>::Ok(Response::bad_request()); + } + + assert_eq!(length, expected_length, "length must match when no error"); + Result::<_, Infallible>::Ok(Response::ok()) + }) + .tcp() + }) + .await; + + let addr = srv.addr(); + let mut buf = [0; 12]; + + let mut conn = TcpStream::connect(&addr).await.unwrap(); + conn.write_all(b"POST /10000 HTTP/1.1\r\nContent-Length: 10000\r\n\r\ndata_truncated") + .await + .unwrap(); + conn.shutdown().await.unwrap(); + conn.read_exact(&mut buf).await.unwrap(); + assert_eq!(&buf, b"HTTP/1.1 400"); + + let mut conn = TcpStream::connect(&addr).await.unwrap(); + conn.write_all(b"POST /4 HTTP/1.1\r\nContent-Length: 4\r\n\r\ndata") + .await + .unwrap(); + conn.shutdown().await.unwrap(); + conn.read_exact(&mut buf).await.unwrap(); + assert_eq!(&buf, b"HTTP/1.1 200"); + + srv.stop().await; +} + #[actix_rt::test] async fn h1_headers() { let data = STR.repeat(10); From ef75c402d65183f156231d4a52cf0db8ad6b97e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 06:09:02 +0900 Subject: [PATCH 19/32] build(deps): bump bytes from 1.11.0 to 1.11.1 (#3903) Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.11.0 to 1.11.1. - [Release notes](https://github.com/tokio-rs/bytes/releases) - [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md) - [Commits](https://github.com/tokio-rs/bytes/compare/v1.11.0...v1.11.1) --- updated-dependencies: - dependency-name: bytes dependency-version: 1.11.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 886aba632..0316a0076 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -742,9 +742,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bytesize" From 6efc4bdfb52efecb7b4df88ecf098c6a2764043b Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Wed, 4 Feb 2026 18:59:11 +0900 Subject: [PATCH 20/32] chore(*): update deps (#3904) --- Cargo.lock | 419 +++++++++++++++++++++++++---------------------------- 1 file changed, 195 insertions(+), 224 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0316a0076..113c8fc20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,7 +112,7 @@ dependencies = [ "rand 0.9.2", "rcgen", "regex", - "rustls 0.23.35", + "rustls 0.23.36", "rustls-pki-types", "rustversion", "serde", @@ -296,7 +296,7 @@ dependencies = [ "rustls 0.20.9", "rustls 0.21.12", "rustls 0.22.4", - "rustls 0.23.35", + "rustls 0.23.36", "serde", "serde_json", "serde_urlencoded", @@ -387,7 +387,7 @@ dependencies = [ "rcgen", "regex", "regex-lite", - "rustls 0.23.35", + "rustls 0.23.36", "rustls-pki-types", "serde", "serde_json", @@ -648,7 +648,7 @@ dependencies = [ "rustls 0.20.9", "rustls 0.21.12", "rustls 0.22.4", - "rustls 0.23.35", + "rustls 0.23.36", "rustls-pki-types", "serde", "serde_json", @@ -660,9 +660,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.15.1" +version = "1.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" +checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" dependencies = [ "aws-lc-sys", "zeroize", @@ -670,9 +670,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.34.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" +checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" dependencies = [ "cc", "cmake", @@ -736,9 +736,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytes" @@ -769,9 +769,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.47" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "jobserver", @@ -824,18 +824,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" dependencies = [ "anstyle", "clap_lex", @@ -844,15 +844,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "cmake" -version = "0.1.54" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" dependencies = [ "cc", ] @@ -1102,9 +1102,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "deranged" @@ -1265,15 +1265,15 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1420,9 +1420,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -1478,12 +1478,13 @@ dependencies = [ [[package]] name = "half" -version = "2.4.1" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -1522,7 +1523,7 @@ dependencies = [ "once_cell", "rand 0.9.2", "ring 0.17.14", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tokio", "tracing", @@ -1545,7 +1546,7 @@ dependencies = [ "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1609,21 +1610,22 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1632,99 +1634,61 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -1744,9 +1708,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1760,9 +1724,9 @@ checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] name = "indexmap" -version = "2.11.4" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", @@ -1839,9 +1803,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" dependencies = [ "jiff-static", "log", @@ -1852,9 +1816,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" dependencies = [ "proc-macro2", "quote", @@ -1873,9 +1837,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -1889,9 +1853,9 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "linux-raw-sys" @@ -1901,9 +1865,9 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.7.4" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "local-channel" @@ -1971,9 +1935,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", @@ -1983,9 +1947,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.11" +version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" +checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -1993,7 +1957,6 @@ dependencies = [ "equivalent", "parking_lot", "portable-atomic", - "rustc_version", "smallvec", "tagptr", "uuid", @@ -2097,9 +2060,9 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" @@ -2232,19 +2195,28 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2302,7 +2274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2322,7 +2294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2331,23 +2303,23 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -2355,9 +2327,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -2387,9 +2359,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2399,9 +2371,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2410,15 +2382,15 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "resolv-conf" @@ -2449,7 +2421,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted 0.9.0", "windows-sys 0.52.0", @@ -2466,9 +2438,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags 2.10.0", "errno", @@ -2517,24 +2489,24 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "aws-lc-rs", "log", "once_cell", "rustls-pki-types", - "rustls-webpki 0.103.8", + "rustls-webpki 0.103.9", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2574,9 +2546,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "aws-lc-rs", "ring 0.17.14", @@ -2598,9 +2570,9 @@ checksum = "b6ceb60223ee771fb5dfe462e29e5ee92bca9a7b9c555584f4d361045dae0e12" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "same-file" @@ -2719,9 +2691,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ "serde_core", ] @@ -2768,18 +2740,19 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "slab" @@ -2861,9 +2834,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -2895,9 +2868,9 @@ checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -2936,11 +2909,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -2956,9 +2929,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -2998,9 +2971,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -3108,7 +3081,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.35", + "rustls 0.23.36", "tokio", ] @@ -3153,9 +3126,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "0.9.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" dependencies = [ "indexmap", "serde_core", @@ -3168,27 +3141,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tracing" @@ -3245,9 +3218,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" @@ -3301,12 +3274,6 @@ dependencies = [ "serde", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -3321,9 +3288,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ "getrandom 0.3.4", "js-sys", @@ -3366,18 +3333,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -3388,9 +3355,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3398,9 +3365,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -3411,18 +3378,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -3459,14 +3426,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.4", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -3738,9 +3705,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" [[package]] name = "winreg" @@ -3754,21 +3721,15 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yasna" @@ -3781,11 +3742,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3793,9 +3753,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -3805,18 +3765,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.30" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" +checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.30" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" +checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" dependencies = [ "proc-macro2", "quote", @@ -3825,9 +3785,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] @@ -3851,10 +3811,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] -name = "zerovec" -version = "0.10.4" +name = "zerotrie" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -3863,9 +3834,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", @@ -3874,9 +3845,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.2" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4a4e8e9dc5c62d159f04fcdbe07f4c3fb710415aab4754bf11505501e3251d" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" [[package]] name = "zstd" From 2c0be64b68b8466239c66c05fa7cf9f315fecf80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 06:09:25 +0900 Subject: [PATCH 21/32] build(deps): bump time from 0.3.46 to 0.3.47 (#3906) Bumps [time](https://github.com/time-rs/time) from 0.3.46 to 0.3.47. - [Release notes](https://github.com/time-rs/time/releases) - [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md) - [Commits](https://github.com/time-rs/time/compare/v0.3.46...v0.3.47) --- updated-dependencies: - dependency-name: time dependency-version: 0.3.47 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 113c8fc20..0dbf5db9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2940,9 +2940,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.46" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -2961,9 +2961,9 @@ checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", From d9b96e635d1c09a57fb81cd983a6b1f18e4ade4e Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Fri, 6 Feb 2026 21:04:03 +0900 Subject: [PATCH 22/32] Merge commit from fork --- actix-files/src/files.rs | 6 +++++- actix-files/src/lib.rs | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index afc852a18..a5e22a6d0 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -96,6 +96,9 @@ impl Files { /// If the mount path is set as the root path `/`, services registered after this one will /// be inaccessible. Register more specific handlers and services first. /// + /// If `serve_from` cannot be canonicalized at startup, an error is logged and the original + /// path is preserved. Requests will return `404 Not Found` until the path exists. + /// /// `Files` utilizes the existing Tokio thread-pool for blocking filesystem operations. /// The number of running threads is adjusted over time as needed, up to a maximum of 512 times /// the number of server [workers](actix_web::HttpServer::workers), by default. @@ -105,7 +108,8 @@ impl Files { Ok(canon_dir) => canon_dir, Err(_) => { log::error!("Specified path is not a directory: {:?}", orig_dir); - PathBuf::new() + // Preserve original path so requests don't fall back to CWD. + orig_dir } }; diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index b0bb76ace..43bda9585 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -780,6 +780,16 @@ mod tests { assert_eq!(resp.status(), StatusCode::NOT_FOUND); } + #[actix_rt::test] + async fn test_static_files_bad_directory_does_not_serve_cwd_files() { + let service = Files::new("/", "./missing").new_service(()).await.unwrap(); + + let req = TestRequest::with_uri("/Cargo.toml").to_srv_request(); + let resp = test::call_service(&service, req).await; + + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + #[actix_rt::test] async fn test_default_handler_file_missing() { let st = Files::new("/", ".") From 06a354fe52561944fb2881afee6879a25964e1db Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Fri, 6 Feb 2026 21:05:15 +0900 Subject: [PATCH 23/32] Merge commit from fork --- actix-files/src/lib.rs | 18 ++++++++++++++++++ actix-files/src/named.rs | 9 ++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 43bda9585..5ea578c0d 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -470,6 +470,24 @@ mod tests { assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); } + #[actix_rt::test] + async fn test_named_file_empty_range_headers() { + let srv = actix_test::start(|| App::new().service(Files::new("/", "."))); + + for range in ["", "bytes="] { + let response = srv + .get("/tests/test.binary") + .insert_header((header::RANGE, range)) + .send() + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); + let content_range = response.headers().get(header::CONTENT_RANGE).unwrap(); + assert_eq!(content_range.to_str().unwrap(), "bytes */100"); + } + } + #[actix_rt::test] async fn test_named_file_content_range_headers() { let srv = actix_test::start(|| App::new().service(Files::new("/", "."))); diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 7ff66e74d..3588ae8bc 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -550,9 +550,12 @@ impl NamedFile { // check for range header if let Some(ranges) = req.headers().get(header::RANGE) { if let Ok(ranges_header) = ranges.to_str() { - if let Ok(ranges) = HttpRange::parse(ranges_header, length) { - length = ranges[0].length; - offset = ranges[0].start; + if let Some(range) = HttpRange::parse(ranges_header, length) + .ok() + .and_then(|ranges| ranges.first().copied()) + { + length = range.length; + offset = range.start; // When a Content-Encoding header is present in a 206 partial content response // for video content, it prevents browser video players from starting playback From bc27fd2724fb507292f38de556ce38e94efb10b1 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Fri, 6 Feb 2026 21:12:50 +0900 Subject: [PATCH 24/32] chore(files): prepare v0.6.10 release --- Cargo.lock | 2 +- actix-files/CHANGES.md | 13 +++++++++++++ actix-files/Cargo.toml | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0dbf5db9d..04f0cbd72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,7 +44,7 @@ dependencies = [ [[package]] name = "actix-files" -version = "0.6.9" +version = "0.6.10" dependencies = [ "actix-http", "actix-rt", diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 307f5852d..281e05312 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -2,6 +2,19 @@ ## Unreleased +## 0.6.10 + +### Security Notice + +We addressed 2 vulnerabilities in this release: + +- Do not panic with empty Range header. +- Avoid serving CWD on invalid `Files::new` inputs. + +We encourage updating your `actix-files` version as soon as possible. + +### Other changes + - Minimum supported Rust version (MSRV) is now 1.88. - `PathBufWrap` & `UriSegmentError` made public. [#3694] diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 148442d4f..12f99708e 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.9" +version = "0.6.10" authors = ["Nikolay Kim ", "Rob Ede "] description = "Static file serving for Actix Web" keywords = ["actix", "http", "async", "futures"] From 80d7d9c01a63d787ca9d027d752566161b69ca26 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sun, 8 Feb 2026 10:30:19 +0900 Subject: [PATCH 25/32] chore(awc): address clippy warnings (#3909) --- awc/src/request.rs | 10 +++------- awc/src/ws.rs | 10 +++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/awc/src/request.rs b/awc/src/request.rs index 71ea2ef1e..02ff4ef57 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -263,13 +263,9 @@ impl ClientRequest { /// ``` #[cfg(feature = "cookies")] pub fn cookie(mut self, cookie: Cookie<'_>) -> Self { - if self.cookies.is_none() { - let mut jar = CookieJar::new(); - jar.add(cookie.into_owned()); - self.cookies = Some(jar) - } else { - self.cookies.as_mut().unwrap().add(cookie.into_owned()); - } + self.cookies + .get_or_insert_with(CookieJar::new) + .add(cookie.into_owned()); self } diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 3ce1d286a..c33531d41 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -125,13 +125,9 @@ impl WebsocketsRequest { /// Set a cookie #[cfg(feature = "cookies")] pub fn cookie(mut self, cookie: Cookie<'_>) -> Self { - if self.cookies.is_none() { - let mut jar = CookieJar::new(); - jar.add(cookie.into_owned()); - self.cookies = Some(jar) - } else { - self.cookies.as_mut().unwrap().add(cookie.into_owned()); - } + self.cookies + .get_or_insert_with(CookieJar::new) + .add(cookie.into_owned()); self } From 9856a3b056adc7c3747a2a41c95ec4599a63da67 Mon Sep 17 00:00:00 2001 From: Anton Lazarev <22821309+antonok-edm@users.noreply.github.com> Date: Sat, 7 Feb 2026 18:15:38 -0800 Subject: [PATCH 26/32] Support serving pre-compressed files for static sites (#2615) * support serving pre-compressed files for static sites * Update CHANGES.md * fix behavior change for audio file * follow-up some inconsistency * test(files): make encoding test independent of fixture line endings --------- Co-authored-by: Rob Ede Co-authored-by: Yuki Okushi --- actix-files/CHANGES.md | 4 + actix-files/src/files.rs | 13 ++++ actix-files/src/named.rs | 107 +++++++++++++++----------- actix-files/src/service.rs | 141 +++++++++++++++++++++++++++++++++- actix-files/tests/encoding.rs | 130 +++++++++++++++++++++++++++++++ actix-files/tests/utf8.txt.br | Bin 0 -> 49 bytes actix-files/tests/utf8.txt.gz | Bin 0 -> 76 bytes 7 files changed, 346 insertions(+), 49 deletions(-) create mode 100644 actix-files/tests/utf8.txt.br create mode 100644 actix-files/tests/utf8.txt.gz diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 281e05312..3cc5afb53 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -2,6 +2,10 @@ ## Unreleased +- Add `Files::try_compressed()` to support serving pre-compressed static files [#2615] + +[#2615]: https://github.com/actix/actix-web/pull/2615 + ## 0.6.10 ### Security Notice diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index a5e22a6d0..7440a43e7 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -50,6 +50,7 @@ pub struct Files { use_guards: Option>, guards: Vec>, hidden_files: bool, + try_compressed: bool, read_mode_threshold: u64, } @@ -76,6 +77,7 @@ impl Clone for Files { use_guards: self.use_guards.clone(), guards: self.guards.clone(), hidden_files: self.hidden_files, + try_compressed: self.try_compressed, read_mode_threshold: self.read_mode_threshold, } } @@ -128,6 +130,7 @@ impl Files { use_guards: None, guards: Vec::new(), hidden_files: false, + try_compressed: false, read_mode_threshold: 0, } } @@ -351,6 +354,15 @@ impl Files { self.hidden_files = true; self } + + /// Attempts to search for a suitable pre-compressed version of a file on disk before falling + /// back to the uncompressed version. + /// + /// Currently, `.gz`, `.br`, and `.zst` files are supported. + pub fn try_compressed(mut self) -> Self { + self.try_compressed = true; + self + } } impl HttpServiceFactory for Files { @@ -402,6 +414,7 @@ impl ServiceFactory for Files { file_flags: self.file_flags, guards: self.use_guards.clone(), hidden_files: self.hidden_files, + try_compressed: self.try_compressed, size_threshold: self.read_mode_threshold, with_permanent_redirect: self.with_permanent_redirect, }; diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 3588ae8bc..77ae53d1c 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -91,6 +91,55 @@ pub(crate) use tokio_uring::fs::File; use super::chunked; +pub(crate) fn get_content_type_and_disposition( + path: &Path, +) -> Result<(mime::Mime, ContentDisposition), io::Error> { + let filename = match path.file_name() { + Some(name) => name.to_string_lossy(), + None => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Provided path has no filename", + )); + } + }; + + let ct = mime_guess::from_path(path).first_or_octet_stream(); + + let disposition = match ct.type_() { + mime::IMAGE | mime::TEXT | mime::AUDIO | mime::VIDEO => DispositionType::Inline, + mime::APPLICATION => match ct.subtype() { + mime::JAVASCRIPT | mime::JSON => DispositionType::Inline, + name if name == "wasm" || name == "xhtml" => DispositionType::Inline, + _ => DispositionType::Attachment, + }, + _ => DispositionType::Attachment, + }; + + // replace special characters in filenames which could occur on some filesystems + let filename_s = filename + .replace('\n', "%0A") // \n line break + .replace('\x0B', "%0B") // \v vertical tab + .replace('\x0C', "%0C") // \f form feed + .replace('\r', "%0D"); // \r carriage return + let mut parameters = vec![DispositionParam::Filename(filename_s)]; + + if !filename.is_ascii() { + parameters.push(DispositionParam::FilenameExt(ExtendedValue { + charset: Charset::Ext(String::from("UTF-8")), + language_tag: None, + value: filename.into_owned().into_bytes(), + })) + } + + let cd = ContentDisposition { + disposition, + parameters, + }; + + Ok((ct, cd)) +} + impl NamedFile { /// Creates an instance from a previously opened file. /// @@ -117,52 +166,7 @@ impl NamedFile { // Get the name of the file and use it to construct default Content-Type // and Content-Disposition values - let (content_type, content_disposition) = { - let filename = match path.file_name() { - Some(name) => name.to_string_lossy(), - None => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Provided path has no filename", - )); - } - }; - - let ct = mime_guess::from_path(&path).first_or_octet_stream(); - - let disposition = match ct.type_() { - mime::IMAGE | mime::TEXT | mime::AUDIO | mime::VIDEO => DispositionType::Inline, - mime::APPLICATION => match ct.subtype() { - mime::JAVASCRIPT | mime::JSON => DispositionType::Inline, - name if name == "wasm" || name == "xhtml" => DispositionType::Inline, - _ => DispositionType::Attachment, - }, - _ => DispositionType::Attachment, - }; - - // replace special characters in filenames which could occur on some filesystems - let filename_s = filename - .replace('\n', "%0A") // \n line break - .replace('\x0B', "%0B") // \v vertical tab - .replace('\x0C', "%0C") // \f form feed - .replace('\r', "%0D"); // \r carriage return - let mut parameters = vec![DispositionParam::Filename(filename_s)]; - - if !filename.is_ascii() { - parameters.push(DispositionParam::FilenameExt(ExtendedValue { - charset: Charset::Ext(String::from("UTF-8")), - language_tag: None, - value: filename.into_owned().into_bytes(), - })) - } - - let cd = ContentDisposition { - disposition, - parameters, - }; - - (ct, cd) - }; + let (content_type, content_disposition) = get_content_type_and_disposition(&path)?; let md = { #[cfg(not(feature = "experimental-io-uring"))] @@ -710,3 +714,14 @@ impl HttpServiceFactory for NamedFile { ) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn audio_files_use_inline_content_disposition() { + let (_ct, cd) = get_content_type_and_disposition(Path::new("sound.mp3")).unwrap(); + assert_eq!(cd.disposition, DispositionType::Inline); + } +} diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index f63ba46c6..ae6725385 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -1,4 +1,9 @@ -use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc}; +use std::{ + fmt, io, + ops::Deref, + path::{Path, PathBuf}, + rc::Rc, +}; use actix_web::{ body::BoxBody, @@ -39,6 +44,7 @@ pub struct FilesServiceInner { pub(crate) file_flags: named::Flags, pub(crate) guards: Option>, pub(crate) hidden_files: bool, + pub(crate) try_compressed: bool, pub(crate) size_threshold: u64, pub(crate) with_permanent_redirect: bool, } @@ -64,7 +70,12 @@ impl FilesService { } } - fn serve_named_file(&self, req: ServiceRequest, mut named_file: NamedFile) -> ServiceResponse { + fn serve_named_file_with_encoding( + &self, + req: ServiceRequest, + mut named_file: NamedFile, + encoding: header::ContentEncoding, + ) -> ServiceResponse { if let Some(ref mime_override) = self.mime_override { let new_disposition = mime_override(&named_file.content_type.type_()); named_file.content_disposition.disposition = new_disposition; @@ -72,12 +83,36 @@ impl FilesService { named_file.flags = self.file_flags; let (req, _) = req.into_parts(); - let res = named_file + let mut res = named_file .read_mode_threshold(self.size_threshold) .into_response(&req); + + let header_value = match encoding { + header::ContentEncoding::Brotli => Some("br"), + header::ContentEncoding::Gzip => Some("gzip"), + header::ContentEncoding::Zstd => Some("zstd"), + header::ContentEncoding::Identity => None, + // Only variants in SUPPORTED_PRECOMPRESSION_ENCODINGS can occur here + _ => unreachable!(), + }; + if let Some(header_value) = header_value { + res.headers_mut().insert( + header::CONTENT_ENCODING, + header::HeaderValue::from_static(header_value), + ); + // Response representation varies by Accept-Encoding when serving pre-compressed assets. + res.headers_mut().append( + header::VARY, + header::HeaderValue::from_static("accept-encoding"), + ); + } ServiceResponse::new(req, res) } + fn serve_named_file(&self, req: ServiceRequest, named_file: NamedFile) -> ServiceResponse { + self.serve_named_file_with_encoding(req, named_file, header::ContentEncoding::Identity) + } + fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse { let dir = Directory::new(self.directory.clone(), path); @@ -138,6 +173,15 @@ impl Service for FilesService { // full file path let path = this.directory.join(&path_on_disk); + + // Try serving pre-compressed file even if the uncompressed file doesn't exist yet. + // Still handle directories (index/listing) through the normal branch below. + if this.try_compressed && !path.is_dir() { + if let Some((named_file, encoding)) = find_compressed(&req, &path).await { + return Ok(this.serve_named_file_with_encoding(req, named_file, encoding)); + } + } + if let Err(err) = path.canonicalize() { return this.handle_err(err, req).await; } @@ -163,6 +207,16 @@ impl Service for FilesService { match this.index { Some(ref index) => { let named_path = path.join(index); + if this.try_compressed { + if let Some((named_file, encoding)) = + find_compressed(&req, &named_path).await + { + return Ok( + this.serve_named_file_with_encoding(req, named_file, encoding) + ); + } + } + // fallback to the uncompressed version match NamedFile::open_async(named_path).await { Ok(named_file) => Ok(this.serve_named_file(req, named_file)), Err(_) if this.show_index => Ok(this.show_index(req, path)), @@ -184,3 +238,84 @@ impl Service for FilesService { }) } } + +/// Flate doesn't have an accepted file extension, so it is not included here. +const SUPPORTED_PRECOMPRESSION_ENCODINGS: &[header::ContentEncoding] = &[ + header::ContentEncoding::Brotli, + header::ContentEncoding::Gzip, + header::ContentEncoding::Zstd, + header::ContentEncoding::Identity, +]; + +/// Searches disk for an acceptable alternate encoding of the content at the given path, as +/// preferred by the request's `Accept-Encoding` header. Returns the corresponding `NamedFile` with +/// the most appropriate supported encoding, if any exist. +async fn find_compressed( + req: &ServiceRequest, + original_path: &Path, +) -> Option<(NamedFile, header::ContentEncoding)> { + use actix_web::HttpMessage; + use header::{AcceptEncoding, ContentEncoding, Encoding}; + + // Retrieve the content type and content disposition based on the original filename. If we + // can't get these successfully, don't even try to find a compressed file. + let (content_type, content_disposition) = + match crate::named::get_content_type_and_disposition(original_path) { + Ok(values) => values, + Err(_) => return None, + }; + + let accept_encoding = req.get_header::()?; + + let mut supported = SUPPORTED_PRECOMPRESSION_ENCODINGS + .iter() + .copied() + .map(Encoding::Known) + .collect::>(); + + // Only move the original content-type/disposition into the chosen compressed file once. + let mut content_type = Some(content_type); + let mut content_disposition = Some(content_disposition); + + loop { + // Select next acceptable encoding (honouring q=0 rejections) from remaining supported set. + let chosen = accept_encoding.negotiate(supported.iter())?; + + let encoding = match chosen { + Encoding::Known(enc) => enc, + // No supported encoding should ever be unknown here. + Encoding::Unknown(_) => return None, + }; + + // Identity indicates there is no acceptable pre-compressed representation. + if encoding == ContentEncoding::Identity { + return None; + } + + let extension = match encoding { + ContentEncoding::Brotli => ".br", + ContentEncoding::Gzip => ".gz", + ContentEncoding::Zstd => ".zst", + ContentEncoding::Identity => unreachable!(), + // Only variants in SUPPORTED_PRECOMPRESSION_ENCODINGS can occur here. + _ => unreachable!(), + }; + + let mut compressed_path = original_path.to_owned(); + let mut filename = compressed_path.file_name()?.to_owned(); + filename.push(extension); + compressed_path.set_file_name(filename); + + match NamedFile::open_async(&compressed_path).await { + Ok(mut named_file) => { + named_file.content_type = content_type.take().unwrap(); + named_file.content_disposition = content_disposition.take().unwrap(); + return Some((named_file, encoding)); + } + // Ignore errors while searching disk for a suitable encoding. + Err(_) => { + supported.retain(|enc| enc != &chosen); + } + } + } +} diff --git a/actix-files/tests/encoding.rs b/actix-files/tests/encoding.rs index 3c8bdb59b..019abfb57 100644 --- a/actix-files/tests/encoding.rs +++ b/actix-files/tests/encoding.rs @@ -36,6 +36,136 @@ async fn test_utf8_file_contents() { ); } +#[actix_web::test] +async fn test_compression_encodings() { + use actix_web::body::MessageBody; + + let utf8_txt_len = std::fs::metadata("./tests/utf8.txt").unwrap().len(); + let utf8_txt_br_len = std::fs::metadata("./tests/utf8.txt.br").unwrap().len(); + let utf8_txt_gz_len = std::fs::metadata("./tests/utf8.txt.gz").unwrap().len(); + + let srv = + test::init_service(App::new().service(Files::new("/", "./tests").try_compressed())).await; + + // Select the requested encoding when present + let mut req = TestRequest::with_uri("/utf8.txt").to_request(); + req.headers_mut().insert( + header::ACCEPT_ENCODING, + header::HeaderValue::from_static("gzip"), + ); + let res = test::call_service(&srv, req).await; + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(header::CONTENT_TYPE), + Some(&HeaderValue::from_static("text/plain; charset=utf-8")), + ); + assert_eq!( + res.headers().get(header::CONTENT_ENCODING), + Some(&HeaderValue::from_static("gzip")), + ); + assert_eq!( + res.headers().get(header::VARY), + Some(&HeaderValue::from_static("accept-encoding")), + ); + assert_eq!( + res.into_body().size(), + actix_web::body::BodySize::Sized(utf8_txt_gz_len), + ); + + // Select the highest priority encoding + let mut req = TestRequest::with_uri("/utf8.txt").to_request(); + req.headers_mut().insert( + header::ACCEPT_ENCODING, + header::HeaderValue::from_static("gzip;q=0.6,br;q=0.8,*"), + ); + let res = test::call_service(&srv, req).await; + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(header::CONTENT_TYPE), + Some(&HeaderValue::from_static("text/plain; charset=utf-8")), + ); + assert_eq!( + res.headers().get(header::CONTENT_ENCODING), + Some(&HeaderValue::from_static("br")), + ); + assert_eq!( + res.headers().get(header::VARY), + Some(&HeaderValue::from_static("accept-encoding")), + ); + assert_eq!( + res.into_body().size(), + actix_web::body::BodySize::Sized(utf8_txt_br_len), + ); + + // Request encoding that doesn't exist on disk and fallback to no encoding + let mut req = TestRequest::with_uri("/utf8.txt").to_request(); + req.headers_mut().insert( + header::ACCEPT_ENCODING, + header::HeaderValue::from_static("zstd"), + ); + let res = test::call_service(&srv, req).await; + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(header::CONTENT_TYPE), + Some(&HeaderValue::from_static("text/plain; charset=utf-8")), + ); + assert_eq!(res.headers().get(header::CONTENT_ENCODING), None,); + assert_eq!( + res.into_body().size(), + actix_web::body::BodySize::Sized(utf8_txt_len), + ); + + // Do not select an encoding explicitly refused via q=0 + let mut req = TestRequest::with_uri("/utf8.txt").to_request(); + req.headers_mut().insert( + header::ACCEPT_ENCODING, + header::HeaderValue::from_static("zstd;q=1, gzip;q=0"), + ); + let res = test::call_service(&srv, req).await; + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(header::CONTENT_TYPE), + Some(&HeaderValue::from_static("text/plain; charset=utf-8")), + ); + assert_eq!(res.headers().get(header::CONTENT_ENCODING), None,); + assert_eq!( + res.into_body().size(), + actix_web::body::BodySize::Sized(utf8_txt_len), + ); + + // Can still request a compressed file directly + let req = TestRequest::with_uri("/utf8.txt.gz").to_request(); + let res = test::call_service(&srv, req).await; + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(header::CONTENT_TYPE), + Some(&HeaderValue::from_static("application/gzip")), + ); + assert_eq!(res.headers().get(header::CONTENT_ENCODING), None,); + + // Don't try compressed files + let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await; + + let mut req = TestRequest::with_uri("/utf8.txt").to_request(); + req.headers_mut().insert( + header::ACCEPT_ENCODING, + header::HeaderValue::from_static("gzip"), + ); + let res = test::call_service(&srv, req).await; + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(header::CONTENT_TYPE), + Some(&HeaderValue::from_static("text/plain; charset=utf-8")), + ); + assert_eq!(res.headers().get(header::CONTENT_ENCODING), None); +} + #[actix_web::test] async fn partial_range_response_encoding() { let srv = test::init_service(App::new().default_service(web::to(|| async { diff --git a/actix-files/tests/utf8.txt.br b/actix-files/tests/utf8.txt.br new file mode 100644 index 0000000000000000000000000000000000000000..c06efd6c97eb60626b2fbba25c908057bd066131 GIT binary patch literal 49 zcmV-10M7p*tN;Y$xUJ@vhvkNa<*vErn7-$vy5_B;=b^6SfPxANMQ&$oX>({GX>%Y? HOD+ln$5I&k literal 0 HcmV?d00001 diff --git a/actix-files/tests/utf8.txt.gz b/actix-files/tests/utf8.txt.gz new file mode 100644 index 0000000000000000000000000000000000000000..3fbf022644d7893f40b9262fe2eabec30662ce50 GIT binary patch literal 76 zcmV-S0JHxeiwFovi1T3p19fy}I4*Q}bN~S?0Mq~ExUJ@vhvkNa<*vErn7-$vy5_B; i=b^6SfPxANMQ&$oX>({GX>%Y?OD+nJ%#b830000{wIWvl literal 0 HcmV?d00001 From 41e4863748ac0c8e446d96528fa1c31b2b20aae1 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sun, 8 Feb 2026 16:03:04 +0900 Subject: [PATCH 27/32] fix(awc): do not request as chunked if body is empty (#3910) --- awc/CHANGES.md | 1 + awc/src/client/h1proto.rs | 46 ++++++++++++++--- awc/tests/test_empty_stream.rs | 91 ++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 awc/tests/test_empty_stream.rs diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 7d09212c2..35a3dde2f 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -3,6 +3,7 @@ ## Unreleased - Minimum supported Rust version (MSRV) is now 1.88. +- Fix empty streaming request bodies being sent with chunked transfer encoding. ## 3.8.1 diff --git a/awc/src/client/h1proto.rs b/awc/src/client/h1proto.rs index 3f4c9f979..3d8d8db08 100644 --- a/awc/src/client/h1proto.rs +++ b/awc/src/client/h1proto.rs @@ -34,6 +34,35 @@ where B: MessageBody, B::Error: Into, { + actix_rt::pin!(body); + + let orig_length = body.size(); + let mut length = orig_length; + let mut first_chunk = None; + + // This avoids sending `Transfer-Encoding: chunked` for requests with an empty body stream. + // https://github.com/actix/actix-web/issues/2320 + if matches!(orig_length, BodySize::Stream) { + enum Peek { + Pending, + Item(Result), + Eof, + } + + match poll_fn(|cx| match body.as_mut().poll_next(cx) { + Poll::Pending => Poll::Ready(Peek::Pending), + Poll::Ready(Some(res)) => Poll::Ready(Peek::Item(res)), + Poll::Ready(None) => Poll::Ready(Peek::Eof), + }) + .await + { + Peek::Pending => {} + Peek::Eof => length = BodySize::Sized(0), + Peek::Item(Ok(chunk)) => first_chunk = Some(chunk), + Peek::Item(Err(err)) => return Err(SendRequestError::Body(err.into())), + } + } + // set request host header if !head.as_ref().headers.contains_key(HOST) && !head.extra_headers().iter().any(|h| h.contains_key(HOST)) @@ -67,7 +96,7 @@ where // Check EXPECT header and enable expect handle flag accordingly. // See https://datatracker.ietf.org/doc/html/rfc7231#section-5.1.1 let is_expect = if head.as_ref().headers.contains_key(EXPECT) { - match body.size() { + match orig_length { BodySize::None | BodySize::Sized(0) => { let keep_alive = framed.codec_ref().keep_alive(); framed.io_mut().on_release(keep_alive); @@ -86,7 +115,7 @@ where // special handle for EXPECT request. let (do_send, mut res_head) = if is_expect { - pin_framed.send((head, body.size()).into()).await?; + pin_framed.send((head, length).into()).await?; let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx)) .await @@ -96,18 +125,18 @@ where // and current head would be used as final response head. (head.status == StatusCode::CONTINUE, Some(head)) } else { - pin_framed.feed((head, body.size()).into()).await?; + pin_framed.feed((head, length).into()).await?; (true, None) }; if do_send { // send request body - match body.size() { + match length { BodySize::None | BodySize::Sized(0) => { poll_fn(|cx| pin_framed.as_mut().flush(cx)).await?; } - _ => send_body(body, pin_framed.as_mut()).await?, + _ => send_body(body.as_mut(), pin_framed.as_mut(), first_chunk).await?, }; // read response and init read body @@ -157,15 +186,18 @@ where /// send request body to the peer pub(crate) async fn send_body( - body: B, + mut body: Pin<&mut B>, mut framed: Pin<&mut Framed>, + first_chunk: Option, ) -> Result<(), SendRequestError> where Io: ConnectionIo, B: MessageBody, B::Error: Into, { - actix_rt::pin!(body); + if let Some(chunk) = first_chunk { + framed.as_mut().write(h1::Message::Chunk(Some(chunk)))?; + } let mut eof = false; while !eof { diff --git a/awc/tests/test_empty_stream.rs b/awc/tests/test_empty_stream.rs new file mode 100644 index 000000000..76f6337ca --- /dev/null +++ b/awc/tests/test_empty_stream.rs @@ -0,0 +1,91 @@ +use std::{convert::Infallible, time::Duration}; + +use actix_rt::net::TcpListener; +use awc::Client; +use bytes::Bytes; +use futures_util::stream; +use tokio::{ + io::{AsyncReadExt as _, AsyncWriteExt as _}, + time::timeout, +}; + +#[actix_rt::test] +async fn empty_body_stream_does_not_use_chunked_encoding() { + let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap(); + let addr = listener.local_addr().unwrap(); + + // Minimal HTTP/1.1 server that rejects chunked requests. + let srv = actix_rt::spawn(async move { + let (mut sock, _) = listener.accept().await.unwrap(); + + let mut buf = Vec::with_capacity(1024); + let mut tmp = [0u8; 1024]; + + let header_end = loop { + let n = timeout(Duration::from_secs(2), sock.read(&mut tmp)) + .await + .unwrap() + .unwrap(); + if n == 0 { + break None; + } + + buf.extend_from_slice(&tmp[..n]); + + if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") { + break Some(pos + 4); + } + + if buf.len() > 16 * 1024 { + break None; + } + } + .expect("did not receive complete request headers"); + + let headers_lower = String::from_utf8_lossy(&buf[..header_end]).to_ascii_lowercase(); + let has_chunked = headers_lower.contains("\r\ntransfer-encoding: chunked\r\n"); + + if has_chunked { + // Drain terminating chunk so client doesn't error on write before response is read. + let terminator = b"0\r\n\r\n"; + while !buf[header_end..] + .windows(terminator.len()) + .any(|w| w == terminator) + { + let n = match timeout(Duration::from_secs(2), sock.read(&mut tmp)).await { + Ok(Ok(n)) => n, + _ => break, + }; + + if n == 0 { + break; + } + + buf.extend_from_slice(&tmp[..n]); + + if buf.len() > 32 * 1024 { + break; + } + } + } + + let status = if has_chunked { + "400 Bad Request" + } else { + "200 OK" + }; + let resp = format!("HTTP/1.1 {status}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); + sock.write_all(resp.as_bytes()).await.unwrap(); + }); + + let url = format!("http://{addr}/"); + let res = Client::default() + .get(url) + .send_stream(stream::empty::>()) + .await + .unwrap(); + + assert!(res.status().is_success()); + + srv.await.unwrap(); +} From b2523fb1cc8d9de8cbd73a2577c8df68d6223a74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:32:59 +0900 Subject: [PATCH 28/32] build(deps): bump taiki-e/install-action from 2.67.18 to 2.67.25 (#3912) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.67.18 to 2.67.25. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/650c5ca14212efbbf3e580844b04bdccf68dac31...f176c07a0a40cbfdd08ee9aa8bf1655701d11e69) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.67.25 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-post-merge.yml | 4 ++-- .github/workflows/ci.yml | 4 ++-- .github/workflows/coverage.yml | 2 +- .github/workflows/lint.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-post-merge.yml b/.github/workflows/ci-post-merge.yml index acf0e6135..cfdb53daa 100644 --- a/.github/workflows/ci-post-merge.yml +++ b/.github/workflows/ci-post-merge.yml @@ -49,7 +49,7 @@ jobs: toolchain: ${{ matrix.version.version }} - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 + uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25 with: tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean @@ -83,7 +83,7 @@ jobs: uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2 - name: Install just, cargo-hack - uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 + uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25 with: tool: just,cargo-hack diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db82b6e36..2f8f0695c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: toolchain: ${{ matrix.version.version }} - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 + uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25 with: tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean @@ -117,7 +117,7 @@ jobs: toolchain: nightly - name: Install just - uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 + uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25 with: tool: just diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 6fba6ad08..e5759292b 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -24,7 +24,7 @@ jobs: components: llvm-tools - name: Install just, cargo-llvm-cov, cargo-nextest - uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 + uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25 with: tool: just,cargo-llvm-cov,cargo-nextest diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a630b9ba8..85f4434a0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -77,7 +77,7 @@ jobs: toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }} - name: Install just - uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2.67.18 + uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25 with: tool: just From 747d7c0def41c1ad564dc85f183fe874df9aa52c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:58:49 +0000 Subject: [PATCH 29/32] build(deps): bump memchr from 2.7.6 to 2.8.0 (#3913) Bumps [memchr](https://github.com/BurntSushi/memchr) from 2.7.6 to 2.8.0. - [Commits](https://github.com/BurntSushi/memchr/compare/2.7.6...2.8.0) --- updated-dependencies: - dependency-name: memchr dependency-version: 2.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yuki Okushi --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04f0cbd72..aa289d720 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1903,9 +1903,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mime" From 32cb3b8361356e75b0b043798cc46efb0d9a85bf Mon Sep 17 00:00:00 2001 From: Filip Gregor <44952616+gregofi@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:53:40 +0100 Subject: [PATCH 30/32] feat: ignore unparsable cookies in Cookie header (#3814) fix: ignore unparsable cookies in Cookie header Co-authored-by: Rob Ede --- actix-web/CHANGES.md | 1 + actix-web/src/request.rs | 25 ++++++++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index 47624967d..173d08224 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -4,6 +4,7 @@ - Minimum supported Rust version (MSRV) is now 1.88. - Add `HttpRequest::url_for_map` and `HttpRequest::url_for_iter` methods for named URL parameters. [#3895] +- Ignore unparsable cookies in `Cookie` request header. [#3895]: https://github.com/actix/actix-web/pull/3895 diff --git a/actix-web/src/request.rs b/actix-web/src/request.rs index 3a3e539bf..90a437928 100644 --- a/actix-web/src/request.rs +++ b/actix-web/src/request.rs @@ -414,6 +414,9 @@ impl HttpRequest { } /// Load request cookies. + /// + /// Any cookie that cannot be parsed is omitted from the result. + /// This includes cookies with an empty name (e.g. `document.cookie = "=value"`). #[cfg(feature = "cookies")] pub fn cookies(&self) -> Result>>, CookieParseError> { use actix_http::header::COOKIE; @@ -422,9 +425,9 @@ impl HttpRequest { let mut cookies = Vec::new(); for hdr in self.headers().get_all(COOKIE) { let s = str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; - for cookie_str in s.split(';').map(|s| s.trim()) { - if !cookie_str.is_empty() { - cookies.push(Cookie::parse_encoded(cookie_str)?.into_owned()); + for cookie_str in s.split(';').map(|s| s.trim()).filter(|s| !s.is_empty()) { + if let Ok(cookie) = Cookie::parse_encoded(cookie_str) { + cookies.push(cookie.into_owned()); } } } @@ -677,6 +680,22 @@ mod tests { assert!(cookie.is_none()); } + #[test] + #[cfg(feature = "cookies")] + fn test_empty_key() { + let req = TestRequest::default() + .append_header((header::COOKIE, "cookie1=value1; value2; cookie3=value3")) + .to_http_request(); + { + let cookies = req.cookies().unwrap(); + assert_eq!(cookies.len(), 2); + assert_eq!(cookies[0].name(), "cookie1"); + assert_eq!(cookies[0].value(), "value1"); + assert_eq!(cookies[1].name(), "cookie3"); + assert_eq!(cookies[1].value(), "value3"); + } + } + #[test] fn test_request_query() { let req = TestRequest::with_uri("/?id=test").to_http_request(); From 5548fadc7d5d14151658be5630d449045e0bc368 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Tue, 10 Feb 2026 06:40:29 +0900 Subject: [PATCH 31/32] fix(files): handle `bytes=0-` nicely (#3914) --- actix-files/CHANGES.md | 1 + actix-files/src/lib.rs | 24 ++++++++++++++++++++++++ actix-files/src/named.rs | 4 +++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 3cc5afb53..ef0b79ae4 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -3,6 +3,7 @@ ## Unreleased - Add `Files::try_compressed()` to support serving pre-compressed static files [#2615] +- Fix handling of `bytes=0-` [#2615]: https://github.com/actix/actix-web/pull/2615 diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 5ea578c0d..bf5397ecf 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -513,6 +513,30 @@ mod tests { assert_eq!(content_range.to_str().unwrap(), "bytes */100"); } + #[actix_rt::test] + async fn test_named_file_range_header_from_zero_to_end_returns_partial_content() { + let srv = actix_test::start(|| App::new().service(Files::new("/", "."))); + + let response = srv + .get("/tests/test.binary") + .insert_header((header::RANGE, "bytes=0-")) + .send() + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + + let content_range = response.headers().get(header::CONTENT_RANGE).unwrap(); + assert_eq!(content_range.to_str().unwrap(), "bytes 0-99/100"); + + let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap(); + assert_eq!(content_length.to_str().unwrap(), "100"); + + // Should be no transfer-encoding + let transfer_encoding = response.headers().get(header::TRANSFER_ENCODING); + assert!(transfer_encoding.is_none()); + } + #[actix_rt::test] async fn test_named_file_content_length_headers() { let srv = actix_test::start(|| App::new().service(Files::new("/", "."))); diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 77ae53d1c..1a3c2b3a1 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -550,6 +550,7 @@ impl NamedFile { let mut length = self.md.len(); let mut offset = 0; + let mut ranged_req = false; // check for range header if let Some(ranges) = req.headers().get(header::RANGE) { @@ -558,6 +559,7 @@ impl NamedFile { .ok() .and_then(|ranges| ranges.first().copied()) { + ranged_req = true; length = range.length; offset = range.start; @@ -606,7 +608,7 @@ impl NamedFile { let reader = chunked::new_chunked_read(length, offset, self.file, self.read_mode_threshold); - if offset != 0 || length != self.md.len() { + if ranged_req { res.status(StatusCode::PARTIAL_CONTENT); } From 4f0912d1c730b167bc45872090326d93ffc9fd24 Mon Sep 17 00:00:00 2001 From: nitn3lav <77448526+nitn3lav@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:53:23 +0100 Subject: [PATCH 32/32] PathDeserializer: use `deserialize_str` for `deserialize_any` (#2881) * PathDeserializer: use `deserialize_str` for `deserialize_any` * fix `deserialize_any` for `seq` and `map` * add tests for `deserialize_any` * parse numeric values as well --------- Co-authored-by: Rob Ede Co-authored-by: Yuki Okushi --- actix-router/CHANGES.md | 3 + actix-router/src/de.rs | 154 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 154 insertions(+), 3 deletions(-) diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 781bdefe5..950880dc8 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -3,6 +3,9 @@ ## Unreleased - Minimum supported Rust version (MSRV) is now 1.88. +- Support `deserialize_any` in `PathDeserializer` (enables derived `#[serde(untagged)]` enums in path segments). [#2881] + +[#2881]: https://github.com/actix/actix-web/pull/2881 ## 0.5.3 diff --git a/actix-router/src/de.rs b/actix-router/src/de.rs index 2f50619f8..f06911b34 100644 --- a/actix-router/src/de.rs +++ b/actix-router/src/de.rs @@ -27,6 +27,9 @@ macro_rules! unsupported_type { macro_rules! parse_single_value { ($trait_fn:ident) => { + parse_single_value!($trait_fn, $trait_fn); + }; + ($trait_fn:ident, $visit_fn:ident) => { fn $trait_fn(self, visitor: V) -> Result where V: Visitor<'de>, @@ -43,7 +46,7 @@ macro_rules! parse_single_value { Value { value: &self.path[0], } - .$trait_fn(visitor) + .$visit_fn(visitor) } } }; @@ -205,11 +208,11 @@ impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T> }) } - unsupported_type!(deserialize_any, "'any'"); unsupported_type!(deserialize_option, "Option"); unsupported_type!(deserialize_identifier, "identifier"); unsupported_type!(deserialize_ignored_any, "ignored_any"); + parse_single_value!(deserialize_any); parse_single_value!(deserialize_bool); parse_single_value!(deserialize_i8); parse_single_value!(deserialize_i16); @@ -427,7 +430,39 @@ impl<'de> Deserializer<'de> for Value<'de> { Err(de::value::Error::custom("unsupported type: tuple struct")) } - unsupported_type!(deserialize_any, "any"); + fn deserialize_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + let decoded = FULL_QUOTER + .with(|q| q.requote_str_lossy(self.value)) + .map(Cow::Owned) + .unwrap_or(Cow::Borrowed(self.value)); + + let s = decoded.as_ref(); + // We have to do it manually here on behalf of serde. + if let Ok(v) = s.parse::() { + if let Ok(v) = u32::try_from(v) { + return visitor.visit_u32(v); + } + + return visitor.visit_u64(v); + } + + if let Ok(v) = s.parse::() { + if let Ok(v) = i32::try_from(v) { + return visitor.visit_i32(v); + } + + return visitor.visit_i64(v); + } + + match decoded { + Cow::Borrowed(value) => visitor.visit_borrowed_str(value), + Cow::Owned(value) => visitor.visit_string(value), + } + } + unsupported_type!(deserialize_seq, "seq"); unsupported_type!(deserialize_map, "map"); unsupported_type!(deserialize_identifier, "identifier"); @@ -704,6 +739,119 @@ mod tests { assert_eq!(vals.value, "/"); } + #[test] + fn deserialize_path_decode_any() { + #[derive(Debug, PartialEq)] + pub enum AnyEnumCustom { + String(String), + Int(u32), + Other, + } + + impl<'de> Deserialize<'de> for AnyEnumCustom { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Vis; + impl<'de> Visitor<'de> for Vis { + type Value = AnyEnumCustom; + + fn expecting<'a>(&self, f: &mut std::fmt::Formatter<'a>) -> std::fmt::Result { + write!(f, "my thing") + } + + fn visit_u32(self, v: u32) -> Result + where + E: serde::de::Error, + { + Ok(AnyEnumCustom::Int(v)) + } + + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + match u32::try_from(v) { + Ok(v) => Ok(AnyEnumCustom::Int(v)), + Err(_) => Ok(AnyEnumCustom::String(format!("some str: {v}"))), + } + } + + fn visit_i64(self, v: i64) -> Result + where + E: serde::de::Error, + { + match u32::try_from(v) { + Ok(v) => Ok(AnyEnumCustom::Int(v)), + Err(_) => Ok(AnyEnumCustom::String(format!("some str: {v}"))), + } + } + + fn visit_str(self, v: &str) -> Result { + v.parse().map(AnyEnumCustom::Int).or_else(|_| { + Ok(match v { + "other" => AnyEnumCustom::Other, + _ => AnyEnumCustom::String(format!("some str: {v}")), + }) + }) + } + } + + deserializer.deserialize_any(Vis) + } + } + + #[derive(Debug, Deserialize, PartialEq)] + #[serde(untagged)] + pub enum AnyEnumDerive { + String(String), + Int(u32), + Other, + } + + // single + let rdef = ResourceDef::new("/{key}"); + + let mut path = Path::new("/%25"); + rdef.capture_match_info(&mut path); + let de = PathDeserializer::new(&path); + let segment: AnyEnumCustom = serde::Deserialize::deserialize(de).unwrap(); + assert_eq!(segment, AnyEnumCustom::String("some str: %".to_string())); + + let mut path = Path::new("/%25"); + rdef.capture_match_info(&mut path); + let de = PathDeserializer::new(&path); + let segment: AnyEnumDerive = serde::Deserialize::deserialize(de).unwrap(); + assert_eq!(segment, AnyEnumDerive::String("%".to_string())); + + // seq + let rdef = ResourceDef::new("/{key}/{value}"); + + let mut path = Path::new("/other/123"); + rdef.capture_match_info(&mut path); + let de = PathDeserializer::new(&path); + let segment: (AnyEnumCustom, AnyEnumDerive) = serde::Deserialize::deserialize(de).unwrap(); + assert_eq!(segment.0, AnyEnumCustom::Other); + assert_eq!(segment.1, AnyEnumDerive::Int(123)); + + // map + #[derive(Deserialize)] + struct Vals { + key: AnyEnumCustom, + value: AnyEnumDerive, + } + + let rdef = ResourceDef::new("/{key}/{value}"); + + let mut path = Path::new("/123/%2F"); + rdef.capture_match_info(&mut path); + let de = PathDeserializer::new(&path); + let vals: Vals = serde::Deserialize::deserialize(de).unwrap(); + assert_eq!(vals.key, AnyEnumCustom::Int(123)); + assert_eq!(vals.value, AnyEnumDerive::String("/".to_string())); + } + #[test] fn deserialize_borrowed() { #[derive(Debug, Deserialize)]