mirror of https://github.com/fafhrd91/actix-web
Merge branch 'main' into feat/http-response-take-error
This commit is contained in:
commit
7ed55c4351
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ jobs:
|
|||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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("/", ".")
|
||||
|
|
|
|||
|
|
@ -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\"",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ proc-macro = true
|
|||
|
||||
[dependencies]
|
||||
bytesize = "2"
|
||||
darling = "0.20"
|
||||
darling = "0.23"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = "2"
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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) => {{
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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::{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue