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

This commit is contained in:
FAIZAL KHAN 2026-05-08 21:14:28 +05:30 committed by GitHub
commit 7ed55c4351
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 1172 additions and 274 deletions

View File

@ -4,8 +4,12 @@ updates:
directory: /
schedule:
interval: weekly
cooldown:
default-days: 3
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
cooldown:
default-days: 3
versioning-strategy: lockfile-only

View File

@ -17,6 +17,8 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust
run: |

View File

@ -29,6 +29,8 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install nasm
if: matrix.target.os == 'windows-latest'
@ -49,7 +51,7 @@ jobs:
toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18
uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -72,18 +74,20 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Free Disk Space
run: ./scripts/free-disk-space.sh
- name: Setup mold linker
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
- name: Install just, cargo-hack
uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18
uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30
with:
tool: just,cargo-hack

View File

@ -40,6 +40,8 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install nasm
if: matrix.target.os == 'windows-latest'
@ -56,7 +58,7 @@ jobs:
- name: Setup mold linker
if: matrix.target.os == 'ubuntu-latest'
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1
- name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
@ -64,7 +66,7 @@ jobs:
toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18
uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -94,6 +96,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
@ -110,6 +114,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
@ -117,7 +123,7 @@ jobs:
toolchain: nightly
- name: Install just
uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18
uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30
with:
tool: just

View File

@ -16,6 +16,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
@ -24,7 +26,7 @@ jobs:
components: llvm-tools
- name: Install just, cargo-llvm-cov, cargo-nextest
uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18
uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30
with:
tool: just,cargo-llvm-cov,cargo-nextest

View File

@ -13,4 +13,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1

View File

@ -12,10 +12,30 @@ concurrency:
cancel-in-progress: true
jobs:
zizmor:
name: zizmor
permissions:
actions: read
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
with:
advanced-security: false
annotations: true
version: v1.24.1
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
@ -34,6 +54,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
@ -53,6 +75,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
@ -70,6 +94,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }})
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
@ -77,14 +103,16 @@ jobs:
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
- name: Install just
uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18
uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30
with:
tool: just
- name: Install cargo-check-external-types
uses: taiki-e/cache-cargo-install-action@f9eed3e4680f27610dc6d8c67be1b88593f7dade # v3.0.6
uses: taiki-e/cache-cargo-install-action@417450f3c33ee20393705369577571770643d4c7 # v3.0.7
with:
tool: cargo-check-external-types
- name: check external types
run: just check-external-types-all +${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
run: just check-external-types-all +"${RUST_VERSION_EXTERNAL_TYPES}"
env:
RUST_VERSION_EXTERNAL_TYPES: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}

View File

