mirror of
https://github.com/fafhrd91/actix-web
synced 2025-08-31 17:07:01 +02:00
Compare commits
72 Commits
awc-v3.7.0
...
files-v0.6
Author | SHA1 | Date | |
---|---|---|---|
|
98d7d0b46b | ||
|
4966a54e05 | ||
|
00b0f8f700 | ||
|
3c2907da41 | ||
|
5041cd1c65 | ||
|
d3c46537b3 | ||
|
8996198f2c | ||
|
68624ec63b | ||
|
bcd0ffb016 | ||
|
9fb6c13a1a | ||
|
05cfef7f4b | ||
|
8f3eb32a32 | ||
|
ddd16ec9db | ||
|
9c47a247fb | ||
|
2536823e3b | ||
|
e3f81d0643 | ||
|
a84aee836b | ||
|
1f08cb24c3 | ||
|
1b49047086 | ||
|
9fe033a963 | ||
|
2ba69a1904 | ||
|
c6352005f7 | ||
|
aeb42486af | ||
|
30f7df5853 | ||
|
e3ae20de30 | ||
|
5f6c84494a | ||
|
37203c7fd8 | ||
|
971bbc1468 | ||
|
d08a85b470 | ||
|
d94442246f | ||
|
706636da67 | ||
|
be6be9fbcc | ||
|
cd71768509 | ||
|
d5abb5ae45 | ||
|
522f60fa8c | ||
|
c65633b548 | ||
|
351f7a17ed | ||
|
254fcab19d | ||
|
3c67d04cbe | ||
|
b355603c99 | ||
|
a81c05818f | ||
|
aceff9d13f | ||
|
1896ff2ec7 | ||
|
0eef04ed0a | ||
|
6e396bc352 | ||
|
b6ad483699 | ||
|
c002fd783c | ||
|
95ad1caa23 | ||
|
eb906d077a | ||
|
f08fa6b684 | ||
|
3f9d88f859 | ||
|
2eb801cb59 | ||
|
43d3c6af83 | ||
|
fdde0b8b14 | ||
|
2a634a8af2 | ||
|
5e0f56f572 | ||
|
10266204ee | ||
|
ebc8fdb29a | ||
|
b61346e2ef | ||
|
db0a0512d9 | ||
|
eb1429d6dd | ||
|
10f56f6412 | ||
|
8562b7d7bb | ||
|
442bb4cecf | ||
|
9d98753ffa | ||
|
fd81e6ee0d | ||
|
1f9bc2de7d | ||
|
79f2486dc1 | ||
|
9a12bbd118 | ||
|
072fdc182d | ||
|
347c7204a6 | ||
|
573090e01f |
@@ -3,6 +3,6 @@ disallowed-names = [
|
|||||||
"e", # no single letter error bindings
|
"e", # no single letter error bindings
|
||||||
]
|
]
|
||||||
disallowed-methods = [
|
disallowed-methods = [
|
||||||
{ path = "std::cell::RefCell::default()", reason = "prefer explicit inner type default" },
|
{ path = "std::cell::RefCell::default()", reason = "prefer explicit inner type default (remove allow-invalid when rust-lang/rust-clippy/#8581 is fixed)", allow-invalid = true },
|
||||||
{ path = "std::rc::Rc::default()", reason = "prefer explicit inner type default" },
|
{ path = "std::rc::Rc::default()", reason = "prefer explicit inner type default (remove allow-invalid when rust-lang/rust-clippy/#8581 is fixed)", allow-invalid = true },
|
||||||
]
|
]
|
||||||
|
@@ -9,4 +9,5 @@ words:
|
|||||||
- rustls
|
- rustls
|
||||||
- rustup
|
- rustup
|
||||||
- serde
|
- serde
|
||||||
|
- uring
|
||||||
- zstd
|
- zstd
|
||||||
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,3 +1,3 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: [robjtede]
|
github: [robjtede, JohnTitor]
|
||||||
|
2
.github/workflows/bench.yml
vendored
2
.github/workflows/bench.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
run: |
|
run: |
|
||||||
|
16
.github/workflows/ci-post-merge.yml
vendored
16
.github/workflows/ci-post-merge.yml
vendored
@@ -28,11 +28,11 @@ jobs:
|
|||||||
runs-on: ${{ matrix.target.os }}
|
runs-on: ${{ matrix.target.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Install nasm
|
- name: Install nasm
|
||||||
if: matrix.target.os == 'windows-latest'
|
if: matrix.target.os == 'windows-latest'
|
||||||
uses: ilammy/setup-nasm@v1.5.2
|
uses: ilammy/setup-nasm@72793074d3c8cdda771dba85f6deafe00623038b # v1.5.2
|
||||||
|
|
||||||
- name: Install OpenSSL
|
- name: Install OpenSSL
|
||||||
if: matrix.target.os == 'windows-latest'
|
if: matrix.target.os == 'windows-latest'
|
||||||
@@ -44,12 +44,12 @@ jobs:
|
|||||||
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
|
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Install Rust (${{ matrix.version.name }})
|
- name: Install Rust (${{ matrix.version.name }})
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ matrix.version.version }}
|
toolchain: ${{ matrix.version.version }}
|
||||||
|
|
||||||
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
|
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
|
||||||
uses: taiki-e/install-action@v2.50.10
|
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
|
||||||
with:
|
with:
|
||||||
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
|
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
|
||||||
|
|
||||||
@@ -71,19 +71,19 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Free Disk Space
|
- name: Free Disk Space
|
||||||
run: ./scripts/free-disk-space.sh
|
run: ./scripts/free-disk-space.sh
|
||||||
|
|
||||||
- name: Setup mold linker
|
- name: Setup mold linker
|
||||||
uses: rui314/setup-mold@v1
|
uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
|
||||||
|
|
||||||
- name: Install just, cargo-hack
|
- name: Install just, cargo-hack
|
||||||
uses: taiki-e/install-action@v2.50.10
|
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
|
||||||
with:
|
with:
|
||||||
tool: just,cargo-hack
|
tool: just,cargo-hack
|
||||||
|
|
||||||
|
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@@ -18,7 +18,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
read_msrv:
|
read_msrv:
|
||||||
name: Read MSRV
|
name: Read MSRV
|
||||||
uses: actions-rust-lang/msrv/.github/workflows/msrv.yml@v0.1.0
|
uses: actions-rust-lang/msrv/.github/workflows/msrv.yml@8b553824444060021f2843d7b4d803f3624d15e5 # v0.1.0
|
||||||
|
|
||||||
build_and_test:
|
build_and_test:
|
||||||
needs: read_msrv
|
needs: read_msrv
|
||||||
@@ -39,11 +39,11 @@ jobs:
|
|||||||
runs-on: ${{ matrix.target.os }}
|
runs-on: ${{ matrix.target.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Install nasm
|
- name: Install nasm
|
||||||
if: matrix.target.os == 'windows-latest'
|
if: matrix.target.os == 'windows-latest'
|
||||||
uses: ilammy/setup-nasm@v1.5.2
|
uses: ilammy/setup-nasm@72793074d3c8cdda771dba85f6deafe00623038b # v1.5.2
|
||||||
|
|
||||||
- name: Install OpenSSL
|
- name: Install OpenSSL
|
||||||
if: matrix.target.os == 'windows-latest'
|
if: matrix.target.os == 'windows-latest'
|
||||||
@@ -56,15 +56,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup mold linker
|
- name: Setup mold linker
|
||||||
if: matrix.target.os == 'ubuntu-latest'
|
if: matrix.target.os == 'ubuntu-latest'
|
||||||
uses: rui314/setup-mold@v1
|
uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
|
||||||
|
|
||||||
- name: Install Rust (${{ matrix.version.name }})
|
- name: Install Rust (${{ matrix.version.name }})
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ matrix.version.version }}
|
toolchain: ${{ matrix.version.version }}
|
||||||
|
|
||||||
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
|
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
|
||||||
uses: taiki-e/install-action@v2.50.10
|
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
|
||||||
with:
|
with:
|
||||||
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
|
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
|
||||||
|
|
||||||
@@ -89,10 +89,10 @@ jobs:
|
|||||||
name: io-uring tests
|
name: io-uring tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
|
|
||||||
@@ -105,15 +105,15 @@ jobs:
|
|||||||
name: doc tests
|
name: doc tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Install Rust (nightly)
|
- name: Install Rust (nightly)
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
|
|
||||||
- name: Install just
|
- name: Install just
|
||||||
uses: taiki-e/install-action@v2.50.10
|
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
|
||||||
with:
|
with:
|
||||||
tool: just
|
tool: just
|
||||||
|
|
||||||
|
8
.github/workflows/coverage.yml
vendored
8
.github/workflows/coverage.yml
vendored
@@ -15,16 +15,16 @@ jobs:
|
|||||||
coverage:
|
coverage:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Install Rust (nightly)
|
- name: Install Rust (nightly)
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
components: llvm-tools
|
components: llvm-tools
|
||||||
|
|
||||||
- name: Install just, cargo-llvm-cov, cargo-nextest
|
- name: Install just, cargo-llvm-cov, cargo-nextest
|
||||||
uses: taiki-e/install-action@v2.50.10
|
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
|
||||||
with:
|
with:
|
||||||
tool: just,cargo-llvm-cov,cargo-nextest
|
tool: just,cargo-llvm-cov,cargo-nextest
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
run: just test-coverage-codecov
|
run: just test-coverage-codecov
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v5.4.2
|
uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0
|
||||||
with:
|
with:
|
||||||
files: codecov.json
|
files: codecov.json
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
|
22
.github/workflows/lint.yml
vendored
22
.github/workflows/lint.yml
vendored
@@ -15,10 +15,10 @@ jobs:
|
|||||||
fmt:
|
fmt:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Install Rust (nightly)
|
- name: Install Rust (nightly)
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
@@ -33,15 +33,15 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
|
||||||
with:
|
with:
|
||||||
components: clippy
|
components: clippy
|
||||||
|
|
||||||
- name: Check with Clippy
|
- name: Check with Clippy
|
||||||
uses: giraffate/clippy-action@v1.0.1
|
uses: giraffate/clippy-action@13b9d32482f25d29ead141b79e7e04e7900281e0 # v1.0.1
|
||||||
with:
|
with:
|
||||||
reporter: github-pr-check
|
reporter: github-pr-check
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -52,10 +52,10 @@ jobs:
|
|||||||
lint-docs:
|
lint-docs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Install Rust (nightly)
|
- name: Install Rust (nightly)
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
components: rust-docs
|
components: rust-docs
|
||||||
@@ -69,20 +69,20 @@ jobs:
|
|||||||
if: false # rustdoc mismatch currently
|
if: false # rustdoc mismatch currently
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }})
|
- name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }})
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
|
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
|
||||||
|
|
||||||
- name: Install just
|
- name: Install just
|
||||||
uses: taiki-e/install-action@v2.50.10
|
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
|
||||||
with:
|
with:
|
||||||
tool: just
|
tool: just
|
||||||
|
|
||||||
- name: Install cargo-check-external-types
|
- name: Install cargo-check-external-types
|
||||||
uses: taiki-e/cache-cargo-install-action@v2.1.1
|
uses: taiki-e/cache-cargo-install-action@b33c63d3b3c85540f4eba8a4f71a5cc0ce030855 # v2.3.0
|
||||||
with:
|
with:
|
||||||
tool: cargo-check-external-types
|
tool: cargo-check-external-types
|
||||||
|
|
||||||
|
785
Cargo.lock
generated
785
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## 0.6.7
|
||||||
|
|
||||||
|
- Add `{Files, NamedFile}::read_mode_threshold()` methods to allow faster synchronous reads of small files.
|
||||||
- Minimum supported Rust version (MSRV) is now 1.75.
|
- Minimum supported Rust version (MSRV) is now 1.75.
|
||||||
|
|
||||||
## 0.6.6
|
## 0.6.6
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-files"
|
name = "actix-files"
|
||||||
version = "0.6.6"
|
version = "0.6.7"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
|
||||||
description = "Static file serving for Actix Web"
|
description = "Static file serving for Actix Web"
|
||||||
keywords = ["actix", "http", "async", "futures"]
|
keywords = ["actix", "http", "async", "futures"]
|
||||||
|
@@ -3,11 +3,11 @@
|
|||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-files)
|
[](https://crates.io/crates/actix-files)
|
||||||
[](https://docs.rs/actix-files/0.6.6)
|
[](https://docs.rs/actix-files/0.6.7)
|
||||||

|

|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-files/0.6.6)
|
[](https://deps.rs/crate/actix-files/0.6.7)
|
||||||
[](https://crates.io/crates/actix-files)
|
[](https://crates.io/crates/actix-files)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
@@ -14,6 +14,12 @@ use pin_project_lite::pin_project;
|
|||||||
|
|
||||||
use super::named::File;
|
use super::named::File;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub(crate) enum ReadMode {
|
||||||
|
Sync,
|
||||||
|
Async,
|
||||||
|
}
|
||||||
|
|
||||||
pin_project! {
|
pin_project! {
|
||||||
/// Adapter to read a `std::file::File` in chunks.
|
/// Adapter to read a `std::file::File` in chunks.
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
@@ -24,6 +30,7 @@ pin_project! {
|
|||||||
state: ChunkedReadFileState<Fut>,
|
state: ChunkedReadFileState<Fut>,
|
||||||
counter: u64,
|
counter: u64,
|
||||||
callback: F,
|
callback: F,
|
||||||
|
read_mode: ReadMode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +64,7 @@ pub(crate) fn new_chunked_read(
|
|||||||
size: u64,
|
size: u64,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
file: File,
|
file: File,
|
||||||
|
read_mode_threshold: u64,
|
||||||
) -> impl Stream<Item = Result<Bytes, Error>> {
|
) -> impl Stream<Item = Result<Bytes, Error>> {
|
||||||
ChunkedReadFile {
|
ChunkedReadFile {
|
||||||
size,
|
size,
|
||||||
@@ -69,31 +77,50 @@ pub(crate) fn new_chunked_read(
|
|||||||
},
|
},
|
||||||
counter: 0,
|
counter: 0,
|
||||||
callback: chunked_read_file_callback,
|
callback: chunked_read_file_callback,
|
||||||
|
read_mode: if size < read_mode_threshold {
|
||||||
|
ReadMode::Sync
|
||||||
|
} else {
|
||||||
|
ReadMode::Async
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "experimental-io-uring"))]
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
async fn chunked_read_file_callback(
|
fn chunked_read_file_callback_sync(
|
||||||
mut file: File,
|
mut file: File,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
max_bytes: usize,
|
max_bytes: usize,
|
||||||
) -> Result<(File, Bytes), Error> {
|
) -> Result<(File, Bytes), io::Error> {
|
||||||
use io::{Read as _, Seek as _};
|
use io::{Read as _, Seek as _};
|
||||||
|
|
||||||
let res = actix_web::web::block(move || {
|
let mut buf = Vec::with_capacity(max_bytes);
|
||||||
let mut buf = Vec::with_capacity(max_bytes);
|
|
||||||
|
|
||||||
file.seek(io::SeekFrom::Start(offset))?;
|
file.seek(io::SeekFrom::Start(offset))?;
|
||||||
|
|
||||||
let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
|
let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
|
||||||
|
|
||||||
if n_bytes == 0 {
|
if n_bytes == 0 {
|
||||||
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
|
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
|
||||||
} else {
|
} else {
|
||||||
Ok((file, Bytes::from(buf)))
|
Ok((file, Bytes::from(buf)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
#[inline]
|
||||||
|
async fn chunked_read_file_callback(
|
||||||
|
file: File,
|
||||||
|
offset: u64,
|
||||||
|
max_bytes: usize,
|
||||||
|
read_mode: ReadMode,
|
||||||
|
) -> Result<(File, Bytes), Error> {
|
||||||
|
let res = match read_mode {
|
||||||
|
ReadMode::Sync => chunked_read_file_callback_sync(file, offset, max_bytes)?,
|
||||||
|
ReadMode::Async => {
|
||||||
|
actix_web::web::block(move || chunked_read_file_callback_sync(file, offset, max_bytes))
|
||||||
|
.await??
|
||||||
}
|
}
|
||||||
})
|
};
|
||||||
.await??;
|
|
||||||
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
@@ -171,7 +198,7 @@ where
|
|||||||
#[cfg(not(feature = "experimental-io-uring"))]
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
|
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
|
||||||
where
|
where
|
||||||
F: Fn(File, u64, usize) -> Fut,
|
F: Fn(File, u64, usize, ReadMode) -> Fut,
|
||||||
Fut: Future<Output = Result<(File, Bytes), Error>>,
|
Fut: Future<Output = Result<(File, Bytes), Error>>,
|
||||||
{
|
{
|
||||||
type Item = Result<Bytes, Error>;
|
type Item = Result<Bytes, Error>;
|
||||||
@@ -193,7 +220,7 @@ where
|
|||||||
.take()
|
.take()
|
||||||
.expect("ChunkedReadFile polled after completion");
|
.expect("ChunkedReadFile polled after completion");
|
||||||
|
|
||||||
let fut = (this.callback)(file, offset, max_bytes);
|
let fut = (this.callback)(file, offset, max_bytes, *this.read_mode);
|
||||||
|
|
||||||
this.state
|
this.state
|
||||||
.project_replace(ChunkedReadFileState::Future { fut });
|
.project_replace(ChunkedReadFileState::Future { fut });
|
||||||
|
@@ -49,6 +49,7 @@ pub struct Files {
|
|||||||
use_guards: Option<Rc<dyn Guard>>,
|
use_guards: Option<Rc<dyn Guard>>,
|
||||||
guards: Vec<Rc<dyn Guard>>,
|
guards: Vec<Rc<dyn Guard>>,
|
||||||
hidden_files: bool,
|
hidden_files: bool,
|
||||||
|
read_mode_threshold: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Files {
|
impl fmt::Debug for Files {
|
||||||
@@ -73,6 +74,7 @@ impl Clone for Files {
|
|||||||
use_guards: self.use_guards.clone(),
|
use_guards: self.use_guards.clone(),
|
||||||
guards: self.guards.clone(),
|
guards: self.guards.clone(),
|
||||||
hidden_files: self.hidden_files,
|
hidden_files: self.hidden_files,
|
||||||
|
read_mode_threshold: self.read_mode_threshold,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,6 +121,7 @@ impl Files {
|
|||||||
use_guards: None,
|
use_guards: None,
|
||||||
guards: Vec::new(),
|
guards: Vec::new(),
|
||||||
hidden_files: false,
|
hidden_files: false,
|
||||||
|
read_mode_threshold: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +207,23 @@ impl Files {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the size threshold that determines file read mode (sync/async).
|
||||||
|
///
|
||||||
|
/// When a file is smaller than the threshold (bytes), the reader will switch from synchronous
|
||||||
|
/// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large
|
||||||
|
/// files.
|
||||||
|
///
|
||||||
|
/// Tweaking this value according to your expected usage may lead to signifiant performance
|
||||||
|
/// gains (or losses in other handlers, if `size` is too high).
|
||||||
|
///
|
||||||
|
/// When the `experimental-io-uring` crate feature is enabled, file reads are always async.
|
||||||
|
///
|
||||||
|
/// Default is 0, meaning all files are read asynchronously.
|
||||||
|
pub fn read_mode_threshold(mut self, size: u64) -> Self {
|
||||||
|
self.read_mode_threshold = size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Specifies whether to use ETag or not.
|
/// Specifies whether to use ETag or not.
|
||||||
///
|
///
|
||||||
/// Default is true.
|
/// Default is true.
|
||||||
@@ -367,6 +387,7 @@ impl ServiceFactory<ServiceRequest> for Files {
|
|||||||
file_flags: self.file_flags,
|
file_flags: self.file_flags,
|
||||||
guards: self.use_guards.clone(),
|
guards: self.use_guards.clone(),
|
||||||
hidden_files: self.hidden_files,
|
hidden_files: self.hidden_files,
|
||||||
|
size_threshold: self.read_mode_threshold,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref default) = *self.default.borrow() {
|
if let Some(ref default) = *self.default.borrow() {
|
||||||
|
@@ -80,6 +80,7 @@ pub struct NamedFile {
|
|||||||
pub(crate) content_type: Mime,
|
pub(crate) content_type: Mime,
|
||||||
pub(crate) content_disposition: ContentDisposition,
|
pub(crate) content_disposition: ContentDisposition,
|
||||||
pub(crate) encoding: Option<ContentEncoding>,
|
pub(crate) encoding: Option<ContentEncoding>,
|
||||||
|
pub(crate) read_mode_threshold: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "experimental-io-uring"))]
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
@@ -200,6 +201,7 @@ impl NamedFile {
|
|||||||
encoding,
|
encoding,
|
||||||
status_code: StatusCode::OK,
|
status_code: StatusCode::OK,
|
||||||
flags: Flags::default(),
|
flags: Flags::default(),
|
||||||
|
read_mode_threshold: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,6 +355,23 @@ impl NamedFile {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the size threshold that determines file read mode (sync/async).
|
||||||
|
///
|
||||||
|
/// When a file is smaller than the threshold (bytes), the reader will switch from synchronous
|
||||||
|
/// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large
|
||||||
|
/// files.
|
||||||
|
///
|
||||||
|
/// Tweaking this value according to your expected usage may lead to signifiant performance
|
||||||
|
/// gains (or losses in other handlers, if `size` is too high).
|
||||||
|
///
|
||||||
|
/// When the `experimental-io-uring` crate feature is enabled, file reads are always async.
|
||||||
|
///
|
||||||
|
/// Default is 0, meaning all files are read asynchronously.
|
||||||
|
pub fn read_mode_threshold(mut self, size: u64) -> Self {
|
||||||
|
self.read_mode_threshold = size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Specifies whether to return `ETag` header in response.
|
/// Specifies whether to return `ETag` header in response.
|
||||||
///
|
///
|
||||||
/// Default is true.
|
/// Default is true.
|
||||||
@@ -440,7 +459,8 @@ impl NamedFile {
|
|||||||
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
|
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let reader = chunked::new_chunked_read(self.md.len(), 0, self.file);
|
let reader =
|
||||||
|
chunked::new_chunked_read(self.md.len(), 0, self.file, self.read_mode_threshold);
|
||||||
|
|
||||||
return res.streaming(reader);
|
return res.streaming(reader);
|
||||||
}
|
}
|
||||||
@@ -577,7 +597,7 @@ impl NamedFile {
|
|||||||
.map_into_boxed_body();
|
.map_into_boxed_body();
|
||||||
}
|
}
|
||||||
|
|
||||||
let reader = chunked::new_chunked_read(length, offset, self.file);
|
let reader = chunked::new_chunked_read(length, offset, self.file, self.read_mode_threshold);
|
||||||
|
|
||||||
if offset != 0 || length != self.md.len() {
|
if offset != 0 || length != self.md.len() {
|
||||||
res.status(StatusCode::PARTIAL_CONTENT);
|
res.status(StatusCode::PARTIAL_CONTENT);
|
||||||
|
@@ -39,6 +39,7 @@ pub struct FilesServiceInner {
|
|||||||
pub(crate) file_flags: named::Flags,
|
pub(crate) file_flags: named::Flags,
|
||||||
pub(crate) guards: Option<Rc<dyn Guard>>,
|
pub(crate) guards: Option<Rc<dyn Guard>>,
|
||||||
pub(crate) hidden_files: bool,
|
pub(crate) hidden_files: bool,
|
||||||
|
pub(crate) size_threshold: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for FilesServiceInner {
|
impl fmt::Debug for FilesServiceInner {
|
||||||
@@ -70,7 +71,9 @@ impl FilesService {
|
|||||||
named_file.flags = self.file_flags;
|
named_file.flags = self.file_flags;
|
||||||
|
|
||||||
let (req, _) = req.into_parts();
|
let (req, _) = req.into_parts();
|
||||||
let res = named_file.into_response(&req);
|
let res = named_file
|
||||||
|
.read_mode_threshold(self.size_threshold)
|
||||||
|
.into_response(&req);
|
||||||
ServiceResponse::new(req, res)
|
ServiceResponse::new(req, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,17 +172,7 @@ impl Service<ServiceRequest> for FilesService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match NamedFile::open_async(&path).await {
|
match NamedFile::open_async(&path).await {
|
||||||
Ok(mut named_file) => {
|
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
|
||||||
if let Some(ref mime_override) = this.mime_override {
|
|
||||||
let new_disposition = mime_override(&named_file.content_type.type_());
|
|
||||||
named_file.content_disposition.disposition = new_disposition;
|
|
||||||
}
|
|
||||||
named_file.flags = this.file_flags;
|
|
||||||
|
|
||||||
let (req, _) = req.into_parts();
|
|
||||||
let res = named_file.into_response(&req);
|
|
||||||
Ok(ServiceResponse::new(req, res))
|
|
||||||
}
|
|
||||||
Err(err) => this.handle_err(err, req).await,
|
Err(err) => this.handle_err(err, req).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -53,7 +53,7 @@ serde = "1"
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
slab = "0.4"
|
slab = "0.4"
|
||||||
socket2 = "0.5"
|
socket2 = "0.6"
|
||||||
tls-openssl = { version = "0.10.55", package = "openssl", optional = true }
|
tls-openssl = { version = "0.10.55", package = "openssl", optional = true }
|
||||||
tokio = { version = "1.38.2", features = ["sync"] }
|
tokio = { version = "1.38.2", features = ["sync"] }
|
||||||
|
|
||||||
|
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Properly wake Payload receivers when feeding errors or EOF
|
||||||
|
|
||||||
|
## 3.11.1
|
||||||
|
|
||||||
|
- Prevent more hangs after client disconnects.
|
||||||
|
- More malformed WebSocket frames are now gracefully rejected.
|
||||||
|
- Using `TestRequest::set_payload()` now sets a Content-Length header.
|
||||||
|
|
||||||
## 3.11.0
|
## 3.11.0
|
||||||
|
|
||||||
- Update `brotli` dependency to `8`.
|
- Update `brotli` dependency to `8`.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.11.0"
|
version = "3.11.1"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
|
||||||
description = "HTTP types and services for the Actix ecosystem"
|
description = "HTTP types and services for the Actix ecosystem"
|
||||||
keywords = ["actix", "http", "framework", "async", "futures"]
|
keywords = ["actix", "http", "framework", "async", "futures"]
|
||||||
@@ -17,7 +17,6 @@ edition.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
features = [
|
features = [
|
||||||
"http2",
|
"http2",
|
||||||
"ws",
|
"ws",
|
||||||
@@ -119,7 +118,7 @@ tokio-util = { version = "0.7", features = ["io", "codec"] }
|
|||||||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||||
|
|
||||||
# http2
|
# http2
|
||||||
h2 = { version = "0.3.26", optional = true }
|
h2 = { version = "0.3.27", optional = true }
|
||||||
|
|
||||||
# websockets
|
# websockets
|
||||||
base64 = { version = "0.22", optional = true }
|
base64 = { version = "0.22", optional = true }
|
||||||
@@ -157,7 +156,7 @@ serde_json = "1.0"
|
|||||||
static_assertions = "1"
|
static_assertions = "1"
|
||||||
tls-openssl = { package = "openssl", version = "0.10.55" }
|
tls-openssl = { package = "openssl", version = "0.10.55" }
|
||||||
tls-rustls_023 = { package = "rustls", version = "0.23" }
|
tls-rustls_023 = { package = "rustls", version = "0.23" }
|
||||||
tokio = { version = "1.38.2", features = ["net", "rt", "macros"] }
|
tokio = { version = "1.38.2", features = ["net", "rt", "macros", "sync"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
@@ -5,11 +5,11 @@
|
|||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-http)
|
[](https://crates.io/crates/actix-http)
|
||||||
[](https://docs.rs/actix-http/3.11.0)
|
[](https://docs.rs/actix-http/3.11.1)
|
||||||

|

|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-http/3.11.0)
|
[](https://deps.rs/crate/actix-http/3.11.1)
|
||||||
[](https://crates.io/crates/actix-http)
|
[](https://crates.io/crates/actix-http)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
@@ -1182,7 +1182,7 @@ where
|
|||||||
let state_is_none = inner_p.state.is_none();
|
let state_is_none = inner_p.state.is_none();
|
||||||
|
|
||||||
// read half is closed; we do not process any responses
|
// read half is closed; we do not process any responses
|
||||||
if inner_p.flags.contains(Flags::READ_DISCONNECT) && state_is_none {
|
if inner_p.flags.contains(Flags::READ_DISCONNECT) {
|
||||||
trace!("read half closed; start shutdown");
|
trace!("read half closed; start shutdown");
|
||||||
inner_p.flags.insert(Flags::SHUTDOWN);
|
inner_p.flags.insert(Flags::SHUTDOWN);
|
||||||
}
|
}
|
||||||
@@ -1216,6 +1216,9 @@ where
|
|||||||
inner_p.shutdown_timer,
|
inner_p.shutdown_timer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if inner_p.flags.contains(Flags::SHUTDOWN) {
|
||||||
|
cx.waker().wake_by_ref();
|
||||||
|
}
|
||||||
Poll::Pending
|
Poll::Pending
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -200,11 +200,13 @@ impl Inner {
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn set_error(&mut self, err: PayloadError) {
|
fn set_error(&mut self, err: PayloadError) {
|
||||||
self.err = Some(err);
|
self.err = Some(err);
|
||||||
|
self.wake();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn feed_eof(&mut self) {
|
fn feed_eof(&mut self) {
|
||||||
self.eof = true;
|
self.eof = true;
|
||||||
|
self.wake();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -253,8 +255,13 @@ impl Inner {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::{task::Poll, time::Duration};
|
||||||
|
|
||||||
|
use actix_rt::time::timeout;
|
||||||
use actix_utils::future::poll_fn;
|
use actix_utils::future::poll_fn;
|
||||||
|
use futures_util::{FutureExt, StreamExt};
|
||||||
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -263,6 +270,67 @@ mod tests {
|
|||||||
|
|
||||||
assert_impl_all!(Inner: Unpin, Send, Sync);
|
assert_impl_all!(Inner: Unpin, Send, Sync);
|
||||||
|
|
||||||
|
const WAKE_TIMEOUT: Duration = Duration::from_secs(2);
|
||||||
|
|
||||||
|
fn prepare_waking_test(
|
||||||
|
mut payload: Payload,
|
||||||
|
expected: Option<Result<(), ()>>,
|
||||||
|
) -> (oneshot::Receiver<()>, actix_rt::task::JoinHandle<()>) {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
|
let handle = actix_rt::spawn(async move {
|
||||||
|
// Make sure to poll once to set the waker
|
||||||
|
poll_fn(|cx| {
|
||||||
|
assert!(payload.poll_next_unpin(cx).is_pending());
|
||||||
|
Poll::Ready(())
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
tx.send(()).unwrap();
|
||||||
|
|
||||||
|
// actix-rt is single-threaded, so this won't race with `rx.await`
|
||||||
|
let mut pend_once = false;
|
||||||
|
poll_fn(|_| {
|
||||||
|
if pend_once {
|
||||||
|
Poll::Ready(())
|
||||||
|
} else {
|
||||||
|
// Return pending without storing wakers, we already did on the previous
|
||||||
|
// `poll_fn`, now this task will only continue if the `sender` wakes us
|
||||||
|
pend_once = true;
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let got = payload.next().now_or_never().unwrap();
|
||||||
|
match expected {
|
||||||
|
Some(Ok(_)) => assert!(got.unwrap().is_ok()),
|
||||||
|
Some(Err(_)) => assert!(got.unwrap().is_err()),
|
||||||
|
None => assert!(got.is_none()),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
(rx, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn wake_on_error() {
|
||||||
|
let (mut sender, payload) = Payload::create(false);
|
||||||
|
let (rx, handle) = prepare_waking_test(payload, Some(Err(())));
|
||||||
|
|
||||||
|
rx.await.unwrap();
|
||||||
|
sender.set_error(PayloadError::Incomplete(None));
|
||||||
|
timeout(WAKE_TIMEOUT, handle).await.unwrap().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn wake_on_eof() {
|
||||||
|
let (mut sender, payload) = Payload::create(false);
|
||||||
|
let (rx, handle) = prepare_waking_test(payload, None);
|
||||||
|
|
||||||
|
rx.await.unwrap();
|
||||||
|
sender.feed_eof();
|
||||||
|
timeout(WAKE_TIMEOUT, handle).await.unwrap().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_unread_data() {
|
async fn test_unread_data() {
|
||||||
let (_, mut payload) = Payload::create(false);
|
let (_, mut payload) = Payload::create(false);
|
||||||
|
@@ -11,7 +11,7 @@ use std::{
|
|||||||
|
|
||||||
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
|
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use http::{Method, Uri, Version};
|
use http::{header, Method, Uri, Version};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
header::{HeaderMap, TryIntoHeaderPair},
|
header::{HeaderMap, TryIntoHeaderPair},
|
||||||
@@ -98,9 +98,13 @@ impl TestRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set request payload.
|
/// Set request payload.
|
||||||
|
///
|
||||||
|
/// This sets the `Content-Length` header with the size of `data`.
|
||||||
pub fn set_payload(&mut self, data: impl Into<Bytes>) -> &mut Self {
|
pub fn set_payload(&mut self, data: impl Into<Bytes>) -> &mut Self {
|
||||||
let mut payload = crate::h1::Payload::empty();
|
let mut payload = crate::h1::Payload::empty();
|
||||||
payload.unread_data(data.into());
|
let bytes = data.into();
|
||||||
|
self.insert_header((header::CONTENT_LENGTH, bytes.len()));
|
||||||
|
payload.unread_data(bytes);
|
||||||
parts(&mut self.0).payload = Some(payload.into());
|
parts(&mut self.0).payload = Some(payload.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@@ -94,11 +94,21 @@ impl Parser {
|
|||||||
Some(res) => res,
|
Some(res) => res,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let frame_len = match idx.checked_add(length) {
|
||||||
|
Some(len) => len,
|
||||||
|
None => return Err(ProtocolError::Overflow),
|
||||||
|
};
|
||||||
|
|
||||||
// not enough data
|
// not enough data
|
||||||
if src.len() < idx + length {
|
if src.len() < frame_len {
|
||||||
let min_length = min(length, max_size);
|
let min_length = min(length, max_size);
|
||||||
if src.capacity() < idx + min_length {
|
let required_cap = match idx.checked_add(min_length) {
|
||||||
src.reserve(idx + min_length - src.capacity());
|
Some(cap) => cap,
|
||||||
|
None => return Err(ProtocolError::Overflow),
|
||||||
|
};
|
||||||
|
|
||||||
|
if src.capacity() < required_cap {
|
||||||
|
src.reserve(required_cap - src.capacity());
|
||||||
}
|
}
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@@ -402,4 +412,14 @@ mod tests {
|
|||||||
Parser::write_close(&mut buf, None, false);
|
Parser::write_close(&mut buf, None, false);
|
||||||
assert_eq!(&buf[..], &vec![0x88, 0x00][..]);
|
assert_eq!(&buf[..], &vec![0x88, 0x00][..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_length_overflow() {
|
||||||
|
let buf: [u8; 14] = [
|
||||||
|
0x0a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xeb, 0x0e, 0x8f,
|
||||||
|
];
|
||||||
|
let mut buf = BytesMut::from(&buf[..]);
|
||||||
|
let result = Parser::parse(&mut buf, true, 65536);
|
||||||
|
assert!(matches!(result, Err(ProtocolError::Overflow)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,6 @@ edition.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
@@ -14,7 +14,6 @@ license.workspace = true
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|
||||||
[package.metadata.cargo_check_external_types]
|
[package.metadata.cargo_check_external_types]
|
||||||
|
@@ -24,9 +24,10 @@ Due to additional requirements for `multipart/form-data` requests, the higher le
|
|||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use actix_web::{post, App, HttpServer, Responder};
|
use actix_multipart::form::{
|
||||||
|
json::Json as MpJson, tempfile::TempFile, MultipartForm, MultipartFormConfig,
|
||||||
use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm};
|
};
|
||||||
|
use actix_web::{middleware::Logger, post, App, HttpServer, Responder};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -36,25 +37,37 @@ struct Metadata {
|
|||||||
|
|
||||||
#[derive(Debug, MultipartForm)]
|
#[derive(Debug, MultipartForm)]
|
||||||
struct UploadForm {
|
struct UploadForm {
|
||||||
|
// Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
|
||||||
#[multipart(limit = "100MB")]
|
#[multipart(limit = "100MB")]
|
||||||
file: TempFile,
|
file: TempFile,
|
||||||
json: MpJson<Metadata>,
|
json: MpJson<Metadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/videos")]
|
#[post("/videos")]
|
||||||
pub async fn post_video(MultipartForm(form): MultipartForm<UploadForm>) -> impl Responder {
|
async fn post_video(MultipartForm(form): MultipartForm<UploadForm>) -> impl Responder {
|
||||||
format!(
|
format!(
|
||||||
"Uploaded file {}, with size: {}",
|
"Uploaded file {}, with size: {}\ntemporary file ({}) was deleted\n",
|
||||||
form.json.name, form.file.size
|
form.json.name,
|
||||||
|
form.file.size,
|
||||||
|
form.file.file.path().display(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
HttpServer::new(move || App::new().service(post_video))
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
.bind(("127.0.0.1", 8080))?
|
|
||||||
.run()
|
HttpServer::new(move || {
|
||||||
.await
|
App::new()
|
||||||
|
.service(post_video)
|
||||||
|
.wrap(Logger::default())
|
||||||
|
// Also increase the global total limit to 100MiB.
|
||||||
|
.app_data(MultipartFormConfig::default().total_limit(100 * 1024 * 1024))
|
||||||
|
})
|
||||||
|
.workers(2)
|
||||||
|
.bind(("127.0.0.1", 8080))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm};
|
use actix_multipart::form::{
|
||||||
|
json::Json as MpJson, tempfile::TempFile, MultipartForm, MultipartFormConfig,
|
||||||
|
};
|
||||||
use actix_web::{middleware::Logger, post, App, HttpServer, Responder};
|
use actix_web::{middleware::Logger, post, App, HttpServer, Responder};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
@@ -9,6 +11,7 @@ struct Metadata {
|
|||||||
|
|
||||||
#[derive(Debug, MultipartForm)]
|
#[derive(Debug, MultipartForm)]
|
||||||
struct UploadForm {
|
struct UploadForm {
|
||||||
|
// Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
|
||||||
#[multipart(limit = "100MB")]
|
#[multipart(limit = "100MB")]
|
||||||
file: TempFile,
|
file: TempFile,
|
||||||
json: MpJson<Metadata>,
|
json: MpJson<Metadata>,
|
||||||
@@ -28,9 +31,15 @@ async fn post_video(MultipartForm(form): MultipartForm<UploadForm>) -> impl Resp
|
|||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
|
|
||||||
HttpServer::new(move || App::new().service(post_video).wrap(Logger::default()))
|
HttpServer::new(move || {
|
||||||
.workers(2)
|
App::new()
|
||||||
.bind(("127.0.0.1", 8080))?
|
.service(post_video)
|
||||||
.run()
|
.wrap(Logger::default())
|
||||||
.await
|
// Also increase the global total limit to 100MiB.
|
||||||
|
.app_data(MultipartFormConfig::default().total_limit(100 * 1024 * 1024))
|
||||||
|
})
|
||||||
|
.workers(2)
|
||||||
|
.bind(("127.0.0.1", 8080))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,7 @@
|
|||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! use actix_web::{post, App, HttpServer, Responder};
|
//! use actix_web::{post, App, HttpServer, Responder};
|
||||||
//!
|
//!
|
||||||
//! use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm};
|
//! use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm, MultipartFormConfig};
|
||||||
//! use serde::Deserialize;
|
//! use serde::Deserialize;
|
||||||
//!
|
//!
|
||||||
//! #[derive(Debug, Deserialize)]
|
//! #[derive(Debug, Deserialize)]
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
//!
|
//!
|
||||||
//! #[derive(Debug, MultipartForm)]
|
//! #[derive(Debug, MultipartForm)]
|
||||||
//! struct UploadForm {
|
//! struct UploadForm {
|
||||||
|
//! // Note: the form is also subject to the global limits configured using `MultipartFormConfig`.
|
||||||
//! #[multipart(limit = "100MB")]
|
//! #[multipart(limit = "100MB")]
|
||||||
//! file: TempFile,
|
//! file: TempFile,
|
||||||
//! json: MpJson<Metadata>,
|
//! json: MpJson<Metadata>,
|
||||||
@@ -38,10 +39,15 @@
|
|||||||
//!
|
//!
|
||||||
//! #[actix_web::main]
|
//! #[actix_web::main]
|
||||||
//! async fn main() -> std::io::Result<()> {
|
//! async fn main() -> std::io::Result<()> {
|
||||||
//! HttpServer::new(move || App::new().service(post_video))
|
//! HttpServer::new(move || {
|
||||||
//! .bind(("127.0.0.1", 8080))?
|
//! App::new()
|
||||||
//! .run()
|
//! .service(post_video)
|
||||||
//! .await
|
//! // Also increase the global total limit to 100MiB.
|
||||||
|
//! .app_data(MultipartFormConfig::default().total_limit(100 * 1024 * 1024))
|
||||||
|
//! })
|
||||||
|
//! .bind(("127.0.0.1", 8080))?
|
||||||
|
//! .run()
|
||||||
|
//! .await
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
|
@@ -13,6 +13,7 @@ macro_rules! register {
|
|||||||
register!(finish => "(.*)", "(.*)", "(.*)", "(.*)")
|
register!(finish => "(.*)", "(.*)", "(.*)", "(.*)")
|
||||||
}};
|
}};
|
||||||
(finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{
|
(finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{
|
||||||
|
#[expect(clippy::useless_concat)]
|
||||||
let arr = [
|
let arr = [
|
||||||
concat!("/authorizations"),
|
concat!("/authorizations"),
|
||||||
concat!("/authorizations/", $p1),
|
concat!("/authorizations/", $p1),
|
||||||
|
@@ -24,7 +24,7 @@ allowed_external_types = [
|
|||||||
actix = { version = ">=0.12, <0.14", default-features = false }
|
actix = { version = ">=0.12, <0.14", default-features = false }
|
||||||
actix-codec = "0.5"
|
actix-codec = "0.5"
|
||||||
actix-http = "3"
|
actix-http = "3"
|
||||||
actix-web = { version = "4", default-features = false }
|
actix-web = { version = "4", default-features = false, features = ["ws"] }
|
||||||
|
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
bytestring = "1"
|
bytestring = "1"
|
||||||
|
@@ -59,6 +59,7 @@ macro_rules! standard_method_type {
|
|||||||
(
|
(
|
||||||
$($variant:ident, $upper:ident, $lower:ident,)+
|
$($variant:ident, $upper:ident, $lower:ident,)+
|
||||||
) => {
|
) => {
|
||||||
|
#[doc(hidden)]
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum MethodType {
|
pub enum MethodType {
|
||||||
$(
|
$(
|
||||||
@@ -466,7 +467,7 @@ impl ToTokens for Route {
|
|||||||
|
|
||||||
let stream = quote! {
|
let stream = quote! {
|
||||||
#(#doc_attributes)*
|
#(#doc_attributes)*
|
||||||
#[allow(non_camel_case_types, missing_docs)]
|
#[allow(non_camel_case_types)]
|
||||||
#vis struct #name;
|
#vis struct #name;
|
||||||
|
|
||||||
impl ::actix_web::dev::HttpServiceFactory for #name {
|
impl ::actix_web::dev::HttpServiceFactory for #name {
|
||||||
|
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- `actix_web::response::builder::HttpResponseBuilder::streaming()` now sets `Content-Type` to `application/octet-stream` if `Content-Type` does not exist.
|
||||||
|
- `actix_web::response::builder::HttpResponseBuilder::streaming()` now calls `actix_web::response::builder::HttpResponseBuilder::no_chunking()` if `Content-Length` is set by user.
|
||||||
|
- Add `ws` crate feature (on-by-default) which forwards to `actix-http` and guards some of its `ResponseError` impls.
|
||||||
|
|
||||||
## 4.11.0
|
## 4.11.0
|
||||||
|
|
||||||
- Add `Logger::log_level()` method.
|
- Add `Logger::log_level()` method.
|
||||||
|
@@ -17,7 +17,6 @@ edition.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
features = [
|
features = [
|
||||||
"macros",
|
"macros",
|
||||||
"openssl",
|
"openssl",
|
||||||
@@ -68,6 +67,7 @@ default = [
|
|||||||
"http2",
|
"http2",
|
||||||
"unicode",
|
"unicode",
|
||||||
"compat",
|
"compat",
|
||||||
|
"ws",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Brotli algorithm content-encoding support
|
# Brotli algorithm content-encoding support
|
||||||
@@ -86,9 +86,12 @@ cookies = ["dep:cookie"]
|
|||||||
# Secure & signed cookies
|
# Secure & signed cookies
|
||||||
secure-cookies = ["cookies", "cookie/secure"]
|
secure-cookies = ["cookies", "cookie/secure"]
|
||||||
|
|
||||||
# HTTP/2 support (including h2c).
|
# HTTP/2 support (including h2c)
|
||||||
http2 = ["actix-http/http2"]
|
http2 = ["actix-http/http2"]
|
||||||
|
|
||||||
|
# WebSocket support
|
||||||
|
ws = ["actix-http/ws"]
|
||||||
|
|
||||||
# TLS via OpenSSL
|
# TLS via OpenSSL
|
||||||
openssl = ["__tls", "http2", "actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
|
openssl = ["__tls", "http2", "actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
|
||||||
|
|
||||||
@@ -132,7 +135,7 @@ actix-service = "2"
|
|||||||
actix-tls = { version = "3.4", default-features = false, optional = true }
|
actix-tls = { version = "3.4", default-features = false, optional = true }
|
||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
|
|
||||||
actix-http = { version = "3.11", features = ["ws"] }
|
actix-http = "3.11"
|
||||||
actix-router = { version = "0.5.3", default-features = false, features = ["http"] }
|
actix-router = { version = "0.5.3", default-features = false, features = ["http"] }
|
||||||
actix-web-codegen = { version = "4.3", optional = true, default-features = false }
|
actix-web-codegen = { version = "4.3", optional = true, default-features = false }
|
||||||
|
|
||||||
@@ -158,7 +161,7 @@ serde = "1.0"
|
|||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
smallvec = "1.6.1"
|
smallvec = "1.6.1"
|
||||||
socket2 = "0.5"
|
socket2 = "0.6"
|
||||||
time = { version = "0.3", default-features = false, features = ["formatting"] }
|
time = { version = "0.3", default-features = false, features = ["formatting"] }
|
||||||
tracing = "0.1.30"
|
tracing = "0.1.30"
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
|
@@ -3,7 +3,6 @@
|
|||||||
- The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
|
- The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
|
||||||
|
|
||||||
- Cookie handling has been offloaded to the `cookie` crate:
|
- Cookie handling has been offloaded to the `cookie` crate:
|
||||||
|
|
||||||
- `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs.
|
- `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs.
|
||||||
- Some types now require lifetime parameters.
|
- Some types now require lifetime parameters.
|
||||||
|
|
||||||
|
@@ -7,7 +7,6 @@ use std::{
|
|||||||
io::{self, Write as _},
|
io::{self, Write as _},
|
||||||
};
|
};
|
||||||
|
|
||||||
use actix_http::Response;
|
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -126,20 +125,24 @@ impl ResponseError for actix_http::error::PayloadError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for actix_http::ws::ProtocolError {}
|
|
||||||
|
|
||||||
impl ResponseError for actix_http::error::ContentTypeError {
|
impl ResponseError for actix_http::error::ContentTypeError {
|
||||||
fn status_code(&self) -> StatusCode {
|
fn status_code(&self) -> StatusCode {
|
||||||
StatusCode::BAD_REQUEST
|
StatusCode::BAD_REQUEST
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ws")]
|
||||||
impl ResponseError for actix_http::ws::HandshakeError {
|
impl ResponseError for actix_http::ws::HandshakeError {
|
||||||
fn error_response(&self) -> HttpResponse<BoxBody> {
|
fn error_response(&self) -> HttpResponse<BoxBody> {
|
||||||
Response::from(self).map_into_boxed_body().into()
|
actix_http::Response::from(self)
|
||||||
|
.map_into_boxed_body()
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ws")]
|
||||||
|
impl ResponseError for actix_http::ws::ProtocolError {}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@@ -2,16 +2,80 @@
|
|||||||
|
|
||||||
## What Is A Middleware?
|
## What Is A Middleware?
|
||||||
|
|
||||||
|
Middleware in Actix Web is a powerful mechanism that allows you to add additional behavior to request/response processing. It enables you to:
|
||||||
|
|
||||||
|
- Pre-process incoming requests (e.g., path normalization, authentication)
|
||||||
|
- Post-process outgoing responses (e.g., logging, compression)
|
||||||
|
- Modify application state through ServiceRequest
|
||||||
|
- Access external services (e.g., sessions, caching)
|
||||||
|
|
||||||
|
Middleware is registered for each App, Scope, or Resource and executed in the reverse order of registration. This means the last registered middleware is the first to process the request.
|
||||||
|
|
||||||
## Middleware Traits
|
## Middleware Traits
|
||||||
|
|
||||||
|
Actix Web's middleware system is built on two main traits:
|
||||||
|
|
||||||
|
1. `Transform<S, Req>`: The builder trait that creates the actual Service. It's responsible for:
|
||||||
|
|
||||||
|
- Creating new middleware instances
|
||||||
|
- Assembling the middleware chain
|
||||||
|
- Handling initialization errors
|
||||||
|
|
||||||
|
2. `Service<Req>`: The trait that represents the actual middleware functionality. It:
|
||||||
|
- Processes requests and responses
|
||||||
|
- Can modify both request and response
|
||||||
|
- Can short-circuit request processing
|
||||||
|
- Must be implemented for the middleware to work
|
||||||
|
|
||||||
## Understanding Body Types
|
## Understanding Body Types
|
||||||
|
|
||||||
|
When working with middleware, it's important to understand body types:
|
||||||
|
|
||||||
|
- Middleware can work with different body types for requests and responses
|
||||||
|
- The `MessageBody` trait is used to handle different body types
|
||||||
|
- You can use `EitherBody` when you need to handle multiple body types
|
||||||
|
- Be careful with body consumption - once a body is consumed, it cannot be read again
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
|
1. Keep middleware focused and single-purpose
|
||||||
|
2. Handle errors appropriately and propagate them correctly
|
||||||
|
3. Be mindful of performance impact
|
||||||
|
4. Use appropriate body types and handle them correctly
|
||||||
|
5. Consider middleware ordering carefully
|
||||||
|
6. Document your middleware's behavior and requirements
|
||||||
|
7. Test your middleware thoroughly
|
||||||
|
|
||||||
## Error Propagation
|
## Error Propagation
|
||||||
|
|
||||||
|
Proper error handling is crucial in middleware:
|
||||||
|
|
||||||
|
1. Always propagate errors from the inner service
|
||||||
|
2. Use appropriate error types
|
||||||
|
3. Handle initialization errors
|
||||||
|
4. Consider using custom error types for specific middleware errors
|
||||||
|
5. Document error conditions and handling
|
||||||
|
|
||||||
## When To (Not) Use Middleware
|
## When To (Not) Use Middleware
|
||||||
|
|
||||||
|
Use middleware when you need to:
|
||||||
|
|
||||||
|
- Add cross-cutting concerns
|
||||||
|
- Modify requests/responses globally
|
||||||
|
- Add authentication/authorization
|
||||||
|
- Add logging or monitoring
|
||||||
|
- Handle compression or caching
|
||||||
|
|
||||||
|
Avoid middleware when:
|
||||||
|
|
||||||
|
- The functionality is specific to a single route
|
||||||
|
- The operation is better handled by a service
|
||||||
|
- The overhead would be too high
|
||||||
|
- The functionality can be implemented more simply
|
||||||
|
|
||||||
## Author's References
|
## Author's References
|
||||||
|
|
||||||
- `EitherBody` + when is middleware appropriate: https://discord.com/channels/771444961383153695/952016890723729428
|
- `EitherBody` + when is middleware appropriate: https://discord.com/channels/771444961383153695/952016890723729428
|
||||||
|
- Actix Web Documentation: https://docs.rs/actix-web
|
||||||
|
- Service Trait Documentation: https://docs.rs/actix-service
|
||||||
|
- MessageBody Trait Documentation: https://docs.rs/actix-web/latest/actix_web/body/trait.MessageBody.html
|
||||||
|
@@ -318,12 +318,33 @@ impl HttpResponseBuilder {
|
|||||||
/// Set a streaming body and build the `HttpResponse`.
|
/// Set a streaming body and build the `HttpResponse`.
|
||||||
///
|
///
|
||||||
/// `HttpResponseBuilder` can not be used after this call.
|
/// `HttpResponseBuilder` can not be used after this call.
|
||||||
|
///
|
||||||
|
/// If `Content-Type` is not set, then it is automatically set to `application/octet-stream`.
|
||||||
|
///
|
||||||
|
/// If `Content-Length` is set, then [`no_chunking()`](Self::no_chunking) is automatically called.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn streaming<S, E>(&mut self, stream: S) -> HttpResponse
|
pub fn streaming<S, E>(&mut self, stream: S) -> HttpResponse
|
||||||
where
|
where
|
||||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||||
E: Into<BoxError> + 'static,
|
E: Into<BoxError> + 'static,
|
||||||
{
|
{
|
||||||
|
// Set mime type to application/octet-stream if it is not set
|
||||||
|
if let Some(parts) = self.inner() {
|
||||||
|
if !parts.headers.contains_key(header::CONTENT_TYPE) {
|
||||||
|
self.insert_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(parts) = self.inner() {
|
||||||
|
if let Some(length) = parts.headers.get(header::CONTENT_LENGTH) {
|
||||||
|
if let Ok(length) = length.to_str() {
|
||||||
|
if let Ok(length) = length.parse::<u64>() {
|
||||||
|
self.no_chunking(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.body(BodyStream::new(stream))
|
self.body(BodyStream::new(stream))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,7 +6,8 @@ use crate::{HttpResponse, HttpResponseBuilder};
|
|||||||
|
|
||||||
macro_rules! static_resp {
|
macro_rules! static_resp {
|
||||||
($name:ident, $status:expr) => {
|
($name:ident, $status:expr) => {
|
||||||
#[allow(non_snake_case, missing_docs)]
|
#[allow(non_snake_case)]
|
||||||
|
#[doc = concat!("Creates a new response builder with the status code `", stringify!($status), "`.")]
|
||||||
pub fn $name() -> HttpResponseBuilder {
|
pub fn $name() -> HttpResponseBuilder {
|
||||||
HttpResponseBuilder::new($status)
|
HttpResponseBuilder::new($status)
|
||||||
}
|
}
|
||||||
|
@@ -616,7 +616,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
let (req, mut pl) = TestRequest::default()
|
let (mut req, mut pl) = TestRequest::default()
|
||||||
.insert_header((
|
.insert_header((
|
||||||
header::CONTENT_TYPE,
|
header::CONTENT_TYPE,
|
||||||
header::HeaderValue::from_static("application/json"),
|
header::HeaderValue::from_static("application/json"),
|
||||||
@@ -624,6 +624,7 @@ mod tests {
|
|||||||
.set_payload(Bytes::from_static(&[0u8; 1000]))
|
.set_payload(Bytes::from_static(&[0u8; 1000]))
|
||||||
.to_http_parts();
|
.to_http_parts();
|
||||||
|
|
||||||
|
req.head_mut().headers_mut().remove(header::CONTENT_LENGTH);
|
||||||
let json = JsonBody::<MyObject>::new(&req, &mut pl, None, true)
|
let json = JsonBody::<MyObject>::new(&req, &mut pl, None, true)
|
||||||
.limit(100)
|
.limit(100)
|
||||||
.await;
|
.await;
|
||||||
|
@@ -38,7 +38,7 @@ use crate::{
|
|||||||
///
|
///
|
||||||
/// A dynamic segment is specified in the form `{identifier}`, where the identifier can be used
|
/// A dynamic segment is specified in the form `{identifier}`, where the identifier can be used
|
||||||
/// later in a request handler to access the matched value for that segment. This is done by looking
|
/// later in a request handler to access the matched value for that segment. This is done by looking
|
||||||
/// up the identifier in the `Path` object returned by [`HttpRequest.match_info()`] method.
|
/// up the identifier in the `Path` object returned by [`HttpRequest::match_info()`](crate::HttpRequest::match_info) method.
|
||||||
///
|
///
|
||||||
/// By default, each segment matches the regular expression `[^{}/]+`.
|
/// By default, each segment matches the regular expression `[^{}/]+`.
|
||||||
///
|
///
|
||||||
|
115
actix-web/tests/test_streaming_response.rs
Normal file
115
actix-web/tests/test_streaming_response.rs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
use std::{
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_web::{
|
||||||
|
http::header::{self, HeaderValue},
|
||||||
|
HttpResponse,
|
||||||
|
};
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures_core::Stream;
|
||||||
|
|
||||||
|
struct FixedSizeStream {
|
||||||
|
data: Vec<u8>,
|
||||||
|
yielded: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FixedSizeStream {
|
||||||
|
fn new(size: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
data: vec![0u8; size],
|
||||||
|
yielded: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stream for FixedSizeStream {
|
||||||
|
type Item = Result<Bytes, std::io::Error>;
|
||||||
|
|
||||||
|
fn poll_next(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
|
if self.yielded {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
self.yielded = true;
|
||||||
|
let data = std::mem::take(&mut self.data);
|
||||||
|
Poll::Ready(Some(Ok(Bytes::from(data))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_streaming_response_with_content_length() {
|
||||||
|
let stream = FixedSizeStream::new(100);
|
||||||
|
|
||||||
|
let resp = HttpResponse::Ok()
|
||||||
|
.append_header((header::CONTENT_LENGTH, "100"))
|
||||||
|
.streaming(stream);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(header::CONTENT_LENGTH),
|
||||||
|
Some(&HeaderValue::from_static("100")),
|
||||||
|
"Content-Length should be preserved when explicitly set"
|
||||||
|
);
|
||||||
|
|
||||||
|
let has_chunked = resp
|
||||||
|
.headers()
|
||||||
|
.get(header::TRANSFER_ENCODING)
|
||||||
|
.map(|v| v.to_str().unwrap_or(""))
|
||||||
|
.unwrap_or("")
|
||||||
|
.contains("chunked");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!has_chunked,
|
||||||
|
"chunked should not be used when Content-Length is provided"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(header::CONTENT_TYPE),
|
||||||
|
Some(&HeaderValue::from_static("application/octet-stream")),
|
||||||
|
"Content-Type should default to application/octet-stream"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_streaming_response_default_content_type() {
|
||||||
|
let stream = FixedSizeStream::new(50);
|
||||||
|
|
||||||
|
let resp = HttpResponse::Ok().streaming(stream);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(header::CONTENT_TYPE),
|
||||||
|
Some(&HeaderValue::from_static("application/octet-stream")),
|
||||||
|
"Content-Type should default to application/octet-stream"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_streaming_response_user_defined_content_type() {
|
||||||
|
let stream = FixedSizeStream::new(25);
|
||||||
|
|
||||||
|
let resp = HttpResponse::Ok()
|
||||||
|
.insert_header((header::CONTENT_TYPE, "text/plain"))
|
||||||
|
.streaming(stream);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(header::CONTENT_TYPE),
|
||||||
|
Some(&HeaderValue::from_static("text/plain")),
|
||||||
|
"User-defined Content-Type should be preserved"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_streaming_response_empty_stream() {
|
||||||
|
let stream = FixedSizeStream::new(0);
|
||||||
|
|
||||||
|
let resp = HttpResponse::Ok()
|
||||||
|
.append_header((header::CONTENT_LENGTH, "0"))
|
||||||
|
.streaming(stream);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(header::CONTENT_LENGTH),
|
||||||
|
Some(&HeaderValue::from_static("0")),
|
||||||
|
"Content-Length 0 should be preserved for empty streams"
|
||||||
|
);
|
||||||
|
}
|
@@ -16,7 +16,6 @@ license = "MIT OR Apache-2.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
features = [
|
features = [
|
||||||
"cookies",
|
"cookies",
|
||||||
"openssl",
|
"openssl",
|
||||||
@@ -109,7 +108,7 @@ cfg-if = "1"
|
|||||||
derive_more = { version = "2", features = ["display", "error", "from"] }
|
derive_more = { version = "2", features = ["display", "error", "from"] }
|
||||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||||
futures-util = { version = "0.3.17", default-features = false, features = ["alloc", "sink"] }
|
futures-util = { version = "0.3.17", default-features = false, features = ["alloc", "sink"] }
|
||||||
h2 = "0.3.26"
|
h2 = "0.3.27"
|
||||||
http = "0.2.7"
|
http = "0.2.7"
|
||||||
itoa = "1"
|
itoa = "1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
8
justfile
8
justfile
@@ -13,6 +13,8 @@ fmt:
|
|||||||
[private]
|
[private]
|
||||||
downgrade-for-msrv:
|
downgrade-for-msrv:
|
||||||
cargo {{ toolchain }} update -p=divan --precise=0.1.15 # next ver: 1.80.0
|
cargo {{ toolchain }} update -p=divan --precise=0.1.15 # next ver: 1.80.0
|
||||||
|
cargo {{ toolchain }} update -p=rayon --precise=1.10.0 # next ver: 1.80.0
|
||||||
|
cargo {{ toolchain }} update -p=rayon-core --precise=1.12.1 # next ver: 1.80.0
|
||||||
cargo {{ toolchain }} update -p=half --precise=2.4.1 # next ver: 1.81.0
|
cargo {{ toolchain }} update -p=half --precise=2.4.1 # next ver: 1.81.0
|
||||||
cargo {{ toolchain }} update -p=idna_adapter --precise=1.2.0 # next ver: 1.82.0
|
cargo {{ toolchain }} update -p=idna_adapter --precise=1.2.0 # next ver: 1.82.0
|
||||||
cargo {{ toolchain }} update -p=litemap --precise=0.7.4 # next ver: 1.81.0
|
cargo {{ toolchain }} update -p=litemap --precise=0.7.4 # next ver: 1.81.0
|
||||||
@@ -50,8 +52,7 @@ clippy:
|
|||||||
cargo {{ toolchain }} clippy --workspace --all-targets {{ all_crate_features }}
|
cargo {{ toolchain }} clippy --workspace --all-targets {{ all_crate_features }}
|
||||||
|
|
||||||
# Run Clippy over workspace using MSRV.
|
# Run Clippy over workspace using MSRV.
|
||||||
clippy-msrv:
|
clippy-msrv: downgrade-for-msrv
|
||||||
@just toolchain={{ msrv_rustup }} downgrade-for-msrv
|
|
||||||
@just toolchain={{ msrv_rustup }} clippy
|
@just toolchain={{ msrv_rustup }} clippy
|
||||||
|
|
||||||
# Test workspace code.
|
# Test workspace code.
|
||||||
@@ -62,8 +63,7 @@ test:
|
|||||||
cargo {{ toolchain }} nextest run --no-tests=warn --workspace --exclude=actix-web-codegen --exclude=actix-multipart-derive {{ all_crate_features }} --filter-expr="not test(test_reading_deflate_encoding_large_random_rustls)"
|
cargo {{ toolchain }} nextest run --no-tests=warn --workspace --exclude=actix-web-codegen --exclude=actix-multipart-derive {{ all_crate_features }} --filter-expr="not test(test_reading_deflate_encoding_large_random_rustls)"
|
||||||
|
|
||||||
# Test workspace using MSRV.
|
# Test workspace using MSRV.
|
||||||
test-msrv:
|
test-msrv: downgrade-for-msrv
|
||||||
@just toolchain={{ msrv_rustup }} downgrade-for-msrv
|
|
||||||
@just toolchain={{ msrv_rustup }} test
|
@just toolchain={{ msrv_rustup }} test
|
||||||
|
|
||||||
# Test workspace docs.
|
# Test workspace docs.
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# developed on macOS and probably doesn't work on Linux yet due to minor
|
# developed on macOS and probably doesn't work on Linux yet due to minor
|
||||||
# differences in flags on sed
|
# differences in flags on sed
|
||||||
|
@@ -1,38 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# run tests matching what CI does for non-linux feature sets
|
|
||||||
|
|
||||||
set -x
|
|
||||||
|
|
||||||
EXIT=0
|
|
||||||
|
|
||||||
save_exit_code() {
|
|
||||||
eval $@
|
|
||||||
local CMD_EXIT=$?
|
|
||||||
[ "$CMD_EXIT" = "0" ] || EXIT=$CMD_EXIT
|
|
||||||
}
|
|
||||||
|
|
||||||
save_exit_code cargo test --lib --tests -p=actix-router --all-features -- --nocapture
|
|
||||||
save_exit_code cargo test --lib --tests -p=actix-http --all-features -- --nocapture
|
|
||||||
save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --nocapture
|
|
||||||
save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features -- --nocapture
|
|
||||||
save_exit_code cargo test --lib --tests -p=awc --all-features -- --nocapture
|
|
||||||
save_exit_code cargo test --lib --tests -p=actix-http-test --all-features -- --nocapture
|
|
||||||
save_exit_code cargo test --lib --tests -p=actix-test --all-features -- --nocapture
|
|
||||||
save_exit_code cargo test --lib --tests -p=actix-files -- --nocapture
|
|
||||||
save_exit_code cargo test --lib --tests -p=actix-multipart --all-features -- --nocapture
|
|
||||||
save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features -- --nocapture
|
|
||||||
|
|
||||||
save_exit_code cargo test --workspace --doc
|
|
||||||
|
|
||||||
if [ "$EXIT" = "0" ]; then
|
|
||||||
PASSED="All tests passed!"
|
|
||||||
|
|
||||||
if [ "$(command -v figlet)" ]; then
|
|
||||||
figlet "$PASSED"
|
|
||||||
else
|
|
||||||
echo "$PASSED"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit $EXIT
|
|
25
scripts/publish
Executable file
25
scripts/publish
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Euo pipefail
|
||||||
|
|
||||||
|
for dir in $@; do
|
||||||
|
cd "$dir"
|
||||||
|
|
||||||
|
cargo publish --dry-run
|
||||||
|
|
||||||
|
read -p "Look okay? "
|
||||||
|
read -p "Sure? "
|
||||||
|
|
||||||
|
cargo publish
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo
|
||||||
|
read -p "Was the above error caused by cyclic dev-deps? Choosing yes will publish without a git backreference. (y/N) " publish_no_dev_deps
|
||||||
|
|
||||||
|
if [[ "$publish_no_dev_deps" == "y" || "$publish_no_dev_deps" == "Y" ]]; then
|
||||||
|
cargo hack --no-dev-deps publish --allow-dirty
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
done
|
Reference in New Issue
Block a user