@ -13,6 +13,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
@ -20,7 +21,7 @@ jobs:
toolchain: stable
- name: Install cargo-semver-checks
uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18
uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30
with:
tool: cargo-semver-checks
@ -59,16 +60,22 @@ jobs:
- name: Summarize cargo semver-checks output
if: always() && steps.semver.outcome != 'skipped'
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
SEMVER_TYPE: ${{ steps.semver.outputs.semver_type }}
STATUS: ${{ steps.semver.outputs.exit_code }}
SUMMARY_FILE: ${{ steps.semver.outputs.output_file }}
run: |
summary_file="${{ steps.semver.outputs.output_file }}"
status="${{ steps.semver.outputs.exit_code }}"
summary_file="$SUMMARY_FILE"
status="$STATUS"
{
echo "## cargo semver-checks"
echo
echo "- Base SHA: \`${{ github.event.pull_request.base.sha }}\`"
echo "- Head SHA: \`${{ github.event.pull_request.head.sha }}\`"
echo "- Required release: \`${{ steps.semver.outputs.semver_type }}\`"
echo "- Base SHA: \`${BASE_SHA}\`"
echo "- Head SHA: \`${HEAD_SHA}\`"
echo "- Required release: \`${SEMVER_TYPE}\`"
echo "- cargo semver-checks exit code: \`$status\`"
echo

316
Cargo.lock generated
View File

@ -294,7 +294,7 @@ dependencies = [
"futures-core",
"http 0.2.12",
"http 1.4.0",
"impl-more",
"impl-more 0.1.9",
"openssl",
"pin-project-lite",
"rustls-native-certs",
@ -354,7 +354,7 @@ dependencies = [
"foldhash 0.2.0",
"futures-core",
"futures-util",
"impl-more",
"impl-more 0.3.1",
"itoa",
"language-tags",
"log",
@ -465,6 +465,15 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "alloca"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
dependencies = [
"cc",
]
[[package]]
name = "anes"
version = "0.1.6"
@ -726,9 +735,9 @@ checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3"
[[package]]
name = "bytestring"
version = "1.5.0"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289"
checksum = "86566c496f2f47d9b8147a4c8b02ffdb69c919fe0c2b2e7195d22cbba0e635c9"
dependencies = [
"bytes",
]
@ -846,6 +855,16 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "combine"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
"bytes",
"memchr",
]
[[package]]
name = "common-multipart-rfc7578"
version = "0.7.0"
@ -907,6 +926,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
@ -963,25 +992,24 @@ dependencies = [
[[package]]
name = "criterion"
version = "0.5.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3"
dependencies = [
"alloca",
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"is-terminal",
"itertools",
"num-traits",
"once_cell",
"oorandom",
"page_size",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
@ -989,9 +1017,9 @@ dependencies = [
[[package]]
name = "criterion-plot"
version = "0.5.0"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea"
dependencies = [
"cast",
"itertools",
@ -1074,9 +1102,9 @@ dependencies = [
[[package]]
name = "darling"
version = "0.20.11"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [
"darling_core",
"darling_macro",
@ -1084,11 +1112,10 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.20.11"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
@ -1098,9 +1125,9 @@ dependencies = [
[[package]]
name = "darling_macro"
version = "0.20.11"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
"darling_core",
"quote",
@ -1224,18 +1251,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "enum-as-inner"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "env_filter"
version = "1.0.1"
@ -1435,6 +1450,7 @@ checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
@ -1563,23 +1579,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hickory-proto"
version = "0.25.2"
name = "hickory-net"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502"
checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183"
dependencies = [
"async-trait",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"hickory-proto",
"idna",
"ipnet",
"once_cell",
"rand 0.9.4",
"ring 0.17.14",
"jni",
"rand 0.10.1",
"thiserror 2.0.18",
"tinyvec",
"tokio",
@ -1588,21 +1603,46 @@ dependencies = [
]
[[package]]
name = "hickory-resolver"
version = "0.25.2"
name = "hickory-proto"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a"
checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643"
dependencies = [
"data-encoding",
"idna",
"ipnet",
"jni",
"once_cell",
"prefix-trie",
"rand 0.10.1",
"ring 0.17.14",
"thiserror 2.0.18",
"tinyvec",
"tracing",
"url",
]
[[package]]
name = "hickory-resolver"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c"
dependencies = [
"cfg-if",
"futures-util",
"hickory-net",
"hickory-proto",
"ipconfig",
"ipnet",
"jni",
"moka",
"ndk-context",
"once_cell",
"parking_lot",
"rand 0.9.4",
"rand 0.10.1",
"resolv-conf",
"smallvec",
"system-configuration",
"thiserror 2.0.18",
"tokio",
"tracing",
@ -1795,6 +1835,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
[[package]]
name = "impl-more"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35a84fd5aa25fae5c0f4a33d9cac2ca017fc622cbd089be2229993514990f870"
[[package]]
name = "indexmap"
version = "2.14.0"
@ -1844,16 +1890,8 @@ name = "ipnet"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.61.2",
"serde",
]
[[package]]
@ -1864,9 +1902,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.10.5"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
@ -1901,6 +1939,55 @@ dependencies = [
"syn",
]
[[package]]
name = "jni"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [
"cfg-if",
"combine",
"jni-macros",
"jni-sys",
"log",
"simd_cesu8",
"thiserror 2.0.18",
"walkdir",
"windows-link",
]
[[package]]
name = "jni-macros"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"simd_cesu8",
"syn",
]
[[package]]
name = "jni-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
dependencies = [
"jni-sys-macros",
]
[[package]]
name = "jni-sys-macros"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "jobserver"
version = "0.1.34"
@ -2073,6 +2160,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "ndk-context"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
[[package]]
name = "num-conv"
version = "0.2.1"
@ -2128,15 +2221,14 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl"
version = "0.10.78"
version = "0.10.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222"
checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542"
dependencies = [
"bitflags 2.11.1",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
@ -2160,9 +2252,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-sys"
version = "0.9.114"
version = "0.9.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6"
checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
dependencies = [
"cc",
"libc",
@ -2170,6 +2262,16 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "page_size"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "parking_lot"
version = "0.12.5"
@ -2326,6 +2428,17 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "prefix-trie"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23370be78b7e5bcbb0cab4a02047eb040279a693c78daad04c2c5f1c24a83503"
dependencies = [
"either",
"ipnet",
"num-traits",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
@ -2373,20 +2486,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_chacha",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.5",
]
[[package]]
name = "rand"
version = "0.10.1"
@ -2408,16 +2511,6 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.5",
]
[[package]]
name = "rand_core"
version = "0.6.4"
@ -2427,15 +2520,6 @@ dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rand_core"
version = "0.10.1"
@ -2652,9 +2736,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.14.0"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
dependencies = [
"zeroize",
]
@ -2751,7 +2835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.11.1",
"core-foundation",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
@ -2890,6 +2974,22 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "simd_cesu8"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
dependencies = [
"rustc_version",
"simdutf8",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "slab"
version = "0.4.12"
@ -2990,6 +3090,27 @@ dependencies = [
"syn",
]
[[package]]
name = "system-configuration"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [
"bitflags 2.11.1",
"core-foundation 0.9.4",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "tagptr"
version = "0.2.0"
@ -3434,10 +3555,19 @@ dependencies = [
]
[[package]]
name = "v_htmlescape"
version = "0.15.8"
name = "v_escape-base"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
checksum = "f1212fce830b75af194b578e55b3db9049f2c8c45f58d397fb25602fdb50fb3d"
[[package]]
name = "v_htmlescape"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "befb3d53c9e3ec641417685896cbc8cc5bd264d6a2e190c56aaef1af24740d99"
dependencies = [
"v_escape-base",
]
[[package]]
name = "vcpkg"

View File

@ -2,11 +2,14 @@
## Unreleased
- Add support for passing multiple root directories to `Files::new`. [#3402]
- Add `Files::try_compressed()` to support serving pre-compressed static files [#2615]
- Fix handling of `bytes=0-`
- Fix `NamedFile` panic when serving files with pre-UNIX epoch modification times. [#2748]
- Fix invalid `Content-Encoding: identity` header in `NamedFile` range responses. [#3191]
- Update `v_htmlescape` dependency to `0.17`.
[#3402]: https://github.com/actix/actix-web/issues/3402
[#2615]: https://github.com/actix/actix-web/pull/2615
[#2748]: https://github.com/actix/actix-web/issues/2748
[#3191]: https://github.com/actix/actix-web/issues/3191

View File

@ -32,7 +32,7 @@ mime = "0.3.9"
mime_guess = "2.0.1"
percent-encoding = "2.1"
pin-project-lite = "0.2.7"
v_htmlescape = "0.15.5"
v_htmlescape = "0.17"
# experimental-io-uring
[target.'cfg(target_os = "linux")'.dependencies]

View File

@ -7,7 +7,7 @@ use std::{
use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
use percent_encoding::{utf8_percent_encode, CONTROLS};
use v_htmlescape::escape as escape_html_entity;
use v_htmlescape::escape_fmt;
/// A directory; responds with the generated directory listing.
#[derive(Debug)]
@ -64,7 +64,7 @@ macro_rules! encode_file_url {
/// ```
macro_rules! encode_file_name {
($entry:ident) => {
escape_html_entity(&$entry.file_name().to_string_lossy())
escape_fmt(&$entry.file_name().to_string_lossy())
};
}

View File

@ -1,5 +1,7 @@
use std::{
borrow::Cow,
cell::RefCell,
ffi::{OsStr, OsString},
fmt, io,
path::{Path, PathBuf},
rc::Rc,
@ -37,7 +39,7 @@ use crate::{
/// ```
pub struct Files {
mount_path: String,
directory: PathBuf,
directories: Vec<PathBuf>,
index: Option<String>,
show_index: bool,
redirect_to_slash: bool,
@ -63,7 +65,7 @@ impl fmt::Debug for Files {
impl Clone for Files {
fn clone(&self) -> Self {
Self {
directory: self.directory.clone(),
directories: self.directories.clone(),
index: self.index.clone(),
show_index: self.show_index,
redirect_to_slash: self.redirect_to_slash,
@ -83,6 +85,131 @@ impl Clone for Files {
}
}
/// File serving root directories for [`Files`].
///
/// This type is used by [`Files::new`] to accept either one root directory or an ordered
/// collection of root directories.
#[derive(Debug)]
pub struct FilesDirs(Vec<PathBuf>);
impl FilesDirs {
fn canonicalize(self) -> Vec<PathBuf> {
self.0
.into_iter()
.map(|orig_dir| match orig_dir.canonicalize() {
Ok(canon_dir) => canon_dir,
Err(_) => {
log::error!("Specified path is not a directory: {:?}", orig_dir);
// Preserve original path so requests don't fall back to CWD.
orig_dir
}
})
.collect()
}
}
impl From<&Path> for FilesDirs {
fn from(dir: &Path) -> Self {
Self(vec![dir.into()])
}
}
impl From<&PathBuf> for FilesDirs {
fn from(dir: &PathBuf) -> Self {
Self(vec![dir.into()])
}
}
impl From<PathBuf> for FilesDirs {
fn from(dir: PathBuf) -> Self {
Self(vec![dir])
}
}
impl From<&str> for FilesDirs {
fn from(dir: &str) -> Self {
Self(vec![dir.into()])
}
}
impl From<&String> for FilesDirs {
fn from(dir: &String) -> Self {
Self(vec![dir.into()])
}
}
impl From<String> for FilesDirs {
fn from(dir: String) -> Self {
Self(vec![dir.into()])
}
}
impl From<&OsStr> for FilesDirs {
fn from(dir: &OsStr) -> Self {
Self(vec![dir.into()])
}
}
impl From<OsString> for FilesDirs {
fn from(dir: OsString) -> Self {
Self(vec![dir.into()])
}
}
impl From<&OsString> for FilesDirs {
fn from(dir: &OsString) -> Self {
Self(vec![dir.into()])
}
}
impl From<Box<Path>> for FilesDirs {
fn from(dir: Box<Path>) -> Self {
Self(vec![dir.into()])
}
}
impl From<Cow<'_, Path>> for FilesDirs {
fn from(dir: Cow<'_, Path>) -> Self {
Self(vec![dir.into()])
}
}
impl<P, const N: usize> From<[P; N]> for FilesDirs
where
P: Into<PathBuf>,
{
fn from(dirs: [P; N]) -> Self {
Self(dirs.into_iter().map(Into::into).collect())
}
}
impl<P, const N: usize> From<&[P; N]> for FilesDirs
where
P: Clone + Into<PathBuf>,
{
fn from(dirs: &[P; N]) -> Self {
Self(dirs.iter().cloned().map(Into::into).collect())
}
}
impl<P> From<&[P]> for FilesDirs
where
P: Clone + Into<PathBuf>,
{
fn from(dirs: &[P]) -> Self {
Self(dirs.iter().cloned().map(Into::into).collect())
}
}
impl<P> From<Vec<P>> for FilesDirs
where
P: Into<PathBuf>,
{
fn from(dirs: Vec<P>) -> Self {
Self(dirs.into_iter().map(Into::into).collect())
}
}
impl Files {
/// Create new `Files` instance for a specified base directory.
///
@ -90,34 +217,34 @@ impl Files {
/// The first argument (`mount_path`) is the root URL at which the static files are served.
/// For example, `/assets` will serve files at `example.com/assets/...`.
///
/// The second argument (`serve_from`) is the location on disk at which files are loaded.
/// This can be a relative path. For example, `./` would serve files from the current
/// working directory.
/// The second argument (`serve_from`) is the location on disk that files are served from. This
/// can be a single path or an ordered collection of paths. Relative paths are resolved from the
/// current working directory.
///
/// When multiple directories are provided, they are checked in order. The first directory that
/// can serve the requested path is used.
///
/// Directory listings are generated from the first matching directory and are not merged across
/// roots. When [`Files::index_file()`] is configured, later roots are searched if an earlier
/// matching directory does not contain the index file.
///
/// Empty root collections never match files; requests fall through to the default handler, or
/// return `404 Not Found` if none is configured.
///
/// # Implementation Notes
/// 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.
/// If a `serve_from` path 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.
pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
let orig_dir = serve_from.into();
let dir = match orig_dir.canonicalize() {
Ok(canon_dir) => canon_dir,
Err(_) => {
log::error!("Specified path is not a directory: {:?}", orig_dir);
// Preserve original path so requests don't fall back to CWD.
orig_dir
}
};
pub fn new<T: Into<FilesDirs>>(mount_path: &str, serve_from: T) -> Files {
Files {
mount_path: mount_path.trim_end_matches('/').to_owned(),
directory: dir,
directories: serve_from.into().canonicalize(),
index: None,
show_index: false,
redirect_to_slash: false,
@ -149,6 +276,9 @@ impl Files {
/// Redirects to a slash-ended path when browsing a directory.
///
/// By default never redirect.
///
/// When multiple root directories are configured, a matching directory in an earlier root can
/// trigger a redirect before later roots are checked for a file at the same path.
pub fn redirect_to_slash_directory(mut self) -> Self {
self.redirect_to_slash = true;
self
@ -407,7 +537,7 @@ impl ServiceFactory<ServiceRequest> for Files {
fn new_service(&self, _: ()) -> Self::Future {
let mut inner = FilesServiceInner {
directory: self.directory.clone(),
directories: self.directories.clone(),
index: self.index.clone(),
show_index: self.show_index,
redirect_to_slash: self.redirect_to_slash,

View File

@ -37,8 +37,14 @@ mod range;
mod service;
pub use self::{
chunked::ChunkedReadFile, directory::Directory, error::UriSegmentError, files::Files,
named::NamedFile, path_buf::PathBufWrap, range::HttpRange, service::FilesService,
chunked::ChunkedReadFile,
directory::Directory,
error::UriSegmentError,
files::{Files, FilesDirs},
named::NamedFile,
path_buf::PathBufWrap,
range::HttpRange,
service::FilesService,
};
use self::{
directory::{directory_listing, DirectoryRenderer},
@ -63,9 +69,11 @@ type PathFilter = dyn Fn(&Path, &RequestHead) -> bool;
#[cfg(test)]
mod tests {
use std::{
ffi::OsString,
fmt::Write as _,
fs::{self},
ops::Add,
path::PathBuf,
time::{Duration, SystemTime},
};
@ -832,6 +840,243 @@ mod tests {
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[actix_rt::test]
async fn test_static_files_accepts_borrowed_os_string_directory() {
let dir = OsString::from(".");
let service = Files::new("/", &dir).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::OK);
}
#[actix_rt::test]
async fn test_static_files_empty_directories() {
let service = Files::new("/", Vec::<PathBuf>::new())
.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);
let service = Files::new("/", Vec::<PathBuf>::new())
.default_handler(|req: ServiceRequest| async {
Ok(req.into_response(HttpResponse::Ok().body("default content")))
})
.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::OK);
assert_eq!(
test::read_body(resp).await,
Bytes::from_static(b"default content")
);
}
#[actix_rt::test]
async fn test_static_files_multiple_directories() {
let first_dir = tempfile::tempdir().unwrap();
let second_dir = tempfile::tempdir().unwrap();
fs::write(first_dir.path().join("shared.txt"), "first").unwrap();
fs::write(second_dir.path().join("shared.txt"), "second").unwrap();
fs::write(second_dir.path().join("fallback.txt"), "fallback").unwrap();
let service = Files::new("/", [first_dir.path(), second_dir.path()])
.new_service(())
.await
.unwrap();
let req = TestRequest::with_uri("/shared.txt").to_srv_request();
let resp = test::call_service(&service, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(test::read_body(resp).await, Bytes::from_static(b"first"));
let req = TestRequest::with_uri("/fallback.txt").to_srv_request();
let resp = test::call_service(&service, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback"));
}
#[actix_rt::test]
async fn test_static_files_multiple_directories_file_as_parent_falls_back() {
let first_dir = tempfile::tempdir().unwrap();
let second_dir = tempfile::tempdir().unwrap();
fs::write(first_dir.path().join("assets"), "file").unwrap();
fs::create_dir(second_dir.path().join("assets")).unwrap();
fs::write(
second_dir.path().join("assets").join("fallback.txt"),
"fallback",
)
.unwrap();
let service = Files::new("/", [first_dir.path(), second_dir.path()])
.new_service(())
.await
.unwrap();
let req = TestRequest::with_uri("/assets/fallback.txt").to_srv_request();
let resp = test::call_service(&service, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback"));
}
#[actix_rt::test]
async fn test_static_files_multiple_directories_default_handler() {
let first_dir = tempfile::tempdir().unwrap();
let second_dir = tempfile::tempdir().unwrap();
fs::write(second_dir.path().join("fallback.txt"), "fallback").unwrap();
let service = Files::new("/", vec![first_dir.path(), second_dir.path()])
.default_handler(|req: ServiceRequest| async {
Ok(req.into_response(HttpResponse::Ok().body("default content")))
})
.new_service(())
.await
.unwrap();
let req = TestRequest::with_uri("/fallback.txt").to_srv_request();
let resp = test::call_service(&service, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback"));
let req = TestRequest::with_uri("/missing.txt").to_srv_request();
let resp = test::call_service(&service, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
test::read_body(resp).await,
Bytes::from_static(b"default content")
);
}
#[actix_rt::test]
async fn test_static_files_multiple_directories_index_file() {
let first_dir = tempfile::tempdir().unwrap();
let second_dir = tempfile::tempdir().unwrap();
fs::create_dir(first_dir.path().join("nested")).unwrap();
fs::create_dir(second_dir.path().join("nested")).unwrap();
fs::write(
second_dir.path().join("nested").join("index.html"),
"second index",
)
.unwrap();
let service = Files::new("/", [first_dir.path(), second_dir.path()])
.index_file("index.html")
.new_service(())
.await
.unwrap();
let req = TestRequest::with_uri("/nested/").to_srv_request();
let resp = test::call_service(&service, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
test::read_body(resp).await,
Bytes::from_static(b"second index")
);
}
#[actix_rt::test]
async fn test_static_files_multiple_directories_index_file_as_parent_falls_back() {
let first_dir = tempfile::tempdir().unwrap();
let second_dir = tempfile::tempdir().unwrap();
fs::create_dir(first_dir.path().join("nested")).unwrap();
fs::write(first_dir.path().join("nested").join("index.html"), "file").unwrap();
fs::create_dir(second_dir.path().join("nested")).unwrap();
fs::create_dir(second_dir.path().join("nested").join("index.html")).unwrap();
fs::write(
second_dir
.path()
.join("nested")
.join("index.html")
.join("fallback.txt"),
"fallback",
)
.unwrap();
let service = Files::new("/", [first_dir.path(), second_dir.path()])
.index_file("index.html/fallback.txt")
.new_service(())
.await
.unwrap();
let req = TestRequest::with_uri("/nested/").to_srv_request();
let resp = test::call_service(&service, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback"));
}
#[actix_rt::test]
async fn test_static_files_index_file_error_falls_back_to_listing() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("listed.txt"), "listed").unwrap();
let service = Files::new("/", dir.path())
.index_file("index.html\0")
.show_files_listing()
.new_service(())
.await
.unwrap();
let req = TestRequest::with_uri("/").to_srv_request();
let resp = test::call_service(&service, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let bytes = test::read_body(resp).await;
assert!(format!("{bytes:?}").contains("listed.txt"));
}
#[actix_rt::test]
async fn test_static_files_multiple_directories_show_files_listing() {
let first_dir = tempfile::tempdir().unwrap();
let second_dir = tempfile::tempdir().unwrap();
fs::write(first_dir.path().join("listed.txt"), "listed").unwrap();
let service = Files::new("/", [first_dir.path(), second_dir.path()])
.show_files_listing()
.new_service(())
.await
.unwrap();
let req = TestRequest::with_uri("/").to_srv_request();
let resp = test::call_service(&service, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let bytes = test::read_body(resp).await;
assert!(format!("{bytes:?}").contains("listed.txt"));
}
#[actix_rt::test]
async fn test_static_files_multiple_directories_redirect_precedence() {
let first_dir = tempfile::tempdir().unwrap();
let second_dir = tempfile::tempdir().unwrap();
fs::create_dir(first_dir.path().join("item")).unwrap();
fs::write(second_dir.path().join("item"), "file").unwrap();
let service = Files::new("/", [first_dir.path(), second_dir.path()])
.show_files_listing()
.redirect_to_slash_directory()
.new_service(())
.await
.unwrap();
let req = TestRequest::with_uri("/item").to_srv_request();
let resp = test::call_service(&service, req).await;
assert_eq!(resp.status(), StatusCode::TEMPORARY_REDIRECT);
assert_eq!(resp.headers().get(header::LOCATION).unwrap(), "/item/");
}
#[actix_rt::test]
async fn test_default_handler_file_missing() {
let st = Files::new("/", ".")

View File

@ -117,14 +117,35 @@ pub(crate) fn get_content_type_and_disposition(
};
// 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)];
let mut escaped_len = filename.len();
for byte in filename.bytes() {
if matches!(byte, b'\n' | b'\x0B' | b'\x0C' | b'\r') {
escaped_len += 2;
}
}
if !filename.is_ascii() {
let filename_s = if escaped_len == filename.len() {
filename.to_string()
} else {
let mut escaped = String::with_capacity(escaped_len);
for ch in filename.chars() {
match ch {
'\n' => escaped.push_str("%0A"), // \n line break
'\x0B' => escaped.push_str("%0B"), // \v vertical tab
'\x0C' => escaped.push_str("%0C"), // \f form feed
'\r' => escaped.push_str("%0D"), // \r carriage return
ch => escaped.push(ch),
}
}
escaped
};
let is_ascii = filename.is_ascii();
let mut parameters = Vec::with_capacity(if is_ascii { 1 } else { 2 });
parameters.push(DispositionParam::Filename(filename_s));
if !is_ascii {
parameters.push(DispositionParam::FilenameExt(ExtendedValue {
charset: Charset::Ext(String::from("UTF-8")),
language_tag: None,
@ -735,4 +756,15 @@ mod tests {
let (_ct, cd) = get_content_type_and_disposition(Path::new("sound.mp3")).unwrap();
assert_eq!(cd.disposition, DispositionType::Inline);
}
#[test]
fn special_chars_are_escaped_in_content_disposition_filename() {
let (_ct, cd) =
get_content_type_and_disposition(Path::new("test\n\x0B\x0C\rnewline.text")).unwrap();
assert_eq!(
cd.to_string(),
"inline; filename=\"test%0A%0B%0C%0Dnewline.text\"",
);
}
}

View File

@ -1,4 +1,5 @@
use std::{
borrow::Cow,
path::{Component, Path, PathBuf},
str::FromStr,
};
@ -70,8 +71,10 @@ impl PathBufWrap {
.map_err(|_| UriSegmentError::NotValidUtf8)?;
// disallow decoding `%2F` into `/`
if segment_count != path.matches('/').count() + 1 {
return Err(UriSegmentError::BadChar('/'));
if let Cow::Owned(ref path) = path {
if segment_count != path.matches('/').count() + 1 {
return Err(UriSegmentError::BadChar('/'));
}
}
for segment in path.split('/') {
@ -199,6 +202,14 @@ mod tests {
);
}
#[test]
fn encoded_slash_is_rejected() {
assert_eq!(
PathBufWrap::parse_path("/test%2Ffile.txt", false),
Err(UriSegmentError::BadChar('/'))
);
}
#[test]
#[cfg_attr(windows, should_panic)]
fn windows_drive_traversal() {

View File

@ -33,7 +33,7 @@ impl Deref for FilesService {
}
pub struct FilesServiceInner {
pub(crate) directory: PathBuf,
pub(crate) directories: Vec<PathBuf>,
pub(crate) index: Option<String>,
pub(crate) show_index: bool,
pub(crate) redirect_to_slash: bool,
@ -113,8 +113,8 @@ impl FilesService {
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);
fn show_index(&self, req: ServiceRequest, base: PathBuf, path: PathBuf) -> ServiceResponse {
let dir = Directory::new(base, path);
let (req, _) = req.into_parts();
@ -171,70 +171,124 @@ impl Service<ServiceRequest> for FilesService {
}
}
// full file path
let path = this.directory.join(&path_on_disk);
let mut last_miss = None;
let mut first_index_listing = None;
let mut found_unrenderable_dir = false;
// 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));
}
}
for directory in &this.directories {
// full file path
let path = directory.join(&path_on_disk);
if let Err(err) = path.canonicalize() {
return this.handle_err(err, req).await;
}
if path.is_dir() {
if this.redirect_to_slash
&& !req.path().ends_with('/')
&& (this.index.is_some() || this.show_index)
{
let redirect_to = format!("{}/", req.path());
let response = if this.with_permanent_redirect {
HttpResponse::PermanentRedirect()
} else {
HttpResponse::TemporaryRedirect()
// 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));
}
.insert_header((header::LOCATION, redirect_to))
.finish();
return Ok(req.into_response(response));
}
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)
);
if let Err(err) = path.canonicalize() {
if matches!(
err.kind(),
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
) {
last_miss = Some(err);
continue;
}
return this.handle_err(err, req).await;
}
if path.is_dir() {
if this.redirect_to_slash
&& !req.path().ends_with('/')
&& (this.index.is_some() || this.show_index)
{
let redirect_to = format!("{}/", req.path());
let response = if this.with_permanent_redirect {
HttpResponse::PermanentRedirect()
} else {
HttpResponse::TemporaryRedirect()
}
.insert_header((header::LOCATION, redirect_to))
.finish();
return Ok(req.into_response(response));
}
match &this.index {
Some(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) => return Ok(this.serve_named_file(req, named_file)),
Err(err)
if matches!(
err.kind(),
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
) =>
{
if this.show_index && first_index_listing.is_none() {
first_index_listing =
Some((directory.to_path_buf(), path.clone()));
}
last_miss = Some(err);
}
Err(_) if this.show_index => {
if first_index_listing.is_none() {
first_index_listing =
Some((directory.to_path_buf(), path.clone()));
}
break;
}
Err(err) => return this.handle_err(err, req).await,
}
}
// 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)),
Err(err) => this.handle_err(err, req).await,
None if this.show_index => {
return Ok(this.show_index(req, directory.to_path_buf(), path));
}
None => found_unrenderable_dir = true,
}
} else {
match NamedFile::open_async(&path).await {
Ok(named_file) => return Ok(this.serve_named_file(req, named_file)),
Err(err)
if matches!(
err.kind(),
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
) =>
{
last_miss = Some(err);
}
Err(err) => return this.handle_err(err, req).await,
}
None if this.show_index => Ok(this.show_index(req, path)),
None => Ok(ServiceResponse::from_err(
FilesError::IsDirectory,
req.into_parts().0,
)),
}
} else {
match NamedFile::open_async(&path).await {
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
Err(err) => this.handle_err(err, req).await,
}
}
if let Some((base, path)) = first_index_listing {
return Ok(this.show_index(req, base, path));
}
if found_unrenderable_dir {
return Ok(ServiceResponse::from_err(
FilesError::IsDirectory,
req.into_parts().0,
));
}
let err = last_miss
.unwrap_or_else(|| io::Error::new(io::ErrorKind::NotFound, "No such file"));
this.handle_err(err, req).await
})
}
}

View File

@ -166,6 +166,44 @@ async fn test_compression_encodings() {
assert_eq!(res.headers().get(header::CONTENT_ENCODING), None);
}
#[actix_web::test]
async fn test_compression_encodings_multiple_directories() {
use actix_web::body::MessageBody;
let first_dir = tempfile::tempdir().unwrap();
let second_dir = tempfile::tempdir().unwrap();
let compressed_path = second_dir.path().join("fallback.txt.gz");
std::fs::write(&compressed_path, b"compressed").unwrap();
let compressed_len = std::fs::metadata(compressed_path).unwrap().len();
let srv = test::init_service(
App::new().service(Files::new("/", [first_dir.path(), second_dir.path()]).try_compressed()),
)
.await;
let mut req = TestRequest::with_uri("/fallback.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.into_body().size(),
actix_web::body::BodySize::Sized(compressed_len),
);
}
#[actix_web::test]
async fn partial_range_response_encoding() {
let srv = test::init_service(App::new().default_service(web::to(|| async {

View File

@ -3,9 +3,11 @@
## Unreleased
- When configured, gracefully close HTTP/1 connections after early responses to unread request bodies. [#3967]
- Wake HTTP/1 payload receivers with an incomplete-payload error when the sender is dropped before EOF. [#3100]
- Update `foldhash` dependency to `0.2`.
[#3967]: https://github.com/actix/actix-web/issues/3967
[#3100]: https://github.com/actix/actix-web/issues/3100
## 3.12.1

View File

@ -142,7 +142,7 @@ actix-web = "4"
awc = { version = "3", default-features = false, features = ["openssl"] }
async-stream = "0.3"
criterion = { version = "0.5", features = ["html_reports"] }
criterion = { version = "0.8", features = ["html_reports"] }
divan = "0.1.8"
env_logger = "0.11"
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }

View File

@ -1,8 +1,8 @@
use std::convert::Infallible;
use std::{convert::Infallible, hint::black_box};
use actix_http::{encoding::Encoder, ContentEncoding, Request, Response, StatusCode};
use actix_service::{fn_service, Service as _};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion::{criterion_group, criterion_main, Criterion};
static BODY: &[u8] = include_bytes!("../Cargo.toml");

View File

@ -366,10 +366,9 @@ where
io.poll_flush(cx)
}
fn enter_linger(mut self: Pin<&mut Self>) {
let this = self.as_mut().project();
this.flags.remove(Flags::KEEP_ALIVE);
this.flags.insert(Flags::LINGER | Flags::FINISHED);
fn enter_linger(flags: &mut Flags) {
flags.remove(Flags::KEEP_ALIVE);
flags.insert(Flags::LINGER | Flags::FINISHED);
}
fn ensure_linger_timer(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> bool {
@ -464,23 +463,19 @@ where
let size = self.as_mut().send_response_inner(res, &body)?;
match size {
BodySize::None | BodySize::Sized(0) => {
let this = self.as_mut().project();
let mut this = self.as_mut().project();
if close_after_response {
if this.config.client_disconnect_deadline().is_some() {
drop(this);
self.as_mut().enter_linger();
Self::enter_linger(this.flags);
} else {
self.as_mut()
.project()
.flags
.insert(Flags::SHUTDOWN | Flags::FINISHED);
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
}
} else {
this.flags.insert(Flags::FINISHED);
}
self.as_mut().project().state.set(State::None);
this.state.set(State::None);
}
_ => self
.as_mut()
@ -509,23 +504,19 @@ where
let size = self.as_mut().send_response_inner(res, &body)?;
match size {
BodySize::None | BodySize::Sized(0) => {
let this = self.as_mut().project();
let mut this = self.as_mut().project();
if close_after_response {
if this.config.client_disconnect_deadline().is_some() {
drop(this);
self.as_mut().enter_linger();
Self::enter_linger(this.flags);
} else {
self.as_mut()
.project()
.flags
.insert(Flags::SHUTDOWN | Flags::FINISHED);
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
}
} else {
this.flags.insert(Flags::FINISHED);
}
self.as_mut().project().state.set(State::None);
this.state.set(State::None);
}
_ => self
.as_mut()
@ -646,13 +637,9 @@ where
if not_pipelined && close_after_response {
if this.config.client_disconnect_deadline().is_some() {
drop(this);
self.as_mut().enter_linger();
Self::enter_linger(this.flags);
} else {
self.as_mut()
.project()
.flags
.insert(Flags::SHUTDOWN | Flags::FINISHED);
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
}
} else {
this.flags.insert(Flags::FINISHED);
@ -708,13 +695,9 @@ where
if not_pipelined && close_after_response {
if this.config.client_disconnect_deadline().is_some() {
drop(this);
self.as_mut().enter_linger();
Self::enter_linger(this.flags);
} else {
self.as_mut()
.project()
.flags
.insert(Flags::SHUTDOWN | Flags::FINISHED);
this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED);
}
} else {
this.flags.insert(Flags::FINISHED);

View File

@ -140,11 +140,20 @@ impl PayloadSender {
}
}
impl Drop for PayloadSender {
fn drop(&mut self) {
if let Some(shared) = self.inner.upgrade() {
shared.borrow_mut().close_sender();
}
}
}
#[derive(Debug)]
struct Inner {
len: usize,
eof: bool,
err: Option<PayloadError>,
sender_closed: bool,
need_read: bool,
items: VecDeque<Bytes>,
task: Option<Waker>,
@ -157,6 +166,7 @@ impl Inner {
eof,
len: 0,
err: None,
sender_closed: eof,
items: VecDeque::new(),
need_read: true,
task: None,
@ -200,12 +210,21 @@ impl Inner {
#[inline]
fn set_error(&mut self, err: PayloadError) {
self.sender_closed = true;
self.err = Some(err);
self.wake();
}
fn close_sender(&mut self) {
if !self.sender_closed {
self.sender_closed = true;
self.set_error(PayloadError::Incomplete(None));
}
}
#[inline]
fn feed_eof(&mut self) {
self.sender_closed = true;
self.eof = true;
self.wake();
}
@ -332,6 +351,16 @@ mod tests {
timeout(WAKE_TIMEOUT, handle).await.unwrap().unwrap();
}
#[actix_rt::test]
async fn wake_on_sender_drop() {
let (sender, payload) = Payload::create(false);
let (rx, handle) = prepare_waking_test(payload, Some(Err(())));
rx.await.unwrap();
drop(sender);
timeout(WAKE_TIMEOUT, handle).await.unwrap().unwrap();
}
#[actix_rt::test]
async fn test_unread_data() {
let (_, mut payload) = Payload::create(false);

View File

@ -5,6 +5,7 @@
## 0.8.0
- Minimum supported Rust version (MSRV) is now 1.88.
- Update `darling` dependency to `0.23`.
## 0.7.0

View File

@ -18,7 +18,7 @@ proc-macro = true
[dependencies]
bytesize = "2"
darling = "0.20"
darling = "0.23"
proc-macro2 = "1"
quote = "1"
syn = "2"

View File

@ -1,4 +1,4 @@
error: Unknown literal value `no`
error: Unknown value: `no`. Available values: `deny`, `ignore`, `replace`
--> tests/trybuild/deny-parse-fail.rs:4:31
|
4 | #[multipart(duplicate_field = "no")]

View File

@ -47,7 +47,7 @@ enum Flow {
/// [`Multipart`] extractor configuration.
///
/// Add to your app data to have it picked up by [`Multipart`] extractors.
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct MultipartConfig {
buffer_limit: usize,
@ -74,7 +74,7 @@ impl MultipartConfig {
}
}
static DEFAULT_CONFIG: MultipartConfig = MultipartConfig {
const DEFAULT_CONFIG: MultipartConfig = MultipartConfig {
buffer_limit: DEFAULT_BUFFER_LIMIT,
};
@ -1013,7 +1013,7 @@ mod tests {
#[actix_rt::test]
async fn test_multipart_payload_consumption() {
// with sample payload and HttpRequest with no headers
let (_, inner_payload) = h1::Payload::create(false);
let (_sender, inner_payload) = h1::Payload::create(false);
let mut payload = actix_web::dev::Payload::from(inner_payload);
let req = TestRequest::default().to_http_request();

View File

@ -231,7 +231,7 @@ mod tests {
#[actix_rt::test]
async fn basic() {
let (_, payload) = h1::Payload::create(false);
let (_sender, payload) = h1::Payload::create(false);
let mut payload = PayloadBuffer::new_with_limit(payload, DEFAULT_BUFFER_LIMIT);
assert_eq!(payload.buf.len(), 0);

View File

@ -30,7 +30,7 @@ serde = "1"
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
criterion = { version = "0.8", features = ["html_reports"] }
http = "0.2.7"
percent-encoding = "2.1"
serde = { version = "1", features = ["derive"] }

View File

@ -1,6 +1,6 @@
use std::{borrow::Cow, fmt::Write as _};
use std::{borrow::Cow, fmt::Write as _, hint::black_box};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion::{criterion_group, criterion_main, Criterion};
fn compare_quoters(c: &mut Criterion) {
let mut group = c.benchmark_group("Compare Quoters");

View File

@ -1,6 +1,8 @@
//! Based on https://github.com/ibraheemdev/matchit/blob/master/benches/bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use std::hint::black_box;
use criterion::{criterion_group, criterion_main, Criterion};
macro_rules! register {
(colon) => {{

View File

@ -513,20 +513,17 @@ pub(crate) fn with_methods(input: TokenStream) -> TokenStream {
Err(err) => return input_and_compile_error(input, err),
};
let (methods, others) = ast
.attrs
.into_iter()
.map(|attr| match MethodType::from_path(attr.path()) {
Ok(method) => Ok((method, attr)),
Err(_) => Err(attr),
})
.partition::<Vec<_>, _>(Result::is_ok);
let mut methods = Vec::new();
ast.attrs = others.into_iter().map(Result::unwrap_err).collect();
for attr in std::mem::take(&mut ast.attrs) {
match MethodType::from_path(attr.path()) {
Ok(method) => methods.push((method, attr)),
Err(_) => ast.attrs.push(attr),
}
}
let methods = match methods
.into_iter()
.map(Result::unwrap)
.map(|(method, attr)| {
attr.parse_args()
.and_then(|args| Args::new(args, Some(method)))

View File

@ -7,13 +7,16 @@
- Panic when calling `Route::to()` or `Route::service()` after `Route::wrap()` to prevent silently dropping route middleware. [#3944]
- Fix `HttpRequest::{match_pattern,match_name}` reporting path-only matches when route guards disambiguate overlapping resources. [#3346]
- Fix `Readlines` handling of lines split across payload chunks so combined line limits are enforced and complete lines are yielded.
- Fix app data being retained after graceful shutdown with in-flight slow request bodies. [#3100]
- Update `foldhash` dependency to `0.2`.
- Update `rand` dependency to `0.10`.
- Update `impl-more` dependency to `0.3`.
- Add `HttpServer::h1_write_buffer_size()`.
[#3944]: https://github.com/actix/actix-web/pull/3944
[#3346]: https://github.com/actix/actix-web/issues/3346
[#3542]: https://github.com/actix/actix-web/issues/3542
[#3100]: https://github.com/actix/actix-web/issues/3100
## 4.13.0

View File

@ -150,7 +150,7 @@ encoding_rs = "0.8"
foldhash = "0.2"
futures-core = { version = "0.3.17", default-features = false }
futures-util = { version = "0.3.17", default-features = false }
impl-more = "0.1.4"
impl-more = "0.3.1"
itoa = "1"
language-tags = "0.3"
log = "0.4"
@ -176,7 +176,7 @@ awc = { version = "3", features = ["openssl"] }
brotli = "8"
const-str = "1.1"
core_affinity = "0.8"
criterion = { version = "0.5", features = ["html_reports"] }
criterion = { version = "0.8", features = ["html_reports"] }
env_logger = "0.11"
flate2 = "1.0.13"
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }

View File

@ -255,7 +255,7 @@ where
T: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
{
fn drop(&mut self) {
self.app_state.pool().clear();
self.app_state.pool().disable();
}
}

View File

@ -1,5 +1,5 @@
use std::{
cell::{Ref, RefCell, RefMut},
cell::{Cell, Ref, RefCell, RefMut},
collections::HashMap,
fmt,
hash::{BuildHasher, Hash},
@ -669,6 +669,7 @@ impl fmt::Debug for HttpRequest {
/// The pool's default capacity is 128 items.
pub(crate) struct HttpRequestPool {
inner: RefCell<Vec<Rc<HttpRequestInner>>>,
enabled: Cell<bool>,
cap: usize,
}
@ -682,6 +683,7 @@ impl HttpRequestPool {
pub(crate) fn with_capacity(cap: usize) -> Self {
HttpRequestPool {
inner: RefCell::new(Vec::with_capacity(cap)),
enabled: Cell::new(true),
cap,
}
}
@ -698,7 +700,7 @@ impl HttpRequestPool {
/// Check if the pool still has capacity for request storage.
#[inline]
pub(crate) fn is_available(&self) -> bool {
self.inner.borrow_mut().len() < self.cap
self.enabled.get() && self.inner.borrow().len() < self.cap
}
/// Push a request to pool.
@ -707,15 +709,16 @@ impl HttpRequestPool {
self.inner.borrow_mut().push(req);
}
/// Clears all allocated HttpRequest objects.
pub(crate) fn clear(&self) {
self.inner.borrow_mut().clear()
/// Prevents future requests from being returned to the pool and clears existing entries.
pub(crate) fn disable(&self) {
self.enabled.set(false);
self.inner.borrow_mut().clear();
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use bytes::Bytes;
@ -993,6 +996,41 @@ mod tests {
assert_eq!(resp.headers().get("pool_cap").unwrap(), "128");
}
#[actix_rt::test]
async fn test_request_dropped_after_service_does_not_reenter_pool() {
struct State {
_data: Arc<String>,
}
let (weak_data, app_data) = {
let data = Arc::new("data".to_owned());
(Arc::downgrade(&data), web::Data::new(State { _data: data }))
};
let held_req = Rc::new(RefCell::new(None));
{
let held_req = Rc::clone(&held_req);
let srv = init_service(App::new().app_data(app_data).service(web::resource("/").to(
move |req: HttpRequest| {
*held_req.borrow_mut() = Some(req.clone());
HttpResponse::Ok()
},
)))
.await;
let resp = call_service(&srv, TestRequest::default().to_request()).await;
assert_eq!(resp.status(), StatusCode::OK);
drop(resp);
drop(srv);
}
assert!(weak_data.upgrade().is_some());
drop(held_req.borrow_mut().take());
assert!(weak_data.upgrade().is_none());
}
#[actix_rt::test]
async fn test_data() {
let srv = init_service(App::new().app_data(10usize).service(web::resource("/").to(

View File

@ -1,9 +1,16 @@
#[cfg(feature = "openssl")]
extern crate tls_openssl as openssl;
use std::{sync::mpsc, thread, time::Duration};
use std::{
convert::Infallible,
sync::{mpsc, Arc},
thread,
time::Duration,
};
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
use actix_web::{rt::time::sleep, web, App, HttpRequest, HttpResponse, HttpServer};
use bytes::Bytes;
use futures_util::stream;
#[actix_rt::test]
async fn test_start() {
@ -74,6 +81,74 @@ async fn test_start() {
srv.stop(false).await;
}
#[actix_rt::test]
async fn test_app_data_dropped_after_graceful_shutdown_with_slow_request() {
struct State {
_data: Arc<String>,
}
async fn echo(_body: web::Json<String>) -> HttpResponse {
HttpResponse::Ok().finish()
}
let (weak_data, app_data) = {
let data = Arc::new("data".to_owned());
(Arc::downgrade(&data), web::Data::new(State { _data: data }))
};
let server = HttpServer::new(move || {
App::new()
.app_data(app_data.clone())
.service(web::resource("/echo").route(web::post().to(echo)))
})
.workers(1)
.shutdown_timeout(1)
.bind(("127.0.0.1", 0))
.unwrap();
let addr = server.addrs()[0];
let server = server.run();
let server_handle = server.handle();
let send_request = async move {
sleep(Duration::from_millis(100)).await;
let slow_body = stream::unfold(0, |idx| async move {
if idx < 8 {
sleep(Duration::from_millis(200)).await;
Some((Ok::<_, Infallible>(Bytes::from_static(b" ")), idx + 1))
} else {
None
}
});
let client = awc::Client::default();
let _ = client
.post(format!("http://{addr}/echo"))
.insert_header(("content-type", "application/json"))
.send_stream(slow_body)
.await;
};
let graceful_stop = async move {
sleep(Duration::from_millis(300)).await;
server_handle.stop(true).await;
};
let (server_res, (), ()) = tokio::join!(server, send_request, graceful_stop);
server_res.unwrap();
for _ in 0..20 {
sleep(Duration::from_millis(100)).await;
if weak_data.upgrade().is_none() {
return;
}
}
panic!("app data still referenced after graceful shutdown");
}
#[cfg(feature = "openssl")]
fn ssl_acceptor() -> openssl::ssl::SslAcceptorBuilder {
use openssl::{

View File

@ -3,6 +3,7 @@
## Unreleased
- Add camel-case header controls to `WebsocketsRequest` via `camel_case_headers()` and `set_camel_case_headers()`. [#3953]
- Update `hickory-resolver` dependency to `0.26.1`.
- Update `rand` dependency to `0.10`.
[#3953]: https://github.com/actix/actix-web/pull/3953

View File

@ -131,7 +131,7 @@ tls-rustls-0_21 = { package = "rustls", version = "0.21", optional = true, featu
tls-rustls-0_22 = { package = "rustls", version = "0.22", optional = true }
tls-rustls-0_23 = { package = "rustls", version = "0.23", optional = true, default-features = false }
hickory-resolver = { version = "0.25", optional = true, features = ["system-config", "tokio"] }
hickory-resolver = { version = "0.26.1", optional = true, features = ["system-config", "tokio"] }
[dev-dependencies]
actix-http = { version = "3.12", features = ["openssl"] }

View File

@ -1053,7 +1053,7 @@ mod resolver {
use actix_tls::connect::Resolve;
use hickory_resolver::{
config::{ResolverConfig, ResolverOpts},
name_server::TokioConnectionProvider,
net::runtime::TokioRuntimeProvider,
system_conf::read_system_conf,
TokioResolver,
};
@ -1102,9 +1102,10 @@ mod resolver {
};
let resolver =
TokioResolver::builder_with_config(cfg, TokioConnectionProvider::default())
TokioResolver::builder_with_config(cfg, TokioRuntimeProvider::default())
.with_options(opts)
.build();
.build()
.expect("failed to build Hickory DNS resolver");
Resolver::custom(HickoryDnsResolver(resolver))
})

View File

@ -2,16 +2,24 @@
extern crate tls_openssl as openssl;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
use std::{
convert::Infallible,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
},
};
use actix_http::HttpService;
use actix_http::{HttpService, Request, Response};
use actix_http_test::test_server;
use actix_service::{fn_service, map_config, ServiceFactoryExt};
use actix_utils::future::ok;
use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse};
use actix_web::{
dev::AppConfig,
http::{header, Version},
web, App, HttpResponse,
};
use futures_util::stream;
use openssl::{
pkey::PKey,
ssl::{SslAcceptor, SslConnector, SslMethod, SslVerifyMode},
@ -92,3 +100,55 @@ async fn test_connection_reuse_h2() {
// one connection
assert_eq!(num.load(Ordering::Relaxed), 1);
}
// Regression test for https://github.com/actix/actix-web/issues/2305.
#[actix_rt::test]
async fn h2_streaming_body_does_not_send_transfer_encoding() {
let has_transfer_encoding = Arc::new(AtomicBool::new(false));
let has_transfer_encoding2 = Arc::clone(&has_transfer_encoding);
let srv = test_server(move || {
let has_transfer_encoding = Arc::clone(&has_transfer_encoding2);
HttpService::build()
.h2(move |req: Request| {
let has_transfer_encoding = Arc::clone(&has_transfer_encoding);
async move {
has_transfer_encoding.store(
req.head().headers.contains_key(header::TRANSFER_ENCODING),
Ordering::Relaxed,
);
Ok::<_, Infallible>(Response::ok())
}
})
.openssl(tls_config())
.map_err(|_| ())
})
.await;
// disable ssl verification
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
builder.set_verify(SslVerifyMode::NONE);
let _ = builder
.set_alpn_protos(b"\x02h2\x08http/1.1")
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
let client = awc::Client::builder()
.connector(awc::Connector::new().openssl(builder.build()))
.finish();
let response = client
.post(srv.surl("/"))
.version(Version::HTTP_2)
.send_stream(stream::once(async {
Ok::<_, Infallible>(bytes::Bytes::from_static(b"hello"))
}))
.await
.unwrap();
assert!(response.status().is_success());
assert_eq!(response.version(), Version::HTTP_2);
assert!(!has_transfer_encoding.load(Ordering::Relaxed));
}

8
zizmor.yml Normal file
View File

@ -0,0 +1,8 @@
rules:
dangerous-triggers:
ignore:
# Required for labeling PRs from forks; does not check out PR head.
- labeler.yml:3
dependabot-cooldown:
config:
days: 3