mirror of
https://github.com/fafhrd91/actix-web
synced 2025-08-19 20:35:36 +02:00
Compare commits
269 Commits
awc-v3.0.0
...
awc-v3.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5469b02638 | ||
|
|
a66cd38ec5 | ||
|
|
20609e93fd | ||
|
|
bf282472ab | ||
|
|
7f4b44c258 | ||
|
|
66243717b3 | ||
|
|
102720d398 | ||
|
|
c3c7eb8df9 | ||
|
|
21f57caf4a | ||
|
|
47f5faf26e | ||
|
|
9777653dc0 | ||
|
|
9fde5b30db | ||
|
|
fd412a8223 | ||
|
|
cd511affd5 | ||
|
|
3200de3f34 | ||
|
|
b3e84b5c4b | ||
|
|
a3416112a5 | ||
|
|
21a08ca796 | ||
|
|
a9f497d05f | ||
|
|
cc9ba162f7 | ||
|
|
37799df978 | ||
|
|
0d93a8c273 | ||
|
|
3ae4f0a629 | ||
|
|
14a4f325d3 | ||
|
|
1bd2076b35 | ||
|
|
5454699bab | ||
|
|
d7c5c966d2 | ||
|
|
50894e392e | ||
|
|
008753f07a | ||
|
|
c92aa31f91 | ||
|
|
c25dd23820 | ||
|
|
acacb90b2e | ||
|
|
8459f566a8 | ||
|
|
232a14dc8b | ||
|
|
6e9f5fba24 | ||
|
|
c5d6df0078 | ||
|
|
8865540f3b | ||
|
|
141790b200 | ||
|
|
9668a2396f | ||
|
|
cb7347216c | ||
|
|
ae7f71e317 | ||
|
|
bc89f0bfc2 | ||
|
|
c959916346 | ||
|
|
f227e880d7 | ||
|
|
68ad81f989 | ||
|
|
f2e736719a | ||
|
|
81ef12a0fd | ||
|
|
1bc1538118 | ||
|
|
1cc3e7b24c | ||
|
|
3dd98c308c | ||
|
|
cb5d9a7e64 | ||
|
|
5ee555462f | ||
|
|
ad159f5219 | ||
|
|
2ffc21dd4f | ||
|
|
7b8a392ef5 | ||
|
|
3c7ccf5521 | ||
|
|
e7cae5a95b | ||
|
|
455d5c460d | ||
|
|
8faca783fa | ||
|
|
edbb9b047e | ||
|
|
32742d0715 | ||
|
|
d90c1a2331 | ||
|
|
2a12b41456 | ||
|
|
6c97d448b7 | ||
|
|
c3ce33df05 | ||
|
|
4431c8da65 | ||
|
|
2d11ab5977 | ||
|
|
4ebf16890d | ||
|
|
fe0bbfb3da | ||
|
|
2462b6dd5d | ||
|
|
49cfabeaf5 | ||
|
|
0f7292c69a | ||
|
|
8bbf2b5052 | ||
|
|
8c975bcc1f | ||
|
|
742ad56d30 | ||
|
|
bcc8d5c441 | ||
|
|
f659098d21 | ||
|
|
8621ae12f8 | ||
|
|
b338eb8473 | ||
|
|
5abd1c2c2c | ||
|
|
05336269f9 | ||
|
|
86df295ee2 | ||
|
|
85c9b1a263 | ||
|
|
577597a80a | ||
|
|
374dc9bfc9 | ||
|
|
93754f307f | ||
|
|
c7639bc3be | ||
|
|
0bc4ae9158 | ||
|
|
19a46e3925 | ||
|
|
68cd853aa2 | ||
|
|
25fe1bbaa5 | ||
|
|
e890307091 | ||
|
|
b708924590 | ||
|
|
5dcb250237 | ||
|
|
b4ff6addfe | ||
|
|
231a24ef8d | ||
|
|
6df4974234 | ||
|
|
a80e93d6db | ||
|
|
542c92c9a7 | ||
|
|
74738c63a7 | ||
|
|
a87e01f0d1 | ||
|
|
9779010a5a | ||
|
|
11d50d792b | ||
|
|
798e9911e9 | ||
|
|
2b2de29800 | ||
|
|
0f5c876c6b | ||
|
|
96a4dc9dec | ||
|
|
4616ca8ee6 | ||
|
|
36193b0a50 | ||
|
|
76684a786e | ||
|
|
2308f8afa4 | ||
|
|
554ae7a868 | ||
|
|
ac0c4eb684 | ||
|
|
2e493cf791 | ||
|
|
5860fe5381 | ||
|
|
adf9935841 | ||
|
|
34e5c7c799 | ||
|
|
01cbfc5724 | ||
|
|
3756dfc2ce | ||
|
|
d2590fd46c | ||
|
|
1296e07c48 | ||
|
|
7b1512d863 | ||
|
|
cd025f5c0b | ||
|
|
1769812d0b | ||
|
|
324eba7e0b | ||
|
|
b3ac918d70 | ||
|
|
de20d21703 | ||
|
|
212c6926f9 | ||
|
|
1ea619f2a1 | ||
|
|
40a0162074 | ||
|
|
f8488aff1e | ||
|
|
64c2e5e1cd | ||
|
|
17f636a183 | ||
|
|
2e00776d5e | ||
|
|
7d507a41ee | ||
|
|
fb036264cc | ||
|
|
d2b9724010 | ||
|
|
5c53db1e4d | ||
|
|
84ea9e7e88 | ||
|
|
0bd5ccc432 | ||
|
|
9cd8526085 | ||
|
|
73bbe56971 | ||
|
|
8340b63b7b | ||
|
|
6c2c7b68e2 | ||
|
|
7bf47967cc | ||
|
|
ae47d96fc6 | ||
|
|
5842a3279d | ||
|
|
1d6f5ba6d6 | ||
|
|
aa31086af5 | ||
|
|
57ea322ce5 | ||
|
|
2cf27863cb | ||
|
|
5359fa56c2 | ||
|
|
a2467718ac | ||
|
|
3c0d059d92 | ||
|
|
44b7302845 | ||
|
|
a6d5776481 | ||
|
|
156cc20ac8 | ||
|
|
dd4a372613 | ||
|
|
05255c7f7c | ||
|
|
fb091b2b88 | ||
|
|
11ee8ec3ab | ||
|
|
551a0d973c | ||
|
|
cea44be670 | ||
|
|
b41b346c00 | ||
|
|
5b0a50249b | ||
|
|
60b030ff53 | ||
|
|
fc4e9ff96b | ||
|
|
6481a5fb73 | ||
|
|
0cd7c17682 | ||
|
|
ed2f5b40b9 | ||
|
|
cc37be9700 | ||
|
|
e1cdabe5cb | ||
|
|
d0f4c809ca | ||
|
|
65dd5dfa7b | ||
|
|
f62383a975 | ||
|
|
f9348d7129 | ||
|
|
774ac7fec4 | ||
|
|
69fa17f66f | ||
|
|
816d68dee8 | ||
|
|
7dc034f0fb | ||
|
|
07f2fe385b | ||
|
|
406f694095 | ||
|
|
e49e559f47 | ||
|
|
d35b7644dc | ||
|
|
069cf2da07 | ||
|
|
6460e67f84 | ||
|
|
9587261c20 | ||
|
|
606a371ec3 | ||
|
|
bed72d9bb7 | ||
|
|
c596f573a6 | ||
|
|
627c0dc22f | ||
|
|
2d053b7036 | ||
|
|
59be0c65c6 | ||
|
|
e1a2d9c606 | ||
|
|
d89c706cd6 | ||
|
|
4c9ca7196d | ||
|
|
fa7f3e6908 | ||
|
|
c7c02ef99d | ||
|
|
a2d5c5a058 | ||
|
|
deece8d519 | ||
|
|
2a72bdae09 | ||
|
|
075d871e63 | ||
|
|
c4b20df56a | ||
|
|
0df275c478 | ||
|
|
697238fadc | ||
|
|
e045418038 | ||
|
|
a978b417f3 | ||
|
|
fa82b698b7 | ||
|
|
fc4cdf81eb | ||
|
|
654dc64a09 | ||
|
|
cf54388534 | ||
|
|
39243095b5 | ||
|
|
89c6d62656 | ||
|
|
52bbbd1d73 | ||
|
|
3e6e9779dc | ||
|
|
9bdd334bb4 | ||
|
|
bcbbc115aa | ||
|
|
ab5eb7c1aa | ||
|
|
18b8ef0765 | ||
|
|
b806b4773c | ||
|
|
0062d99b6f | ||
|
|
99e6a9c26d | ||
|
|
5f5bd2184e | ||
|
|
88e074879d | ||
|
|
e7987e7429 | ||
|
|
a172f5968d | ||
|
|
a2a42ec152 | ||
|
|
dd347e0bd0 | ||
|
|
194a691537 | ||
|
|
56ee97f722 | ||
|
|
66620a1012 | ||
|
|
e33618ed6d | ||
|
|
1fe309bcc6 | ||
|
|
168a7284d3 | ||
|
|
68a3acb9c2 | ||
|
|
84c6d25fd3 | ||
|
|
0a135c7dc9 | ||
|
|
668a33c793 | ||
|
|
d8cbb879dd | ||
|
|
13cf5a9e44 | ||
|
|
4df1cd78b7 | ||
|
|
e8a0e16863 | ||
|
|
a2f59c02f7 | ||
|
|
2754608f3c | ||
|
|
c020cedb63 | ||
|
|
5e554dca35 | ||
|
|
6ec2d7b909 | ||
|
|
ec6d284a8e | ||
|
|
be9530eb72 | ||
|
|
855e260fdb | ||
|
|
d13854505f | ||
|
|
d40b6748bc | ||
|
|
c79b9a0df3 | ||
|
|
4af414064b | ||
|
|
9abe166d52 | ||
|
|
c09ec6af4c | ||
|
|
37f2bf5625 | ||
|
|
4f6f0b0137 | ||
|
|
591abc37c3 | ||
|
|
ad22cc4e7f | ||
|
|
efdf3ab1c3 | ||
|
|
6b3ea4fc61 | ||
|
|
99985fc4ec | ||
|
|
a6707fb7ee | ||
|
|
a3806cde19 | ||
|
|
efefa0d0ce | ||
|
|
450ff5fa1d | ||
|
|
8ae278cb68 | ||
|
|
46699e3429 |
@@ -1,9 +1,14 @@
|
|||||||
[alias]
|
[alias]
|
||||||
chk = "check --workspace --all-features --tests --examples --bins"
|
lint = "clippy --workspace --tests --examples --bins -- -Dclippy::todo"
|
||||||
lint = "clippy --workspace --all-features --tests --examples --bins"
|
lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dclippy::todo"
|
||||||
ci-min = "hack check --workspace --no-default-features"
|
|
||||||
ci-min-test = "hack check --workspace --no-default-features --tests --examples"
|
# lib checking
|
||||||
ci-default = "check --workspace --bins --tests --examples"
|
ci-check-min = "hack --workspace check --no-default-features"
|
||||||
ci-full = "check --workspace --all-features --bins --tests --examples"
|
ci-check-default = "hack --workspace check"
|
||||||
ci-test = "test --workspace --all-features --lib --tests --no-fail-fast -- --nocapture"
|
ci-check-default-tests = "check --workspace --tests"
|
||||||
|
ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,io-uring check"
|
||||||
|
ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check"
|
||||||
|
|
||||||
|
# testing
|
||||||
|
ci-doctest-default = "test --workspace --doc --no-fail-fast -- --nocapture"
|
||||||
ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture"
|
ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture"
|
||||||
|
|||||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [robjtede]
|
||||||
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -33,5 +33,5 @@ Please search on the [Actix Web issue tracker](https://github.com/actix/actix-we
|
|||||||
## Your Environment
|
## Your Environment
|
||||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||||
|
|
||||||
* Rust Version (I.e, output of `rustc -V`):
|
- Rust Version (I.e, output of `rustc -V`):
|
||||||
* Actix Web Version:
|
- Actix Web Version:
|
||||||
|
|||||||
2
.github/workflows/bench.yml
vendored
2
.github/workflows/bench.yml
vendored
@@ -1,8 +1,6 @@
|
|||||||
name: Benchmark
|
name: Benchmark
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened]
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|||||||
153
.github/workflows/ci-master.yml
vendored
Normal file
153
.github/workflows/ci-master.yml
vendored
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
name: CI (master only)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_test_nightly:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
target:
|
||||||
|
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
|
||||||
|
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
||||||
|
- { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
|
||||||
|
version:
|
||||||
|
- nightly
|
||||||
|
|
||||||
|
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
||||||
|
runs-on: ${{ matrix.target.os }}
|
||||||
|
|
||||||
|
env:
|
||||||
|
CI: 1
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
VCPKGRS_DYNAMIC: 1
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# install OpenSSL on Windows
|
||||||
|
# TODO: GitHub actions docs state that OpenSSL is
|
||||||
|
# already installed on these Windows machines somewhere
|
||||||
|
- name: Set vcpkg root
|
||||||
|
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||||
|
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||||
|
- name: Install OpenSSL
|
||||||
|
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||||
|
run: vcpkg install openssl:x64-windows
|
||||||
|
|
||||||
|
- name: Install ${{ matrix.version }}
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Generate Cargo.lock
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: generate-lockfile }
|
||||||
|
- name: Cache Dependencies
|
||||||
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
|
- name: Install cargo-hack
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: install
|
||||||
|
args: cargo-hack
|
||||||
|
|
||||||
|
- name: check minimal
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: ci-check-min }
|
||||||
|
|
||||||
|
- name: check default
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: ci-check-default }
|
||||||
|
|
||||||
|
- name: tests
|
||||||
|
timeout-minutes: 60
|
||||||
|
run: |
|
||||||
|
cargo test --lib --tests -p=actix-router --all-features
|
||||||
|
cargo test --lib --tests -p=actix-http --all-features
|
||||||
|
cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
|
||||||
|
cargo test --lib --tests -p=actix-web-codegen --all-features
|
||||||
|
cargo test --lib --tests -p=awc --all-features
|
||||||
|
cargo test --lib --tests -p=actix-http-test --all-features
|
||||||
|
cargo test --lib --tests -p=actix-test --all-features
|
||||||
|
cargo test --lib --tests -p=actix-files
|
||||||
|
cargo test --lib --tests -p=actix-multipart --all-features
|
||||||
|
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||||
|
|
||||||
|
- name: tests (io-uring)
|
||||||
|
if: matrix.target.os == 'ubuntu-latest'
|
||||||
|
timeout-minutes: 60
|
||||||
|
run: >
|
||||||
|
sudo bash -c "ulimit -Sl 512
|
||||||
|
&& ulimit -Hl 512
|
||||||
|
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
||||||
|
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
|
||||||
|
|
||||||
|
- name: Clear the cargo caches
|
||||||
|
run: |
|
||||||
|
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
|
||||||
|
cargo-cache
|
||||||
|
|
||||||
|
ci_feature_powerset_check:
|
||||||
|
name: Verify Feature Combinations
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install stable
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable-x86_64-unknown-linux-gnu
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Generate Cargo.lock
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: generate-lockfile }
|
||||||
|
- name: Cache Dependencies
|
||||||
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
|
- name: Install cargo-hack
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: install
|
||||||
|
args: cargo-hack
|
||||||
|
|
||||||
|
- name: check feature combinations
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: ci-check-all-feature-powerset }
|
||||||
|
|
||||||
|
- name: check feature combinations
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: ci-check-all-feature-powerset-linux }
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
name: coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install stable
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable-x86_64-unknown-linux-gnu
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Generate Cargo.lock
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: generate-lockfile }
|
||||||
|
- name: Cache Dependencies
|
||||||
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
|
- name: Generate coverage file
|
||||||
|
run: |
|
||||||
|
cargo install cargo-tarpaulin --vers "^0.13"
|
||||||
|
cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose
|
||||||
|
- name: Upload to Codecov
|
||||||
|
uses: codecov/codecov-action@v1
|
||||||
|
with: { file: cobertura.xml }
|
||||||
74
.github/workflows/ci.yml
vendored
74
.github/workflows/ci.yml
vendored
@@ -14,11 +14,10 @@ jobs:
|
|||||||
target:
|
target:
|
||||||
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
|
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
|
||||||
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
||||||
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
|
- { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
|
||||||
version:
|
version:
|
||||||
- 1.51.0 # MSRV
|
- 1.54.0 # MSRV
|
||||||
- stable
|
- stable
|
||||||
- nightly
|
|
||||||
|
|
||||||
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
||||||
runs-on: ${{ matrix.target.os }}
|
runs-on: ${{ matrix.target.os }}
|
||||||
@@ -32,6 +31,8 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
# install OpenSSL on Windows
|
# install OpenSSL on Windows
|
||||||
|
# TODO: GitHub actions docs state that OpenSSL is
|
||||||
|
# already installed on these Windows machines somewhere
|
||||||
- name: Set vcpkg root
|
- name: Set vcpkg root
|
||||||
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||||
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
|
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||||
@@ -48,8 +49,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate Cargo.lock
|
- name: Generate Cargo.lock
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with: { command: generate-lockfile }
|
||||||
command: generate-lockfile
|
|
||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.2.0
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
@@ -61,43 +61,34 @@ jobs:
|
|||||||
|
|
||||||
- name: check minimal
|
- name: check minimal
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: ci-min }
|
with: { command: ci-check-min }
|
||||||
|
|
||||||
- name: check minimal + tests
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with: { command: ci-min-test }
|
|
||||||
|
|
||||||
- name: check default
|
- name: check default
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: ci-default }
|
with: { command: ci-check-default }
|
||||||
|
|
||||||
- name: check full
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with: { command: ci-full }
|
|
||||||
|
|
||||||
- name: tests
|
- name: tests
|
||||||
uses: actions-rs/cargo@v1
|
timeout-minutes: 60
|
||||||
timeout-minutes: 40
|
|
||||||
with:
|
|
||||||
command: ci-test
|
|
||||||
args: --skip=test_reading_deflate_encoding_large_random_rustls
|
|
||||||
|
|
||||||
- name: Generate coverage file
|
|
||||||
if: >
|
|
||||||
matrix.target.os == 'ubuntu-latest'
|
|
||||||
&& matrix.version == 'stable'
|
|
||||||
&& github.ref == 'refs/heads/master'
|
|
||||||
run: |
|
run: |
|
||||||
cargo install cargo-tarpaulin --vers "^0.13"
|
cargo test --lib --tests -p=actix-router --all-features
|
||||||
cargo tarpaulin --out Xml --verbose
|
cargo test --lib --tests -p=actix-http --all-features
|
||||||
- name: Upload to Codecov
|
cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
|
||||||
if: >
|
cargo test --lib --tests -p=actix-web-codegen --all-features
|
||||||
matrix.target.os == 'ubuntu-latest'
|
cargo test --lib --tests -p=awc --all-features
|
||||||
&& matrix.version == 'stable'
|
cargo test --lib --tests -p=actix-http-test --all-features
|
||||||
&& github.ref == 'refs/heads/master'
|
cargo test --lib --tests -p=actix-test --all-features
|
||||||
uses: codecov/codecov-action@v1
|
cargo test --lib --tests -p=actix-files
|
||||||
with:
|
cargo test --lib --tests -p=actix-multipart --all-features
|
||||||
file: cobertura.xml
|
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||||
|
|
||||||
|
- name: tests (io-uring)
|
||||||
|
if: matrix.target.os == 'ubuntu-latest'
|
||||||
|
timeout-minutes: 60
|
||||||
|
run: >
|
||||||
|
sudo bash -c "ulimit -Sl 512
|
||||||
|
&& ulimit -Hl 512
|
||||||
|
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
||||||
|
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
|
||||||
|
|
||||||
- name: Clear the cargo caches
|
- name: Clear the cargo caches
|
||||||
run: |
|
run: |
|
||||||
@@ -105,9 +96,8 @@ jobs:
|
|||||||
cargo-cache
|
cargo-cache
|
||||||
|
|
||||||
rustdoc:
|
rustdoc:
|
||||||
name: rustdoc
|
name: doc tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
@@ -124,13 +114,7 @@ jobs:
|
|||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.3.0
|
uses: Swatinem/rust-cache@v1.3.0
|
||||||
|
|
||||||
- name: Install cargo-hack
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: install
|
|
||||||
args: cargo-hack
|
|
||||||
|
|
||||||
- name: doc tests
|
- name: doc tests
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
timeout-minutes: 40
|
timeout-minutes: 60
|
||||||
with: { command: ci-doctest }
|
with: { command: ci-doctest }
|
||||||
|
|||||||
29
.github/workflows/clippy-fmt.yml
vendored
29
.github/workflows/clippy-fmt.yml
vendored
@@ -14,6 +14,7 @@ jobs:
|
|||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
- name: Check with rustfmt
|
- name: Check with rustfmt
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
@@ -30,10 +31,36 @@ jobs:
|
|||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
components: clippy
|
components: clippy
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
|
- name: Generate Cargo.lock
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: generate-lockfile }
|
||||||
|
- name: Cache Dependencies
|
||||||
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
- name: Check with Clippy
|
- name: Check with Clippy
|
||||||
uses: actions-rs/clippy-check@v1
|
uses: actions-rs/clippy-check@v1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
args: --workspace --all-features --tests
|
args: --workspace --tests --examples --all-features
|
||||||
|
|
||||||
|
lint-docs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Install Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
components: rust-docs
|
||||||
|
- name: Check for broken intra-doc links
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
env:
|
||||||
|
RUSTDOCFLAGS: "-D warnings"
|
||||||
|
with:
|
||||||
|
command: doc
|
||||||
|
args: --no-deps --all-features --workspace
|
||||||
|
|||||||
700
CHANGES.md
700
CHANGES.md
File diff suppressed because it is too large
Load Diff
@@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo
|
|||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment include:
|
Examples of behavior that contributes to creating a positive environment include:
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
- Using welcoming and inclusive language
|
||||||
* Being respectful of differing viewpoints and experiences
|
- Being respectful of differing viewpoints and experiences
|
||||||
* Gracefully accepting constructive criticism
|
- Gracefully accepting constructive criticism
|
||||||
* Focusing on what is best for the community
|
- Focusing on what is best for the community
|
||||||
* Showing empathy towards other community members
|
- Showing empathy towards other community members
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
- The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
- Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
- Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||||
|
|
||||||
## Our Responsibilities
|
## Our Responsibilities
|
||||||
|
|
||||||
|
|||||||
85
Cargo.toml
85
Cargo.toml
@@ -1,7 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-web"
|
name = "actix-web"
|
||||||
version = "4.0.0-beta.9"
|
version = "4.0.0-rc.1"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = [
|
||||||
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
|
]
|
||||||
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
||||||
keywords = ["actix", "http", "web", "framework", "async"]
|
keywords = ["actix", "http", "web", "framework", "async"]
|
||||||
categories = [
|
categories = [
|
||||||
@@ -11,13 +14,14 @@ categories = [
|
|||||||
"web-programming::websocket"
|
"web-programming::websocket"
|
||||||
]
|
]
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
repository = "https://github.com/actix/actix-web"
|
repository = "https://github.com/actix/actix-web.git"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
# features that docs.rs will build with
|
# features that docs.rs will build with
|
||||||
features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"]
|
features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"]
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "actix_web"
|
name = "actix_web"
|
||||||
@@ -27,18 +31,16 @@ path = "src/lib.rs"
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
".",
|
".",
|
||||||
"awc",
|
|
||||||
"actix-http",
|
|
||||||
"actix-files",
|
"actix-files",
|
||||||
|
"actix-http-test",
|
||||||
|
"actix-http",
|
||||||
"actix-multipart",
|
"actix-multipart",
|
||||||
|
"actix-router",
|
||||||
|
"actix-test",
|
||||||
"actix-web-actors",
|
"actix-web-actors",
|
||||||
"actix-web-codegen",
|
"actix-web-codegen",
|
||||||
"actix-http-test",
|
"awc",
|
||||||
"actix-test",
|
|
||||||
"actix-router",
|
|
||||||
]
|
]
|
||||||
# enable when MSRV is 1.51+
|
|
||||||
# resolver = "2"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
|
default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
|
||||||
@@ -62,61 +64,67 @@ openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
|
|||||||
# rustls
|
# rustls
|
||||||
rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"]
|
rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"]
|
||||||
|
|
||||||
# Internal (PRIVATE!) features used to aid testing and cheking feature status.
|
# Internal (PRIVATE!) features used to aid testing and checking feature status.
|
||||||
# Don't rely on these whatsoever. They may disappear at anytime.
|
# Don't rely on these whatsoever. They may disappear at anytime.
|
||||||
__compress = []
|
__compress = []
|
||||||
|
|
||||||
|
# io-uring feature only avaiable for Linux OSes.
|
||||||
|
experimental-io-uring = ["actix-server/io-uring"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-codec = "0.4.0"
|
actix-codec = "0.4.1"
|
||||||
actix-macros = "0.2.1"
|
actix-macros = "0.2.3"
|
||||||
actix-router = "0.5.0-beta.2"
|
actix-rt = "2.6"
|
||||||
actix-rt = "2.2"
|
actix-server = "2"
|
||||||
actix-server = "2.0.0-beta.3"
|
|
||||||
actix-service = "2.0.0"
|
actix-service = "2.0.0"
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3.0.0"
|
||||||
actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true }
|
actix-tls = { version = "3.0.0", default-features = false, optional = true }
|
||||||
|
|
||||||
actix-web-codegen = "0.5.0-beta.4"
|
actix-http = { version = "3.0.0-rc.1", features = ["http2", "ws"] }
|
||||||
actix-http = "3.0.0-beta.10"
|
actix-router = "0.5.0-rc.3"
|
||||||
|
actix-web-codegen = "0.5.0-rc.2"
|
||||||
|
|
||||||
ahash = "0.7"
|
ahash = "0.7"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
cfg-if = "1"
|
cfg-if = "1"
|
||||||
cookie = { version = "0.15", features = ["percent-encode"], optional = true }
|
cookie = { version = "0.16", features = ["percent-encode"], optional = true }
|
||||||
derive_more = "0.99.5"
|
derive_more = "0.99.5"
|
||||||
either = "1.5.3"
|
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
futures-core = { version = "0.3.7", default-features = false }
|
futures-core = { version = "0.3.7", default-features = false }
|
||||||
futures-util = { version = "0.3.7", default-features = false }
|
futures-util = { version = "0.3.7", default-features = false }
|
||||||
itoa = "0.4"
|
itoa = "1"
|
||||||
language-tags = "0.3"
|
language-tags = "0.3"
|
||||||
once_cell = "1.5"
|
once_cell = "1.5"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
paste = "1"
|
pin-project-lite = "0.2.7"
|
||||||
pin-project = "1.0.0"
|
|
||||||
regex = "1.4"
|
regex = "1.4"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
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.4.0"
|
socket2 = "0.4.0"
|
||||||
time = { version = "0.2.23", default-features = false, features = ["std"] }
|
time = { version = "0.3", default-features = false, features = ["formatting"] }
|
||||||
url = "2.1"
|
url = "2.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] }
|
actix-files = "0.6.0-beta.16"
|
||||||
awc = { version = "3.0.0-beta.8", features = ["openssl"] }
|
actix-test = { version = "0.1.0-beta.12", features = ["openssl", "rustls"] }
|
||||||
|
awc = { version = "3.0.0-beta.20", features = ["openssl"] }
|
||||||
|
|
||||||
brotli2 = "0.3.2"
|
brotli = "3.3.3"
|
||||||
|
const-str = "0.3"
|
||||||
criterion = { version = "0.3", features = ["html_reports"] }
|
criterion = { version = "0.3", features = ["html_reports"] }
|
||||||
env_logger = "0.8"
|
env_logger = "0.9"
|
||||||
flate2 = "1.0.13"
|
flate2 = "1.0.13"
|
||||||
zstd = "0.7"
|
futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
rcgen = "0.8"
|
rcgen = "0.8"
|
||||||
|
rustls-pemfile = "0.2"
|
||||||
|
static_assertions = "1"
|
||||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||||
tls-rustls = { package = "rustls", version = "0.19.0" }
|
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||||
|
zstd = "0.9"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
# Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much.
|
# Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much.
|
||||||
@@ -139,10 +147,23 @@ actix-web-actors = { path = "actix-web-actors" }
|
|||||||
actix-web-codegen = { path = "actix-web-codegen" }
|
actix-web-codegen = { path = "actix-web-codegen" }
|
||||||
awc = { path = "awc" }
|
awc = { path = "awc" }
|
||||||
|
|
||||||
|
# uncomment for quick testing against local actix-net repo
|
||||||
|
# actix-service = { path = "../actix-net/actix-service" }
|
||||||
|
# actix-macros = { path = "../actix-net/actix-macros" }
|
||||||
|
# actix-rt = { path = "../actix-net/actix-rt" }
|
||||||
|
# actix-codec = { path = "../actix-net/actix-codec" }
|
||||||
|
# actix-utils = { path = "../actix-net/actix-utils" }
|
||||||
|
# actix-tls = { path = "../actix-net/actix-tls" }
|
||||||
|
# actix-server = { path = "../actix-net/actix-server" }
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "test_server"
|
name = "test_server"
|
||||||
required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
|
required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "compression"
|
||||||
|
required-features = ["compress-brotli", "compress-gzip", "compress-zstd"]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "basic"
|
name = "basic"
|
||||||
required-features = ["compress-gzip"]
|
required-features = ["compress-gzip"]
|
||||||
@@ -152,7 +173,7 @@ name = "uds"
|
|||||||
required-features = ["compress-gzip"]
|
required-features = ["compress-gzip"]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "on_connect"
|
name = "on-connect"
|
||||||
required-features = []
|
required-features = []
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
|
|||||||
156
MIGRATION.md
156
MIGRATION.md
@@ -1,6 +1,6 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
* The default `NormalizePath` behavior now strips trailing slashes by default. This was
|
- The default `NormalizePath` behavior now strips trailing slashes by default. This was
|
||||||
previously documented to be the case in v3 but the behavior now matches. The effect is that
|
previously documented to be the case in v3 but the behavior now matches. The effect is that
|
||||||
routes defined with trailing slashes will become inaccessible when
|
routes defined with trailing slashes will become inaccessible when
|
||||||
using `NormalizePath::default()`. As such, calling `NormalizePath::default()` will log a warning.
|
using `NormalizePath::default()`. As such, calling `NormalizePath::default()` will log a warning.
|
||||||
@@ -11,7 +11,9 @@
|
|||||||
|
|
||||||
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
|
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
|
||||||
|
|
||||||
* Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd).
|
- The `type Config` of `FromRequest` was removed.
|
||||||
|
|
||||||
|
- Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd).
|
||||||
By default all compression algorithms are enabled.
|
By default all compression algorithms are enabled.
|
||||||
To select algorithm you want to include with `middleware::Compress` use following flags:
|
To select algorithm you want to include with `middleware::Compress` use following flags:
|
||||||
- `compress-brotli`
|
- `compress-brotli`
|
||||||
@@ -26,30 +28,30 @@
|
|||||||
|
|
||||||
## 3.0.0
|
## 3.0.0
|
||||||
|
|
||||||
* The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to
|
- 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>>()`.
|
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.
|
||||||
|
|
||||||
* The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects
|
- The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects
|
||||||
any `actix-web` method previously expecting a time v0.1 input.
|
any `actix-web` method previously expecting a time v0.1 input.
|
||||||
|
|
||||||
* Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now
|
- Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now
|
||||||
result in `SameSite=None` being sent with the response Set-Cookie header.
|
result in `SameSite=None` being sent with the response Set-Cookie header.
|
||||||
To create a cookie without a SameSite attribute, remove any calls setting same_site.
|
To create a cookie without a SameSite attribute, remove any calls setting same_site.
|
||||||
|
|
||||||
* actix-http support for Actors messages was moved to actix-http crate and is enabled
|
- actix-http support for Actors messages was moved to actix-http crate and is enabled
|
||||||
with feature `actors`
|
with feature `actors`
|
||||||
|
|
||||||
* content_length function is removed from actix-http.
|
- content_length function is removed from actix-http.
|
||||||
You can set Content-Length by normally setting the response body or calling no_chunking function.
|
You can set Content-Length by normally setting the response body or calling no_chunking function.
|
||||||
|
|
||||||
* `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
|
- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
|
||||||
`u64` instead of a `usize`.
|
`u64` instead of a `usize`.
|
||||||
|
|
||||||
* Code that was using `path.<index>` to access a `web::Path<(A, B, C)>`s elements now needs to use
|
- Code that was using `path.<index>` to access a `web::Path<(A, B, C)>`s elements now needs to use
|
||||||
destructuring or `.into_inner()`. For example:
|
destructuring or `.into_inner()`. For example:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
@@ -69,35 +71,35 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
* `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one.
|
- `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one.
|
||||||
It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`,
|
It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`,
|
||||||
or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`.
|
or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`.
|
||||||
|
|
||||||
* `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`.
|
- `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`.
|
||||||
|
|
||||||
* `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`.
|
- `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`.
|
||||||
|
|
||||||
|
|
||||||
## 2.0.0
|
## 2.0.0
|
||||||
|
|
||||||
* `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to
|
- `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to
|
||||||
`.await` on `run` method result, in that case it awaits server exit.
|
`.await` on `run` method result, in that case it awaits server exit.
|
||||||
|
|
||||||
* `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`.
|
- `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`.
|
||||||
Stored data is available via `HttpRequest::app_data()` method at runtime.
|
Stored data is available via `HttpRequest::app_data()` method at runtime.
|
||||||
|
|
||||||
* Extractor configuration must be registered with `App::app_data()` instead of `App::data()`
|
- Extractor configuration must be registered with `App::app_data()` instead of `App::data()`
|
||||||
|
|
||||||
* Sync handlers has been removed. `.to_async()` method has been renamed to `.to()`
|
- Sync handlers has been removed. `.to_async()` method has been renamed to `.to()`
|
||||||
replace `fn` with `async fn` to convert sync handler to async
|
replace `fn` with `async fn` to convert sync handler to async
|
||||||
|
|
||||||
* `actix_http_test::TestServer` moved to `actix_web::test` module. To start
|
- `actix_http_test::TestServer` moved to `actix_web::test` module. To start
|
||||||
test server use `test::start()` or `test_start_with_config()` methods
|
test server use `test::start()` or `test_start_with_config()` methods
|
||||||
|
|
||||||
* `ResponseError` trait has been reafctored. `ResponseError::error_response()` renders
|
- `ResponseError` trait has been reafctored. `ResponseError::error_response()` renders
|
||||||
http response.
|
http response.
|
||||||
|
|
||||||
* Feature `rust-tls` renamed to `rustls`
|
- Feature `rust-tls` renamed to `rustls`
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
@@ -111,7 +113,7 @@
|
|||||||
actix-web = { version = "2.0.0", features = ["rustls"] }
|
actix-web = { version = "2.0.0", features = ["rustls"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
* Feature `ssl` renamed to `openssl`
|
- Feature `ssl` renamed to `openssl`
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
@@ -124,11 +126,11 @@
|
|||||||
```rust
|
```rust
|
||||||
actix-web = { version = "2.0.0", features = ["openssl"] }
|
actix-web = { version = "2.0.0", features = ["openssl"] }
|
||||||
```
|
```
|
||||||
* `Cors` builder now requires that you call `.finish()` to construct the middleware
|
- `Cors` builder now requires that you call `.finish()` to construct the middleware
|
||||||
|
|
||||||
## 1.0.1
|
## 1.0.1
|
||||||
|
|
||||||
* Cors middleware has been moved to `actix-cors` crate
|
- Cors middleware has been moved to `actix-cors` crate
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
@@ -142,7 +144,7 @@
|
|||||||
use actix_cors::Cors;
|
use actix_cors::Cors;
|
||||||
```
|
```
|
||||||
|
|
||||||
* Identity middleware has been moved to `actix-identity` crate
|
- Identity middleware has been moved to `actix-identity` crate
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
@@ -159,7 +161,7 @@
|
|||||||
|
|
||||||
## 1.0.0
|
## 1.0.0
|
||||||
|
|
||||||
* Extractor configuration. In version 1.0 this is handled with the new `Data` mechanism for both setting and retrieving the configuration
|
- Extractor configuration. In version 1.0 this is handled with the new `Data` mechanism for both setting and retrieving the configuration
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
@@ -217,7 +219,7 @@
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
* Resource registration. 1.0 version uses generalized resource
|
- Resource registration. 1.0 version uses generalized resource
|
||||||
registration via `.service()` method.
|
registration via `.service()` method.
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
@@ -237,7 +239,7 @@
|
|||||||
.route(web::post().to(post_handler))
|
.route(web::post().to(post_handler))
|
||||||
```
|
```
|
||||||
|
|
||||||
* Scope registration.
|
- Scope registration.
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
@@ -261,7 +263,7 @@
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
* `.with()`, `.with_async()` registration methods have been renamed to `.to()` and `.to_async()`.
|
- `.with()`, `.with_async()` registration methods have been renamed to `.to()` and `.to_async()`.
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
@@ -275,7 +277,7 @@
|
|||||||
App.new().service(web::resource("/welcome").to(welcome))
|
App.new().service(web::resource("/welcome").to(welcome))
|
||||||
```
|
```
|
||||||
|
|
||||||
* Passing arguments to handler with extractors, multiple arguments are allowed
|
- Passing arguments to handler with extractors, multiple arguments are allowed
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
@@ -293,7 +295,7 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
* `.f()`, `.a()` and `.h()` handler registration methods have been removed.
|
- `.f()`, `.a()` and `.h()` handler registration methods have been removed.
|
||||||
Use `.to()` for handlers and `.to_async()` for async handlers. Handler function
|
Use `.to()` for handlers and `.to_async()` for async handlers. Handler function
|
||||||
must use extractors.
|
must use extractors.
|
||||||
|
|
||||||
@@ -309,7 +311,7 @@
|
|||||||
App.new().service(web::resource("/welcome").to(welcome))
|
App.new().service(web::resource("/welcome").to(welcome))
|
||||||
```
|
```
|
||||||
|
|
||||||
* `HttpRequest` does not provide access to request's payload stream.
|
- `HttpRequest` does not provide access to request's payload stream.
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
@@ -339,7 +341,7 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
* `State` is now `Data`. You register Data during the App initialization process
|
- `State` is now `Data`. You register Data during the App initialization process
|
||||||
and then access it from handlers either using a Data extractor or using
|
and then access it from handlers either using a Data extractor or using
|
||||||
HttpRequest's api.
|
HttpRequest's api.
|
||||||
|
|
||||||
@@ -375,7 +377,7 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
* AsyncResponder is removed, use `.to_async()` registration method and `impl Future<>` as result type.
|
- AsyncResponder is removed, use `.to_async()` registration method and `impl Future<>` as result type.
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
@@ -391,7 +393,7 @@
|
|||||||
.. simply omit AsyncResponder and the corresponding responder() finish method
|
.. simply omit AsyncResponder and the corresponding responder() finish method
|
||||||
|
|
||||||
|
|
||||||
* Middleware
|
- Middleware
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
@@ -408,7 +410,7 @@
|
|||||||
.route("/index.html", web::get().to(index));
|
.route("/index.html", web::get().to(index));
|
||||||
```
|
```
|
||||||
|
|
||||||
* `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()`
|
- `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()`
|
||||||
method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead.
|
method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead.
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
@@ -430,9 +432,9 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
* `actix_web::server` module has been removed. To start http server use `actix_web::HttpServer` type
|
- `actix_web::server` module has been removed. To start http server use `actix_web::HttpServer` type
|
||||||
|
|
||||||
* StaticFiles and NamedFile have been moved to a separate crate.
|
- StaticFiles and NamedFile have been moved to a separate crate.
|
||||||
|
|
||||||
instead of `use actix_web::fs::StaticFile`
|
instead of `use actix_web::fs::StaticFile`
|
||||||
|
|
||||||
@@ -442,20 +444,20 @@
|
|||||||
|
|
||||||
use `use actix_files::NamedFile`
|
use `use actix_files::NamedFile`
|
||||||
|
|
||||||
* Multipart has been moved to a separate crate.
|
- Multipart has been moved to a separate crate.
|
||||||
|
|
||||||
instead of `use actix_web::multipart::Multipart`
|
instead of `use actix_web::multipart::Multipart`
|
||||||
|
|
||||||
use `use actix_multipart::Multipart`
|
use `use actix_multipart::Multipart`
|
||||||
|
|
||||||
* Response compression is not enabled by default.
|
- Response compression is not enabled by default.
|
||||||
To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`.
|
To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`.
|
||||||
|
|
||||||
* Session middleware moved to actix-session crate
|
- Session middleware moved to actix-session crate
|
||||||
|
|
||||||
* Actors support have been moved to `actix-web-actors` crate
|
- Actors support have been moved to `actix-web-actors` crate
|
||||||
|
|
||||||
* Custom Error
|
- Custom Error
|
||||||
|
|
||||||
Instead of error_response method alone, ResponseError now provides two methods: error_response and render_response respectively. Where, error_response creates the error response and render_response returns the error response to the caller.
|
Instead of error_response method alone, ResponseError now provides two methods: error_response and render_response respectively. Where, error_response creates the error response and render_response returns the error response to the caller.
|
||||||
|
|
||||||
@@ -469,7 +471,7 @@
|
|||||||
|
|
||||||
## 0.7.15
|
## 0.7.15
|
||||||
|
|
||||||
* The `' '` character is not percent decoded anymore before matching routes. If you need to use it in
|
- The `' '` character is not percent decoded anymore before matching routes. If you need to use it in
|
||||||
your routes, you should use `%20`.
|
your routes, you should use `%20`.
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
@@ -494,18 +496,18 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
* If you used `AsyncResult::async` you need to replace it with `AsyncResult::future`
|
- If you used `AsyncResult::async` you need to replace it with `AsyncResult::future`
|
||||||
|
|
||||||
|
|
||||||
## 0.7.4
|
## 0.7.4
|
||||||
|
|
||||||
* `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple
|
- `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple
|
||||||
even for handler with one parameter.
|
even for handler with one parameter.
|
||||||
|
|
||||||
|
|
||||||
## 0.7
|
## 0.7
|
||||||
|
|
||||||
* `HttpRequest` does not implement `Stream` anymore. If you need to read request payload
|
- `HttpRequest` does not implement `Stream` anymore. If you need to read request payload
|
||||||
use `HttpMessage::payload()` method.
|
use `HttpMessage::payload()` method.
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
@@ -531,10 +533,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
* [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html)
|
- [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html)
|
||||||
trait uses `&HttpRequest` instead of `&mut HttpRequest`.
|
trait uses `&HttpRequest` instead of `&mut HttpRequest`.
|
||||||
|
|
||||||
* Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead.
|
- Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead.
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
|
|
||||||
@@ -548,17 +550,17 @@
|
|||||||
fn index((query, json): (Query<..>, Json<MyStruct)) -> impl Responder {}
|
fn index((query, json): (Query<..>, Json<MyStruct)) -> impl Responder {}
|
||||||
```
|
```
|
||||||
|
|
||||||
* `Handler::handle()` uses `&self` instead of `&mut self`
|
- `Handler::handle()` uses `&self` instead of `&mut self`
|
||||||
|
|
||||||
* `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value
|
- `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value
|
||||||
|
|
||||||
* Removed deprecated `HttpServer::threads()`, use
|
- Removed deprecated `HttpServer::threads()`, use
|
||||||
[HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead.
|
[HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead.
|
||||||
|
|
||||||
* Renamed `client::ClientConnectorError::Connector` to
|
- Renamed `client::ClientConnectorError::Connector` to
|
||||||
`client::ClientConnectorError::Resolver`
|
`client::ClientConnectorError::Resolver`
|
||||||
|
|
||||||
* `Route::with()` does not return `ExtractorConfig`, to configure
|
- `Route::with()` does not return `ExtractorConfig`, to configure
|
||||||
extractor use `Route::with_config()`
|
extractor use `Route::with_config()`
|
||||||
|
|
||||||
instead of
|
instead of
|
||||||
@@ -587,26 +589,26 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
* `Route::with_async()` does not return `ExtractorConfig`, to configure
|
- `Route::with_async()` does not return `ExtractorConfig`, to configure
|
||||||
extractor use `Route::with_async_config()`
|
extractor use `Route::with_async_config()`
|
||||||
|
|
||||||
|
|
||||||
## 0.6
|
## 0.6
|
||||||
|
|
||||||
* `Path<T>` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest`
|
- `Path<T>` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest`
|
||||||
|
|
||||||
* `ws::Message::Close` now includes optional close reason.
|
- `ws::Message::Close` now includes optional close reason.
|
||||||
`ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed.
|
`ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed.
|
||||||
|
|
||||||
* `HttpServer::threads()` renamed to `HttpServer::workers()`.
|
- `HttpServer::threads()` renamed to `HttpServer::workers()`.
|
||||||
|
|
||||||
* `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated.
|
- `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated.
|
||||||
Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead.
|
Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead.
|
||||||
|
|
||||||
* `HttpRequest::extensions()` returns read only reference to the request's Extension
|
- `HttpRequest::extensions()` returns read only reference to the request's Extension
|
||||||
`HttpRequest::extensions_mut()` returns mutable reference.
|
`HttpRequest::extensions_mut()` returns mutable reference.
|
||||||
|
|
||||||
* Instead of
|
- Instead of
|
||||||
|
|
||||||
`use actix_web::middleware::{
|
`use actix_web::middleware::{
|
||||||
CookieSessionBackend, CookieSessionError, RequestSession,
|
CookieSessionBackend, CookieSessionError, RequestSession,
|
||||||
@@ -617,15 +619,15 @@
|
|||||||
`use actix_web::middleware::session{CookieSessionBackend, CookieSessionError,
|
`use actix_web::middleware::session{CookieSessionBackend, CookieSessionError,
|
||||||
RequestSession, Session, SessionBackend, SessionImpl, SessionStorage};`
|
RequestSession, Session, SessionBackend, SessionImpl, SessionStorage};`
|
||||||
|
|
||||||
* `FromRequest::from_request()` accepts mutable reference to a request
|
- `FromRequest::from_request()` accepts mutable reference to a request
|
||||||
|
|
||||||
* `FromRequest::Result` has to implement `Into<Reply<Self>>`
|
- `FromRequest::Result` has to implement `Into<Reply<Self>>`
|
||||||
|
|
||||||
* [`Responder::respond_to()`](
|
- [`Responder::respond_to()`](
|
||||||
https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to)
|
https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to)
|
||||||
is generic over `S`
|
is generic over `S`
|
||||||
|
|
||||||
* Use `Query` extractor instead of HttpRequest::query()`.
|
- Use `Query` extractor instead of HttpRequest::query()`.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn index(q: Query<HashMap<String, String>>) -> Result<..> {
|
fn index(q: Query<HashMap<String, String>>) -> Result<..> {
|
||||||
@@ -639,37 +641,37 @@
|
|||||||
let q = Query::<HashMap<String, String>>::extract(req);
|
let q = Query::<HashMap<String, String>>::extract(req);
|
||||||
```
|
```
|
||||||
|
|
||||||
* Websocket operations are implemented as `WsWriter` trait.
|
- Websocket operations are implemented as `WsWriter` trait.
|
||||||
you need to use `use actix_web::ws::WsWriter`
|
you need to use `use actix_web::ws::WsWriter`
|
||||||
|
|
||||||
|
|
||||||
## 0.5
|
## 0.5
|
||||||
|
|
||||||
* `HttpResponseBuilder::body()`, `.finish()`, `.json()`
|
- `HttpResponseBuilder::body()`, `.finish()`, `.json()`
|
||||||
methods return `HttpResponse` instead of `Result<HttpResponse>`
|
methods return `HttpResponse` instead of `Result<HttpResponse>`
|
||||||
|
|
||||||
* `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version`
|
- `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version`
|
||||||
moved to `actix_web::http` module
|
moved to `actix_web::http` module
|
||||||
|
|
||||||
* `actix_web::header` moved to `actix_web::http::header`
|
- `actix_web::header` moved to `actix_web::http::header`
|
||||||
|
|
||||||
* `NormalizePath` moved to `actix_web::http` module
|
- `NormalizePath` moved to `actix_web::http` module
|
||||||
|
|
||||||
* `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function,
|
- `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function,
|
||||||
shortcut for `actix_web::server::HttpServer::new()`
|
shortcut for `actix_web::server::HttpServer::new()`
|
||||||
|
|
||||||
* `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself
|
- `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself
|
||||||
|
|
||||||
* `StaticFiles::new()`'s show_index parameter removed, use `show_files_listing()` method instead.
|
- `StaticFiles::new()`'s show_index parameter removed, use `show_files_listing()` method instead.
|
||||||
|
|
||||||
* `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type
|
- `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type
|
||||||
|
|
||||||
* `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()`
|
- `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()`
|
||||||
functions should be used instead
|
functions should be used instead
|
||||||
|
|
||||||
* `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>`
|
- `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>`
|
||||||
instead of `Result<_, http::Error>`
|
instead of `Result<_, http::Error>`
|
||||||
|
|
||||||
* `Application` renamed to a `App`
|
- `Application` renamed to a `App`
|
||||||
|
|
||||||
* `actix_web::Reply`, `actix_web::Resource` moved to `actix_web::dev`
|
- `actix_web::Reply`, `actix_web::Resource` moved to `actix_web::dev`
|
||||||
|
|||||||
90
README.md
90
README.md
@@ -6,12 +6,12 @@
|
|||||||
<p>
|
<p>
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-web)
|
[](https://crates.io/crates/actix-web)
|
||||||
[](https://docs.rs/actix-web/4.0.0-beta.9)
|
[](https://docs.rs/actix-web/4.0.0-rc.1)
|
||||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html)
|

|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-web/4.0.0-beta.9)
|
[](https://deps.rs/crate/actix-web/4.0.0-rc.1)
|
||||||
<br />
|
<br />
|
||||||
[](https://github.com/actix/actix-web/actions)
|
[](https://github.com/actix/actix-web/actions/workflows/ci.yml)
|
||||||
[](https://codecov.io/gh/actix/actix-web)
|
[](https://codecov.io/gh/actix/actix-web)
|
||||||

|

|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
@@ -21,25 +21,26 @@
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Supports *HTTP/1.x* and *HTTP/2*
|
- Supports _HTTP/1.x_ and _HTTP/2_
|
||||||
* Streaming and pipelining
|
- Streaming and pipelining
|
||||||
* Keep-alive and slow requests handling
|
- Powerful [request routing](https://actix.rs/docs/url-dispatch/) with optional macros
|
||||||
* Client/server [WebSockets](https://actix.rs/docs/websockets/) support
|
- Full [Tokio](https://tokio.rs) compatibility
|
||||||
* Transparent content compression/decompression (br, gzip, deflate, zstd)
|
- Keep-alive and slow requests handling
|
||||||
* Powerful [request routing](https://actix.rs/docs/url-dispatch/)
|
- Client/server [WebSockets](https://actix.rs/docs/websockets/) support
|
||||||
* Multipart streams
|
- Transparent content compression/decompression (br, gzip, deflate, zstd)
|
||||||
* Static assets
|
- Multipart streams
|
||||||
* SSL support using OpenSSL or Rustls
|
- Static assets
|
||||||
* Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
|
- SSL support using OpenSSL or Rustls
|
||||||
* Includes an async [HTTP client](https://docs.rs/awc/)
|
- Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
|
||||||
* Runs on stable Rust 1.51+
|
- Includes an async [HTTP client](https://docs.rs/awc/)
|
||||||
|
- Runs on stable Rust 1.54+
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
* [Website & User Guide](https://actix.rs)
|
- [Website & User Guide](https://actix.rs)
|
||||||
* [Examples Repository](https://github.com/actix/examples)
|
- [Examples Repository](https://github.com/actix/examples)
|
||||||
* [API Documentation](https://docs.rs/actix-web)
|
- [API Documentation](https://docs.rs/actix-web)
|
||||||
* [API Documentation (master branch)](https://actix.rs/actix-web/actix_web)
|
- [API Documentation (master branch)](https://actix.rs/actix-web/actix_web)
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ Dependencies:
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "3"
|
actix-web = "4.0.0-rc.1"
|
||||||
```
|
```
|
||||||
|
|
||||||
Code:
|
Code:
|
||||||
@@ -56,14 +57,15 @@ Code:
|
|||||||
use actix_web::{get, web, App, HttpServer, Responder};
|
use actix_web::{get, web, App, HttpServer, Responder};
|
||||||
|
|
||||||
#[get("/{id}/{name}/index.html")]
|
#[get("/{id}/{name}/index.html")]
|
||||||
async fn index(web::Path((id, name)): web::Path<(u32, String)>) -> impl Responder {
|
async fn index(params: web::Path<(u32, String)>) -> impl Responder {
|
||||||
|
let (id, name) = params.into_inner();
|
||||||
format!("Hello {}! id:{}", name, id)
|
format!("Hello {}! id:{}", name, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main] // or #[tokio::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
HttpServer::new(|| App::new().service(index))
|
HttpServer::new(|| App::new().service(index))
|
||||||
.bind("127.0.0.1:8080")?
|
.bind(("127.0.0.1", 8080))?
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -71,37 +73,31 @@ async fn main() -> std::io::Result<()> {
|
|||||||
|
|
||||||
### More examples
|
### More examples
|
||||||
|
|
||||||
* [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics/)
|
- [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics/)
|
||||||
* [Application State](https://github.com/actix/examples/tree/master/basics/state/)
|
- [Application State](https://github.com/actix/examples/tree/master/basics/state/)
|
||||||
* [JSON Handling](https://github.com/actix/examples/tree/master/json/json/)
|
- [JSON Handling](https://github.com/actix/examples/tree/master/json/json/)
|
||||||
* [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart/)
|
- [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart/)
|
||||||
* [Diesel Integration](https://github.com/actix/examples/tree/master/database_interactions/diesel/)
|
- [Diesel Integration](https://github.com/actix/examples/tree/master/database_interactions/diesel/)
|
||||||
* [r2d2 Integration](https://github.com/actix/examples/tree/master/database_interactions/r2d2/)
|
- [r2d2 Integration](https://github.com/actix/examples/tree/master/database_interactions/r2d2/)
|
||||||
* [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets/websocket/)
|
- [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets/websocket/)
|
||||||
* [Tera Templates](https://github.com/actix/examples/tree/master/template_engines/tera/)
|
- [Tera Templates](https://github.com/actix/examples/tree/master/template_engines/tera/)
|
||||||
* [Askama Templates](https://github.com/actix/examples/tree/master/template_engines/askama/)
|
- [Askama Templates](https://github.com/actix/examples/tree/master/template_engines/askama/)
|
||||||
* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/security/rustls/)
|
- [HTTPS using Rustls](https://github.com/actix/examples/tree/master/security/rustls/)
|
||||||
* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/security/openssl/)
|
- [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/security/openssl/)
|
||||||
* [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat/)
|
- [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat/)
|
||||||
|
|
||||||
You may consider checking out
|
You may consider checking out [this directory](https://github.com/actix/examples/tree/master/) for more examples.
|
||||||
[this directory](https://github.com/actix/examples/tree/master/) for more examples.
|
|
||||||
|
|
||||||
## Benchmarks
|
## Benchmarks
|
||||||
|
|
||||||
One of the fastest web frameworks available according to the
|
One of the fastest web frameworks available according to the [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite).
|
||||||
[TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite).
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under either of
|
This project is licensed under either of the following licenses, at your option:
|
||||||
|
|
||||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0])
|
||||||
[http://www.apache.org/licenses/LICENSE-2.0])
|
- MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT])
|
||||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
|
||||||
[http://opensource.org/licenses/MIT])
|
|
||||||
|
|
||||||
at your option.
|
|
||||||
|
|
||||||
## Code of Conduct
|
## Code of Conduct
|
||||||
|
|
||||||
|
|||||||
@@ -3,23 +3,73 @@
|
|||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.16 - 2022-01-31
|
||||||
|
- No significant changes since `0.6.0-beta.15`.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.15 - 2022-01-21
|
||||||
|
- No significant changes since `0.6.0-beta.14`.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.14 - 2022-01-14
|
||||||
|
- The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583]
|
||||||
|
|
||||||
|
[#2583]: https://github.com/actix/actix-web/pull/2583
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.13 - 2022-01-04
|
||||||
|
- The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398]
|
||||||
|
- The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398]
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||||
|
|
||||||
|
[#2398]: https://github.com/actix/actix-web/pull/2398
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.12 - 2021-12-29
|
||||||
|
- No significant changes since `0.6.0-beta.11`.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.11 - 2021-12-27
|
||||||
|
- No significant changes since `0.6.0-beta.10`.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.10 - 2021-12-11
|
||||||
|
- No significant changes since `0.6.0-beta.9`.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.9 - 2021-11-22
|
||||||
|
- Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408]
|
||||||
|
- Add `NamedFile::open_async`. [#2408]
|
||||||
|
- Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453]
|
||||||
|
- The `Responder` impl for `NamedFile` now has a boxed future associated type. [#2408]
|
||||||
|
- The `Service` impl for `NamedFileService` now has a boxed future associated type. [#2408]
|
||||||
|
- Add `impl Clone` for `FilesService`. [#2408]
|
||||||
|
|
||||||
|
[#2408]: https://github.com/actix/actix-web/pull/2408
|
||||||
|
[#2453]: https://github.com/actix/actix-web/pull/2453
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.8 - 2021-10-20
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.7 - 2021-09-09
|
## 0.6.0-beta.7 - 2021-09-09
|
||||||
* Minimum supported Rust version (MSRV) is now 1.51.
|
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.6 - 2021-06-26
|
## 0.6.0-beta.6 - 2021-06-26
|
||||||
* Added `Files::path_filter()`. [#2274]
|
- Added `Files::path_filter()`. [#2274]
|
||||||
* `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228]
|
- `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228]
|
||||||
|
|
||||||
[#2274]: https://github.com/actix/actix-web/pull/2274
|
[#2274]: https://github.com/actix/actix-web/pull/2274
|
||||||
[#2228]: https://github.com/actix/actix-web/pull/2228
|
[#2228]: https://github.com/actix/actix-web/pull/2228
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.5 - 2021-06-17
|
## 0.6.0-beta.5 - 2021-06-17
|
||||||
* `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135]
|
- `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135]
|
||||||
* For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156]
|
- For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156]
|
||||||
* `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225]
|
- `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225]
|
||||||
* `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257]
|
- `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257]
|
||||||
|
|
||||||
[#2135]: https://github.com/actix/actix-web/pull/2135
|
[#2135]: https://github.com/actix/actix-web/pull/2135
|
||||||
[#2156]: https://github.com/actix/actix-web/pull/2156
|
[#2156]: https://github.com/actix/actix-web/pull/2156
|
||||||
@@ -28,130 +78,130 @@
|
|||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.4 - 2021-04-02
|
## 0.6.0-beta.4 - 2021-04-02
|
||||||
* Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046]
|
- Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046]
|
||||||
|
|
||||||
[#2046]: https://github.com/actix/actix-web/pull/2046
|
[#2046]: https://github.com/actix/actix-web/pull/2046
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.3 - 2021-03-09
|
## 0.6.0-beta.3 - 2021-03-09
|
||||||
* No notable changes.
|
- No notable changes.
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.2 - 2021-02-10
|
## 0.6.0-beta.2 - 2021-02-10
|
||||||
* Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887]
|
- Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887]
|
||||||
* Replace `v_htmlescape` with `askama_escape`. [#1953]
|
- Replace `v_htmlescape` with `askama_escape`. [#1953]
|
||||||
|
|
||||||
[#1887]: https://github.com/actix/actix-web/pull/1887
|
[#1887]: https://github.com/actix/actix-web/pull/1887
|
||||||
[#1953]: https://github.com/actix/actix-web/pull/1953
|
[#1953]: https://github.com/actix/actix-web/pull/1953
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.1 - 2021-01-07
|
## 0.6.0-beta.1 - 2021-01-07
|
||||||
* `HttpRange::parse` now has its own error type.
|
- `HttpRange::parse` now has its own error type.
|
||||||
* Update `bytes` to `1.0`. [#1813]
|
- Update `bytes` to `1.0`. [#1813]
|
||||||
|
|
||||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||||
|
|
||||||
|
|
||||||
## 0.5.0 - 2020-12-26
|
## 0.5.0 - 2020-12-26
|
||||||
* Optionally support hidden files/directories. [#1811]
|
- Optionally support hidden files/directories. [#1811]
|
||||||
|
|
||||||
[#1811]: https://github.com/actix/actix-web/pull/1811
|
[#1811]: https://github.com/actix/actix-web/pull/1811
|
||||||
|
|
||||||
|
|
||||||
## 0.4.1 - 2020-11-24
|
## 0.4.1 - 2020-11-24
|
||||||
* Clarify order of parameters in `Files::new` and improve docs.
|
- Clarify order of parameters in `Files::new` and improve docs.
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0 - 2020-10-06
|
## 0.4.0 - 2020-10-06
|
||||||
* Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714]
|
- Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714]
|
||||||
|
|
||||||
[#1714]: https://github.com/actix/actix-web/pull/1714
|
[#1714]: https://github.com/actix/actix-web/pull/1714
|
||||||
|
|
||||||
|
|
||||||
## 0.3.0 - 2020-09-11
|
## 0.3.0 - 2020-09-11
|
||||||
* No significant changes from 0.3.0-beta.1.
|
- No significant changes from 0.3.0-beta.1.
|
||||||
|
|
||||||
|
|
||||||
## 0.3.0-beta.1 - 2020-07-15
|
## 0.3.0-beta.1 - 2020-07-15
|
||||||
* Update `v_htmlescape` to 0.10
|
- Update `v_htmlescape` to 0.10
|
||||||
* Update `actix-web` and `actix-http` dependencies to beta.1
|
- Update `actix-web` and `actix-http` dependencies to beta.1
|
||||||
|
|
||||||
|
|
||||||
## 0.3.0-alpha.1 - 2020-05-23
|
## 0.3.0-alpha.1 - 2020-05-23
|
||||||
* Update `actix-web` and `actix-http` dependencies to alpha
|
- Update `actix-web` and `actix-http` dependencies to alpha
|
||||||
* Fix some typos in the docs
|
- Fix some typos in the docs
|
||||||
* Bump minimum supported Rust version to 1.40
|
- Bump minimum supported Rust version to 1.40
|
||||||
* Support sending Content-Length when Content-Range is specified [#1384]
|
- Support sending Content-Length when Content-Range is specified [#1384]
|
||||||
|
|
||||||
[#1384]: https://github.com/actix/actix-web/pull/1384
|
[#1384]: https://github.com/actix/actix-web/pull/1384
|
||||||
|
|
||||||
|
|
||||||
## 0.2.1 - 2019-12-22
|
## 0.2.1 - 2019-12-22
|
||||||
* Use the same format for file URLs regardless of platforms
|
- Use the same format for file URLs regardless of platforms
|
||||||
|
|
||||||
|
|
||||||
## 0.2.0 - 2019-12-20
|
## 0.2.0 - 2019-12-20
|
||||||
* Fix BodyEncoding trait import #1220
|
- Fix BodyEncoding trait import #1220
|
||||||
|
|
||||||
|
|
||||||
## 0.2.0-alpha.1 - 2019-12-07
|
## 0.2.0-alpha.1 - 2019-12-07
|
||||||
* Migrate to `std::future`
|
- Migrate to `std::future`
|
||||||
|
|
||||||
|
|
||||||
## 0.1.7 - 2019-11-06
|
## 0.1.7 - 2019-11-06
|
||||||
* Add an additional `filename*` param in the `Content-Disposition` header of
|
- Add an additional `filename*` param in the `Content-Disposition` header of
|
||||||
`actix_files::NamedFile` to be more compatible. (#1151)
|
`actix_files::NamedFile` to be more compatible. (#1151)
|
||||||
|
|
||||||
## 0.1.6 - 2019-10-14
|
## 0.1.6 - 2019-10-14
|
||||||
* Add option to redirect to a slash-ended path `Files` #1132
|
- Add option to redirect to a slash-ended path `Files` #1132
|
||||||
|
|
||||||
|
|
||||||
## 0.1.5 - 2019-10-08
|
## 0.1.5 - 2019-10-08
|
||||||
* Bump up `mime_guess` crate version to 2.0.1
|
- Bump up `mime_guess` crate version to 2.0.1
|
||||||
* Bump up `percent-encoding` crate version to 2.1
|
- Bump up `percent-encoding` crate version to 2.1
|
||||||
* Allow user defined request guards for `Files` #1113
|
- Allow user defined request guards for `Files` #1113
|
||||||
|
|
||||||
|
|
||||||
## 0.1.4 - 2019-07-20
|
## 0.1.4 - 2019-07-20
|
||||||
* Allow to disable `Content-Disposition` header #686
|
- Allow to disable `Content-Disposition` header #686
|
||||||
|
|
||||||
|
|
||||||
## 0.1.3 - 2019-06-28
|
## 0.1.3 - 2019-06-28
|
||||||
* Do not set `Content-Length` header, let actix-http set it #930
|
- Do not set `Content-Length` header, let actix-http set it #930
|
||||||
|
|
||||||
|
|
||||||
## 0.1.2 - 2019-06-13
|
## 0.1.2 - 2019-06-13
|
||||||
* Content-Length is 0 for NamedFile HEAD request #914
|
- Content-Length is 0 for NamedFile HEAD request #914
|
||||||
* Fix ring dependency from actix-web default features for #741
|
- Fix ring dependency from actix-web default features for #741
|
||||||
|
|
||||||
|
|
||||||
## 0.1.1 - 2019-06-01
|
## 0.1.1 - 2019-06-01
|
||||||
* Static files are incorrectly served as both chunked and with length #812
|
- Static files are incorrectly served as both chunked and with length #812
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0 - 2019-05-25
|
## 0.1.0 - 2019-05-25
|
||||||
* NamedFile last-modified check always fails due to nano-seconds in file modified date #820
|
- NamedFile last-modified check always fails due to nano-seconds in file modified date #820
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.4 - 2019-05-12
|
## 0.1.0-beta.4 - 2019-05-12
|
||||||
* Update actix-web to beta.4
|
- Update actix-web to beta.4
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.1 - 2019-04-20
|
## 0.1.0-beta.1 - 2019-04-20
|
||||||
* Update actix-web to beta.1
|
- Update actix-web to beta.1
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.6 - 2019-04-14
|
## 0.1.0-alpha.6 - 2019-04-14
|
||||||
* Update actix-web to alpha6
|
- Update actix-web to alpha6
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.4 - 2019-04-08
|
## 0.1.0-alpha.4 - 2019-04-08
|
||||||
* Update actix-web to alpha4
|
- Update actix-web to alpha4
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.2 - 2019-04-02
|
## 0.1.0-alpha.2 - 2019-04-02
|
||||||
* Add default handler support
|
- Add default handler support
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.1 - 2019-03-28
|
## 0.1.0-alpha.1 - 2019-03-28
|
||||||
* Initial impl
|
- Initial impl
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-files"
|
name = "actix-files"
|
||||||
version = "0.6.0-beta.7"
|
version = "0.6.0-beta.16"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = [
|
||||||
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
|
"fakeshadow <24548779@qq.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"]
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
@@ -14,24 +18,31 @@ edition = "2018"
|
|||||||
name = "actix_files"
|
name = "actix_files"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = { version = "4.0.0-beta.9", default-features = false }
|
actix-http = "3.0.0-rc.1"
|
||||||
actix-http = "3.0.0-beta.10"
|
actix-service = "2"
|
||||||
actix-service = "2.0.0"
|
actix-utils = "3"
|
||||||
actix-utils = "3.0.0"
|
actix-web = { version = "4.0.0-rc.1", default-features = false }
|
||||||
|
|
||||||
askama_escape = "0.10"
|
askama_escape = "0.10"
|
||||||
bitflags = "1"
|
bitflags = "1"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
|
derive_more = "0.99.5"
|
||||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||||
http-range = "0.1.4"
|
http-range = "0.1.4"
|
||||||
derive_more = "0.99.5"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
mime_guess = "2.0.1"
|
mime_guess = "2.0.1"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
|
pin-project-lite = "0.2.7"
|
||||||
|
|
||||||
|
tokio-uring = { version = "0.2", optional = true, features = ["bytes"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-web = "4.0.0-beta.9"
|
actix-test = "0.1.0-beta.12"
|
||||||
actix-test = "0.1.0-beta.3"
|
actix-web = "4.0.0-rc.1"
|
||||||
|
tempfile = "3.2"
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
> Static file serving for Actix Web
|
> Static file serving for Actix Web
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-files)
|
[](https://crates.io/crates/actix-files)
|
||||||
[](https://docs.rs/actix-files/0.6.0-beta.7)
|
[](https://docs.rs/actix-files/0.6.0-beta.16)
|
||||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html)
|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-files/0.6.0-beta.7)
|
[](https://deps.rs/crate/actix-files/0.6.0-beta.16)
|
||||||
[](https://crates.io/crates/actix-files)
|
[](https://crates.io/crates/actix-files)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
@@ -15,4 +15,4 @@
|
|||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-files/)
|
- [API Documentation](https://docs.rs/actix-files/)
|
||||||
- [Example Project](https://github.com/actix/examples/tree/master/basics/static_index)
|
- [Example Project](https://github.com/actix/examples/tree/master/basics/static_index)
|
||||||
- Minimum supported Rust version: 1.51 or later
|
- Minimum Supported Rust Version (MSRV): 1.54
|
||||||
|
|||||||
@@ -1,95 +1,216 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cmp, fmt,
|
cmp, fmt,
|
||||||
fs::File,
|
|
||||||
future::Future,
|
future::Future,
|
||||||
io::{self, Read, Seek},
|
io,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
|
|
||||||
use actix_web::{
|
use actix_web::{error::Error, web::Bytes};
|
||||||
error::{BlockingError, Error},
|
|
||||||
rt::task::{spawn_blocking, JoinHandle},
|
|
||||||
};
|
|
||||||
use bytes::Bytes;
|
|
||||||
use futures_core::{ready, Stream};
|
use futures_core::{ready, Stream};
|
||||||
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
use bytes::BytesMut;
|
||||||
|
|
||||||
|
use super::named::File;
|
||||||
|
|
||||||
|
pin_project! {
|
||||||
|
/// Adapter to read a `std::file::File` in chunks.
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
/// A helper created from a `std::fs::File` which reads the file
|
pub struct ChunkedReadFile<F, Fut> {
|
||||||
/// chunk-by-chunk on a `ThreadPool`.
|
|
||||||
pub struct ChunkedReadFile {
|
|
||||||
size: u64,
|
size: u64,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
state: ChunkedReadFileState,
|
#[pin]
|
||||||
|
state: ChunkedReadFileState<Fut>,
|
||||||
counter: u64,
|
counter: u64,
|
||||||
}
|
callback: F,
|
||||||
|
|
||||||
enum ChunkedReadFileState {
|
|
||||||
File(Option<File>),
|
|
||||||
Future(JoinHandle<Result<(File, Bytes), io::Error>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChunkedReadFile {
|
|
||||||
pub(crate) fn new(size: u64, offset: u64, file: File) -> Self {
|
|
||||||
Self {
|
|
||||||
size,
|
|
||||||
offset,
|
|
||||||
state: ChunkedReadFileState::File(Some(file)),
|
|
||||||
counter: 0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for ChunkedReadFile {
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
pin_project! {
|
||||||
|
#[project = ChunkedReadFileStateProj]
|
||||||
|
#[project_replace = ChunkedReadFileStateProjReplace]
|
||||||
|
enum ChunkedReadFileState<Fut> {
|
||||||
|
File { file: Option<File>, },
|
||||||
|
Future { #[pin] fut: Fut },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
pin_project! {
|
||||||
|
#[project = ChunkedReadFileStateProj]
|
||||||
|
#[project_replace = ChunkedReadFileStateProjReplace]
|
||||||
|
enum ChunkedReadFileState<Fut> {
|
||||||
|
File { file: Option<(File, BytesMut)> },
|
||||||
|
Future { #[pin] fut: Fut },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F, Fut> fmt::Debug for ChunkedReadFile<F, Fut> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str("ChunkedReadFile")
|
f.write_str("ChunkedReadFile")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stream for ChunkedReadFile {
|
pub(crate) fn new_chunked_read(
|
||||||
type Item = Result<Bytes, Error>;
|
size: u64,
|
||||||
|
offset: u64,
|
||||||
|
file: File,
|
||||||
|
) -> impl Stream<Item = Result<Bytes, Error>> {
|
||||||
|
ChunkedReadFile {
|
||||||
|
size,
|
||||||
|
offset,
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
state: ChunkedReadFileState::File { file: Some(file) },
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
state: ChunkedReadFileState::File {
|
||||||
|
file: Some((file, BytesMut::new())),
|
||||||
|
},
|
||||||
|
counter: 0,
|
||||||
|
callback: chunked_read_file_callback,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
let this = self.as_mut().get_mut();
|
async fn chunked_read_file_callback(
|
||||||
match this.state {
|
mut file: File,
|
||||||
ChunkedReadFileState::File(ref mut file) => {
|
offset: u64,
|
||||||
let size = this.size;
|
max_bytes: usize,
|
||||||
let offset = this.offset;
|
) -> Result<(File, Bytes), Error> {
|
||||||
let counter = this.counter;
|
use io::{Read as _, Seek as _};
|
||||||
|
|
||||||
if size == counter {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
let mut file = file
|
|
||||||
.take()
|
|
||||||
.expect("ChunkedReadFile polled after completion");
|
|
||||||
|
|
||||||
let fut = spawn_blocking(move || {
|
|
||||||
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
|
|
||||||
|
|
||||||
|
let res = actix_web::rt::task::spawn_blocking(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 =
|
let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
|
||||||
file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
|
|
||||||
|
if n_bytes == 0 {
|
||||||
|
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
|
||||||
|
} else {
|
||||||
|
Ok((file, Bytes::from(buf)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| actix_web::error::BlockingError)??;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
async fn chunked_read_file_callback(
|
||||||
|
file: File,
|
||||||
|
offset: u64,
|
||||||
|
max_bytes: usize,
|
||||||
|
mut bytes_mut: BytesMut,
|
||||||
|
) -> io::Result<(File, Bytes, BytesMut)> {
|
||||||
|
bytes_mut.reserve(max_bytes);
|
||||||
|
|
||||||
|
let (res, mut bytes_mut) = file.read_at(bytes_mut, offset).await;
|
||||||
|
let n_bytes = res?;
|
||||||
|
|
||||||
if n_bytes == 0 {
|
if n_bytes == 0 {
|
||||||
return Err(io::ErrorKind::UnexpectedEof.into());
|
return Err(io::ErrorKind::UnexpectedEof.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((file, Bytes::from(buf)))
|
let bytes = bytes_mut.split_to(n_bytes).freeze();
|
||||||
});
|
|
||||||
this.state = ChunkedReadFileState::Future(fut);
|
Ok((file, bytes, bytes_mut))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
|
||||||
|
where
|
||||||
|
F: Fn(File, u64, usize, BytesMut) -> Fut,
|
||||||
|
Fut: Future<Output = io::Result<(File, Bytes, BytesMut)>>,
|
||||||
|
{
|
||||||
|
type Item = Result<Bytes, Error>;
|
||||||
|
|
||||||
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
|
let mut this = self.as_mut().project();
|
||||||
|
match this.state.as_mut().project() {
|
||||||
|
ChunkedReadFileStateProj::File { file } => {
|
||||||
|
let size = *this.size;
|
||||||
|
let offset = *this.offset;
|
||||||
|
let counter = *this.counter;
|
||||||
|
|
||||||
|
if size == counter {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
|
||||||
|
|
||||||
|
let (file, bytes_mut) = file
|
||||||
|
.take()
|
||||||
|
.expect("ChunkedReadFile polled after completion");
|
||||||
|
|
||||||
|
let fut = (this.callback)(file, offset, max_bytes, bytes_mut);
|
||||||
|
|
||||||
|
this.state
|
||||||
|
.project_replace(ChunkedReadFileState::Future { fut });
|
||||||
|
|
||||||
self.poll_next(cx)
|
self.poll_next(cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChunkedReadFileState::Future(ref mut fut) => {
|
ChunkedReadFileStateProj::Future { fut } => {
|
||||||
let (file, bytes) =
|
let (file, bytes, bytes_mut) = ready!(fut.poll(cx))?;
|
||||||
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
|
|
||||||
this.state = ChunkedReadFileState::File(Some(file));
|
|
||||||
|
|
||||||
this.offset += bytes.len() as u64;
|
this.state.project_replace(ChunkedReadFileState::File {
|
||||||
this.counter += bytes.len() as u64;
|
file: Some((file, bytes_mut)),
|
||||||
|
});
|
||||||
|
|
||||||
|
*this.offset += bytes.len() as u64;
|
||||||
|
*this.counter += bytes.len() as u64;
|
||||||
|
|
||||||
|
Poll::Ready(Some(Ok(bytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
|
||||||
|
where
|
||||||
|
F: Fn(File, u64, usize) -> Fut,
|
||||||
|
Fut: Future<Output = Result<(File, Bytes), Error>>,
|
||||||
|
{
|
||||||
|
type Item = Result<Bytes, Error>;
|
||||||
|
|
||||||
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
|
let mut this = self.as_mut().project();
|
||||||
|
match this.state.as_mut().project() {
|
||||||
|
ChunkedReadFileStateProj::File { file } => {
|
||||||
|
let size = *this.size;
|
||||||
|
let offset = *this.offset;
|
||||||
|
let counter = *this.counter;
|
||||||
|
|
||||||
|
if size == counter {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
|
||||||
|
|
||||||
|
let file = file
|
||||||
|
.take()
|
||||||
|
.expect("ChunkedReadFile polled after completion");
|
||||||
|
|
||||||
|
let fut = (this.callback)(file, offset, max_bytes);
|
||||||
|
|
||||||
|
this.state
|
||||||
|
.project_replace(ChunkedReadFileState::Future { fut });
|
||||||
|
|
||||||
|
self.poll_next(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChunkedReadFileStateProj::Future { fut } => {
|
||||||
|
let (file, bytes) = ready!(fut.poll(cx))?;
|
||||||
|
|
||||||
|
this.state
|
||||||
|
.project_replace(ChunkedReadFileState::File { file: Some(file) });
|
||||||
|
|
||||||
|
*this.offset += bytes.len() as u64;
|
||||||
|
*this.counter += bytes.len() as u64;
|
||||||
|
|
||||||
Poll::Ready(Some(Ok(bytes)))
|
Poll::Ready(Some(Ok(bytes)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,14 +40,23 @@ impl Directory {
|
|||||||
pub(crate) type DirectoryRenderer =
|
pub(crate) type DirectoryRenderer =
|
||||||
dyn Fn(&Directory, &HttpRequest) -> Result<ServiceResponse, io::Error>;
|
dyn Fn(&Directory, &HttpRequest) -> Result<ServiceResponse, io::Error>;
|
||||||
|
|
||||||
// show file url as relative to static path
|
/// Returns percent encoded file URL path.
|
||||||
macro_rules! encode_file_url {
|
macro_rules! encode_file_url {
|
||||||
($path:ident) => {
|
($path:ident) => {
|
||||||
utf8_percent_encode(&$path, CONTROLS)
|
utf8_percent_encode(&$path, CONTROLS)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// " -- " & -- & ' -- ' < -- < > -- > / -- /
|
/// Returns HTML entity encoded formatter.
|
||||||
|
///
|
||||||
|
/// ```plain
|
||||||
|
/// " => "
|
||||||
|
/// & => &
|
||||||
|
/// ' => '
|
||||||
|
/// < => <
|
||||||
|
/// > => >
|
||||||
|
/// / => /
|
||||||
|
/// ```
|
||||||
macro_rules! encode_file_name {
|
macro_rules! encode_file_name {
|
||||||
($entry:ident) => {
|
($entry:ident) => {
|
||||||
escape_html_entity(&$entry.file_name().to_string_lossy(), Html)
|
escape_html_entity(&$entry.file_name().to_string_lossy(), Html)
|
||||||
|
|||||||
@@ -23,16 +23,23 @@ impl ResponseError for FilesError {
|
|||||||
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
#[allow(clippy::enum_variant_names)]
|
||||||
#[derive(Display, Debug, PartialEq)]
|
#[derive(Display, Debug, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub enum UriSegmentError {
|
pub enum UriSegmentError {
|
||||||
/// The segment started with the wrapped invalid character.
|
/// The segment started with the wrapped invalid character.
|
||||||
#[display(fmt = "The segment started with the wrapped invalid character")]
|
#[display(fmt = "The segment started with the wrapped invalid character")]
|
||||||
BadStart(char),
|
BadStart(char),
|
||||||
|
|
||||||
/// The segment contained the wrapped invalid character.
|
/// The segment contained the wrapped invalid character.
|
||||||
#[display(fmt = "The segment contained the wrapped invalid character")]
|
#[display(fmt = "The segment contained the wrapped invalid character")]
|
||||||
BadChar(char),
|
BadChar(char),
|
||||||
|
|
||||||
/// The segment ended with the wrapped invalid character.
|
/// The segment ended with the wrapped invalid character.
|
||||||
#[display(fmt = "The segment ended with the wrapped invalid character")]
|
#[display(fmt = "The segment ended with the wrapped invalid character")]
|
||||||
BadEnd(char),
|
BadEnd(char),
|
||||||
|
|
||||||
|
/// The path is not a valid UTF-8 string after doing percent decoding.
|
||||||
|
#[display(fmt = "The path is not a valid UTF-8 string after percent-decoding")]
|
||||||
|
NotValidUtf8,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `BadRequest` for `UriSegmentError`
|
/// Return `BadRequest` for `UriSegmentError`
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
|
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
|
||||||
use actix_utils::future::ok;
|
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
dev::{
|
dev::{
|
||||||
AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest,
|
AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest,
|
||||||
@@ -20,14 +19,16 @@ use actix_web::{
|
|||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService,
|
directory_listing, named,
|
||||||
MimeOverride, PathFilter,
|
service::{FilesService, FilesServiceInner},
|
||||||
|
Directory, DirectoryRenderer, HttpNewService, MimeOverride, PathFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Static files handling service.
|
/// Static files handling service.
|
||||||
///
|
///
|
||||||
/// `Files` service must be registered with `App::service()` method.
|
/// `Files` service must be registered with `App::service()` method.
|
||||||
///
|
///
|
||||||
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::App;
|
/// use actix_web::App;
|
||||||
/// use actix_files::Files;
|
/// use actix_files::Files;
|
||||||
@@ -36,7 +37,7 @@ use crate::{
|
|||||||
/// .service(Files::new("/static", "."));
|
/// .service(Files::new("/static", "."));
|
||||||
/// ```
|
/// ```
|
||||||
pub struct Files {
|
pub struct Files {
|
||||||
path: String,
|
mount_path: String,
|
||||||
directory: PathBuf,
|
directory: PathBuf,
|
||||||
index: Option<String>,
|
index: Option<String>,
|
||||||
show_index: bool,
|
show_index: bool,
|
||||||
@@ -67,7 +68,7 @@ impl Clone for Files {
|
|||||||
default: self.default.clone(),
|
default: self.default.clone(),
|
||||||
renderer: self.renderer.clone(),
|
renderer: self.renderer.clone(),
|
||||||
file_flags: self.file_flags,
|
file_flags: self.file_flags,
|
||||||
path: self.path.clone(),
|
mount_path: self.mount_path.clone(),
|
||||||
mime_override: self.mime_override.clone(),
|
mime_override: self.mime_override.clone(),
|
||||||
path_filter: self.path_filter.clone(),
|
path_filter: self.path_filter.clone(),
|
||||||
use_guards: self.use_guards.clone(),
|
use_guards: self.use_guards.clone(),
|
||||||
@@ -106,7 +107,7 @@ impl Files {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Files {
|
Files {
|
||||||
path: mount_path.trim_end_matches('/').to_owned(),
|
mount_path: mount_path.trim_end_matches('/').to_owned(),
|
||||||
directory: dir,
|
directory: dir,
|
||||||
index: None,
|
index: None,
|
||||||
show_index: false,
|
show_index: false,
|
||||||
@@ -262,9 +263,9 @@ impl Files {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// See [`Files::method_guard`].
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[deprecated(since = "0.6.0", note = "Renamed to `method_guard`.")]
|
#[deprecated(since = "0.6.0", note = "Renamed to `method_guard`.")]
|
||||||
/// See [`Files::method_guard`].
|
|
||||||
pub fn use_guards<G: Guard + 'static>(self, guard: G) -> Self {
|
pub fn use_guards<G: Guard + 'static>(self, guard: G) -> Self {
|
||||||
self.method_guard(guard)
|
self.method_guard(guard)
|
||||||
}
|
}
|
||||||
@@ -283,11 +284,17 @@ impl Files {
|
|||||||
/// Setting a fallback static file handler:
|
/// Setting a fallback static file handler:
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_files::{Files, NamedFile};
|
/// use actix_files::{Files, NamedFile};
|
||||||
|
/// use actix_web::dev::{ServiceRequest, ServiceResponse, fn_service};
|
||||||
///
|
///
|
||||||
/// # fn run() -> Result<(), actix_web::Error> {
|
/// # fn run() -> Result<(), actix_web::Error> {
|
||||||
/// let files = Files::new("/", "./static")
|
/// let files = Files::new("/", "./static")
|
||||||
/// .index_file("index.html")
|
/// .index_file("index.html")
|
||||||
/// .default_handler(NamedFile::open("./static/404.html")?);
|
/// .default_handler(fn_service(|req: ServiceRequest| async {
|
||||||
|
/// let (req, _) = req.into_parts();
|
||||||
|
/// let file = NamedFile::open_async("./static/404.html").await?;
|
||||||
|
/// let res = file.into_response(&req);
|
||||||
|
/// Ok(ServiceResponse::new(req, res))
|
||||||
|
/// }));
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
@@ -335,9 +342,9 @@ impl HttpServiceFactory for Files {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let rdef = if config.is_root() {
|
let rdef = if config.is_root() {
|
||||||
ResourceDef::root_prefix(&self.path)
|
ResourceDef::root_prefix(&self.mount_path)
|
||||||
} else {
|
} else {
|
||||||
ResourceDef::prefix(&self.path)
|
ResourceDef::prefix(&self.mount_path)
|
||||||
};
|
};
|
||||||
|
|
||||||
config.register_service(rdef, guards, self, None)
|
config.register_service(rdef, guards, self, None)
|
||||||
@@ -353,7 +360,7 @@ impl ServiceFactory<ServiceRequest> for Files {
|
|||||||
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||||
|
|
||||||
fn new_service(&self, _: ()) -> Self::Future {
|
fn new_service(&self, _: ()) -> Self::Future {
|
||||||
let mut srv = FilesService {
|
let mut inner = FilesServiceInner {
|
||||||
directory: self.directory.clone(),
|
directory: self.directory.clone(),
|
||||||
index: self.index.clone(),
|
index: self.index.clone(),
|
||||||
show_index: self.show_index,
|
show_index: self.show_index,
|
||||||
@@ -372,14 +379,14 @@ impl ServiceFactory<ServiceRequest> for Files {
|
|||||||
Box::pin(async {
|
Box::pin(async {
|
||||||
match fut.await {
|
match fut.await {
|
||||||
Ok(default) => {
|
Ok(default) => {
|
||||||
srv.default = Some(default);
|
inner.default = Some(default);
|
||||||
Ok(srv)
|
Ok(FilesService(Rc::new(inner)))
|
||||||
}
|
}
|
||||||
Err(_) => Err(()),
|
Err(_) => Err(()),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Box::pin(ok(srv))
|
Box::pin(async move { Ok(FilesService(Rc::new(inner))) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//!
|
//!
|
||||||
//! Provides a non-blocking service for serving static files from disk.
|
//! Provides a non-blocking service for serving static files from disk.
|
||||||
//!
|
//!
|
||||||
//! # Example
|
//! # Examples
|
||||||
//! ```
|
//! ```
|
||||||
//! use actix_web::App;
|
//! use actix_web::App;
|
||||||
//! use actix_files::Files;
|
//! use actix_files::Files;
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
//! .service(Files::new("/static", ".").prefer_utf8(true));
|
//! .service(Files::new("/static", ".").prefer_utf8(true));
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
#![deny(rust_2018_idioms)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
#![warn(missing_docs, missing_debug_implementations)]
|
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
|
||||||
|
|
||||||
use actix_service::boxed::{BoxService, BoxServiceFactory};
|
use actix_service::boxed::{BoxService, BoxServiceFactory};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
@@ -33,12 +33,12 @@ mod path_buf;
|
|||||||
mod range;
|
mod range;
|
||||||
mod service;
|
mod service;
|
||||||
|
|
||||||
pub use crate::chunked::ChunkedReadFile;
|
pub use self::chunked::ChunkedReadFile;
|
||||||
pub use crate::directory::Directory;
|
pub use self::directory::Directory;
|
||||||
pub use crate::files::Files;
|
pub use self::files::Files;
|
||||||
pub use crate::named::NamedFile;
|
pub use self::named::NamedFile;
|
||||||
pub use crate::range::HttpRange;
|
pub use self::range::HttpRange;
|
||||||
pub use crate::service::FilesService;
|
pub use self::service::FilesService;
|
||||||
|
|
||||||
use self::directory::{directory_listing, DirectoryRenderer};
|
use self::directory::{directory_listing, DirectoryRenderer};
|
||||||
use self::error::FilesError;
|
use self::error::FilesError;
|
||||||
@@ -62,14 +62,13 @@ type PathFilter = dyn Fn(&Path, &RequestHead) -> bool;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{
|
use std::{
|
||||||
fs::{self, File},
|
fs::{self},
|
||||||
ops::Add,
|
ops::Add,
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
use actix_service::ServiceFactory;
|
|
||||||
use actix_utils::future::ok;
|
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
|
dev::ServiceFactory,
|
||||||
guard,
|
guard,
|
||||||
http::{
|
http::{
|
||||||
header::{self, ContentDisposition, DispositionParam, DispositionType},
|
header::{self, ContentDisposition, DispositionParam, DispositionType},
|
||||||
@@ -82,8 +81,9 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::named::File;
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_web::test]
|
||||||
async fn test_file_extension_to_mime() {
|
async fn test_file_extension_to_mime() {
|
||||||
let m = file_extension_to_mime("");
|
let m = file_extension_to_mime("");
|
||||||
assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
|
assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
|
||||||
@@ -100,69 +100,69 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_if_modified_since_without_if_none_match() {
|
async fn test_if_modified_since_without_if_none_match() {
|
||||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
|
let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
|
||||||
|
|
||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.insert_header((header::IF_MODIFIED_SINCE, since))
|
.insert_header((header::IF_MODIFIED_SINCE, since))
|
||||||
.to_http_request();
|
.to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
|
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_if_modified_since_without_if_none_match_same() {
|
async fn test_if_modified_since_without_if_none_match_same() {
|
||||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
let since = file.last_modified().unwrap();
|
let since = file.last_modified().unwrap();
|
||||||
|
|
||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.insert_header((header::IF_MODIFIED_SINCE, since))
|
.insert_header((header::IF_MODIFIED_SINCE, since))
|
||||||
.to_http_request();
|
.to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
|
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_if_modified_since_with_if_none_match() {
|
async fn test_if_modified_since_with_if_none_match() {
|
||||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
|
let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
|
||||||
|
|
||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.insert_header((header::IF_NONE_MATCH, "miss_etag"))
|
.insert_header((header::IF_NONE_MATCH, "miss_etag"))
|
||||||
.insert_header((header::IF_MODIFIED_SINCE, since))
|
.insert_header((header::IF_MODIFIED_SINCE, since))
|
||||||
.to_http_request();
|
.to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req);
|
||||||
assert_ne!(resp.status(), StatusCode::NOT_MODIFIED);
|
assert_ne!(resp.status(), StatusCode::NOT_MODIFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_if_unmodified_since() {
|
async fn test_if_unmodified_since() {
|
||||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
let since = file.last_modified().unwrap();
|
let since = file.last_modified().unwrap();
|
||||||
|
|
||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.insert_header((header::IF_UNMODIFIED_SINCE, since))
|
.insert_header((header::IF_UNMODIFIED_SINCE, since))
|
||||||
.to_http_request();
|
.to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(resp.status(), StatusCode::OK);
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_if_unmodified_since_failed() {
|
async fn test_if_unmodified_since_failed() {
|
||||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
let since = header::HttpDate::from(SystemTime::UNIX_EPOCH);
|
let since = header::HttpDate::from(SystemTime::UNIX_EPOCH);
|
||||||
|
|
||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.insert_header((header::IF_UNMODIFIED_SINCE, since))
|
.insert_header((header::IF_UNMODIFIED_SINCE, since))
|
||||||
.to_http_request();
|
.to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED);
|
assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_text() {
|
async fn test_named_file_text() {
|
||||||
assert!(NamedFile::open("test--").is_err());
|
assert!(NamedFile::open_async("test--").await.is_err());
|
||||||
let mut file = NamedFile::open("Cargo.toml").unwrap();
|
let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
{
|
{
|
||||||
file.file();
|
file.file();
|
||||||
let _f: &File = &file;
|
let _f: &File = &file;
|
||||||
@@ -172,7 +172,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||||
"text/x-toml"
|
"text/x-toml"
|
||||||
@@ -185,8 +185,8 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_content_disposition() {
|
async fn test_named_file_content_disposition() {
|
||||||
assert!(NamedFile::open("test--").is_err());
|
assert!(NamedFile::open_async("test--").await.is_err());
|
||||||
let mut file = NamedFile::open("Cargo.toml").unwrap();
|
let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
{
|
{
|
||||||
file.file();
|
file.file();
|
||||||
let _f: &File = &file;
|
let _f: &File = &file;
|
||||||
@@ -196,24 +196,36 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||||
"inline; filename=\"Cargo.toml\""
|
"inline; filename=\"Cargo.toml\""
|
||||||
);
|
);
|
||||||
|
|
||||||
let file = NamedFile::open("Cargo.toml")
|
let file = NamedFile::open_async("Cargo.toml")
|
||||||
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.disable_content_disposition();
|
.disable_content_disposition();
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req);
|
||||||
assert!(resp.headers().get(header::CONTENT_DISPOSITION).is_none());
|
assert!(resp.headers().get(header::CONTENT_DISPOSITION).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_non_ascii_file_name() {
|
async fn test_named_file_non_ascii_file_name() {
|
||||||
let mut file =
|
let file = {
|
||||||
NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml").unwrap();
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
{
|
||||||
|
crate::named::File::open("Cargo.toml").await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
{
|
||||||
|
crate::named::File::open("Cargo.toml").unwrap()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut file = NamedFile::from_file(file, "貨物.toml").unwrap();
|
||||||
{
|
{
|
||||||
file.file();
|
file.file();
|
||||||
let _f: &File = &file;
|
let _f: &File = &file;
|
||||||
@@ -223,7 +235,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||||
"text/x-toml"
|
"text/x-toml"
|
||||||
@@ -236,7 +248,8 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_set_content_type() {
|
async fn test_named_file_set_content_type() {
|
||||||
let mut file = NamedFile::open("Cargo.toml")
|
let mut file = NamedFile::open_async("Cargo.toml")
|
||||||
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_content_type(mime::TEXT_XML);
|
.set_content_type(mime::TEXT_XML);
|
||||||
{
|
{
|
||||||
@@ -248,7 +261,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||||
"text/xml"
|
"text/xml"
|
||||||
@@ -261,7 +274,7 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_image() {
|
async fn test_named_file_image() {
|
||||||
let mut file = NamedFile::open("tests/test.png").unwrap();
|
let mut file = NamedFile::open_async("tests/test.png").await.unwrap();
|
||||||
{
|
{
|
||||||
file.file();
|
file.file();
|
||||||
let _f: &File = &file;
|
let _f: &File = &file;
|
||||||
@@ -271,7 +284,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||||
"image/png"
|
"image/png"
|
||||||
@@ -284,13 +297,13 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_javascript() {
|
async fn test_named_file_javascript() {
|
||||||
let file = NamedFile::open("tests/test.js").unwrap();
|
let file = NamedFile::open_async("tests/test.js").await.unwrap();
|
||||||
|
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||||
"application/javascript"
|
"application/javascript; charset=utf-8"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||||
@@ -304,7 +317,8 @@ mod tests {
|
|||||||
disposition: DispositionType::Attachment,
|
disposition: DispositionType::Attachment,
|
||||||
parameters: vec![DispositionParam::Filename(String::from("test.png"))],
|
parameters: vec![DispositionParam::Filename(String::from("test.png"))],
|
||||||
};
|
};
|
||||||
let mut file = NamedFile::open("tests/test.png")
|
let mut file = NamedFile::open_async("tests/test.png")
|
||||||
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_content_disposition(cd);
|
.set_content_disposition(cd);
|
||||||
{
|
{
|
||||||
@@ -316,7 +330,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||||
"image/png"
|
"image/png"
|
||||||
@@ -329,7 +343,7 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_binary() {
|
async fn test_named_file_binary() {
|
||||||
let mut file = NamedFile::open("tests/test.binary").unwrap();
|
let mut file = NamedFile::open_async("tests/test.binary").await.unwrap();
|
||||||
{
|
{
|
||||||
file.file();
|
file.file();
|
||||||
let _f: &File = &file;
|
let _f: &File = &file;
|
||||||
@@ -339,7 +353,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||||
"application/octet-stream"
|
"application/octet-stream"
|
||||||
@@ -352,7 +366,8 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_status_code_text() {
|
async fn test_named_file_status_code_text() {
|
||||||
let mut file = NamedFile::open("Cargo.toml")
|
let mut file = NamedFile::open_async("Cargo.toml")
|
||||||
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_status_code(StatusCode::NOT_FOUND);
|
.set_status_code(StatusCode::NOT_FOUND);
|
||||||
{
|
{
|
||||||
@@ -364,7 +379,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||||
"text/x-toml"
|
"text/x-toml"
|
||||||
@@ -568,7 +583,8 @@ mod tests {
|
|||||||
async fn test_named_file_content_encoding() {
|
async fn test_named_file_content_encoding() {
|
||||||
let srv = test::init_service(App::new().wrap(Compress::default()).service(
|
let srv = test::init_service(App::new().wrap(Compress::default()).service(
|
||||||
web::resource("/").to(|| async {
|
web::resource("/").to(|| async {
|
||||||
NamedFile::open("Cargo.toml")
|
NamedFile::open_async("Cargo.toml")
|
||||||
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_content_encoding(header::ContentEncoding::Identity)
|
.set_content_encoding(header::ContentEncoding::Identity)
|
||||||
}),
|
}),
|
||||||
@@ -581,14 +597,16 @@ mod tests {
|
|||||||
.to_request();
|
.to_request();
|
||||||
let res = test::call_service(&srv, request).await;
|
let res = test::call_service(&srv, request).await;
|
||||||
assert_eq!(res.status(), StatusCode::OK);
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
assert!(!res.headers().contains_key(header::CONTENT_ENCODING));
|
assert!(res.headers().contains_key(header::CONTENT_ENCODING));
|
||||||
|
assert!(!test::read_body(res).await.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_content_encoding_gzip() {
|
async fn test_named_file_content_encoding_gzip() {
|
||||||
let srv = test::init_service(App::new().wrap(Compress::default()).service(
|
let srv = test::init_service(App::new().wrap(Compress::default()).service(
|
||||||
web::resource("/").to(|| async {
|
web::resource("/").to(|| async {
|
||||||
NamedFile::open("Cargo.toml")
|
NamedFile::open_async("Cargo.toml")
|
||||||
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_content_encoding(header::ContentEncoding::Gzip)
|
.set_content_encoding(header::ContentEncoding::Gzip)
|
||||||
}),
|
}),
|
||||||
@@ -614,8 +632,8 @@ mod tests {
|
|||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_allowed_method() {
|
async fn test_named_file_allowed_method() {
|
||||||
let req = TestRequest::default().method(Method::GET).to_http_request();
|
let req = TestRequest::default().method(Method::GET).to_http_request();
|
||||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(resp.status(), StatusCode::OK);
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,8 +723,8 @@ mod tests {
|
|||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_default_handler_file_missing() {
|
async fn test_default_handler_file_missing() {
|
||||||
let st = Files::new("/", ".")
|
let st = Files::new("/", ".")
|
||||||
.default_handler(|req: ServiceRequest| {
|
.default_handler(|req: ServiceRequest| async {
|
||||||
ok(req.into_response(HttpResponse::Ok().body("default content")))
|
Ok(req.into_response(HttpResponse::Ok().body("default content")))
|
||||||
})
|
})
|
||||||
.new_service(())
|
.new_service(())
|
||||||
.await
|
.await
|
||||||
@@ -785,13 +803,44 @@ mod tests {
|
|||||||
let req = TestRequest::get().uri("/test/%43argo.toml").to_request();
|
let req = TestRequest::get().uri("/test/%43argo.toml").to_request();
|
||||||
let res = test::call_service(&srv, req).await;
|
let res = test::call_service(&srv, req).await;
|
||||||
assert_eq!(res.status(), StatusCode::OK);
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
// `%2F` == `/`
|
||||||
|
let req = TestRequest::get().uri("/test%2Ftest.binary").to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
|
let req = TestRequest::get().uri("/test/Cargo.toml%00").to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_percent_encoding_2() {
|
||||||
|
let tmpdir = tempfile::tempdir().unwrap();
|
||||||
|
let filename = match cfg!(unix) {
|
||||||
|
true => "ض:?#[]{}<>()@!$&'`|*+,;= %20.test",
|
||||||
|
false => "ض#[]{}()@!$&'`+,;= %20.test",
|
||||||
|
};
|
||||||
|
let filename_encoded = filename
|
||||||
|
.as_bytes()
|
||||||
|
.iter()
|
||||||
|
.map(|c| format!("%{:02X}", c))
|
||||||
|
.collect::<String>();
|
||||||
|
std::fs::File::create(tmpdir.path().join(filename)).unwrap();
|
||||||
|
|
||||||
|
let srv = test::init_service(App::new().service(Files::new("", tmpdir.path()))).await;
|
||||||
|
|
||||||
|
let req = TestRequest::get()
|
||||||
|
.uri(&format!("/{}", filename_encoded))
|
||||||
|
.to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_serve_named_file() {
|
async fn test_serve_named_file() {
|
||||||
let srv =
|
let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
test::init_service(App::new().service(NamedFile::open("Cargo.toml").unwrap()))
|
let srv = test::init_service(App::new().service(factory)).await;
|
||||||
.await;
|
|
||||||
|
|
||||||
let req = TestRequest::get().uri("/Cargo.toml").to_request();
|
let req = TestRequest::get().uri("/Cargo.toml").to_request();
|
||||||
let res = test::call_service(&srv, req).await;
|
let res = test::call_service(&srv, req).await;
|
||||||
@@ -808,11 +857,9 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_serve_named_file_prefix() {
|
async fn test_serve_named_file_prefix() {
|
||||||
let srv = test::init_service(
|
let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
App::new()
|
let srv =
|
||||||
.service(web::scope("/test").service(NamedFile::open("Cargo.toml").unwrap())),
|
test::init_service(App::new().service(web::scope("/test").service(factory))).await;
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let req = TestRequest::get().uri("/test/Cargo.toml").to_request();
|
let req = TestRequest::get().uri("/test/Cargo.toml").to_request();
|
||||||
let res = test::call_service(&srv, req).await;
|
let res = test::call_service(&srv, req).await;
|
||||||
@@ -829,10 +876,8 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_default_service() {
|
async fn test_named_file_default_service() {
|
||||||
let srv = test::init_service(
|
let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
App::new().default_service(NamedFile::open("Cargo.toml").unwrap()),
|
let srv = test::init_service(App::new().default_service(factory)).await;
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
for route in ["/foobar", "/baz", "/"].iter() {
|
for route in ["/foobar", "/baz", "/"].iter() {
|
||||||
let req = TestRequest::get().uri(route).to_request();
|
let req = TestRequest::get().uri(route).to_request();
|
||||||
@@ -847,8 +892,9 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_default_handler_named_file() {
|
async fn test_default_handler_named_file() {
|
||||||
|
let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
let st = Files::new("/", ".")
|
let st = Files::new("/", ".")
|
||||||
.default_handler(NamedFile::open("Cargo.toml").unwrap())
|
.default_handler(factory)
|
||||||
.new_service(())
|
.new_service(())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -926,8 +972,8 @@ mod tests {
|
|||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_default_handler_filter() {
|
async fn test_default_handler_filter() {
|
||||||
let st = Files::new("/", ".")
|
let st = Files::new("/", ".")
|
||||||
.default_handler(|req: ServiceRequest| {
|
.default_handler(|req: ServiceRequest| async {
|
||||||
ok(req.into_response(HttpResponse::Ok().body("default content")))
|
Ok(req.into_response(HttpResponse::Ok().body("default content")))
|
||||||
})
|
})
|
||||||
.path_filter(|path, _| path.extension() == Some("png".as_ref()))
|
.path_filter(|path, _| path.extension() == Some("png".as_ref()))
|
||||||
.new_service(())
|
.new_service(())
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
use actix_service::{Service, ServiceFactory};
|
use std::{
|
||||||
use actix_utils::future::{ok, ready, Ready};
|
fs::Metadata,
|
||||||
use actix_web::dev::{AppService, HttpServiceFactory, ResourceDef};
|
io,
|
||||||
use std::fs::{File, Metadata};
|
path::{Path, PathBuf},
|
||||||
use std::io;
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
use std::ops::{Deref, DerefMut};
|
};
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
use std::os::unix::fs::MetadataExt;
|
|
||||||
|
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
dev::{BodyEncoding, ServiceRequest, ServiceResponse, SizedStream},
|
body::{self, BoxBody, SizedStream},
|
||||||
|
dev::{
|
||||||
|
self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory,
|
||||||
|
ServiceRequest, ServiceResponse,
|
||||||
|
},
|
||||||
http::{
|
http::{
|
||||||
header::{
|
header::{
|
||||||
self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
|
self, Charset, ContentDisposition, ContentEncoding, DispositionParam,
|
||||||
|
DispositionType, ExtendedValue, HeaderValue,
|
||||||
},
|
},
|
||||||
ContentEncoding, StatusCode,
|
StatusCode,
|
||||||
},
|
},
|
||||||
Error, HttpMessage, HttpRequest, HttpResponse, Responder,
|
Error, HttpMessage, HttpRequest, HttpResponse, Responder,
|
||||||
};
|
};
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
|
use derive_more::{Deref, DerefMut};
|
||||||
|
use futures_core::future::LocalBoxFuture;
|
||||||
use mime_guess::from_path;
|
use mime_guess::from_path;
|
||||||
|
|
||||||
use crate::ChunkedReadFile;
|
|
||||||
use crate::{encoding::equiv_utf8_text, range::HttpRange};
|
use crate::{encoding::equiv_utf8_text, range::HttpRange};
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
@@ -37,7 +38,7 @@ bitflags! {
|
|||||||
|
|
||||||
impl Default for Flags {
|
impl Default for Flags {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Flags::from_bits_truncate(0b0000_0111)
|
Flags::from_bits_truncate(0b0000_1111)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,9 +49,9 @@ impl Default for Flags {
|
|||||||
/// use actix_web::App;
|
/// use actix_web::App;
|
||||||
/// use actix_files::NamedFile;
|
/// use actix_files::NamedFile;
|
||||||
///
|
///
|
||||||
/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
|
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let app = App::new()
|
/// let file = NamedFile::open_async("./static/index.html").await?;
|
||||||
/// .service(NamedFile::open("./static/index.html")?);
|
/// let app = App::new().service(file);
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
@@ -62,13 +63,15 @@ impl Default for Flags {
|
|||||||
///
|
///
|
||||||
/// #[get("/")]
|
/// #[get("/")]
|
||||||
/// async fn index() -> impl Responder {
|
/// async fn index() -> impl Responder {
|
||||||
/// NamedFile::open("./static/index.html")
|
/// NamedFile::open_async("./static/index.html").await
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Deref, DerefMut)]
|
||||||
pub struct NamedFile {
|
pub struct NamedFile {
|
||||||
path: PathBuf,
|
#[deref]
|
||||||
|
#[deref_mut]
|
||||||
file: File,
|
file: File,
|
||||||
|
path: PathBuf,
|
||||||
modified: Option<SystemTime>,
|
modified: Option<SystemTime>,
|
||||||
pub(crate) md: Metadata,
|
pub(crate) md: Metadata,
|
||||||
pub(crate) flags: Flags,
|
pub(crate) flags: Flags,
|
||||||
@@ -78,6 +81,13 @@ pub struct NamedFile {
|
|||||||
pub(crate) encoding: Option<ContentEncoding>,
|
pub(crate) encoding: Option<ContentEncoding>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
pub(crate) use std::fs::File;
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
pub(crate) use tokio_uring::fs::File;
|
||||||
|
|
||||||
|
use super::chunked;
|
||||||
|
|
||||||
impl NamedFile {
|
impl NamedFile {
|
||||||
/// Creates an instance from a previously opened file.
|
/// Creates an instance from a previously opened file.
|
||||||
///
|
///
|
||||||
@@ -85,8 +95,7 @@ impl NamedFile {
|
|||||||
/// `ContentDisposition` headers.
|
/// `ContentDisposition` headers.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
/// ```ignore
|
||||||
/// ```
|
|
||||||
/// use actix_files::NamedFile;
|
/// use actix_files::NamedFile;
|
||||||
/// use std::io::{self, Write};
|
/// use std::io::{self, Write};
|
||||||
/// use std::env;
|
/// use std::env;
|
||||||
@@ -147,7 +156,30 @@ impl NamedFile {
|
|||||||
(ct, cd)
|
(ct, cd)
|
||||||
};
|
};
|
||||||
|
|
||||||
let md = file.metadata()?;
|
let md = {
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
{
|
||||||
|
file.metadata()?
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
{
|
||||||
|
use std::os::unix::prelude::{AsRawFd, FromRawFd};
|
||||||
|
|
||||||
|
let fd = file.as_raw_fd();
|
||||||
|
|
||||||
|
// SAFETY: fd is borrowed and lives longer than the unsafe block
|
||||||
|
unsafe {
|
||||||
|
let file = std::fs::File::from_raw_fd(fd);
|
||||||
|
let md = file.metadata();
|
||||||
|
// SAFETY: forget the fd before exiting block in success or error case but don't
|
||||||
|
// run destructor (that would close file handle)
|
||||||
|
std::mem::forget(file);
|
||||||
|
md?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let modified = md.modified().ok();
|
let modified = md.modified().ok();
|
||||||
let encoding = None;
|
let encoding = None;
|
||||||
|
|
||||||
@@ -167,14 +199,43 @@ impl NamedFile {
|
|||||||
/// Attempts to open a file in read-only mode.
|
/// Attempts to open a file in read-only mode.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_files::NamedFile;
|
/// use actix_files::NamedFile;
|
||||||
///
|
|
||||||
/// let file = NamedFile::open("foo.txt");
|
/// let file = NamedFile::open("foo.txt");
|
||||||
/// ```
|
/// ```
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
||||||
Self::from_file(File::open(&path)?, path)
|
let file = File::open(&path)?;
|
||||||
|
Self::from_file(file, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(rustdoc::broken_intra_doc_links)]
|
||||||
|
/// Attempts to open a file asynchronously in read-only mode.
|
||||||
|
///
|
||||||
|
/// When the `experimental-io-uring` crate feature is enabled, this will be async.
|
||||||
|
/// Otherwise, it will be just like [`open`][Self::open].
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use actix_files::NamedFile;
|
||||||
|
/// # async fn open() {
|
||||||
|
/// let file = NamedFile::open_async("foo.txt").await.unwrap();
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub async fn open_async<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
||||||
|
let file = {
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
{
|
||||||
|
File::open(&path)?
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
{
|
||||||
|
File::open(&path).await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::from_file(file, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns reference to the underlying `File` object.
|
/// Returns reference to the underlying `File` object.
|
||||||
@@ -186,13 +247,12 @@ impl NamedFile {
|
|||||||
/// Retrieve the path of this file.
|
/// Retrieve the path of this file.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
|
||||||
/// ```
|
/// ```
|
||||||
/// # use std::io;
|
/// # use std::io;
|
||||||
/// use actix_files::NamedFile;
|
/// use actix_files::NamedFile;
|
||||||
///
|
///
|
||||||
/// # fn path() -> io::Result<()> {
|
/// # async fn path() -> io::Result<()> {
|
||||||
/// let file = NamedFile::open("test.txt")?;
|
/// let file = NamedFile::open_async("test.txt").await?;
|
||||||
/// assert_eq!(file.path().as_os_str(), "foo.txt");
|
/// assert_eq!(file.path().as_os_str(), "foo.txt");
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
@@ -208,23 +268,21 @@ impl NamedFile {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the MIME Content-Type for serving this file. By default
|
/// Set the MIME Content-Type for serving this file. By default the Content-Type is inferred
|
||||||
/// the Content-Type is inferred from the filename extension.
|
/// from the filename extension.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self {
|
pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self {
|
||||||
self.content_type = mime_type;
|
self.content_type = mime_type;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the Content-Disposition for serving this file. This allows
|
/// Set the Content-Disposition for serving this file. This allows changing the
|
||||||
/// changing the inline/attachment disposition as well as the filename
|
/// `inline/attachment` disposition as well as the filename sent to the peer.
|
||||||
/// sent to the peer.
|
|
||||||
///
|
///
|
||||||
/// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and
|
/// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and
|
||||||
/// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise,
|
/// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise, and the
|
||||||
/// and the filename is taken from the path provided in the `open` method
|
/// filename is taken from the path provided in the `open` method after converting it to UTF-8
|
||||||
/// after converting it to UTF-8 using.
|
/// (using `to_string_lossy`).
|
||||||
/// [`std::ffi::OsStr::to_string_lossy`]
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
|
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
|
||||||
self.content_disposition = cd;
|
self.content_disposition = cd;
|
||||||
@@ -241,16 +299,18 @@ impl NamedFile {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set content encoding for serving this file
|
/// Sets content encoding for this file.
|
||||||
///
|
///
|
||||||
/// Must be used with [`actix_web::middleware::Compress`] to take effect.
|
/// This prevents the `Compress` middleware from modifying the file contents and signals to
|
||||||
|
/// browsers/clients how to decode it. For example, if serving a compressed HTML file (e.g.,
|
||||||
|
/// `index.html.gz`) then use `.set_content_encoding(ContentEncoding::Gzip)`.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
|
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
|
||||||
self.encoding = Some(enc);
|
self.encoding = Some(enc);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Specifies whether to use ETag or not.
|
/// Specifies whether to return `ETag` header in response.
|
||||||
///
|
///
|
||||||
/// Default is true.
|
/// Default is true.
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -259,7 +319,7 @@ impl NamedFile {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Specifies whether to use Last-Modified or not.
|
/// Specifies whether to return `Last-Modified` header in response.
|
||||||
///
|
///
|
||||||
/// Default is true.
|
/// Default is true.
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -277,14 +337,18 @@ impl NamedFile {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates an `ETag` in a format is similar to Apache's.
|
||||||
pub(crate) fn etag(&self) -> Option<header::EntityTag> {
|
pub(crate) fn etag(&self) -> Option<header::EntityTag> {
|
||||||
// This etag format is similar to Apache's.
|
|
||||||
self.modified.as_ref().map(|mtime| {
|
self.modified.as_ref().map(|mtime| {
|
||||||
let ino = {
|
let ino = {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::fs::MetadataExt as _;
|
||||||
|
|
||||||
self.md.ino()
|
self.md.ino()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
{
|
{
|
||||||
0
|
0
|
||||||
@@ -295,7 +359,7 @@ impl NamedFile {
|
|||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.expect("modification time must be after epoch");
|
.expect("modification time must be after epoch");
|
||||||
|
|
||||||
header::EntityTag::strong(format!(
|
header::EntityTag::new_strong(format!(
|
||||||
"{:x}:{:x}:{:x}:{:x}",
|
"{:x}:{:x}:{:x}:{:x}",
|
||||||
ino,
|
ino,
|
||||||
self.md.len(),
|
self.md.len(),
|
||||||
@@ -310,16 +374,17 @@ impl NamedFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an `HttpResponse` with file as a streaming body.
|
/// Creates an `HttpResponse` with file as a streaming body.
|
||||||
pub fn into_response(self, req: &HttpRequest) -> HttpResponse {
|
pub fn into_response(self, req: &HttpRequest) -> HttpResponse<BoxBody> {
|
||||||
if self.status_code != StatusCode::OK {
|
if self.status_code != StatusCode::OK {
|
||||||
let mut res = HttpResponse::build(self.status_code);
|
let mut res = HttpResponse::build(self.status_code);
|
||||||
|
|
||||||
if self.flags.contains(Flags::PREFER_UTF8) {
|
let ct = if self.flags.contains(Flags::PREFER_UTF8) {
|
||||||
let ct = equiv_utf8_text(self.content_type.clone());
|
equiv_utf8_text(self.content_type.clone())
|
||||||
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
|
||||||
} else {
|
} else {
|
||||||
res.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
|
self.content_type
|
||||||
}
|
};
|
||||||
|
|
||||||
|
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||||
|
|
||||||
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
||||||
res.insert_header((
|
res.insert_header((
|
||||||
@@ -329,10 +394,10 @@ impl NamedFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(current_encoding) = self.encoding {
|
if let Some(current_encoding) = self.encoding {
|
||||||
res.encoding(current_encoding);
|
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let reader = ChunkedReadFile::new(self.md.len(), 0, self.file);
|
let reader = chunked::new_chunked_read(self.md.len(), 0, self.file);
|
||||||
|
|
||||||
return res.streaming(reader);
|
return res.streaming(reader);
|
||||||
}
|
}
|
||||||
@@ -385,36 +450,36 @@ impl NamedFile {
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut resp = HttpResponse::build(self.status_code);
|
let mut res = HttpResponse::build(self.status_code);
|
||||||
|
|
||||||
if self.flags.contains(Flags::PREFER_UTF8) {
|
let ct = if self.flags.contains(Flags::PREFER_UTF8) {
|
||||||
let ct = equiv_utf8_text(self.content_type.clone());
|
equiv_utf8_text(self.content_type.clone())
|
||||||
resp.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
|
||||||
} else {
|
} else {
|
||||||
resp.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
|
self.content_type
|
||||||
}
|
};
|
||||||
|
|
||||||
|
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||||
|
|
||||||
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
||||||
resp.insert_header((
|
res.insert_header((
|
||||||
header::CONTENT_DISPOSITION,
|
header::CONTENT_DISPOSITION,
|
||||||
self.content_disposition.to_string(),
|
self.content_disposition.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// default compressing
|
|
||||||
if let Some(current_encoding) = self.encoding {
|
if let Some(current_encoding) = self.encoding {
|
||||||
resp.encoding(current_encoding);
|
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(lm) = last_modified {
|
if let Some(lm) = last_modified {
|
||||||
resp.insert_header((header::LAST_MODIFIED, lm.to_string()));
|
res.insert_header((header::LAST_MODIFIED, lm.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(etag) = etag {
|
if let Some(etag) = etag {
|
||||||
resp.insert_header((header::ETAG, etag.to_string()));
|
res.insert_header((header::ETAG, etag.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.insert_header((header::ACCEPT_RANGES, "bytes"));
|
res.insert_header((header::ACCEPT_RANGES, "bytes"));
|
||||||
|
|
||||||
let mut length = self.md.len();
|
let mut length = self.md.len();
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
@@ -426,47 +491,41 @@ impl NamedFile {
|
|||||||
length = ranges[0].length;
|
length = ranges[0].length;
|
||||||
offset = ranges[0].start;
|
offset = ranges[0].start;
|
||||||
|
|
||||||
resp.encoding(ContentEncoding::Identity);
|
// don't allow compression middleware to modify partial content
|
||||||
resp.insert_header((
|
res.insert_header((
|
||||||
|
header::CONTENT_ENCODING,
|
||||||
|
HeaderValue::from_static("identity"),
|
||||||
|
));
|
||||||
|
|
||||||
|
res.insert_header((
|
||||||
header::CONTENT_RANGE,
|
header::CONTENT_RANGE,
|
||||||
format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
|
format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
resp.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length)));
|
res.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length)));
|
||||||
return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
|
return res.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return resp.status(StatusCode::BAD_REQUEST).finish();
|
return res.status(StatusCode::BAD_REQUEST).finish();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if precondition_failed {
|
if precondition_failed {
|
||||||
return resp.status(StatusCode::PRECONDITION_FAILED).finish();
|
return res.status(StatusCode::PRECONDITION_FAILED).finish();
|
||||||
} else if not_modified {
|
} else if not_modified {
|
||||||
return resp.status(StatusCode::NOT_MODIFIED).finish();
|
return res
|
||||||
|
.status(StatusCode::NOT_MODIFIED)
|
||||||
|
.body(body::None::new())
|
||||||
|
.map_into_boxed_body();
|
||||||
}
|
}
|
||||||
|
|
||||||
let reader = ChunkedReadFile::new(length, offset, self.file);
|
let reader = chunked::new_chunked_read(length, offset, self.file);
|
||||||
|
|
||||||
if offset != 0 || length != self.md.len() {
|
if offset != 0 || length != self.md.len() {
|
||||||
resp.status(StatusCode::PARTIAL_CONTENT);
|
res.status(StatusCode::PARTIAL_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.body(SizedStream::new(length, reader))
|
res.body(SizedStream::new(length, reader))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for NamedFile {
|
|
||||||
type Target = File;
|
|
||||||
|
|
||||||
fn deref(&self) -> &File {
|
|
||||||
&self.file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for NamedFile {
|
|
||||||
fn deref_mut(&mut self) -> &mut File {
|
|
||||||
&mut self.file
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,7 +570,9 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Responder for NamedFile {
|
impl Responder for NamedFile {
|
||||||
fn respond_to(self, req: &HttpRequest) -> HttpResponse {
|
type Body = BoxBody;
|
||||||
|
|
||||||
|
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||||
self.into_response(req)
|
self.into_response(req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -520,14 +581,16 @@ impl ServiceFactory<ServiceRequest> for NamedFile {
|
|||||||
type Response = ServiceResponse;
|
type Response = ServiceResponse;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Config = ();
|
type Config = ();
|
||||||
type InitError = ();
|
|
||||||
type Service = NamedFileService;
|
type Service = NamedFileService;
|
||||||
type Future = Ready<Result<Self::Service, ()>>;
|
type InitError = ();
|
||||||
|
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||||
|
|
||||||
fn new_service(&self, _: ()) -> Self::Future {
|
fn new_service(&self, _: ()) -> Self::Future {
|
||||||
ok(NamedFileService {
|
let service = NamedFileService {
|
||||||
path: self.path.clone(),
|
path: self.path.clone(),
|
||||||
})
|
};
|
||||||
|
|
||||||
|
Box::pin(async move { Ok(service) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,18 +603,19 @@ pub struct NamedFileService {
|
|||||||
impl Service<ServiceRequest> for NamedFileService {
|
impl Service<ServiceRequest> for NamedFileService {
|
||||||
type Response = ServiceResponse;
|
type Response = ServiceResponse;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
actix_service::always_ready!();
|
dev::always_ready!();
|
||||||
|
|
||||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
let (req, _) = req.into_parts();
|
let (req, _) = req.into_parts();
|
||||||
ready(
|
|
||||||
NamedFile::open(&self.path)
|
let path = self.path.clone();
|
||||||
.map_err(|e| e.into())
|
Box::pin(async move {
|
||||||
.map(|f| f.into_response(&req))
|
let file = NamedFile::open_async(path).await?;
|
||||||
.map(|res| ServiceResponse::new(req, res)),
|
let res = file.into_response(&req);
|
||||||
)
|
Ok(ServiceResponse::new(req, res))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Component, Path, PathBuf},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ use actix_web::{dev::Payload, FromRequest, HttpRequest};
|
|||||||
|
|
||||||
use crate::error::UriSegmentError;
|
use crate::error::UriSegmentError;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub(crate) struct PathBufWrap(PathBuf);
|
pub(crate) struct PathBufWrap(PathBuf);
|
||||||
|
|
||||||
impl FromStr for PathBufWrap {
|
impl FromStr for PathBufWrap {
|
||||||
@@ -21,11 +21,28 @@ impl FromStr for PathBufWrap {
|
|||||||
|
|
||||||
impl PathBufWrap {
|
impl PathBufWrap {
|
||||||
/// Parse a path, giving the choice of allowing hidden files to be considered valid segments.
|
/// Parse a path, giving the choice of allowing hidden files to be considered valid segments.
|
||||||
|
///
|
||||||
|
/// Path traversal is guarded by this method.
|
||||||
pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
|
pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
|
||||||
let mut buf = PathBuf::new();
|
let mut buf = PathBuf::new();
|
||||||
|
|
||||||
|
// equivalent to `path.split('/').count()`
|
||||||
|
let mut segment_count = path.matches('/').count() + 1;
|
||||||
|
|
||||||
|
// we can decode the whole path here (instead of per-segment decoding)
|
||||||
|
// because we will reject `%2F` in paths using `segement_count`.
|
||||||
|
let path = percent_encoding::percent_decode_str(path)
|
||||||
|
.decode_utf8()
|
||||||
|
.map_err(|_| UriSegmentError::NotValidUtf8)?;
|
||||||
|
|
||||||
|
// disallow decoding `%2F` into `/`
|
||||||
|
if segment_count != path.matches('/').count() + 1 {
|
||||||
|
return Err(UriSegmentError::BadChar('/'));
|
||||||
|
}
|
||||||
|
|
||||||
for segment in path.split('/') {
|
for segment in path.split('/') {
|
||||||
if segment == ".." {
|
if segment == ".." {
|
||||||
|
segment_count -= 1;
|
||||||
buf.pop();
|
buf.pop();
|
||||||
} else if !hidden_files && segment.starts_with('.') {
|
} else if !hidden_files && segment.starts_with('.') {
|
||||||
return Err(UriSegmentError::BadStart('.'));
|
return Err(UriSegmentError::BadStart('.'));
|
||||||
@@ -38,14 +55,27 @@ impl PathBufWrap {
|
|||||||
} else if segment.ends_with('<') {
|
} else if segment.ends_with('<') {
|
||||||
return Err(UriSegmentError::BadEnd('<'));
|
return Err(UriSegmentError::BadEnd('<'));
|
||||||
} else if segment.is_empty() {
|
} else if segment.is_empty() {
|
||||||
|
segment_count -= 1;
|
||||||
continue;
|
continue;
|
||||||
} else if cfg!(windows) && segment.contains('\\') {
|
} else if cfg!(windows) && segment.contains('\\') {
|
||||||
return Err(UriSegmentError::BadChar('\\'));
|
return Err(UriSegmentError::BadChar('\\'));
|
||||||
|
} else if cfg!(windows) && segment.contains(':') {
|
||||||
|
return Err(UriSegmentError::BadChar(':'));
|
||||||
} else {
|
} else {
|
||||||
buf.push(segment)
|
buf.push(segment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make sure we agree with stdlib parser
|
||||||
|
for (i, component) in buf.components().enumerate() {
|
||||||
|
assert!(
|
||||||
|
matches!(component, Component::Normal(_)),
|
||||||
|
"component `{:?}` is not normal",
|
||||||
|
component
|
||||||
|
);
|
||||||
|
assert!(i < segment_count);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(PathBufWrap(buf))
|
Ok(PathBufWrap(buf))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,10 +89,9 @@ impl AsRef<Path> for PathBufWrap {
|
|||||||
impl FromRequest for PathBufWrap {
|
impl FromRequest for PathBufWrap {
|
||||||
type Error = UriSegmentError;
|
type Error = UriSegmentError;
|
||||||
type Future = Ready<Result<Self, Self::Error>>;
|
type Future = Ready<Result<Self, Self::Error>>;
|
||||||
type Config = ();
|
|
||||||
|
|
||||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||||
ready(req.match_info().path().parse())
|
ready(req.match_info().unprocessed().parse())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,4 +145,46 @@ mod tests {
|
|||||||
PathBuf::from_iter(vec!["test", ".tt"])
|
PathBuf::from_iter(vec!["test", ".tt"])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn path_traversal() {
|
||||||
|
assert_eq!(
|
||||||
|
PathBufWrap::parse_path("/../README.md", false).unwrap().0,
|
||||||
|
PathBuf::from_iter(vec!["README.md"])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
PathBufWrap::parse_path("/../README.md", true).unwrap().0,
|
||||||
|
PathBuf::from_iter(vec!["README.md"])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
PathBufWrap::parse_path("/../../../../../../../../../../etc/passwd", false)
|
||||||
|
.unwrap()
|
||||||
|
.0,
|
||||||
|
PathBuf::from_iter(vec!["etc/passwd"])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg_attr(windows, should_panic)]
|
||||||
|
fn windows_drive_traversal() {
|
||||||
|
// detect issues in windows that could lead to path traversal
|
||||||
|
// see <https://github.com/SergioBenitez/Rocket/issues/1949
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
PathBufWrap::parse_path("C:test.txt", false).unwrap().0,
|
||||||
|
PathBuf::from_iter(vec!["C:test.txt"])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
PathBufWrap::parse_path("C:../whatever", false).unwrap().0,
|
||||||
|
PathBuf::from_iter(vec!["C:../whatever"])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
PathBufWrap::parse_path(":test.txt", false).unwrap().0,
|
||||||
|
PathBuf::from_iter(vec![":test.txt"])
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
use std::{fmt, io, path::PathBuf, rc::Rc};
|
use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc};
|
||||||
|
|
||||||
use actix_service::Service;
|
|
||||||
use actix_utils::future::ok;
|
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
dev::{ServiceRequest, ServiceResponse},
|
body::BoxBody,
|
||||||
|
dev::{self, Service, ServiceRequest, ServiceResponse},
|
||||||
error::Error,
|
error::Error,
|
||||||
guard::Guard,
|
guard::Guard,
|
||||||
http::{header, Method},
|
http::{header, Method},
|
||||||
@@ -17,7 +16,18 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Assembled file serving service.
|
/// Assembled file serving service.
|
||||||
pub struct FilesService {
|
#[derive(Clone)]
|
||||||
|
pub struct FilesService(pub(crate) Rc<FilesServiceInner>);
|
||||||
|
|
||||||
|
impl Deref for FilesService {
|
||||||
|
type Target = FilesServiceInner;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&*self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FilesServiceInner {
|
||||||
pub(crate) directory: PathBuf,
|
pub(crate) directory: PathBuf,
|
||||||
pub(crate) index: Option<String>,
|
pub(crate) index: Option<String>,
|
||||||
pub(crate) show_index: bool,
|
pub(crate) show_index: bool,
|
||||||
@@ -31,20 +41,50 @@ pub struct FilesService {
|
|||||||
pub(crate) hidden_files: bool,
|
pub(crate) hidden_files: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for FilesServiceInner {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str("FilesServiceInner")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FilesService {
|
impl FilesService {
|
||||||
fn handle_err(
|
async fn handle_err(
|
||||||
&self,
|
&self,
|
||||||
err: io::Error,
|
err: io::Error,
|
||||||
req: ServiceRequest,
|
req: ServiceRequest,
|
||||||
) -> LocalBoxFuture<'static, Result<ServiceResponse, Error>> {
|
) -> Result<ServiceResponse, Error> {
|
||||||
log::debug!("error handling {}: {}", req.path(), err);
|
log::debug!("error handling {}: {}", req.path(), err);
|
||||||
|
|
||||||
if let Some(ref default) = self.default {
|
if let Some(ref default) = self.default {
|
||||||
Box::pin(default.call(req))
|
default.call(req).await
|
||||||
} else {
|
} else {
|
||||||
Box::pin(ok(req.error_response(err)))
|
Ok(req.error_response(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn serve_named_file(
|
||||||
|
&self,
|
||||||
|
req: ServiceRequest,
|
||||||
|
mut named_file: NamedFile,
|
||||||
|
) -> ServiceResponse {
|
||||||
|
if let Some(ref mime_override) = self.mime_override {
|
||||||
|
let new_disposition = mime_override(&named_file.content_type.type_());
|
||||||
|
named_file.content_disposition.disposition = new_disposition;
|
||||||
|
}
|
||||||
|
named_file.flags = self.file_flags;
|
||||||
|
|
||||||
|
let (req, _) = req.into_parts();
|
||||||
|
let res = named_file.into_response(&req);
|
||||||
|
ServiceResponse::new(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse {
|
||||||
|
let dir = Directory::new(self.directory.clone(), path);
|
||||||
|
|
||||||
|
let (req, _) = req.into_parts();
|
||||||
|
|
||||||
|
(self.renderer)(&dir, &req).unwrap_or_else(|e| ServiceResponse::from_err(e, req))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for FilesService {
|
impl fmt::Debug for FilesService {
|
||||||
@@ -54,118 +94,102 @@ impl fmt::Debug for FilesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Service<ServiceRequest> for FilesService {
|
impl Service<ServiceRequest> for FilesService {
|
||||||
type Response = ServiceResponse;
|
type Response = ServiceResponse<BoxBody>;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Future = LocalBoxFuture<'static, Result<ServiceResponse, Error>>;
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
actix_service::always_ready!();
|
dev::always_ready!();
|
||||||
|
|
||||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
let is_method_valid = if let Some(guard) = &self.guards {
|
let is_method_valid = if let Some(guard) = &self.guards {
|
||||||
// execute user defined guards
|
// execute user defined guards
|
||||||
(**guard).check(req.head())
|
(**guard).check(&req.guard_ctx())
|
||||||
} else {
|
} else {
|
||||||
// default behavior
|
// default behavior
|
||||||
matches!(*req.method(), Method::HEAD | Method::GET)
|
matches!(*req.method(), Method::HEAD | Method::GET)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let this = self.clone();
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
if !is_method_valid {
|
if !is_method_valid {
|
||||||
return Box::pin(ok(req.into_response(
|
return Ok(req.into_response(
|
||||||
actix_web::HttpResponse::MethodNotAllowed()
|
HttpResponse::MethodNotAllowed()
|
||||||
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
|
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
|
||||||
.body("Request did not meet this resource's requirements."),
|
.body("Request did not meet this resource's requirements."),
|
||||||
)));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let real_path =
|
let path_on_disk = match PathBufWrap::parse_path(
|
||||||
match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) {
|
req.match_info().unprocessed(),
|
||||||
|
this.hidden_files,
|
||||||
|
) {
|
||||||
Ok(item) => item,
|
Ok(item) => item,
|
||||||
Err(e) => return Box::pin(ok(req.error_response(e))),
|
Err(err) => return Ok(req.error_response(err)),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(filter) = &self.path_filter {
|
if let Some(filter) = &this.path_filter {
|
||||||
if !filter(real_path.as_ref(), req.head()) {
|
if !filter(path_on_disk.as_ref(), req.head()) {
|
||||||
if let Some(ref default) = self.default {
|
if let Some(ref default) = this.default {
|
||||||
return Box::pin(default.call(req));
|
return default.call(req).await;
|
||||||
} else {
|
} else {
|
||||||
return Box::pin(ok(
|
return Ok(req.into_response(HttpResponse::NotFound().finish()));
|
||||||
req.into_response(actix_web::HttpResponse::NotFound().finish())
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// full file path
|
// full file path
|
||||||
let path = self.directory.join(&real_path);
|
let path = this.directory.join(&path_on_disk);
|
||||||
if let Err(err) = path.canonicalize() {
|
if let Err(err) = path.canonicalize() {
|
||||||
return Box::pin(self.handle_err(err, req));
|
return this.handle_err(err, req).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
if self.redirect_to_slash
|
if this.redirect_to_slash
|
||||||
&& !req.path().ends_with('/')
|
&& !req.path().ends_with('/')
|
||||||
&& (self.index.is_some() || self.show_index)
|
&& (this.index.is_some() || this.show_index)
|
||||||
{
|
{
|
||||||
let redirect_to = format!("{}/", req.path());
|
let redirect_to = format!("{}/", req.path());
|
||||||
|
|
||||||
return Box::pin(ok(req.into_response(
|
return Ok(req.into_response(
|
||||||
HttpResponse::Found()
|
HttpResponse::Found()
|
||||||
.insert_header((header::LOCATION, redirect_to))
|
.insert_header((header::LOCATION, redirect_to))
|
||||||
.finish(),
|
.finish(),
|
||||||
)));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let serve_named_file = |req: ServiceRequest, mut named_file: NamedFile| {
|
match this.index {
|
||||||
if let Some(ref mime_override) = self.mime_override {
|
Some(ref index) => {
|
||||||
let new_disposition = mime_override(&named_file.content_type.type_());
|
let named_path = path.join(index);
|
||||||
named_file.content_disposition.disposition = new_disposition;
|
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,
|
||||||
}
|
}
|
||||||
named_file.flags = self.file_flags;
|
}
|
||||||
|
None if this.show_index => Ok(this.show_index(req, path)),
|
||||||
let (req, _) = req.into_parts();
|
None => Ok(ServiceResponse::from_err(
|
||||||
let res = named_file.into_response(&req);
|
|
||||||
Box::pin(ok(ServiceResponse::new(req, res)))
|
|
||||||
};
|
|
||||||
|
|
||||||
let show_index = |req: ServiceRequest| {
|
|
||||||
let dir = Directory::new(self.directory.clone(), path.clone());
|
|
||||||
|
|
||||||
let (req, _) = req.into_parts();
|
|
||||||
let x = (self.renderer)(&dir, &req);
|
|
||||||
|
|
||||||
Box::pin(match x {
|
|
||||||
Ok(resp) => ok(resp),
|
|
||||||
Err(err) => ok(ServiceResponse::from_err(err, req)),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
match self.index {
|
|
||||||
Some(ref index) => match NamedFile::open(path.join(index)) {
|
|
||||||
Ok(named_file) => serve_named_file(req, named_file),
|
|
||||||
Err(_) if self.show_index => show_index(req),
|
|
||||||
Err(err) => self.handle_err(err, req),
|
|
||||||
},
|
|
||||||
None if self.show_index => show_index(req),
|
|
||||||
_ => Box::pin(ok(ServiceResponse::from_err(
|
|
||||||
FilesError::IsDirectory,
|
FilesError::IsDirectory,
|
||||||
req.into_parts().0,
|
req.into_parts().0,
|
||||||
))),
|
)),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match NamedFile::open(path) {
|
match NamedFile::open_async(&path).await {
|
||||||
Ok(mut named_file) => {
|
Ok(mut named_file) => {
|
||||||
if let Some(ref mime_override) = self.mime_override {
|
if let Some(ref mime_override) = this.mime_override {
|
||||||
let new_disposition = mime_override(&named_file.content_type.type_());
|
let new_disposition =
|
||||||
|
mime_override(&named_file.content_type.type_());
|
||||||
named_file.content_disposition.disposition = new_disposition;
|
named_file.content_disposition.disposition = new_disposition;
|
||||||
}
|
}
|
||||||
named_file.flags = self.file_flags;
|
named_file.flags = this.file_flags;
|
||||||
|
|
||||||
let (req, _) = req.into_parts();
|
let (req, _) = req.into_parts();
|
||||||
let res = named_file.into_response(&req);
|
let res = named_file.into_response(&req);
|
||||||
Box::pin(ok(ServiceResponse::new(req, res)))
|
Ok(ServiceResponse::new(req, res))
|
||||||
}
|
}
|
||||||
Err(err) => self.handle_err(err, req),
|
Err(err) => this.handle_err(err, req).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use actix_web::{
|
|||||||
App,
|
App,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_web::test]
|
||||||
async fn test_utf8_file_contents() {
|
async fn test_utf8_file_contents() {
|
||||||
// use default ISO-8859-1 encoding
|
// use default ISO-8859-1 encoding
|
||||||
let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await;
|
let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await;
|
||||||
@@ -19,12 +19,12 @@ async fn test_utf8_file_contents() {
|
|||||||
assert_eq!(res.status(), StatusCode::OK);
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
res.headers().get(header::CONTENT_TYPE),
|
res.headers().get(header::CONTENT_TYPE),
|
||||||
Some(&HeaderValue::from_static("text/plain")),
|
Some(&HeaderValue::from_static("text/plain; charset=utf-8")),
|
||||||
);
|
);
|
||||||
|
|
||||||
// prefer UTF-8 encoding
|
// disable UTF-8 attribute
|
||||||
let srv =
|
let srv =
|
||||||
test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(true)))
|
test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(false)))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let req = TestRequest::with_uri("/utf8.txt").to_request();
|
let req = TestRequest::with_uri("/utf8.txt").to_request();
|
||||||
@@ -33,6 +33,6 @@ async fn test_utf8_file_contents() {
|
|||||||
assert_eq!(res.status(), StatusCode::OK);
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
res.headers().get(header::CONTENT_TYPE),
|
res.headers().get(header::CONTENT_TYPE),
|
||||||
Some(&HeaderValue::from_static("text/plain; charset=utf-8")),
|
Some(&HeaderValue::from_static("text/plain")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use actix_web::{
|
|||||||
};
|
};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_web::test]
|
||||||
async fn test_guard_filter() {
|
async fn test_guard_filter() {
|
||||||
let srv = test::init_service(
|
let srv = test::init_service(
|
||||||
App::new()
|
App::new()
|
||||||
|
|||||||
27
actix-files/tests/traversal.rs
Normal file
27
actix-files/tests/traversal.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use actix_files::Files;
|
||||||
|
use actix_web::{
|
||||||
|
http::StatusCode,
|
||||||
|
test::{self, TestRequest},
|
||||||
|
App,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_directory_traversal_prevention() {
|
||||||
|
let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await;
|
||||||
|
|
||||||
|
let req =
|
||||||
|
TestRequest::with_uri("/../../../../../../../../../../../etc/passwd").to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri(
|
||||||
|
"/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd",
|
||||||
|
)
|
||||||
|
.to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/%00/etc/passwd%00").to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
@@ -3,102 +3,140 @@
|
|||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.12 - 2022-01-31
|
||||||
|
- No significant changes since `3.0.0-beta.11`.
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.11 - 2022-01-04
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.10 - 2021-12-27
|
||||||
|
- Update `actix-server` to `2.0.0-rc.2`. [#2550]
|
||||||
|
|
||||||
|
[#2550]: https://github.com/actix/actix-web/pull/2550
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.9 - 2021-12-11
|
||||||
|
- No significant changes since `3.0.0-beta.8`.
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.8 - 2021-11-30
|
||||||
|
- Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
||||||
|
|
||||||
|
[#2474]: https://github.com/actix/actix-web/pull/2474
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.7 - 2021-11-22
|
||||||
|
- Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
|
||||||
|
|
||||||
|
[#2408]: https://github.com/actix/actix-web/pull/2408
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.6 - 2021-11-15
|
||||||
|
- `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
|
||||||
|
- Update `actix-server` to `2.0.0-beta.9`. [#2442]
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||||
|
|
||||||
|
[#2442]: https://github.com/actix/actix-web/pull/2442
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.5 - 2021-09-09
|
## 3.0.0-beta.5 - 2021-09-09
|
||||||
* Minimum supported Rust version (MSRV) is now 1.51.
|
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.4 - 2021-04-02
|
## 3.0.0-beta.4 - 2021-04-02
|
||||||
* Added `TestServer::client_headers` method. [#2097]
|
- Added `TestServer::client_headers` method. [#2097]
|
||||||
|
|
||||||
[#2097]: https://github.com/actix/actix-web/pull/2097
|
[#2097]: https://github.com/actix/actix-web/pull/2097
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.3 - 2021-03-09
|
## 3.0.0-beta.3 - 2021-03-09
|
||||||
* No notable changes.
|
- No notable changes.
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.2 - 2021-02-10
|
## 3.0.0-beta.2 - 2021-02-10
|
||||||
* No notable changes.
|
- No notable changes.
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.1 - 2021-01-07
|
## 3.0.0-beta.1 - 2021-01-07
|
||||||
* Update `bytes` to `1.0`. [#1813]
|
- Update `bytes` to `1.0`. [#1813]
|
||||||
|
|
||||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||||
|
|
||||||
|
|
||||||
## 2.1.0 - 2020-11-25
|
## 2.1.0 - 2020-11-25
|
||||||
* Add ability to set address for `TestServer`. [#1645]
|
- Add ability to set address for `TestServer`. [#1645]
|
||||||
* Upgrade `base64` to `0.13`.
|
- Upgrade `base64` to `0.13`.
|
||||||
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
- Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||||
|
|
||||||
[#1773]: https://github.com/actix/actix-web/pull/1773
|
[#1773]: https://github.com/actix/actix-web/pull/1773
|
||||||
[#1645]: https://github.com/actix/actix-web/pull/1645
|
[#1645]: https://github.com/actix/actix-web/pull/1645
|
||||||
|
|
||||||
|
|
||||||
## 2.0.0 - 2020-09-11
|
## 2.0.0 - 2020-09-11
|
||||||
* Update actix-codec and actix-utils dependencies.
|
- Update actix-codec and actix-utils dependencies.
|
||||||
|
|
||||||
|
|
||||||
## 2.0.0-alpha.1 - 2020-05-23
|
## 2.0.0-alpha.1 - 2020-05-23
|
||||||
* Update the `time` dependency to 0.2.7
|
- Update the `time` dependency to 0.2.7
|
||||||
* Update `actix-connect` dependency to 2.0.0-alpha.2
|
- Update `actix-connect` dependency to 2.0.0-alpha.2
|
||||||
* Make `test_server` `async` fn.
|
- Make `test_server` `async` fn.
|
||||||
* Bump minimum supported Rust version to 1.40
|
- Bump minimum supported Rust version to 1.40
|
||||||
* Replace deprecated `net2` crate with `socket2`
|
- Replace deprecated `net2` crate with `socket2`
|
||||||
* Update `base64` dependency to 0.12
|
- Update `base64` dependency to 0.12
|
||||||
* Update `env_logger` dependency to 0.7
|
- Update `env_logger` dependency to 0.7
|
||||||
|
|
||||||
## 1.0.0 - 2019-12-13
|
## 1.0.0 - 2019-12-13
|
||||||
* Replaced `TestServer::start()` with `test_server()`
|
- Replaced `TestServer::start()` with `test_server()`
|
||||||
|
|
||||||
|
|
||||||
## 1.0.0-alpha.3 - 2019-12-07
|
## 1.0.0-alpha.3 - 2019-12-07
|
||||||
* Migrate to `std::future`
|
- Migrate to `std::future`
|
||||||
|
|
||||||
|
|
||||||
## 0.2.5 - 2019-09-17
|
## 0.2.5 - 2019-09-17
|
||||||
* Update serde_urlencoded to "0.6.1"
|
- Update serde_urlencoded to "0.6.1"
|
||||||
* Increase TestServerRuntime timeouts from 500ms to 3000ms
|
- Increase TestServerRuntime timeouts from 500ms to 3000ms
|
||||||
* Do not override current `System`
|
- Do not override current `System`
|
||||||
|
|
||||||
|
|
||||||
## 0.2.4 - 2019-07-18
|
## 0.2.4 - 2019-07-18
|
||||||
* Update actix-server to 0.6
|
- Update actix-server to 0.6
|
||||||
|
|
||||||
|
|
||||||
## 0.2.3 - 2019-07-16
|
## 0.2.3 - 2019-07-16
|
||||||
* Add `delete`, `options`, `patch` methods to `TestServerRunner`
|
- Add `delete`, `options`, `patch` methods to `TestServerRunner`
|
||||||
|
|
||||||
|
|
||||||
## 0.2.2 - 2019-06-16
|
## 0.2.2 - 2019-06-16
|
||||||
* Add .put() and .sput() methods
|
- Add .put() and .sput() methods
|
||||||
|
|
||||||
|
|
||||||
## 0.2.1 - 2019-06-05
|
## 0.2.1 - 2019-06-05
|
||||||
* Add license files
|
- Add license files
|
||||||
|
|
||||||
|
|
||||||
## 0.2.0 - 2019-05-12
|
## 0.2.0 - 2019-05-12
|
||||||
* Update awc and actix-http deps
|
- Update awc and actix-http deps
|
||||||
|
|
||||||
|
|
||||||
## 0.1.1 - 2019-04-24
|
## 0.1.1 - 2019-04-24
|
||||||
* Always make new connection for http client
|
- Always make new connection for http client
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0 - 2019-04-16
|
## 0.1.0 - 2019-04-16
|
||||||
* No changes
|
- No changes
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.3 - 2019-04-02
|
## 0.1.0-alpha.3 - 2019-04-02
|
||||||
* Request functions accept path #743
|
- Request functions accept path #743
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.2 - 2019-03-29
|
## 0.1.0-alpha.2 - 2019-03-29
|
||||||
* Added TestServerRuntime::load_body() method
|
- Added TestServerRuntime::load_body() method
|
||||||
* Update actix-http and awc libraries
|
- Update actix-http and awc libraries
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.1 - 2019-03-28
|
## 0.1.0-alpha.1 - 2019-03-28
|
||||||
* Initial impl
|
- Initial impl
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-http-test"
|
name = "actix-http-test"
|
||||||
version = "3.0.0-beta.5"
|
version = "3.0.0-beta.12"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Various helpers for Actix applications to use during testing"
|
description = "Various helpers for Actix applications to use during testing"
|
||||||
keywords = ["http", "web", "framework", "async", "futures"]
|
keywords = ["http", "web", "framework", "async", "futures"]
|
||||||
@@ -30,26 +30,26 @@ openssl = ["tls-openssl", "awc/openssl"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-service = "2.0.0"
|
actix-service = "2.0.0"
|
||||||
actix-codec = "0.4.0"
|
actix-codec = "0.4.1"
|
||||||
actix-tls = "3.0.0-beta.5"
|
actix-tls = "3.0.0"
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3.0.0"
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-server = "2.0.0-beta.3"
|
actix-server = "2"
|
||||||
awc = { version = "3.0.0-beta.8", default-features = false }
|
awc = { version = "3.0.0-beta.20", default-features = false }
|
||||||
|
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
futures-core = { version = "0.3.7", default-features = false }
|
futures-core = { version = "0.3.7", default-features = false }
|
||||||
http = "0.2.2"
|
http = "0.2.5"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
socket2 = "0.4"
|
socket2 = "0.4"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
slab = "0.4"
|
slab = "0.4"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
time = { version = "0.2.23", default-features = false, features = ["std"] }
|
|
||||||
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||||
|
tokio = { version = "1.8.4", features = ["sync"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-web = { version = "4.0.0-beta.9", default-features = false, features = ["cookies"] }
|
actix-web = { version = "4.0.0-rc.1", default-features = false, features = ["cookies"] }
|
||||||
actix-http = "3.0.0-beta.10"
|
actix-http = "3.0.0-rc.1"
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
> Various helpers for Actix applications to use during testing.
|
> Various helpers for Actix applications to use during testing.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-http-test)
|
[](https://crates.io/crates/actix-http-test)
|
||||||
[](https://docs.rs/actix-http-test/3.0.0-beta.5)
|
[](https://docs.rs/actix-http-test/3.0.0-beta.12)
|
||||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html)
|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||||

|

|
||||||
<br>
|
<br>
|
||||||
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.5)
|
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.12)
|
||||||
[](https://crates.io/crates/actix-http-test)
|
[](https://crates.io/crates/actix-http-test)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-http-test)
|
- [API Documentation](https://docs.rs/actix-http-test)
|
||||||
- Minimum Supported Rust Version (MSRV): 1.51.0
|
- Minimum Supported Rust Version (MSRV): 1.54
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
//! Various helpers for Actix applications to use during testing.
|
//! Various helpers for Actix applications to use during testing.
|
||||||
|
|
||||||
#![deny(rust_2018_idioms)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
|
#![warn(future_incompatible)]
|
||||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
|
|
||||||
#[cfg(feature = "openssl")]
|
#[cfg(feature = "openssl")]
|
||||||
extern crate tls_openssl as openssl;
|
extern crate tls_openssl as openssl;
|
||||||
|
|
||||||
use std::sync::mpsc;
|
use std::{net, thread, time::Duration};
|
||||||
use std::{net, thread, time};
|
|
||||||
|
|
||||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||||
use actix_rt::{net::TcpStream, System};
|
use actix_rt::{net::TcpStream, System};
|
||||||
use actix_server::{Server, ServiceFactory};
|
use actix_server::{Server, ServerServiceFactory};
|
||||||
use awc::{
|
use awc::{
|
||||||
error::PayloadError, http::HeaderMap, ws, Client, ClientRequest, ClientResponse, Connector,
|
error::PayloadError, http::header::HeaderMap, ws, Client, ClientRequest, ClientResponse,
|
||||||
|
Connector,
|
||||||
};
|
};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_core::stream::Stream;
|
use futures_core::stream::Stream;
|
||||||
use http::Method;
|
use http::Method;
|
||||||
use socket2::{Domain, Protocol, Socket, Type};
|
use socket2::{Domain, Protocol, Socket, Type};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
/// Start test server
|
/// Start test server.
|
||||||
///
|
///
|
||||||
/// `TestServer` is very simple test server that simplify process of writing
|
/// `TestServer` is very simple test server that simplify process of writing integration tests cases
|
||||||
/// integration tests cases for actix web applications.
|
/// for HTTP applications.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
/// ```no_run
|
||||||
/// ```
|
|
||||||
/// use actix_http::HttpService;
|
/// use actix_http::HttpService;
|
||||||
/// use actix_http_test::TestServer;
|
/// use actix_http_test::test_server;
|
||||||
/// use actix_web::{web, App, HttpResponse, Error};
|
/// use actix_web::{web, App, HttpResponse, Error};
|
||||||
///
|
///
|
||||||
/// async fn my_handler() -> Result<HttpResponse, Error> {
|
/// async fn my_handler() -> Result<HttpResponse, Error> {
|
||||||
/// Ok(HttpResponse::Ok().into())
|
/// Ok(HttpResponse::Ok().into())
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// #[actix_rt::test]
|
/// #[actix_web::test]
|
||||||
/// async fn test_example() {
|
/// async fn test_example() {
|
||||||
/// let mut srv = TestServer::start(
|
/// let mut srv = TestServer::start(||
|
||||||
/// || HttpService::new(
|
/// HttpService::new(
|
||||||
/// App::new().service(
|
/// App::new().service(web::resource("/").to(my_handler))
|
||||||
/// web::resource("/").to(my_handler))
|
|
||||||
/// )
|
/// )
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
@@ -51,77 +51,91 @@ use socket2::{Domain, Protocol, Socket, Type};
|
|||||||
/// assert!(response.status().is_success());
|
/// assert!(response.status().is_success());
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer {
|
pub async fn test_server<F: ServerServiceFactory<TcpStream>>(factory: F) -> TestServer {
|
||||||
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
|
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
test_server_with_addr(tcp, factory).await
|
test_server_with_addr(tcp, factory).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start [`test server`](test_server()) on a concrete Address
|
/// Start [`test server`](test_server()) on an existing address binding.
|
||||||
pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
|
pub async fn test_server_with_addr<F: ServerServiceFactory<TcpStream>>(
|
||||||
tcp: net::TcpListener,
|
tcp: net::TcpListener,
|
||||||
factory: F,
|
factory: F,
|
||||||
) -> TestServer {
|
) -> TestServer {
|
||||||
let (tx, rx) = mpsc::channel();
|
let (started_tx, started_rx) = std::sync::mpsc::channel();
|
||||||
|
let (thread_stop_tx, thread_stop_rx) = mpsc::channel(1);
|
||||||
|
|
||||||
// run server in separate thread
|
// run server in separate thread
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let sys = System::new();
|
System::new().block_on(async move {
|
||||||
let local_addr = tcp.local_addr().unwrap();
|
let local_addr = tcp.local_addr().unwrap();
|
||||||
|
|
||||||
let srv = Server::build()
|
let srv = Server::build()
|
||||||
.listen("test", tcp, factory)?
|
|
||||||
.workers(1)
|
.workers(1)
|
||||||
.disable_signals();
|
.disable_signals()
|
||||||
|
.system_exit()
|
||||||
|
.listen("test", tcp, factory)
|
||||||
|
.expect("test server could not be created");
|
||||||
|
|
||||||
sys.block_on(async {
|
let srv = srv.run();
|
||||||
srv.run();
|
started_tx
|
||||||
tx.send((System::current(), local_addr)).unwrap();
|
.send((System::current(), srv.handle(), local_addr))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// drive server loop
|
||||||
|
srv.await.unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
sys.run()
|
// notify TestServer that server and system have shut down
|
||||||
|
// all thread managed resources should be dropped at this point
|
||||||
|
let _ = thread_stop_tx.send(());
|
||||||
});
|
});
|
||||||
|
|
||||||
let (system, addr) = rx.recv().unwrap();
|
let (system, server, addr) = started_rx.recv().unwrap();
|
||||||
|
|
||||||
let client = {
|
let client = {
|
||||||
let connector = {
|
|
||||||
#[cfg(feature = "openssl")]
|
#[cfg(feature = "openssl")]
|
||||||
{
|
let connector = {
|
||||||
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
|
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
|
||||||
|
|
||||||
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
|
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
|
||||||
|
|
||||||
builder.set_verify(SslVerifyMode::NONE);
|
builder.set_verify(SslVerifyMode::NONE);
|
||||||
let _ = builder
|
let _ = builder
|
||||||
.set_alpn_protos(b"\x02h2\x08http/1.1")
|
.set_alpn_protos(b"\x02h2\x08http/1.1")
|
||||||
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
|
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
|
||||||
|
|
||||||
Connector::new()
|
Connector::new()
|
||||||
.conn_lifetime(time::Duration::from_secs(0))
|
.conn_lifetime(Duration::from_secs(0))
|
||||||
.timeout(time::Duration::from_millis(30000))
|
.timeout(Duration::from_millis(30000))
|
||||||
.ssl(builder.build())
|
.openssl(builder.build())
|
||||||
}
|
};
|
||||||
|
|
||||||
#[cfg(not(feature = "openssl"))]
|
#[cfg(not(feature = "openssl"))]
|
||||||
{
|
let connector = {
|
||||||
Connector::new()
|
Connector::new()
|
||||||
.conn_lifetime(time::Duration::from_secs(0))
|
.conn_lifetime(Duration::from_secs(0))
|
||||||
.timeout(time::Duration::from_millis(30000))
|
.timeout(Duration::from_millis(30000))
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Client::builder().connector(connector).finish()
|
Client::builder().connector(connector).finish()
|
||||||
};
|
};
|
||||||
|
|
||||||
TestServer {
|
TestServer {
|
||||||
addr,
|
server,
|
||||||
client,
|
client,
|
||||||
system,
|
system,
|
||||||
|
addr,
|
||||||
|
thread_stop_rx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test server controller
|
/// Test server controller
|
||||||
pub struct TestServer {
|
pub struct TestServer {
|
||||||
|
server: actix_server::ServerHandle,
|
||||||
|
client: awc::Client,
|
||||||
|
system: actix_rt::System,
|
||||||
addr: net::SocketAddr,
|
addr: net::SocketAddr,
|
||||||
client: Client,
|
thread_stop_rx: mpsc::Receiver<()>,
|
||||||
system: System,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestServer {
|
impl TestServer {
|
||||||
@@ -258,15 +272,32 @@ impl TestServer {
|
|||||||
self.client.headers()
|
self.client.headers()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop HTTP server
|
/// Stop HTTP server.
|
||||||
fn stop(&mut self) {
|
///
|
||||||
|
/// Waits for spawned `Server` and `System` to (force) shutdown.
|
||||||
|
pub async fn stop(&mut self) {
|
||||||
|
// signal server to stop
|
||||||
|
self.server.stop(false).await;
|
||||||
|
|
||||||
|
// also signal system to stop
|
||||||
|
// though this is handled by `ServerBuilder::exit_system` too
|
||||||
self.system.stop();
|
self.system.stop();
|
||||||
|
|
||||||
|
// wait for thread to be stopped but don't care about result
|
||||||
|
let _ = self.thread_stop_rx.recv().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for TestServer {
|
impl Drop for TestServer {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.stop()
|
// calls in this Drop impl should be enough to shut down the server, system, and thread
|
||||||
|
// without needing to await anything
|
||||||
|
|
||||||
|
// signal server to stop
|
||||||
|
let _ = self.server.stop(true);
|
||||||
|
|
||||||
|
// signal system to stop
|
||||||
|
self.system.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,232 @@
|
|||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.10 - 2021-09-09
|
## 3.0.0-rc.1 - 2022-01-31
|
||||||
|
### Added
|
||||||
|
- Implement `Default` for `KeepAlive`. [#2611]
|
||||||
|
- Implement `From<Duration>` for `KeepAlive`. [#2611]
|
||||||
|
- Implement `From<Option<Duration>>` for `KeepAlive`. [#2611]
|
||||||
|
- Implement `Default` for `HttpServiceBuilder`. [#2611]
|
||||||
|
- Crate `ws` feature flag, disabled by default. [#2618]
|
||||||
|
- Crate `http2` feature flag, disabled by default. [#2618]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377]
|
- Rename `ServiceConfig::{client_timer_expire => client_request_deadline}`. [#2611]
|
||||||
* Minimum supported Rust version (MSRV) is now 1.51.
|
- Rename `ServiceConfig::{client_disconnect_timer => client_disconnect_deadline}`. [#2611]
|
||||||
|
- Deadline methods in `ServiceConfig` now return `std::time::Instant`s instead of Tokio's wrapper type. [#2611]
|
||||||
|
- Rename `h1::Codec::{keepalive => keep_alive}`. [#2611]
|
||||||
|
- Rename `h1::Codec::{keepalive_enabled => keep_alive_enabled}`. [#2611]
|
||||||
|
- Rename `h1::ClientCodec::{keepalive => keep_alive}`. [#2611]
|
||||||
|
- Rename `h1::ClientPayloadCodec::{keepalive => keep_alive}`. [#2611]
|
||||||
|
- `ServiceConfig::keep_alive` now returns a `KeepAlive`. [#2611]
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364]
|
- HTTP/1.1 dispatcher correctly uses client request timeout. [#2611]
|
||||||
* Remove `Into<Error>` bound on `Encoder` body types. [#2375]
|
|
||||||
* Fix quality parse error in Accept-Encoding header. [#2344]
|
### Removed
|
||||||
|
- `ServiceConfig::{client_timer, keep_alive_timer}`. [#2611]
|
||||||
|
- `impl From<usize> for KeepAlive`; use `Duration`s instead. [#2611]
|
||||||
|
- `impl From<Option<usize>> for KeepAlive`; use `Duration`s instead. [#2611]
|
||||||
|
- `HttpServiceBuilder::new`; use `default` instead. [#2611]
|
||||||
|
|
||||||
|
[#2611]: https://github.com/actix/actix-web/pull/2611
|
||||||
|
[#2618]: https://github.com/actix/actix-web/pull/2618
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.19 - 2022-01-21
|
||||||
|
### Added
|
||||||
|
- Response headers can be sent as camel case using `res.head_mut().set_camel_case_headers(true)`. [#2587]
|
||||||
|
- `ResponseHead` now implements `Clone`. [#2585]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Brotli (de)compression support is now provided by the `brotli` crate. [#2538]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `ResponseHead::extensions[_mut]()`. [#2585]
|
||||||
|
- `ResponseBuilder::extensions[_mut]()`. [#2585]
|
||||||
|
|
||||||
|
[#2538]: https://github.com/actix/actix-web/pull/2538
|
||||||
|
[#2585]: https://github.com/actix/actix-web/pull/2585
|
||||||
|
[#2587]: https://github.com/actix/actix-web/pull/2587
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.18 - 2022-01-04
|
||||||
|
### Added
|
||||||
|
- `impl Eq` for `header::ContentEncoding`. [#2501]
|
||||||
|
- `impl Copy` for `QualityItem` where `T: Copy`. [#2501]
|
||||||
|
- `Quality::ZERO` equivalent to `q=0`. [#2501]
|
||||||
|
- `QualityItem::zero` that uses `Quality::ZERO`. [#2501]
|
||||||
|
- `ContentEncoding::to_header_value()`. [#2501]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `Quality::MIN` is now the smallest non-zero value. [#2501]
|
||||||
|
- `QualityItem::min` semantics changed with `QualityItem::MIN`. [#2501]
|
||||||
|
- Rename `ContentEncoding::{Br => Brotli}`. [#2501]
|
||||||
|
- Rename `header::EntityTag::{weak => new_weak, strong => new_strong}`. [#2565]
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `ContentEncoding::Identity` can now be parsed from a string. [#2501]
|
||||||
|
- A `Vary` header is now correctly sent along with compressed content. [#2501]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `ContentEncoding::Auto` variant. [#2501]
|
||||||
|
- `ContentEncoding::is_compression()`. [#2501]
|
||||||
|
|
||||||
|
[#2501]: https://github.com/actix/actix-web/pull/2501
|
||||||
|
[#2565]: https://github.com/actix/actix-web/pull/2565
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.17 - 2021-12-27
|
||||||
|
### Changes
|
||||||
|
- `HeaderMap::get_all` now returns a `std::slice::Iter`. [#2527]
|
||||||
|
- `Payload` inner fields are now named. [#2545]
|
||||||
|
- `impl Stream` for `Payload` no longer requires the `Stream` variant be `Unpin`. [#2545]
|
||||||
|
- `impl Future` for `h1::SendResponse` no longer requires the body type be `Unpin`. [#2545]
|
||||||
|
- `impl Stream` for `encoding::Decoder` no longer requires the stream type be `Unpin`. [#2545]
|
||||||
|
- Rename `PayloadStream` to `BoxedPayloadStream`. [#2545]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `h1::Payload::readany`. [#2545]
|
||||||
|
|
||||||
|
[#2527]: https://github.com/actix/actix-web/pull/2527
|
||||||
|
[#2545]: https://github.com/actix/actix-web/pull/2545
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.16 - 2021-12-17
|
||||||
|
### Added
|
||||||
|
- New method on `MessageBody` trait, `try_into_bytes`, with default implementation, for optimizations on body types that complete in exactly one poll. Replaces `is_complete_body` and `take_complete_body`. [#2522]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Rename trait `IntoHeaderPair => TryIntoHeaderPair`. [#2510]
|
||||||
|
- Rename `TryIntoHeaderPair::{try_into_header_pair => try_into_pair}`. [#2510]
|
||||||
|
- Rename trait `IntoHeaderValue => TryIntoHeaderValue`. [#2510]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `MessageBody::{is_complete_body,take_complete_body}`. [#2522]
|
||||||
|
|
||||||
|
[#2510]: https://github.com/actix/actix-web/pull/2510
|
||||||
|
[#2522]: https://github.com/actix/actix-web/pull/2522
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.15 - 2021-12-11
|
||||||
|
### Added
|
||||||
|
- Add timeout for canceling HTTP/2 server side connection handshake. Default to 5 seconds. [#2483]
|
||||||
|
- HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483]
|
||||||
|
- `Response::map_into_boxed_body`. [#2468]
|
||||||
|
- `body::EitherBody` enum. [#2468]
|
||||||
|
- `body::None` struct. [#2468]
|
||||||
|
- Impl `MessageBody` for `bytestring::ByteString`. [#2468]
|
||||||
|
- `impl Clone for ws::HandshakeError`. [#2468]
|
||||||
|
- `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920]
|
||||||
|
- `impl Default ` for `ws::Codec`. [#1920]
|
||||||
|
- `header::QualityItem::{max, min}`. [#2486]
|
||||||
|
- `header::Quality::{MAX, MIN}`. [#2486]
|
||||||
|
- `impl Display` for `header::Quality`. [#2486]
|
||||||
|
- Connection data set through the `on_connect_ext` callbacks is now accessible only from the new `Request::conn_data()` method. [#2491]
|
||||||
|
- `Request::take_conn_data()`. [#2491]
|
||||||
|
- `Request::take_req_data()`. [#2487]
|
||||||
|
- `impl Clone` for `RequestHead`. [#2487]
|
||||||
|
- New methods on `MessageBody` trait, `is_complete_body` and `take_complete_body`, both with default implementations, for optimizations on body types that are done in exactly one poll/chunk. [#2497]
|
||||||
|
- New `boxed` method on `MessageBody` trait for wrapping body type. [#2520]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Rename `body::BoxBody::{from_body => new}`. [#2468]
|
||||||
|
- Body type for `Responses` returned from `Response::{new, ok, etc...}` is now `BoxBody`. [#2468]
|
||||||
|
- The `Error` associated type on `MessageBody` type now requires `impl Error` (or similar). [#2468]
|
||||||
|
- Error types using in service builders now require `Into<Response<BoxBody>>`. [#2468]
|
||||||
|
- `From` implementations on error types now return a `Response<BoxBody>`. [#2468]
|
||||||
|
- `ResponseBuilder::body(B)` now returns `Response<EitherBody<B>>`. [#2468]
|
||||||
|
- `ResponseBuilder::finish()` now returns `Response<EitherBody<()>>`. [#2468]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `ResponseBuilder::streaming`. [#2468]
|
||||||
|
- `impl Future` for `ResponseBuilder`. [#2468]
|
||||||
|
- Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468]
|
||||||
|
- Move `body::AnyBody` to `awc`. Replaced with `EitherBody` and `BoxBody`. [#2468]
|
||||||
|
- `impl Copy` for `ws::Codec`. [#1920]
|
||||||
|
- `header::qitem` helper. Replaced with `header::QualityItem::max`. [#2486]
|
||||||
|
- `impl TryFrom<u16>` for `header::Quality`. [#2486]
|
||||||
|
- `http` module. Most everything it contained is exported at the crate root. [#2488]
|
||||||
|
|
||||||
|
[#2483]: https://github.com/actix/actix-web/pull/2483
|
||||||
|
[#2468]: https://github.com/actix/actix-web/pull/2468
|
||||||
|
[#1920]: https://github.com/actix/actix-web/pull/1920
|
||||||
|
[#2486]: https://github.com/actix/actix-web/pull/2486
|
||||||
|
[#2487]: https://github.com/actix/actix-web/pull/2487
|
||||||
|
[#2488]: https://github.com/actix/actix-web/pull/2488
|
||||||
|
[#2491]: https://github.com/actix/actix-web/pull/2491
|
||||||
|
[#2497]: https://github.com/actix/actix-web/pull/2497
|
||||||
|
[#2520]: https://github.com/actix/actix-web/pull/2520
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.14 - 2021-11-30
|
||||||
|
### Changed
|
||||||
|
- Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467]
|
||||||
|
- Expose `header::map` module. [#2467]
|
||||||
|
- Implement `ExactSizeIterator` and `FusedIterator` for all `HeaderMap` iterators. [#2470]
|
||||||
|
- Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
||||||
|
|
||||||
|
[#2467]: https://github.com/actix/actix-web/pull/2467
|
||||||
|
[#2470]: https://github.com/actix/actix-web/pull/2470
|
||||||
|
[#2474]: https://github.com/actix/actix-web/pull/2474
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.13 - 2021-11-22
|
||||||
|
### Added
|
||||||
|
- `body::AnyBody::empty` for quickly creating an empty body. [#2446]
|
||||||
|
- `body::AnyBody::none` for quickly creating a "none" body. [#2456]
|
||||||
|
- `impl Clone` for `body::AnyBody<S> where S: Clone`. [#2448]
|
||||||
|
- `body::AnyBody::into_boxed` for quickly converting to a type-erased, boxed body type. [#2448]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Rename `body::AnyBody::{Message => Body}`. [#2446]
|
||||||
|
- Rename `body::AnyBody::{from_message => new_boxed}`. [#2448]
|
||||||
|
- Rename `body::AnyBody::{from_slice => copy_from_slice}`. [#2448]
|
||||||
|
- Rename `body::{BoxAnyBody => BoxBody}`. [#2448]
|
||||||
|
- Change representation of `AnyBody` to include a type parameter in `Body` variant. Defaults to `BoxBody`. [#2448]
|
||||||
|
- `Encoder::response` now returns `AnyBody<Encoder<B>>`. [#2448]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `body::AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446]
|
||||||
|
- `body::BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446]
|
||||||
|
- `EncoderError::Boxed`; it is no longer required. [#2446]
|
||||||
|
- `body::ResponseBody`; is function is replaced by the new `body::AnyBody` enum. [#2446]
|
||||||
|
|
||||||
|
[#2446]: https://github.com/actix/actix-web/pull/2446
|
||||||
|
[#2448]: https://github.com/actix/actix-web/pull/2448
|
||||||
|
[#2456]: https://github.com/actix/actix-web/pull/2456
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.12 - 2021-11-15
|
||||||
|
### Changed
|
||||||
|
- Update `actix-server` to `2.0.0-beta.9`. [#2442]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `client` module. [#2425]
|
||||||
|
- `trust-dns` feature. [#2425]
|
||||||
|
|
||||||
|
[#2425]: https://github.com/actix/actix-web/pull/2425
|
||||||
|
[#2442]: https://github.com/actix/actix-web/pull/2442
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.11 - 2021-10-20
|
||||||
|
### Changed
|
||||||
|
- Updated rustls to v0.20. [#2414]
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||||
|
|
||||||
|
[#2414]: https://github.com/actix/actix-web/pull/2414
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.10 - 2021-09-09
|
||||||
|
### Changed
|
||||||
|
- `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377]
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364]
|
||||||
|
- Remove `Into<Error>` bound on `Encoder` body types. [#2375]
|
||||||
|
- Fix quality parse error in Accept-Encoding header. [#2344]
|
||||||
|
|
||||||
[#2364]: https://github.com/actix/actix-web/pull/2364
|
[#2364]: https://github.com/actix/actix-web/pull/2364
|
||||||
[#2375]: https://github.com/actix/actix-web/pull/2375
|
[#2375]: https://github.com/actix/actix-web/pull/2375
|
||||||
@@ -21,15 +238,15 @@
|
|||||||
|
|
||||||
## 3.0.0-beta.9 - 2021-08-09
|
## 3.0.0-beta.9 - 2021-08-09
|
||||||
### Fixed
|
### Fixed
|
||||||
* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
|
- Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.8 - 2021-06-26
|
## 3.0.0-beta.8 - 2021-06-26
|
||||||
### Changed
|
### Changed
|
||||||
* Change compression algorithm features flags. [#2250]
|
- Change compression algorithm features flags. [#2250]
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
* `downcast` and `downcast_get_type_id` macros. [#2291]
|
- `downcast` and `downcast_get_type_id` macros. [#2291]
|
||||||
|
|
||||||
[#2291]: https://github.com/actix/actix-web/pull/2291
|
[#2291]: https://github.com/actix/actix-web/pull/2291
|
||||||
[#2250]: https://github.com/actix/actix-web/pull/2250
|
[#2250]: https://github.com/actix/actix-web/pull/2250
|
||||||
@@ -37,37 +254,37 @@
|
|||||||
|
|
||||||
## 3.0.0-beta.7 - 2021-06-17
|
## 3.0.0-beta.7 - 2021-06-17
|
||||||
### Added
|
### Added
|
||||||
* Alias `body::Body` as `body::AnyBody`. [#2215]
|
- Alias `body::Body` as `body::AnyBody`. [#2215]
|
||||||
* `BoxAnyBody`: a boxed message body with boxed errors. [#2183]
|
- `BoxAnyBody`: a boxed message body with boxed errors. [#2183]
|
||||||
* Re-export `http` crate's `Error` type as `error::HttpError`. [#2171]
|
- Re-export `http` crate's `Error` type as `error::HttpError`. [#2171]
|
||||||
* Re-export `StatusCode`, `Method`, `Version` and `Uri` at the crate root. [#2171]
|
- Re-export `StatusCode`, `Method`, `Version` and `Uri` at the crate root. [#2171]
|
||||||
* Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171]
|
- Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171]
|
||||||
* `Response::into_body` that consumes response and returns body type. [#2201]
|
- `Response::into_body` that consumes response and returns body type. [#2201]
|
||||||
* `impl Default` for `Response`. [#2201]
|
- `impl Default` for `Response`. [#2201]
|
||||||
* Add zstd support for `ContentEncoding`. [#2244]
|
- Add zstd support for `ContentEncoding`. [#2244]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* The `MessageBody` trait now has an associated `Error` type. [#2183]
|
- The `MessageBody` trait now has an associated `Error` type. [#2183]
|
||||||
* All error trait bounds in server service builders have changed from `Into<Error>` to `Into<Response<AnyBody>>`. [#2253]
|
- All error trait bounds in server service builders have changed from `Into<Error>` to `Into<Response<AnyBody>>`. [#2253]
|
||||||
* All error trait bounds in message body and stream impls changed from `Into<Error>` to `Into<Box<dyn std::error::Error>>`. [#2253]
|
- All error trait bounds in message body and stream impls changed from `Into<Error>` to `Into<Box<dyn std::error::Error>>`. [#2253]
|
||||||
* Places in `Response` where `ResponseBody<B>` was received or returned now simply use `B`. [#2201]
|
- Places in `Response` where `ResponseBody<B>` was received or returned now simply use `B`. [#2201]
|
||||||
* `header` mod is now public. [#2171]
|
- `header` mod is now public. [#2171]
|
||||||
* `uri` mod is now public. [#2171]
|
- `uri` mod is now public. [#2171]
|
||||||
* Update `language-tags` to `0.3`.
|
- Update `language-tags` to `0.3`.
|
||||||
* Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201]
|
- Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201]
|
||||||
* `ResponseBuilder::message_body` now returns a `Result`. [#2201]
|
- `ResponseBuilder::message_body` now returns a `Result`. [#2201]
|
||||||
* Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253]
|
- Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253]
|
||||||
* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
|
- `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
* Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171]
|
- Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171]
|
||||||
* Down-casting for `MessageBody` types. [#2183]
|
- Down-casting for `MessageBody` types. [#2183]
|
||||||
* `error::Result` alias. [#2201]
|
- `error::Result` alias. [#2201]
|
||||||
* Error field from `Response` and `Response::error`. [#2205]
|
- Error field from `Response` and `Response::error`. [#2205]
|
||||||
* `impl Future` for `Response`. [#2201]
|
- `impl Future` for `Response`. [#2201]
|
||||||
* `Response::take_body` and old `Response::into_body` method that casted body type. [#2201]
|
- `Response::take_body` and old `Response::into_body` method that casted body type. [#2201]
|
||||||
* `InternalError` and all the error types it constructed. [#2215]
|
- `InternalError` and all the error types it constructed. [#2215]
|
||||||
* Conversion (`impl Into`) of `Response<Body>` and `ResponseBuilder` to `Error`. [#2215]
|
- Conversion (`impl Into`) of `Response<Body>` and `ResponseBuilder` to `Error`. [#2215]
|
||||||
|
|
||||||
[#2171]: https://github.com/actix/actix-web/pull/2171
|
[#2171]: https://github.com/actix/actix-web/pull/2171
|
||||||
[#2183]: https://github.com/actix/actix-web/pull/2183
|
[#2183]: https://github.com/actix/actix-web/pull/2183
|
||||||
@@ -82,27 +299,27 @@
|
|||||||
|
|
||||||
## 3.0.0-beta.6 - 2021-04-17
|
## 3.0.0-beta.6 - 2021-04-17
|
||||||
### Added
|
### Added
|
||||||
* `impl<T: MessageBody> MessageBody for Pin<Box<T>>`. [#2152]
|
- `impl<T: MessageBody> MessageBody for Pin<Box<T>>`. [#2152]
|
||||||
* `Response::{ok, bad_request, not_found, internal_server_error}`. [#2159]
|
- `Response::{ok, bad_request, not_found, internal_server_error}`. [#2159]
|
||||||
* Helper `body::to_bytes` for async collecting message body into Bytes. [#2158]
|
- Helper `body::to_bytes` for async collecting message body into Bytes. [#2158]
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
* The type parameter of `Response` no longer has a default. [#2152]
|
- The type parameter of `Response` no longer has a default. [#2152]
|
||||||
* The `Message` variant of `body::Body` is now `Pin<Box<dyn MessageBody>>`. [#2152]
|
- The `Message` variant of `body::Body` is now `Pin<Box<dyn MessageBody>>`. [#2152]
|
||||||
* `BodyStream` and `SizedStream` are no longer restricted to Unpin types. [#2152]
|
- `BodyStream` and `SizedStream` are no longer restricted to Unpin types. [#2152]
|
||||||
* Error enum types are marked `#[non_exhaustive]`. [#2161]
|
- Error enum types are marked `#[non_exhaustive]`. [#2161]
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
* `cookies` feature flag. [#2065]
|
- `cookies` feature flag. [#2065]
|
||||||
* Top-level `cookies` mod (re-export). [#2065]
|
- Top-level `cookies` mod (re-export). [#2065]
|
||||||
* `HttpMessage` trait loses the `cookies` and `cookie` methods. [#2065]
|
- `HttpMessage` trait loses the `cookies` and `cookie` methods. [#2065]
|
||||||
* `impl ResponseError for CookieParseError`. [#2065]
|
- `impl ResponseError for CookieParseError`. [#2065]
|
||||||
* Deprecated methods on `ResponseBuilder`: `if_true`, `if_some`. [#2148]
|
- Deprecated methods on `ResponseBuilder`: `if_true`, `if_some`. [#2148]
|
||||||
* `ResponseBuilder::json`. [#2148]
|
- `ResponseBuilder::json`. [#2148]
|
||||||
* `ResponseBuilder::{set_header, header}`. [#2148]
|
- `ResponseBuilder::{set_header, header}`. [#2148]
|
||||||
* `impl From<serde_json::Value> for Body`. [#2148]
|
- `impl From<serde_json::Value> for Body`. [#2148]
|
||||||
* `Response::build_from`. [#2159]
|
- `Response::build_from`. [#2159]
|
||||||
* Most of the status code builders on `Response`. [#2159]
|
- Most of the status code builders on `Response`. [#2159]
|
||||||
|
|
||||||
[#2065]: https://github.com/actix/actix-web/pull/2065
|
[#2065]: https://github.com/actix/actix-web/pull/2065
|
||||||
[#2148]: https://github.com/actix/actix-web/pull/2148
|
[#2148]: https://github.com/actix/actix-web/pull/2148
|
||||||
@@ -114,16 +331,16 @@
|
|||||||
|
|
||||||
## 3.0.0-beta.5 - 2021-04-02
|
## 3.0.0-beta.5 - 2021-04-02
|
||||||
### Added
|
### Added
|
||||||
* `client::Connector::handshake_timeout` method for customizing TLS connection handshake timeout. [#2081]
|
- `client::Connector::handshake_timeout` method for customizing TLS connection handshake timeout. [#2081]
|
||||||
* `client::ConnectorService` as `client::Connector::finish` method's return type [#2081]
|
- `client::ConnectorService` as `client::Connector::finish` method's return type [#2081]
|
||||||
* `client::ConnectionIo` trait alias [#2081]
|
- `client::ConnectionIo` trait alias [#2081]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* `client::Connector` type now only have one generic type for `actix_service::Service`. [#2063]
|
- `client::Connector` type now only have one generic type for `actix_service::Service`. [#2063]
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
* Common typed HTTP headers were moved to actix-web. [2094]
|
- Common typed HTTP headers were moved to actix-web. [2094]
|
||||||
* `ResponseError` impl for `actix_utils::timeout::TimeoutError`. [#2127]
|
- `ResponseError` impl for `actix_utils::timeout::TimeoutError`. [#2127]
|
||||||
|
|
||||||
[#2063]: https://github.com/actix/actix-web/pull/2063
|
[#2063]: https://github.com/actix/actix-web/pull/2063
|
||||||
[#2081]: https://github.com/actix/actix-web/pull/2081
|
[#2081]: https://github.com/actix/actix-web/pull/2081
|
||||||
@@ -133,13 +350,13 @@
|
|||||||
|
|
||||||
## 3.0.0-beta.4 - 2021-03-08
|
## 3.0.0-beta.4 - 2021-03-08
|
||||||
### Changed
|
### Changed
|
||||||
* Feature `cookies` is now optional and disabled by default. [#1981]
|
- Feature `cookies` is now optional and disabled by default. [#1981]
|
||||||
* `ws::hash_key` now returns array. [#2035]
|
- `ws::hash_key` now returns array. [#2035]
|
||||||
* `ResponseBuilder::json` now takes `impl Serialize`. [#2052]
|
- `ResponseBuilder::json` now takes `impl Serialize`. [#2052]
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
* Re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994]
|
- Re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994]
|
||||||
* `ResponseError` impl for `futures_channel::oneshot::Canceled` is removed. [#1994]
|
- `ResponseError` impl for `futures_channel::oneshot::Canceled` is removed. [#1994]
|
||||||
|
|
||||||
[#1981]: https://github.com/actix/actix-web/pull/1981
|
[#1981]: https://github.com/actix/actix-web/pull/1981
|
||||||
[#1994]: https://github.com/actix/actix-web/pull/1994
|
[#1994]: https://github.com/actix/actix-web/pull/1994
|
||||||
@@ -148,48 +365,48 @@
|
|||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.3 - 2021-02-10
|
## 3.0.0-beta.3 - 2021-02-10
|
||||||
* No notable changes.
|
- No notable changes.
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.2 - 2021-02-10
|
## 3.0.0-beta.2 - 2021-02-10
|
||||||
### Added
|
### Added
|
||||||
* `IntoHeaderPair` trait that allows using typed and untyped headers in the same methods. [#1869]
|
- `TryIntoHeaderPair` trait that allows using typed and untyped headers in the same methods. [#1869]
|
||||||
* `ResponseBuilder::insert_header` method which allows using typed headers. [#1869]
|
- `ResponseBuilder::insert_header` method which allows using typed headers. [#1869]
|
||||||
* `ResponseBuilder::append_header` method which allows using typed headers. [#1869]
|
- `ResponseBuilder::append_header` method which allows using typed headers. [#1869]
|
||||||
* `TestRequest::insert_header` method which allows using typed headers. [#1869]
|
- `TestRequest::insert_header` method which allows using typed headers. [#1869]
|
||||||
* `ContentEncoding` implements all necessary header traits. [#1912]
|
- `ContentEncoding` implements all necessary header traits. [#1912]
|
||||||
* `HeaderMap::len_keys` has the behavior of the old `len` method. [#1964]
|
- `HeaderMap::len_keys` has the behavior of the old `len` method. [#1964]
|
||||||
* `HeaderMap::drain` as an efficient draining iterator. [#1964]
|
- `HeaderMap::drain` as an efficient draining iterator. [#1964]
|
||||||
* Implement `IntoIterator` for owned `HeaderMap`. [#1964]
|
- Implement `IntoIterator` for owned `HeaderMap`. [#1964]
|
||||||
* `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969]
|
- `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* `ResponseBuilder::content_type` now takes an `impl IntoHeaderValue` to support using typed
|
- `ResponseBuilder::content_type` now takes an `impl TryIntoHeaderValue` to support using typed
|
||||||
`mime` types. [#1894]
|
`mime` types. [#1894]
|
||||||
* Renamed `IntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std
|
- Renamed `TryIntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std
|
||||||
`TryInto` trait. [#1894]
|
`TryInto` trait. [#1894]
|
||||||
* `Extensions::insert` returns Option of replaced item. [#1904]
|
- `Extensions::insert` returns Option of replaced item. [#1904]
|
||||||
* Remove `HttpResponseBuilder::json2()`. [#1903]
|
- Remove `HttpResponseBuilder::json2()`. [#1903]
|
||||||
* Enable `HttpResponseBuilder::json()` to receive data by value and reference. [#1903]
|
- Enable `HttpResponseBuilder::json()` to receive data by value and reference. [#1903]
|
||||||
* `client::error::ConnectError` Resolver variant contains `Box<dyn std::error::Error>` type. [#1905]
|
- `client::error::ConnectError` Resolver variant contains `Box<dyn std::error::Error>` type. [#1905]
|
||||||
* `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905]
|
- `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905]
|
||||||
* Simplify `BlockingError` type to a unit struct. It's now only triggered when blocking thread pool
|
- Simplify `BlockingError` type to a unit struct. It's now only triggered when blocking thread pool
|
||||||
is dead. [#1957]
|
is dead. [#1957]
|
||||||
* `HeaderMap::len` now returns number of values instead of number of keys. [#1964]
|
- `HeaderMap::len` now returns number of values instead of number of keys. [#1964]
|
||||||
* `HeaderMap::insert` now returns iterator of removed values. [#1964]
|
- `HeaderMap::insert` now returns iterator of removed values. [#1964]
|
||||||
* `HeaderMap::remove` now returns iterator of removed values. [#1964]
|
- `HeaderMap::remove` now returns iterator of removed values. [#1964]
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
* `ResponseBuilder::set`; use `ResponseBuilder::insert_header`. [#1869]
|
- `ResponseBuilder::set`; use `ResponseBuilder::insert_header`. [#1869]
|
||||||
* `ResponseBuilder::set_header`; use `ResponseBuilder::insert_header`. [#1869]
|
- `ResponseBuilder::set_header`; use `ResponseBuilder::insert_header`. [#1869]
|
||||||
* `ResponseBuilder::header`; use `ResponseBuilder::append_header`. [#1869]
|
- `ResponseBuilder::header`; use `ResponseBuilder::append_header`. [#1869]
|
||||||
* `TestRequest::with_hdr`; use `TestRequest::default().insert_header()`. [#1869]
|
- `TestRequest::with_hdr`; use `TestRequest::default().insert_header()`. [#1869]
|
||||||
* `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869]
|
- `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869]
|
||||||
* `actors` optional feature. [#1969]
|
- `actors` optional feature. [#1969]
|
||||||
* `ResponseError` impl for `actix::MailboxError`. [#1969]
|
- `ResponseError` impl for `actix::MailboxError`. [#1969]
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
* Vastly improve docs and add examples for `HeaderMap`. [#1964]
|
- Vastly improve docs and add examples for `HeaderMap`. [#1964]
|
||||||
|
|
||||||
[#1869]: https://github.com/actix/actix-web/pull/1869
|
[#1869]: https://github.com/actix/actix-web/pull/1869
|
||||||
[#1894]: https://github.com/actix/actix-web/pull/1894
|
[#1894]: https://github.com/actix/actix-web/pull/1894
|
||||||
@@ -204,24 +421,24 @@
|
|||||||
|
|
||||||
## 3.0.0-beta.1 - 2021-01-07
|
## 3.0.0-beta.1 - 2021-01-07
|
||||||
### Added
|
### Added
|
||||||
* Add `Http3` to `Protocol` enum for future compatibility and also mark `#[non_exhaustive]`.
|
- Add `Http3` to `Protocol` enum for future compatibility and also mark `#[non_exhaustive]`.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* Update `actix-*` dependencies to tokio `1.0` based versions. [#1813]
|
- Update `actix-*` dependencies to tokio `1.0` based versions. [#1813]
|
||||||
* Bumped `rand` to `0.8`.
|
- Bumped `rand` to `0.8`.
|
||||||
* Update `bytes` to `1.0`. [#1813]
|
- Update `bytes` to `1.0`. [#1813]
|
||||||
* Update `h2` to `0.3`. [#1813]
|
- Update `h2` to `0.3`. [#1813]
|
||||||
* The `ws::Message::Text` enum variant now contains a `bytestring::ByteString`. [#1864]
|
- The `ws::Message::Text` enum variant now contains a `bytestring::ByteString`. [#1864]
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
* Deprecated `on_connect` methods have been removed. Prefer the new
|
- Deprecated `on_connect` methods have been removed. Prefer the new
|
||||||
`on_connect_ext` technique. [#1857]
|
`on_connect_ext` technique. [#1857]
|
||||||
* Remove `ResponseError` impl for `actix::actors::resolver::ResolverError`
|
- Remove `ResponseError` impl for `actix::actors::resolver::ResolverError`
|
||||||
due to deprecate of resolver actor. [#1813]
|
due to deprecate of resolver actor. [#1813]
|
||||||
* Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`.
|
- Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`.
|
||||||
due to the removal of this type from `tokio-openssl` crate. openssl handshake
|
due to the removal of this type from `tokio-openssl` crate. openssl handshake
|
||||||
error would return as `ConnectError::SslError`. [#1813]
|
error would return as `ConnectError::SslError`. [#1813]
|
||||||
* Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`.
|
- Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`.
|
||||||
Due to this change `actix_threadpool::BlockingError` type is moved into
|
Due to this change `actix_threadpool::BlockingError` type is moved into
|
||||||
`actix_http::error` module. [#1878]
|
`actix_http::error` module. [#1878]
|
||||||
|
|
||||||
@@ -231,22 +448,29 @@
|
|||||||
[#1878]: https://github.com/actix/actix-web/pull/1878
|
[#1878]: https://github.com/actix/actix-web/pull/1878
|
||||||
|
|
||||||
|
|
||||||
|
## 2.2.2 - 2022-01-21
|
||||||
|
### Changed
|
||||||
|
- Migrate to `brotli` crate. [ad7e3c06]
|
||||||
|
|
||||||
|
[ad7e3c06]: https://github.com/actix/actix-web/commit/ad7e3c06
|
||||||
|
|
||||||
|
|
||||||
## 2.2.1 - 2021-08-09
|
## 2.2.1 - 2021-08-09
|
||||||
### Fixed
|
### Fixed
|
||||||
* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
|
- Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
|
||||||
|
|
||||||
|
|
||||||
## 2.2.0 - 2020-11-25
|
## 2.2.0 - 2020-11-25
|
||||||
### Added
|
### Added
|
||||||
* HttpResponse builders for 1xx status codes. [#1768]
|
- HttpResponse builders for 1xx status codes. [#1768]
|
||||||
* `Accept::mime_precedence` and `Accept::mime_preference`. [#1793]
|
- `Accept::mime_precedence` and `Accept::mime_preference`. [#1793]
|
||||||
* `TryFrom<u16>` and `TryFrom<f32>` for `http::header::Quality`. [#1797]
|
- `TryFrom<u16>` and `TryFrom<f32>` for `http::header::Quality`. [#1797]
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* Started dropping `transfer-encoding: chunked` and `Content-Length` for 1XX and 204 responses. [#1767]
|
- Started dropping `transfer-encoding: chunked` and `Content-Length` for 1XX and 204 responses. [#1767]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
- Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||||
|
|
||||||
[#1773]: https://github.com/actix/actix-web/pull/1773
|
[#1773]: https://github.com/actix/actix-web/pull/1773
|
||||||
[#1767]: https://github.com/actix/actix-web/pull/1767
|
[#1767]: https://github.com/actix/actix-web/pull/1767
|
||||||
@@ -257,12 +481,12 @@
|
|||||||
|
|
||||||
## 2.1.0 - 2020-10-30
|
## 2.1.0 - 2020-10-30
|
||||||
### Added
|
### Added
|
||||||
* Added more flexible `on_connect_ext` methods for on-connect handling. [#1754]
|
- Added more flexible `on_connect_ext` methods for on-connect handling. [#1754]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* Upgrade `base64` to `0.13`. [#1744]
|
- Upgrade `base64` to `0.13`. [#1744]
|
||||||
* Upgrade `pin-project` to `1.0`. [#1733]
|
- Upgrade `pin-project` to `1.0`. [#1733]
|
||||||
* Deprecate `ResponseBuilder::{if_some, if_true}`. [#1760]
|
- Deprecate `ResponseBuilder::{if_some, if_true}`. [#1760]
|
||||||
|
|
||||||
[#1760]: https://github.com/actix/actix-web/pull/1760
|
[#1760]: https://github.com/actix/actix-web/pull/1760
|
||||||
[#1754]: https://github.com/actix/actix-web/pull/1754
|
[#1754]: https://github.com/actix/actix-web/pull/1754
|
||||||
@@ -271,28 +495,28 @@
|
|||||||
|
|
||||||
|
|
||||||
## 2.0.0 - 2020-09-11
|
## 2.0.0 - 2020-09-11
|
||||||
* No significant changes from `2.0.0-beta.4`.
|
- No significant changes from `2.0.0-beta.4`.
|
||||||
|
|
||||||
|
|
||||||
## 2.0.0-beta.4 - 2020-09-09
|
## 2.0.0-beta.4 - 2020-09-09
|
||||||
### Changed
|
### Changed
|
||||||
* Update actix-codec and actix-utils dependencies.
|
- Update actix-codec and actix-utils dependencies.
|
||||||
* Update actix-connect and actix-tls dependencies.
|
- Update actix-connect and actix-tls dependencies.
|
||||||
|
|
||||||
|
|
||||||
## 2.0.0-beta.3 - 2020-08-14
|
## 2.0.0-beta.3 - 2020-08-14
|
||||||
### Fixed
|
### Fixed
|
||||||
* Memory leak of `client::pool::ConnectorPoolSupport`. [#1626]
|
- Memory leak of `client::pool::ConnectorPoolSupport`. [#1626]
|
||||||
|
|
||||||
[#1626]: https://github.com/actix/actix-web/pull/1626
|
[#1626]: https://github.com/actix/actix-web/pull/1626
|
||||||
|
|
||||||
|
|
||||||
## 2.0.0-beta.2 - 2020-07-21
|
## 2.0.0-beta.2 - 2020-07-21
|
||||||
### Fixed
|
### Fixed
|
||||||
* Potential UB in h1 decoder using uninitialized memory. [#1614]
|
- Potential UB in h1 decoder using uninitialized memory. [#1614]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* Fix illegal chunked encoding. [#1615]
|
- Fix illegal chunked encoding. [#1615]
|
||||||
|
|
||||||
[#1614]: https://github.com/actix/actix-web/pull/1614
|
[#1614]: https://github.com/actix/actix-web/pull/1614
|
||||||
[#1615]: https://github.com/actix/actix-web/pull/1615
|
[#1615]: https://github.com/actix/actix-web/pull/1615
|
||||||
@@ -300,10 +524,10 @@
|
|||||||
|
|
||||||
## 2.0.0-beta.1 - 2020-07-11
|
## 2.0.0-beta.1 - 2020-07-11
|
||||||
### Changed
|
### Changed
|
||||||
* Migrate cookie handling to `cookie` crate. [#1558]
|
- Migrate cookie handling to `cookie` crate. [#1558]
|
||||||
* Update `sha-1` to 0.9. [#1586]
|
- Update `sha-1` to 0.9. [#1586]
|
||||||
* Fix leak in client pool. [#1580]
|
- Fix leak in client pool. [#1580]
|
||||||
* MSRV is now 1.41.1.
|
- MSRV is now 1.41.1.
|
||||||
|
|
||||||
[#1558]: https://github.com/actix/actix-web/pull/1558
|
[#1558]: https://github.com/actix/actix-web/pull/1558
|
||||||
[#1586]: https://github.com/actix/actix-web/pull/1586
|
[#1586]: https://github.com/actix/actix-web/pull/1586
|
||||||
@@ -312,15 +536,15 @@
|
|||||||
|
|
||||||
## 2.0.0-alpha.4 - 2020-05-21
|
## 2.0.0-alpha.4 - 2020-05-21
|
||||||
### Changed
|
### Changed
|
||||||
* Bump minimum supported Rust version to 1.40
|
- Bump minimum supported Rust version to 1.40
|
||||||
* content_length function is removed, and you can set Content-Length by calling
|
- content_length function is removed, and you can set Content-Length by calling
|
||||||
no_chunking function [#1439]
|
no_chunking function [#1439]
|
||||||
* `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
|
- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
|
||||||
`u64` instead of a `usize`.
|
`u64` instead of a `usize`.
|
||||||
* Update `base64` dependency to 0.12
|
- Update `base64` dependency to 0.12
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* Support parsing of `SameSite=None` [#1503]
|
- Support parsing of `SameSite=None` [#1503]
|
||||||
|
|
||||||
[#1439]: https://github.com/actix/actix-web/pull/1439
|
[#1439]: https://github.com/actix/actix-web/pull/1439
|
||||||
[#1503]: https://github.com/actix/actix-web/pull/1503
|
[#1503]: https://github.com/actix/actix-web/pull/1503
|
||||||
@@ -328,13 +552,13 @@
|
|||||||
|
|
||||||
## 2.0.0-alpha.3 - 2020-05-08
|
## 2.0.0-alpha.3 - 2020-05-08
|
||||||
### Fixed
|
### Fixed
|
||||||
* Correct spelling of ConnectError::Unresolved [#1487]
|
- Correct spelling of ConnectError::Unresolved [#1487]
|
||||||
* Fix a mistake in the encoding of websocket continuation messages wherein
|
- Fix a mistake in the encoding of websocket continuation messages wherein
|
||||||
Item::FirstText and Item::FirstBinary are each encoded as the other.
|
Item::FirstText and Item::FirstBinary are each encoded as the other.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* Implement `std::error::Error` for our custom errors [#1422]
|
- Implement `std::error::Error` for our custom errors [#1422]
|
||||||
* Remove `failure` support for `ResponseError` since that crate
|
- Remove `failure` support for `ResponseError` since that crate
|
||||||
will be deprecated in the near future.
|
will be deprecated in the near future.
|
||||||
|
|
||||||
[#1422]: https://github.com/actix/actix-web/pull/1422
|
[#1422]: https://github.com/actix/actix-web/pull/1422
|
||||||
@@ -343,12 +567,12 @@
|
|||||||
|
|
||||||
## 2.0.0-alpha.2 - 2020-03-07
|
## 2.0.0-alpha.2 - 2020-03-07
|
||||||
### Changed
|
### Changed
|
||||||
* Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395]
|
- Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395]
|
||||||
* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB
|
- Change default initial window size and connection window size for HTTP2 to 2MB and 1MB
|
||||||
respectively to improve download speed for awc when downloading large objects. [#1394]
|
respectively to improve download speed for awc when downloading large objects. [#1394]
|
||||||
* client::Connector accepts initial_window_size and initial_connection_window_size
|
- client::Connector accepts initial_window_size and initial_connection_window_size
|
||||||
HTTP2 configuration. [#1394]
|
HTTP2 configuration. [#1394]
|
||||||
* client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394]
|
- client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394]
|
||||||
|
|
||||||
[#1394]: https://github.com/actix/actix-web/pull/1394
|
[#1394]: https://github.com/actix/actix-web/pull/1394
|
||||||
[#1395]: https://github.com/actix/actix-web/pull/1395
|
[#1395]: https://github.com/actix/actix-web/pull/1395
|
||||||
@@ -356,61 +580,61 @@
|
|||||||
|
|
||||||
## 2.0.0-alpha.1 - 2020-02-27
|
## 2.0.0-alpha.1 - 2020-02-27
|
||||||
### Changed
|
### Changed
|
||||||
* Update the `time` dependency to 0.2.7.
|
- Update the `time` dependency to 0.2.7.
|
||||||
* Moved actors messages support from actix crate, enabled with feature `actors`.
|
- Moved actors messages support from actix crate, enabled with feature `actors`.
|
||||||
* Breaking change: trait MessageBody requires Unpin and accepting `Pin<&mut Self>` instead of
|
- Breaking change: trait MessageBody requires Unpin and accepting `Pin<&mut Self>` instead of
|
||||||
`&mut self` in the poll_next().
|
`&mut self` in the poll_next().
|
||||||
* MessageBody is not implemented for &'static [u8] anymore.
|
- MessageBody is not implemented for &'static [u8] anymore.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* Allow `SameSite=None` cookies to be sent in a response.
|
- Allow `SameSite=None` cookies to be sent in a response.
|
||||||
|
|
||||||
|
|
||||||
## 1.0.1 - 2019-12-20
|
## 1.0.1 - 2019-12-20
|
||||||
### Fixed
|
### Fixed
|
||||||
* Poll upgrade service's readiness from HTTP service handlers
|
- Poll upgrade service's readiness from HTTP service handlers
|
||||||
* Replace brotli with brotli2 #1224
|
- Replace brotli with brotli2 #1224
|
||||||
|
|
||||||
|
|
||||||
## 1.0.0 - 2019-12-13
|
## 1.0.0 - 2019-12-13
|
||||||
### Added
|
### Added
|
||||||
* Add websockets continuation frame support
|
- Add websockets continuation frame support
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* Replace `flate2-xxx` features with `compress`
|
- Replace `flate2-xxx` features with `compress`
|
||||||
|
|
||||||
|
|
||||||
## 1.0.0-alpha.5 - 2019-12-09
|
## 1.0.0-alpha.5 - 2019-12-09
|
||||||
### Fixed
|
### Fixed
|
||||||
* Check `Upgrade` service readiness before calling it
|
- Check `Upgrade` service readiness before calling it
|
||||||
* Fix buffer remaining capacity calculation
|
- Fix buffer remaining capacity calculation
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* Websockets: Ping and Pong should have binary data #1049
|
- Websockets: Ping and Pong should have binary data #1049
|
||||||
|
|
||||||
|
|
||||||
## 1.0.0-alpha.4 - 2019-12-08
|
## 1.0.0-alpha.4 - 2019-12-08
|
||||||
### Added
|
### Added
|
||||||
* Add impl ResponseBuilder for Error
|
- Add impl ResponseBuilder for Error
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* Use rust based brotli compression library
|
- Use rust based brotli compression library
|
||||||
|
|
||||||
## 1.0.0-alpha.3 - 2019-12-07
|
## 1.0.0-alpha.3 - 2019-12-07
|
||||||
### Changed
|
### Changed
|
||||||
* Migrate to tokio 0.2
|
- Migrate to tokio 0.2
|
||||||
* Migrate to `std::future`
|
- Migrate to `std::future`
|
||||||
|
|
||||||
|
|
||||||
## 0.2.11 - 2019-11-06
|
## 0.2.11 - 2019-11-06
|
||||||
### Added
|
### Added
|
||||||
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()
|
- Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()
|
||||||
* Add an additional `filename*` param in the `Content-Disposition` header of
|
- Add an additional `filename*` param in the `Content-Disposition` header of
|
||||||
`actix_files::NamedFile` to be more compatible. (#1151)
|
`actix_files::NamedFile` to be more compatible. (#1151)
|
||||||
* Allow to use `std::convert::Infallible` as `actix_http::error::Error`
|
- Allow to use `std::convert::Infallible` as `actix_http::error::Error`
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain;
|
- To be compatible with non-English error responses, `ResponseError` rendered with `text/plain;
|
||||||
charset=utf-8` header [#1118]
|
charset=utf-8` header [#1118]
|
||||||
|
|
||||||
[#1878]: https://github.com/actix/actix-web/pull/1878
|
[#1878]: https://github.com/actix/actix-web/pull/1878
|
||||||
@@ -418,169 +642,169 @@
|
|||||||
|
|
||||||
## 0.2.10 - 2019-09-11
|
## 0.2.10 - 2019-09-11
|
||||||
### Added
|
### Added
|
||||||
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests
|
- Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests
|
||||||
with `RequestHead`
|
with `RequestHead`
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* h2 will use error response #1080
|
- h2 will use error response #1080
|
||||||
* on_connect result isn't added to request extensions for http2 requests #1009
|
- on_connect result isn't added to request extensions for http2 requests #1009
|
||||||
|
|
||||||
|
|
||||||
## 0.2.9 - 2019-08-13
|
## 0.2.9 - 2019-08-13
|
||||||
### Changed
|
### Changed
|
||||||
* Dropped the `byteorder`-dependency in favor of `stdlib`-implementation
|
- Dropped the `byteorder`-dependency in favor of `stdlib`-implementation
|
||||||
* Update percent-encoding to 2.1
|
- Update percent-encoding to 2.1
|
||||||
* Update serde_urlencoded to 0.6.1
|
- Update serde_urlencoded to 0.6.1
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031)
|
- Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031)
|
||||||
|
|
||||||
|
|
||||||
## 0.2.8 - 2019-08-01
|
## 0.2.8 - 2019-08-01
|
||||||
### Added
|
### Added
|
||||||
* Add `rustls` support
|
- Add `rustls` support
|
||||||
* Add `Clone` impl for `HeaderMap`
|
- Add `Clone` impl for `HeaderMap`
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* awc client panic #1016
|
- awc client panic #1016
|
||||||
* Invalid response with compression middleware enabled, but compression-related features
|
- Invalid response with compression middleware enabled, but compression-related features
|
||||||
disabled #997
|
disabled #997
|
||||||
|
|
||||||
|
|
||||||
## 0.2.7 - 2019-07-18
|
## 0.2.7 - 2019-07-18
|
||||||
### Added
|
### Added
|
||||||
* Add support for downcasting response errors #986
|
- Add support for downcasting response errors #986
|
||||||
|
|
||||||
|
|
||||||
## 0.2.6 - 2019-07-17
|
## 0.2.6 - 2019-07-17
|
||||||
### Changed
|
### Changed
|
||||||
* Replace `ClonableService` with local copy
|
- Replace `ClonableService` with local copy
|
||||||
* Upgrade `rand` dependency version to 0.7
|
- Upgrade `rand` dependency version to 0.7
|
||||||
|
|
||||||
|
|
||||||
## 0.2.5 - 2019-06-28
|
## 0.2.5 - 2019-06-28
|
||||||
### Added
|
### Added
|
||||||
* Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946
|
- Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* Use `encoding_rs` crate instead of unmaintained `encoding` crate
|
- Use `encoding_rs` crate instead of unmaintained `encoding` crate
|
||||||
* Add `Copy` and `Clone` impls for `ws::Codec`
|
- Add `Copy` and `Clone` impls for `ws::Codec`
|
||||||
|
|
||||||
|
|
||||||
## 0.2.4 - 2019-06-16
|
## 0.2.4 - 2019-06-16
|
||||||
### Fixed
|
### Fixed
|
||||||
* Do not compress NoContent (204) responses #918
|
- Do not compress NoContent (204) responses #918
|
||||||
|
|
||||||
|
|
||||||
## 0.2.3 - 2019-06-02
|
## 0.2.3 - 2019-06-02
|
||||||
### Added
|
### Added
|
||||||
* Debug impl for ResponseBuilder
|
- Debug impl for ResponseBuilder
|
||||||
* From SizedStream and BodyStream for Body
|
- From SizedStream and BodyStream for Body
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* SizedStream uses u64
|
- SizedStream uses u64
|
||||||
|
|
||||||
|
|
||||||
## 0.2.2 - 2019-05-29
|
## 0.2.2 - 2019-05-29
|
||||||
### Fixed
|
### Fixed
|
||||||
* Parse incoming stream before closing stream on disconnect #868
|
- Parse incoming stream before closing stream on disconnect #868
|
||||||
|
|
||||||
|
|
||||||
## 0.2.1 - 2019-05-25
|
## 0.2.1 - 2019-05-25
|
||||||
### Fixed
|
### Fixed
|
||||||
* Handle socket read disconnect
|
- Handle socket read disconnect
|
||||||
|
|
||||||
|
|
||||||
## 0.2.0 - 2019-05-12
|
## 0.2.0 - 2019-05-12
|
||||||
### Changed
|
### Changed
|
||||||
* Update actix-service to 0.4
|
- Update actix-service to 0.4
|
||||||
* Expect and upgrade services accept `ServerConfig` config.
|
- Expect and upgrade services accept `ServerConfig` config.
|
||||||
|
|
||||||
### Deleted
|
### Deleted
|
||||||
* `OneRequest` service
|
- `OneRequest` service
|
||||||
|
|
||||||
|
|
||||||
## 0.1.5 - 2019-05-04
|
## 0.1.5 - 2019-05-04
|
||||||
### Fixed
|
### Fixed
|
||||||
* Clean up response extensions in response pool #817
|
- Clean up response extensions in response pool #817
|
||||||
|
|
||||||
|
|
||||||
## 0.1.4 - 2019-04-24
|
## 0.1.4 - 2019-04-24
|
||||||
### Added
|
### Added
|
||||||
* Allow to render h1 request headers in `Camel-Case`
|
- Allow to render h1 request headers in `Camel-Case`
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* Read until eof for http/1.0 responses #771
|
- Read until eof for http/1.0 responses #771
|
||||||
|
|
||||||
|
|
||||||
## 0.1.3 - 2019-04-23
|
## 0.1.3 - 2019-04-23
|
||||||
### Fixed
|
### Fixed
|
||||||
* Fix http client pool management
|
- Fix http client pool management
|
||||||
* Fix http client wait queue management #794
|
- Fix http client wait queue management #794
|
||||||
|
|
||||||
|
|
||||||
## 0.1.2 - 2019-04-23
|
## 0.1.2 - 2019-04-23
|
||||||
### Fixed
|
### Fixed
|
||||||
* Fix BorrowMutError panic in client connector #793
|
- Fix BorrowMutError panic in client connector #793
|
||||||
|
|
||||||
|
|
||||||
## 0.1.1 - 2019-04-19
|
## 0.1.1 - 2019-04-19
|
||||||
### Changed
|
### Changed
|
||||||
* Cookie::max_age() accepts value in seconds
|
- Cookie::max_age() accepts value in seconds
|
||||||
* Cookie::max_age_time() accepts value in time::Duration
|
- Cookie::max_age_time() accepts value in time::Duration
|
||||||
* Allow to specify server address for client connector
|
- Allow to specify server address for client connector
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0 - 2019-04-16
|
## 0.1.0 - 2019-04-16
|
||||||
### Added
|
### Added
|
||||||
* Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr`
|
- Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* `actix_http::encoding` always available
|
- `actix_http::encoding` always available
|
||||||
* use trust-dns-resolver 0.11.0
|
- use trust-dns-resolver 0.11.0
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.5 - 2019-04-12
|
## 0.1.0-alpha.5 - 2019-04-12
|
||||||
### Added
|
### Added
|
||||||
* Allow to use custom service for upgrade requests
|
- Allow to use custom service for upgrade requests
|
||||||
* Added `h1::SendResponse` future.
|
- Added `h1::SendResponse` future.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* MessageBody::length() renamed to MessageBody::size() for consistency
|
- MessageBody::length() renamed to MessageBody::size() for consistency
|
||||||
* ws handshake verification functions take RequestHead instead of Request
|
- ws handshake verification functions take RequestHead instead of Request
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.4 - 2019-04-08
|
## 0.1.0-alpha.4 - 2019-04-08
|
||||||
### Added
|
### Added
|
||||||
* Allow to use custom `Expect` handler
|
- Allow to use custom `Expect` handler
|
||||||
* Add minimal `std::error::Error` impl for `Error`
|
- Add minimal `std::error::Error` impl for `Error`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* Export IntoHeaderValue
|
- Export IntoHeaderValue
|
||||||
* Render error and return as response body
|
- Render error and return as response body
|
||||||
* Use thread pool for response body compression
|
- Use thread pool for response body compression
|
||||||
|
|
||||||
### Deleted
|
### Deleted
|
||||||
* Removed PayloadBuffer
|
- Removed PayloadBuffer
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.3 - 2019-04-02
|
## 0.1.0-alpha.3 - 2019-04-02
|
||||||
### Added
|
### Added
|
||||||
* Warn when an unsealed private cookie isn't valid UTF-8
|
- Warn when an unsealed private cookie isn't valid UTF-8
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* Rust 1.31.0 compatibility
|
- Rust 1.31.0 compatibility
|
||||||
* Preallocate read buffer for h1 codec
|
- Preallocate read buffer for h1 codec
|
||||||
* Detect socket disconnection during protocol selection
|
- Detect socket disconnection during protocol selection
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.2 - 2019-03-29
|
## 0.1.0-alpha.2 - 2019-03-29
|
||||||
### Added
|
### Added
|
||||||
* Added ws::Message::Nop, no-op websockets message
|
- Added ws::Message::Nop, no-op websockets message
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* Do not use thread pool for decompression if chunk size is smaller than 2048.
|
- Do not use thread pool for decompression if chunk size is smaller than 2048.
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-alpha.1 - 2019-03-28
|
## 0.1.0-alpha.1 - 2019-03-28
|
||||||
* Initial impl
|
- Initial impl
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.0.0-beta.10"
|
version = "3.0.0-rc.1"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = [
|
||||||
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
|
]
|
||||||
description = "HTTP primitives for the Actix ecosystem"
|
description = "HTTP primitives for the Actix ecosystem"
|
||||||
keywords = ["actix", "http", "framework", "async", "futures"]
|
keywords = ["actix", "http", "framework", "async", "futures"]
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
repository = "https://github.com/actix/actix-web"
|
repository = "https://github.com/actix/actix-web.git"
|
||||||
categories = ["network-programming", "asynchronous",
|
categories = [
|
||||||
|
"network-programming",
|
||||||
|
"asynchronous",
|
||||||
"web-programming::http-server",
|
"web-programming::http-server",
|
||||||
"web-programming::websocket"]
|
"web-programming::websocket",
|
||||||
|
]
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
@@ -23,80 +29,94 @@ path = "src/lib.rs"
|
|||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|
||||||
# openssl
|
# HTTP/2 protocol support
|
||||||
openssl = ["actix-tls/openssl"]
|
http2 = ["h2"]
|
||||||
|
|
||||||
# rustls support
|
# WebSocket protocol implementation
|
||||||
rustls = ["actix-tls/rustls"]
|
ws = [
|
||||||
|
"local-channel",
|
||||||
|
"base64",
|
||||||
|
"rand",
|
||||||
|
"sha-1",
|
||||||
|
]
|
||||||
|
|
||||||
# enable compression support
|
# TLS via OpenSSL
|
||||||
compress-brotli = ["brotli2", "__compress"]
|
openssl = ["actix-tls/accept", "actix-tls/openssl"]
|
||||||
compress-gzip = ["flate2", "__compress"]
|
|
||||||
compress-zstd = ["zstd", "__compress"]
|
|
||||||
|
|
||||||
# trust-dns as client dns resolver
|
# TLS via Rustls
|
||||||
trust-dns = ["trust-dns-resolver"]
|
rustls = ["actix-tls/accept", "actix-tls/rustls"]
|
||||||
|
|
||||||
|
# Compression codecs
|
||||||
|
compress-brotli = ["__compress", "brotli"]
|
||||||
|
compress-gzip = ["__compress", "flate2"]
|
||||||
|
compress-zstd = ["__compress", "zstd"]
|
||||||
|
|
||||||
# Internal (PRIVATE!) features used to aid testing and cheking feature status.
|
# Internal (PRIVATE!) features used to aid testing and cheking feature status.
|
||||||
# Don't rely on these whatsoever. They may disappear at anytime.
|
# Don't rely on these whatsoever. They are semver-exempt and may disappear at anytime.
|
||||||
__compress = []
|
__compress = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-service = "2.0.0"
|
actix-service = "2"
|
||||||
actix-codec = "0.4.0"
|
actix-codec = "0.4.1"
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3"
|
||||||
actix-rt = "2.2"
|
actix-rt = { version = "2.2", default-features = false }
|
||||||
actix-tls = { version = "3.0.0-beta.5", features = ["accept", "connect"] }
|
|
||||||
|
|
||||||
ahash = "0.7"
|
ahash = "0.7"
|
||||||
base64 = "0.13"
|
|
||||||
bitflags = "1.2"
|
bitflags = "1.2"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
bytestring = "1"
|
bytestring = "1"
|
||||||
derive_more = "0.99.5"
|
derive_more = "0.99.5"
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
|
http = "0.2.5"
|
||||||
h2 = "0.3.1"
|
|
||||||
http = "0.2.2"
|
|
||||||
httparse = "1.5.1"
|
httparse = "1.5.1"
|
||||||
itoa = "0.4"
|
httpdate = "1.0.1"
|
||||||
|
itoa = "1"
|
||||||
language-tags = "0.3"
|
language-tags = "0.3"
|
||||||
local-channel = "0.1"
|
|
||||||
once_cell = "1.5"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
pin-project = "1.0.0"
|
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
rand = "0.8"
|
|
||||||
regex = "1.3"
|
|
||||||
serde = "1.0"
|
|
||||||
sha-1 = "0.9"
|
|
||||||
smallvec = "1.6.1"
|
smallvec = "1.6.1"
|
||||||
time = { version = "0.2.23", default-features = false, features = ["std"] }
|
|
||||||
tokio = { version = "1.2", features = ["sync"] }
|
|
||||||
|
|
||||||
# compression
|
# http2
|
||||||
brotli2 = { version="0.3.2", optional = true }
|
h2 = { version = "0.3.9", optional = true }
|
||||||
|
|
||||||
|
# websockets
|
||||||
|
local-channel = { version = "0.1", optional = true }
|
||||||
|
base64 = { version = "0.13", optional = true }
|
||||||
|
rand = { version = "0.8", optional = true }
|
||||||
|
sha-1 = { version = "0.10", optional = true }
|
||||||
|
|
||||||
|
# openssl/rustls
|
||||||
|
actix-tls = { version = "3.0.0", default-features = false, optional = true }
|
||||||
|
|
||||||
|
# compress-*
|
||||||
|
brotli = { version = "3.3.3", optional = true }
|
||||||
flate2 = { version = "1.0.13", optional = true }
|
flate2 = { version = "1.0.13", optional = true }
|
||||||
zstd = { version = "0.7", optional = true }
|
zstd = { version = "0.9", optional = true }
|
||||||
|
|
||||||
trust-dns-resolver = { version = "0.20.0", optional = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-server = "2.0.0-beta.3"
|
actix-http-test = { version = "3.0.0-beta.12", features = ["openssl"] }
|
||||||
actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] }
|
actix-server = "2"
|
||||||
actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] }
|
actix-tls = { version = "3.0.0", features = ["openssl"] }
|
||||||
|
actix-web = "4.0.0-rc.1"
|
||||||
|
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
criterion = { version = "0.3", features = ["html_reports"] }
|
criterion = { version = "0.3", features = ["html_reports"] }
|
||||||
env_logger = "0.8"
|
env_logger = "0.9"
|
||||||
|
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||||
|
memchr = "2.4"
|
||||||
|
once_cell = "1.9"
|
||||||
rcgen = "0.8"
|
rcgen = "0.8"
|
||||||
|
regex = "1.3"
|
||||||
|
rustls-pemfile = "0.2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tls-openssl = { version = "0.10", package = "openssl" }
|
static_assertions = "1"
|
||||||
tls-rustls = { version = "0.19", package = "rustls" }
|
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||||
webpki = { version = "0.21.0" }
|
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||||
|
tokio = { version = "1.8.4", features = ["net", "rt", "macros"] }
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "ws"
|
name = "ws"
|
||||||
@@ -113,3 +133,7 @@ harness = false
|
|||||||
[[bench]]
|
[[bench]]
|
||||||
name = "uninit-headers"
|
name = "uninit-headers"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "quality-value"
|
||||||
|
harness = false
|
||||||
|
|||||||
@@ -3,18 +3,18 @@
|
|||||||
> HTTP primitives for the Actix ecosystem.
|
> HTTP primitives for the Actix ecosystem.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-http)
|
[](https://crates.io/crates/actix-http)
|
||||||
[](https://docs.rs/actix-http/3.0.0-beta.10)
|
[](https://docs.rs/actix-http/3.0.0-rc.1)
|
||||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html)
|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-http/3.0.0-beta.10)
|
[](https://deps.rs/crate/actix-http/3.0.0-rc.1)
|
||||||
[](https://crates.io/crates/actix-http)
|
[](https://crates.io/crates/actix-http)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-http)
|
- [API Documentation](https://docs.rs/actix-http)
|
||||||
- Minimum Supported Rust Version (MSRV): 1.51.0
|
- Minimum Supported Rust Version (MSRV): 1.54
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
@@ -54,8 +54,8 @@ async fn main() -> io::Result<()> {
|
|||||||
|
|
||||||
This project is licensed under either of
|
This project is licensed under either of
|
||||||
|
|
||||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
|
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
|
||||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
|
- MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
|
||||||
|
|
||||||
at your option.
|
at your option.
|
||||||
|
|
||||||
|
|||||||
95
actix-http/benches/quality-value.rs
Normal file
95
actix-http/benches/quality-value.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||||
|
|
||||||
|
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
|
||||||
|
|
||||||
|
fn bench_quality_display_impls(c: &mut Criterion) {
|
||||||
|
let mut group = c.benchmark_group("quality value display impls");
|
||||||
|
|
||||||
|
for i in CODES.iter() {
|
||||||
|
group.bench_with_input(BenchmarkId::new("New (fast?)", i), i, |b, &i| {
|
||||||
|
b.iter(|| _new::Quality(i).to_string())
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
|
||||||
|
b.iter(|| _naive::Quality(i).to_string())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, bench_quality_display_impls);
|
||||||
|
criterion_main!(benches);
|
||||||
|
|
||||||
|
mod _new {
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
pub struct Quality(pub(crate) u16);
|
||||||
|
|
||||||
|
impl fmt::Display for Quality {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self.0 {
|
||||||
|
0 => f.write_str("0"),
|
||||||
|
1000 => f.write_str("1"),
|
||||||
|
|
||||||
|
// some number in the range 1–999
|
||||||
|
x => {
|
||||||
|
f.write_str("0.")?;
|
||||||
|
|
||||||
|
// this implementation avoids string allocation otherwise required
|
||||||
|
// for `.trim_end_matches('0')`
|
||||||
|
|
||||||
|
if x < 10 {
|
||||||
|
f.write_str("00")?;
|
||||||
|
// 0 is handled so it's not possible to have a trailing 0, we can just return
|
||||||
|
itoa_fmt(f, x)
|
||||||
|
} else if x < 100 {
|
||||||
|
f.write_str("0")?;
|
||||||
|
if x % 10 == 0 {
|
||||||
|
// trailing 0, divide by 10 and write
|
||||||
|
itoa_fmt(f, x / 10)
|
||||||
|
} else {
|
||||||
|
itoa_fmt(f, x)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// x is in range 101–999
|
||||||
|
|
||||||
|
if x % 100 == 0 {
|
||||||
|
// two trailing 0s, divide by 100 and write
|
||||||
|
itoa_fmt(f, x / 100)
|
||||||
|
} else if x % 10 == 0 {
|
||||||
|
// one trailing 0, divide by 10 and write
|
||||||
|
itoa_fmt(f, x / 10)
|
||||||
|
} else {
|
||||||
|
itoa_fmt(f, x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Result {
|
||||||
|
let mut buf = itoa::Buffer::new();
|
||||||
|
wr.write_str(buf.format(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod _naive {
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
pub struct Quality(pub(crate) u16);
|
||||||
|
|
||||||
|
impl fmt::Display for Quality {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self.0 {
|
||||||
|
0 => f.write_str("0"),
|
||||||
|
1000 => f.write_str("1"),
|
||||||
|
|
||||||
|
x => {
|
||||||
|
write!(f, "{}", format!("{:03}", x).trim_end_matches('0'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -189,11 +189,7 @@ mod _original {
|
|||||||
n /= 100;
|
n /= 100;
|
||||||
curr -= 2;
|
curr -= 2;
|
||||||
unsafe {
|
unsafe {
|
||||||
ptr::copy_nonoverlapping(
|
ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2);
|
||||||
lut_ptr.offset(d1 as isize),
|
|
||||||
buf_ptr.offset(curr),
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode last 1 or 2 chars
|
// decode last 1 or 2 chars
|
||||||
@@ -206,11 +202,7 @@ mod _original {
|
|||||||
let d1 = n << 1;
|
let d1 = n << 1;
|
||||||
curr -= 2;
|
curr -= 2;
|
||||||
unsafe {
|
unsafe {
|
||||||
ptr::copy_nonoverlapping(
|
ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2);
|
||||||
lut_ptr.offset(d1 as isize),
|
|
||||||
buf_ptr.offset(curr),
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,15 +54,10 @@ const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex {
|
|||||||
value: (0, 0),
|
value: (0, 0),
|
||||||
};
|
};
|
||||||
|
|
||||||
const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] =
|
const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] = [EMPTY_HEADER_INDEX; MAX_HEADERS];
|
||||||
[EMPTY_HEADER_INDEX; MAX_HEADERS];
|
|
||||||
|
|
||||||
impl HeaderIndex {
|
impl HeaderIndex {
|
||||||
fn record(
|
fn record(bytes: &[u8], headers: &[httparse::Header<'_>], indices: &mut [HeaderIndex]) {
|
||||||
bytes: &[u8],
|
|
||||||
headers: &[httparse::Header<'_>],
|
|
||||||
indices: &mut [HeaderIndex],
|
|
||||||
) {
|
|
||||||
let bytes_ptr = bytes.as_ptr() as usize;
|
let bytes_ptr = bytes.as_ptr() as usize;
|
||||||
for (header, indices) in headers.iter().zip(indices.iter_mut()) {
|
for (header, indices) in headers.iter().zip(indices.iter_mut()) {
|
||||||
let name_start = header.name.as_ptr() as usize - bytes_ptr;
|
let name_start = header.name.as_ptr() as usize - bytes_ptr;
|
||||||
|
|||||||
26
actix-http/examples/actix-web.rs
Normal file
26
actix-http/examples/actix-web.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
use actix_http::HttpService;
|
||||||
|
use actix_server::Server;
|
||||||
|
use actix_service::map_config;
|
||||||
|
use actix_web::{dev::AppConfig, get, App};
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
async fn index() -> &'static str {
|
||||||
|
"Hello, world. From Actix Web!"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
Server::build()
|
||||||
|
.bind("hello-world", "127.0.0.1:8080", || {
|
||||||
|
// construct actix-web app
|
||||||
|
let app = App::new().service(index);
|
||||||
|
|
||||||
|
HttpService::build()
|
||||||
|
// pass the app to service builder
|
||||||
|
// map_config is used to map App's configuration to ServiceBuilder
|
||||||
|
.finish(map_config(app, |_| AppConfig::default()))
|
||||||
|
.tcp()
|
||||||
|
})?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
27
actix-http/examples/bench.rs
Normal file
27
actix-http/examples/bench.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use std::{convert::Infallible, io, time::Duration};
|
||||||
|
|
||||||
|
use actix_http::{HttpService, Request, Response, StatusCode};
|
||||||
|
use actix_server::Server;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
static STR: Lazy<String> = Lazy::new(|| "HELLO WORLD ".repeat(20));
|
||||||
|
|
||||||
|
#[actix_rt::main]
|
||||||
|
async fn main() -> io::Result<()> {
|
||||||
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
|
|
||||||
|
Server::build()
|
||||||
|
.bind("dispatcher-benchmark", ("127.0.0.1", 8080), || {
|
||||||
|
HttpService::build()
|
||||||
|
.client_request_timeout(Duration::from_secs(1))
|
||||||
|
.finish(|_: Request| async move {
|
||||||
|
let mut res = Response::build(StatusCode::OK);
|
||||||
|
Ok::<_, Infallible>(res.body(&**STR))
|
||||||
|
})
|
||||||
|
.tcp()
|
||||||
|
})?
|
||||||
|
// limiting number of workers so that bench client is not sharing as many resources
|
||||||
|
.workers(4)
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::io;
|
use std::{io, time::Duration};
|
||||||
|
|
||||||
use actix_http::{http::StatusCode, Error, HttpService, Request, Response};
|
use actix_http::{Error, HttpService, Request, Response, StatusCode};
|
||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use futures_util::StreamExt as _;
|
use futures_util::StreamExt as _;
|
||||||
@@ -13,8 +13,9 @@ async fn main() -> io::Result<()> {
|
|||||||
Server::build()
|
Server::build()
|
||||||
.bind("echo", ("127.0.0.1", 8080), || {
|
.bind("echo", ("127.0.0.1", 8080), || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.client_timeout(1000)
|
.client_request_timeout(Duration::from_secs(1))
|
||||||
.client_disconnect(1000)
|
.client_disconnect_timeout(Duration::from_secs(1))
|
||||||
|
// handles HTTP/1.1 and HTTP/2
|
||||||
.finish(|mut req: Request| async move {
|
.finish(|mut req: Request| async move {
|
||||||
let mut body = BytesMut::new();
|
let mut body = BytesMut::new();
|
||||||
while let Some(item) = req.payload().next().await {
|
while let Some(item) = req.payload().next().await {
|
||||||
@@ -23,15 +24,13 @@ async fn main() -> io::Result<()> {
|
|||||||
|
|
||||||
log::info!("request body: {:?}", body);
|
log::info!("request body: {:?}", body);
|
||||||
|
|
||||||
Ok::<_, Error>(
|
let res = Response::build(StatusCode::OK)
|
||||||
Response::build(StatusCode::OK)
|
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
|
||||||
.insert_header((
|
.body(body);
|
||||||
"x-head",
|
|
||||||
HeaderValue::from_static("dummy value!"),
|
Ok::<_, Error>(res)
|
||||||
))
|
|
||||||
.body(body),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
// No TLS
|
||||||
.tcp()
|
.tcp()
|
||||||
})?
|
})?
|
||||||
.run()
|
.run()
|
||||||
|
|||||||
@@ -1,31 +1,34 @@
|
|||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use actix_http::{body::Body, http::HeaderValue, http::StatusCode};
|
use actix_http::{
|
||||||
use actix_http::{Error, HttpService, Request, Response};
|
body::{BodyStream, MessageBody},
|
||||||
use actix_server::Server;
|
header, Error, HttpMessage, HttpService, Request, Response, StatusCode,
|
||||||
use bytes::BytesMut;
|
};
|
||||||
use futures_util::StreamExt as _;
|
|
||||||
|
|
||||||
async fn handle_request(mut req: Request) -> Result<Response<Body>, Error> {
|
async fn handle_request(mut req: Request) -> Result<Response<impl MessageBody>, Error> {
|
||||||
let mut body = BytesMut::new();
|
let mut res = Response::build(StatusCode::OK);
|
||||||
while let Some(item) = req.payload().next().await {
|
|
||||||
body.extend_from_slice(&item?)
|
if let Some(ct) = req.headers().get(header::CONTENT_TYPE) {
|
||||||
|
res.insert_header((header::CONTENT_TYPE, ct));
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("request body: {:?}", body);
|
// echo request payload stream as (chunked) response body
|
||||||
|
let res = res.message_body(BodyStream::new(req.payload().take()))?;
|
||||||
|
|
||||||
Ok(Response::build(StatusCode::OK)
|
Ok(res)
|
||||||
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
|
|
||||||
.body(body))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> 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"));
|
||||||
|
|
||||||
Server::build()
|
actix_server::Server::build()
|
||||||
.bind("echo", ("127.0.0.1", 8080), || {
|
.bind("echo", ("127.0.0.1", 8080), || {
|
||||||
HttpService::build().finish(handle_request).tcp()
|
HttpService::build()
|
||||||
|
// handles HTTP/1.1 only
|
||||||
|
.h1(handle_request)
|
||||||
|
// No TLS
|
||||||
|
.tcp()
|
||||||
})?
|
})?
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
|
|||||||
25
actix-http/examples/h2spec.rs
Normal file
25
actix-http/examples/h2spec.rs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
use std::{convert::Infallible, io};
|
||||||
|
|
||||||
|
use actix_http::{HttpService, Request, Response, StatusCode};
|
||||||
|
use actix_server::Server;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
static STR: Lazy<String> = Lazy::new(|| "HELLO WORLD ".repeat(100));
|
||||||
|
|
||||||
|
#[actix_rt::main]
|
||||||
|
async fn main() -> io::Result<()> {
|
||||||
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
|
|
||||||
|
Server::build()
|
||||||
|
.bind("h2spec", ("127.0.0.1", 8080), || {
|
||||||
|
HttpService::build()
|
||||||
|
.h2(|_: Request| async move {
|
||||||
|
let mut res = Response::build(StatusCode::OK);
|
||||||
|
Ok::<_, Infallible>(res.body(&**STR))
|
||||||
|
})
|
||||||
|
.tcp()
|
||||||
|
})?
|
||||||
|
.workers(4)
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
use std::{convert::Infallible, io};
|
use std::{convert::Infallible, io, time::Duration};
|
||||||
|
|
||||||
use actix_http::{http::StatusCode, HttpService, Response};
|
use actix_http::{
|
||||||
|
header::HeaderValue, HttpMessage, HttpService, Request, Response, StatusCode,
|
||||||
|
};
|
||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
use http::header::HeaderValue;
|
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
@@ -11,15 +12,21 @@ async fn main() -> io::Result<()> {
|
|||||||
Server::build()
|
Server::build()
|
||||||
.bind("hello-world", ("127.0.0.1", 8080), || {
|
.bind("hello-world", ("127.0.0.1", 8080), || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.client_timeout(1000)
|
.client_request_timeout(Duration::from_secs(1))
|
||||||
.client_disconnect(1000)
|
.client_disconnect_timeout(Duration::from_secs(1))
|
||||||
.finish(|req| async move {
|
.on_connect_ext(|_, ext| {
|
||||||
|
ext.insert(42u32);
|
||||||
|
})
|
||||||
|
.finish(|req: Request| async move {
|
||||||
log::info!("{:?}", req);
|
log::info!("{:?}", req);
|
||||||
|
|
||||||
let mut res = Response::build(StatusCode::OK);
|
let mut res = Response::build(StatusCode::OK);
|
||||||
|
res.insert_header(("x-head", HeaderValue::from_static("dummy value!")));
|
||||||
|
|
||||||
|
let forty_two = req.extensions().get::<u32>().unwrap().to_string();
|
||||||
res.insert_header((
|
res.insert_header((
|
||||||
"x-head",
|
"x-forty-two",
|
||||||
HeaderValue::from_static("dummy value!"),
|
HeaderValue::from_str(&forty_two).unwrap(),
|
||||||
));
|
));
|
||||||
|
|
||||||
Ok::<_, Infallible>(res.body("Hello world!"))
|
Ok::<_, Infallible>(res.body("Hello world!"))
|
||||||
|
|||||||
@@ -60,10 +60,7 @@ impl Heartbeat {
|
|||||||
impl Stream for Heartbeat {
|
impl Stream for Heartbeat {
|
||||||
type Item = Result<Bytes, Error>;
|
type Item = Result<Bytes, Error>;
|
||||||
|
|
||||||
fn poll_next(
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
mut self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Self::Item>> {
|
|
||||||
log::trace!("poll");
|
log::trace!("poll");
|
||||||
|
|
||||||
ready!(self.as_mut().interval.poll_tick(cx));
|
ready!(self.as_mut().interval.poll_tick(cx));
|
||||||
@@ -85,22 +82,31 @@ impl Stream for Heartbeat {
|
|||||||
fn tls_config() -> rustls::ServerConfig {
|
fn tls_config() -> rustls::ServerConfig {
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
|
|
||||||
use rustls::{
|
use rustls::{Certificate, PrivateKey};
|
||||||
internal::pemfile::{certs, pkcs8_private_keys},
|
use rustls_pemfile::{certs, pkcs8_private_keys};
|
||||||
NoClientAuth, ServerConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
|
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
|
||||||
let cert_file = cert.serialize_pem().unwrap();
|
let cert_file = cert.serialize_pem().unwrap();
|
||||||
let key_file = cert.serialize_private_key_pem();
|
let key_file = cert.serialize_private_key_pem();
|
||||||
|
|
||||||
let mut config = ServerConfig::new(NoClientAuth::new());
|
|
||||||
let cert_file = &mut BufReader::new(cert_file.as_bytes());
|
let cert_file = &mut BufReader::new(cert_file.as_bytes());
|
||||||
let key_file = &mut BufReader::new(key_file.as_bytes());
|
let key_file = &mut BufReader::new(key_file.as_bytes());
|
||||||
|
|
||||||
let cert_chain = certs(cert_file).unwrap();
|
let cert_chain = certs(cert_file)
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.map(Certificate)
|
||||||
|
.collect();
|
||||||
let mut keys = pkcs8_private_keys(key_file).unwrap();
|
let mut keys = pkcs8_private_keys(key_file).unwrap();
|
||||||
config.set_single_cert(cert_chain, keys.remove(0)).unwrap();
|
|
||||||
|
let mut config = rustls::ServerConfig::builder()
|
||||||
|
.with_safe_defaults()
|
||||||
|
.with_no_client_auth()
|
||||||
|
.with_single_cert(cert_chain, PrivateKey(keys.remove(0)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
config.alpn_protocols.push(b"http/1.1".to_vec());
|
||||||
|
config.alpn_protocols.push(b"h2".to_vec());
|
||||||
|
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
max_width = 89
|
|
||||||
reorder_imports = true
|
|
||||||
#wrap_comments = true
|
|
||||||
#fn_args_density = "Compressed"
|
|
||||||
#use_small_heuristics = false
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
use std::{
|
|
||||||
borrow::Cow,
|
|
||||||
error::Error as StdError,
|
|
||||||
fmt, mem,
|
|
||||||
pin::Pin,
|
|
||||||
task::{Context, Poll},
|
|
||||||
};
|
|
||||||
|
|
||||||
use bytes::{Bytes, BytesMut};
|
|
||||||
use futures_core::Stream;
|
|
||||||
|
|
||||||
use crate::error::Error;
|
|
||||||
|
|
||||||
use super::{BodySize, BodyStream, MessageBody, MessageBodyMapErr, SizedStream};
|
|
||||||
|
|
||||||
pub type Body = AnyBody;
|
|
||||||
|
|
||||||
/// Represents various types of HTTP message body.
|
|
||||||
pub enum AnyBody {
|
|
||||||
/// Empty response. `Content-Length` header is not set.
|
|
||||||
None,
|
|
||||||
|
|
||||||
/// Zero sized response body. `Content-Length` header is set to `0`.
|
|
||||||
Empty,
|
|
||||||
|
|
||||||
/// Specific response body.
|
|
||||||
Bytes(Bytes),
|
|
||||||
|
|
||||||
/// Generic message body.
|
|
||||||
Message(BoxAnyBody),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AnyBody {
|
|
||||||
/// Create body from slice (copy)
|
|
||||||
pub fn from_slice(s: &[u8]) -> Self {
|
|
||||||
Self::Bytes(Bytes::copy_from_slice(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create body from generic message body.
|
|
||||||
pub fn from_message<B>(body: B) -> Self
|
|
||||||
where
|
|
||||||
B: MessageBody + 'static,
|
|
||||||
B::Error: Into<Box<dyn StdError + 'static>>,
|
|
||||||
{
|
|
||||||
Self::Message(BoxAnyBody::from_body(body))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for AnyBody {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
match self {
|
|
||||||
AnyBody::None => BodySize::None,
|
|
||||||
AnyBody::Empty => BodySize::Empty,
|
|
||||||
AnyBody::Bytes(ref bin) => BodySize::Sized(bin.len() as u64),
|
|
||||||
AnyBody::Message(ref body) => body.size(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
|
||||||
match self.get_mut() {
|
|
||||||
AnyBody::None => Poll::Ready(None),
|
|
||||||
AnyBody::Empty => Poll::Ready(None),
|
|
||||||
AnyBody::Bytes(ref mut bin) => {
|
|
||||||
let len = bin.len();
|
|
||||||
if len == 0 {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
Poll::Ready(Some(Ok(mem::take(bin))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AnyBody::Message(body) => body
|
|
||||||
.as_pin_mut()
|
|
||||||
.poll_next(cx)
|
|
||||||
.map_err(|err| Error::new_body().with_cause(err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for AnyBody {
|
|
||||||
fn eq(&self, other: &Body) -> bool {
|
|
||||||
match *self {
|
|
||||||
AnyBody::None => matches!(*other, AnyBody::None),
|
|
||||||
AnyBody::Empty => matches!(*other, AnyBody::Empty),
|
|
||||||
AnyBody::Bytes(ref b) => match *other {
|
|
||||||
AnyBody::Bytes(ref b2) => b == b2,
|
|
||||||
_ => false,
|
|
||||||
},
|
|
||||||
AnyBody::Message(_) => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for AnyBody {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
AnyBody::None => write!(f, "AnyBody::None"),
|
|
||||||
AnyBody::Empty => write!(f, "AnyBody::Empty"),
|
|
||||||
AnyBody::Bytes(ref b) => write!(f, "AnyBody::Bytes({:?})", b),
|
|
||||||
AnyBody::Message(_) => write!(f, "AnyBody::Message(_)"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&'static str> for AnyBody {
|
|
||||||
fn from(s: &'static str) -> Body {
|
|
||||||
AnyBody::Bytes(Bytes::from_static(s.as_ref()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&'static [u8]> for AnyBody {
|
|
||||||
fn from(s: &'static [u8]) -> Body {
|
|
||||||
AnyBody::Bytes(Bytes::from_static(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Vec<u8>> for AnyBody {
|
|
||||||
fn from(vec: Vec<u8>) -> Body {
|
|
||||||
AnyBody::Bytes(Bytes::from(vec))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for AnyBody {
|
|
||||||
fn from(s: String) -> Body {
|
|
||||||
s.into_bytes().into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&'_ String> for AnyBody {
|
|
||||||
fn from(s: &String) -> Body {
|
|
||||||
AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Cow<'_, str>> for AnyBody {
|
|
||||||
fn from(s: Cow<'_, str>) -> Body {
|
|
||||||
match s {
|
|
||||||
Cow::Owned(s) => AnyBody::from(s),
|
|
||||||
Cow::Borrowed(s) => {
|
|
||||||
AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Bytes> for AnyBody {
|
|
||||||
fn from(s: Bytes) -> Body {
|
|
||||||
AnyBody::Bytes(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<BytesMut> for AnyBody {
|
|
||||||
fn from(s: BytesMut) -> Body {
|
|
||||||
AnyBody::Bytes(s.freeze())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, E> From<SizedStream<S>> for AnyBody
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
|
||||||
E: Into<Box<dyn StdError>> + 'static,
|
|
||||||
{
|
|
||||||
fn from(s: SizedStream<S>) -> Body {
|
|
||||||
AnyBody::from_message(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, E> From<BodyStream<S>> for AnyBody
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
|
||||||
E: Into<Box<dyn StdError>> + 'static,
|
|
||||||
{
|
|
||||||
fn from(s: BodyStream<S>) -> Body {
|
|
||||||
AnyBody::from_message(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A boxed message body with boxed errors.
|
|
||||||
pub struct BoxAnyBody(Pin<Box<dyn MessageBody<Error = Box<dyn StdError + 'static>>>>);
|
|
||||||
|
|
||||||
impl BoxAnyBody {
|
|
||||||
/// Boxes a `MessageBody` and any errors it generates.
|
|
||||||
pub fn from_body<B>(body: B) -> Self
|
|
||||||
where
|
|
||||||
B: MessageBody + 'static,
|
|
||||||
B::Error: Into<Box<dyn StdError + 'static>>,
|
|
||||||
{
|
|
||||||
let body = MessageBodyMapErr::new(body, Into::into);
|
|
||||||
Self(Box::pin(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a mutable pinned reference to the inner message body type.
|
|
||||||
pub fn as_pin_mut(
|
|
||||||
&mut self,
|
|
||||||
) -> Pin<&mut (dyn MessageBody<Error = Box<dyn StdError + 'static>>)> {
|
|
||||||
self.0.as_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for BoxAnyBody {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.write_str("BoxAnyBody(dyn MessageBody)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for BoxAnyBody {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
self.0.size()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
mut self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
|
||||||
self.0
|
|
||||||
.as_mut()
|
|
||||||
.poll_next(cx)
|
|
||||||
.map_err(|err| Error::new_body().with_cause(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,11 +20,14 @@ pin_project! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: from_infallible method
|
||||||
|
|
||||||
impl<S, E> BodyStream<S>
|
impl<S, E> BodyStream<S>
|
||||||
where
|
where
|
||||||
S: Stream<Item = Result<Bytes, E>>,
|
S: Stream<Item = Result<Bytes, E>>,
|
||||||
E: Into<Box<dyn StdError>> + 'static,
|
E: Into<Box<dyn StdError>> + 'static,
|
||||||
{
|
{
|
||||||
|
#[inline]
|
||||||
pub fn new(stream: S) -> Self {
|
pub fn new(stream: S) -> Self {
|
||||||
BodyStream { stream }
|
BodyStream { stream }
|
||||||
}
|
}
|
||||||
@@ -37,6 +40,7 @@ where
|
|||||||
{
|
{
|
||||||
type Error = E;
|
type Error = E;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
BodySize::Stream
|
BodySize::Stream
|
||||||
}
|
}
|
||||||
@@ -75,10 +79,23 @@ mod tests {
|
|||||||
use derive_more::{Display, Error};
|
use derive_more::{Display, Error};
|
||||||
use futures_core::ready;
|
use futures_core::ready;
|
||||||
use futures_util::{stream, FutureExt as _};
|
use futures_util::{stream, FutureExt as _};
|
||||||
|
use pin_project_lite::pin_project;
|
||||||
|
use static_assertions::{assert_impl_all, assert_not_impl_all};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::body::to_bytes;
|
use crate::body::to_bytes;
|
||||||
|
|
||||||
|
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, crate::Error>>>: MessageBody);
|
||||||
|
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, &'static str>>>: MessageBody);
|
||||||
|
assert_impl_all!(BodyStream<stream::Repeat<Result<Bytes, &'static str>>>: MessageBody);
|
||||||
|
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, Infallible>>>: MessageBody);
|
||||||
|
assert_impl_all!(BodyStream<stream::Repeat<Result<Bytes, Infallible>>>: MessageBody);
|
||||||
|
|
||||||
|
assert_not_impl_all!(BodyStream<stream::Empty<Bytes>>: MessageBody);
|
||||||
|
assert_not_impl_all!(BodyStream<stream::Repeat<Bytes>>: MessageBody);
|
||||||
|
// crate::Error is not Clone
|
||||||
|
assert_not_impl_all!(BodyStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn skips_empty_chunks() {
|
async fn skips_empty_chunks() {
|
||||||
let body = BodyStream::new(stream::iter(
|
let body = BodyStream::new(stream::iter(
|
||||||
@@ -124,19 +141,44 @@ mod tests {
|
|||||||
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
|
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn stream_string_error() {
|
||||||
|
// `&'static str` does not impl `Error`
|
||||||
|
// but it does impl `Into<Box<dyn Error>>`
|
||||||
|
|
||||||
|
let body = BodyStream::new(stream::once(async { Err("stringy error") }));
|
||||||
|
assert!(matches!(to_bytes(body).await, Err("stringy error")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn stream_boxed_error() {
|
||||||
|
// `Box<dyn Error>` does not impl `Error`
|
||||||
|
// but it does impl `Into<Box<dyn Error>>`
|
||||||
|
|
||||||
|
let body = BodyStream::new(stream::once(async {
|
||||||
|
Err(Box::<dyn StdError>::from("stringy error"))
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
to_bytes(body).await.unwrap_err().to_string(),
|
||||||
|
"stringy error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn stream_delayed_error() {
|
async fn stream_delayed_error() {
|
||||||
let body =
|
let body = BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)]));
|
||||||
BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)]));
|
|
||||||
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
|
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
|
||||||
|
|
||||||
#[pin_project::pin_project(project = TimeDelayStreamProj)]
|
pin_project! {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[project = TimeDelayStreamProj]
|
||||||
enum TimeDelayStream {
|
enum TimeDelayStream {
|
||||||
Start,
|
Start,
|
||||||
Sleep(Pin<Box<Sleep>>),
|
Sleep { delay: Pin<Box<Sleep>> },
|
||||||
Done,
|
Done,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Stream for TimeDelayStream {
|
impl Stream for TimeDelayStream {
|
||||||
type Item = Result<Bytes, StreamErr>;
|
type Item = Result<Bytes, StreamErr>;
|
||||||
@@ -148,12 +190,14 @@ mod tests {
|
|||||||
match self.as_mut().get_mut() {
|
match self.as_mut().get_mut() {
|
||||||
TimeDelayStream::Start => {
|
TimeDelayStream::Start => {
|
||||||
let sleep = sleep(Duration::from_millis(1));
|
let sleep = sleep(Duration::from_millis(1));
|
||||||
self.as_mut().set(TimeDelayStream::Sleep(Box::pin(sleep)));
|
self.as_mut().set(TimeDelayStream::Sleep {
|
||||||
|
delay: Box::pin(sleep),
|
||||||
|
});
|
||||||
cx.waker().wake_by_ref();
|
cx.waker().wake_by_ref();
|
||||||
Poll::Pending
|
Poll::Pending
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeDelayStream::Sleep(ref mut delay) => {
|
TimeDelayStream::Sleep { ref mut delay } => {
|
||||||
ready!(delay.poll_unpin(cx));
|
ready!(delay.poll_unpin(cx));
|
||||||
self.set(TimeDelayStream::Done);
|
self.set(TimeDelayStream::Done);
|
||||||
cx.waker().wake_by_ref();
|
cx.waker().wake_by_ref();
|
||||||
|
|||||||
127
actix-http/src/body/boxed.rs
Normal file
127
actix-http/src/body/boxed.rs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
use std::{
|
||||||
|
error::Error as StdError,
|
||||||
|
fmt,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
|
||||||
|
use super::{BodySize, MessageBody, MessageBodyMapErr};
|
||||||
|
use crate::body;
|
||||||
|
|
||||||
|
/// A boxed message body with boxed errors.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BoxBody(BoxBodyInner);
|
||||||
|
|
||||||
|
enum BoxBodyInner {
|
||||||
|
None(body::None),
|
||||||
|
Bytes(Bytes),
|
||||||
|
Stream(Pin<Box<dyn MessageBody<Error = Box<dyn StdError>>>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for BoxBodyInner {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::None(arg0) => f.debug_tuple("None").field(arg0).finish(),
|
||||||
|
Self::Bytes(arg0) => f.debug_tuple("Bytes").field(arg0).finish(),
|
||||||
|
Self::Stream(_) => f.debug_tuple("Stream").field(&"dyn MessageBody").finish(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoxBody {
|
||||||
|
/// Boxes body type, erasing type information.
|
||||||
|
///
|
||||||
|
/// If the body type to wrap is unknown or generic it is better to use [`MessageBody::boxed`] to
|
||||||
|
/// avoid double boxing.
|
||||||
|
#[inline]
|
||||||
|
pub fn new<B>(body: B) -> Self
|
||||||
|
where
|
||||||
|
B: MessageBody + 'static,
|
||||||
|
{
|
||||||
|
match body.size() {
|
||||||
|
BodySize::None => Self(BoxBodyInner::None(body::None)),
|
||||||
|
_ => match body.try_into_bytes() {
|
||||||
|
Ok(bytes) => Self(BoxBodyInner::Bytes(bytes)),
|
||||||
|
Err(body) => {
|
||||||
|
let body = MessageBodyMapErr::new(body, Into::into);
|
||||||
|
Self(BoxBodyInner::Stream(Box::pin(body)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable pinned reference to the inner message body type.
|
||||||
|
#[inline]
|
||||||
|
pub fn as_pin_mut(&mut self) -> Pin<&mut Self> {
|
||||||
|
Pin::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for BoxBody {
|
||||||
|
type Error = Box<dyn StdError>;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
match &self.0 {
|
||||||
|
BoxBodyInner::None(none) => none.size(),
|
||||||
|
BoxBodyInner::Bytes(bytes) => bytes.size(),
|
||||||
|
BoxBodyInner::Stream(stream) => stream.size(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn poll_next(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
match &mut self.0 {
|
||||||
|
BoxBodyInner::None(body) => {
|
||||||
|
Pin::new(body).poll_next(cx).map_err(|err| match err {})
|
||||||
|
}
|
||||||
|
BoxBodyInner::Bytes(body) => {
|
||||||
|
Pin::new(body).poll_next(cx).map_err(|err| match err {})
|
||||||
|
}
|
||||||
|
BoxBodyInner::Stream(body) => Pin::new(body).poll_next(cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||||
|
match self.0 {
|
||||||
|
BoxBodyInner::None(body) => Ok(body.try_into_bytes().unwrap()),
|
||||||
|
BoxBodyInner::Bytes(body) => Ok(body.try_into_bytes().unwrap()),
|
||||||
|
_ => Err(self),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn boxed(self) -> BoxBody {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use static_assertions::{assert_impl_all, assert_not_impl_all};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::body::to_bytes;
|
||||||
|
|
||||||
|
assert_impl_all!(BoxBody: MessageBody, fmt::Debug, Unpin);
|
||||||
|
|
||||||
|
assert_not_impl_all!(BoxBody: Send, Sync, Unpin);
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn nested_boxed_body() {
|
||||||
|
let body = Bytes::from_static(&[1, 2, 3]);
|
||||||
|
let boxed_body = BoxBody::new(BoxBody::new(body));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
to_bytes(boxed_body).await.unwrap(),
|
||||||
|
Bytes::from(vec![1, 2, 3]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
108
actix-http/src/body/either.rs
Normal file
108
actix-http/src/body/either.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use std::{
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
|
use super::{BodySize, BoxBody, MessageBody};
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
pin_project! {
|
||||||
|
#[project = EitherBodyProj]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum EitherBody<L, R = BoxBody> {
|
||||||
|
/// A body of type `L`.
|
||||||
|
Left { #[pin] body: L },
|
||||||
|
|
||||||
|
/// A body of type `R`.
|
||||||
|
Right { #[pin] body: R },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L> EitherBody<L, BoxBody> {
|
||||||
|
/// Creates new `EitherBody` using left variant and boxed right variant.
|
||||||
|
#[inline]
|
||||||
|
pub fn new(body: L) -> Self {
|
||||||
|
Self::Left { body }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L, R> EitherBody<L, R> {
|
||||||
|
/// Creates new `EitherBody` using left variant.
|
||||||
|
#[inline]
|
||||||
|
pub fn left(body: L) -> Self {
|
||||||
|
Self::Left { body }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates new `EitherBody` using right variant.
|
||||||
|
#[inline]
|
||||||
|
pub fn right(body: R) -> Self {
|
||||||
|
Self::Right { body }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L, R> MessageBody for EitherBody<L, R>
|
||||||
|
where
|
||||||
|
L: MessageBody + 'static,
|
||||||
|
R: MessageBody + 'static,
|
||||||
|
{
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
match self {
|
||||||
|
EitherBody::Left { body } => body.size(),
|
||||||
|
EitherBody::Right { body } => body.size(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
match self.project() {
|
||||||
|
EitherBodyProj::Left { body } => body
|
||||||
|
.poll_next(cx)
|
||||||
|
.map_err(|err| Error::new_body().with_cause(err)),
|
||||||
|
EitherBodyProj::Right { body } => body
|
||||||
|
.poll_next(cx)
|
||||||
|
.map_err(|err| Error::new_body().with_cause(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||||
|
match self {
|
||||||
|
EitherBody::Left { body } => body
|
||||||
|
.try_into_bytes()
|
||||||
|
.map_err(|body| EitherBody::Left { body }),
|
||||||
|
EitherBody::Right { body } => body
|
||||||
|
.try_into_bytes()
|
||||||
|
.map_err(|body| EitherBody::Right { body }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn boxed(self) -> BoxBody {
|
||||||
|
match self {
|
||||||
|
EitherBody::Left { body } => body.boxed(),
|
||||||
|
EitherBody::Right { body } => body.boxed(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn type_parameter_inference() {
|
||||||
|
let _body: EitherBody<(), _> = EitherBody::new(());
|
||||||
|
|
||||||
|
let _body: EitherBody<_, ()> = EitherBody::left(());
|
||||||
|
let _body: EitherBody<(), _> = EitherBody::right(());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
convert::Infallible,
|
convert::Infallible,
|
||||||
|
error::Error as StdError,
|
||||||
mem,
|
mem,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
@@ -11,47 +12,165 @@ use bytes::{Bytes, BytesMut};
|
|||||||
use futures_core::ready;
|
use futures_core::ready;
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
use super::BodySize;
|
use super::{BodySize, BoxBody};
|
||||||
|
|
||||||
/// An interface for response bodies.
|
/// An interface for types that can be used as a response body.
|
||||||
|
///
|
||||||
|
/// It is not usually necessary to create custom body types, this trait is already [implemented for
|
||||||
|
/// a large number of sensible body types](#foreign-impls) including:
|
||||||
|
/// - Empty body: `()`
|
||||||
|
/// - Text-based: `String`, `&'static str`, `ByteString`.
|
||||||
|
/// - Byte-based: `Bytes`, `BytesMut`, `Vec<u8>`, `&'static [u8]`;
|
||||||
|
/// - Streams: [`BodyStream`](super::BodyStream), [`SizedStream`](super::SizedStream)
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// # use std::convert::Infallible;
|
||||||
|
/// # use std::task::{Poll, Context};
|
||||||
|
/// # use std::pin::Pin;
|
||||||
|
/// # use bytes::Bytes;
|
||||||
|
/// # use actix_http::body::{BodySize, MessageBody};
|
||||||
|
/// struct Repeat {
|
||||||
|
/// chunk: String,
|
||||||
|
/// n_times: usize,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl MessageBody for Repeat {
|
||||||
|
/// type Error = Infallible;
|
||||||
|
///
|
||||||
|
/// fn size(&self) -> BodySize {
|
||||||
|
/// BodySize::Sized((self.chunk.len() * self.n_times) as u64)
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn poll_next(
|
||||||
|
/// self: Pin<&mut Self>,
|
||||||
|
/// _cx: &mut Context<'_>,
|
||||||
|
/// ) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
/// let payload_string = self.chunk.repeat(self.n_times);
|
||||||
|
/// let payload_bytes = Bytes::from(payload_string);
|
||||||
|
/// Poll::Ready(Some(Ok(payload_bytes)))
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
pub trait MessageBody {
|
pub trait MessageBody {
|
||||||
type Error;
|
/// The type of error that will be returned if streaming body fails.
|
||||||
|
///
|
||||||
|
/// Since it is not appropriate to generate a response mid-stream, it only requires `Error` for
|
||||||
|
/// internal use and logging.
|
||||||
|
type Error: Into<Box<dyn StdError>>;
|
||||||
|
|
||||||
/// Body size hint.
|
/// Body size hint.
|
||||||
|
///
|
||||||
|
/// If [`BodySize::None`] is returned, optimizations that skip reading the body are allowed.
|
||||||
fn size(&self) -> BodySize;
|
fn size(&self) -> BodySize;
|
||||||
|
|
||||||
/// Attempt to pull out the next chunk of body bytes.
|
/// Attempt to pull out the next chunk of body bytes.
|
||||||
|
///
|
||||||
|
/// # Return Value
|
||||||
|
/// Similar to the `Stream` interface, there are several possible return values, each indicating
|
||||||
|
/// a distinct state:
|
||||||
|
/// - `Poll::Pending` means that this body's next chunk is not ready yet. Implementations must
|
||||||
|
/// ensure that the current task will be notified when the next chunk may be ready.
|
||||||
|
/// - `Poll::Ready(Some(val))` means that the body has successfully produced a chunk, `val`,
|
||||||
|
/// and may produce further values on subsequent `poll_next` calls.
|
||||||
|
/// - `Poll::Ready(None)` means that the body is complete, and `poll_next` should not be
|
||||||
|
/// invoked again.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Once a body is complete (i.e., `poll_next` returned `Ready(None)`), calling its `poll_next`
|
||||||
|
/// method again may panic, block forever, or cause other kinds of problems; this trait places
|
||||||
|
/// no requirements on the effects of such a call. However, as the `poll_next` method is not
|
||||||
|
/// marked unsafe, Rust’s usual rules apply: calls must never cause UB, regardless of its state.
|
||||||
fn poll_next(
|
fn poll_next(
|
||||||
self: Pin<&mut Self>,
|
self: Pin<&mut Self>,
|
||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
) -> Poll<Option<Result<Bytes, Self::Error>>>;
|
) -> Poll<Option<Result<Bytes, Self::Error>>>;
|
||||||
|
|
||||||
|
/// Try to convert into the complete chunk of body bytes.
|
||||||
|
///
|
||||||
|
/// Override this method if the complete body can be trivially extracted. This is useful for
|
||||||
|
/// optimizations where `poll_next` calls can be avoided.
|
||||||
|
///
|
||||||
|
/// Body types with [`BodySize::None`] are allowed to return empty `Bytes`. Although, if calling
|
||||||
|
/// this method, it is recommended to check `size` first and return early.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The default implementation will error and return the original type back to the caller for
|
||||||
|
/// further use.
|
||||||
|
#[inline]
|
||||||
|
fn try_into_bytes(self) -> Result<Bytes, Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Err(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps this body into a `BoxBody`.
|
||||||
|
///
|
||||||
|
/// No-op when called on a `BoxBody`, meaning there is no risk of double boxing when calling
|
||||||
|
/// this on a generic `MessageBody`. Prefer this over [`BoxBody::new`] when a boxed body
|
||||||
|
/// is required.
|
||||||
|
#[inline]
|
||||||
|
fn boxed(self) -> BoxBody
|
||||||
|
where
|
||||||
|
Self: Sized + 'static,
|
||||||
|
{
|
||||||
|
BoxBody::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod foreign_impls {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl MessageBody for Infallible {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
match *self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
match *self {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageBody for () {
|
impl MessageBody for () {
|
||||||
type Error = Infallible;
|
type Error = Infallible;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
BodySize::Empty
|
BodySize::Sized(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn poll_next(
|
fn poll_next(
|
||||||
self: Pin<&mut Self>,
|
self: Pin<&mut Self>,
|
||||||
_: &mut Context<'_>,
|
_cx: &mut Context<'_>,
|
||||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
Poll::Ready(None)
|
Poll::Ready(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||||
|
Ok(Bytes::new())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B> MessageBody for Box<B>
|
impl<B> MessageBody for Box<B>
|
||||||
where
|
where
|
||||||
B: MessageBody + Unpin,
|
B: MessageBody + Unpin + ?Sized,
|
||||||
{
|
{
|
||||||
type Error = B::Error;
|
type Error = B::Error;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
self.as_ref().size()
|
self.as_ref().size()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn poll_next(
|
fn poll_next(
|
||||||
self: Pin<&mut Self>,
|
self: Pin<&mut Self>,
|
||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
@@ -62,32 +181,62 @@ where
|
|||||||
|
|
||||||
impl<B> MessageBody for Pin<Box<B>>
|
impl<B> MessageBody for Pin<Box<B>>
|
||||||
where
|
where
|
||||||
B: MessageBody,
|
B: MessageBody + ?Sized,
|
||||||
{
|
{
|
||||||
type Error = B::Error;
|
type Error = B::Error;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
self.as_ref().size()
|
self.as_ref().size()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn poll_next(
|
fn poll_next(
|
||||||
mut self: Pin<&mut Self>,
|
self: Pin<&mut Self>,
|
||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
self.as_mut().poll_next(cx)
|
self.get_mut().as_mut().poll_next(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for &'static [u8] {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
if self.is_empty() {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
Poll::Ready(Some(Ok(Bytes::from_static(mem::take(self.get_mut())))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||||
|
Ok(Bytes::from_static(self))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageBody for Bytes {
|
impl MessageBody for Bytes {
|
||||||
type Error = Infallible;
|
type Error = Infallible;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
BodySize::Sized(self.len() as u64)
|
BodySize::Sized(self.len() as u64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn poll_next(
|
fn poll_next(
|
||||||
self: Pin<&mut Self>,
|
self: Pin<&mut Self>,
|
||||||
_: &mut Context<'_>,
|
_cx: &mut Context<'_>,
|
||||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
if self.is_empty() {
|
if self.is_empty() {
|
||||||
Poll::Ready(None)
|
Poll::Ready(None)
|
||||||
@@ -95,18 +244,25 @@ impl MessageBody for Bytes {
|
|||||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
|
Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageBody for BytesMut {
|
impl MessageBody for BytesMut {
|
||||||
type Error = Infallible;
|
type Error = Infallible;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
BodySize::Sized(self.len() as u64)
|
BodySize::Sized(self.len() as u64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn poll_next(
|
fn poll_next(
|
||||||
self: Pin<&mut Self>,
|
self: Pin<&mut Self>,
|
||||||
_: &mut Context<'_>,
|
_cx: &mut Context<'_>,
|
||||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
if self.is_empty() {
|
if self.is_empty() {
|
||||||
Poll::Ready(None)
|
Poll::Ready(None)
|
||||||
@@ -114,65 +270,114 @@ impl MessageBody for BytesMut {
|
|||||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
|
Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for &'static str {
|
#[inline]
|
||||||
type Error = Infallible;
|
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||||
|
Ok(self.freeze())
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Sized(self.len() as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
|
||||||
if self.is_empty() {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
Poll::Ready(Some(Ok(Bytes::from_static(
|
|
||||||
mem::take(self.get_mut()).as_ref(),
|
|
||||||
))))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageBody for Vec<u8> {
|
impl MessageBody for Vec<u8> {
|
||||||
type Error = Infallible;
|
type Error = Infallible;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
BodySize::Sized(self.len() as u64)
|
BodySize::Sized(self.len() as u64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn poll_next(
|
fn poll_next(
|
||||||
self: Pin<&mut Self>,
|
self: Pin<&mut Self>,
|
||||||
_: &mut Context<'_>,
|
_cx: &mut Context<'_>,
|
||||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
if self.is_empty() {
|
if self.is_empty() {
|
||||||
Poll::Ready(None)
|
Poll::Ready(None)
|
||||||
} else {
|
} else {
|
||||||
Poll::Ready(Some(Ok(Bytes::from(mem::take(self.get_mut())))))
|
Poll::Ready(Some(Ok(mem::take(self.get_mut()).into())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||||
|
Ok(Bytes::from(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for &'static str {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
if self.is_empty() {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
let string = mem::take(self.get_mut());
|
||||||
|
let bytes = Bytes::from_static(string.as_bytes());
|
||||||
|
Poll::Ready(Some(Ok(bytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||||
|
Ok(Bytes::from_static(self.as_bytes()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageBody for String {
|
impl MessageBody for String {
|
||||||
type Error = Infallible;
|
type Error = Infallible;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
BodySize::Sized(self.len() as u64)
|
BodySize::Sized(self.len() as u64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn poll_next(
|
fn poll_next(
|
||||||
self: Pin<&mut Self>,
|
self: Pin<&mut Self>,
|
||||||
_: &mut Context<'_>,
|
_cx: &mut Context<'_>,
|
||||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
if self.is_empty() {
|
if self.is_empty() {
|
||||||
Poll::Ready(None)
|
Poll::Ready(None)
|
||||||
} else {
|
} else {
|
||||||
Poll::Ready(Some(Ok(Bytes::from(
|
let string = mem::take(self.get_mut());
|
||||||
mem::take(self.get_mut()).into_bytes(),
|
Poll::Ready(Some(Ok(Bytes::from(string))))
|
||||||
))))
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||||
|
Ok(Bytes::from(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for bytestring::ByteString {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
let string = mem::take(self.get_mut());
|
||||||
|
Poll::Ready(Some(Ok(string.into_bytes())))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||||
|
Ok(self.into_bytes())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,9 +407,11 @@ impl<B, F, E> MessageBody for MessageBodyMapErr<B, F>
|
|||||||
where
|
where
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
F: FnOnce(B::Error) -> E,
|
F: FnOnce(B::Error) -> E,
|
||||||
|
E: Into<Box<dyn StdError>>,
|
||||||
{
|
{
|
||||||
type Error = E;
|
type Error = E;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
self.body.size()
|
self.body.size()
|
||||||
}
|
}
|
||||||
@@ -225,4 +432,178 @@ where
|
|||||||
None => Poll::Ready(None),
|
None => Poll::Ready(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||||
|
let Self { body, mapper } = self;
|
||||||
|
body.try_into_bytes().map_err(|body| Self { body, mapper })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use actix_rt::pin;
|
||||||
|
use actix_utils::future::poll_fn;
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::body::{self, EitherBody};
|
||||||
|
|
||||||
|
macro_rules! assert_poll_next {
|
||||||
|
($pin:expr, $exp:expr) => {
|
||||||
|
assert_eq!(
|
||||||
|
poll_fn(|cx| $pin.as_mut().poll_next(cx))
|
||||||
|
.await
|
||||||
|
.unwrap() // unwrap option
|
||||||
|
.unwrap(), // unwrap result
|
||||||
|
$exp
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! assert_poll_next_none {
|
||||||
|
($pin:expr) => {
|
||||||
|
assert!(poll_fn(|cx| $pin.as_mut().poll_next(cx)).await.is_none());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn boxing_equivalence() {
|
||||||
|
assert_eq!(().size(), BodySize::Sized(0));
|
||||||
|
assert_eq!(().size(), Box::new(()).size());
|
||||||
|
assert_eq!(().size(), Box::pin(()).size());
|
||||||
|
|
||||||
|
let pl = Box::new(());
|
||||||
|
pin!(pl);
|
||||||
|
assert_poll_next_none!(pl);
|
||||||
|
|
||||||
|
let mut pl = Box::pin(());
|
||||||
|
assert_poll_next_none!(pl);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_unit() {
|
||||||
|
let pl = ();
|
||||||
|
assert_eq!(pl.size(), BodySize::Sized(0));
|
||||||
|
pin!(pl);
|
||||||
|
assert_poll_next_none!(pl);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_static_str() {
|
||||||
|
assert_eq!("".size(), BodySize::Sized(0));
|
||||||
|
assert_eq!("test".size(), BodySize::Sized(4));
|
||||||
|
|
||||||
|
let pl = "test";
|
||||||
|
pin!(pl);
|
||||||
|
assert_poll_next!(pl, Bytes::from("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_static_bytes() {
|
||||||
|
assert_eq!(b"".as_ref().size(), BodySize::Sized(0));
|
||||||
|
assert_eq!(b"test".as_ref().size(), BodySize::Sized(4));
|
||||||
|
|
||||||
|
let pl = b"test".as_ref();
|
||||||
|
pin!(pl);
|
||||||
|
assert_poll_next!(pl, Bytes::from("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_vec() {
|
||||||
|
assert_eq!(vec![0; 0].size(), BodySize::Sized(0));
|
||||||
|
assert_eq!(Vec::from("test").size(), BodySize::Sized(4));
|
||||||
|
|
||||||
|
let pl = Vec::from("test");
|
||||||
|
pin!(pl);
|
||||||
|
assert_poll_next!(pl, Bytes::from("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_bytes() {
|
||||||
|
assert_eq!(Bytes::new().size(), BodySize::Sized(0));
|
||||||
|
assert_eq!(Bytes::from_static(b"test").size(), BodySize::Sized(4));
|
||||||
|
|
||||||
|
let pl = Bytes::from_static(b"test");
|
||||||
|
pin!(pl);
|
||||||
|
assert_poll_next!(pl, Bytes::from("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_bytes_mut() {
|
||||||
|
assert_eq!(BytesMut::new().size(), BodySize::Sized(0));
|
||||||
|
assert_eq!(BytesMut::from(b"test".as_ref()).size(), BodySize::Sized(4));
|
||||||
|
|
||||||
|
let pl = BytesMut::from("test");
|
||||||
|
pin!(pl);
|
||||||
|
assert_poll_next!(pl, Bytes::from("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_string() {
|
||||||
|
assert_eq!(String::new().size(), BodySize::Sized(0));
|
||||||
|
assert_eq!("test".to_owned().size(), BodySize::Sized(4));
|
||||||
|
|
||||||
|
let pl = "test".to_owned();
|
||||||
|
pin!(pl);
|
||||||
|
assert_poll_next!(pl, Bytes::from("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn complete_body_combinators() {
|
||||||
|
let body = Bytes::from_static(b"test");
|
||||||
|
let body = BoxBody::new(body);
|
||||||
|
let body = EitherBody::<_, ()>::left(body);
|
||||||
|
let body = EitherBody::<(), _>::right(body);
|
||||||
|
// Do not support try_into_bytes:
|
||||||
|
// let body = Box::new(body);
|
||||||
|
// let body = Box::pin(body);
|
||||||
|
|
||||||
|
assert_eq!(body.try_into_bytes().unwrap(), Bytes::from("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn complete_body_combinators_poll() {
|
||||||
|
let body = Bytes::from_static(b"test");
|
||||||
|
let body = BoxBody::new(body);
|
||||||
|
let body = EitherBody::<_, ()>::left(body);
|
||||||
|
let body = EitherBody::<(), _>::right(body);
|
||||||
|
let mut body = body;
|
||||||
|
|
||||||
|
assert_eq!(body.size(), BodySize::Sized(4));
|
||||||
|
assert_poll_next!(Pin::new(&mut body), Bytes::from("test"));
|
||||||
|
assert_poll_next_none!(Pin::new(&mut body));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn none_body_combinators() {
|
||||||
|
fn none_body() -> BoxBody {
|
||||||
|
let body = body::None;
|
||||||
|
let body = BoxBody::new(body);
|
||||||
|
let body = EitherBody::<_, ()>::left(body);
|
||||||
|
let body = EitherBody::<(), _>::right(body);
|
||||||
|
body.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(none_body().size(), BodySize::None);
|
||||||
|
assert_eq!(none_body().try_into_bytes().unwrap(), Bytes::new());
|
||||||
|
assert_poll_next_none!(Pin::new(&mut none_body()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// down-casting used to be done with a method on MessageBody trait
|
||||||
|
// test is kept to demonstrate equivalence of Any trait
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_body_casting() {
|
||||||
|
let mut body = String::from("hello cast");
|
||||||
|
// let mut resp_body: &mut dyn MessageBody<Error = Error> = &mut body;
|
||||||
|
let resp_body: &mut dyn std::any::Any = &mut body;
|
||||||
|
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||||
|
assert_eq!(body, "hello cast");
|
||||||
|
let body = &mut resp_body.downcast_mut::<String>().unwrap();
|
||||||
|
body.push('!');
|
||||||
|
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||||
|
assert_eq!(body, "hello cast!");
|
||||||
|
let not_body = resp_body.downcast_ref::<()>();
|
||||||
|
assert!(not_body.is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,263 +1,25 @@
|
|||||||
//! Traits and structures to aid consuming and writing HTTP payloads.
|
//! Traits and structures to aid consuming and writing HTTP payloads.
|
||||||
|
//!
|
||||||
|
//! "Body" and "payload" are used somewhat interchangeably in this documentation.
|
||||||
|
|
||||||
use std::task::Poll;
|
// Though the spec kinda reads like "payload" is the possibly-transfer-encoded part of the message
|
||||||
|
// and the "body" is the intended possibly-decoded version of that.
|
||||||
|
|
||||||
use actix_rt::pin;
|
|
||||||
use actix_utils::future::poll_fn;
|
|
||||||
use bytes::{Bytes, BytesMut};
|
|
||||||
use futures_core::ready;
|
|
||||||
|
|
||||||
#[allow(clippy::module_inception)]
|
|
||||||
mod body;
|
|
||||||
mod body_stream;
|
mod body_stream;
|
||||||
|
mod boxed;
|
||||||
|
mod either;
|
||||||
mod message_body;
|
mod message_body;
|
||||||
mod response_body;
|
mod none;
|
||||||
mod size;
|
mod size;
|
||||||
mod sized_stream;
|
mod sized_stream;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
pub use self::body::{AnyBody, Body, BoxAnyBody};
|
|
||||||
pub use self::body_stream::BodyStream;
|
pub use self::body_stream::BodyStream;
|
||||||
|
pub use self::boxed::BoxBody;
|
||||||
|
pub use self::either::EitherBody;
|
||||||
pub use self::message_body::MessageBody;
|
pub use self::message_body::MessageBody;
|
||||||
pub(crate) use self::message_body::MessageBodyMapErr;
|
pub(crate) use self::message_body::MessageBodyMapErr;
|
||||||
pub use self::response_body::ResponseBody;
|
pub use self::none::None;
|
||||||
pub use self::size::BodySize;
|
pub use self::size::BodySize;
|
||||||
pub use self::sized_stream::SizedStream;
|
pub use self::sized_stream::SizedStream;
|
||||||
|
pub use self::utils::to_bytes;
|
||||||
/// Collects the body produced by a `MessageBody` implementation into `Bytes`.
|
|
||||||
///
|
|
||||||
/// Any errors produced by the body stream are returned immediately.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// use actix_http::body::{Body, to_bytes};
|
|
||||||
/// use bytes::Bytes;
|
|
||||||
///
|
|
||||||
/// # async fn test_to_bytes() {
|
|
||||||
/// let body = Body::Empty;
|
|
||||||
/// let bytes = to_bytes(body).await.unwrap();
|
|
||||||
/// assert!(bytes.is_empty());
|
|
||||||
///
|
|
||||||
/// let body = Body::Bytes(Bytes::from_static(b"123"));
|
|
||||||
/// let bytes = to_bytes(body).await.unwrap();
|
|
||||||
/// assert_eq!(bytes, b"123"[..]);
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
|
|
||||||
let cap = match body.size() {
|
|
||||||
BodySize::None | BodySize::Empty | BodySize::Sized(0) => return Ok(Bytes::new()),
|
|
||||||
BodySize::Sized(size) => size as usize,
|
|
||||||
BodySize::Stream => 32_768,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut buf = BytesMut::with_capacity(cap);
|
|
||||||
|
|
||||||
pin!(body);
|
|
||||||
|
|
||||||
poll_fn(|cx| loop {
|
|
||||||
let body = body.as_mut();
|
|
||||||
|
|
||||||
match ready!(body.poll_next(cx)) {
|
|
||||||
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
|
|
||||||
None => return Poll::Ready(Ok(())),
|
|
||||||
Some(Err(err)) => return Poll::Ready(Err(err)),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(buf.freeze())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::pin::Pin;
|
|
||||||
|
|
||||||
use actix_rt::pin;
|
|
||||||
use actix_utils::future::poll_fn;
|
|
||||||
use bytes::{Bytes, BytesMut};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
impl Body {
|
|
||||||
pub(crate) fn get_ref(&self) -> &[u8] {
|
|
||||||
match *self {
|
|
||||||
Body::Bytes(ref bin) => bin,
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_static_str() {
|
|
||||||
assert_eq!(Body::from("").size(), BodySize::Sized(0));
|
|
||||||
assert_eq!(Body::from("test").size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from("test").get_ref(), b"test");
|
|
||||||
|
|
||||||
assert_eq!("test".size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| Pin::new(&mut "test").poll_next(cx))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_static_bytes() {
|
|
||||||
assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test");
|
|
||||||
assert_eq!(
|
|
||||||
Body::from_slice(b"test".as_ref()).size(),
|
|
||||||
BodySize::Sized(4)
|
|
||||||
);
|
|
||||||
assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test");
|
|
||||||
let sb = Bytes::from(&b"test"[..]);
|
|
||||||
pin!(sb);
|
|
||||||
|
|
||||||
assert_eq!(sb.size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| sb.as_mut().poll_next(cx)).await.unwrap().ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_vec() {
|
|
||||||
assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test");
|
|
||||||
let test_vec = Vec::from("test");
|
|
||||||
pin!(test_vec);
|
|
||||||
|
|
||||||
assert_eq!(test_vec.size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| test_vec.as_mut().poll_next(cx))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_bytes() {
|
|
||||||
let b = Bytes::from("test");
|
|
||||||
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
|
||||||
pin!(b);
|
|
||||||
|
|
||||||
assert_eq!(b.size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_bytes_mut() {
|
|
||||||
let b = BytesMut::from("test");
|
|
||||||
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
|
||||||
pin!(b);
|
|
||||||
|
|
||||||
assert_eq!(b.size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_string() {
|
|
||||||
let b = "test".to_owned();
|
|
||||||
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
|
||||||
assert_eq!(Body::from(&b).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(&b).get_ref(), b"test");
|
|
||||||
pin!(b);
|
|
||||||
|
|
||||||
assert_eq!(b.size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_unit() {
|
|
||||||
assert_eq!(().size(), BodySize::Empty);
|
|
||||||
assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx))
|
|
||||||
.await
|
|
||||||
.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_box_and_pin() {
|
|
||||||
let val = Box::new(());
|
|
||||||
pin!(val);
|
|
||||||
assert_eq!(val.size(), BodySize::Empty);
|
|
||||||
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
|
|
||||||
|
|
||||||
let mut val = Box::pin(());
|
|
||||||
assert_eq!(val.size(), BodySize::Empty);
|
|
||||||
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_body_eq() {
|
|
||||||
assert!(
|
|
||||||
Body::Bytes(Bytes::from_static(b"1"))
|
|
||||||
== Body::Bytes(Bytes::from_static(b"1"))
|
|
||||||
);
|
|
||||||
assert!(Body::Bytes(Bytes::from_static(b"1")) != Body::None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_body_debug() {
|
|
||||||
assert!(format!("{:?}", Body::None).contains("Body::None"));
|
|
||||||
assert!(format!("{:?}", Body::Empty).contains("Body::Empty"));
|
|
||||||
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1'));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_serde_json() {
|
|
||||||
use serde_json::{json, Value};
|
|
||||||
assert_eq!(
|
|
||||||
Body::from(serde_json::to_vec(&Value::String("test".to_owned())).unwrap())
|
|
||||||
.size(),
|
|
||||||
BodySize::Sized(6)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Body::from(serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap())
|
|
||||||
.size(),
|
|
||||||
BodySize::Sized(25)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// down-casting used to be done with a method on MessageBody trait
|
|
||||||
// test is kept to demonstrate equivalence of Any trait
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_body_casting() {
|
|
||||||
let mut body = String::from("hello cast");
|
|
||||||
// let mut resp_body: &mut dyn MessageBody<Error = Error> = &mut body;
|
|
||||||
let resp_body: &mut dyn std::any::Any = &mut body;
|
|
||||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
|
||||||
assert_eq!(body, "hello cast");
|
|
||||||
let body = &mut resp_body.downcast_mut::<String>().unwrap();
|
|
||||||
body.push('!');
|
|
||||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
|
||||||
assert_eq!(body, "hello cast!");
|
|
||||||
let not_body = resp_body.downcast_ref::<()>();
|
|
||||||
assert!(not_body.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_to_bytes() {
|
|
||||||
let body = Body::Empty;
|
|
||||||
let bytes = to_bytes(body).await.unwrap();
|
|
||||||
assert!(bytes.is_empty());
|
|
||||||
|
|
||||||
let body = Body::Bytes(Bytes::from_static(b"123"));
|
|
||||||
let bytes = to_bytes(body).await.unwrap();
|
|
||||||
assert_eq!(bytes, b"123"[..]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
51
actix-http/src/body/none.rs
Normal file
51
actix-http/src/body/none.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
use std::{
|
||||||
|
convert::Infallible,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
|
||||||
|
use super::{BodySize, MessageBody};
|
||||||
|
|
||||||
|
/// Body type for responses that forbid payloads.
|
||||||
|
///
|
||||||
|
/// This is distinct from an "empty" response which _would_ contain a `Content-Length` header.
|
||||||
|
/// For an "empty" body, use `()` or `Bytes::new()`.
|
||||||
|
///
|
||||||
|
/// For example, the HTTP spec forbids a payload to be sent with a `204 No Content` response.
|
||||||
|
/// In this case, the payload (or lack thereof) is implicit from the status code, so a
|
||||||
|
/// `Content-Length` header is not required.
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct None;
|
||||||
|
|
||||||
|
impl None {
|
||||||
|
/// Constructs new "none" body.
|
||||||
|
#[inline]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for None {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
Poll::Ready(Option::None)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||||
|
Ok(Bytes::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
use std::{
|
|
||||||
mem,
|
|
||||||
pin::Pin,
|
|
||||||
task::{Context, Poll},
|
|
||||||
};
|
|
||||||
|
|
||||||
use bytes::Bytes;
|
|
||||||
use futures_core::Stream;
|
|
||||||
use pin_project::pin_project;
|
|
||||||
|
|
||||||
use crate::error::Error;
|
|
||||||
|
|
||||||
use super::{Body, BodySize, MessageBody};
|
|
||||||
|
|
||||||
#[pin_project(project = ResponseBodyProj)]
|
|
||||||
pub enum ResponseBody<B> {
|
|
||||||
Body(#[pin] B),
|
|
||||||
Other(Body),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResponseBody<Body> {
|
|
||||||
pub fn into_body<B>(self) -> ResponseBody<B> {
|
|
||||||
match self {
|
|
||||||
ResponseBody::Body(b) => ResponseBody::Other(b),
|
|
||||||
ResponseBody::Other(b) => ResponseBody::Other(b),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B> ResponseBody<B> {
|
|
||||||
pub fn take_body(&mut self) -> ResponseBody<B> {
|
|
||||||
mem::replace(self, ResponseBody::Other(Body::None))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B: MessageBody> ResponseBody<B> {
|
|
||||||
pub fn as_ref(&self) -> Option<&B> {
|
|
||||||
if let ResponseBody::Body(ref b) = self {
|
|
||||||
Some(b)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B> MessageBody for ResponseBody<B>
|
|
||||||
where
|
|
||||||
B: MessageBody,
|
|
||||||
B::Error: Into<Error>,
|
|
||||||
{
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
match self {
|
|
||||||
ResponseBody::Body(ref body) => body.size(),
|
|
||||||
ResponseBody::Other(ref body) => body.size(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
|
||||||
Stream::poll_next(self, cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B> Stream for ResponseBody<B>
|
|
||||||
where
|
|
||||||
B: MessageBody,
|
|
||||||
B::Error: Into<Error>,
|
|
||||||
{
|
|
||||||
type Item = Result<Bytes, Error>;
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Self::Item>> {
|
|
||||||
match self.project() {
|
|
||||||
ResponseBodyProj::Body(body) => body.poll_next(cx).map_err(Into::into),
|
|
||||||
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,16 @@
|
|||||||
/// Body size hint.
|
/// Body size hint.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum BodySize {
|
pub enum BodySize {
|
||||||
/// Absence of body can be assumed from method or status code.
|
/// Implicitly empty body.
|
||||||
///
|
///
|
||||||
/// Will skip writing Content-Length header.
|
/// Will omit the Content-Length header. Used for responses to certain methods (e.g., `HEAD`) or
|
||||||
|
/// with particular status codes (e.g., 204 No Content). Consumers that read this as a body size
|
||||||
|
/// hint are allowed to make optimizations that skip reading or writing the payload.
|
||||||
None,
|
None,
|
||||||
|
|
||||||
/// Zero size body.
|
|
||||||
///
|
|
||||||
/// Will write `Content-Length: 0` header.
|
|
||||||
Empty,
|
|
||||||
|
|
||||||
/// Known size body.
|
/// Known size body.
|
||||||
///
|
///
|
||||||
/// Will write `Content-Length: N` header. `Sized(0)` is treated the same as `Empty`.
|
/// Will write `Content-Length: N` header.
|
||||||
Sized(u64),
|
Sized(u64),
|
||||||
|
|
||||||
/// Unknown size body.
|
/// Unknown size body.
|
||||||
@@ -23,18 +20,22 @@ pub enum BodySize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BodySize {
|
impl BodySize {
|
||||||
/// Returns true if size hint indicates no or empty body.
|
/// Equivalent to `BodySize::Sized(0)`;
|
||||||
|
pub const ZERO: Self = Self::Sized(0);
|
||||||
|
|
||||||
|
/// Returns true if size hint indicates omitted or empty body.
|
||||||
|
///
|
||||||
|
/// Streams will return false because it cannot be known without reading the stream.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::body::BodySize;
|
/// # use actix_http::body::BodySize;
|
||||||
/// assert!(BodySize::None.is_eof());
|
/// assert!(BodySize::None.is_eof());
|
||||||
/// assert!(BodySize::Empty.is_eof());
|
|
||||||
/// assert!(BodySize::Sized(0).is_eof());
|
/// assert!(BodySize::Sized(0).is_eof());
|
||||||
///
|
///
|
||||||
/// assert!(!BodySize::Sized(64).is_eof());
|
/// assert!(!BodySize::Sized(64).is_eof());
|
||||||
/// assert!(!BodySize::Stream.is_eof());
|
/// assert!(!BodySize::Stream.is_eof());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn is_eof(&self) -> bool {
|
pub fn is_eof(&self) -> bool {
|
||||||
matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0))
|
matches!(self, BodySize::None | BodySize::Sized(0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,14 @@ where
|
|||||||
S: Stream<Item = Result<Bytes, E>>,
|
S: Stream<Item = Result<Bytes, E>>,
|
||||||
E: Into<Box<dyn StdError>> + 'static,
|
E: Into<Box<dyn StdError>> + 'static,
|
||||||
{
|
{
|
||||||
|
#[inline]
|
||||||
pub fn new(size: u64, stream: S) -> Self {
|
pub fn new(size: u64, stream: S) -> Self {
|
||||||
SizedStream { size, stream }
|
SizedStream { size, stream }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: from_infallible method
|
||||||
|
|
||||||
impl<S, E> MessageBody for SizedStream<S>
|
impl<S, E> MessageBody for SizedStream<S>
|
||||||
where
|
where
|
||||||
S: Stream<Item = Result<Bytes, E>>,
|
S: Stream<Item = Result<Bytes, E>>,
|
||||||
@@ -39,6 +42,7 @@ where
|
|||||||
{
|
{
|
||||||
type Error = E;
|
type Error = E;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
BodySize::Sized(self.size as u64)
|
BodySize::Sized(self.size as u64)
|
||||||
}
|
}
|
||||||
@@ -72,10 +76,22 @@ mod tests {
|
|||||||
use actix_rt::pin;
|
use actix_rt::pin;
|
||||||
use actix_utils::future::poll_fn;
|
use actix_utils::future::poll_fn;
|
||||||
use futures_util::stream;
|
use futures_util::stream;
|
||||||
|
use static_assertions::{assert_impl_all, assert_not_impl_all};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::body::to_bytes;
|
use crate::body::to_bytes;
|
||||||
|
|
||||||
|
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, crate::Error>>>: MessageBody);
|
||||||
|
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, &'static str>>>: MessageBody);
|
||||||
|
assert_impl_all!(SizedStream<stream::Repeat<Result<Bytes, &'static str>>>: MessageBody);
|
||||||
|
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, Infallible>>>: MessageBody);
|
||||||
|
assert_impl_all!(SizedStream<stream::Repeat<Result<Bytes, Infallible>>>: MessageBody);
|
||||||
|
|
||||||
|
assert_not_impl_all!(SizedStream<stream::Empty<Bytes>>: MessageBody);
|
||||||
|
assert_not_impl_all!(SizedStream<stream::Repeat<Bytes>>: MessageBody);
|
||||||
|
// crate::Error is not Clone
|
||||||
|
assert_not_impl_all!(SizedStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn skips_empty_chunks() {
|
async fn skips_empty_chunks() {
|
||||||
let body = SizedStream::new(
|
let body = SizedStream::new(
|
||||||
@@ -119,4 +135,37 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
|
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn stream_string_error() {
|
||||||
|
// `&'static str` does not impl `Error`
|
||||||
|
// but it does impl `Into<Box<dyn Error>>`
|
||||||
|
|
||||||
|
let body = SizedStream::new(0, stream::once(async { Err("stringy error") }));
|
||||||
|
assert_eq!(to_bytes(body).await, Ok(Bytes::new()));
|
||||||
|
|
||||||
|
let body = SizedStream::new(1, stream::once(async { Err("stringy error") }));
|
||||||
|
assert!(matches!(to_bytes(body).await, Err("stringy error")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn stream_boxed_error() {
|
||||||
|
// `Box<dyn Error>` does not impl `Error`
|
||||||
|
// but it does impl `Into<Box<dyn Error>>`
|
||||||
|
|
||||||
|
let body = SizedStream::new(
|
||||||
|
0,
|
||||||
|
stream::once(async { Err(Box::<dyn StdError>::from("stringy error")) }),
|
||||||
|
);
|
||||||
|
assert_eq!(to_bytes(body).await.unwrap(), Bytes::new());
|
||||||
|
|
||||||
|
let body = SizedStream::new(
|
||||||
|
1,
|
||||||
|
stream::once(async { Err(Box::<dyn StdError>::from("stringy error")) }),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
to_bytes(body).await.unwrap_err().to_string(),
|
||||||
|
"stringy error"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
77
actix-http/src/body/utils.rs
Normal file
77
actix-http/src/body/utils.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use std::task::Poll;
|
||||||
|
|
||||||
|
use actix_rt::pin;
|
||||||
|
use actix_utils::future::poll_fn;
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
|
use futures_core::ready;
|
||||||
|
|
||||||
|
use super::{BodySize, MessageBody};
|
||||||
|
|
||||||
|
/// Collects the body produced by a `MessageBody` implementation into `Bytes`.
|
||||||
|
///
|
||||||
|
/// Any errors produced by the body stream are returned immediately.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use actix_http::body::{self, to_bytes};
|
||||||
|
/// use bytes::Bytes;
|
||||||
|
///
|
||||||
|
/// # async fn test_to_bytes() {
|
||||||
|
/// let body = body::None::new();
|
||||||
|
/// let bytes = to_bytes(body).await.unwrap();
|
||||||
|
/// assert!(bytes.is_empty());
|
||||||
|
///
|
||||||
|
/// let body = Bytes::from_static(b"123");
|
||||||
|
/// let bytes = to_bytes(body).await.unwrap();
|
||||||
|
/// assert_eq!(bytes, b"123"[..]);
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
|
||||||
|
let cap = match body.size() {
|
||||||
|
BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()),
|
||||||
|
BodySize::Sized(size) => size as usize,
|
||||||
|
// good enough first guess for chunk size
|
||||||
|
BodySize::Stream => 32_768,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buf = BytesMut::with_capacity(cap);
|
||||||
|
|
||||||
|
pin!(body);
|
||||||
|
|
||||||
|
poll_fn(|cx| loop {
|
||||||
|
let body = body.as_mut();
|
||||||
|
|
||||||
|
match ready!(body.poll_next(cx)) {
|
||||||
|
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
|
||||||
|
None => return Poll::Ready(Ok(())),
|
||||||
|
Some(Err(err)) => return Poll::Ready(Err(err)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(buf.freeze())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use futures_util::{stream, StreamExt as _};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::{body::BodyStream, Error};
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_to_bytes() {
|
||||||
|
let bytes = to_bytes(()).await.unwrap();
|
||||||
|
assert!(bytes.is_empty());
|
||||||
|
|
||||||
|
let body = Bytes::from_static(b"123");
|
||||||
|
let bytes = to_bytes(body).await.unwrap();
|
||||||
|
assert_eq!(bytes, b"123"[..]);
|
||||||
|
|
||||||
|
let stream = stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")])
|
||||||
|
.map(Ok::<_, Error>);
|
||||||
|
let body = BodyStream::new(stream);
|
||||||
|
let bytes = to_bytes(body).await.unwrap();
|
||||||
|
assert_eq!(bytes, b"123abc"[..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +1,22 @@
|
|||||||
use std::{error::Error as StdError, fmt, marker::PhantomData, net, rc::Rc};
|
use std::{fmt, marker::PhantomData, net, rc::Rc, time::Duration};
|
||||||
|
|
||||||
use actix_codec::Framed;
|
use actix_codec::Framed;
|
||||||
use actix_service::{IntoServiceFactory, Service, ServiceFactory};
|
use actix_service::{IntoServiceFactory, Service, ServiceFactory};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{AnyBody, MessageBody},
|
body::{BoxBody, MessageBody},
|
||||||
config::{KeepAlive, ServiceConfig},
|
|
||||||
h1::{self, ExpectHandler, H1Service, UpgradeHandler},
|
h1::{self, ExpectHandler, H1Service, UpgradeHandler},
|
||||||
h2::H2Service,
|
|
||||||
service::HttpService,
|
service::HttpService,
|
||||||
ConnectCallback, Extensions, Request, Response,
|
ConnectCallback, Extensions, KeepAlive, Request, Response, ServiceConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A HTTP service builder
|
/// An HTTP service builder.
|
||||||
///
|
///
|
||||||
/// This type can be used to construct an instance of [`HttpService`] through a
|
/// This type can construct an instance of [`HttpService`] through a builder-like pattern.
|
||||||
/// builder-like pattern.
|
|
||||||
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
|
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
|
||||||
keep_alive: KeepAlive,
|
keep_alive: KeepAlive,
|
||||||
client_timeout: u64,
|
client_request_timeout: Duration,
|
||||||
client_disconnect: u64,
|
client_disconnect_timeout: Duration,
|
||||||
secure: bool,
|
secure: bool,
|
||||||
local_addr: Option<net::SocketAddr>,
|
local_addr: Option<net::SocketAddr>,
|
||||||
expect: X,
|
expect: X,
|
||||||
@@ -28,21 +25,23 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
|
|||||||
_phantom: PhantomData<S>,
|
_phantom: PhantomData<S>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
|
impl<T, S> Default for HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Response<AnyBody>> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
<S::Service as Service<Request>>::Future: 'static,
|
||||||
{
|
{
|
||||||
/// Create instance of `ServiceConfigBuilder`
|
fn default() -> Self {
|
||||||
pub fn new() -> Self {
|
|
||||||
HttpServiceBuilder {
|
HttpServiceBuilder {
|
||||||
keep_alive: KeepAlive::Timeout(5),
|
// ServiceConfig parts (make sure defaults match)
|
||||||
client_timeout: 5000,
|
keep_alive: KeepAlive::default(),
|
||||||
client_disconnect: 0,
|
client_request_timeout: Duration::from_secs(5),
|
||||||
|
client_disconnect_timeout: Duration::ZERO,
|
||||||
secure: false,
|
secure: false,
|
||||||
local_addr: None,
|
local_addr: None,
|
||||||
|
|
||||||
|
// dispatcher parts
|
||||||
expect: ExpectHandler,
|
expect: ExpectHandler,
|
||||||
upgrade: None,
|
upgrade: None,
|
||||||
on_connect_ext: None,
|
on_connect_ext: None,
|
||||||
@@ -54,19 +53,21 @@ where
|
|||||||
impl<T, S, X, U> HttpServiceBuilder<T, S, X, U>
|
impl<T, S, X, U> HttpServiceBuilder<T, S, X, U>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Response<AnyBody>> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
<S::Service as Service<Request>>::Future: 'static,
|
||||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||||
X::Error: Into<Response<AnyBody>>,
|
X::Error: Into<Response<BoxBody>>,
|
||||||
X::InitError: fmt::Debug,
|
X::InitError: fmt::Debug,
|
||||||
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
|
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
|
||||||
U::Error: fmt::Display,
|
U::Error: fmt::Display,
|
||||||
U::InitError: fmt::Debug,
|
U::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
/// Set server keep-alive setting.
|
/// Set connection keep-alive setting.
|
||||||
///
|
///
|
||||||
/// By default keep alive is set to a 5 seconds.
|
/// Applies to HTTP/1.1 keep-alive and HTTP/2 ping-pong.
|
||||||
|
///
|
||||||
|
/// By default keep-alive is 5 seconds.
|
||||||
pub fn keep_alive<W: Into<KeepAlive>>(mut self, val: W) -> Self {
|
pub fn keep_alive<W: Into<KeepAlive>>(mut self, val: W) -> Self {
|
||||||
self.keep_alive = val.into();
|
self.keep_alive = val.into();
|
||||||
self
|
self
|
||||||
@@ -84,33 +85,45 @@ where
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set server client timeout in milliseconds for first request.
|
/// Set client request timeout (for first request).
|
||||||
///
|
///
|
||||||
/// Defines a timeout for reading client request header. If a client does not transmit
|
/// Defines a timeout for reading client request header. If the client does not transmit the
|
||||||
/// the entire set headers within this time, the request is terminated with
|
/// request head within this duration, the connection is terminated with a `408 Request Timeout`
|
||||||
/// the 408 (Request Time-out) error.
|
/// response error.
|
||||||
///
|
///
|
||||||
/// To disable timeout set value to 0.
|
/// A duration of zero disables the timeout.
|
||||||
///
|
///
|
||||||
/// By default client timeout is set to 5000 milliseconds.
|
/// By default, the client timeout is 5 seconds.
|
||||||
pub fn client_timeout(mut self, val: u64) -> Self {
|
pub fn client_request_timeout(mut self, dur: Duration) -> Self {
|
||||||
self.client_timeout = val;
|
self.client_request_timeout = dur;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set server connection disconnect timeout in milliseconds.
|
#[doc(hidden)]
|
||||||
|
#[deprecated(since = "3.0.0", note = "Renamed to `client_request_timeout`.")]
|
||||||
|
pub fn client_timeout(self, dur: Duration) -> Self {
|
||||||
|
self.client_request_timeout(dur)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set client connection disconnect timeout.
|
||||||
///
|
///
|
||||||
/// Defines a timeout for disconnect connection. If a disconnect procedure does not complete
|
/// Defines a timeout for disconnect connection. If a disconnect procedure does not complete
|
||||||
/// within this time, the request get dropped. This timeout affects secure connections.
|
/// within this time, the request get dropped. This timeout affects secure connections.
|
||||||
///
|
///
|
||||||
/// To disable timeout set value to 0.
|
/// A duration of zero disables the timeout.
|
||||||
///
|
///
|
||||||
/// By default disconnect timeout is set to 0.
|
/// By default, the disconnect timeout is disabled.
|
||||||
pub fn client_disconnect(mut self, val: u64) -> Self {
|
pub fn client_disconnect_timeout(mut self, dur: Duration) -> Self {
|
||||||
self.client_disconnect = val;
|
self.client_disconnect_timeout = dur;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[deprecated(since = "3.0.0", note = "Renamed to `client_disconnect_timeout`.")]
|
||||||
|
pub fn client_disconnect(self, dur: Duration) -> Self {
|
||||||
|
self.client_disconnect_timeout(dur)
|
||||||
|
}
|
||||||
|
|
||||||
/// Provide service for `EXPECT: 100-Continue` support.
|
/// Provide service for `EXPECT: 100-Continue` support.
|
||||||
///
|
///
|
||||||
/// Service get called with request that contains `EXPECT` header.
|
/// Service get called with request that contains `EXPECT` header.
|
||||||
@@ -120,13 +133,13 @@ where
|
|||||||
where
|
where
|
||||||
F: IntoServiceFactory<X1, Request>,
|
F: IntoServiceFactory<X1, Request>,
|
||||||
X1: ServiceFactory<Request, Config = (), Response = Request>,
|
X1: ServiceFactory<Request, Config = (), Response = Request>,
|
||||||
X1::Error: Into<Response<AnyBody>>,
|
X1::Error: Into<Response<BoxBody>>,
|
||||||
X1::InitError: fmt::Debug,
|
X1::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
HttpServiceBuilder {
|
HttpServiceBuilder {
|
||||||
keep_alive: self.keep_alive,
|
keep_alive: self.keep_alive,
|
||||||
client_timeout: self.client_timeout,
|
client_request_timeout: self.client_request_timeout,
|
||||||
client_disconnect: self.client_disconnect,
|
client_disconnect_timeout: self.client_disconnect_timeout,
|
||||||
secure: self.secure,
|
secure: self.secure,
|
||||||
local_addr: self.local_addr,
|
local_addr: self.local_addr,
|
||||||
expect: expect.into_factory(),
|
expect: expect.into_factory(),
|
||||||
@@ -149,8 +162,8 @@ where
|
|||||||
{
|
{
|
||||||
HttpServiceBuilder {
|
HttpServiceBuilder {
|
||||||
keep_alive: self.keep_alive,
|
keep_alive: self.keep_alive,
|
||||||
client_timeout: self.client_timeout,
|
client_request_timeout: self.client_request_timeout,
|
||||||
client_disconnect: self.client_disconnect,
|
client_disconnect_timeout: self.client_disconnect_timeout,
|
||||||
secure: self.secure,
|
secure: self.secure,
|
||||||
local_addr: self.local_addr,
|
local_addr: self.local_addr,
|
||||||
expect: self.expect,
|
expect: self.expect,
|
||||||
@@ -178,14 +191,14 @@ where
|
|||||||
where
|
where
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
F: IntoServiceFactory<S, Request>,
|
F: IntoServiceFactory<S, Request>,
|
||||||
S::Error: Into<Response<AnyBody>>,
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
{
|
{
|
||||||
let cfg = ServiceConfig::new(
|
let cfg = ServiceConfig::new(
|
||||||
self.keep_alive,
|
self.keep_alive,
|
||||||
self.client_timeout,
|
self.client_request_timeout,
|
||||||
self.client_disconnect,
|
self.client_disconnect_timeout,
|
||||||
self.secure,
|
self.secure,
|
||||||
self.local_addr,
|
self.local_addr,
|
||||||
);
|
);
|
||||||
@@ -197,25 +210,25 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Finish service configuration and create a HTTP service for HTTP/2 protocol.
|
/// Finish service configuration and create a HTTP service for HTTP/2 protocol.
|
||||||
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
|
#[cfg(feature = "http2")]
|
||||||
|
pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>
|
||||||
where
|
where
|
||||||
F: IntoServiceFactory<S, Request>,
|
F: IntoServiceFactory<S, Request>,
|
||||||
S::Error: Into<Response<AnyBody>> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
|
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
B::Error: Into<Box<dyn StdError>>,
|
|
||||||
{
|
{
|
||||||
let cfg = ServiceConfig::new(
|
let cfg = ServiceConfig::new(
|
||||||
self.keep_alive,
|
self.keep_alive,
|
||||||
self.client_timeout,
|
self.client_request_timeout,
|
||||||
self.client_disconnect,
|
self.client_disconnect_timeout,
|
||||||
self.secure,
|
self.secure,
|
||||||
self.local_addr,
|
self.local_addr,
|
||||||
);
|
);
|
||||||
|
|
||||||
H2Service::with_config(cfg, service.into_factory())
|
crate::h2::H2Service::with_config(cfg, service.into_factory())
|
||||||
.on_connect_ext(self.on_connect_ext)
|
.on_connect_ext(self.on_connect_ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,17 +236,16 @@ where
|
|||||||
pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
|
pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
|
||||||
where
|
where
|
||||||
F: IntoServiceFactory<S, Request>,
|
F: IntoServiceFactory<S, Request>,
|
||||||
S::Error: Into<Response<AnyBody>> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
|
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
B::Error: Into<Box<dyn StdError>>,
|
|
||||||
{
|
{
|
||||||
let cfg = ServiceConfig::new(
|
let cfg = ServiceConfig::new(
|
||||||
self.keep_alive,
|
self.keep_alive,
|
||||||
self.client_timeout,
|
self.client_request_timeout,
|
||||||
self.client_disconnect,
|
self.client_disconnect_timeout,
|
||||||
self.secure,
|
self.secure,
|
||||||
self.local_addr,
|
self.local_addr,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
//! HTTP client.
|
|
||||||
|
|
||||||
use http::Uri;
|
|
||||||
|
|
||||||
mod config;
|
|
||||||
mod connection;
|
|
||||||
mod connector;
|
|
||||||
mod error;
|
|
||||||
mod h1proto;
|
|
||||||
mod h2proto;
|
|
||||||
mod pool;
|
|
||||||
|
|
||||||
pub use actix_tls::connect::{
|
|
||||||
Connect as TcpConnect, ConnectError as TcpConnectError, Connection as TcpConnection,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use self::connection::{Connection, ConnectionIo};
|
|
||||||
pub use self::connector::{Connector, ConnectorService};
|
|
||||||
pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};
|
|
||||||
pub use crate::Protocol;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Connect {
|
|
||||||
pub uri: Uri,
|
|
||||||
pub addr: Option<std::net::SocketAddr>,
|
|
||||||
}
|
|
||||||
@@ -1,68 +1,36 @@
|
|||||||
use std::cell::Cell;
|
use std::{
|
||||||
use std::fmt::Write;
|
net,
|
||||||
use std::rc::Rc;
|
rc::Rc,
|
||||||
use std::time::Duration;
|
time::{Duration, Instant},
|
||||||
use std::{fmt, net};
|
|
||||||
|
|
||||||
use actix_rt::{
|
|
||||||
task::JoinHandle,
|
|
||||||
time::{interval, sleep_until, Instant, Sleep},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use time::OffsetDateTime;
|
|
||||||
|
|
||||||
/// "Sun, 06 Nov 1994 08:49:37 GMT".len()
|
use crate::{date::DateService, KeepAlive};
|
||||||
const DATE_VALUE_LENGTH: usize = 29;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
/// HTTP service configuration.
|
||||||
/// Server keep-alive setting
|
#[derive(Debug, Clone)]
|
||||||
pub enum KeepAlive {
|
|
||||||
/// Keep alive in seconds
|
|
||||||
Timeout(usize),
|
|
||||||
/// Rely on OS to shutdown tcp connection
|
|
||||||
Os,
|
|
||||||
/// Disabled
|
|
||||||
Disabled,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<usize> for KeepAlive {
|
|
||||||
fn from(keepalive: usize) -> Self {
|
|
||||||
KeepAlive::Timeout(keepalive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Option<usize>> for KeepAlive {
|
|
||||||
fn from(keepalive: Option<usize>) -> Self {
|
|
||||||
if let Some(keepalive) = keepalive {
|
|
||||||
KeepAlive::Timeout(keepalive)
|
|
||||||
} else {
|
|
||||||
KeepAlive::Disabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Http service configuration
|
|
||||||
pub struct ServiceConfig(Rc<Inner>);
|
pub struct ServiceConfig(Rc<Inner>);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
struct Inner {
|
struct Inner {
|
||||||
keep_alive: Option<Duration>,
|
keep_alive: KeepAlive,
|
||||||
client_timeout: u64,
|
client_request_timeout: Duration,
|
||||||
client_disconnect: u64,
|
client_disconnect_timeout: Duration,
|
||||||
ka_enabled: bool,
|
|
||||||
secure: bool,
|
secure: bool,
|
||||||
local_addr: Option<std::net::SocketAddr>,
|
local_addr: Option<std::net::SocketAddr>,
|
||||||
date_service: DateService,
|
date_service: DateService,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for ServiceConfig {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
ServiceConfig(self.0.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ServiceConfig {
|
impl Default for ServiceConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new(KeepAlive::Timeout(5), 0, 0, false, None)
|
Self::new(
|
||||||
|
KeepAlive::default(),
|
||||||
|
Duration::from_secs(5),
|
||||||
|
Duration::ZERO,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,34 +38,22 @@ impl ServiceConfig {
|
|||||||
/// Create instance of `ServiceConfig`
|
/// Create instance of `ServiceConfig`
|
||||||
pub fn new(
|
pub fn new(
|
||||||
keep_alive: KeepAlive,
|
keep_alive: KeepAlive,
|
||||||
client_timeout: u64,
|
client_request_timeout: Duration,
|
||||||
client_disconnect: u64,
|
client_disconnect_timeout: Duration,
|
||||||
secure: bool,
|
secure: bool,
|
||||||
local_addr: Option<net::SocketAddr>,
|
local_addr: Option<net::SocketAddr>,
|
||||||
) -> ServiceConfig {
|
) -> ServiceConfig {
|
||||||
let (keep_alive, ka_enabled) = match keep_alive {
|
|
||||||
KeepAlive::Timeout(val) => (val as u64, true),
|
|
||||||
KeepAlive::Os => (0, true),
|
|
||||||
KeepAlive::Disabled => (0, false),
|
|
||||||
};
|
|
||||||
let keep_alive = if ka_enabled && keep_alive > 0 {
|
|
||||||
Some(Duration::from_secs(keep_alive))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
ServiceConfig(Rc::new(Inner {
|
ServiceConfig(Rc::new(Inner {
|
||||||
keep_alive,
|
keep_alive: keep_alive.normalize(),
|
||||||
ka_enabled,
|
client_request_timeout,
|
||||||
client_timeout,
|
client_disconnect_timeout,
|
||||||
client_disconnect,
|
|
||||||
secure,
|
secure,
|
||||||
local_addr,
|
local_addr,
|
||||||
date_service: DateService::new(),
|
date_service: DateService::new(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if connection is secure (HTTPS)
|
/// Returns `true` if connection is secure (i.e., using TLS / HTTPS).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn secure(&self) -> bool {
|
pub fn secure(&self) -> bool {
|
||||||
self.0.secure
|
self.0.secure
|
||||||
@@ -111,241 +67,92 @@ impl ServiceConfig {
|
|||||||
self.0.local_addr
|
self.0.local_addr
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keep alive duration if configured.
|
/// Connection keep-alive setting.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn keep_alive(&self) -> Option<Duration> {
|
pub fn keep_alive(&self) -> KeepAlive {
|
||||||
self.0.keep_alive
|
self.0.keep_alive
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return state of connection keep-alive functionality
|
/// Creates a time object representing the deadline for this connection's keep-alive period, if
|
||||||
#[inline]
|
/// enabled.
|
||||||
pub fn keep_alive_enabled(&self) -> bool {
|
///
|
||||||
self.0.ka_enabled
|
/// When [`KeepAlive::Os`] or [`KeepAlive::Disabled`] is set, this will return `None`.
|
||||||
}
|
pub fn keep_alive_deadline(&self) -> Option<Instant> {
|
||||||
|
match self.keep_alive() {
|
||||||
/// Client timeout for first request.
|
KeepAlive::Timeout(dur) => Some(self.now() + dur),
|
||||||
#[inline]
|
KeepAlive::Os => None,
|
||||||
pub fn client_timer(&self) -> Option<Sleep> {
|
KeepAlive::Disabled => None,
|
||||||
let delay_time = self.0.client_timeout;
|
|
||||||
if delay_time != 0 {
|
|
||||||
Some(sleep_until(self.now() + Duration::from_millis(delay_time)))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Client timeout for first request.
|
/// Creates a time object representing the deadline for the client to finish sending the head of
|
||||||
pub fn client_timer_expire(&self) -> Option<Instant> {
|
/// its first request.
|
||||||
let delay = self.0.client_timeout;
|
///
|
||||||
if delay != 0 {
|
/// Returns `None` if this `ServiceConfig was` constructed with `client_request_timeout: 0`.
|
||||||
Some(self.now() + Duration::from_millis(delay))
|
pub fn client_request_deadline(&self) -> Option<Instant> {
|
||||||
} else {
|
let timeout = self.0.client_request_timeout;
|
||||||
None
|
(timeout != Duration::ZERO).then(|| self.now() + timeout)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Client disconnect timer
|
/// Creates a time object representing the deadline for the client to disconnect.
|
||||||
pub fn client_disconnect_timer(&self) -> Option<Instant> {
|
pub fn client_disconnect_deadline(&self) -> Option<Instant> {
|
||||||
let delay = self.0.client_disconnect;
|
let timeout = self.0.client_disconnect_timeout;
|
||||||
if delay != 0 {
|
(timeout != Duration::ZERO).then(|| self.now() + timeout)
|
||||||
Some(self.now() + Duration::from_millis(delay))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return keep-alive timer delay is configured.
|
|
||||||
#[inline]
|
|
||||||
pub fn keep_alive_timer(&self) -> Option<Sleep> {
|
|
||||||
self.keep_alive().map(|ka| sleep_until(self.now() + ka))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Keep-alive expire time
|
|
||||||
pub fn keep_alive_expire(&self) -> Option<Instant> {
|
|
||||||
self.keep_alive().map(|ka| self.now() + ka)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn now(&self) -> Instant {
|
pub(crate) fn now(&self) -> Instant {
|
||||||
self.0.date_service.now()
|
self.0.date_service.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
pub(crate) fn write_date_header(&self, dst: &mut BytesMut, camel_case: bool) {
|
||||||
pub fn set_date(&self, dst: &mut BytesMut) {
|
|
||||||
let mut buf: [u8; 39] = [0; 39];
|
let mut buf: [u8; 39] = [0; 39];
|
||||||
buf[..6].copy_from_slice(b"date: ");
|
|
||||||
|
buf[..6].copy_from_slice(if camel_case { b"Date: " } else { b"date: " });
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.date_service
|
.date_service
|
||||||
.set_date(|date| buf[6..35].copy_from_slice(&date.bytes));
|
.with_date(|date| buf[6..35].copy_from_slice(&date.bytes));
|
||||||
|
|
||||||
buf[35..].copy_from_slice(b"\r\n\r\n");
|
buf[35..].copy_from_slice(b"\r\n\r\n");
|
||||||
dst.extend_from_slice(&buf);
|
dst.extend_from_slice(&buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_date_header(&self, dst: &mut BytesMut) {
|
#[allow(unused)] // used with `http2` feature flag
|
||||||
|
pub(crate) fn write_date_header_value(&self, dst: &mut BytesMut) {
|
||||||
self.0
|
self.0
|
||||||
.date_service
|
.date_service
|
||||||
.set_date(|date| dst.extend_from_slice(&date.bytes));
|
.with_date(|date| dst.extend_from_slice(&date.bytes));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
struct Date {
|
|
||||||
bytes: [u8; DATE_VALUE_LENGTH],
|
|
||||||
pos: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Date {
|
|
||||||
fn new() -> Date {
|
|
||||||
let mut date = Date {
|
|
||||||
bytes: [0; DATE_VALUE_LENGTH],
|
|
||||||
pos: 0,
|
|
||||||
};
|
|
||||||
date.update();
|
|
||||||
date
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self) {
|
|
||||||
self.pos = 0;
|
|
||||||
write!(
|
|
||||||
self,
|
|
||||||
"{}",
|
|
||||||
OffsetDateTime::now_utc().format("%a, %d %b %Y %H:%M:%S GMT")
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Write for Date {
|
|
||||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
|
||||||
let len = s.len();
|
|
||||||
self.bytes[self.pos..self.pos + len].copy_from_slice(s.as_bytes());
|
|
||||||
self.pos += len;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Service for update Date and Instant periodically at 500 millis interval.
|
|
||||||
struct DateService {
|
|
||||||
current: Rc<Cell<(Date, Instant)>>,
|
|
||||||
handle: JoinHandle<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for DateService {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
// stop the timer update async task on drop.
|
|
||||||
self.handle.abort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DateService {
|
|
||||||
fn new() -> Self {
|
|
||||||
// shared date and timer for DateService and update async task.
|
|
||||||
let current = Rc::new(Cell::new((Date::new(), Instant::now())));
|
|
||||||
let current_clone = Rc::clone(¤t);
|
|
||||||
// spawn an async task sleep for 500 milli and update current date/timer in a loop.
|
|
||||||
// handle is used to stop the task on DateService drop.
|
|
||||||
let handle = actix_rt::spawn(async move {
|
|
||||||
#[cfg(test)]
|
|
||||||
let _notify = notify_on_drop::NotifyOnDrop::new();
|
|
||||||
|
|
||||||
let mut interval = interval(Duration::from_millis(500));
|
|
||||||
loop {
|
|
||||||
let now = interval.tick().await;
|
|
||||||
let date = Date::new();
|
|
||||||
current_clone.set((date, now));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
DateService { current, handle }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn now(&self) -> Instant {
|
|
||||||
self.current.get().1
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_date<F: FnMut(&Date)>(&self, mut f: F) {
|
|
||||||
f(&self.current.get().0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move to a util module for testing all spawn handle drop style tasks.
|
|
||||||
#[cfg(test)]
|
|
||||||
/// Test Module for checking the drop state of certain async tasks that are spawned
|
|
||||||
/// with `actix_rt::spawn`
|
|
||||||
///
|
|
||||||
/// The target task must explicitly generate `NotifyOnDrop` when spawn the task
|
|
||||||
mod notify_on_drop {
|
|
||||||
use std::cell::RefCell;
|
|
||||||
|
|
||||||
thread_local! {
|
|
||||||
static NOTIFY_DROPPED: RefCell<Option<bool>> = RefCell::new(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the spawned task is dropped.
|
|
||||||
///
|
|
||||||
/// # Panic:
|
|
||||||
///
|
|
||||||
/// When there was no `NotifyOnDrop` instance on current thread
|
|
||||||
pub(crate) fn is_dropped() -> bool {
|
|
||||||
NOTIFY_DROPPED.with(|bool| {
|
|
||||||
bool.borrow()
|
|
||||||
.expect("No NotifyOnDrop existed on current thread")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct NotifyOnDrop;
|
|
||||||
|
|
||||||
impl NotifyOnDrop {
|
|
||||||
/// # Panic:
|
|
||||||
///
|
|
||||||
/// When construct multiple instances on any given thread.
|
|
||||||
pub(crate) fn new() -> Self {
|
|
||||||
NOTIFY_DROPPED.with(|bool| {
|
|
||||||
let mut bool = bool.borrow_mut();
|
|
||||||
if bool.is_some() {
|
|
||||||
panic!("NotifyOnDrop existed on current thread");
|
|
||||||
} else {
|
|
||||||
*bool = Some(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
NotifyOnDrop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for NotifyOnDrop {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
NOTIFY_DROPPED.with(|bool| {
|
|
||||||
if let Some(b) = bool.borrow_mut().as_mut() {
|
|
||||||
*b = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::{date::DATE_VALUE_LENGTH, notify_on_drop};
|
||||||
|
|
||||||
use actix_rt::{task::yield_now, time::sleep};
|
use actix_rt::{
|
||||||
|
task::yield_now,
|
||||||
|
time::{sleep, sleep_until},
|
||||||
|
};
|
||||||
|
use memchr::memmem;
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_date_service_update() {
|
async fn test_date_service_update() {
|
||||||
let settings = ServiceConfig::new(KeepAlive::Os, 0, 0, false, None);
|
let settings =
|
||||||
|
ServiceConfig::new(KeepAlive::Os, Duration::ZERO, Duration::ZERO, false, None);
|
||||||
|
|
||||||
yield_now().await;
|
yield_now().await;
|
||||||
|
|
||||||
let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
||||||
settings.set_date(&mut buf1);
|
settings.write_date_header(&mut buf1, false);
|
||||||
let now1 = settings.now();
|
let now1 = settings.now();
|
||||||
|
|
||||||
sleep_until(Instant::now() + Duration::from_secs(2)).await;
|
sleep_until((Instant::now() + Duration::from_secs(2)).into()).await;
|
||||||
yield_now().await;
|
yield_now().await;
|
||||||
|
|
||||||
let now2 = settings.now();
|
let now2 = settings.now();
|
||||||
let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
||||||
settings.set_date(&mut buf2);
|
settings.write_date_header(&mut buf2, false);
|
||||||
|
|
||||||
assert_ne!(now1, now2);
|
assert_ne!(now1, now2);
|
||||||
|
|
||||||
@@ -398,11 +205,27 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_date() {
|
async fn test_date() {
|
||||||
let settings = ServiceConfig::new(KeepAlive::Os, 0, 0, false, None);
|
let settings = ServiceConfig::default();
|
||||||
|
|
||||||
let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
||||||
settings.set_date(&mut buf1);
|
settings.write_date_header(&mut buf1, false);
|
||||||
|
|
||||||
let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
||||||
settings.set_date(&mut buf2);
|
settings.write_date_header(&mut buf2, false);
|
||||||
|
|
||||||
assert_eq!(buf1, buf2);
|
assert_eq!(buf1, buf2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_date_camel_case() {
|
||||||
|
let settings = ServiceConfig::default();
|
||||||
|
|
||||||
|
let mut buf = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
||||||
|
settings.write_date_header(&mut buf, false);
|
||||||
|
assert!(memmem::find(&buf, b"date:").is_some());
|
||||||
|
|
||||||
|
let mut buf = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
||||||
|
settings.write_date_header(&mut buf, true);
|
||||||
|
assert!(memmem::find(&buf, b"Date:").is_some());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
92
actix-http/src/date.rs
Normal file
92
actix-http/src/date.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
use std::{
|
||||||
|
cell::Cell,
|
||||||
|
fmt::{self, Write},
|
||||||
|
rc::Rc,
|
||||||
|
time::{Duration, Instant, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_rt::{task::JoinHandle, time::interval};
|
||||||
|
|
||||||
|
/// "Thu, 01 Jan 1970 00:00:00 GMT".len()
|
||||||
|
pub(crate) const DATE_VALUE_LENGTH: usize = 29;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) struct Date {
|
||||||
|
pub(crate) bytes: [u8; DATE_VALUE_LENGTH],
|
||||||
|
pos: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Date {
|
||||||
|
fn new() -> Date {
|
||||||
|
let mut date = Date {
|
||||||
|
bytes: [0; DATE_VALUE_LENGTH],
|
||||||
|
pos: 0,
|
||||||
|
};
|
||||||
|
date.update();
|
||||||
|
date
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self) {
|
||||||
|
self.pos = 0;
|
||||||
|
write!(self, "{}", httpdate::fmt_http_date(SystemTime::now())).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Write for Date {
|
||||||
|
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||||
|
let len = s.len();
|
||||||
|
self.bytes[self.pos..self.pos + len].copy_from_slice(s.as_bytes());
|
||||||
|
self.pos += len;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Service for update Date and Instant periodically at 500 millis interval.
|
||||||
|
pub(crate) struct DateService {
|
||||||
|
current: Rc<Cell<(Date, Instant)>>,
|
||||||
|
handle: JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DateService {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
// shared date and timer for DateService and update async task.
|
||||||
|
let current = Rc::new(Cell::new((Date::new(), Instant::now())));
|
||||||
|
let current_clone = Rc::clone(¤t);
|
||||||
|
// spawn an async task sleep for 500 millis and update current date/timer in a loop.
|
||||||
|
// handle is used to stop the task on DateService drop.
|
||||||
|
let handle = actix_rt::spawn(async move {
|
||||||
|
#[cfg(test)]
|
||||||
|
let _notify = crate::notify_on_drop::NotifyOnDrop::new();
|
||||||
|
|
||||||
|
let mut interval = interval(Duration::from_millis(500));
|
||||||
|
loop {
|
||||||
|
let now = interval.tick().await;
|
||||||
|
let date = Date::new();
|
||||||
|
current_clone.set((date, now.into_std()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
DateService { current, handle }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn now(&self) -> Instant {
|
||||||
|
self.current.get().1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with_date<F: FnMut(&Date)>(&self, mut f: F) {
|
||||||
|
f(&self.current.get().0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for DateService {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("DateService").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for DateService {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// stop the timer update async task on drop.
|
||||||
|
self.handle.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,9 +11,6 @@ use actix_rt::task::{spawn_blocking, JoinHandle};
|
|||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_core::{ready, Stream};
|
use futures_core::{ready, Stream};
|
||||||
|
|
||||||
#[cfg(feature = "compress-brotli")]
|
|
||||||
use brotli2::write::BrotliDecoder;
|
|
||||||
|
|
||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
use flate2::write::{GzDecoder, ZlibDecoder};
|
use flate2::write::{GzDecoder, ZlibDecoder};
|
||||||
|
|
||||||
@@ -23,17 +20,20 @@ use zstd::stream::write::Decoder as ZstdDecoder;
|
|||||||
use crate::{
|
use crate::{
|
||||||
encoding::Writer,
|
encoding::Writer,
|
||||||
error::{BlockingError, PayloadError},
|
error::{BlockingError, PayloadError},
|
||||||
http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING},
|
header::{ContentEncoding, HeaderMap, CONTENT_ENCODING},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049;
|
const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049;
|
||||||
|
|
||||||
|
pin_project_lite::pin_project! {
|
||||||
pub struct Decoder<S> {
|
pub struct Decoder<S> {
|
||||||
decoder: Option<ContentDecoder>,
|
decoder: Option<ContentDecoder>,
|
||||||
|
#[pin]
|
||||||
stream: S,
|
stream: S,
|
||||||
eof: bool,
|
eof: bool,
|
||||||
fut: Option<JoinHandle<Result<(Option<Bytes>, ContentDecoder), io::Error>>>,
|
fut: Option<JoinHandle<Result<(Option<Bytes>, ContentDecoder), io::Error>>>,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<S> Decoder<S>
|
impl<S> Decoder<S>
|
||||||
where
|
where
|
||||||
@@ -44,17 +44,17 @@ where
|
|||||||
pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> {
|
pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> {
|
||||||
let decoder = match encoding {
|
let decoder = match encoding {
|
||||||
#[cfg(feature = "compress-brotli")]
|
#[cfg(feature = "compress-brotli")]
|
||||||
ContentEncoding::Br => Some(ContentDecoder::Br(Box::new(
|
ContentEncoding::Brotli => Some(ContentDecoder::Brotli(Box::new(
|
||||||
BrotliDecoder::new(Writer::new()),
|
brotli::DecompressorWriter::new(Writer::new(), 8_096),
|
||||||
))),
|
))),
|
||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
|
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
|
||||||
ZlibDecoder::new(Writer::new()),
|
ZlibDecoder::new(Writer::new()),
|
||||||
))),
|
))),
|
||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(
|
ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(GzDecoder::new(
|
||||||
GzDecoder::new(Writer::new()),
|
Writer::new(),
|
||||||
))),
|
)))),
|
||||||
#[cfg(feature = "compress-zstd")]
|
#[cfg(feature = "compress-zstd")]
|
||||||
ContentEncoding::Zstd => Some(ContentDecoder::Zstd(Box::new(
|
ContentEncoding::Zstd => Some(ContentDecoder::Zstd(Box::new(
|
||||||
ZstdDecoder::new(Writer::new()).expect(
|
ZstdDecoder::new(Writer::new()).expect(
|
||||||
@@ -89,45 +89,44 @@ where
|
|||||||
|
|
||||||
impl<S> Stream for Decoder<S>
|
impl<S> Stream for Decoder<S>
|
||||||
where
|
where
|
||||||
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin,
|
S: Stream<Item = Result<Bytes, PayloadError>>,
|
||||||
{
|
{
|
||||||
type Item = Result<Bytes, PayloadError>;
|
type Item = Result<Bytes, PayloadError>;
|
||||||
|
|
||||||
fn poll_next(
|
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
mut self: Pin<&mut Self>,
|
let mut this = self.project();
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Self::Item>> {
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(ref mut fut) = self.fut {
|
if let Some(ref mut fut) = this.fut {
|
||||||
let (chunk, decoder) =
|
let (chunk, decoder) =
|
||||||
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
|
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
|
||||||
|
|
||||||
self.decoder = Some(decoder);
|
*this.decoder = Some(decoder);
|
||||||
self.fut.take();
|
this.fut.take();
|
||||||
|
|
||||||
if let Some(chunk) = chunk {
|
if let Some(chunk) = chunk {
|
||||||
return Poll::Ready(Some(Ok(chunk)));
|
return Poll::Ready(Some(Ok(chunk)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.eof {
|
if *this.eof {
|
||||||
return Poll::Ready(None);
|
return Poll::Ready(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
match ready!(Pin::new(&mut self.stream).poll_next(cx)) {
|
match ready!(this.stream.as_mut().poll_next(cx)) {
|
||||||
Some(Err(err)) => return Poll::Ready(Some(Err(err))),
|
Some(Err(err)) => return Poll::Ready(Some(Err(err))),
|
||||||
|
|
||||||
Some(Ok(chunk)) => {
|
Some(Ok(chunk)) => {
|
||||||
if let Some(mut decoder) = self.decoder.take() {
|
if let Some(mut decoder) = this.decoder.take() {
|
||||||
if chunk.len() < MAX_CHUNK_SIZE_DECODE_IN_PLACE {
|
if chunk.len() < MAX_CHUNK_SIZE_DECODE_IN_PLACE {
|
||||||
let chunk = decoder.feed_data(chunk)?;
|
let chunk = decoder.feed_data(chunk)?;
|
||||||
self.decoder = Some(decoder);
|
*this.decoder = Some(decoder);
|
||||||
|
|
||||||
if let Some(chunk) = chunk {
|
if let Some(chunk) = chunk {
|
||||||
return Poll::Ready(Some(Ok(chunk)));
|
return Poll::Ready(Some(Ok(chunk)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.fut = Some(spawn_blocking(move || {
|
*this.fut = Some(spawn_blocking(move || {
|
||||||
let chunk = decoder.feed_data(chunk)?;
|
let chunk = decoder.feed_data(chunk)?;
|
||||||
Ok((chunk, decoder))
|
Ok((chunk, decoder))
|
||||||
}));
|
}));
|
||||||
@@ -140,9 +139,9 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
None => {
|
None => {
|
||||||
self.eof = true;
|
*this.eof = true;
|
||||||
|
|
||||||
return if let Some(mut decoder) = self.decoder.take() {
|
return if let Some(mut decoder) = this.decoder.take() {
|
||||||
match decoder.feed_eof() {
|
match decoder.feed_eof() {
|
||||||
Ok(Some(res)) => Poll::Ready(Some(Ok(res))),
|
Ok(Some(res)) => Poll::Ready(Some(Ok(res))),
|
||||||
Ok(None) => Poll::Ready(None),
|
Ok(None) => Poll::Ready(None),
|
||||||
@@ -163,7 +162,7 @@ enum ContentDecoder {
|
|||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
Gzip(Box<GzDecoder<Writer>>),
|
Gzip(Box<GzDecoder<Writer>>),
|
||||||
#[cfg(feature = "compress-brotli")]
|
#[cfg(feature = "compress-brotli")]
|
||||||
Br(Box<BrotliDecoder<Writer>>),
|
Brotli(Box<brotli::DecompressorWriter<Writer>>),
|
||||||
// We need explicit 'static lifetime here because ZstdDecoder need lifetime
|
// We need explicit 'static lifetime here because ZstdDecoder need lifetime
|
||||||
// argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static`
|
// argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static`
|
||||||
#[cfg(feature = "compress-zstd")]
|
#[cfg(feature = "compress-zstd")]
|
||||||
@@ -174,7 +173,7 @@ impl ContentDecoder {
|
|||||||
fn feed_eof(&mut self) -> io::Result<Option<Bytes>> {
|
fn feed_eof(&mut self) -> io::Result<Option<Bytes>> {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature = "compress-brotli")]
|
#[cfg(feature = "compress-brotli")]
|
||||||
ContentDecoder::Br(ref mut decoder) => match decoder.flush() {
|
ContentDecoder::Brotli(ref mut decoder) => match decoder.flush() {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let b = decoder.get_mut().take();
|
let b = decoder.get_mut().take();
|
||||||
|
|
||||||
@@ -232,7 +231,7 @@ impl ContentDecoder {
|
|||||||
fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> {
|
fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature = "compress-brotli")]
|
#[cfg(feature = "compress-brotli")]
|
||||||
ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) {
|
ContentDecoder::Brotli(ref mut decoder) => match decoder.write_all(&data) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
decoder.flush()?;
|
decoder.flush()?;
|
||||||
let b = decoder.get_mut().take();
|
let b = decoder.get_mut().take();
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ use actix_rt::task::{spawn_blocking, JoinHandle};
|
|||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use derive_more::Display;
|
use derive_more::Display;
|
||||||
use futures_core::ready;
|
use futures_core::ready;
|
||||||
use pin_project::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
#[cfg(feature = "compress-brotli")]
|
|
||||||
use brotli2::write::BrotliEncoder;
|
|
||||||
|
|
||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
use flate2::write::{GzEncoder, ZlibEncoder};
|
use flate2::write::{GzEncoder, ZlibEncoder};
|
||||||
@@ -23,98 +20,98 @@ use flate2::write::{GzEncoder, ZlibEncoder};
|
|||||||
#[cfg(feature = "compress-zstd")]
|
#[cfg(feature = "compress-zstd")]
|
||||||
use zstd::stream::write::Encoder as ZstdEncoder;
|
use zstd::stream::write::Encoder as ZstdEncoder;
|
||||||
|
|
||||||
use crate::{
|
|
||||||
body::{Body, BodySize, BoxAnyBody, MessageBody, ResponseBody},
|
|
||||||
http::{
|
|
||||||
header::{ContentEncoding, CONTENT_ENCODING},
|
|
||||||
HeaderValue, StatusCode,
|
|
||||||
},
|
|
||||||
ResponseHead,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::Writer;
|
use super::Writer;
|
||||||
use crate::error::BlockingError;
|
use crate::{
|
||||||
|
body::{self, BodySize, MessageBody},
|
||||||
|
error::BlockingError,
|
||||||
|
header::{self, ContentEncoding, HeaderValue, CONTENT_ENCODING},
|
||||||
|
ResponseHead, StatusCode,
|
||||||
|
};
|
||||||
|
|
||||||
const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024;
|
const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024;
|
||||||
|
|
||||||
#[pin_project]
|
pin_project! {
|
||||||
pub struct Encoder<B> {
|
pub struct Encoder<B> {
|
||||||
eof: bool,
|
|
||||||
#[pin]
|
#[pin]
|
||||||
body: EncoderBody<B>,
|
body: EncoderBody<B>,
|
||||||
encoder: Option<ContentEncoder>,
|
encoder: Option<ContentEncoder>,
|
||||||
fut: Option<JoinHandle<Result<ContentEncoder, io::Error>>>,
|
fut: Option<JoinHandle<Result<ContentEncoder, io::Error>>>,
|
||||||
|
eof: bool,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B: MessageBody> Encoder<B> {
|
impl<B: MessageBody> Encoder<B> {
|
||||||
pub fn response(
|
fn none() -> Self {
|
||||||
encoding: ContentEncoding,
|
Encoder {
|
||||||
head: &mut ResponseHead,
|
body: EncoderBody::None {
|
||||||
body: ResponseBody<B>,
|
body: body::None::new(),
|
||||||
) -> ResponseBody<Encoder<B>> {
|
},
|
||||||
let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|
encoder: None,
|
||||||
|
fut: None,
|
||||||
|
eof: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self {
|
||||||
|
// no need to compress an empty body
|
||||||
|
if matches!(body.size(), BodySize::None) {
|
||||||
|
return Self::none();
|
||||||
|
}
|
||||||
|
|
||||||
|
let should_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|
||||||
|| head.status == StatusCode::SWITCHING_PROTOCOLS
|
|| head.status == StatusCode::SWITCHING_PROTOCOLS
|
||||||
|| head.status == StatusCode::NO_CONTENT
|
|| head.status == StatusCode::NO_CONTENT
|
||||||
|| encoding == ContentEncoding::Identity
|
|| encoding == ContentEncoding::Identity);
|
||||||
|| encoding == ContentEncoding::Auto);
|
|
||||||
|
|
||||||
let body = match body {
|
let body = match body.try_into_bytes() {
|
||||||
ResponseBody::Other(b) => match b {
|
Ok(body) => EncoderBody::Full { body },
|
||||||
Body::None => return ResponseBody::Other(Body::None),
|
Err(body) => EncoderBody::Stream { body },
|
||||||
Body::Empty => return ResponseBody::Other(Body::Empty),
|
|
||||||
Body::Bytes(buf) => {
|
|
||||||
if can_encode {
|
|
||||||
EncoderBody::Bytes(buf)
|
|
||||||
} else {
|
|
||||||
return ResponseBody::Other(Body::Bytes(buf));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Body::Message(stream) => EncoderBody::BoxedStream(stream),
|
|
||||||
},
|
|
||||||
ResponseBody::Body(stream) => EncoderBody::Stream(stream),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if can_encode {
|
if should_encode {
|
||||||
// Modify response body only if encoder is not None
|
// wrap body only if encoder is feature-enabled
|
||||||
if let Some(enc) = ContentEncoder::encoder(encoding) {
|
if let Some(enc) = ContentEncoder::select(encoding) {
|
||||||
update_head(encoding, head);
|
update_head(encoding, head);
|
||||||
head.no_chunking(false);
|
|
||||||
return ResponseBody::Body(Encoder {
|
return Encoder {
|
||||||
body,
|
body,
|
||||||
eof: false,
|
|
||||||
fut: None,
|
|
||||||
encoder: Some(enc),
|
encoder: Some(enc),
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ResponseBody::Body(Encoder {
|
|
||||||
body,
|
|
||||||
eof: false,
|
|
||||||
fut: None,
|
fut: None,
|
||||||
encoder: None,
|
eof: false,
|
||||||
})
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pin_project(project = EncoderBodyProj)]
|
Encoder {
|
||||||
|
body,
|
||||||
|
encoder: None,
|
||||||
|
fut: None,
|
||||||
|
eof: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pin_project! {
|
||||||
|
#[project = EncoderBodyProj]
|
||||||
enum EncoderBody<B> {
|
enum EncoderBody<B> {
|
||||||
Bytes(Bytes),
|
None { body: body::None },
|
||||||
Stream(#[pin] B),
|
Full { body: Bytes },
|
||||||
BoxedStream(BoxAnyBody),
|
Stream { #[pin] body: B },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B> MessageBody for EncoderBody<B>
|
impl<B> MessageBody for EncoderBody<B>
|
||||||
where
|
where
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
{
|
{
|
||||||
type Error = EncoderError<B::Error>;
|
type Error = EncoderError;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
match self {
|
match self {
|
||||||
EncoderBody::Bytes(ref b) => b.size(),
|
EncoderBody::None { body } => body.size(),
|
||||||
EncoderBody::Stream(ref b) => b.size(),
|
EncoderBody::Full { body } => body.size(),
|
||||||
EncoderBody::BoxedStream(ref b) => b.size(),
|
EncoderBody::Stream { body } => body.size(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,17 +120,27 @@ where
|
|||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
match self.project() {
|
match self.project() {
|
||||||
EncoderBodyProj::Bytes(b) => {
|
EncoderBodyProj::None { body } => {
|
||||||
if b.is_empty() {
|
Pin::new(body).poll_next(cx).map_err(|err| match err {})
|
||||||
Poll::Ready(None)
|
}
|
||||||
} else {
|
EncoderBodyProj::Full { body } => {
|
||||||
Poll::Ready(Some(Ok(std::mem::take(b))))
|
Pin::new(body).poll_next(cx).map_err(|err| match err {})
|
||||||
|
}
|
||||||
|
EncoderBodyProj::Stream { body } => body
|
||||||
|
.poll_next(cx)
|
||||||
|
.map_err(|err| EncoderError::Body(err.into())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EncoderBodyProj::Stream(b) => b.poll_next(cx).map_err(EncoderError::Body),
|
|
||||||
EncoderBodyProj::BoxedStream(ref mut b) => {
|
#[inline]
|
||||||
b.as_pin_mut().poll_next(cx).map_err(EncoderError::Boxed)
|
fn try_into_bytes(self) -> Result<Bytes, Self>
|
||||||
}
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
EncoderBody::None { body } => Ok(body.try_into_bytes().unwrap()),
|
||||||
|
EncoderBody::Full { body } => Ok(body.try_into_bytes().unwrap()),
|
||||||
|
_ => Err(self),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,13 +149,14 @@ impl<B> MessageBody for Encoder<B>
|
|||||||
where
|
where
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
{
|
{
|
||||||
type Error = EncoderError<B::Error>;
|
type Error = EncoderError;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
if self.encoder.is_none() {
|
if self.encoder.is_some() {
|
||||||
self.body.size()
|
|
||||||
} else {
|
|
||||||
BodySize::Stream
|
BodySize::Stream
|
||||||
|
} else {
|
||||||
|
self.body.size()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +165,7 @@ where
|
|||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
let mut this = self.project();
|
let mut this = self.project();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if *this.eof {
|
if *this.eof {
|
||||||
return Poll::Ready(None);
|
return Poll::Ready(None);
|
||||||
@@ -205,6 +214,7 @@ where
|
|||||||
None => {
|
None => {
|
||||||
if let Some(encoder) = this.encoder.take() {
|
if let Some(encoder) = this.encoder.take() {
|
||||||
let chunk = encoder.finish().map_err(EncoderError::Io)?;
|
let chunk = encoder.finish().map_err(EncoderError::Io)?;
|
||||||
|
|
||||||
if chunk.is_empty() {
|
if chunk.is_empty() {
|
||||||
return Poll::Ready(None);
|
return Poll::Ready(None);
|
||||||
} else {
|
} else {
|
||||||
@@ -218,50 +228,75 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_bytes(mut self) -> Result<Bytes, Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
if self.encoder.is_some() {
|
||||||
|
Err(self)
|
||||||
|
} else {
|
||||||
|
match self.body.try_into_bytes() {
|
||||||
|
Ok(body) => Ok(body),
|
||||||
|
Err(body) => {
|
||||||
|
self.body = body;
|
||||||
|
Err(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
|
fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
|
||||||
head.headers_mut().insert(
|
head.headers_mut()
|
||||||
CONTENT_ENCODING,
|
.insert(header::CONTENT_ENCODING, encoding.to_header_value());
|
||||||
HeaderValue::from_static(encoding.as_str()),
|
head.headers_mut()
|
||||||
);
|
.insert(header::VARY, HeaderValue::from_static("accept-encoding"));
|
||||||
|
|
||||||
|
head.no_chunking(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ContentEncoder {
|
enum ContentEncoder {
|
||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
Deflate(ZlibEncoder<Writer>),
|
Deflate(ZlibEncoder<Writer>),
|
||||||
|
|
||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
Gzip(GzEncoder<Writer>),
|
Gzip(GzEncoder<Writer>),
|
||||||
|
|
||||||
#[cfg(feature = "compress-brotli")]
|
#[cfg(feature = "compress-brotli")]
|
||||||
Br(BrotliEncoder<Writer>),
|
Brotli(Box<brotli::CompressorWriter<Writer>>),
|
||||||
// We need explicit 'static lifetime here because ZstdEncoder need lifetime
|
|
||||||
// argument, and we use `spawn_blocking` in `Encoder::poll_next` that require `FnOnce() -> R + Send + 'static`
|
// Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we
|
||||||
|
// use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`.
|
||||||
#[cfg(feature = "compress-zstd")]
|
#[cfg(feature = "compress-zstd")]
|
||||||
Zstd(ZstdEncoder<'static, Writer>),
|
Zstd(ZstdEncoder<'static, Writer>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContentEncoder {
|
impl ContentEncoder {
|
||||||
fn encoder(encoding: ContentEncoding) -> Option<Self> {
|
fn select(encoding: ContentEncoding) -> Option<Self> {
|
||||||
match encoding {
|
match encoding {
|
||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new(
|
ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new(
|
||||||
Writer::new(),
|
Writer::new(),
|
||||||
flate2::Compression::fast(),
|
flate2::Compression::fast(),
|
||||||
))),
|
))),
|
||||||
|
|
||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new(
|
ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new(
|
||||||
Writer::new(),
|
Writer::new(),
|
||||||
flate2::Compression::fast(),
|
flate2::Compression::fast(),
|
||||||
))),
|
))),
|
||||||
|
|
||||||
#[cfg(feature = "compress-brotli")]
|
#[cfg(feature = "compress-brotli")]
|
||||||
ContentEncoding::Br => {
|
ContentEncoding::Brotli => Some(ContentEncoder::Brotli(new_brotli_compressor())),
|
||||||
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3)))
|
|
||||||
}
|
|
||||||
#[cfg(feature = "compress-zstd")]
|
#[cfg(feature = "compress-zstd")]
|
||||||
ContentEncoding::Zstd => {
|
ContentEncoding::Zstd => {
|
||||||
let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?;
|
let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?;
|
||||||
Some(ContentEncoder::Zstd(encoder))
|
Some(ContentEncoder::Zstd(encoder))
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,11 +305,14 @@ impl ContentEncoder {
|
|||||||
pub(crate) fn take(&mut self) -> Bytes {
|
pub(crate) fn take(&mut self) -> Bytes {
|
||||||
match *self {
|
match *self {
|
||||||
#[cfg(feature = "compress-brotli")]
|
#[cfg(feature = "compress-brotli")]
|
||||||
ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(),
|
ContentEncoder::Brotli(ref mut encoder) => encoder.get_mut().take(),
|
||||||
|
|
||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
|
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
|
||||||
|
|
||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(),
|
ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(),
|
||||||
|
|
||||||
#[cfg(feature = "compress-zstd")]
|
#[cfg(feature = "compress-zstd")]
|
||||||
ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(),
|
ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(),
|
||||||
}
|
}
|
||||||
@@ -283,20 +321,23 @@ impl ContentEncoder {
|
|||||||
fn finish(self) -> Result<Bytes, io::Error> {
|
fn finish(self) -> Result<Bytes, io::Error> {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature = "compress-brotli")]
|
#[cfg(feature = "compress-brotli")]
|
||||||
ContentEncoder::Br(encoder) => match encoder.finish() {
|
ContentEncoder::Brotli(mut encoder) => match encoder.flush() {
|
||||||
Ok(writer) => Ok(writer.buf.freeze()),
|
Ok(()) => Ok(encoder.into_inner().buf.freeze()),
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
},
|
},
|
||||||
|
|
||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoder::Gzip(encoder) => match encoder.finish() {
|
ContentEncoder::Gzip(encoder) => match encoder.finish() {
|
||||||
Ok(writer) => Ok(writer.buf.freeze()),
|
Ok(writer) => Ok(writer.buf.freeze()),
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
},
|
},
|
||||||
|
|
||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoder::Deflate(encoder) => match encoder.finish() {
|
ContentEncoder::Deflate(encoder) => match encoder.finish() {
|
||||||
Ok(writer) => Ok(writer.buf.freeze()),
|
Ok(writer) => Ok(writer.buf.freeze()),
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
},
|
},
|
||||||
|
|
||||||
#[cfg(feature = "compress-zstd")]
|
#[cfg(feature = "compress-zstd")]
|
||||||
ContentEncoder::Zstd(encoder) => match encoder.finish() {
|
ContentEncoder::Zstd(encoder) => match encoder.finish() {
|
||||||
Ok(writer) => Ok(writer.buf.freeze()),
|
Ok(writer) => Ok(writer.buf.freeze()),
|
||||||
@@ -308,34 +349,37 @@ impl ContentEncoder {
|
|||||||
fn write(&mut self, data: &[u8]) -> Result<(), io::Error> {
|
fn write(&mut self, data: &[u8]) -> Result<(), io::Error> {
|
||||||
match *self {
|
match *self {
|
||||||
#[cfg(feature = "compress-brotli")]
|
#[cfg(feature = "compress-brotli")]
|
||||||
ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
trace!("Error decoding br encoding: {}", err);
|
log::trace!("Error decoding br encoding: {}", err);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
trace!("Error decoding gzip encoding: {}", err);
|
log::trace!("Error decoding gzip encoding: {}", err);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
trace!("Error decoding deflate encoding: {}", err);
|
log::trace!("Error decoding deflate encoding: {}", err);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
#[cfg(feature = "compress-zstd")]
|
#[cfg(feature = "compress-zstd")]
|
||||||
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
trace!("Error decoding ztsd encoding: {}", err);
|
log::trace!("Error decoding ztsd encoding: {}", err);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -343,14 +387,21 @@ impl ContentEncoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-brotli")]
|
||||||
|
fn new_brotli_compressor() -> Box<brotli::CompressorWriter<Writer>> {
|
||||||
|
Box::new(brotli::CompressorWriter::new(
|
||||||
|
Writer::new(),
|
||||||
|
32 * 1024, // 32 KiB buffer
|
||||||
|
3, // BROTLI_PARAM_QUALITY
|
||||||
|
22, // BROTLI_PARAM_LGWIN
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Display)]
|
#[derive(Debug, Display)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum EncoderError<E> {
|
pub enum EncoderError {
|
||||||
#[display(fmt = "body")]
|
#[display(fmt = "body")]
|
||||||
Body(E),
|
Body(Box<dyn StdError>),
|
||||||
|
|
||||||
#[display(fmt = "boxed")]
|
|
||||||
Boxed(Box<dyn StdError>),
|
|
||||||
|
|
||||||
#[display(fmt = "blocking")]
|
#[display(fmt = "blocking")]
|
||||||
Blocking(BlockingError),
|
Blocking(BlockingError),
|
||||||
@@ -359,19 +410,18 @@ pub enum EncoderError<E> {
|
|||||||
Io(io::Error),
|
Io(io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: StdError + 'static> StdError for EncoderError<E> {
|
impl StdError for EncoderError {
|
||||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||||
match self {
|
match self {
|
||||||
EncoderError::Body(err) => Some(err),
|
EncoderError::Body(err) => Some(&**err),
|
||||||
EncoderError::Boxed(err) => Some(&**err),
|
|
||||||
EncoderError::Blocking(err) => Some(err),
|
EncoderError::Blocking(err) => Some(err),
|
||||||
EncoderError::Io(err) => Some(err),
|
EncoderError::Io(err) => Some(err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: StdError + 'static> From<EncoderError<E>> for crate::Error {
|
impl From<EncoderError> for crate::Error {
|
||||||
fn from(err: EncoderError<E>) -> Self {
|
fn from(err: EncoderError) -> Self {
|
||||||
crate::Error::new_encoder().with_cause(err)
|
crate::Error::new_encoder().with_cause(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ mod encoder;
|
|||||||
pub use self::decoder::Decoder;
|
pub use self::decoder::Decoder;
|
||||||
pub use self::encoder::Encoder;
|
pub use self::encoder::Encoder;
|
||||||
|
|
||||||
|
/// Special-purpose writer for streaming (de-)compression.
|
||||||
|
///
|
||||||
|
/// Pre-allocates 8KiB of capacity.
|
||||||
pub(self) struct Writer {
|
pub(self) struct Writer {
|
||||||
buf: BytesMut,
|
buf: BytesMut,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Err
|
|||||||
use derive_more::{Display, Error, From};
|
use derive_more::{Display, Error, From};
|
||||||
use http::{uri::InvalidUri, StatusCode};
|
use http::{uri::InvalidUri, StatusCode};
|
||||||
|
|
||||||
use crate::{
|
use crate::{body::BoxBody, Response};
|
||||||
body::{AnyBody, Body},
|
|
||||||
ws, Response,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use http::Error as HttpError;
|
pub use http::Error as HttpError;
|
||||||
|
|
||||||
@@ -29,6 +26,11 @@ impl Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with_cause(mut self, cause: impl Into<Box<dyn StdError>>) -> Self {
|
||||||
|
self.inner.cause = Some(cause.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn new_http() -> Self {
|
pub(crate) fn new_http() -> Self {
|
||||||
Self::new(Kind::Http)
|
Self::new(Kind::Http)
|
||||||
}
|
}
|
||||||
@@ -49,41 +51,36 @@ impl Error {
|
|||||||
Self::new(Kind::SendResponse)
|
Self::new(Kind::SendResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove allow
|
#[allow(unused)] // reserved for future use (TODO: remove allow when being used)
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) fn new_io() -> Self {
|
pub(crate) fn new_io() -> Self {
|
||||||
Self::new(Kind::Io)
|
Self::new(Kind::Io)
|
||||||
}
|
}
|
||||||
|
|
||||||
// used in encoder behind feature flag so ignore unused warning
|
#[allow(unused)] // used in encoder behind feature flag so ignore unused warning
|
||||||
#[allow(unused)]
|
|
||||||
pub(crate) fn new_encoder() -> Self {
|
pub(crate) fn new_encoder() -> Self {
|
||||||
Self::new(Kind::Encoder)
|
Self::new(Kind::Encoder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused)] // used with `ws` feature flag
|
||||||
pub(crate) fn new_ws() -> Self {
|
pub(crate) fn new_ws() -> Self {
|
||||||
Self::new(Kind::Ws)
|
Self::new(Kind::Ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn with_cause(mut self, cause: impl Into<Box<dyn StdError>>) -> Self {
|
|
||||||
self.inner.cause = Some(cause.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Error> for Response<AnyBody> {
|
impl From<Error> for Response<BoxBody> {
|
||||||
fn from(err: Error) -> Self {
|
fn from(err: Error) -> Self {
|
||||||
|
// TODO: more appropriate error status codes, usage assessment needed
|
||||||
let status_code = match err.inner.kind {
|
let status_code = match err.inner.kind {
|
||||||
Kind::Parse => StatusCode::BAD_REQUEST,
|
Kind::Parse => StatusCode::BAD_REQUEST,
|
||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
};
|
};
|
||||||
|
|
||||||
Response::new(status_code).set_body(Body::from(err.to_string()))
|
Response::new(status_code).set_body(BoxBody::new(err.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)]
|
||||||
pub enum Kind {
|
pub(crate) enum Kind {
|
||||||
#[display(fmt = "error processing HTTP")]
|
#[display(fmt = "error processing HTTP")]
|
||||||
Http,
|
Http,
|
||||||
|
|
||||||
@@ -137,20 +134,22 @@ impl From<std::convert::Infallible> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ws::ProtocolError> for Error {
|
|
||||||
fn from(err: ws::ProtocolError) -> Self {
|
|
||||||
Self::new_ws().with_cause(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<HttpError> for Error {
|
impl From<HttpError> for Error {
|
||||||
fn from(err: HttpError) -> Self {
|
fn from(err: HttpError) -> Self {
|
||||||
Self::new_http().with_cause(err)
|
Self::new_http().with_cause(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ws::HandshakeError> for Error {
|
#[cfg(feature = "ws")]
|
||||||
fn from(err: ws::HandshakeError) -> Self {
|
impl From<crate::ws::HandshakeError> for Error {
|
||||||
|
fn from(err: crate::ws::HandshakeError) -> Self {
|
||||||
|
Self::new_ws().with_cause(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ws")]
|
||||||
|
impl From<crate::ws::ProtocolError> for Error {
|
||||||
|
fn from(err: crate::ws::ProtocolError) -> Self {
|
||||||
Self::new_ws().with_cause(err)
|
Self::new_ws().with_cause(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,7 +244,7 @@ impl From<ParseError> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ParseError> for Response<AnyBody> {
|
impl From<ParseError> for Response<BoxBody> {
|
||||||
fn from(err: ParseError) -> Self {
|
fn from(err: ParseError) -> Self {
|
||||||
Error::from(err).into()
|
Error::from(err).into()
|
||||||
}
|
}
|
||||||
@@ -254,6 +253,7 @@ impl From<ParseError> for Response<AnyBody> {
|
|||||||
/// A set of errors that can occur running blocking tasks in thread pool.
|
/// A set of errors that can occur running blocking tasks in thread pool.
|
||||||
#[derive(Debug, Display, Error)]
|
#[derive(Debug, Display, Error)]
|
||||||
#[display(fmt = "Blocking thread pool is gone")]
|
#[display(fmt = "Blocking thread pool is gone")]
|
||||||
|
// TODO: non-exhaustive
|
||||||
pub struct BlockingError;
|
pub struct BlockingError;
|
||||||
|
|
||||||
/// A set of errors that can occur during payload parsing.
|
/// A set of errors that can occur during payload parsing.
|
||||||
@@ -280,8 +280,9 @@ pub enum PayloadError {
|
|||||||
UnknownLength,
|
UnknownLength,
|
||||||
|
|
||||||
/// HTTP/2 payload error.
|
/// HTTP/2 payload error.
|
||||||
|
#[cfg(feature = "http2")]
|
||||||
#[display(fmt = "{}", _0)]
|
#[display(fmt = "{}", _0)]
|
||||||
Http2Payload(h2::Error),
|
Http2Payload(::h2::Error),
|
||||||
|
|
||||||
/// Generic I/O error.
|
/// Generic I/O error.
|
||||||
#[display(fmt = "{}", _0)]
|
#[display(fmt = "{}", _0)]
|
||||||
@@ -296,14 +297,16 @@ impl std::error::Error for PayloadError {
|
|||||||
PayloadError::EncodingCorrupted => None,
|
PayloadError::EncodingCorrupted => None,
|
||||||
PayloadError::Overflow => None,
|
PayloadError::Overflow => None,
|
||||||
PayloadError::UnknownLength => None,
|
PayloadError::UnknownLength => None,
|
||||||
|
#[cfg(feature = "http2")]
|
||||||
PayloadError::Http2Payload(err) => Some(err as &dyn std::error::Error),
|
PayloadError::Http2Payload(err) => Some(err as &dyn std::error::Error),
|
||||||
PayloadError::Io(err) => Some(err as &dyn std::error::Error),
|
PayloadError::Io(err) => Some(err as &dyn std::error::Error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<h2::Error> for PayloadError {
|
#[cfg(feature = "http2")]
|
||||||
fn from(err: h2::Error) -> Self {
|
impl From<::h2::Error> for PayloadError {
|
||||||
|
fn from(err: ::h2::Error) -> Self {
|
||||||
PayloadError::Http2Payload(err)
|
PayloadError::Http2Payload(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,32 +339,30 @@ impl From<PayloadError> for Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A set of errors that can occur during dispatching HTTP requests.
|
/// A set of errors that can occur during dispatching HTTP requests.
|
||||||
#[derive(Debug, Display, Error, From)]
|
#[derive(Debug, Display, From)]
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum DispatchError {
|
pub enum DispatchError {
|
||||||
/// Service error
|
/// Service error.
|
||||||
// FIXME: display and error type
|
|
||||||
#[display(fmt = "Service Error")]
|
#[display(fmt = "Service Error")]
|
||||||
Service(#[error(not(source))] Response<AnyBody>),
|
Service(Response<BoxBody>),
|
||||||
|
|
||||||
/// Body error
|
/// Body streaming error.
|
||||||
// FIXME: display and error type
|
#[display(fmt = "Body error: {}", _0)]
|
||||||
#[display(fmt = "Body Error")]
|
Body(Box<dyn StdError>),
|
||||||
Body(#[error(not(source))] Box<dyn StdError>),
|
|
||||||
|
|
||||||
/// Upgrade service error
|
/// Upgrade service error.
|
||||||
Upgrade,
|
Upgrade,
|
||||||
|
|
||||||
/// An `io::Error` that occurred while trying to read or write to a network stream.
|
/// An `io::Error` that occurred while trying to read or write to a network stream.
|
||||||
#[display(fmt = "IO error: {}", _0)]
|
#[display(fmt = "IO error: {}", _0)]
|
||||||
Io(io::Error),
|
Io(io::Error),
|
||||||
|
|
||||||
/// Http request parse error.
|
/// Request parse error.
|
||||||
#[display(fmt = "Parse error: {}", _0)]
|
#[display(fmt = "Request parse error: {}", _0)]
|
||||||
Parse(ParseError),
|
Parse(ParseError),
|
||||||
|
|
||||||
/// Http/2 error
|
/// HTTP/2 error.
|
||||||
#[display(fmt = "{}", _0)]
|
#[display(fmt = "{}", _0)]
|
||||||
|
#[cfg(feature = "http2")]
|
||||||
H2(h2::Error),
|
H2(h2::Error),
|
||||||
|
|
||||||
/// The first request did not complete within the specified timeout.
|
/// The first request did not complete within the specified timeout.
|
||||||
@@ -372,25 +373,31 @@ pub enum DispatchError {
|
|||||||
#[display(fmt = "Connection shutdown timeout")]
|
#[display(fmt = "Connection shutdown timeout")]
|
||||||
DisconnectTimeout,
|
DisconnectTimeout,
|
||||||
|
|
||||||
/// Payload is not consumed
|
/// Internal error.
|
||||||
#[display(fmt = "Task is completed but request's payload is not consumed")]
|
|
||||||
PayloadIsNotConsumed,
|
|
||||||
|
|
||||||
/// Malformed request
|
|
||||||
#[display(fmt = "Malformed request")]
|
|
||||||
MalformedRequest,
|
|
||||||
|
|
||||||
/// Internal error
|
|
||||||
#[display(fmt = "Internal error")]
|
#[display(fmt = "Internal error")]
|
||||||
InternalError,
|
InternalError,
|
||||||
|
}
|
||||||
|
|
||||||
/// Unknown error
|
impl StdError for DispatchError {
|
||||||
#[display(fmt = "Unknown error")]
|
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||||
Unknown,
|
match self {
|
||||||
|
// TODO: error source extraction?
|
||||||
|
DispatchError::Service(_res) => None,
|
||||||
|
DispatchError::Body(err) => Some(&**err),
|
||||||
|
DispatchError::Io(err) => Some(err),
|
||||||
|
DispatchError::Parse(err) => Some(err),
|
||||||
|
|
||||||
|
#[cfg(feature = "http2")]
|
||||||
|
DispatchError::H2(err) => Some(err),
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A set of error that can occur during parsing content type.
|
/// A set of error that can occur during parsing content type.
|
||||||
#[derive(Debug, Display, Error)]
|
#[derive(Debug, Display, Error)]
|
||||||
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum ContentTypeError {
|
pub enum ContentTypeError {
|
||||||
/// Can not parse content type
|
/// Can not parse content type
|
||||||
@@ -402,35 +409,21 @@ pub enum ContentTypeError {
|
|||||||
UnknownEncoding,
|
UnknownEncoding,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod content_type_test_impls {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
impl std::cmp::PartialEq for ContentTypeError {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::ParseError => matches!(other, ContentTypeError::ParseError),
|
|
||||||
Self::UnknownEncoding => {
|
|
||||||
matches!(other, ContentTypeError::UnknownEncoding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use http::{Error as HttpError, StatusCode};
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
|
use http::{Error as HttpError, StatusCode};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_into_response() {
|
fn test_into_response() {
|
||||||
let resp: Response<AnyBody> = ParseError::Incomplete.into();
|
let resp: Response<BoxBody> = ParseError::Incomplete.into();
|
||||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||||
|
|
||||||
let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into();
|
let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into();
|
||||||
let resp: Response<AnyBody> = Error::new_http().with_cause(err).into();
|
let resp: Response<BoxBody> = Error::new_http().with_cause(err).into();
|
||||||
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,14 +448,13 @@ mod tests {
|
|||||||
fn test_error_http_response() {
|
fn test_error_http_response() {
|
||||||
let orig = io::Error::new(io::ErrorKind::Other, "other");
|
let orig = io::Error::new(io::ErrorKind::Other, "other");
|
||||||
let err = Error::new_io().with_cause(orig);
|
let err = Error::new_io().with_cause(orig);
|
||||||
let resp: Response<AnyBody> = err.into();
|
let resp: Response<BoxBody> = err.into();
|
||||||
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_payload_error() {
|
fn test_payload_error() {
|
||||||
let err: PayloadError =
|
let err: PayloadError = io::Error::new(io::ErrorKind::Other, "ParseError").into();
|
||||||
io::Error::new(io::ErrorKind::Other, "ParseError").into();
|
|
||||||
assert!(err.to_string().contains("ParseError"));
|
assert!(err.to_string().contains("ParseError"));
|
||||||
|
|
||||||
let err = PayloadError::Incomplete(None);
|
let err = PayloadError::Incomplete(None);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
fmt, mem,
|
fmt,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ahash::AHashMap;
|
use ahash::AHashMap;
|
||||||
@@ -10,8 +10,7 @@ use ahash::AHashMap;
|
|||||||
/// All entries into this map must be owned types (or static references).
|
/// All entries into this map must be owned types (or static references).
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Extensions {
|
pub struct Extensions {
|
||||||
/// Use FxHasher with a std HashMap with for faster
|
/// Use AHasher with a std HashMap with for faster lookups on the small `TypeId` keys.
|
||||||
/// lookups on the small `TypeId` (u64 equivalent) keys.
|
|
||||||
map: AHashMap<TypeId, Box<dyn Any>>,
|
map: AHashMap<TypeId, Box<dyn Any>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +19,7 @@ impl Extensions {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn new() -> Extensions {
|
pub fn new() -> Extensions {
|
||||||
Extensions {
|
Extensions {
|
||||||
map: AHashMap::default(),
|
map: AHashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,11 +122,6 @@ impl Extensions {
|
|||||||
pub fn extend(&mut self, other: Extensions) {
|
pub fn extend(&mut self, other: Extensions) {
|
||||||
self.map.extend(other.map);
|
self.map.extend(other.map);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets (or overrides) items from `other` into this map.
|
|
||||||
pub(crate) fn drain_from(&mut self, other: &mut Self) {
|
|
||||||
self.map.extend(mem::take(&mut other.map));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Extensions {
|
impl fmt::Debug for Extensions {
|
||||||
@@ -179,6 +173,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_integers() {
|
fn test_integers() {
|
||||||
|
static A: u32 = 8;
|
||||||
|
|
||||||
let mut map = Extensions::new();
|
let mut map = Extensions::new();
|
||||||
|
|
||||||
map.insert::<i8>(8);
|
map.insert::<i8>(8);
|
||||||
@@ -191,6 +187,7 @@ mod tests {
|
|||||||
map.insert::<u32>(32);
|
map.insert::<u32>(32);
|
||||||
map.insert::<u64>(64);
|
map.insert::<u64>(64);
|
||||||
map.insert::<u128>(128);
|
map.insert::<u128>(128);
|
||||||
|
map.insert::<&'static u32>(&A);
|
||||||
assert!(map.get::<i8>().is_some());
|
assert!(map.get::<i8>().is_some());
|
||||||
assert!(map.get::<i16>().is_some());
|
assert!(map.get::<i16>().is_some());
|
||||||
assert!(map.get::<i32>().is_some());
|
assert!(map.get::<i32>().is_some());
|
||||||
@@ -201,6 +198,7 @@ mod tests {
|
|||||||
assert!(map.get::<u32>().is_some());
|
assert!(map.get::<u32>().is_some());
|
||||||
assert!(map.get::<u64>().is_some());
|
assert!(map.get::<u64>().is_some());
|
||||||
assert!(map.get::<u128>().is_some());
|
assert!(map.get::<u128>().is_some());
|
||||||
|
assert!(map.get::<&'static u32>().is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -279,27 +277,4 @@ mod tests {
|
|||||||
assert_eq!(extensions.get(), Some(&20u8));
|
assert_eq!(extensions.get(), Some(&20u8));
|
||||||
assert_eq!(extensions.get_mut(), Some(&mut 20u8));
|
assert_eq!(extensions.get_mut(), Some(&mut 20u8));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_drain_from() {
|
|
||||||
let mut ext = Extensions::new();
|
|
||||||
ext.insert(2isize);
|
|
||||||
|
|
||||||
let mut more_ext = Extensions::new();
|
|
||||||
|
|
||||||
more_ext.insert(5isize);
|
|
||||||
more_ext.insert(5usize);
|
|
||||||
|
|
||||||
assert_eq!(ext.get::<isize>(), Some(&2isize));
|
|
||||||
assert_eq!(ext.get::<usize>(), None);
|
|
||||||
assert_eq!(more_ext.get::<isize>(), Some(&5isize));
|
|
||||||
assert_eq!(more_ext.get::<usize>(), Some(&5usize));
|
|
||||||
|
|
||||||
ext.drain_from(&mut more_ext);
|
|
||||||
|
|
||||||
assert_eq!(ext.get::<isize>(), Some(&5isize));
|
|
||||||
assert_eq!(ext.get::<usize>(), Some(&5usize));
|
|
||||||
assert_eq!(more_ext.get::<isize>(), None);
|
|
||||||
assert_eq!(more_ext.get::<usize>(), None);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,10 +50,7 @@ impl ChunkedState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_size(
|
fn read_size(rdr: &mut BytesMut, size: &mut u64) -> Poll<Result<ChunkedState, io::Error>> {
|
||||||
rdr: &mut BytesMut,
|
|
||||||
size: &mut u64,
|
|
||||||
) -> Poll<Result<ChunkedState, io::Error>> {
|
|
||||||
let radix = 16;
|
let radix = 16;
|
||||||
|
|
||||||
let rem = match byte!(rdr) {
|
let rem = match byte!(rdr) {
|
||||||
@@ -111,10 +108,7 @@ impl ChunkedState {
|
|||||||
_ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions
|
_ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn read_size_lf(
|
fn read_size_lf(rdr: &mut BytesMut, size: u64) -> Poll<Result<ChunkedState, io::Error>> {
|
||||||
rdr: &mut BytesMut,
|
|
||||||
size: u64,
|
|
||||||
) -> Poll<Result<ChunkedState, io::Error>> {
|
|
||||||
match byte!(rdr) {
|
match byte!(rdr) {
|
||||||
b'\n' if size > 0 => Poll::Ready(Ok(ChunkedState::Body)),
|
b'\n' if size > 0 => Poll::Ready(Ok(ChunkedState::Body)),
|
||||||
b'\n' if size == 0 => Poll::Ready(Ok(ChunkedState::EndCr)),
|
b'\n' if size == 0 => Poll::Ready(Ok(ChunkedState::EndCr)),
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
use std::io;
|
use std::{fmt, io};
|
||||||
|
|
||||||
use actix_codec::{Decoder, Encoder};
|
use actix_codec::{Decoder, Encoder};
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use http::{Method, Version};
|
use http::{Method, Version};
|
||||||
|
|
||||||
use super::decoder::{PayloadDecoder, PayloadItem, PayloadType};
|
use super::{
|
||||||
use super::{decoder, encoder, reserve_readbuf};
|
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
|
||||||
use super::{Message, MessageType};
|
encoder, reserve_readbuf, Message, MessageType,
|
||||||
use crate::body::BodySize;
|
};
|
||||||
use crate::config::ServiceConfig;
|
use crate::{
|
||||||
use crate::error::{ParseError, PayloadError};
|
body::BodySize,
|
||||||
use crate::message::{ConnectionType, RequestHeadType, ResponseHead};
|
error::{ParseError, PayloadError},
|
||||||
|
ConnectionType, RequestHeadType, ResponseHead, ServiceConfig,
|
||||||
|
};
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
struct Flags: u8 {
|
struct Flags: u8 {
|
||||||
const HEAD = 0b0000_0001;
|
const HEAD = 0b0000_0001;
|
||||||
const KEEPALIVE_ENABLED = 0b0000_1000;
|
const KEEP_ALIVE_ENABLED = 0b0000_1000;
|
||||||
const STREAM = 0b0001_0000;
|
const STREAM = 0b0001_0000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,7 +38,7 @@ struct ClientCodecInner {
|
|||||||
decoder: decoder::MessageDecoder<ResponseHead>,
|
decoder: decoder::MessageDecoder<ResponseHead>,
|
||||||
payload: Option<PayloadDecoder>,
|
payload: Option<PayloadDecoder>,
|
||||||
version: Version,
|
version: Version,
|
||||||
ctype: ConnectionType,
|
conn_type: ConnectionType,
|
||||||
|
|
||||||
// encoder part
|
// encoder part
|
||||||
flags: Flags,
|
flags: Flags,
|
||||||
@@ -49,23 +51,32 @@ impl Default for ClientCodec {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for ClientCodec {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("h1::ClientCodec")
|
||||||
|
.field("flags", &self.inner.flags)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ClientCodec {
|
impl ClientCodec {
|
||||||
/// Create HTTP/1 codec.
|
/// Create HTTP/1 codec.
|
||||||
///
|
///
|
||||||
/// `keepalive_enabled` how response `connection` header get generated.
|
/// `keepalive_enabled` how response `connection` header get generated.
|
||||||
pub fn new(config: ServiceConfig) -> Self {
|
pub fn new(config: ServiceConfig) -> Self {
|
||||||
let flags = if config.keep_alive_enabled() {
|
let flags = if config.keep_alive().enabled() {
|
||||||
Flags::KEEPALIVE_ENABLED
|
Flags::KEEP_ALIVE_ENABLED
|
||||||
} else {
|
} else {
|
||||||
Flags::empty()
|
Flags::empty()
|
||||||
};
|
};
|
||||||
|
|
||||||
ClientCodec {
|
ClientCodec {
|
||||||
inner: ClientCodecInner {
|
inner: ClientCodecInner {
|
||||||
config,
|
config,
|
||||||
decoder: decoder::MessageDecoder::default(),
|
decoder: decoder::MessageDecoder::default(),
|
||||||
payload: None,
|
payload: None,
|
||||||
version: Version::HTTP_11,
|
version: Version::HTTP_11,
|
||||||
ctype: ConnectionType::Close,
|
conn_type: ConnectionType::Close,
|
||||||
|
|
||||||
flags,
|
flags,
|
||||||
encoder: encoder::MessageEncoder::default(),
|
encoder: encoder::MessageEncoder::default(),
|
||||||
@@ -75,12 +86,12 @@ impl ClientCodec {
|
|||||||
|
|
||||||
/// Check if request is upgrade
|
/// Check if request is upgrade
|
||||||
pub fn upgrade(&self) -> bool {
|
pub fn upgrade(&self) -> bool {
|
||||||
self.inner.ctype == ConnectionType::Upgrade
|
self.inner.conn_type == ConnectionType::Upgrade
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if last response is keep-alive
|
/// Check if last response is keep-alive
|
||||||
pub fn keepalive(&self) -> bool {
|
pub fn keep_alive(&self) -> bool {
|
||||||
self.inner.ctype == ConnectionType::KeepAlive
|
self.inner.conn_type == ConnectionType::KeepAlive
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check last request's message type
|
/// Check last request's message type
|
||||||
@@ -102,8 +113,8 @@ impl ClientCodec {
|
|||||||
|
|
||||||
impl ClientPayloadCodec {
|
impl ClientPayloadCodec {
|
||||||
/// Check if last response is keep-alive
|
/// Check if last response is keep-alive
|
||||||
pub fn keepalive(&self) -> bool {
|
pub fn keep_alive(&self) -> bool {
|
||||||
self.inner.ctype == ConnectionType::KeepAlive
|
self.inner.conn_type == ConnectionType::KeepAlive
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transform payload codec to a message codec
|
/// Transform payload codec to a message codec
|
||||||
@@ -120,12 +131,12 @@ impl Decoder for ClientCodec {
|
|||||||
debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set");
|
debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set");
|
||||||
|
|
||||||
if let Some((req, payload)) = self.inner.decoder.decode(src)? {
|
if let Some((req, payload)) = self.inner.decoder.decode(src)? {
|
||||||
if let Some(ctype) = req.ctype() {
|
if let Some(conn_type) = req.conn_type() {
|
||||||
// do not use peer's keep-alive
|
// do not use peer's keep-alive
|
||||||
self.inner.ctype = if ctype == ConnectionType::KeepAlive {
|
self.inner.conn_type = if conn_type == ConnectionType::KeepAlive {
|
||||||
self.inner.ctype
|
self.inner.conn_type
|
||||||
} else {
|
} else {
|
||||||
ctype
|
conn_type
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,9 +201,9 @@ impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
|
|||||||
.set(Flags::HEAD, head.as_ref().method == Method::HEAD);
|
.set(Flags::HEAD, head.as_ref().method == Method::HEAD);
|
||||||
|
|
||||||
// connection status
|
// connection status
|
||||||
inner.ctype = match head.as_ref().connection_type() {
|
inner.conn_type = match head.as_ref().connection_type() {
|
||||||
ConnectionType::KeepAlive => {
|
ConnectionType::KeepAlive => {
|
||||||
if inner.flags.contains(Flags::KEEPALIVE_ENABLED) {
|
if inner.flags.contains(Flags::KEEP_ALIVE_ENABLED) {
|
||||||
ConnectionType::KeepAlive
|
ConnectionType::KeepAlive
|
||||||
} else {
|
} else {
|
||||||
ConnectionType::Close
|
ConnectionType::Close
|
||||||
@@ -209,7 +220,7 @@ impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
|
|||||||
false,
|
false,
|
||||||
inner.version,
|
inner.version,
|
||||||
length,
|
length,
|
||||||
inner.ctype,
|
inner.conn_type,
|
||||||
&inner.config,
|
&inner.config,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,18 @@ use bitflags::bitflags;
|
|||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use http::{Method, Version};
|
use http::{Method, Version};
|
||||||
|
|
||||||
use super::decoder::{PayloadDecoder, PayloadItem, PayloadType};
|
use super::{
|
||||||
use super::{decoder, encoder};
|
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
|
||||||
use super::{Message, MessageType};
|
encoder, Message, MessageType,
|
||||||
use crate::body::BodySize;
|
};
|
||||||
use crate::config::ServiceConfig;
|
use crate::{
|
||||||
use crate::error::ParseError;
|
body::BodySize, error::ParseError, ConnectionType, Request, Response, ServiceConfig,
|
||||||
use crate::message::ConnectionType;
|
};
|
||||||
use crate::request::Request;
|
|
||||||
use crate::response::Response;
|
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
struct Flags: u8 {
|
struct Flags: u8 {
|
||||||
const HEAD = 0b0000_0001;
|
const HEAD = 0b0000_0001;
|
||||||
const KEEPALIVE_ENABLED = 0b0000_0010;
|
const KEEP_ALIVE_ENABLED = 0b0000_0010;
|
||||||
const STREAM = 0b0000_0100;
|
const STREAM = 0b0000_0100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +27,7 @@ pub struct Codec {
|
|||||||
decoder: decoder::MessageDecoder<Request>,
|
decoder: decoder::MessageDecoder<Request>,
|
||||||
payload: Option<PayloadDecoder>,
|
payload: Option<PayloadDecoder>,
|
||||||
version: Version,
|
version: Version,
|
||||||
ctype: ConnectionType,
|
conn_type: ConnectionType,
|
||||||
|
|
||||||
// encoder part
|
// encoder part
|
||||||
flags: Flags,
|
flags: Flags,
|
||||||
@@ -44,7 +42,9 @@ impl Default for Codec {
|
|||||||
|
|
||||||
impl fmt::Debug for Codec {
|
impl fmt::Debug for Codec {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "h1::Codec({:?})", self.flags)
|
f.debug_struct("h1::Codec")
|
||||||
|
.field("flags", &self.flags)
|
||||||
|
.finish_non_exhaustive()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,8 +53,8 @@ impl Codec {
|
|||||||
///
|
///
|
||||||
/// `keepalive_enabled` how response `connection` header get generated.
|
/// `keepalive_enabled` how response `connection` header get generated.
|
||||||
pub fn new(config: ServiceConfig) -> Self {
|
pub fn new(config: ServiceConfig) -> Self {
|
||||||
let flags = if config.keep_alive_enabled() {
|
let flags = if config.keep_alive().enabled() {
|
||||||
Flags::KEEPALIVE_ENABLED
|
Flags::KEEP_ALIVE_ENABLED
|
||||||
} else {
|
} else {
|
||||||
Flags::empty()
|
Flags::empty()
|
||||||
};
|
};
|
||||||
@@ -65,7 +65,7 @@ impl Codec {
|
|||||||
decoder: decoder::MessageDecoder::default(),
|
decoder: decoder::MessageDecoder::default(),
|
||||||
payload: None,
|
payload: None,
|
||||||
version: Version::HTTP_11,
|
version: Version::HTTP_11,
|
||||||
ctype: ConnectionType::Close,
|
conn_type: ConnectionType::Close,
|
||||||
encoder: encoder::MessageEncoder::default(),
|
encoder: encoder::MessageEncoder::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,19 +73,19 @@ impl Codec {
|
|||||||
/// Check if request is upgrade.
|
/// Check if request is upgrade.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn upgrade(&self) -> bool {
|
pub fn upgrade(&self) -> bool {
|
||||||
self.ctype == ConnectionType::Upgrade
|
self.conn_type == ConnectionType::Upgrade
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if last response is keep-alive.
|
/// Check if last response is keep-alive.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn keepalive(&self) -> bool {
|
pub fn keep_alive(&self) -> bool {
|
||||||
self.ctype == ConnectionType::KeepAlive
|
self.conn_type == ConnectionType::KeepAlive
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if keep-alive enabled on server level.
|
/// Check if keep-alive enabled on server level.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn keepalive_enabled(&self) -> bool {
|
pub fn keep_alive_enabled(&self) -> bool {
|
||||||
self.flags.contains(Flags::KEEPALIVE_ENABLED)
|
self.flags.contains(Flags::KEEP_ALIVE_ENABLED)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check last request's message type.
|
/// Check last request's message type.
|
||||||
@@ -124,11 +124,11 @@ impl Decoder for Codec {
|
|||||||
let head = req.head();
|
let head = req.head();
|
||||||
self.flags.set(Flags::HEAD, head.method == Method::HEAD);
|
self.flags.set(Flags::HEAD, head.method == Method::HEAD);
|
||||||
self.version = head.version;
|
self.version = head.version;
|
||||||
self.ctype = head.connection_type();
|
self.conn_type = head.connection_type();
|
||||||
if self.ctype == ConnectionType::KeepAlive
|
if self.conn_type == ConnectionType::KeepAlive
|
||||||
&& !self.flags.contains(Flags::KEEPALIVE_ENABLED)
|
&& !self.flags.contains(Flags::KEEP_ALIVE_ENABLED)
|
||||||
{
|
{
|
||||||
self.ctype = ConnectionType::Close
|
self.conn_type = ConnectionType::Close
|
||||||
}
|
}
|
||||||
match payload {
|
match payload {
|
||||||
PayloadType::None => self.payload = None,
|
PayloadType::None => self.payload = None,
|
||||||
@@ -159,14 +159,14 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
|
|||||||
res.head_mut().version = self.version;
|
res.head_mut().version = self.version;
|
||||||
|
|
||||||
// connection status
|
// connection status
|
||||||
self.ctype = if let Some(ct) = res.head().ctype() {
|
self.conn_type = if let Some(ct) = res.head().conn_type() {
|
||||||
if ct == ConnectionType::KeepAlive {
|
if ct == ConnectionType::KeepAlive {
|
||||||
self.ctype
|
self.conn_type
|
||||||
} else {
|
} else {
|
||||||
ct
|
ct
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.ctype
|
self.conn_type
|
||||||
};
|
};
|
||||||
|
|
||||||
// encode message
|
// encode message
|
||||||
@@ -177,18 +177,20 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
|
|||||||
self.flags.contains(Flags::STREAM),
|
self.flags.contains(Flags::STREAM),
|
||||||
self.version,
|
self.version,
|
||||||
length,
|
length,
|
||||||
self.ctype,
|
self.conn_type,
|
||||||
&self.config,
|
&self.config,
|
||||||
)?;
|
)?;
|
||||||
// self.headers_size = (dst.len() - len) as u32;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::Chunk(Some(bytes)) => {
|
Message::Chunk(Some(bytes)) => {
|
||||||
self.encoder.encode_chunk(bytes.as_ref(), dst)?;
|
self.encoder.encode_chunk(bytes.as_ref(), dst)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::Chunk(None) => {
|
Message::Chunk(None) => {
|
||||||
self.encoder.encode_eof(dst)?;
|
self.encoder.encode_eof(dst)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,7 +201,7 @@ mod tests {
|
|||||||
use http::Method;
|
use http::Method;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::HttpMessage;
|
use crate::HttpMessage as _;
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_http_request_chunked_payload_and_next_message() {
|
async fn test_http_request_chunked_payload_and_next_message() {
|
||||||
|
|||||||
@@ -2,17 +2,14 @@ use std::{convert::TryFrom, io, marker::PhantomData, mem::MaybeUninit, task::Pol
|
|||||||
|
|
||||||
use actix_codec::Decoder;
|
use actix_codec::Decoder;
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use http::header::{HeaderName, HeaderValue};
|
use http::{
|
||||||
use http::{header, Method, StatusCode, Uri, Version};
|
header::{self, HeaderName, HeaderValue},
|
||||||
|
Method, StatusCode, Uri, Version,
|
||||||
|
};
|
||||||
use log::{debug, error, trace};
|
use log::{debug, error, trace};
|
||||||
|
|
||||||
use super::chunked::ChunkedState;
|
use super::chunked::ChunkedState;
|
||||||
use crate::{
|
use crate::{error::ParseError, header::HeaderMap, ConnectionType, Request, ResponseHead};
|
||||||
error::ParseError,
|
|
||||||
header::HeaderMap,
|
|
||||||
message::{ConnectionType, ResponseHead},
|
|
||||||
request::Request,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) const MAX_BUFFER_SIZE: usize = 131_072;
|
pub(crate) const MAX_BUFFER_SIZE: usize = 131_072;
|
||||||
const MAX_HEADERS: usize = 96;
|
const MAX_HEADERS: usize = 96;
|
||||||
@@ -50,7 +47,7 @@ pub(crate) enum PayloadLength {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait MessageType: Sized {
|
pub(crate) trait MessageType: Sized {
|
||||||
fn set_connection_type(&mut self, ctype: Option<ConnectionType>);
|
fn set_connection_type(&mut self, conn_type: Option<ConnectionType>);
|
||||||
|
|
||||||
fn set_expect(&mut self);
|
fn set_expect(&mut self);
|
||||||
|
|
||||||
@@ -74,8 +71,7 @@ pub(crate) trait MessageType: Sized {
|
|||||||
let headers = self.headers_mut();
|
let headers = self.headers_mut();
|
||||||
|
|
||||||
for idx in raw_headers.iter() {
|
for idx in raw_headers.iter() {
|
||||||
let name =
|
let name = HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]).unwrap();
|
||||||
HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]).unwrap();
|
|
||||||
|
|
||||||
// SAFETY: httparse already checks header value is only visible ASCII bytes
|
// SAFETY: httparse already checks header value is only visible ASCII bytes
|
||||||
// from_maybe_shared_unchecked contains debug assertions so they are omitted here
|
// from_maybe_shared_unchecked contains debug assertions so they are omitted here
|
||||||
@@ -174,7 +170,7 @@ pub(crate) trait MessageType: Sized {
|
|||||||
self.set_expect()
|
self.set_expect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc7230#section-3.3.3
|
// https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3
|
||||||
if chunked {
|
if chunked {
|
||||||
// Chunked encoding
|
// Chunked encoding
|
||||||
Ok(PayloadLength::Payload(PayloadType::Payload(
|
Ok(PayloadLength::Payload(PayloadType::Payload(
|
||||||
@@ -194,8 +190,8 @@ pub(crate) trait MessageType: Sized {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MessageType for Request {
|
impl MessageType for Request {
|
||||||
fn set_connection_type(&mut self, ctype: Option<ConnectionType>) {
|
fn set_connection_type(&mut self, conn_type: Option<ConnectionType>) {
|
||||||
if let Some(ctype) = ctype {
|
if let Some(ctype) = conn_type {
|
||||||
self.head_mut().set_connection_type(ctype);
|
self.head_mut().set_connection_type(ctype);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,8 +275,8 @@ impl MessageType for Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MessageType for ResponseHead {
|
impl MessageType for ResponseHead {
|
||||||
fn set_connection_type(&mut self, ctype: Option<ConnectionType>) {
|
fn set_connection_type(&mut self, conn_type: Option<ConnectionType>) {
|
||||||
if let Some(ctype) = ctype {
|
if let Some(ctype) = conn_type {
|
||||||
ResponseHead::set_connection_type(self, ctype);
|
ResponseHead::set_connection_type(self, ctype);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,34 +380,36 @@ impl HeaderIndex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
/// Http payload item
|
/// Chunk type yielded while decoding a payload.
|
||||||
pub enum PayloadItem {
|
pub enum PayloadItem {
|
||||||
Chunk(Bytes),
|
Chunk(Bytes),
|
||||||
Eof,
|
Eof,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decoders to handle different Transfer-Encodings.
|
/// Decoder that can handle different payload types.
|
||||||
///
|
///
|
||||||
/// If a message body does not include a Transfer-Encoding, it *should*
|
/// If a message body does not use `Transfer-Encoding`, it should include a `Content-Length`.
|
||||||
/// include a Content-Length header.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct PayloadDecoder {
|
pub struct PayloadDecoder {
|
||||||
kind: Kind,
|
kind: Kind,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PayloadDecoder {
|
impl PayloadDecoder {
|
||||||
|
/// Constructs a fixed-length payload decoder.
|
||||||
pub fn length(x: u64) -> PayloadDecoder {
|
pub fn length(x: u64) -> PayloadDecoder {
|
||||||
PayloadDecoder {
|
PayloadDecoder {
|
||||||
kind: Kind::Length(x),
|
kind: Kind::Length(x),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs a chunked encoding decoder.
|
||||||
pub fn chunked() -> PayloadDecoder {
|
pub fn chunked() -> PayloadDecoder {
|
||||||
PayloadDecoder {
|
PayloadDecoder {
|
||||||
kind: Kind::Chunked(ChunkedState::Size, 0),
|
kind: Kind::Chunked(ChunkedState::Size, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates an decoder that yields chunks until the stream returns EOF.
|
||||||
pub fn eof() -> PayloadDecoder {
|
pub fn eof() -> PayloadDecoder {
|
||||||
PayloadDecoder { kind: Kind::Eof }
|
PayloadDecoder { kind: Kind::Eof }
|
||||||
}
|
}
|
||||||
@@ -419,25 +417,26 @@ impl PayloadDecoder {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
enum Kind {
|
enum Kind {
|
||||||
/// A Reader used when a Content-Length header is passed with a positive
|
/// A reader used when a `Content-Length` header is passed with a positive integer.
|
||||||
/// integer.
|
|
||||||
Length(u64),
|
Length(u64),
|
||||||
/// A Reader used when Transfer-Encoding is `chunked`.
|
|
||||||
|
/// A reader used when `Transfer-Encoding` is `chunked`.
|
||||||
Chunked(ChunkedState, u64),
|
Chunked(ChunkedState, u64),
|
||||||
/// A Reader used for responses that don't indicate a length or chunked.
|
|
||||||
|
/// A reader used for responses that don't indicate a length or chunked.
|
||||||
///
|
///
|
||||||
/// Note: This should only used for `Response`s. It is illegal for a
|
/// Note: This should only used for `Response`s. It is illegal for a `Request` to be made
|
||||||
/// `Request` to be made with both `Content-Length` and
|
/// without either of `Content-Length` and `Transfer-Encoding: chunked` missing, as explained
|
||||||
/// `Transfer-Encoding: chunked` missing, as explained from the spec:
|
/// in [RFC 7230 §3.3.3]:
|
||||||
///
|
///
|
||||||
/// > If a Transfer-Encoding header field is present in a response and
|
/// > If a Transfer-Encoding header field is present in a response and the chunked transfer
|
||||||
/// > the chunked transfer coding is not the final encoding, the
|
/// > coding is not the final encoding, the message body length is determined by reading the
|
||||||
/// > message body length is determined by reading the connection until
|
/// > connection until it is closed by the server. If a Transfer-Encoding header field is
|
||||||
/// > it is closed by the server. If a Transfer-Encoding header field
|
/// > present in a request and the chunked transfer coding is not the final encoding, the
|
||||||
/// > is present in a request and the chunked transfer coding is not
|
/// > message body length cannot be determined reliably; the server MUST respond with the 400
|
||||||
/// > the final encoding, the message body length cannot be determined
|
/// > (Bad Request) status code and then close the connection.
|
||||||
/// > reliably; the server MUST respond with the 400 (Bad Request)
|
///
|
||||||
/// > status code and then close the connection.
|
/// [RFC 7230 §3.3.3]: https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3
|
||||||
Eof,
|
Eof,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,6 +466,7 @@ impl Decoder for PayloadDecoder {
|
|||||||
Ok(Some(PayloadItem::Chunk(buf)))
|
Ok(Some(PayloadItem::Chunk(buf)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Kind::Chunked(ref mut state, ref mut size) => {
|
Kind::Chunked(ref mut state, ref mut size) => {
|
||||||
loop {
|
loop {
|
||||||
let mut buf = None;
|
let mut buf = None;
|
||||||
@@ -492,6 +492,7 @@ impl Decoder for PayloadDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Kind::Eof => {
|
Kind::Eof => {
|
||||||
if src.is_empty() {
|
if src.is_empty() {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -511,7 +512,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
error::ParseError,
|
error::ParseError,
|
||||||
http::header::{HeaderName, SET_COOKIE},
|
header::{HeaderName, SET_COOKIE},
|
||||||
HttpMessage as _,
|
HttpMessage as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -605,8 +606,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_body() {
|
fn test_parse_body() {
|
||||||
let mut buf =
|
let mut buf = BytesMut::from("GET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody");
|
||||||
BytesMut::from("GET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody");
|
|
||||||
|
|
||||||
let mut reader = MessageDecoder::<Request>::default();
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
@@ -622,8 +622,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_body_crlf() {
|
fn test_parse_body_crlf() {
|
||||||
let mut buf =
|
let mut buf = BytesMut::from("\r\nGET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody");
|
||||||
BytesMut::from("\r\nGET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody");
|
|
||||||
|
|
||||||
let mut reader = MessageDecoder::<Request>::default();
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
773
actix-http/src/h1/dispatcher_tests.rs
Normal file
773
actix-http/src/h1/dispatcher_tests.rs
Normal file
@@ -0,0 +1,773 @@
|
|||||||
|
use std::{future::Future, str, task::Poll, time::Duration};
|
||||||
|
|
||||||
|
use actix_rt::time::sleep;
|
||||||
|
use actix_service::fn_service;
|
||||||
|
use actix_utils::future::{ready, Ready};
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures_util::future::lazy;
|
||||||
|
|
||||||
|
use actix_codec::Framed;
|
||||||
|
use actix_service::Service;
|
||||||
|
use bytes::{Buf, BytesMut};
|
||||||
|
|
||||||
|
use super::dispatcher::{Dispatcher, DispatcherState, DispatcherStateProj, Flags};
|
||||||
|
use crate::{
|
||||||
|
body::MessageBody,
|
||||||
|
config::ServiceConfig,
|
||||||
|
h1::{Codec, ExpectHandler, UpgradeHandler},
|
||||||
|
service::HttpFlow,
|
||||||
|
test::{TestBuffer, TestSeqBuffer},
|
||||||
|
Error, HttpMessage, KeepAlive, Method, OnConnectData, Request, Response, StatusCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
|
||||||
|
memchr::memmem::find(&haystack[from..], needle)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stabilize_date_header(payload: &mut [u8]) {
|
||||||
|
let mut from = 0;
|
||||||
|
while let Some(pos) = find_slice(payload, b"date", from) {
|
||||||
|
payload[(from + pos)..(from + pos + 35)]
|
||||||
|
.copy_from_slice(b"date: Thu, 01 Jan 1970 12:34:56 UTC");
|
||||||
|
from += 35;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ok_service() -> impl Service<Request, Response = Response<impl MessageBody>, Error = Error> {
|
||||||
|
status_service(StatusCode::OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status_service(
|
||||||
|
status: StatusCode,
|
||||||
|
) -> impl Service<Request, Response = Response<impl MessageBody>, Error = Error> {
|
||||||
|
fn_service(move |_req: Request| ready(Ok::<_, Error>(Response::new(status))))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn echo_path_service(
|
||||||
|
) -> impl Service<Request, Response = Response<impl MessageBody>, Error = Error> {
|
||||||
|
fn_service(|req: Request| {
|
||||||
|
let path = req.path().as_bytes();
|
||||||
|
ready(Ok::<_, Error>(
|
||||||
|
Response::ok().set_body(Bytes::copy_from_slice(path)),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn echo_payload_service() -> impl Service<Request, Response = Response<Bytes>, Error = Error> {
|
||||||
|
fn_service(|mut req: Request| {
|
||||||
|
Box::pin(async move {
|
||||||
|
use futures_util::stream::StreamExt as _;
|
||||||
|
|
||||||
|
let mut pl = req.take_payload();
|
||||||
|
let mut body = BytesMut::new();
|
||||||
|
while let Some(chunk) = pl.next().await {
|
||||||
|
body.extend_from_slice(chunk.unwrap().chunk())
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<_, Error>(Response::ok().set_body(body.freeze()))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn late_request() {
|
||||||
|
let mut buf = TestBuffer::empty();
|
||||||
|
|
||||||
|
let cfg = ServiceConfig::new(
|
||||||
|
KeepAlive::Disabled,
|
||||||
|
Duration::from_millis(100),
|
||||||
|
Duration::ZERO,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let services = HttpFlow::new(ok_service(), ExpectHandler, None);
|
||||||
|
|
||||||
|
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||||
|
buf.clone(),
|
||||||
|
services,
|
||||||
|
cfg,
|
||||||
|
None,
|
||||||
|
OnConnectData::default(),
|
||||||
|
);
|
||||||
|
actix_rt::pin!(h1);
|
||||||
|
|
||||||
|
lazy(|cx| {
|
||||||
|
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||||
|
|
||||||
|
match h1.as_mut().poll(cx) {
|
||||||
|
Poll::Ready(_) => panic!("first poll should not be ready"),
|
||||||
|
Poll::Pending => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// polls: initial
|
||||||
|
assert_eq!(h1.poll_count, 1);
|
||||||
|
|
||||||
|
buf.extend_read_buf("GET /abcd HTTP/1.1\r\nConnection: close\r\n\r\n");
|
||||||
|
|
||||||
|
match h1.as_mut().poll(cx) {
|
||||||
|
Poll::Pending => panic!("second poll should not be pending"),
|
||||||
|
Poll::Ready(res) => assert!(res.is_ok()),
|
||||||
|
}
|
||||||
|
|
||||||
|
// polls: initial pending => handle req => shutdown
|
||||||
|
assert_eq!(h1.poll_count, 3);
|
||||||
|
|
||||||
|
let mut res = buf.take_write_buf().to_vec();
|
||||||
|
stabilize_date_header(&mut res);
|
||||||
|
let res = &res[..];
|
||||||
|
|
||||||
|
let exp = b"\
|
||||||
|
HTTP/1.1 200 OK\r\n\
|
||||||
|
content-length: 0\r\n\
|
||||||
|
connection: close\r\n\
|
||||||
|
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||||
|
";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
exp,
|
||||||
|
"\nexpected response not in write buffer:\n\
|
||||||
|
response: {:?}\n\
|
||||||
|
expected: {:?}",
|
||||||
|
String::from_utf8_lossy(res),
|
||||||
|
String::from_utf8_lossy(exp)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn oneshot_connection() {
|
||||||
|
let buf = TestBuffer::new("GET /abcd HTTP/1.1\r\n\r\n");
|
||||||
|
|
||||||
|
let cfg = ServiceConfig::new(
|
||||||
|
KeepAlive::Disabled,
|
||||||
|
Duration::from_millis(100),
|
||||||
|
Duration::ZERO,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||||
|
|
||||||
|
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||||
|
buf.clone(),
|
||||||
|
services,
|
||||||
|
cfg,
|
||||||
|
None,
|
||||||
|
OnConnectData::default(),
|
||||||
|
);
|
||||||
|
actix_rt::pin!(h1);
|
||||||
|
|
||||||
|
lazy(|cx| {
|
||||||
|
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||||
|
|
||||||
|
match h1.as_mut().poll(cx) {
|
||||||
|
Poll::Pending => panic!("first poll should not be pending"),
|
||||||
|
Poll::Ready(res) => assert!(res.is_ok()),
|
||||||
|
}
|
||||||
|
|
||||||
|
// polls: initial => shutdown
|
||||||
|
assert_eq!(h1.poll_count, 2);
|
||||||
|
|
||||||
|
let mut res = buf.take_write_buf().to_vec();
|
||||||
|
stabilize_date_header(&mut res);
|
||||||
|
let res = &res[..];
|
||||||
|
|
||||||
|
let exp = b"\
|
||||||
|
HTTP/1.1 200 OK\r\n\
|
||||||
|
content-length: 5\r\n\
|
||||||
|
connection: close\r\n\
|
||||||
|
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||||
|
/abcd\
|
||||||
|
";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
exp,
|
||||||
|
"\nexpected response not in write buffer:\n\
|
||||||
|
response: {:?}\n\
|
||||||
|
expected: {:?}",
|
||||||
|
String::from_utf8_lossy(res),
|
||||||
|
String::from_utf8_lossy(exp)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn keep_alive_timeout() {
|
||||||
|
let buf = TestBuffer::new("GET /abcd HTTP/1.1\r\n\r\n");
|
||||||
|
|
||||||
|
let cfg = ServiceConfig::new(
|
||||||
|
KeepAlive::Timeout(Duration::from_millis(200)),
|
||||||
|
Duration::from_millis(100),
|
||||||
|
Duration::ZERO,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||||
|
|
||||||
|
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||||
|
buf.clone(),
|
||||||
|
services,
|
||||||
|
cfg,
|
||||||
|
None,
|
||||||
|
OnConnectData::default(),
|
||||||
|
);
|
||||||
|
actix_rt::pin!(h1);
|
||||||
|
|
||||||
|
lazy(|cx| {
|
||||||
|
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
h1.as_mut().poll(cx).is_pending(),
|
||||||
|
"keep-alive should prevent poll from resolving"
|
||||||
|
);
|
||||||
|
|
||||||
|
// polls: initial
|
||||||
|
assert_eq!(h1.poll_count, 1);
|
||||||
|
|
||||||
|
let mut res = buf.take_write_buf().to_vec();
|
||||||
|
stabilize_date_header(&mut res);
|
||||||
|
let res = &res[..];
|
||||||
|
|
||||||
|
let exp = b"\
|
||||||
|
HTTP/1.1 200 OK\r\n\
|
||||||
|
content-length: 5\r\n\
|
||||||
|
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||||
|
/abcd\
|
||||||
|
";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
exp,
|
||||||
|
"\nexpected response not in write buffer:\n\
|
||||||
|
response: {:?}\n\
|
||||||
|
expected: {:?}",
|
||||||
|
String::from_utf8_lossy(res),
|
||||||
|
String::from_utf8_lossy(exp)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// sleep slightly longer than keep-alive timeout
|
||||||
|
sleep(Duration::from_millis(250)).await;
|
||||||
|
|
||||||
|
lazy(|cx| {
|
||||||
|
assert!(
|
||||||
|
h1.as_mut().poll(cx).is_ready(),
|
||||||
|
"keep-alive should have resolved",
|
||||||
|
);
|
||||||
|
|
||||||
|
// polls: initial => keep-alive wake-up shutdown
|
||||||
|
assert_eq!(h1.poll_count, 2);
|
||||||
|
|
||||||
|
if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() {
|
||||||
|
// connection closed
|
||||||
|
assert!(inner.flags.contains(Flags::SHUTDOWN));
|
||||||
|
assert!(inner.flags.contains(Flags::WRITE_DISCONNECT));
|
||||||
|
// and nothing added to write buffer
|
||||||
|
assert!(buf.write_buf_slice().is_empty());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn keep_alive_follow_up_req() {
|
||||||
|
let mut buf = TestBuffer::new("GET /abcd HTTP/1.1\r\n\r\n");
|
||||||
|
|
||||||
|
let cfg = ServiceConfig::new(
|
||||||
|
KeepAlive::Timeout(Duration::from_millis(500)),
|
||||||
|
Duration::from_millis(100),
|
||||||
|
Duration::ZERO,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||||
|
|
||||||
|
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||||
|
buf.clone(),
|
||||||
|
services,
|
||||||
|
cfg,
|
||||||
|
None,
|
||||||
|
OnConnectData::default(),
|
||||||
|
);
|
||||||
|
actix_rt::pin!(h1);
|
||||||
|
|
||||||
|
lazy(|cx| {
|
||||||
|
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
h1.as_mut().poll(cx).is_pending(),
|
||||||
|
"keep-alive should prevent poll from resolving"
|
||||||
|
);
|
||||||
|
|
||||||
|
// polls: initial
|
||||||
|
assert_eq!(h1.poll_count, 1);
|
||||||
|
|
||||||
|
let mut res = buf.take_write_buf().to_vec();
|
||||||
|
stabilize_date_header(&mut res);
|
||||||
|
let res = &res[..];
|
||||||
|
|
||||||
|
let exp = b"\
|
||||||
|
HTTP/1.1 200 OK\r\n\
|
||||||
|
content-length: 5\r\n\
|
||||||
|
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||||
|
/abcd\
|
||||||
|
";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
exp,
|
||||||
|
"\nexpected response not in write buffer:\n\
|
||||||
|
response: {:?}\n\
|
||||||
|
expected: {:?}",
|
||||||
|
String::from_utf8_lossy(res),
|
||||||
|
String::from_utf8_lossy(exp)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// sleep for less than KA timeout
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
|
||||||
|
lazy(|cx| {
|
||||||
|
assert!(
|
||||||
|
h1.as_mut().poll(cx).is_pending(),
|
||||||
|
"keep-alive should not have resolved dispatcher yet",
|
||||||
|
);
|
||||||
|
|
||||||
|
// polls: initial => manual
|
||||||
|
assert_eq!(h1.poll_count, 2);
|
||||||
|
|
||||||
|
if let DispatcherStateProj::Normal { inner } = h1.as_mut().project().inner.project() {
|
||||||
|
// connection not closed
|
||||||
|
assert!(!inner.flags.contains(Flags::SHUTDOWN));
|
||||||
|
assert!(!inner.flags.contains(Flags::WRITE_DISCONNECT));
|
||||||
|
// and nothing added to write buffer
|
||||||
|
assert!(buf.write_buf_slice().is_empty());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
lazy(|cx| {
|
||||||
|
buf.extend_read_buf(
|
||||||
|
"\
|
||||||
|
GET /efg HTTP/1.1\r\n\
|
||||||
|
Connection: close\r\n\
|
||||||
|
\r\n\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
h1.as_mut().poll(cx).is_ready(),
|
||||||
|
"connection close header should override keep-alive setting",
|
||||||
|
);
|
||||||
|
|
||||||
|
// polls: initial => manual => follow-up req => shutdown
|
||||||
|
assert_eq!(h1.poll_count, 4);
|
||||||
|
|
||||||
|
if let DispatcherStateProj::Normal { inner } = h1.as_mut().project().inner.project() {
|
||||||
|
// connection closed
|
||||||
|
assert!(inner.flags.contains(Flags::SHUTDOWN));
|
||||||
|
assert!(!inner.flags.contains(Flags::WRITE_DISCONNECT));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut res = buf.take_write_buf().to_vec();
|
||||||
|
stabilize_date_header(&mut res);
|
||||||
|
let res = &res[..];
|
||||||
|
|
||||||
|
let exp = b"\
|
||||||
|
HTTP/1.1 200 OK\r\n\
|
||||||
|
content-length: 4\r\n\
|
||||||
|
connection: close\r\n\
|
||||||
|
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||||
|
/efg\
|
||||||
|
";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
exp,
|
||||||
|
"\nexpected response not in write buffer:\n\
|
||||||
|
response: {:?}\n\
|
||||||
|
expected: {:?}",
|
||||||
|
String::from_utf8_lossy(res),
|
||||||
|
String::from_utf8_lossy(exp)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn req_parse_err() {
|
||||||
|
lazy(|cx| {
|
||||||
|
let buf = TestBuffer::new("GET /test HTTP/1\r\n\r\n");
|
||||||
|
|
||||||
|
let services = HttpFlow::new(ok_service(), ExpectHandler, None);
|
||||||
|
|
||||||
|
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||||
|
buf.clone(),
|
||||||
|
services,
|
||||||
|
ServiceConfig::default(),
|
||||||
|
None,
|
||||||
|
OnConnectData::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
actix_rt::pin!(h1);
|
||||||
|
|
||||||
|
match h1.as_mut().poll(cx) {
|
||||||
|
Poll::Pending => panic!(),
|
||||||
|
Poll::Ready(res) => assert!(res.is_err()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() {
|
||||||
|
assert!(inner.flags.contains(Flags::READ_DISCONNECT));
|
||||||
|
assert_eq!(
|
||||||
|
&buf.write_buf_slice()[..26],
|
||||||
|
b"HTTP/1.1 400 Bad Request\r\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn pipelining_ok_then_ok() {
|
||||||
|
lazy(|cx| {
|
||||||
|
let buf = TestBuffer::new(
|
||||||
|
"\
|
||||||
|
GET /abcd HTTP/1.1\r\n\r\n\
|
||||||
|
GET /def HTTP/1.1\r\n\r\n\
|
||||||
|
",
|
||||||
|
);
|
||||||
|
|
||||||
|
let cfg = ServiceConfig::new(
|
||||||
|
KeepAlive::Disabled,
|
||||||
|
Duration::from_millis(1),
|
||||||
|
Duration::from_millis(1),
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||||
|
|
||||||
|
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||||
|
buf.clone(),
|
||||||
|
services,
|
||||||
|
cfg,
|
||||||
|
None,
|
||||||
|
OnConnectData::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
actix_rt::pin!(h1);
|
||||||
|
|
||||||
|
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||||
|
|
||||||
|
match h1.as_mut().poll(cx) {
|
||||||
|
Poll::Pending => panic!("first poll should not be pending"),
|
||||||
|
Poll::Ready(res) => assert!(res.is_ok()),
|
||||||
|
}
|
||||||
|
|
||||||
|
// polls: initial => shutdown
|
||||||
|
assert_eq!(h1.poll_count, 2);
|
||||||
|
|
||||||
|
let mut res = buf.write_buf_slice_mut();
|
||||||
|
stabilize_date_header(&mut res);
|
||||||
|
let res = &res[..];
|
||||||
|
|
||||||
|
let exp = b"\
|
||||||
|
HTTP/1.1 200 OK\r\n\
|
||||||
|
content-length: 5\r\n\
|
||||||
|
connection: close\r\n\
|
||||||
|
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||||
|
/abcd\
|
||||||
|
HTTP/1.1 200 OK\r\n\
|
||||||
|
content-length: 4\r\n\
|
||||||
|
connection: close\r\n\
|
||||||
|
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||||
|
/def\
|
||||||
|
";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
exp,
|
||||||
|
"\nexpected response not in write buffer:\n\
|
||||||
|
response: {:?}\n\
|
||||||
|
expected: {:?}",
|
||||||
|
String::from_utf8_lossy(res),
|
||||||
|
String::from_utf8_lossy(exp)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn pipelining_ok_then_bad() {
|
||||||
|
lazy(|cx| {
|
||||||
|
let buf = TestBuffer::new(
|
||||||
|
"\
|
||||||
|
GET /abcd HTTP/1.1\r\n\r\n\
|
||||||
|
GET /def HTTP/1\r\n\r\n\
|
||||||
|
",
|
||||||
|
);
|
||||||
|
|
||||||
|
let cfg = ServiceConfig::new(
|
||||||
|
KeepAlive::Disabled,
|
||||||
|
Duration::from_millis(1),
|
||||||
|
Duration::from_millis(1),
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||||
|
|
||||||
|
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||||
|
buf.clone(),
|
||||||
|
services,
|
||||||
|
cfg,
|
||||||
|
None,
|
||||||
|
OnConnectData::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
actix_rt::pin!(h1);
|
||||||
|
|
||||||
|
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||||
|
|
||||||
|
match h1.as_mut().poll(cx) {
|
||||||
|
Poll::Pending => panic!("first poll should not be pending"),
|
||||||
|
Poll::Ready(res) => assert!(res.is_err()),
|
||||||
|
}
|
||||||
|
|
||||||
|
// polls: initial => shutdown
|
||||||
|
assert_eq!(h1.poll_count, 1);
|
||||||
|
|
||||||
|
let mut res = buf.write_buf_slice_mut();
|
||||||
|
stabilize_date_header(&mut res);
|
||||||
|
let res = &res[..];
|
||||||
|
|
||||||
|
let exp = b"\
|
||||||
|
HTTP/1.1 200 OK\r\n\
|
||||||
|
content-length: 5\r\n\
|
||||||
|
connection: close\r\n\
|
||||||
|
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||||
|
/abcd\
|
||||||
|
HTTP/1.1 400 Bad Request\r\n\
|
||||||
|
content-length: 0\r\n\
|
||||||
|
connection: close\r\n\
|
||||||
|
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
|
||||||
|
";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
exp,
|
||||||
|
"\nexpected response not in write buffer:\n\
|
||||||
|
response: {:?}\n\
|
||||||
|
expected: {:?}",
|
||||||
|
String::from_utf8_lossy(res),
|
||||||
|
String::from_utf8_lossy(exp)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn expect_handling() {
|
||||||
|
lazy(|cx| {
|
||||||
|
let mut buf = TestSeqBuffer::empty();
|
||||||
|
let cfg = ServiceConfig::new(
|
||||||
|
KeepAlive::Disabled,
|
||||||
|
Duration::ZERO,
|
||||||
|
Duration::ZERO,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let services = HttpFlow::new(echo_payload_service(), ExpectHandler, None);
|
||||||
|
|
||||||
|
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||||
|
buf.clone(),
|
||||||
|
services,
|
||||||
|
cfg,
|
||||||
|
None,
|
||||||
|
OnConnectData::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
buf.extend_read_buf(
|
||||||
|
"\
|
||||||
|
POST /upload HTTP/1.1\r\n\
|
||||||
|
Content-Length: 5\r\n\
|
||||||
|
Expect: 100-continue\r\n\
|
||||||
|
\r\n\
|
||||||
|
",
|
||||||
|
);
|
||||||
|
|
||||||
|
actix_rt::pin!(h1);
|
||||||
|
|
||||||
|
assert!(h1.as_mut().poll(cx).is_pending());
|
||||||
|
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||||
|
|
||||||
|
// polls: manual
|
||||||
|
assert_eq!(h1.poll_count, 1);
|
||||||
|
|
||||||
|
if let DispatcherState::Normal { ref inner } = h1.inner {
|
||||||
|
let io = inner.io.as_ref().unwrap();
|
||||||
|
let res = &io.write_buf()[..];
|
||||||
|
assert_eq!(
|
||||||
|
str::from_utf8(res).unwrap(),
|
||||||
|
"HTTP/1.1 100 Continue\r\n\r\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.extend_read_buf("12345");
|
||||||
|
assert!(h1.as_mut().poll(cx).is_ready());
|
||||||
|
|
||||||
|
// polls: manual manual shutdown
|
||||||
|
assert_eq!(h1.poll_count, 3);
|
||||||
|
|
||||||
|
if let DispatcherState::Normal { ref inner } = h1.inner {
|
||||||
|
let io = inner.io.as_ref().unwrap();
|
||||||
|
let mut res = (&io.write_buf()[..]).to_owned();
|
||||||
|
stabilize_date_header(&mut res);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
str::from_utf8(&res).unwrap(),
|
||||||
|
"\
|
||||||
|
HTTP/1.1 100 Continue\r\n\
|
||||||
|
\r\n\
|
||||||
|
HTTP/1.1 200 OK\r\n\
|
||||||
|
content-length: 5\r\n\
|
||||||
|
connection: close\r\n\
|
||||||
|
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\
|
||||||
|
\r\n\
|
||||||
|
12345\
|
||||||
|
"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn expect_eager() {
|
||||||
|
lazy(|cx| {
|
||||||
|
let mut buf = TestSeqBuffer::empty();
|
||||||
|
let cfg = ServiceConfig::new(
|
||||||
|
KeepAlive::Disabled,
|
||||||
|
Duration::ZERO,
|
||||||
|
Duration::ZERO,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let services = HttpFlow::new(echo_path_service(), ExpectHandler, None);
|
||||||
|
|
||||||
|
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new(
|
||||||
|
buf.clone(),
|
||||||
|
services,
|
||||||
|
cfg,
|
||||||
|
None,
|
||||||
|
OnConnectData::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
buf.extend_read_buf(
|
||||||
|
"\
|
||||||
|
POST /upload HTTP/1.1\r\n\
|
||||||
|
Content-Length: 5\r\n\
|
||||||
|
Expect: 100-continue\r\n\
|
||||||
|
\r\n\
|
||||||
|
",
|
||||||
|
);
|
||||||
|
|
||||||
|
actix_rt::pin!(h1);
|
||||||
|
|
||||||
|
assert!(h1.as_mut().poll(cx).is_ready());
|
||||||
|
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||||
|
|
||||||
|
// polls: manual shutdown
|
||||||
|
assert_eq!(h1.poll_count, 2);
|
||||||
|
|
||||||
|
if let DispatcherState::Normal { ref inner } = h1.inner {
|
||||||
|
let io = inner.io.as_ref().unwrap();
|
||||||
|
let mut res = (&io.write_buf()[..]).to_owned();
|
||||||
|
stabilize_date_header(&mut res);
|
||||||
|
|
||||||
|
// Despite the content-length header and even though the request payload has not
|
||||||
|
// been sent, this test expects a complete service response since the payload
|
||||||
|
// is not used at all. The service passed to dispatcher is path echo and doesn't
|
||||||
|
// consume payload bytes.
|
||||||
|
assert_eq!(
|
||||||
|
str::from_utf8(&res).unwrap(),
|
||||||
|
"\
|
||||||
|
HTTP/1.1 100 Continue\r\n\
|
||||||
|
\r\n\
|
||||||
|
HTTP/1.1 200 OK\r\n\
|
||||||
|
content-length: 7\r\n\
|
||||||
|
connection: close\r\n\
|
||||||
|
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\
|
||||||
|
\r\n\
|
||||||
|
/upload\
|
||||||
|
"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn upgrade_handling() {
|
||||||
|
struct TestUpgrade;
|
||||||
|
|
||||||
|
impl<T> Service<(Request, Framed<T, Codec>)> for TestUpgrade {
|
||||||
|
type Response = ();
|
||||||
|
type Error = Error;
|
||||||
|
type Future = Ready<Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
|
actix_service::always_ready!();
|
||||||
|
|
||||||
|
fn call(&self, (req, _framed): (Request, Framed<T, Codec>)) -> Self::Future {
|
||||||
|
assert_eq!(req.method(), Method::GET);
|
||||||
|
assert!(req.upgrade());
|
||||||
|
assert_eq!(req.headers().get("upgrade").unwrap(), "websocket");
|
||||||
|
ready(Ok(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy(|cx| {
|
||||||
|
let mut buf = TestSeqBuffer::empty();
|
||||||
|
let cfg = ServiceConfig::new(
|
||||||
|
KeepAlive::Disabled,
|
||||||
|
Duration::ZERO,
|
||||||
|
Duration::ZERO,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let services = HttpFlow::new(ok_service(), ExpectHandler, Some(TestUpgrade));
|
||||||
|
|
||||||
|
let h1 = Dispatcher::<_, _, _, _, TestUpgrade>::new(
|
||||||
|
buf.clone(),
|
||||||
|
services,
|
||||||
|
cfg,
|
||||||
|
None,
|
||||||
|
OnConnectData::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
buf.extend_read_buf(
|
||||||
|
"\
|
||||||
|
GET /ws HTTP/1.1\r\n\
|
||||||
|
Connection: Upgrade\r\n\
|
||||||
|
Upgrade: websocket\r\n\
|
||||||
|
\r\n\
|
||||||
|
",
|
||||||
|
);
|
||||||
|
|
||||||
|
actix_rt::pin!(h1);
|
||||||
|
|
||||||
|
assert!(h1.as_mut().poll(cx).is_ready());
|
||||||
|
assert!(matches!(&h1.inner, DispatcherState::Upgrade { .. }));
|
||||||
|
|
||||||
|
// polls: manual shutdown
|
||||||
|
assert_eq!(h1.poll_count, 2);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
@@ -1,25 +1,26 @@
|
|||||||
use std::io::Write;
|
use std::{
|
||||||
use std::marker::PhantomData;
|
cmp,
|
||||||
use std::ptr::copy_nonoverlapping;
|
io::{self, Write as _},
|
||||||
use std::slice::from_raw_parts_mut;
|
marker::PhantomData,
|
||||||
use std::{cmp, io};
|
ptr::copy_nonoverlapping,
|
||||||
|
slice::from_raw_parts_mut,
|
||||||
|
};
|
||||||
|
|
||||||
use bytes::{BufMut, BytesMut};
|
use bytes::{BufMut, BytesMut};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::BodySize,
|
body::BodySize,
|
||||||
config::ServiceConfig,
|
header::{
|
||||||
header::{map::Value, HeaderMap, HeaderName},
|
map::Value, HeaderMap, HeaderName, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING,
|
||||||
header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING},
|
},
|
||||||
helpers,
|
helpers, ConnectionType, RequestHeadType, Response, ServiceConfig, StatusCode, Version,
|
||||||
message::{ConnectionType, RequestHeadType},
|
|
||||||
Response, StatusCode, Version,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const AVERAGE_HEADER_SIZE: usize = 30;
|
const AVERAGE_HEADER_SIZE: usize = 30;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct MessageEncoder<T: MessageType> {
|
pub(crate) struct MessageEncoder<T: MessageType> {
|
||||||
|
#[allow(dead_code)]
|
||||||
pub length: BodySize,
|
pub length: BodySize,
|
||||||
pub te: TransferEncoding,
|
pub te: TransferEncoding,
|
||||||
_phantom: PhantomData<T>,
|
_phantom: PhantomData<T>,
|
||||||
@@ -55,7 +56,7 @@ pub(crate) trait MessageType: Sized {
|
|||||||
dst: &mut BytesMut,
|
dst: &mut BytesMut,
|
||||||
version: Version,
|
version: Version,
|
||||||
mut length: BodySize,
|
mut length: BodySize,
|
||||||
ctype: ConnectionType,
|
conn_type: ConnectionType,
|
||||||
config: &ServiceConfig,
|
config: &ServiceConfig,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let chunked = self.chunked();
|
let chunked = self.chunked();
|
||||||
@@ -70,14 +71,24 @@ pub(crate) trait MessageType: Sized {
|
|||||||
| StatusCode::PROCESSING
|
| StatusCode::PROCESSING
|
||||||
| StatusCode::NO_CONTENT => {
|
| StatusCode::NO_CONTENT => {
|
||||||
// skip content-length and transfer-encoding headers
|
// skip content-length and transfer-encoding headers
|
||||||
// See https://tools.ietf.org/html/rfc7230#section-3.3.1
|
// see https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.1
|
||||||
// and https://tools.ietf.org/html/rfc7230#section-3.3.2
|
// and https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
|
||||||
skip_len = true;
|
skip_len = true;
|
||||||
length = BodySize::None
|
length = BodySize::None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StatusCode::NOT_MODIFIED => {
|
||||||
|
// 304 responses should never have a body but should retain a manually set
|
||||||
|
// content-length header
|
||||||
|
// see https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
|
||||||
|
skip_len = false;
|
||||||
|
length = BodySize::None;
|
||||||
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match length {
|
match length {
|
||||||
BodySize::Stream => {
|
BodySize::Stream => {
|
||||||
if chunked {
|
if chunked {
|
||||||
@@ -92,19 +103,14 @@ pub(crate) trait MessageType: Sized {
|
|||||||
dst.put_slice(b"\r\n");
|
dst.put_slice(b"\r\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BodySize::Empty => {
|
BodySize::Sized(0) if camel_case => dst.put_slice(b"\r\nContent-Length: 0\r\n"),
|
||||||
if camel_case {
|
BodySize::Sized(0) => dst.put_slice(b"\r\ncontent-length: 0\r\n"),
|
||||||
dst.put_slice(b"\r\nContent-Length: 0\r\n");
|
BodySize::Sized(len) => helpers::write_content_length(len, dst, camel_case),
|
||||||
} else {
|
|
||||||
dst.put_slice(b"\r\ncontent-length: 0\r\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BodySize::Sized(len) => helpers::write_content_length(len, dst),
|
|
||||||
BodySize::None => dst.put_slice(b"\r\n"),
|
BodySize::None => dst.put_slice(b"\r\n"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection
|
// Connection
|
||||||
match ctype {
|
match conn_type {
|
||||||
ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"),
|
ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"),
|
||||||
ConnectionType::KeepAlive if version < Version::HTTP_11 => {
|
ConnectionType::KeepAlive if version < Version::HTTP_11 => {
|
||||||
if camel_case {
|
if camel_case {
|
||||||
@@ -146,7 +152,6 @@ pub(crate) trait MessageType: Sized {
|
|||||||
let k = key.as_str().as_bytes();
|
let k = key.as_str().as_bytes();
|
||||||
let k_len = k.len();
|
let k_len = k.len();
|
||||||
|
|
||||||
// TODO: drain?
|
|
||||||
for val in value.iter() {
|
for val in value.iter() {
|
||||||
let v = val.as_ref();
|
let v = val.as_ref();
|
||||||
let v_len = v.len();
|
let v_len = v.len();
|
||||||
@@ -207,7 +212,7 @@ pub(crate) trait MessageType: Sized {
|
|||||||
|
|
||||||
// optimized date header, set_date writes \r\n
|
// optimized date header, set_date writes \r\n
|
||||||
if !has_date {
|
if !has_date {
|
||||||
config.set_date(dst);
|
config.write_date_header(dst, camel_case);
|
||||||
} else {
|
} else {
|
||||||
// msg eof
|
// msg eof
|
||||||
dst.extend_from_slice(b"\r\n");
|
dst.extend_from_slice(b"\r\n");
|
||||||
@@ -252,6 +257,12 @@ impl MessageType for Response<()> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn camel_case(&self) -> bool {
|
||||||
|
self.head()
|
||||||
|
.flags
|
||||||
|
.contains(crate::message::Flags::CAMEL_CASE)
|
||||||
|
}
|
||||||
|
|
||||||
fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> {
|
fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> {
|
||||||
let head = self.head();
|
let head = self.head();
|
||||||
let reason = head.reason().as_bytes();
|
let reason = head.reason().as_bytes();
|
||||||
@@ -299,11 +310,7 @@ impl MessageType for RequestHeadType {
|
|||||||
Version::HTTP_11 => "HTTP/1.1",
|
Version::HTTP_11 => "HTTP/1.1",
|
||||||
Version::HTTP_2 => "HTTP/2.0",
|
Version::HTTP_2 => "HTTP/2.0",
|
||||||
Version::HTTP_3 => "HTTP/3.0",
|
Version::HTTP_3 => "HTTP/3.0",
|
||||||
_ =>
|
_ => return Err(io::Error::new(io::ErrorKind::Other, "unsupported version")),
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::Other,
|
|
||||||
"unsupported version"
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
||||||
@@ -311,16 +318,17 @@ impl MessageType for RequestHeadType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<T: MessageType> MessageEncoder<T> {
|
impl<T: MessageType> MessageEncoder<T> {
|
||||||
/// Encode message
|
/// Encode chunk.
|
||||||
pub fn encode_chunk(&mut self, msg: &[u8], buf: &mut BytesMut) -> io::Result<bool> {
|
pub fn encode_chunk(&mut self, msg: &[u8], buf: &mut BytesMut) -> io::Result<bool> {
|
||||||
self.te.encode(msg, buf)
|
self.te.encode(msg, buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode eof
|
/// Encode EOF.
|
||||||
pub fn encode_eof(&mut self, buf: &mut BytesMut) -> io::Result<()> {
|
pub fn encode_eof(&mut self, buf: &mut BytesMut) -> io::Result<()> {
|
||||||
self.te.encode_eof(buf)
|
self.te.encode_eof(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encode message.
|
||||||
pub fn encode(
|
pub fn encode(
|
||||||
&mut self,
|
&mut self,
|
||||||
dst: &mut BytesMut,
|
dst: &mut BytesMut,
|
||||||
@@ -329,13 +337,13 @@ impl<T: MessageType> MessageEncoder<T> {
|
|||||||
stream: bool,
|
stream: bool,
|
||||||
version: Version,
|
version: Version,
|
||||||
length: BodySize,
|
length: BodySize,
|
||||||
ctype: ConnectionType,
|
conn_type: ConnectionType,
|
||||||
config: &ServiceConfig,
|
config: &ServiceConfig,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
// transfer encoding
|
// transfer encoding
|
||||||
if !head {
|
if !head {
|
||||||
self.te = match length {
|
self.te = match length {
|
||||||
BodySize::Empty => TransferEncoding::empty(),
|
BodySize::Sized(0) => TransferEncoding::empty(),
|
||||||
BodySize::Sized(len) => TransferEncoding::length(len),
|
BodySize::Sized(len) => TransferEncoding::length(len),
|
||||||
BodySize::Stream => {
|
BodySize::Stream => {
|
||||||
if message.chunked() && !stream {
|
if message.chunked() && !stream {
|
||||||
@@ -351,7 +359,7 @@ impl<T: MessageType> MessageEncoder<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message.encode_status(dst)?;
|
message.encode_status(dst)?;
|
||||||
message.encode_headers(dst, version, length, ctype, config)
|
message.encode_headers(dst, version, length, conn_type, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,10 +373,12 @@ pub(crate) struct TransferEncoding {
|
|||||||
enum TransferEncodingKind {
|
enum TransferEncodingKind {
|
||||||
/// An Encoder for when Transfer-Encoding includes `chunked`.
|
/// An Encoder for when Transfer-Encoding includes `chunked`.
|
||||||
Chunked(bool),
|
Chunked(bool),
|
||||||
|
|
||||||
/// An Encoder for when Content-Length is set.
|
/// An Encoder for when Content-Length is set.
|
||||||
///
|
///
|
||||||
/// Enforces that the body is not longer than the Content-Length header.
|
/// Enforces that the body is not longer than the Content-Length header.
|
||||||
Length(u64),
|
Length(u64),
|
||||||
|
|
||||||
/// An Encoder for when Content-Length is not known.
|
/// An Encoder for when Content-Length is not known.
|
||||||
///
|
///
|
||||||
/// Application decides when to stop writing.
|
/// Application decides when to stop writing.
|
||||||
@@ -521,8 +531,10 @@ mod tests {
|
|||||||
use http::header::AUTHORIZATION;
|
use http::header::AUTHORIZATION;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::http::header::{HeaderValue, CONTENT_TYPE};
|
use crate::{
|
||||||
use crate::RequestHead;
|
header::{HeaderValue, CONTENT_TYPE},
|
||||||
|
RequestHead,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_chunked_te() {
|
fn test_chunked_te() {
|
||||||
@@ -552,12 +564,11 @@ mod tests {
|
|||||||
let _ = head.encode_headers(
|
let _ = head.encode_headers(
|
||||||
&mut bytes,
|
&mut bytes,
|
||||||
Version::HTTP_11,
|
Version::HTTP_11,
|
||||||
BodySize::Empty,
|
BodySize::Sized(0),
|
||||||
ConnectionType::Close,
|
ConnectionType::Close,
|
||||||
&ServiceConfig::default(),
|
&ServiceConfig::default(),
|
||||||
);
|
);
|
||||||
let data =
|
let data = String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
||||||
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
|
||||||
|
|
||||||
assert!(data.contains("Content-Length: 0\r\n"));
|
assert!(data.contains("Content-Length: 0\r\n"));
|
||||||
assert!(data.contains("Connection: close\r\n"));
|
assert!(data.contains("Connection: close\r\n"));
|
||||||
@@ -571,8 +582,7 @@ mod tests {
|
|||||||
ConnectionType::KeepAlive,
|
ConnectionType::KeepAlive,
|
||||||
&ServiceConfig::default(),
|
&ServiceConfig::default(),
|
||||||
);
|
);
|
||||||
let data =
|
let data = String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
||||||
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
|
||||||
assert!(data.contains("Transfer-Encoding: chunked\r\n"));
|
assert!(data.contains("Transfer-Encoding: chunked\r\n"));
|
||||||
assert!(data.contains("Content-Type: plain/text\r\n"));
|
assert!(data.contains("Content-Type: plain/text\r\n"));
|
||||||
assert!(data.contains("Date: date\r\n"));
|
assert!(data.contains("Date: date\r\n"));
|
||||||
@@ -593,8 +603,7 @@ mod tests {
|
|||||||
ConnectionType::KeepAlive,
|
ConnectionType::KeepAlive,
|
||||||
&ServiceConfig::default(),
|
&ServiceConfig::default(),
|
||||||
);
|
);
|
||||||
let data =
|
let data = String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
||||||
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
|
||||||
assert!(data.contains("transfer-encoding: chunked\r\n"));
|
assert!(data.contains("transfer-encoding: chunked\r\n"));
|
||||||
assert!(data.contains("content-type: xml\r\n"));
|
assert!(data.contains("content-type: xml\r\n"));
|
||||||
assert!(data.contains("content-type: plain/text\r\n"));
|
assert!(data.contains("content-type: plain/text\r\n"));
|
||||||
@@ -623,12 +632,11 @@ mod tests {
|
|||||||
let _ = head.encode_headers(
|
let _ = head.encode_headers(
|
||||||
&mut bytes,
|
&mut bytes,
|
||||||
Version::HTTP_11,
|
Version::HTTP_11,
|
||||||
BodySize::Empty,
|
BodySize::Sized(0),
|
||||||
ConnectionType::Close,
|
ConnectionType::Close,
|
||||||
&ServiceConfig::default(),
|
&ServiceConfig::default(),
|
||||||
);
|
);
|
||||||
let data =
|
let data = String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
||||||
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
|
||||||
assert!(data.contains("content-length: 0\r\n"));
|
assert!(data.contains("content-length: 0\r\n"));
|
||||||
assert!(data.contains("connection: close\r\n"));
|
assert!(data.contains("connection: close\r\n"));
|
||||||
assert!(data.contains("authorization: another authorization\r\n"));
|
assert!(data.contains("authorization: another authorization\r\n"));
|
||||||
@@ -651,8 +659,7 @@ mod tests {
|
|||||||
ConnectionType::Upgrade,
|
ConnectionType::Upgrade,
|
||||||
&ServiceConfig::default(),
|
&ServiceConfig::default(),
|
||||||
);
|
);
|
||||||
let data =
|
let data = String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
||||||
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
|
||||||
assert!(!data.contains("content-length: 0\r\n"));
|
assert!(!data.contains("content-length: 0\r\n"));
|
||||||
assert!(!data.contains("transfer-encoding: chunked\r\n"));
|
assert!(!data.contains("transfer-encoding: chunked\r\n"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use actix_service::{Service, ServiceFactory};
|
use actix_service::{Service, ServiceFactory};
|
||||||
use actix_utils::future::{ready, Ready};
|
use actix_utils::future::{ready, Ready};
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::{Error, Request};
|
||||||
use crate::request::Request;
|
|
||||||
|
|
||||||
pub struct ExpectHandler;
|
pub struct ExpectHandler;
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ mod client;
|
|||||||
mod codec;
|
mod codec;
|
||||||
mod decoder;
|
mod decoder;
|
||||||
mod dispatcher;
|
mod dispatcher;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod dispatcher_tests;
|
||||||
mod encoder;
|
mod encoder;
|
||||||
mod expect;
|
mod expect;
|
||||||
mod payload;
|
mod payload;
|
||||||
mod service;
|
mod service;
|
||||||
|
mod timer;
|
||||||
mod upgrade;
|
mod upgrade;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
@@ -26,9 +29,10 @@ pub use self::utils::SendResponse;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
/// Codec message
|
/// Codec message
|
||||||
pub enum Message<T> {
|
pub enum Message<T> {
|
||||||
/// Http message
|
/// HTTP message.
|
||||||
Item(T),
|
Item(T),
|
||||||
/// Payload chunk
|
|
||||||
|
/// Payload chunk.
|
||||||
Chunk(Option<Bytes>),
|
Chunk(Option<Bytes>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +63,7 @@ pub(crate) fn reserve_readbuf(src: &mut BytesMut) {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::request::Request;
|
use crate::Request;
|
||||||
|
|
||||||
impl Message<Request> {
|
impl Message<Request> {
|
||||||
pub fn message(self) -> Request {
|
pub fn message(self) -> Request {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
//! Payload stream
|
//! Payload stream
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::collections::VecDeque;
|
use std::{
|
||||||
use std::pin::Pin;
|
cell::RefCell,
|
||||||
use std::rc::{Rc, Weak};
|
collections::VecDeque,
|
||||||
use std::task::{Context, Poll, Waker};
|
pin::Pin,
|
||||||
|
rc::{Rc, Weak},
|
||||||
|
task::{Context, Poll, Waker},
|
||||||
|
};
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_core::Stream;
|
use futures_core::Stream;
|
||||||
@@ -22,39 +25,32 @@ pub enum PayloadStatus {
|
|||||||
|
|
||||||
/// Buffered stream of bytes chunks
|
/// Buffered stream of bytes chunks
|
||||||
///
|
///
|
||||||
/// Payload stores chunks in a vector. First chunk can be received with
|
/// Payload stores chunks in a vector. First chunk can be received with `poll_next`. Payload does
|
||||||
/// `.readany()` method. Payload stream is not thread safe. Payload does not
|
/// not notify current task when new data is available.
|
||||||
/// notify current task when new data is available.
|
|
||||||
///
|
///
|
||||||
/// Payload stream can be used as `Response` body stream.
|
/// Payload can be used as `Response` body stream.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Payload {
|
pub struct Payload {
|
||||||
inner: Rc<RefCell<Inner>>,
|
inner: Rc<RefCell<Inner>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Payload {
|
impl Payload {
|
||||||
/// Create payload stream.
|
/// Creates a payload stream.
|
||||||
///
|
///
|
||||||
/// This method construct two objects responsible for bytes stream
|
/// This method construct two objects responsible for bytes stream generation:
|
||||||
/// generation.
|
/// - `PayloadSender` - *Sender* side of the stream
|
||||||
///
|
/// - `Payload` - *Receiver* side of the stream
|
||||||
/// * `PayloadSender` - *Sender* side of the stream
|
|
||||||
///
|
|
||||||
/// * `Payload` - *Receiver* side of the stream
|
|
||||||
pub fn create(eof: bool) -> (PayloadSender, Payload) {
|
pub fn create(eof: bool) -> (PayloadSender, Payload) {
|
||||||
let shared = Rc::new(RefCell::new(Inner::new(eof)));
|
let shared = Rc::new(RefCell::new(Inner::new(eof)));
|
||||||
|
|
||||||
(
|
(
|
||||||
PayloadSender {
|
PayloadSender::new(Rc::downgrade(&shared)),
|
||||||
inner: Rc::downgrade(&shared),
|
|
||||||
},
|
|
||||||
Payload { inner: shared },
|
Payload { inner: shared },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create empty payload
|
/// Creates an empty payload.
|
||||||
#[doc(hidden)]
|
pub(crate) fn empty() -> Payload {
|
||||||
pub fn empty() -> Payload {
|
|
||||||
Payload {
|
Payload {
|
||||||
inner: Rc::new(RefCell::new(Inner::new(true))),
|
inner: Rc::new(RefCell::new(Inner::new(true))),
|
||||||
}
|
}
|
||||||
@@ -77,14 +73,6 @@ impl Payload {
|
|||||||
pub fn unread_data(&mut self, data: Bytes) {
|
pub fn unread_data(&mut self, data: Bytes) {
|
||||||
self.inner.borrow_mut().unread_data(data);
|
self.inner.borrow_mut().unread_data(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn readany(
|
|
||||||
&mut self,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, PayloadError>>> {
|
|
||||||
self.inner.borrow_mut().readany(cx)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stream for Payload {
|
impl Stream for Payload {
|
||||||
@@ -94,7 +82,7 @@ impl Stream for Payload {
|
|||||||
self: Pin<&mut Self>,
|
self: Pin<&mut Self>,
|
||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
) -> Poll<Option<Result<Bytes, PayloadError>>> {
|
) -> Poll<Option<Result<Bytes, PayloadError>>> {
|
||||||
self.inner.borrow_mut().readany(cx)
|
Pin::new(&mut *self.inner.borrow_mut()).poll_next(cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +92,10 @@ pub struct PayloadSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PayloadSender {
|
impl PayloadSender {
|
||||||
|
fn new(inner: Weak<RefCell<Inner>>) -> Self {
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_error(&mut self, err: PayloadError) {
|
pub fn set_error(&mut self, err: PayloadError) {
|
||||||
if let Some(shared) = self.inner.upgrade() {
|
if let Some(shared) = self.inner.upgrade() {
|
||||||
@@ -227,8 +219,8 @@ impl Inner {
|
|||||||
self.len
|
self.len
|
||||||
}
|
}
|
||||||
|
|
||||||
fn readany(
|
fn poll_next(
|
||||||
&mut self,
|
mut self: Pin<&mut Self>,
|
||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
) -> Poll<Option<Result<Bytes, PayloadError>>> {
|
) -> Poll<Option<Result<Bytes, PayloadError>>> {
|
||||||
if let Some(data) = self.items.pop_front() {
|
if let Some(data) = self.items.pop_front() {
|
||||||
@@ -260,8 +252,18 @@ impl Inner {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||||
|
|
||||||
use actix_utils::future::poll_fn;
|
use actix_utils::future::poll_fn;
|
||||||
|
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
assert_impl_all!(Payload: Unpin);
|
||||||
|
assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe);
|
||||||
|
|
||||||
|
assert_impl_all!(Inner: Unpin, Send, Sync);
|
||||||
|
assert_not_impl_any!(Inner: UnwindSafe, RefUnwindSafe);
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_unread_data() {
|
async fn test_unread_data() {
|
||||||
@@ -273,7 +275,10 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Bytes::from("data"),
|
Bytes::from("data"),
|
||||||
poll_fn(|cx| payload.readany(cx)).await.unwrap().unwrap()
|
poll_fn(|cx| Pin::new(&mut payload).poll_next(cx))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::{
|
use std::{
|
||||||
error::Error as StdError,
|
|
||||||
fmt,
|
fmt,
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
net,
|
net,
|
||||||
@@ -16,7 +15,7 @@ use actix_utils::future::ready;
|
|||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{AnyBody, MessageBody},
|
body::{BoxBody, MessageBody},
|
||||||
config::ServiceConfig,
|
config::ServiceConfig,
|
||||||
error::DispatchError,
|
error::DispatchError,
|
||||||
service::HttpServiceHandler,
|
service::HttpServiceHandler,
|
||||||
@@ -38,7 +37,7 @@ pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> {
|
|||||||
impl<T, S, B> H1Service<T, S, B>
|
impl<T, S, B> H1Service<T, S, B>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Response<AnyBody>>,
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
@@ -63,21 +62,20 @@ impl<S, B, X, U> H1Service<TcpStream, S, B, X, U>
|
|||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Error: Into<Response<AnyBody>>,
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
|
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
B::Error: Into<Box<dyn StdError>>,
|
|
||||||
|
|
||||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||||
X::Future: 'static,
|
X::Future: 'static,
|
||||||
X::Error: Into<Response<AnyBody>>,
|
X::Error: Into<Response<BoxBody>>,
|
||||||
X::InitError: fmt::Debug,
|
X::InitError: fmt::Debug,
|
||||||
|
|
||||||
U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>,
|
U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>,
|
||||||
U::Future: 'static,
|
U::Future: 'static,
|
||||||
U::Error: fmt::Display + Into<Response<AnyBody>>,
|
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||||
U::InitError: fmt::Debug,
|
U::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
/// Create simple tcp stream service
|
/// Create simple tcp stream service
|
||||||
@@ -102,9 +100,11 @@ where
|
|||||||
mod openssl {
|
mod openssl {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use actix_service::ServiceFactoryExt;
|
|
||||||
use actix_tls::accept::{
|
use actix_tls::accept::{
|
||||||
openssl::{Acceptor, SslAcceptor, SslError, TlsStream},
|
openssl::{
|
||||||
|
reexports::{Error as SslError, SslAcceptor},
|
||||||
|
Acceptor, TlsStream,
|
||||||
|
},
|
||||||
TlsError,
|
TlsError,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,16 +112,15 @@ mod openssl {
|
|||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Error: Into<Response<AnyBody>>,
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
|
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
B::Error: Into<Box<dyn StdError>>,
|
|
||||||
|
|
||||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||||
X::Future: 'static,
|
X::Future: 'static,
|
||||||
X::Error: Into<Response<AnyBody>>,
|
X::Error: Into<Response<BoxBody>>,
|
||||||
X::InitError: fmt::Debug,
|
X::InitError: fmt::Debug,
|
||||||
|
|
||||||
U: ServiceFactory<
|
U: ServiceFactory<
|
||||||
@@ -130,10 +129,10 @@ mod openssl {
|
|||||||
Response = (),
|
Response = (),
|
||||||
>,
|
>,
|
||||||
U::Future: 'static,
|
U::Future: 'static,
|
||||||
U::Error: fmt::Display + Into<Response<AnyBody>>,
|
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||||
U::InitError: fmt::Debug,
|
U::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
/// Create openssl based service
|
/// Create OpenSSL based service.
|
||||||
pub fn openssl(
|
pub fn openssl(
|
||||||
self,
|
self,
|
||||||
acceptor: SslAcceptor,
|
acceptor: SslAcceptor,
|
||||||
@@ -145,11 +144,13 @@ mod openssl {
|
|||||||
InitError = (),
|
InitError = (),
|
||||||
> {
|
> {
|
||||||
Acceptor::new(acceptor)
|
Acceptor::new(acceptor)
|
||||||
.map_err(TlsError::Tls)
|
.map_init_err(|_| {
|
||||||
.map_init_err(|_| panic!())
|
unreachable!("TLS acceptor service factory does not error on init")
|
||||||
.and_then(|io: TlsStream<TcpStream>| {
|
})
|
||||||
|
.map_err(TlsError::into_service_error)
|
||||||
|
.map(|io: TlsStream<TcpStream>| {
|
||||||
let peer_addr = io.get_ref().peer_addr().ok();
|
let peer_addr = io.get_ref().peer_addr().ok();
|
||||||
ready(Ok((io, peer_addr)))
|
(io, peer_addr)
|
||||||
})
|
})
|
||||||
.and_then(self.map_err(TlsError::Service))
|
.and_then(self.map_err(TlsError::Service))
|
||||||
}
|
}
|
||||||
@@ -158,30 +159,30 @@ mod openssl {
|
|||||||
|
|
||||||
#[cfg(feature = "rustls")]
|
#[cfg(feature = "rustls")]
|
||||||
mod rustls {
|
mod rustls {
|
||||||
use super::*;
|
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use actix_service::ServiceFactoryExt;
|
use actix_service::ServiceFactoryExt as _;
|
||||||
use actix_tls::accept::{
|
use actix_tls::accept::{
|
||||||
rustls::{Acceptor, ServerConfig, TlsStream},
|
rustls::{reexports::ServerConfig, Acceptor, TlsStream},
|
||||||
TlsError,
|
TlsError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
|
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Error: Into<Response<AnyBody>>,
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
|
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
B::Error: Into<Box<dyn StdError>>,
|
|
||||||
|
|
||||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||||
X::Future: 'static,
|
X::Future: 'static,
|
||||||
X::Error: Into<Response<AnyBody>>,
|
X::Error: Into<Response<BoxBody>>,
|
||||||
X::InitError: fmt::Debug,
|
X::InitError: fmt::Debug,
|
||||||
|
|
||||||
U: ServiceFactory<
|
U: ServiceFactory<
|
||||||
@@ -190,10 +191,10 @@ mod rustls {
|
|||||||
Response = (),
|
Response = (),
|
||||||
>,
|
>,
|
||||||
U::Future: 'static,
|
U::Future: 'static,
|
||||||
U::Error: fmt::Display + Into<Response<AnyBody>>,
|
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||||
U::InitError: fmt::Debug,
|
U::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
/// Create rustls based service
|
/// Create Rustls based service.
|
||||||
pub fn rustls(
|
pub fn rustls(
|
||||||
self,
|
self,
|
||||||
config: ServerConfig,
|
config: ServerConfig,
|
||||||
@@ -205,11 +206,13 @@ mod rustls {
|
|||||||
InitError = (),
|
InitError = (),
|
||||||
> {
|
> {
|
||||||
Acceptor::new(config)
|
Acceptor::new(config)
|
||||||
.map_err(TlsError::Tls)
|
.map_init_err(|_| {
|
||||||
.map_init_err(|_| panic!())
|
unreachable!("TLS acceptor service factory does not error on init")
|
||||||
.and_then(|io: TlsStream<TcpStream>| {
|
})
|
||||||
|
.map_err(TlsError::into_service_error)
|
||||||
|
.map(|io: TlsStream<TcpStream>| {
|
||||||
let peer_addr = io.get_ref().0.peer_addr().ok();
|
let peer_addr = io.get_ref().0.peer_addr().ok();
|
||||||
ready(Ok((io, peer_addr)))
|
(io, peer_addr)
|
||||||
})
|
})
|
||||||
.and_then(self.map_err(TlsError::Service))
|
.and_then(self.map_err(TlsError::Service))
|
||||||
}
|
}
|
||||||
@@ -219,7 +222,7 @@ mod rustls {
|
|||||||
impl<T, S, B, X, U> H1Service<T, S, B, X, U>
|
impl<T, S, B, X, U> H1Service<T, S, B, X, U>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Response<AnyBody>>,
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
@@ -227,7 +230,7 @@ where
|
|||||||
pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U>
|
pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U>
|
||||||
where
|
where
|
||||||
X1: ServiceFactory<Request, Response = Request>,
|
X1: ServiceFactory<Request, Response = Request>,
|
||||||
X1::Error: Into<Response<AnyBody>>,
|
X1::Error: Into<Response<BoxBody>>,
|
||||||
X1::InitError: fmt::Debug,
|
X1::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
H1Service {
|
H1Service {
|
||||||
@@ -263,28 +266,26 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, S, B, X, U> ServiceFactory<(T, Option<net::SocketAddr>)>
|
impl<T, S, B, X, U> ServiceFactory<(T, Option<net::SocketAddr>)> for H1Service<T, S, B, X, U>
|
||||||
for H1Service<T, S, B, X, U>
|
|
||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||||
|
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Error: Into<Response<AnyBody>>,
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
|
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
B::Error: Into<Box<dyn StdError>>,
|
|
||||||
|
|
||||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||||
X::Future: 'static,
|
X::Future: 'static,
|
||||||
X::Error: Into<Response<AnyBody>>,
|
X::Error: Into<Response<BoxBody>>,
|
||||||
X::InitError: fmt::Debug,
|
X::InitError: fmt::Debug,
|
||||||
|
|
||||||
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
|
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
|
||||||
U::Future: 'static,
|
U::Future: 'static,
|
||||||
U::Error: fmt::Display + Into<Response<AnyBody>>,
|
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||||
U::InitError: fmt::Debug,
|
U::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
type Response = ();
|
type Response = ();
|
||||||
@@ -308,9 +309,9 @@ where
|
|||||||
|
|
||||||
let upgrade = match upgrade {
|
let upgrade = match upgrade {
|
||||||
Some(upgrade) => {
|
Some(upgrade) => {
|
||||||
let upgrade = upgrade.await.map_err(|e| {
|
let upgrade = upgrade
|
||||||
log::error!("Init http upgrade service error: {:?}", e)
|
.await
|
||||||
})?;
|
.map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?;
|
||||||
Some(upgrade)
|
Some(upgrade)
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
@@ -334,45 +335,35 @@ where
|
|||||||
/// `Service` implementation for HTTP/1 transport
|
/// `Service` implementation for HTTP/1 transport
|
||||||
pub type H1ServiceHandler<T, S, B, X, U> = HttpServiceHandler<T, S, B, X, U>;
|
pub type H1ServiceHandler<T, S, B, X, U> = HttpServiceHandler<T, S, B, X, U>;
|
||||||
|
|
||||||
impl<T, S, B, X, U> Service<(T, Option<net::SocketAddr>)>
|
impl<T, S, B, X, U> Service<(T, Option<net::SocketAddr>)> for HttpServiceHandler<T, S, B, X, U>
|
||||||
for HttpServiceHandler<T, S, B, X, U>
|
|
||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
|
|
||||||
S: Service<Request>,
|
S: Service<Request>,
|
||||||
S::Error: Into<Response<AnyBody>>,
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
|
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
B::Error: Into<Box<dyn StdError>>,
|
|
||||||
|
|
||||||
X: Service<Request, Response = Request>,
|
X: Service<Request, Response = Request>,
|
||||||
X::Error: Into<Response<AnyBody>>,
|
X::Error: Into<Response<BoxBody>>,
|
||||||
|
|
||||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||||
U::Error: fmt::Display + Into<Response<AnyBody>>,
|
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||||
{
|
{
|
||||||
type Response = ();
|
type Response = ();
|
||||||
type Error = DispatchError;
|
type Error = DispatchError;
|
||||||
type Future = Dispatcher<T, S, B, X, U>;
|
type Future = Dispatcher<T, S, B, X, U>;
|
||||||
|
|
||||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
self._poll_ready(cx).map_err(|e| {
|
self._poll_ready(cx).map_err(|err| {
|
||||||
log::error!("HTTP/1 service readiness error: {:?}", e);
|
log::error!("HTTP/1 service readiness error: {:?}", err);
|
||||||
DispatchError::Service(e)
|
DispatchError::Service(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
|
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
|
||||||
let on_connect_data =
|
let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
|
||||||
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
|
Dispatcher::new(io, self.flow.clone(), self.cfg.clone(), addr, conn_data)
|
||||||
|
|
||||||
Dispatcher::new(
|
|
||||||
io,
|
|
||||||
self.cfg.clone(),
|
|
||||||
self.flow.clone(),
|
|
||||||
on_connect_data,
|
|
||||||
addr,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
80
actix-http/src/h1/timer.rs
Normal file
80
actix-http/src/h1/timer.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
use std::{fmt, future::Future, pin::Pin, task::Context};
|
||||||
|
|
||||||
|
use actix_rt::time::{Instant, Sleep};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(super) enum TimerState {
|
||||||
|
Disabled,
|
||||||
|
Inactive,
|
||||||
|
Active { timer: Pin<Box<Sleep>> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimerState {
|
||||||
|
pub(super) fn new(enabled: bool) -> Self {
|
||||||
|
if enabled {
|
||||||
|
Self::Inactive
|
||||||
|
} else {
|
||||||
|
Self::Disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn is_enabled(&self) -> bool {
|
||||||
|
matches!(self, Self::Active { .. } | Self::Inactive)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set(&mut self, timer: Sleep, line: u32) {
|
||||||
|
if matches!(self, Self::Disabled) {
|
||||||
|
log::trace!("setting disabled timer from line {}", line);
|
||||||
|
}
|
||||||
|
|
||||||
|
*self = Self::Active {
|
||||||
|
timer: Box::pin(timer),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_and_init(&mut self, cx: &mut Context<'_>, timer: Sleep, line: u32) {
|
||||||
|
self.set(timer, line);
|
||||||
|
self.init(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn clear(&mut self, line: u32) {
|
||||||
|
if matches!(self, Self::Disabled) {
|
||||||
|
log::trace!("trying to clear a disabled timer from line {}", line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(self, Self::Inactive) {
|
||||||
|
log::trace!("trying to clear an inactive timer from line {}", line);
|
||||||
|
}
|
||||||
|
|
||||||
|
*self = Self::Inactive;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn init(&mut self, cx: &mut Context<'_>) {
|
||||||
|
if let TimerState::Active { timer } = self {
|
||||||
|
let _ = timer.as_mut().poll(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TimerState {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
TimerState::Disabled => f.write_str("timer is disabled"),
|
||||||
|
TimerState::Inactive => f.write_str("timer is inactive"),
|
||||||
|
TimerState::Active { timer } => {
|
||||||
|
let deadline = timer.deadline();
|
||||||
|
let now = Instant::now();
|
||||||
|
|
||||||
|
if deadline < now {
|
||||||
|
f.write_str("timer is active and has reached deadline")
|
||||||
|
} else {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"timer is active and due to expire in {} milliseconds",
|
||||||
|
((deadline - now).as_secs_f32() * 1000.0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,7 @@ use actix_codec::Framed;
|
|||||||
use actix_service::{Service, ServiceFactory};
|
use actix_service::{Service, ServiceFactory};
|
||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::{h1::Codec, Error, Request};
|
||||||
use crate::h1::Codec;
|
|
||||||
use crate::request::Request;
|
|
||||||
|
|
||||||
pub struct UpgradeHandler;
|
pub struct UpgradeHandler;
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
use std::future::Future;
|
use std::{
|
||||||
use std::pin::Pin;
|
future::Future,
|
||||||
use std::task::{Context, Poll};
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||||
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
use crate::body::{BodySize, MessageBody};
|
use crate::{
|
||||||
use crate::error::Error;
|
body::{BodySize, MessageBody},
|
||||||
use crate::h1::{Codec, Message};
|
h1::{Codec, Message},
|
||||||
use crate::response::Response;
|
Error, Response,
|
||||||
|
};
|
||||||
|
|
||||||
|
pin_project! {
|
||||||
/// Send HTTP/1 response
|
/// Send HTTP/1 response
|
||||||
#[pin_project::pin_project]
|
|
||||||
pub struct SendResponse<T, B> {
|
pub struct SendResponse<T, B> {
|
||||||
res: Option<Message<(Response<()>, BodySize)>>,
|
res: Option<Message<(Response<()>, BodySize)>>,
|
||||||
|
|
||||||
#[pin]
|
#[pin]
|
||||||
body: Option<B>,
|
body: Option<B>,
|
||||||
|
|
||||||
#[pin]
|
#[pin]
|
||||||
framed: Option<Framed<T, Codec>>,
|
framed: Option<Framed<T, Codec>>,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T, B> SendResponse<T, B>
|
impl<T, B> SendResponse<T, B>
|
||||||
where
|
where
|
||||||
@@ -38,7 +45,7 @@ where
|
|||||||
impl<T, B> Future for SendResponse<T, B>
|
impl<T, B> Future for SendResponse<T, B>
|
||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
B: MessageBody + Unpin,
|
B: MessageBody,
|
||||||
B::Error: Into<Error>,
|
B::Error: Into<Error>,
|
||||||
{
|
{
|
||||||
type Output = Result<Framed<T, Codec>, Error>;
|
type Output = Result<Framed<T, Codec>, Error>;
|
||||||
@@ -62,12 +69,9 @@ where
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.is_write_buf_full()
|
.is_write_buf_full()
|
||||||
{
|
{
|
||||||
let next =
|
let next = match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx) {
|
||||||
match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx) {
|
|
||||||
Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(item)),
|
Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(item)),
|
||||||
Poll::Ready(Some(Err(err))) => {
|
Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err.into())),
|
||||||
return Poll::Ready(Err(err.into()))
|
|
||||||
}
|
|
||||||
Poll::Ready(None) => Poll::Ready(None),
|
Poll::Ready(None) => Poll::Ready(None),
|
||||||
Poll::Pending => Poll::Pending,
|
Poll::Pending => Poll::Pending,
|
||||||
};
|
};
|
||||||
@@ -77,12 +81,12 @@ where
|
|||||||
// body is done when item is None
|
// body is done when item is None
|
||||||
body_done = item.is_none();
|
body_done = item.is_none();
|
||||||
if body_done {
|
if body_done {
|
||||||
let _ = this.body.take();
|
this.body.set(None);
|
||||||
}
|
}
|
||||||
let framed = this.framed.as_mut().as_pin_mut().unwrap();
|
let framed = this.framed.as_mut().as_pin_mut().unwrap();
|
||||||
framed.write(Message::Chunk(item)).map_err(|err| {
|
framed
|
||||||
Error::new_send_response().with_cause(err)
|
.write(Message::Chunk(item))
|
||||||
})?;
|
.map_err(|err| Error::new_send_response().with_cause(err))?;
|
||||||
}
|
}
|
||||||
Poll::Pending => body_ready = false,
|
Poll::Pending => body_ready = false,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,20 +10,24 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use actix_codec::{AsyncRead, AsyncWrite};
|
use actix_codec::{AsyncRead, AsyncWrite};
|
||||||
|
use actix_rt::time::{sleep, Sleep};
|
||||||
use actix_service::Service;
|
use actix_service::Service;
|
||||||
use actix_utils::future::poll_fn;
|
use actix_utils::future::poll_fn;
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use futures_core::ready;
|
use futures_core::ready;
|
||||||
use h2::server::{Connection, SendResponse};
|
use h2::{
|
||||||
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
|
server::{Connection, SendResponse},
|
||||||
|
Ping, PingPong,
|
||||||
|
};
|
||||||
use log::{error, trace};
|
use log::{error, trace};
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{AnyBody, BodySize, MessageBody},
|
body::{BodySize, BoxBody, MessageBody},
|
||||||
config::ServiceConfig,
|
config::ServiceConfig,
|
||||||
|
header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING},
|
||||||
service::HttpFlow,
|
service::HttpFlow,
|
||||||
OnConnectData, Payload, Request, Response, ResponseHead,
|
Extensions, OnConnectData, Payload, Request, Response, ResponseHead,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CHUNK_SIZE: usize = 16_384;
|
const CHUNK_SIZE: usize = 16_384;
|
||||||
@@ -33,43 +37,66 @@ pin_project! {
|
|||||||
pub struct Dispatcher<T, S, B, X, U> {
|
pub struct Dispatcher<T, S, B, X, U> {
|
||||||
flow: Rc<HttpFlow<S, X, U>>,
|
flow: Rc<HttpFlow<S, X, U>>,
|
||||||
connection: Connection<T, Bytes>,
|
connection: Connection<T, Bytes>,
|
||||||
on_connect_data: OnConnectData,
|
conn_data: Option<Rc<Extensions>>,
|
||||||
config: ServiceConfig,
|
config: ServiceConfig,
|
||||||
peer_addr: Option<net::SocketAddr>,
|
peer_addr: Option<net::SocketAddr>,
|
||||||
_phantom: PhantomData<B>,
|
ping_pong: Option<H2PingPong>,
|
||||||
|
_phantom: PhantomData<B>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, S, B, X, U> Dispatcher<T, S, B, X, U> {
|
impl<T, S, B, X, U> Dispatcher<T, S, B, X, U>
|
||||||
|
where
|
||||||
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
|
{
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
|
mut conn: Connection<T, Bytes>,
|
||||||
flow: Rc<HttpFlow<S, X, U>>,
|
flow: Rc<HttpFlow<S, X, U>>,
|
||||||
connection: Connection<T, Bytes>,
|
|
||||||
on_connect_data: OnConnectData,
|
|
||||||
config: ServiceConfig,
|
config: ServiceConfig,
|
||||||
peer_addr: Option<net::SocketAddr>,
|
peer_addr: Option<net::SocketAddr>,
|
||||||
|
conn_data: OnConnectData,
|
||||||
|
timer: Option<Pin<Box<Sleep>>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let ping_pong = config.keep_alive().duration().map(|dur| H2PingPong {
|
||||||
|
timer: timer
|
||||||
|
.map(|mut timer| {
|
||||||
|
// reuse timer slot if it was initialized for handshake
|
||||||
|
timer.as_mut().reset((config.now() + dur).into());
|
||||||
|
timer
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| Box::pin(sleep(dur))),
|
||||||
|
on_flight: false,
|
||||||
|
ping_pong: conn.ping_pong().unwrap(),
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
flow,
|
flow,
|
||||||
config,
|
config,
|
||||||
peer_addr,
|
peer_addr,
|
||||||
connection,
|
connection: conn,
|
||||||
on_connect_data,
|
conn_data: conn_data.0.map(Rc::new),
|
||||||
|
ping_pong,
|
||||||
_phantom: PhantomData,
|
_phantom: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct H2PingPong {
|
||||||
|
timer: Pin<Box<Sleep>>,
|
||||||
|
on_flight: bool,
|
||||||
|
ping_pong: PingPong,
|
||||||
|
}
|
||||||
|
|
||||||
impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U>
|
impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U>
|
||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
|
|
||||||
S: Service<Request>,
|
S: Service<Request>,
|
||||||
S::Error: Into<Response<AnyBody>>,
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
|
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
B::Error: Into<Box<dyn StdError>>,
|
|
||||||
{
|
{
|
||||||
type Output = Result<(), crate::error::DispatchError>;
|
type Output = Result<(), crate::error::DispatchError>;
|
||||||
|
|
||||||
@@ -77,12 +104,12 @@ where
|
|||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
let this = self.get_mut();
|
let this = self.get_mut();
|
||||||
|
|
||||||
while let Some((req, tx)) =
|
loop {
|
||||||
ready!(Pin::new(&mut this.connection).poll_accept(cx)?)
|
match Pin::new(&mut this.connection).poll_accept(cx)? {
|
||||||
{
|
Poll::Ready(Some((req, tx))) => {
|
||||||
let (parts, body) = req.into_parts();
|
let (parts, body) = req.into_parts();
|
||||||
let pl = crate::h2::Payload::new(body);
|
let payload = crate::h2::Payload::new(body);
|
||||||
let pl = Payload::<crate::payload::PayloadStream>::H2(pl);
|
let pl = Payload::H2 { payload };
|
||||||
let mut req = Request::with_payload(pl);
|
let mut req = Request::with_payload(pl);
|
||||||
|
|
||||||
let head = req.head_mut();
|
let head = req.head_mut();
|
||||||
@@ -92,8 +119,7 @@ where
|
|||||||
head.headers = parts.headers.into();
|
head.headers = parts.headers.into();
|
||||||
head.peer_addr = this.peer_addr;
|
head.peer_addr = this.peer_addr;
|
||||||
|
|
||||||
// merge on_connect_ext data into request extensions
|
req.conn_data = this.conn_data.as_ref().map(Rc::clone);
|
||||||
this.on_connect_data.merge_into(&mut req);
|
|
||||||
|
|
||||||
let fut = this.flow.service.call(req);
|
let fut = this.flow.service.call(req);
|
||||||
let config = this.config.clone();
|
let config = this.config.clone();
|
||||||
@@ -104,7 +130,7 @@ where
|
|||||||
let res = match fut.await {
|
let res = match fut.await {
|
||||||
Ok(res) => handle_response(res.into(), tx, config).await,
|
Ok(res) => handle_response(res.into(), tx, config).await,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let res: Response<AnyBody> = err.into();
|
let res: Response<BoxBody> = err.into();
|
||||||
handle_response(res, tx, config).await
|
handle_response(res, tx, config).await
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -115,7 +141,7 @@ where
|
|||||||
DispatchError::SendResponse(err) => {
|
DispatchError::SendResponse(err) => {
|
||||||
trace!("Error sending HTTP/2 response: {:?}", err)
|
trace!("Error sending HTTP/2 response: {:?}", err)
|
||||||
}
|
}
|
||||||
DispatchError::SendData(err) => warn!("{:?}", err),
|
DispatchError::SendData(err) => log::warn!("{:?}", err),
|
||||||
DispatchError::ResponseBody(err) => {
|
DispatchError::ResponseBody(err) => {
|
||||||
error!("Response payload stream error: {:?}", err)
|
error!("Response payload stream error: {:?}", err)
|
||||||
}
|
}
|
||||||
@@ -123,8 +149,41 @@ where
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Poll::Ready(None) => return Poll::Ready(Ok(())),
|
||||||
|
Poll::Pending => match this.ping_pong.as_mut() {
|
||||||
|
Some(ping_pong) => loop {
|
||||||
|
if ping_pong.on_flight {
|
||||||
|
// When have on flight ping pong. poll pong and and keep alive timer.
|
||||||
|
// on success pong received update keep alive timer to determine the next timing of
|
||||||
|
// ping pong.
|
||||||
|
match ping_pong.ping_pong.poll_pong(cx)? {
|
||||||
|
Poll::Ready(_) => {
|
||||||
|
ping_pong.on_flight = false;
|
||||||
|
|
||||||
Poll::Ready(Ok(()))
|
let dead_line = this.config.keep_alive_deadline().unwrap();
|
||||||
|
ping_pong.timer.as_mut().reset(dead_line.into());
|
||||||
|
}
|
||||||
|
Poll::Pending => {
|
||||||
|
return ping_pong.timer.as_mut().poll(cx).map(|_| Ok(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// When there is no on flight ping pong. keep alive timer is used to wait for next
|
||||||
|
// timing of ping pong. Therefore at this point it serves as an interval instead.
|
||||||
|
ready!(ping_pong.timer.as_mut().poll(cx));
|
||||||
|
|
||||||
|
ping_pong.ping_pong.send_ping(Ping::opaque())?;
|
||||||
|
|
||||||
|
let dead_line = this.config.keep_alive_deadline().unwrap();
|
||||||
|
ping_pong.timer.as_mut().reset(dead_line.into());
|
||||||
|
|
||||||
|
ping_pong.on_flight = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => return Poll::Pending,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +200,6 @@ async fn handle_response<B>(
|
|||||||
) -> Result<(), DispatchError>
|
) -> Result<(), DispatchError>
|
||||||
where
|
where
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
B::Error: Into<Box<dyn StdError>>,
|
|
||||||
{
|
{
|
||||||
let (res, body) = res.replace_body(());
|
let (res, body) = res.replace_body(());
|
||||||
|
|
||||||
@@ -159,25 +217,28 @@ where
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// poll response body and send chunks to client.
|
// poll response body and send chunks to client
|
||||||
actix_rt::pin!(body);
|
actix_rt::pin!(body);
|
||||||
|
|
||||||
while let Some(res) = poll_fn(|cx| body.as_mut().poll_next(cx)).await {
|
while let Some(res) = poll_fn(|cx| body.as_mut().poll_next(cx)).await {
|
||||||
let mut chunk = res.map_err(|err| DispatchError::ResponseBody(err.into()))?;
|
let mut chunk = res.map_err(|err| DispatchError::ResponseBody(err.into()))?;
|
||||||
|
|
||||||
'send: loop {
|
'send: loop {
|
||||||
|
let chunk_size = cmp::min(chunk.len(), CHUNK_SIZE);
|
||||||
|
|
||||||
// reserve enough space and wait for stream ready.
|
// reserve enough space and wait for stream ready.
|
||||||
stream.reserve_capacity(cmp::min(chunk.len(), CHUNK_SIZE));
|
stream.reserve_capacity(chunk_size);
|
||||||
|
|
||||||
match poll_fn(|cx| stream.poll_capacity(cx)).await {
|
match poll_fn(|cx| stream.poll_capacity(cx)).await {
|
||||||
// No capacity left. drop body and return.
|
// No capacity left. drop body and return.
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
Some(res) => {
|
|
||||||
// Split chuck to writeable size and send to client.
|
|
||||||
let cap = res.map_err(DispatchError::SendData)?;
|
|
||||||
|
|
||||||
|
Some(Err(err)) => return Err(DispatchError::SendData(err)),
|
||||||
|
|
||||||
|
Some(Ok(cap)) => {
|
||||||
|
// split chunk to writeable size and send to client
|
||||||
let len = chunk.len();
|
let len = chunk.len();
|
||||||
let bytes = chunk.split_to(cmp::min(cap, len));
|
let bytes = chunk.split_to(cmp::min(len, cap));
|
||||||
|
|
||||||
stream
|
stream
|
||||||
.send_data(bytes, false)
|
.send_data(bytes, false)
|
||||||
@@ -226,9 +287,13 @@ fn prepare_response(
|
|||||||
|
|
||||||
let _ = match size {
|
let _ = match size {
|
||||||
BodySize::None | BodySize::Stream => None,
|
BodySize::None | BodySize::Stream => None,
|
||||||
BodySize::Empty => res
|
|
||||||
.headers_mut()
|
BodySize::Sized(0) => {
|
||||||
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
|
#[allow(clippy::declare_interior_mutable_const)]
|
||||||
|
const HV_ZERO: HeaderValue = HeaderValue::from_static("0");
|
||||||
|
res.headers_mut().insert(CONTENT_LENGTH, HV_ZERO)
|
||||||
|
}
|
||||||
|
|
||||||
BodySize::Sized(len) => {
|
BodySize::Sized(len) => {
|
||||||
let mut buf = itoa::Buffer::new();
|
let mut buf = itoa::Buffer::new();
|
||||||
|
|
||||||
@@ -243,7 +308,7 @@ fn prepare_response(
|
|||||||
for (key, value) in head.headers.iter() {
|
for (key, value) in head.headers.iter() {
|
||||||
match *key {
|
match *key {
|
||||||
// TODO: consider skipping other headers according to:
|
// TODO: consider skipping other headers according to:
|
||||||
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2
|
// https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2
|
||||||
// omit HTTP/1.x only headers
|
// omit HTTP/1.x only headers
|
||||||
CONNECTION | TRANSFER_ENCODING => continue,
|
CONNECTION | TRANSFER_ENCODING => continue,
|
||||||
CONTENT_LENGTH if skip_len => continue,
|
CONTENT_LENGTH if skip_len => continue,
|
||||||
@@ -257,7 +322,7 @@ fn prepare_response(
|
|||||||
// set date header
|
// set date header
|
||||||
if !has_date {
|
if !has_date {
|
||||||
let mut bytes = BytesMut::with_capacity(29);
|
let mut bytes = BytesMut::with_capacity(29);
|
||||||
config.set_date_header(&mut bytes);
|
config.write_date_header_value(&mut bytes);
|
||||||
res.headers_mut().insert(
|
res.headers_mut().insert(
|
||||||
DATE,
|
DATE,
|
||||||
// SAFETY: serialized date-times are known ASCII strings
|
// SAFETY: serialized date-times are known ASCII strings
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
//! HTTP/2 protocol.
|
//! HTTP/2 protocol.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
future::Future,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use actix_codec::{AsyncRead, AsyncWrite};
|
||||||
|
use actix_rt::time::{sleep_until, Sleep};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_core::{ready, Stream};
|
use futures_core::{ready, Stream};
|
||||||
use h2::RecvStream;
|
use h2::{
|
||||||
|
server::{handshake, Connection, Handshake},
|
||||||
|
RecvStream,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::ServiceConfig,
|
||||||
|
error::{DispatchError, PayloadError},
|
||||||
|
};
|
||||||
|
|
||||||
mod dispatcher;
|
mod dispatcher;
|
||||||
mod service;
|
mod service;
|
||||||
|
|
||||||
pub use self::dispatcher::Dispatcher;
|
pub use self::dispatcher::Dispatcher;
|
||||||
pub use self::service::H2Service;
|
pub use self::service::H2Service;
|
||||||
use crate::error::PayloadError;
|
|
||||||
|
|
||||||
/// HTTP/2 peer stream.
|
/// HTTP/2 peer stream.
|
||||||
pub struct Payload {
|
pub struct Payload {
|
||||||
@@ -30,10 +40,7 @@ impl Payload {
|
|||||||
impl Stream for Payload {
|
impl Stream for Payload {
|
||||||
type Item = Result<Bytes, PayloadError>;
|
type Item = Result<Bytes, PayloadError>;
|
||||||
|
|
||||||
fn poll_next(
|
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Self::Item>> {
|
|
||||||
let this = self.get_mut();
|
let this = self.get_mut();
|
||||||
|
|
||||||
match ready!(Pin::new(&mut this.stream).poll_data(cx)) {
|
match ready!(Pin::new(&mut this.stream).poll_data(cx)) {
|
||||||
@@ -50,3 +57,57 @@ impl Stream for Payload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn handshake_with_timeout<T>(
|
||||||
|
io: T,
|
||||||
|
config: &ServiceConfig,
|
||||||
|
) -> HandshakeWithTimeout<T>
|
||||||
|
where
|
||||||
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
|
{
|
||||||
|
HandshakeWithTimeout {
|
||||||
|
handshake: handshake(io),
|
||||||
|
timer: config
|
||||||
|
.client_request_deadline()
|
||||||
|
.map(|deadline| Box::pin(sleep_until(deadline.into()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct HandshakeWithTimeout<T: AsyncRead + AsyncWrite + Unpin> {
|
||||||
|
handshake: Handshake<T>,
|
||||||
|
timer: Option<Pin<Box<Sleep>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Future for HandshakeWithTimeout<T>
|
||||||
|
where
|
||||||
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
|
{
|
||||||
|
type Output = Result<(Connection<T, Bytes>, Option<Pin<Box<Sleep>>>), DispatchError>;
|
||||||
|
|
||||||
|
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
|
let this = self.get_mut();
|
||||||
|
|
||||||
|
match Pin::new(&mut this.handshake).poll(cx)? {
|
||||||
|
// return the timer on success handshake; its slot can be re-used for h2 ping-pong
|
||||||
|
Poll::Ready(conn) => Poll::Ready(Ok((conn, this.timer.take()))),
|
||||||
|
Poll::Pending => match this.timer.as_mut() {
|
||||||
|
Some(timer) => {
|
||||||
|
ready!(timer.as_mut().poll(cx));
|
||||||
|
Poll::Ready(Err(DispatchError::SlowRequestTimeout))
|
||||||
|
}
|
||||||
|
None => Poll::Pending,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||||
|
|
||||||
|
use static_assertions::assert_impl_all;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
assert_impl_all!(Payload: Unpin, Send, Sync, UnwindSafe, RefUnwindSafe);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
error::Error as StdError,
|
|
||||||
future::Future,
|
future::Future,
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
net,
|
mem, net,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
@@ -11,24 +10,21 @@ use std::{
|
|||||||
use actix_codec::{AsyncRead, AsyncWrite};
|
use actix_codec::{AsyncRead, AsyncWrite};
|
||||||
use actix_rt::net::TcpStream;
|
use actix_rt::net::TcpStream;
|
||||||
use actix_service::{
|
use actix_service::{
|
||||||
fn_factory, fn_service, IntoServiceFactory, Service, ServiceFactory,
|
fn_factory, fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _,
|
||||||
ServiceFactoryExt as _,
|
|
||||||
};
|
};
|
||||||
use actix_utils::future::ready;
|
use actix_utils::future::ready;
|
||||||
use bytes::Bytes;
|
|
||||||
use futures_core::{future::LocalBoxFuture, ready};
|
use futures_core::{future::LocalBoxFuture, ready};
|
||||||
use h2::server::{handshake as h2_handshake, Handshake as H2Handshake};
|
|
||||||
use log::error;
|
use log::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{AnyBody, MessageBody},
|
body::{BoxBody, MessageBody},
|
||||||
config::ServiceConfig,
|
config::ServiceConfig,
|
||||||
error::DispatchError,
|
error::DispatchError,
|
||||||
service::HttpFlow,
|
service::HttpFlow,
|
||||||
ConnectCallback, OnConnectData, Request, Response,
|
ConnectCallback, OnConnectData, Request, Response,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::dispatcher::Dispatcher;
|
use super::{dispatcher::Dispatcher, handshake_with_timeout, HandshakeWithTimeout};
|
||||||
|
|
||||||
/// `ServiceFactory` implementation for HTTP/2 transport
|
/// `ServiceFactory` implementation for HTTP/2 transport
|
||||||
pub struct H2Service<T, S, B> {
|
pub struct H2Service<T, S, B> {
|
||||||
@@ -41,12 +37,11 @@ pub struct H2Service<T, S, B> {
|
|||||||
impl<T, S, B> H2Service<T, S, B>
|
impl<T, S, B> H2Service<T, S, B>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Response<AnyBody>> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
<S::Service as Service<Request>>::Future: 'static,
|
||||||
|
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
B::Error: Into<Box<dyn StdError>>,
|
|
||||||
{
|
{
|
||||||
/// Create new `H2Service` instance with config.
|
/// Create new `H2Service` instance with config.
|
||||||
pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
|
pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
|
||||||
@@ -72,12 +67,11 @@ impl<S, B> H2Service<TcpStream, S, B>
|
|||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Error: Into<Response<AnyBody>> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
<S::Service as Service<Request>>::Future: 'static,
|
||||||
|
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
B::Error: Into<Box<dyn StdError>>,
|
|
||||||
{
|
{
|
||||||
/// Create plain TCP based service
|
/// Create plain TCP based service
|
||||||
pub fn tcp(
|
pub fn tcp(
|
||||||
@@ -101,9 +95,14 @@ where
|
|||||||
|
|
||||||
#[cfg(feature = "openssl")]
|
#[cfg(feature = "openssl")]
|
||||||
mod openssl {
|
mod openssl {
|
||||||
use actix_service::{fn_factory, fn_service, ServiceFactoryExt};
|
use actix_service::ServiceFactoryExt as _;
|
||||||
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream};
|
use actix_tls::accept::{
|
||||||
use actix_tls::accept::TlsError;
|
openssl::{
|
||||||
|
reexports::{Error as SslError, SslAcceptor},
|
||||||
|
Acceptor, TlsStream,
|
||||||
|
},
|
||||||
|
TlsError,
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -111,14 +110,13 @@ mod openssl {
|
|||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Error: Into<Response<AnyBody>> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
<S::Service as Service<Request>>::Future: 'static,
|
||||||
|
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
B::Error: Into<Box<dyn StdError>>,
|
|
||||||
{
|
{
|
||||||
/// Create OpenSSL based service
|
/// Create OpenSSL based service.
|
||||||
pub fn openssl(
|
pub fn openssl(
|
||||||
self,
|
self,
|
||||||
acceptor: SslAcceptor,
|
acceptor: SslAcceptor,
|
||||||
@@ -130,16 +128,14 @@ mod openssl {
|
|||||||
InitError = S::InitError,
|
InitError = S::InitError,
|
||||||
> {
|
> {
|
||||||
Acceptor::new(acceptor)
|
Acceptor::new(acceptor)
|
||||||
.map_err(TlsError::Tls)
|
.map_init_err(|_| {
|
||||||
.map_init_err(|_| panic!())
|
unreachable!("TLS acceptor service factory does not error on init")
|
||||||
.and_then(fn_factory(|| {
|
})
|
||||||
ready(Ok::<_, S::InitError>(fn_service(
|
.map_err(TlsError::into_service_error)
|
||||||
|io: TlsStream<TcpStream>| {
|
.map(|io: TlsStream<TcpStream>| {
|
||||||
let peer_addr = io.get_ref().peer_addr().ok();
|
let peer_addr = io.get_ref().peer_addr().ok();
|
||||||
ready(Ok((io, peer_addr)))
|
(io, peer_addr)
|
||||||
},
|
})
|
||||||
)))
|
|
||||||
}))
|
|
||||||
.and_then(self.map_err(TlsError::Service))
|
.and_then(self.map_err(TlsError::Service))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,24 +143,27 @@ mod openssl {
|
|||||||
|
|
||||||
#[cfg(feature = "rustls")]
|
#[cfg(feature = "rustls")]
|
||||||
mod rustls {
|
mod rustls {
|
||||||
use super::*;
|
|
||||||
use actix_service::ServiceFactoryExt;
|
|
||||||
use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream};
|
|
||||||
use actix_tls::accept::TlsError;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
|
use actix_service::ServiceFactoryExt as _;
|
||||||
|
use actix_tls::accept::{
|
||||||
|
rustls::{reexports::ServerConfig, Acceptor, TlsStream},
|
||||||
|
TlsError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
|
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Error: Into<Response<AnyBody>> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
<S::Service as Service<Request>>::Future: 'static,
|
||||||
|
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
B::Error: Into<Box<dyn StdError>>,
|
|
||||||
{
|
{
|
||||||
/// Create Rustls based service
|
/// Create Rustls based service.
|
||||||
pub fn rustls(
|
pub fn rustls(
|
||||||
self,
|
self,
|
||||||
mut config: ServerConfig,
|
mut config: ServerConfig,
|
||||||
@@ -177,19 +176,17 @@ mod rustls {
|
|||||||
> {
|
> {
|
||||||
let mut protos = vec![b"h2".to_vec()];
|
let mut protos = vec![b"h2".to_vec()];
|
||||||
protos.extend_from_slice(&config.alpn_protocols);
|
protos.extend_from_slice(&config.alpn_protocols);
|
||||||
config.set_protocols(&protos);
|
config.alpn_protocols = protos;
|
||||||
|
|
||||||
Acceptor::new(config)
|
Acceptor::new(config)
|
||||||
.map_err(TlsError::Tls)
|
.map_init_err(|_| {
|
||||||
.map_init_err(|_| panic!())
|
unreachable!("TLS acceptor service factory does not error on init")
|
||||||
.and_then(fn_factory(|| {
|
})
|
||||||
ready(Ok::<_, S::InitError>(fn_service(
|
.map_err(TlsError::into_service_error)
|
||||||
|io: TlsStream<TcpStream>| {
|
.map(|io: TlsStream<TcpStream>| {
|
||||||
let peer_addr = io.get_ref().0.peer_addr().ok();
|
let peer_addr = io.get_ref().0.peer_addr().ok();
|
||||||
ready(Ok((io, peer_addr)))
|
(io, peer_addr)
|
||||||
},
|
})
|
||||||
)))
|
|
||||||
}))
|
|
||||||
.and_then(self.map_err(TlsError::Service))
|
.and_then(self.map_err(TlsError::Service))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,12 +198,11 @@ where
|
|||||||
|
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Error: Into<Response<AnyBody>> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
<S::Service as Service<Request>>::Future: 'static,
|
||||||
|
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
B::Error: Into<Box<dyn StdError>>,
|
|
||||||
{
|
{
|
||||||
type Response = ();
|
type Response = ();
|
||||||
type Error = DispatchError;
|
type Error = DispatchError;
|
||||||
@@ -241,7 +237,7 @@ where
|
|||||||
impl<T, S, B> H2ServiceHandler<T, S, B>
|
impl<T, S, B> H2ServiceHandler<T, S, B>
|
||||||
where
|
where
|
||||||
S: Service<Request>,
|
S: Service<Request>,
|
||||||
S::Error: Into<Response<AnyBody>> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
@@ -264,27 +260,25 @@ impl<T, S, B> Service<(T, Option<net::SocketAddr>)> for H2ServiceHandler<T, S, B
|
|||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
S: Service<Request>,
|
S: Service<Request>,
|
||||||
S::Error: Into<Response<AnyBody>> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
B::Error: Into<Box<dyn StdError>>,
|
|
||||||
{
|
{
|
||||||
type Response = ();
|
type Response = ();
|
||||||
type Error = DispatchError;
|
type Error = DispatchError;
|
||||||
type Future = H2ServiceHandlerResponse<T, S, B>;
|
type Future = H2ServiceHandlerResponse<T, S, B>;
|
||||||
|
|
||||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
self.flow.service.poll_ready(cx).map_err(|e| {
|
self.flow.service.poll_ready(cx).map_err(|err| {
|
||||||
let e = e.into();
|
let err = err.into();
|
||||||
error!("Service readiness error: {:?}", e);
|
error!("Service readiness error: {:?}", err);
|
||||||
DispatchError::Service(e)
|
DispatchError::Service(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
|
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
|
||||||
let on_connect_data =
|
let on_connect_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
|
||||||
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
|
|
||||||
|
|
||||||
H2ServiceHandlerResponse {
|
H2ServiceHandlerResponse {
|
||||||
state: State::Handshake(
|
state: State::Handshake(
|
||||||
@@ -292,7 +286,7 @@ where
|
|||||||
Some(self.cfg.clone()),
|
Some(self.cfg.clone()),
|
||||||
addr,
|
addr,
|
||||||
on_connect_data,
|
on_connect_data,
|
||||||
h2_handshake(io),
|
handshake_with_timeout(io, &self.cfg),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,21 +297,21 @@ where
|
|||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
{
|
{
|
||||||
Incoming(Dispatcher<T, S, B, (), ()>),
|
|
||||||
Handshake(
|
Handshake(
|
||||||
Option<Rc<HttpFlow<S, (), ()>>>,
|
Option<Rc<HttpFlow<S, (), ()>>>,
|
||||||
Option<ServiceConfig>,
|
Option<ServiceConfig>,
|
||||||
Option<net::SocketAddr>,
|
Option<net::SocketAddr>,
|
||||||
OnConnectData,
|
OnConnectData,
|
||||||
H2Handshake<T, Bytes>,
|
HandshakeWithTimeout<T>,
|
||||||
),
|
),
|
||||||
|
Established(Dispatcher<T, S, B, (), ()>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct H2ServiceHandlerResponse<T, S, B>
|
pub struct H2ServiceHandlerResponse<T, S, B>
|
||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
S: Service<Request>,
|
S: Service<Request>,
|
||||||
S::Error: Into<Response<AnyBody>> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
@@ -329,40 +323,44 @@ impl<T, S, B> Future for H2ServiceHandlerResponse<T, S, B>
|
|||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
S: Service<Request>,
|
S: Service<Request>,
|
||||||
S::Error: Into<Response<AnyBody>> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
B::Error: Into<Box<dyn StdError>>,
|
|
||||||
{
|
{
|
||||||
type Output = Result<(), DispatchError>;
|
type Output = Result<(), DispatchError>;
|
||||||
|
|
||||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
match self.state {
|
match self.state {
|
||||||
State::Incoming(ref mut disp) => Pin::new(disp).poll(cx),
|
|
||||||
State::Handshake(
|
State::Handshake(
|
||||||
ref mut srv,
|
ref mut srv,
|
||||||
ref mut config,
|
ref mut config,
|
||||||
ref peer_addr,
|
ref peer_addr,
|
||||||
ref mut on_connect_data,
|
ref mut conn_data,
|
||||||
ref mut handshake,
|
ref mut handshake,
|
||||||
) => match ready!(Pin::new(handshake).poll(cx)) {
|
) => match ready!(Pin::new(handshake).poll(cx)) {
|
||||||
Ok(conn) => {
|
Ok((conn, timer)) => {
|
||||||
let on_connect_data = std::mem::take(on_connect_data);
|
let on_connect_data = mem::take(conn_data);
|
||||||
self.state = State::Incoming(Dispatcher::new(
|
|
||||||
srv.take().unwrap(),
|
self.state = State::Established(Dispatcher::new(
|
||||||
conn,
|
conn,
|
||||||
on_connect_data,
|
srv.take().unwrap(),
|
||||||
config.take().unwrap(),
|
config.take().unwrap(),
|
||||||
*peer_addr,
|
*peer_addr,
|
||||||
|
on_connect_data,
|
||||||
|
timer,
|
||||||
));
|
));
|
||||||
|
|
||||||
self.poll(cx)
|
self.poll(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
trace!("H2 handshake error: {}", err);
|
log::trace!("H2 handshake error: {}", err);
|
||||||
Poll::Ready(Err(err.into()))
|
Poll::Ready(Err(err))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
State::Established(ref mut disp) => Pin::new(disp).poll(cx),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
//! Helper trait for types that can be effectively borrowed as a [HeaderValue].
|
//! Sealed [`AsHeaderName`] trait and implementations.
|
||||||
//!
|
|
||||||
//! [HeaderValue]: crate::http::HeaderValue
|
|
||||||
|
|
||||||
use std::{borrow::Cow, str::FromStr};
|
use std::{borrow::Cow, str::FromStr as _};
|
||||||
|
|
||||||
use http::header::{HeaderName, InvalidHeaderName};
|
use http::header::{HeaderName, InvalidHeaderName};
|
||||||
|
|
||||||
|
/// Sealed trait implemented for types that can be effectively borrowed as a [`HeaderValue`].
|
||||||
|
///
|
||||||
|
/// [`HeaderValue`]: super::HeaderValue
|
||||||
pub trait AsHeaderName: Sealed {}
|
pub trait AsHeaderName: Sealed {}
|
||||||
|
|
||||||
pub struct Seal;
|
pub struct Seal;
|
||||||
@@ -15,6 +16,7 @@ pub trait Sealed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Sealed for HeaderName {
|
impl Sealed for HeaderName {
|
||||||
|
#[inline]
|
||||||
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||||
Ok(Cow::Borrowed(self))
|
Ok(Cow::Borrowed(self))
|
||||||
}
|
}
|
||||||
@@ -22,6 +24,7 @@ impl Sealed for HeaderName {
|
|||||||
impl AsHeaderName for HeaderName {}
|
impl AsHeaderName for HeaderName {}
|
||||||
|
|
||||||
impl Sealed for &HeaderName {
|
impl Sealed for &HeaderName {
|
||||||
|
#[inline]
|
||||||
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||||
Ok(Cow::Borrowed(*self))
|
Ok(Cow::Borrowed(*self))
|
||||||
}
|
}
|
||||||
@@ -29,6 +32,7 @@ impl Sealed for &HeaderName {
|
|||||||
impl AsHeaderName for &HeaderName {}
|
impl AsHeaderName for &HeaderName {}
|
||||||
|
|
||||||
impl Sealed for &str {
|
impl Sealed for &str {
|
||||||
|
#[inline]
|
||||||
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||||
HeaderName::from_str(self).map(Cow::Owned)
|
HeaderName::from_str(self).map(Cow::Owned)
|
||||||
}
|
}
|
||||||
@@ -36,6 +40,7 @@ impl Sealed for &str {
|
|||||||
impl AsHeaderName for &str {}
|
impl AsHeaderName for &str {}
|
||||||
|
|
||||||
impl Sealed for String {
|
impl Sealed for String {
|
||||||
|
#[inline]
|
||||||
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||||
HeaderName::from_str(self).map(Cow::Owned)
|
HeaderName::from_str(self).map(Cow::Owned)
|
||||||
}
|
}
|
||||||
@@ -43,6 +48,7 @@ impl Sealed for String {
|
|||||||
impl AsHeaderName for String {}
|
impl AsHeaderName for String {}
|
||||||
|
|
||||||
impl Sealed for &String {
|
impl Sealed for &String {
|
||||||
|
#[inline]
|
||||||
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||||
HeaderName::from_str(self).map(Cow::Owned)
|
HeaderName::from_str(self).map(Cow::Owned)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
use std::convert::TryFrom;
|
//! [`TryIntoHeaderPair`] trait and implementations.
|
||||||
|
|
||||||
use http::{
|
use std::convert::TryFrom as _;
|
||||||
header::{HeaderName, InvalidHeaderName, InvalidHeaderValue},
|
|
||||||
Error as HttpError, HeaderValue,
|
use super::{
|
||||||
|
Header, HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, TryIntoHeaderValue,
|
||||||
};
|
};
|
||||||
|
use crate::error::HttpError;
|
||||||
|
|
||||||
use super::{Header, IntoHeaderValue};
|
/// An interface for types that can be converted into a [`HeaderName`] + [`HeaderValue`] pair for
|
||||||
|
/// insertion into a [`HeaderMap`].
|
||||||
/// Transforms structures into header K/V pairs for inserting into `HeaderMap`s.
|
///
|
||||||
pub trait IntoHeaderPair: Sized {
|
/// [`HeaderMap`]: super::HeaderMap
|
||||||
|
pub trait TryIntoHeaderPair: Sized {
|
||||||
type Error: Into<HttpError>;
|
type Error: Into<HttpError>;
|
||||||
|
|
||||||
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error>;
|
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -29,14 +32,14 @@ impl From<InvalidHeaderPart> for HttpError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V> IntoHeaderPair for (HeaderName, V)
|
impl<V> TryIntoHeaderPair for (HeaderName, V)
|
||||||
where
|
where
|
||||||
V: IntoHeaderValue,
|
V: TryIntoHeaderValue,
|
||||||
V::Error: Into<InvalidHeaderValue>,
|
V::Error: Into<InvalidHeaderValue>,
|
||||||
{
|
{
|
||||||
type Error = InvalidHeaderPart;
|
type Error = InvalidHeaderPart;
|
||||||
|
|
||||||
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||||
let (name, value) = self;
|
let (name, value) = self;
|
||||||
let value = value
|
let value = value
|
||||||
.try_into_value()
|
.try_into_value()
|
||||||
@@ -45,14 +48,14 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V> IntoHeaderPair for (&HeaderName, V)
|
impl<V> TryIntoHeaderPair for (&HeaderName, V)
|
||||||
where
|
where
|
||||||
V: IntoHeaderValue,
|
V: TryIntoHeaderValue,
|
||||||
V::Error: Into<InvalidHeaderValue>,
|
V::Error: Into<InvalidHeaderValue>,
|
||||||
{
|
{
|
||||||
type Error = InvalidHeaderPart;
|
type Error = InvalidHeaderPart;
|
||||||
|
|
||||||
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||||
let (name, value) = self;
|
let (name, value) = self;
|
||||||
let value = value
|
let value = value
|
||||||
.try_into_value()
|
.try_into_value()
|
||||||
@@ -61,14 +64,14 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V> IntoHeaderPair for (&[u8], V)
|
impl<V> TryIntoHeaderPair for (&[u8], V)
|
||||||
where
|
where
|
||||||
V: IntoHeaderValue,
|
V: TryIntoHeaderValue,
|
||||||
V::Error: Into<InvalidHeaderValue>,
|
V::Error: Into<InvalidHeaderValue>,
|
||||||
{
|
{
|
||||||
type Error = InvalidHeaderPart;
|
type Error = InvalidHeaderPart;
|
||||||
|
|
||||||
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||||
let (name, value) = self;
|
let (name, value) = self;
|
||||||
let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?;
|
let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?;
|
||||||
let value = value
|
let value = value
|
||||||
@@ -78,14 +81,14 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V> IntoHeaderPair for (&str, V)
|
impl<V> TryIntoHeaderPair for (&str, V)
|
||||||
where
|
where
|
||||||
V: IntoHeaderValue,
|
V: TryIntoHeaderValue,
|
||||||
V::Error: Into<InvalidHeaderValue>,
|
V::Error: Into<InvalidHeaderValue>,
|
||||||
{
|
{
|
||||||
type Error = InvalidHeaderPart;
|
type Error = InvalidHeaderPart;
|
||||||
|
|
||||||
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||||
let (name, value) = self;
|
let (name, value) = self;
|
||||||
let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?;
|
let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?;
|
||||||
let value = value
|
let value = value
|
||||||
@@ -95,23 +98,25 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V> IntoHeaderPair for (String, V)
|
impl<V> TryIntoHeaderPair for (String, V)
|
||||||
where
|
where
|
||||||
V: IntoHeaderValue,
|
V: TryIntoHeaderValue,
|
||||||
V::Error: Into<InvalidHeaderValue>,
|
V::Error: Into<InvalidHeaderValue>,
|
||||||
{
|
{
|
||||||
type Error = InvalidHeaderPart;
|
type Error = InvalidHeaderPart;
|
||||||
|
|
||||||
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
#[inline]
|
||||||
|
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||||
let (name, value) = self;
|
let (name, value) = self;
|
||||||
(name.as_str(), value).try_into_header_pair()
|
(name.as_str(), value).try_into_pair()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Header> IntoHeaderPair for T {
|
impl<T: Header> TryIntoHeaderPair for T {
|
||||||
type Error = <T as IntoHeaderValue>::Error;
|
type Error = <T as TryIntoHeaderValue>::Error;
|
||||||
|
|
||||||
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
#[inline]
|
||||||
|
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||||
Ok((T::name(), self.try_into_value()?))
|
Ok((T::name(), self.try_into_value()?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
use std::convert::TryFrom;
|
//! [`TryIntoHeaderValue`] trait and implementations.
|
||||||
|
|
||||||
|
use std::convert::TryFrom as _;
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue};
|
use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue};
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
|
|
||||||
/// A trait for any object that can be Converted to a `HeaderValue`
|
/// An interface for types that can be converted into a [`HeaderValue`].
|
||||||
pub trait IntoHeaderValue: Sized {
|
pub trait TryIntoHeaderValue: Sized {
|
||||||
/// The type returned in the event of a conversion error.
|
/// The type returned in the event of a conversion error.
|
||||||
type Error: Into<HttpError>;
|
type Error: Into<HttpError>;
|
||||||
|
|
||||||
@@ -13,7 +15,7 @@ pub trait IntoHeaderValue: Sized {
|
|||||||
fn try_into_value(self) -> Result<HeaderValue, Self::Error>;
|
fn try_into_value(self) -> Result<HeaderValue, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHeaderValue for HeaderValue {
|
impl TryIntoHeaderValue for HeaderValue {
|
||||||
type Error = InvalidHeaderValue;
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -22,7 +24,7 @@ impl IntoHeaderValue for HeaderValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHeaderValue for &HeaderValue {
|
impl TryIntoHeaderValue for &HeaderValue {
|
||||||
type Error = InvalidHeaderValue;
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -31,7 +33,7 @@ impl IntoHeaderValue for &HeaderValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHeaderValue for &str {
|
impl TryIntoHeaderValue for &str {
|
||||||
type Error = InvalidHeaderValue;
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -40,7 +42,7 @@ impl IntoHeaderValue for &str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHeaderValue for &[u8] {
|
impl TryIntoHeaderValue for &[u8] {
|
||||||
type Error = InvalidHeaderValue;
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -49,7 +51,7 @@ impl IntoHeaderValue for &[u8] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHeaderValue for Bytes {
|
impl TryIntoHeaderValue for Bytes {
|
||||||
type Error = InvalidHeaderValue;
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -58,7 +60,7 @@ impl IntoHeaderValue for Bytes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHeaderValue for Vec<u8> {
|
impl TryIntoHeaderValue for Vec<u8> {
|
||||||
type Error = InvalidHeaderValue;
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -67,7 +69,7 @@ impl IntoHeaderValue for Vec<u8> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHeaderValue for String {
|
impl TryIntoHeaderValue for String {
|
||||||
type Error = InvalidHeaderValue;
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -76,7 +78,7 @@ impl IntoHeaderValue for String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHeaderValue for usize {
|
impl TryIntoHeaderValue for usize {
|
||||||
type Error = InvalidHeaderValue;
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -85,7 +87,7 @@ impl IntoHeaderValue for usize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHeaderValue for i64 {
|
impl TryIntoHeaderValue for i64 {
|
||||||
type Error = InvalidHeaderValue;
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -94,7 +96,7 @@ impl IntoHeaderValue for i64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHeaderValue for u64 {
|
impl TryIntoHeaderValue for u64 {
|
||||||
type Error = InvalidHeaderValue;
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -103,7 +105,7 @@ impl IntoHeaderValue for u64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHeaderValue for i32 {
|
impl TryIntoHeaderValue for i32 {
|
||||||
type Error = InvalidHeaderValue;
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -112,7 +114,7 @@ impl IntoHeaderValue for i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHeaderValue for u32 {
|
impl TryIntoHeaderValue for u32 {
|
||||||
type Error = InvalidHeaderValue;
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -121,7 +123,7 @@ impl IntoHeaderValue for u32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHeaderValue for Mime {
|
impl TryIntoHeaderValue for Mime {
|
||||||
type Error = InvalidHeaderValue;
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
//! A multi-value [`HeaderMap`] and its iterators.
|
//! A multi-value [`HeaderMap`] and its iterators.
|
||||||
|
|
||||||
use std::{borrow::Cow, collections::hash_map, ops};
|
use std::{borrow::Cow, collections::hash_map, iter, ops};
|
||||||
|
|
||||||
use ahash::AHashMap;
|
use ahash::AHashMap;
|
||||||
use http::header::{HeaderName, HeaderValue};
|
use http::header::{HeaderName, HeaderValue};
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
|
||||||
use crate::header::AsHeaderName;
|
use super::AsHeaderName;
|
||||||
|
|
||||||
/// A multi-map of HTTP headers.
|
/// A multi-map of HTTP headers.
|
||||||
///
|
///
|
||||||
@@ -14,7 +14,7 @@ use crate::header::AsHeaderName;
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_http::http::{header, HeaderMap, HeaderValue};
|
/// use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
///
|
///
|
||||||
/// let mut map = HeaderMap::new();
|
/// let mut map = HeaderMap::new();
|
||||||
///
|
///
|
||||||
@@ -75,7 +75,7 @@ impl HeaderMap {
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::HeaderMap;
|
/// # use actix_http::header::HeaderMap;
|
||||||
/// let map = HeaderMap::new();
|
/// let map = HeaderMap::new();
|
||||||
///
|
///
|
||||||
/// assert!(map.is_empty());
|
/// assert!(map.is_empty());
|
||||||
@@ -92,7 +92,7 @@ impl HeaderMap {
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::HeaderMap;
|
/// # use actix_http::header::HeaderMap;
|
||||||
/// let map = HeaderMap::with_capacity(16);
|
/// let map = HeaderMap::with_capacity(16);
|
||||||
///
|
///
|
||||||
/// assert!(map.is_empty());
|
/// assert!(map.is_empty());
|
||||||
@@ -123,8 +123,7 @@ impl HeaderMap {
|
|||||||
let mut map = HeaderMap::with_capacity(capacity);
|
let mut map = HeaderMap::with_capacity(capacity);
|
||||||
map.append(first_name.clone(), first_value);
|
map.append(first_name.clone(), first_value);
|
||||||
|
|
||||||
let (map, _) =
|
let (map, _) = drain.fold((map, first_name), |(mut map, prev_name), (name, value)| {
|
||||||
drain.fold((map, first_name), |(mut map, prev_name), (name, value)| {
|
|
||||||
let name = name.unwrap_or(prev_name);
|
let name = name.unwrap_or(prev_name);
|
||||||
map.append(name.clone(), value);
|
map.append(name.clone(), value);
|
||||||
(map, name)
|
(map, name)
|
||||||
@@ -139,7 +138,7 @@ impl HeaderMap {
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
/// let mut map = HeaderMap::new();
|
/// let mut map = HeaderMap::new();
|
||||||
/// assert_eq!(map.len(), 0);
|
/// assert_eq!(map.len(), 0);
|
||||||
///
|
///
|
||||||
@@ -162,7 +161,7 @@ impl HeaderMap {
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
/// let mut map = HeaderMap::new();
|
/// let mut map = HeaderMap::new();
|
||||||
/// assert_eq!(map.len_keys(), 0);
|
/// assert_eq!(map.len_keys(), 0);
|
||||||
///
|
///
|
||||||
@@ -181,7 +180,7 @@ impl HeaderMap {
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
/// let mut map = HeaderMap::new();
|
/// let mut map = HeaderMap::new();
|
||||||
/// assert!(map.is_empty());
|
/// assert!(map.is_empty());
|
||||||
///
|
///
|
||||||
@@ -198,7 +197,7 @@ impl HeaderMap {
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
/// let mut map = HeaderMap::new();
|
/// let mut map = HeaderMap::new();
|
||||||
///
|
///
|
||||||
/// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain"));
|
/// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain"));
|
||||||
@@ -231,7 +230,7 @@ impl HeaderMap {
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
/// let mut map = HeaderMap::new();
|
/// let mut map = HeaderMap::new();
|
||||||
///
|
///
|
||||||
/// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1"));
|
/// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1"));
|
||||||
@@ -264,7 +263,7 @@ impl HeaderMap {
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
/// let mut map = HeaderMap::new();
|
/// let mut map = HeaderMap::new();
|
||||||
///
|
///
|
||||||
/// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1"));
|
/// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1"));
|
||||||
@@ -288,12 +287,12 @@ impl HeaderMap {
|
|||||||
/// Returns an iterator over all values associated with a header name.
|
/// Returns an iterator over all values associated with a header name.
|
||||||
///
|
///
|
||||||
/// The returned iterator does not incur any allocations and will yield no items if there are no
|
/// The returned iterator does not incur any allocations and will yield no items if there are no
|
||||||
/// values associated with the key. Iteration order is **not** guaranteed to be the same as
|
/// values associated with the key. Iteration order is guaranteed to be the same as
|
||||||
/// insertion order.
|
/// insertion order.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
/// let mut map = HeaderMap::new();
|
/// let mut map = HeaderMap::new();
|
||||||
///
|
///
|
||||||
/// let mut none_iter = map.get_all(header::ORIGIN);
|
/// let mut none_iter = map.get_all(header::ORIGIN);
|
||||||
@@ -307,8 +306,11 @@ impl HeaderMap {
|
|||||||
/// assert_eq!(set_cookies_iter.next().unwrap(), "two=2");
|
/// assert_eq!(set_cookies_iter.next().unwrap(), "two=2");
|
||||||
/// assert!(set_cookies_iter.next().is_none());
|
/// assert!(set_cookies_iter.next().is_none());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn get_all(&self, key: impl AsHeaderName) -> GetAll<'_> {
|
pub fn get_all(&self, key: impl AsHeaderName) -> std::slice::Iter<'_, HeaderValue> {
|
||||||
GetAll::new(self.get_value(key))
|
match self.get_value(key) {
|
||||||
|
Some(value) => value.iter(),
|
||||||
|
None => (&[]).iter(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: get_all_mut ?
|
// TODO: get_all_mut ?
|
||||||
@@ -319,7 +321,7 @@ impl HeaderMap {
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
/// let mut map = HeaderMap::new();
|
/// let mut map = HeaderMap::new();
|
||||||
/// assert!(!map.contains_key(header::ACCEPT));
|
/// assert!(!map.contains_key(header::ACCEPT));
|
||||||
///
|
///
|
||||||
@@ -334,7 +336,7 @@ impl HeaderMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inserts a name-value pair into the map.
|
/// Inserts (overrides) a name-value pair in the map.
|
||||||
///
|
///
|
||||||
/// If the map already contained this key, the new value is associated with the key and all
|
/// If the map already contained this key, the new value is associated with the key and all
|
||||||
/// previous values are removed and returned as a `Removed` iterator. The key is not updated;
|
/// previous values are removed and returned as a `Removed` iterator. The key is not updated;
|
||||||
@@ -342,7 +344,7 @@ impl HeaderMap {
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
/// let mut map = HeaderMap::new();
|
/// let mut map = HeaderMap::new();
|
||||||
///
|
///
|
||||||
/// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain"));
|
/// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain"));
|
||||||
@@ -355,12 +357,25 @@ impl HeaderMap {
|
|||||||
///
|
///
|
||||||
/// assert_eq!(map.len(), 1);
|
/// assert_eq!(map.len(), 1);
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// A convenience method is provided on the returned iterator to check if the insertion replaced
|
||||||
|
/// any values.
|
||||||
|
/// ```
|
||||||
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
|
/// let mut map = HeaderMap::new();
|
||||||
|
///
|
||||||
|
/// let removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/plain"));
|
||||||
|
/// assert!(removed.is_empty());
|
||||||
|
///
|
||||||
|
/// let removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/html"));
|
||||||
|
/// assert!(!removed.is_empty());
|
||||||
|
/// ```
|
||||||
pub fn insert(&mut self, key: HeaderName, val: HeaderValue) -> Removed {
|
pub fn insert(&mut self, key: HeaderName, val: HeaderValue) -> Removed {
|
||||||
let value = self.inner.insert(key, Value::one(val));
|
let value = self.inner.insert(key, Value::one(val));
|
||||||
Removed::new(value)
|
Removed::new(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inserts a name-value pair into the map.
|
/// Appends a name-value pair to the map.
|
||||||
///
|
///
|
||||||
/// If the map already contained this key, the new value is added to the list of values
|
/// If the map already contained this key, the new value is added to the list of values
|
||||||
/// currently associated with the key. The key is not updated; this matters for types that can
|
/// currently associated with the key. The key is not updated; this matters for types that can
|
||||||
@@ -368,7 +383,7 @@ impl HeaderMap {
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
/// let mut map = HeaderMap::new();
|
/// let mut map = HeaderMap::new();
|
||||||
///
|
///
|
||||||
/// map.append(header::HOST, HeaderValue::from_static("example.com"));
|
/// map.append(header::HOST, HeaderValue::from_static("example.com"));
|
||||||
@@ -393,9 +408,12 @@ impl HeaderMap {
|
|||||||
|
|
||||||
/// Removes all headers for a particular header name from the map.
|
/// Removes all headers for a particular header name from the map.
|
||||||
///
|
///
|
||||||
|
/// Providing an invalid header names (as a string argument) will have no effect and return
|
||||||
|
/// without error.
|
||||||
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
/// let mut map = HeaderMap::new();
|
/// let mut map = HeaderMap::new();
|
||||||
///
|
///
|
||||||
/// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1"));
|
/// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1"));
|
||||||
@@ -409,6 +427,21 @@ impl HeaderMap {
|
|||||||
/// assert!(removed.next().is_none());
|
/// assert!(removed.next().is_none());
|
||||||
///
|
///
|
||||||
/// assert!(map.is_empty());
|
/// assert!(map.is_empty());
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// A convenience method is provided on the returned iterator to check if the `remove` call
|
||||||
|
/// actually removed any values.
|
||||||
|
/// ```
|
||||||
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
|
/// let mut map = HeaderMap::new();
|
||||||
|
///
|
||||||
|
/// let removed = map.remove("accept");
|
||||||
|
/// assert!(removed.is_empty());
|
||||||
|
///
|
||||||
|
/// map.insert(header::ACCEPT, HeaderValue::from_static("text/html"));
|
||||||
|
/// let removed = map.remove("accept");
|
||||||
|
/// assert!(!removed.is_empty());
|
||||||
|
/// ```
|
||||||
pub fn remove(&mut self, key: impl AsHeaderName) -> Removed {
|
pub fn remove(&mut self, key: impl AsHeaderName) -> Removed {
|
||||||
let value = match key.try_as_name(super::as_name::Seal) {
|
let value = match key.try_as_name(super::as_name::Seal) {
|
||||||
Ok(Cow::Borrowed(name)) => self.inner.remove(name),
|
Ok(Cow::Borrowed(name)) => self.inner.remove(name),
|
||||||
@@ -428,7 +461,7 @@ impl HeaderMap {
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::HeaderMap;
|
/// # use actix_http::header::HeaderMap;
|
||||||
/// let map = HeaderMap::with_capacity(16);
|
/// let map = HeaderMap::with_capacity(16);
|
||||||
///
|
///
|
||||||
/// assert!(map.is_empty());
|
/// assert!(map.is_empty());
|
||||||
@@ -448,7 +481,7 @@ impl HeaderMap {
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::HeaderMap;
|
/// # use actix_http::header::HeaderMap;
|
||||||
/// let mut map = HeaderMap::with_capacity(2);
|
/// let mut map = HeaderMap::with_capacity(2);
|
||||||
/// assert!(map.capacity() >= 2);
|
/// assert!(map.capacity() >= 2);
|
||||||
///
|
///
|
||||||
@@ -468,7 +501,7 @@ impl HeaderMap {
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
/// let mut map = HeaderMap::new();
|
/// let mut map = HeaderMap::new();
|
||||||
///
|
///
|
||||||
/// let mut iter = map.iter();
|
/// let mut iter = map.iter();
|
||||||
@@ -500,7 +533,7 @@ impl HeaderMap {
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
/// let mut map = HeaderMap::new();
|
/// let mut map = HeaderMap::new();
|
||||||
///
|
///
|
||||||
/// let mut iter = map.keys();
|
/// let mut iter = map.keys();
|
||||||
@@ -528,7 +561,7 @@ impl HeaderMap {
|
|||||||
/// Keeps the allocated memory for reuse.
|
/// Keeps the allocated memory for reuse.
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||||
/// let mut map = HeaderMap::new();
|
/// let mut map = HeaderMap::new();
|
||||||
///
|
///
|
||||||
/// let mut iter = map.drain();
|
/// let mut iter = map.drain();
|
||||||
@@ -550,7 +583,8 @@ impl HeaderMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Note that this implementation will clone a [HeaderName] for each value.
|
/// Note that this implementation will clone a [HeaderName] for each value. Consider using
|
||||||
|
/// [`drain`](Self::drain) to control header name cloning.
|
||||||
impl IntoIterator for HeaderMap {
|
impl IntoIterator for HeaderMap {
|
||||||
type Item = (HeaderName, HeaderValue);
|
type Item = (HeaderName, HeaderValue);
|
||||||
type IntoIter = IntoIter;
|
type IntoIter = IntoIter;
|
||||||
@@ -571,60 +605,39 @@ impl<'a> IntoIterator for &'a HeaderMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterator for all values with the same header name.
|
/// Convert `http::HeaderMap` to our `HeaderMap`.
|
||||||
|
impl From<http::HeaderMap> for HeaderMap {
|
||||||
|
fn from(mut map: http::HeaderMap) -> HeaderMap {
|
||||||
|
HeaderMap::from_drain(map.drain())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator over removed, owned values with the same associated name.
|
||||||
///
|
///
|
||||||
/// See [`HeaderMap::get_all`].
|
/// Returned from methods that remove or replace items. See [`HeaderMap::insert`]
|
||||||
#[derive(Debug)]
|
/// and [`HeaderMap::remove`].
|
||||||
pub struct GetAll<'a> {
|
|
||||||
idx: usize,
|
|
||||||
value: Option<&'a Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> GetAll<'a> {
|
|
||||||
fn new(value: Option<&'a Value>) -> Self {
|
|
||||||
Self { idx: 0, value }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Iterator for GetAll<'a> {
|
|
||||||
type Item = &'a HeaderValue;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
let val = self.value?;
|
|
||||||
|
|
||||||
match val.get(self.idx) {
|
|
||||||
Some(val) => {
|
|
||||||
self.idx += 1;
|
|
||||||
Some(val)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// current index is none; remove value to fast-path future next calls
|
|
||||||
self.value = None;
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
|
||||||
match self.value {
|
|
||||||
Some(val) => (val.len(), Some(val.len())),
|
|
||||||
None => (0, Some(0)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterator for owned [`HeaderValue`]s with the same associated [`HeaderName`] returned from methods
|
|
||||||
/// on [`HeaderMap`] that remove or replace items.
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Removed {
|
pub struct Removed {
|
||||||
inner: Option<smallvec::IntoIter<[HeaderValue; 4]>>,
|
inner: Option<smallvec::IntoIter<[HeaderValue; 4]>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Removed {
|
impl Removed {
|
||||||
fn new(value: Option<Value>) -> Self {
|
fn new(value: Option<Value>) -> Self {
|
||||||
let inner = value.map(|value| value.inner.into_iter());
|
let inner = value.map(|value| value.inner.into_iter());
|
||||||
Self { inner }
|
Self { inner }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if iterator contains no elements, without consuming it.
|
||||||
|
///
|
||||||
|
/// If called immediately after [`HeaderMap::insert`] or [`HeaderMap::remove`], it will indicate
|
||||||
|
/// whether any items were actually replaced or removed, respectively.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
match self.inner {
|
||||||
|
// size hint lower bound of smallvec is the correct length
|
||||||
|
Some(ref iter) => iter.size_hint().0 == 0,
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Iterator for Removed {
|
impl Iterator for Removed {
|
||||||
@@ -644,7 +657,11 @@ impl Iterator for Removed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterator over all [`HeaderName`]s in the map.
|
impl ExactSizeIterator for Removed {}
|
||||||
|
|
||||||
|
impl iter::FusedIterator for Removed {}
|
||||||
|
|
||||||
|
/// Iterator over all names in the map.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Keys<'a>(hash_map::Keys<'a, HeaderName, Value>);
|
pub struct Keys<'a>(hash_map::Keys<'a, HeaderName, Value>);
|
||||||
|
|
||||||
@@ -662,6 +679,11 @@ impl<'a> Iterator for Keys<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ExactSizeIterator for Keys<'_> {}
|
||||||
|
|
||||||
|
impl iter::FusedIterator for Keys<'_> {}
|
||||||
|
|
||||||
|
/// Iterator over borrowed name-value pairs.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Iter<'a> {
|
pub struct Iter<'a> {
|
||||||
inner: hash_map::Iter<'a, HeaderName, Value>,
|
inner: hash_map::Iter<'a, HeaderName, Value>,
|
||||||
@@ -713,6 +735,10 @@ impl<'a> Iterator for Iter<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ExactSizeIterator for Iter<'_> {}
|
||||||
|
|
||||||
|
impl iter::FusedIterator for Iter<'_> {}
|
||||||
|
|
||||||
/// Iterator over drained name-value pairs.
|
/// Iterator over drained name-value pairs.
|
||||||
///
|
///
|
||||||
/// Iterator items are `(Option<HeaderName>, HeaderValue)` to avoid cloning.
|
/// Iterator items are `(Option<HeaderName>, HeaderValue)` to avoid cloning.
|
||||||
@@ -764,6 +790,10 @@ impl<'a> Iterator for Drain<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ExactSizeIterator for Drain<'_> {}
|
||||||
|
|
||||||
|
impl iter::FusedIterator for Drain<'_> {}
|
||||||
|
|
||||||
/// Iterator over owned name-value pairs.
|
/// Iterator over owned name-value pairs.
|
||||||
///
|
///
|
||||||
/// Implementation necessarily clones header names for each value.
|
/// Implementation necessarily clones header names for each value.
|
||||||
@@ -814,12 +844,27 @@ impl Iterator for IntoIter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ExactSizeIterator for IntoIter {}
|
||||||
|
|
||||||
|
impl iter::FusedIterator for IntoIter {}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::iter::FusedIterator;
|
||||||
|
|
||||||
use http::header;
|
use http::header;
|
||||||
|
use static_assertions::assert_impl_all;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
assert_impl_all!(HeaderMap: IntoIterator);
|
||||||
|
assert_impl_all!(Keys<'_>: Iterator, ExactSizeIterator, FusedIterator);
|
||||||
|
assert_impl_all!(std::slice::Iter<'_, HeaderValue>: Iterator, ExactSizeIterator, FusedIterator);
|
||||||
|
assert_impl_all!(Removed: Iterator, ExactSizeIterator, FusedIterator);
|
||||||
|
assert_impl_all!(Iter<'_>: Iterator, ExactSizeIterator, FusedIterator);
|
||||||
|
assert_impl_all!(IntoIter: Iterator, ExactSizeIterator, FusedIterator);
|
||||||
|
assert_impl_all!(Drain<'_>: Iterator, ExactSizeIterator, FusedIterator);
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create() {
|
fn create() {
|
||||||
let map = HeaderMap::new();
|
let map = HeaderMap::new();
|
||||||
@@ -945,6 +990,56 @@ mod tests {
|
|||||||
assert_eq!(vals.next(), removed.next().as_ref());
|
assert_eq!(vals.next(), removed.next().as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_all_iteration_order_matches_insertion_order() {
|
||||||
|
let mut map = HeaderMap::new();
|
||||||
|
|
||||||
|
let mut vals = map.get_all(header::COOKIE);
|
||||||
|
assert!(vals.next().is_none());
|
||||||
|
|
||||||
|
map.append(header::COOKIE, HeaderValue::from_static("1"));
|
||||||
|
let mut vals = map.get_all(header::COOKIE);
|
||||||
|
assert_eq!(vals.next().unwrap().as_bytes(), b"1");
|
||||||
|
assert!(vals.next().is_none());
|
||||||
|
|
||||||
|
map.append(header::COOKIE, HeaderValue::from_static("2"));
|
||||||
|
let mut vals = map.get_all(header::COOKIE);
|
||||||
|
assert_eq!(vals.next().unwrap().as_bytes(), b"1");
|
||||||
|
assert_eq!(vals.next().unwrap().as_bytes(), b"2");
|
||||||
|
assert!(vals.next().is_none());
|
||||||
|
|
||||||
|
map.append(header::COOKIE, HeaderValue::from_static("3"));
|
||||||
|
map.append(header::COOKIE, HeaderValue::from_static("4"));
|
||||||
|
map.append(header::COOKIE, HeaderValue::from_static("5"));
|
||||||
|
let mut vals = map.get_all(header::COOKIE);
|
||||||
|
assert_eq!(vals.next().unwrap().as_bytes(), b"1");
|
||||||
|
assert_eq!(vals.next().unwrap().as_bytes(), b"2");
|
||||||
|
assert_eq!(vals.next().unwrap().as_bytes(), b"3");
|
||||||
|
assert_eq!(vals.next().unwrap().as_bytes(), b"4");
|
||||||
|
assert_eq!(vals.next().unwrap().as_bytes(), b"5");
|
||||||
|
assert!(vals.next().is_none());
|
||||||
|
|
||||||
|
let _ = map.insert(header::COOKIE, HeaderValue::from_static("6"));
|
||||||
|
let mut vals = map.get_all(header::COOKIE);
|
||||||
|
assert_eq!(vals.next().unwrap().as_bytes(), b"6");
|
||||||
|
assert!(vals.next().is_none());
|
||||||
|
|
||||||
|
let _ = map.insert(header::COOKIE, HeaderValue::from_static("7"));
|
||||||
|
let _ = map.insert(header::COOKIE, HeaderValue::from_static("8"));
|
||||||
|
let mut vals = map.get_all(header::COOKIE);
|
||||||
|
assert_eq!(vals.next().unwrap().as_bytes(), b"8");
|
||||||
|
assert!(vals.next().is_none());
|
||||||
|
|
||||||
|
map.append(header::COOKIE, HeaderValue::from_static("9"));
|
||||||
|
let mut vals = map.get_all(header::COOKIE);
|
||||||
|
assert_eq!(vals.next().unwrap().as_bytes(), b"8");
|
||||||
|
assert_eq!(vals.next().unwrap().as_bytes(), b"9");
|
||||||
|
assert!(vals.next().is_none());
|
||||||
|
|
||||||
|
// check for fused-ness
|
||||||
|
assert!(vals.next().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
fn owned_pair<'a>(
|
fn owned_pair<'a>(
|
||||||
(name, val): (&'a HeaderName, &'a HeaderValue),
|
(name, val): (&'a HeaderName, &'a HeaderValue),
|
||||||
) -> (HeaderName, HeaderValue) {
|
) -> (HeaderName, HeaderValue) {
|
||||||
|
|||||||
@@ -11,64 +11,54 @@ pub use http::header::{
|
|||||||
pub use http::header::{
|
pub use http::header::{
|
||||||
ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES,
|
ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES,
|
||||||
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
|
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
|
||||||
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN,
|
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS,
|
||||||
ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_MAX_AGE,
|
ACCESS_CONTROL_MAX_AGE, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, AGE,
|
||||||
ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, AGE, ALLOW, ALT_SVC,
|
ALLOW, ALT_SVC, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_DISPOSITION,
|
||||||
AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_DISPOSITION, CONTENT_ENCODING,
|
CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_RANGE,
|
||||||
CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_RANGE,
|
CONTENT_SECURITY_POLICY, CONTENT_SECURITY_POLICY_REPORT_ONLY, CONTENT_TYPE, COOKIE, DATE,
|
||||||
CONTENT_SECURITY_POLICY, CONTENT_SECURITY_POLICY_REPORT_ONLY, CONTENT_TYPE, COOKIE,
|
DNT, ETAG, EXPECT, EXPIRES, FORWARDED, FROM, HOST, IF_MATCH, IF_MODIFIED_SINCE,
|
||||||
DATE, DNT, ETAG, EXPECT, EXPIRES, FORWARDED, FROM, HOST, IF_MATCH,
|
IF_NONE_MATCH, IF_RANGE, IF_UNMODIFIED_SINCE, LAST_MODIFIED, LINK, LOCATION, MAX_FORWARDS,
|
||||||
IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_RANGE, IF_UNMODIFIED_SINCE, LAST_MODIFIED,
|
ORIGIN, PRAGMA, PROXY_AUTHENTICATE, PROXY_AUTHORIZATION, PUBLIC_KEY_PINS,
|
||||||
LINK, LOCATION, MAX_FORWARDS, ORIGIN, PRAGMA, PROXY_AUTHENTICATE,
|
PUBLIC_KEY_PINS_REPORT_ONLY, RANGE, REFERER, REFERRER_POLICY, REFRESH, RETRY_AFTER,
|
||||||
PROXY_AUTHORIZATION, PUBLIC_KEY_PINS, PUBLIC_KEY_PINS_REPORT_ONLY, RANGE, REFERER,
|
SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_EXTENSIONS, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_PROTOCOL,
|
||||||
REFERRER_POLICY, REFRESH, RETRY_AFTER, SEC_WEBSOCKET_ACCEPT,
|
|
||||||
SEC_WEBSOCKET_EXTENSIONS, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_PROTOCOL,
|
|
||||||
SEC_WEBSOCKET_VERSION, SERVER, SET_COOKIE, STRICT_TRANSPORT_SECURITY, TE, TRAILER,
|
SEC_WEBSOCKET_VERSION, SERVER, SET_COOKIE, STRICT_TRANSPORT_SECURITY, TE, TRAILER,
|
||||||
TRANSFER_ENCODING, UPGRADE, UPGRADE_INSECURE_REQUESTS, USER_AGENT, VARY, VIA,
|
TRANSFER_ENCODING, UPGRADE, UPGRADE_INSECURE_REQUESTS, USER_AGENT, VARY, VIA, WARNING,
|
||||||
WARNING, WWW_AUTHENTICATE, X_CONTENT_TYPE_OPTIONS, X_DNS_PREFETCH_CONTROL,
|
WWW_AUTHENTICATE, X_CONTENT_TYPE_OPTIONS, X_DNS_PREFETCH_CONTROL, X_FRAME_OPTIONS,
|
||||||
X_FRAME_OPTIONS, X_XSS_PROTECTION,
|
X_XSS_PROTECTION,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::error::ParseError;
|
use crate::{error::ParseError, HttpMessage};
|
||||||
use crate::HttpMessage;
|
|
||||||
|
|
||||||
mod as_name;
|
mod as_name;
|
||||||
mod into_pair;
|
mod into_pair;
|
||||||
mod into_value;
|
mod into_value;
|
||||||
|
pub mod map;
|
||||||
|
mod shared;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub(crate) mod map;
|
|
||||||
mod shared;
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub use self::shared::*;
|
|
||||||
|
|
||||||
pub use self::as_name::AsHeaderName;
|
pub use self::as_name::AsHeaderName;
|
||||||
pub use self::into_pair::IntoHeaderPair;
|
pub use self::into_pair::TryIntoHeaderPair;
|
||||||
pub use self::into_value::IntoHeaderValue;
|
pub use self::into_value::TryIntoHeaderValue;
|
||||||
#[doc(hidden)]
|
|
||||||
pub use self::map::GetAll;
|
|
||||||
pub use self::map::HeaderMap;
|
pub use self::map::HeaderMap;
|
||||||
pub use self::utils::*;
|
pub use self::shared::{
|
||||||
|
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, LanguageTag,
|
||||||
|
Quality, QualityItem,
|
||||||
|
};
|
||||||
|
pub use self::utils::{
|
||||||
|
fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode,
|
||||||
|
};
|
||||||
|
|
||||||
/// A trait for any object that already represents a valid header field and value.
|
/// An interface for types that already represent a valid header.
|
||||||
pub trait Header: IntoHeaderValue {
|
pub trait Header: TryIntoHeaderValue {
|
||||||
/// Returns the name of the header field
|
/// Returns the name of the header field.
|
||||||
fn name() -> HeaderName;
|
fn name() -> HeaderName;
|
||||||
|
|
||||||
/// Parse a header
|
/// Parse the header from a HTTP message.
|
||||||
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>;
|
fn parse<M: HttpMessage>(msg: &M) -> Result<Self, ParseError>;
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert `http::HeaderMap` to our `HeaderMap`.
|
|
||||||
impl From<http::HeaderMap> for HeaderMap {
|
|
||||||
fn from(mut map: http::HeaderMap) -> HeaderMap {
|
|
||||||
HeaderMap::from_drain(map.drain())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This encode set is used for HTTP header values and is defined at
|
/// This encode set is used for HTTP header values and is defined at
|
||||||
/// https://tools.ietf.org/html/rfc5987#section-3.2.
|
/// <https://datatracker.ietf.org/doc/html/rfc5987#section-3.2>.
|
||||||
pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
|
pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
|
||||||
.add(b' ')
|
.add(b' ')
|
||||||
.add(b'"')
|
.add(b'"')
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
use std::fmt::{self, Display};
|
use std::{fmt, str};
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use self::Charset::*;
|
use self::Charset::*;
|
||||||
|
|
||||||
/// A Mime charset.
|
/// A MIME character set.
|
||||||
///
|
///
|
||||||
/// The string representation is normalized to upper case.
|
/// The string representation is normalized to upper case.
|
||||||
///
|
///
|
||||||
/// See <http://www.iana.org/assignments/character-sets/character-sets.xhtml>.
|
/// See <http://www.iana.org/assignments/character-sets/character-sets.xhtml>.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
pub enum Charset {
|
pub enum Charset {
|
||||||
/// US ASCII
|
/// US ASCII
|
||||||
@@ -88,20 +87,20 @@ impl Charset {
|
|||||||
Iso_8859_8_E => "ISO-8859-8-E",
|
Iso_8859_8_E => "ISO-8859-8-E",
|
||||||
Iso_8859_8_I => "ISO-8859-8-I",
|
Iso_8859_8_I => "ISO-8859-8-I",
|
||||||
Gb2312 => "GB2312",
|
Gb2312 => "GB2312",
|
||||||
Big5 => "big5",
|
Big5 => "Big5",
|
||||||
Koi8_R => "KOI8-R",
|
Koi8_R => "KOI8-R",
|
||||||
Ext(ref s) => s,
|
Ext(ref s) => s,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Charset {
|
impl fmt::Display for Charset {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.label())
|
f.write_str(self.label())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Charset {
|
impl str::FromStr for Charset {
|
||||||
type Err = crate::Error;
|
type Err = crate::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Charset, crate::Error> {
|
fn from_str(s: &str) -> Result<Charset, crate::Error> {
|
||||||
@@ -128,7 +127,7 @@ impl FromStr for Charset {
|
|||||||
"ISO-8859-8-E" => Iso_8859_8_E,
|
"ISO-8859-8-E" => Iso_8859_8_E,
|
||||||
"ISO-8859-8-I" => Iso_8859_8_I,
|
"ISO-8859-8-I" => Iso_8859_8_I,
|
||||||
"GB2312" => Gb2312,
|
"GB2312" => Gb2312,
|
||||||
"big5" => Big5,
|
"BIG5" => Big5,
|
||||||
"KOI8-R" => Koi8_R,
|
"KOI8-R" => Koi8_R,
|
||||||
s => Ext(s.to_owned()),
|
s => Ext(s.to_owned()),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,26 +5,31 @@ use http::header::InvalidHeaderValue;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::ParseError,
|
error::ParseError,
|
||||||
header::{self, from_one_raw_str, Header, HeaderName, HeaderValue, IntoHeaderValue},
|
header::{self, from_one_raw_str, Header, HeaderName, HeaderValue, TryIntoHeaderValue},
|
||||||
HttpMessage,
|
HttpMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Error return when a content encoding is unknown.
|
/// Error returned when a content encoding is unknown.
|
||||||
///
|
|
||||||
/// Example: 'compress'
|
|
||||||
#[derive(Debug, Display, Error)]
|
#[derive(Debug, Display, Error)]
|
||||||
#[display(fmt = "unsupported content encoding")]
|
#[display(fmt = "unsupported content encoding")]
|
||||||
pub struct ContentEncodingParseError;
|
pub struct ContentEncodingParseError;
|
||||||
|
|
||||||
/// Represents a supported content encoding.
|
/// Represents a supported content encoding.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
///
|
||||||
|
/// Includes a commonly-used subset of media types appropriate for use as HTTP content encodings.
|
||||||
|
/// See [IANA HTTP Content Coding Registry].
|
||||||
|
///
|
||||||
|
/// [IANA HTTP Content Coding Registry]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum ContentEncoding {
|
pub enum ContentEncoding {
|
||||||
/// Automatically select encoding based on encoding negotiation.
|
/// Indicates the no-op identity encoding.
|
||||||
Auto,
|
///
|
||||||
|
/// I.e., no compression or modification.
|
||||||
|
Identity,
|
||||||
|
|
||||||
/// A format using the Brotli algorithm.
|
/// A format using the Brotli algorithm.
|
||||||
Br,
|
Brotli,
|
||||||
|
|
||||||
/// A format using the zlib structure with deflate algorithm.
|
/// A format using the zlib structure with deflate algorithm.
|
||||||
Deflate,
|
Deflate,
|
||||||
@@ -32,34 +37,38 @@ pub enum ContentEncoding {
|
|||||||
/// Gzip algorithm.
|
/// Gzip algorithm.
|
||||||
Gzip,
|
Gzip,
|
||||||
|
|
||||||
// Zstd algorithm.
|
/// Zstd algorithm.
|
||||||
Zstd,
|
Zstd,
|
||||||
|
|
||||||
/// Indicates the identity function (i.e. no compression, nor modification).
|
|
||||||
Identity,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContentEncoding {
|
impl ContentEncoding {
|
||||||
/// Is the content compressed?
|
|
||||||
#[inline]
|
|
||||||
pub fn is_compression(self) -> bool {
|
|
||||||
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert content encoding to string.
|
/// Convert content encoding to string.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn as_str(self) -> &'static str {
|
pub const fn as_str(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
ContentEncoding::Br => "br",
|
ContentEncoding::Brotli => "br",
|
||||||
ContentEncoding::Gzip => "gzip",
|
ContentEncoding::Gzip => "gzip",
|
||||||
ContentEncoding::Deflate => "deflate",
|
ContentEncoding::Deflate => "deflate",
|
||||||
ContentEncoding::Zstd => "zstd",
|
ContentEncoding::Zstd => "zstd",
|
||||||
ContentEncoding::Identity | ContentEncoding::Auto => "identity",
|
ContentEncoding::Identity => "identity",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert content encoding to header value.
|
||||||
|
#[inline]
|
||||||
|
pub const fn to_header_value(self) -> HeaderValue {
|
||||||
|
match self {
|
||||||
|
ContentEncoding::Brotli => HeaderValue::from_static("br"),
|
||||||
|
ContentEncoding::Gzip => HeaderValue::from_static("gzip"),
|
||||||
|
ContentEncoding::Deflate => HeaderValue::from_static("deflate"),
|
||||||
|
ContentEncoding::Zstd => HeaderValue::from_static("zstd"),
|
||||||
|
ContentEncoding::Identity => HeaderValue::from_static("identity"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ContentEncoding {
|
impl Default for ContentEncoding {
|
||||||
|
#[inline]
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::Identity
|
Self::Identity
|
||||||
}
|
}
|
||||||
@@ -68,16 +77,18 @@ impl Default for ContentEncoding {
|
|||||||
impl FromStr for ContentEncoding {
|
impl FromStr for ContentEncoding {
|
||||||
type Err = ContentEncodingParseError;
|
type Err = ContentEncodingParseError;
|
||||||
|
|
||||||
fn from_str(val: &str) -> Result<Self, Self::Err> {
|
fn from_str(enc: &str) -> Result<Self, Self::Err> {
|
||||||
let val = val.trim();
|
let enc = enc.trim();
|
||||||
|
|
||||||
if val.eq_ignore_ascii_case("br") {
|
if enc.eq_ignore_ascii_case("br") {
|
||||||
Ok(ContentEncoding::Br)
|
Ok(ContentEncoding::Brotli)
|
||||||
} else if val.eq_ignore_ascii_case("gzip") {
|
} else if enc.eq_ignore_ascii_case("gzip") {
|
||||||
Ok(ContentEncoding::Gzip)
|
Ok(ContentEncoding::Gzip)
|
||||||
} else if val.eq_ignore_ascii_case("deflate") {
|
} else if enc.eq_ignore_ascii_case("deflate") {
|
||||||
Ok(ContentEncoding::Deflate)
|
Ok(ContentEncoding::Deflate)
|
||||||
} else if val.eq_ignore_ascii_case("zstd") {
|
} else if enc.eq_ignore_ascii_case("identity") {
|
||||||
|
Ok(ContentEncoding::Identity)
|
||||||
|
} else if enc.eq_ignore_ascii_case("zstd") {
|
||||||
Ok(ContentEncoding::Zstd)
|
Ok(ContentEncoding::Zstd)
|
||||||
} else {
|
} else {
|
||||||
Err(ContentEncodingParseError)
|
Err(ContentEncodingParseError)
|
||||||
@@ -93,7 +104,7 @@ impl TryFrom<&str> for ContentEncoding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHeaderValue for ContentEncoding {
|
impl TryIntoHeaderValue for ContentEncoding {
|
||||||
type Error = InvalidHeaderValue;
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
fn try_into_value(self) -> Result<http::HeaderValue, Self::Error> {
|
fn try_into_value(self) -> Result<http::HeaderValue, Self::Error> {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
|
//! Originally taken from `hyper::header::parsing`.
|
||||||
|
|
||||||
use std::{fmt, str::FromStr};
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
use language_tags::LanguageTag;
|
use language_tags::LanguageTag;
|
||||||
|
|
||||||
use crate::header::{Charset, HTTP_VALUE};
|
use crate::header::{Charset, HTTP_VALUE};
|
||||||
|
|
||||||
// From hyper v0.11.27 src/header/parsing.rs
|
|
||||||
|
|
||||||
/// The value part of an extended parameter consisting of three parts:
|
/// The value part of an extended parameter consisting of three parts:
|
||||||
/// - The REQUIRED character set name (`charset`).
|
/// - The REQUIRED character set name (`charset`).
|
||||||
/// - The OPTIONAL language information (`language_tag`).
|
/// - The OPTIONAL language information (`language_tag`).
|
||||||
/// - A character sequence representing the actual value (`value`), separated by single quotes.
|
/// - A character sequence representing the actual value (`value`), separated by single quotes.
|
||||||
///
|
///
|
||||||
/// It is defined in [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2).
|
/// It is defined in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2).
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct ExtendedValue {
|
pub struct ExtendedValue {
|
||||||
/// The character set that is used to encode the `value` to a string.
|
/// The character set that is used to encode the `value` to a string.
|
||||||
@@ -24,17 +24,17 @@ pub struct ExtendedValue {
|
|||||||
pub value: Vec<u8>,
|
pub value: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses extended header parameter values (`ext-value`), as defined in
|
/// Parses extended header parameter values (`ext-value`), as defined
|
||||||
/// [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2).
|
/// in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2).
|
||||||
///
|
///
|
||||||
/// Extended values are denoted by parameter names that end with `*`.
|
/// Extended values are denoted by parameter names that end with `*`.
|
||||||
///
|
///
|
||||||
/// ## ABNF
|
/// ## ABNF
|
||||||
///
|
///
|
||||||
/// ```text
|
/// ```plain
|
||||||
/// ext-value = charset "'" [ language ] "'" value-chars
|
/// ext-value = charset "'" [ language ] "'" value-chars
|
||||||
/// ; like RFC 2231's <extended-initial-value>
|
/// ; like RFC 2231's <extended-initial-value>
|
||||||
/// ; (see [RFC2231], Section 7)
|
/// ; (see [RFC 2231 §7])
|
||||||
///
|
///
|
||||||
/// charset = "UTF-8" / "ISO-8859-1" / mime-charset
|
/// charset = "UTF-8" / "ISO-8859-1" / mime-charset
|
||||||
///
|
///
|
||||||
@@ -43,25 +43,27 @@ pub struct ExtendedValue {
|
|||||||
/// / "!" / "#" / "$" / "%" / "&"
|
/// / "!" / "#" / "$" / "%" / "&"
|
||||||
/// / "+" / "-" / "^" / "_" / "`"
|
/// / "+" / "-" / "^" / "_" / "`"
|
||||||
/// / "{" / "}" / "~"
|
/// / "{" / "}" / "~"
|
||||||
/// ; as <mime-charset> in Section 2.3 of [RFC2978]
|
/// ; as <mime-charset> in [RFC 2978 §2.3]
|
||||||
/// ; except that the single quote is not included
|
/// ; except that the single quote is not included
|
||||||
/// ; SHOULD be registered in the IANA charset registry
|
/// ; SHOULD be registered in the IANA charset registry
|
||||||
///
|
///
|
||||||
/// language = <Language-Tag, defined in [RFC5646], Section 2.1>
|
/// language = <Language-Tag, defined in [RFC 5646 §2.1]>
|
||||||
///
|
///
|
||||||
/// value-chars = *( pct-encoded / attr-char )
|
/// value-chars = *( pct-encoded / attr-char )
|
||||||
///
|
///
|
||||||
/// pct-encoded = "%" HEXDIG HEXDIG
|
/// pct-encoded = "%" HEXDIG HEXDIG
|
||||||
/// ; see [RFC3986], Section 2.1
|
/// ; see [RFC 3986 §2.1]
|
||||||
///
|
///
|
||||||
/// attr-char = ALPHA / DIGIT
|
/// attr-char = ALPHA / DIGIT
|
||||||
/// / "!" / "#" / "$" / "&" / "+" / "-" / "."
|
/// / "!" / "#" / "$" / "&" / "+" / "-" / "."
|
||||||
/// / "^" / "_" / "`" / "|" / "~"
|
/// / "^" / "_" / "`" / "|" / "~"
|
||||||
/// ; token except ( "*" / "'" / "%" )
|
/// ; token except ( "*" / "'" / "%" )
|
||||||
/// ```
|
/// ```
|
||||||
pub fn parse_extended_value(
|
///
|
||||||
val: &str,
|
/// [RFC 2231 §7]: https://datatracker.ietf.org/doc/html/rfc2231#section-7
|
||||||
) -> Result<ExtendedValue, crate::error::ParseError> {
|
/// [RFC 2978 §2.3]: https://datatracker.ietf.org/doc/html/rfc2978#section-2.3
|
||||||
|
/// [RFC 3986 §2.1]: https://datatracker.ietf.org/doc/html/rfc5646#section-2.1
|
||||||
|
pub fn parse_extended_value(val: &str) -> Result<ExtendedValue, crate::error::ParseError> {
|
||||||
// Break into three pieces separated by the single-quote character
|
// Break into three pieces separated by the single-quote character
|
||||||
let mut parts = val.splitn(3, '\'');
|
let mut parts = val.splitn(3, '\'');
|
||||||
|
|
||||||
@@ -96,8 +98,7 @@ pub fn parse_extended_value(
|
|||||||
|
|
||||||
impl fmt::Display for ExtendedValue {
|
impl fmt::Display for ExtendedValue {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let encoded_value =
|
let encoded_value = percent_encoding::percent_encode(&self.value[..], HTTP_VALUE);
|
||||||
percent_encoding::percent_encode(&self.value[..], HTTP_VALUE);
|
|
||||||
if let Some(ref lang) = self.language_tag {
|
if let Some(ref lang) = self.language_tag {
|
||||||
write!(f, "{}'{}'{}", self.charset, lang, encoded_value)
|
write!(f, "{}'{}'{}", self.charset, lang, encoded_value)
|
||||||
} else {
|
} else {
|
||||||
@@ -139,8 +140,8 @@ mod tests {
|
|||||||
assert!(extended_value.language_tag.is_none());
|
assert!(extended_value.language_tag.is_none());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vec![
|
vec![
|
||||||
194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a',
|
194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', b't',
|
||||||
b't', b'e', b's',
|
b'e', b's',
|
||||||
],
|
],
|
||||||
extended_value.value
|
extended_value.value
|
||||||
);
|
);
|
||||||
@@ -181,8 +182,8 @@ mod tests {
|
|||||||
charset: Charset::Ext("UTF-8".to_string()),
|
charset: Charset::Ext("UTF-8".to_string()),
|
||||||
language_tag: None,
|
language_tag: None,
|
||||||
value: vec![
|
value: vec![
|
||||||
194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a',
|
194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', b't',
|
||||||
b't', b'e', b's',
|
b'e', b's',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
81
actix-http/src/header/shared/http_date.rs
Normal file
81
actix-http/src/header/shared/http_date.rs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
use std::{fmt, io::Write, str::FromStr, time::SystemTime};
|
||||||
|
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use http::header::{HeaderValue, InvalidHeaderValue};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
date::DATE_VALUE_LENGTH, error::ParseError, header::TryIntoHeaderValue, helpers::MutWriter,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A timestamp with HTTP-style formatting and parsing.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct HttpDate(SystemTime);
|
||||||
|
|
||||||
|
impl FromStr for HttpDate {
|
||||||
|
type Err = ParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<HttpDate, ParseError> {
|
||||||
|
match httpdate::parse_http_date(s) {
|
||||||
|
Ok(sys_time) => Ok(HttpDate(sys_time)),
|
||||||
|
Err(_) => Err(ParseError::Header),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for HttpDate {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let date_str = httpdate::fmt_http_date(self.0);
|
||||||
|
f.write_str(&date_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryIntoHeaderValue for HttpDate {
|
||||||
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
|
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||||
|
let mut buf = BytesMut::with_capacity(DATE_VALUE_LENGTH);
|
||||||
|
let mut wrt = MutWriter(&mut buf);
|
||||||
|
|
||||||
|
// unwrap: date output is known to be well formed and of known length
|
||||||
|
write!(wrt, "{}", httpdate::fmt_http_date(self.0)).unwrap();
|
||||||
|
|
||||||
|
HeaderValue::from_maybe_shared(buf.split().freeze())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SystemTime> for HttpDate {
|
||||||
|
fn from(sys_time: SystemTime) -> HttpDate {
|
||||||
|
HttpDate(sys_time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HttpDate> for SystemTime {
|
||||||
|
fn from(HttpDate(sys_time): HttpDate) -> SystemTime {
|
||||||
|
sys_time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn date_header() {
|
||||||
|
macro_rules! assert_parsed_date {
|
||||||
|
($case:expr, $exp:expr) => {
|
||||||
|
assert_eq!($case.parse::<HttpDate>().unwrap(), $exp);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 784198117 = SystemTime::from(datetime!(1994-11-07 08:48:37).assume_utc()).duration_since(SystemTime::UNIX_EPOCH));
|
||||||
|
let nov_07 = HttpDate(SystemTime::UNIX_EPOCH + Duration::from_secs(784198117));
|
||||||
|
|
||||||
|
assert_parsed_date!("Mon, 07 Nov 1994 08:48:37 GMT", nov_07);
|
||||||
|
assert_parsed_date!("Monday, 07-Nov-94 08:48:37 GMT", nov_07);
|
||||||
|
assert_parsed_date!("Mon Nov 7 08:48:37 1994", nov_07);
|
||||||
|
|
||||||
|
assert!("this-is-no-date".parse::<HttpDate>().is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
use std::{
|
|
||||||
fmt,
|
|
||||||
io::Write,
|
|
||||||
str::FromStr,
|
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
|
||||||
};
|
|
||||||
|
|
||||||
use bytes::buf::BufMut;
|
|
||||||
use bytes::BytesMut;
|
|
||||||
use http::header::{HeaderValue, InvalidHeaderValue};
|
|
||||||
use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset};
|
|
||||||
|
|
||||||
use crate::error::ParseError;
|
|
||||||
use crate::header::IntoHeaderValue;
|
|
||||||
use crate::time_parser;
|
|
||||||
|
|
||||||
/// A timestamp with HTTP formatting and parsing.
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct HttpDate(OffsetDateTime);
|
|
||||||
|
|
||||||
impl FromStr for HttpDate {
|
|
||||||
type Err = ParseError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<HttpDate, ParseError> {
|
|
||||||
match time_parser::parse_http_date(s) {
|
|
||||||
Some(t) => Ok(HttpDate(t.assume_utc())),
|
|
||||||
None => Err(ParseError::Header),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for HttpDate {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
fmt::Display::fmt(&self.0.format("%a, %d %b %Y %H:%M:%S GMT"), f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SystemTime> for HttpDate {
|
|
||||||
fn from(sys: SystemTime) -> HttpDate {
|
|
||||||
HttpDate(PrimitiveDateTime::from(sys).assume_utc())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoHeaderValue for HttpDate {
|
|
||||||
type Error = InvalidHeaderValue;
|
|
||||||
|
|
||||||
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
|
||||||
let mut wrt = BytesMut::with_capacity(29).writer();
|
|
||||||
write!(
|
|
||||||
wrt,
|
|
||||||
"{}",
|
|
||||||
self.0
|
|
||||||
.to_offset(UtcOffset::UTC)
|
|
||||||
.format("%a, %d %b %Y %H:%M:%S GMT")
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
HeaderValue::from_maybe_shared(wrt.get_mut().split().freeze())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<HttpDate> for SystemTime {
|
|
||||||
fn from(date: HttpDate) -> SystemTime {
|
|
||||||
let dt = date.0;
|
|
||||||
let epoch = OffsetDateTime::unix_epoch();
|
|
||||||
|
|
||||||
UNIX_EPOCH + (dt - epoch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::HttpDate;
|
|
||||||
use time::{date, time, PrimitiveDateTime};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_date() {
|
|
||||||
let nov_07 = HttpDate(
|
|
||||||
PrimitiveDateTime::new(date!(1994 - 11 - 07), time!(8:48:37)).assume_utc(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
"Sun, 07 Nov 1994 08:48:37 GMT".parse::<HttpDate>().unwrap(),
|
|
||||||
nov_07
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
"Sunday, 07-Nov-94 08:48:37 GMT"
|
|
||||||
.parse::<HttpDate>()
|
|
||||||
.unwrap(),
|
|
||||||
nov_07
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
"Sun Nov 7 08:48:37 1994".parse::<HttpDate>().unwrap(),
|
|
||||||
nov_07
|
|
||||||
);
|
|
||||||
assert!("this-is-no-date".parse::<HttpDate>().is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,12 +3,14 @@
|
|||||||
mod charset;
|
mod charset;
|
||||||
mod content_encoding;
|
mod content_encoding;
|
||||||
mod extended;
|
mod extended;
|
||||||
mod httpdate;
|
mod http_date;
|
||||||
|
mod quality;
|
||||||
mod quality_item;
|
mod quality_item;
|
||||||
|
|
||||||
pub use self::charset::Charset;
|
pub use self::charset::Charset;
|
||||||
pub use self::content_encoding::ContentEncoding;
|
pub use self::content_encoding::ContentEncoding;
|
||||||
pub use self::extended::{parse_extended_value, ExtendedValue};
|
pub use self::extended::{parse_extended_value, ExtendedValue};
|
||||||
pub use self::httpdate::HttpDate;
|
pub use self::http_date::HttpDate;
|
||||||
pub use self::quality_item::{q, qitem, Quality, QualityItem};
|
pub use self::quality::{q, Quality};
|
||||||
|
pub use self::quality_item::QualityItem;
|
||||||
pub use language_tags::LanguageTag;
|
pub use language_tags::LanguageTag;
|
||||||
|
|||||||
225
actix-http/src/header/shared/quality.rs
Normal file
225
actix-http/src/header/shared/quality.rs
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
use std::{
|
||||||
|
convert::{TryFrom, TryInto},
|
||||||
|
fmt,
|
||||||
|
};
|
||||||
|
|
||||||
|
use derive_more::{Display, Error};
|
||||||
|
|
||||||
|
const MAX_QUALITY_INT: u16 = 1000;
|
||||||
|
const MAX_QUALITY_FLOAT: f32 = 1.0;
|
||||||
|
|
||||||
|
/// Represents a quality used in q-factor values.
|
||||||
|
///
|
||||||
|
/// The default value is equivalent to `q=1.0` (the [max](Self::MAX) value).
|
||||||
|
///
|
||||||
|
/// # Implementation notes
|
||||||
|
/// The quality value is defined as a number between 0.0 and 1.0 with three decimal places.
|
||||||
|
/// This means there are 1001 possible values. Since floating point numbers are not exact and the
|
||||||
|
/// smallest floating point data type (`f32`) consumes four bytes, we use an `u16` value to store
|
||||||
|
/// the quality internally.
|
||||||
|
///
|
||||||
|
/// [RFC 7231 §5.3.1] gives more information on quality values in HTTP header fields.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use actix_http::header::{Quality, q};
|
||||||
|
/// assert_eq!(q(1.0), Quality::MAX);
|
||||||
|
///
|
||||||
|
/// assert_eq!(q(0.42).to_string(), "0.42");
|
||||||
|
/// assert_eq!(q(1.0).to_string(), "1");
|
||||||
|
/// assert_eq!(Quality::MIN.to_string(), "0.001");
|
||||||
|
/// assert_eq!(Quality::ZERO.to_string(), "0");
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [RFC 7231 §5.3.1]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Quality(pub(super) u16);
|
||||||
|
|
||||||
|
impl Quality {
|
||||||
|
/// The maximum quality value, equivalent to `q=1.0`.
|
||||||
|
pub const MAX: Quality = Quality(MAX_QUALITY_INT);
|
||||||
|
|
||||||
|
/// The minimum, non-zero quality value, equivalent to `q=0.001`.
|
||||||
|
pub const MIN: Quality = Quality(1);
|
||||||
|
|
||||||
|
/// The zero quality value, equivalent to `q=0.0`.
|
||||||
|
pub const ZERO: Quality = Quality(0);
|
||||||
|
|
||||||
|
/// Converts a float in the range 0.0–1.0 to a `Quality`.
|
||||||
|
///
|
||||||
|
/// Intentionally private. External uses should rely on the `TryFrom` impl.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0.
|
||||||
|
fn from_f32(value: f32) -> Self {
|
||||||
|
// Check that `value` is within range should be done before calling this method.
|
||||||
|
// Just in case, this debug_assert should catch if we were forgetful.
|
||||||
|
debug_assert!(
|
||||||
|
(0.0..=MAX_QUALITY_FLOAT).contains(&value),
|
||||||
|
"q value must be between 0.0 and 1.0"
|
||||||
|
);
|
||||||
|
|
||||||
|
Quality((value * MAX_QUALITY_INT as f32) as u16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default value is [`Quality::MAX`].
|
||||||
|
impl Default for Quality {
|
||||||
|
fn default() -> Quality {
|
||||||
|
Quality::MAX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Quality {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self.0 {
|
||||||
|
0 => f.write_str("0"),
|
||||||
|
MAX_QUALITY_INT => f.write_str("1"),
|
||||||
|
|
||||||
|
// some number in the range 1–999
|
||||||
|
x => {
|
||||||
|
f.write_str("0.")?;
|
||||||
|
|
||||||
|
// This implementation avoids string allocation for removing trailing zeroes.
|
||||||
|
// In benchmarks it is twice as fast as approach using something like
|
||||||
|
// `format!("{}").trim_end_matches('0')` for non-fast-path quality values.
|
||||||
|
|
||||||
|
if x < 10 {
|
||||||
|
// x in is range 1–9
|
||||||
|
|
||||||
|
f.write_str("00")?;
|
||||||
|
|
||||||
|
// 0 is already handled so it's not possible to have a trailing 0 in this range
|
||||||
|
// we can just write the integer
|
||||||
|
itoa_fmt(f, x)
|
||||||
|
} else if x < 100 {
|
||||||
|
// x in is range 10–99
|
||||||
|
|
||||||
|
f.write_str("0")?;
|
||||||
|
|
||||||
|
if x % 10 == 0 {
|
||||||
|
// trailing 0, divide by 10 and write
|
||||||
|
itoa_fmt(f, x / 10)
|
||||||
|
} else {
|
||||||
|
itoa_fmt(f, x)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// x is in range 100–999
|
||||||
|
|
||||||
|
if x % 100 == 0 {
|
||||||
|
// two trailing 0s, divide by 100 and write
|
||||||
|
itoa_fmt(f, x / 100)
|
||||||
|
} else if x % 10 == 0 {
|
||||||
|
// one trailing 0, divide by 10 and write
|
||||||
|
itoa_fmt(f, x / 10)
|
||||||
|
} else {
|
||||||
|
itoa_fmt(f, x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write integer to a `fmt::Write`.
|
||||||
|
pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Result {
|
||||||
|
let mut buf = itoa::Buffer::new();
|
||||||
|
wr.write_str(buf.format(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Display, Error)]
|
||||||
|
#[display(fmt = "quality out of bounds")]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct QualityOutOfBounds;
|
||||||
|
|
||||||
|
impl TryFrom<f32> for Quality {
|
||||||
|
type Error = QualityOutOfBounds;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_from(value: f32) -> Result<Self, Self::Error> {
|
||||||
|
if (0.0..=MAX_QUALITY_FLOAT).contains(&value) {
|
||||||
|
Ok(Quality::from_f32(value))
|
||||||
|
} else {
|
||||||
|
Err(QualityOutOfBounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience function to create a [`Quality`] from an `f32` (0.0–1.0).
|
||||||
|
///
|
||||||
|
/// Not recommended for use with user input. Rely on the `TryFrom` impls where possible.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if value is out of range.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// # use actix_http::header::{q, Quality};
|
||||||
|
/// let q1 = q(1.0);
|
||||||
|
/// assert_eq!(q1, Quality::MAX);
|
||||||
|
///
|
||||||
|
/// let q2 = q(0.001);
|
||||||
|
/// assert_eq!(q2, Quality::MIN);
|
||||||
|
///
|
||||||
|
/// let q3 = q(0.0);
|
||||||
|
/// assert_eq!(q3, Quality::ZERO);
|
||||||
|
///
|
||||||
|
/// let q4 = q(0.42);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// An out-of-range `f32` quality will panic.
|
||||||
|
/// ```should_panic
|
||||||
|
/// # use actix_http::header::q;
|
||||||
|
/// let _q2 = q(1.42);
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
pub fn q<T>(quality: T) -> Quality
|
||||||
|
where
|
||||||
|
T: TryInto<Quality>,
|
||||||
|
T::Error: fmt::Debug,
|
||||||
|
{
|
||||||
|
quality.try_into().expect("quality value was out of bounds")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn q_helper() {
|
||||||
|
assert_eq!(q(0.5), Quality(500));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_output() {
|
||||||
|
assert_eq!(Quality::ZERO.to_string(), "0");
|
||||||
|
assert_eq!(Quality::MIN.to_string(), "0.001");
|
||||||
|
assert_eq!(Quality::MAX.to_string(), "1");
|
||||||
|
|
||||||
|
assert_eq!(q(0.0).to_string(), "0");
|
||||||
|
assert_eq!(q(1.0).to_string(), "1");
|
||||||
|
assert_eq!(q(0.001).to_string(), "0.001");
|
||||||
|
assert_eq!(q(0.5).to_string(), "0.5");
|
||||||
|
assert_eq!(q(0.22).to_string(), "0.22");
|
||||||
|
assert_eq!(q(0.123).to_string(), "0.123");
|
||||||
|
assert_eq!(q(0.999).to_string(), "0.999");
|
||||||
|
|
||||||
|
for x in 0..=1000 {
|
||||||
|
// if trailing zeroes are handled correctly, we would not expect the serialized length
|
||||||
|
// to ever exceed "0." + 3 decimal places = 5 in length
|
||||||
|
assert!(q(x as f32 / 1000.0).to_string().len() <= 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn negative_quality() {
|
||||||
|
q(-1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn quality_out_of_bounds() {
|
||||||
|
q(2.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,104 +1,70 @@
|
|||||||
use std::{
|
use std::{cmp, convert::TryFrom as _, fmt, str};
|
||||||
cmp,
|
|
||||||
convert::{TryFrom, TryInto},
|
|
||||||
fmt,
|
|
||||||
str::{self, FromStr},
|
|
||||||
};
|
|
||||||
|
|
||||||
use derive_more::{Display, Error};
|
|
||||||
|
|
||||||
use crate::error::ParseError;
|
use crate::error::ParseError;
|
||||||
|
|
||||||
const MAX_QUALITY: u16 = 1000;
|
use super::Quality;
|
||||||
const MAX_FLOAT_QUALITY: f32 = 1.0;
|
|
||||||
|
|
||||||
/// Represents a quality used in quality values.
|
/// Represents an item with a quality value as defined
|
||||||
|
/// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1).
|
||||||
///
|
///
|
||||||
/// Can be created with the [`q`] function.
|
/// # Parsing and Formatting
|
||||||
|
/// This wrapper be used to parse header value items that have a q-factor annotation as well as
|
||||||
|
/// serialize items with a their q-factor.
|
||||||
///
|
///
|
||||||
/// # Implementation notes
|
/// # Ordering
|
||||||
|
/// Since this context of use for this type is header value items, ordering is defined for
|
||||||
|
/// `QualityItem`s but _only_ considers the item's quality. Order of appearance should be used as
|
||||||
|
/// the secondary sorting parameter; i.e., a stable sort over the quality values will produce a
|
||||||
|
/// correctly sorted sequence.
|
||||||
///
|
///
|
||||||
/// The quality value is defined as a number between 0 and 1 with three decimal
|
/// # Examples
|
||||||
/// places. This means there are 1001 possible values. Since floating point
|
/// ```
|
||||||
/// numbers are not exact and the smallest floating point data type (`f32`)
|
/// # use actix_http::header::{QualityItem, q};
|
||||||
/// consumes four bytes, hyper uses an `u16` value to store the
|
/// let q_item: QualityItem<String> = "hello;q=0.3".parse().unwrap();
|
||||||
/// quality internally. For performance reasons you may set quality directly to
|
/// assert_eq!(&q_item.item, "hello");
|
||||||
/// a value between 0 and 1000 e.g. `Quality(532)` matches the quality
|
/// assert_eq!(q_item.quality, q(0.3));
|
||||||
/// `q=0.532`.
|
|
||||||
///
|
///
|
||||||
/// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1)
|
/// // note that format is normalized compared to parsed item
|
||||||
/// gives more information on quality values in HTTP header fields.
|
/// assert_eq!(q_item.to_string(), "hello; q=0.3");
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
///
|
||||||
pub struct Quality(u16);
|
/// // item with q=0.3 is greater than item with q=0.1
|
||||||
|
/// let q_item_fallback: QualityItem<String> = "abc;q=0.1".parse().unwrap();
|
||||||
impl Quality {
|
/// assert!(q_item > q_item_fallback);
|
||||||
/// # Panics
|
/// ```
|
||||||
/// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0.
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
fn from_f32(value: f32) -> Self {
|
|
||||||
// Check that `value` is within range should be done before calling this method.
|
|
||||||
// Just in case, this debug_assert should catch if we were forgetful.
|
|
||||||
debug_assert!(
|
|
||||||
(0.0f32..=1.0f32).contains(&value),
|
|
||||||
"q value must be between 0.0 and 1.0"
|
|
||||||
);
|
|
||||||
|
|
||||||
Quality((value * MAX_QUALITY as f32) as u16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Quality {
|
|
||||||
fn default() -> Quality {
|
|
||||||
Quality(MAX_QUALITY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Display, Error)]
|
|
||||||
pub struct QualityOutOfBounds;
|
|
||||||
|
|
||||||
impl TryFrom<u16> for Quality {
|
|
||||||
type Error = QualityOutOfBounds;
|
|
||||||
|
|
||||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
|
||||||
if (0..=MAX_QUALITY).contains(&value) {
|
|
||||||
Ok(Quality(value))
|
|
||||||
} else {
|
|
||||||
Err(QualityOutOfBounds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<f32> for Quality {
|
|
||||||
type Error = QualityOutOfBounds;
|
|
||||||
|
|
||||||
fn try_from(value: f32) -> Result<Self, Self::Error> {
|
|
||||||
if (0.0..=MAX_FLOAT_QUALITY).contains(&value) {
|
|
||||||
Ok(Quality::from_f32(value))
|
|
||||||
} else {
|
|
||||||
Err(QualityOutOfBounds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents an item with a quality value as defined in
|
|
||||||
/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-5.3.1).
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
|
||||||
pub struct QualityItem<T> {
|
pub struct QualityItem<T> {
|
||||||
/// The actual contents of the field.
|
/// The wrapped contents of the field.
|
||||||
pub item: T,
|
pub item: T,
|
||||||
|
|
||||||
/// The quality (client or server preference) for the value.
|
/// The quality (client or server preference) for the value.
|
||||||
pub quality: Quality,
|
pub quality: Quality,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> QualityItem<T> {
|
impl<T> QualityItem<T> {
|
||||||
/// Creates a new `QualityItem` from an item and a quality.
|
/// Constructs a new `QualityItem` from an item and a quality value.
|
||||||
/// The item can be of any type.
|
///
|
||||||
/// The quality should be a value in the range [0, 1].
|
/// The item can be of any type. The quality should be a value in the range [0, 1].
|
||||||
pub fn new(item: T, quality: Quality) -> QualityItem<T> {
|
pub fn new(item: T, quality: Quality) -> Self {
|
||||||
QualityItem { item, quality }
|
QualityItem { item, quality }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs a new `QualityItem` from an item, using the maximum q-value.
|
||||||
|
pub fn max(item: T) -> Self {
|
||||||
|
Self::new(item, Quality::MAX)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a new `QualityItem` from an item, using the minimum, non-zero q-value.
|
||||||
|
pub fn min(item: T) -> Self {
|
||||||
|
Self::new(item, Quality::MIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a new `QualityItem` from an item, using zero q-value of zero.
|
||||||
|
pub fn zero(item: T) -> Self {
|
||||||
|
Self::new(item, Quality::ZERO)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: PartialEq> cmp::PartialOrd for QualityItem<T> {
|
impl<T: PartialEq> PartialOrd for QualityItem<T> {
|
||||||
fn partial_cmp(&self, other: &QualityItem<T>) -> Option<cmp::Ordering> {
|
fn partial_cmp(&self, other: &QualityItem<T>) -> Option<cmp::Ordering> {
|
||||||
self.quality.partial_cmp(&other.quality)
|
self.quality.partial_cmp(&other.quality)
|
||||||
}
|
}
|
||||||
@@ -108,93 +74,79 @@ impl<T: fmt::Display> fmt::Display for QualityItem<T> {
|
|||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
fmt::Display::fmt(&self.item, f)?;
|
fmt::Display::fmt(&self.item, f)?;
|
||||||
|
|
||||||
match self.quality.0 {
|
match self.quality {
|
||||||
MAX_QUALITY => Ok(()),
|
// q-factor value is implied for max value
|
||||||
0 => f.write_str("; q=0"),
|
Quality::MAX => Ok(()),
|
||||||
x => write!(f, "; q=0.{}", format!("{:03}", x).trim_end_matches('0')),
|
|
||||||
|
// fast path for zero
|
||||||
|
Quality::ZERO => f.write_str("; q=0"),
|
||||||
|
|
||||||
|
// quality formatting is already using itoa
|
||||||
|
q => write!(f, "; q={}", q),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: FromStr> FromStr for QualityItem<T> {
|
impl<T: str::FromStr> str::FromStr for QualityItem<T> {
|
||||||
type Err = ParseError;
|
type Err = ParseError;
|
||||||
|
|
||||||
fn from_str(qitem_str: &str) -> Result<Self, Self::Err> {
|
fn from_str(q_item_str: &str) -> Result<Self, Self::Err> {
|
||||||
if !qitem_str.is_ascii() {
|
if !q_item_str.is_ascii() {
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set defaults used if parsing fails.
|
// set defaults used if quality-item parsing fails, i.e., item has no q attribute
|
||||||
let mut raw_item = qitem_str;
|
let mut raw_item = q_item_str;
|
||||||
let mut quality = 1f32;
|
let mut quality = Quality::MAX;
|
||||||
|
|
||||||
let parts: Vec<_> = qitem_str.rsplitn(2, ';').map(str::trim).collect();
|
let parts = q_item_str
|
||||||
|
.rsplit_once(';')
|
||||||
|
.map(|(item, q_attr)| (item.trim(), q_attr.trim()));
|
||||||
|
|
||||||
if parts.len() == 2 {
|
if let Some((val, q_attr)) = parts {
|
||||||
// example for item with q-factor:
|
// example for item with q-factor:
|
||||||
//
|
//
|
||||||
// gzip;q=0.65
|
// gzip;q=0.65
|
||||||
// ^^^^^^ parts[0]
|
// ^^^^ val
|
||||||
// ^^ start
|
// ^^^^^^ q_attr
|
||||||
|
// ^^ q
|
||||||
// ^^^^ q_val
|
// ^^^^ q_val
|
||||||
// ^^^^ parts[1]
|
|
||||||
|
|
||||||
if parts[0].len() < 2 {
|
if q_attr.len() < 2 {
|
||||||
// Can't possibly be an attribute since an attribute needs at least a name followed
|
// Can't possibly be an attribute since an attribute needs at least a name followed
|
||||||
// by an equals sign. And bare identifiers are forbidden.
|
// by an equals sign. And bare identifiers are forbidden.
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
|
|
||||||
let start = &parts[0][0..2];
|
let q = &q_attr[0..2];
|
||||||
|
|
||||||
if start == "q=" || start == "Q=" {
|
if q == "q=" || q == "Q=" {
|
||||||
let q_val = &parts[0][2..];
|
let q_val = &q_attr[2..];
|
||||||
if q_val.len() > 5 {
|
if q_val.len() > 5 {
|
||||||
// longer than 5 indicates an over-precise q-factor
|
// longer than 5 indicates an over-precise q-factor
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
|
|
||||||
let q_value = q_val.parse::<f32>().map_err(|_| ParseError::Header)?;
|
let q_value = q_val.parse::<f32>().map_err(|_| ParseError::Header)?;
|
||||||
|
let q_value = Quality::try_from(q_value).map_err(|_| ParseError::Header)?;
|
||||||
|
|
||||||
if (0f32..=1f32).contains(&q_value) {
|
|
||||||
quality = q_value;
|
quality = q_value;
|
||||||
raw_item = parts[1];
|
raw_item = val;
|
||||||
} else {
|
|
||||||
return Err(ParseError::Header);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let item = raw_item.parse::<T>().map_err(|_| ParseError::Header)?;
|
let item = raw_item.parse::<T>().map_err(|_| ParseError::Header)?;
|
||||||
|
|
||||||
// we already checked above that the quality is within range
|
Ok(QualityItem::new(item, quality))
|
||||||
Ok(QualityItem::new(item, Quality::from_f32(quality)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience function to wrap a value in a `QualityItem`
|
|
||||||
/// Sets `q` to the default 1.0
|
|
||||||
pub fn qitem<T>(item: T) -> QualityItem<T> {
|
|
||||||
QualityItem::new(item, Quality::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience function to create a `Quality` from a float or integer.
|
|
||||||
///
|
|
||||||
/// Implemented for `u16` and `f32`. Panics if value is out of range.
|
|
||||||
pub fn q<T>(val: T) -> Quality
|
|
||||||
where
|
|
||||||
T: TryInto<Quality>,
|
|
||||||
T::Error: fmt::Debug,
|
|
||||||
{
|
|
||||||
// TODO: on next breaking change, handle unwrap differently
|
|
||||||
val.try_into().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
// copy of encoding from actix-web headers
|
// copy of encoding from actix-web headers
|
||||||
|
#[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub enum Encoding {
|
pub enum Encoding {
|
||||||
Chunked,
|
Chunked,
|
||||||
@@ -223,7 +175,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Encoding {
|
impl str::FromStr for Encoding {
|
||||||
type Err = crate::error::ParseError;
|
type Err = crate::error::ParseError;
|
||||||
fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> {
|
fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> {
|
||||||
use Encoding::*;
|
use Encoding::*;
|
||||||
@@ -243,7 +195,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_quality_item_fmt_q_1() {
|
fn test_quality_item_fmt_q_1() {
|
||||||
use Encoding::*;
|
use Encoding::*;
|
||||||
let x = qitem(Chunked);
|
let x = QualityItem::max(Chunked);
|
||||||
assert_eq!(format!("{}", x), "chunked");
|
assert_eq!(format!("{}", x), "chunked");
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
@@ -342,25 +294,8 @@ mod tests {
|
|||||||
fn test_quality_item_ordering() {
|
fn test_quality_item_ordering() {
|
||||||
let x: QualityItem<Encoding> = "gzip; q=0.5".parse().ok().unwrap();
|
let x: QualityItem<Encoding> = "gzip; q=0.5".parse().ok().unwrap();
|
||||||
let y: QualityItem<Encoding> = "gzip; q=0.273".parse().ok().unwrap();
|
let y: QualityItem<Encoding> = "gzip; q=0.273".parse().ok().unwrap();
|
||||||
let comparision_result: bool = x.gt(&y);
|
let comparison_result: bool = x.gt(&y);
|
||||||
assert!(comparision_result)
|
assert!(comparison_result)
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_quality() {
|
|
||||||
assert_eq!(q(0.5), Quality(500));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn test_quality_invalid() {
|
|
||||||
q(-1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn test_quality_invalid2() {
|
|
||||||
q(2.0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Header parsing utilities.
|
||||||
|
|
||||||
use std::{fmt, str::FromStr};
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
use super::HeaderValue;
|
use super::HeaderValue;
|
||||||
@@ -10,9 +12,12 @@ where
|
|||||||
I: Iterator<Item = &'a HeaderValue> + 'a,
|
I: Iterator<Item = &'a HeaderValue> + 'a,
|
||||||
T: FromStr,
|
T: FromStr,
|
||||||
{
|
{
|
||||||
let mut result = Vec::new();
|
let size_guess = all.size_hint().1.unwrap_or(2);
|
||||||
|
let mut result = Vec::with_capacity(size_guess);
|
||||||
|
|
||||||
for h in all {
|
for h in all {
|
||||||
let s = h.to_str().map_err(|_| ParseError::Header)?;
|
let s = h.to_str().map_err(|_| ParseError::Header)?;
|
||||||
|
|
||||||
result.extend(
|
result.extend(
|
||||||
s.split(',')
|
s.split(',')
|
||||||
.filter_map(|x| match x.trim() {
|
.filter_map(|x| match x.trim() {
|
||||||
@@ -22,6 +27,7 @@ where
|
|||||||
.filter_map(|x| x.trim().parse().ok()),
|
.filter_map(|x| x.trim().parse().ok()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,10 +36,12 @@ where
|
|||||||
pub fn from_one_raw_str<T: FromStr>(val: Option<&HeaderValue>) -> Result<T, ParseError> {
|
pub fn from_one_raw_str<T: FromStr>(val: Option<&HeaderValue>) -> Result<T, ParseError> {
|
||||||
if let Some(line) = val {
|
if let Some(line) = val {
|
||||||
let line = line.to_str().map_err(|_| ParseError::Header)?;
|
let line = line.to_str().map_err(|_| ParseError::Header)?;
|
||||||
|
|
||||||
if !line.is_empty() {
|
if !line.is_empty() {
|
||||||
return T::from_str(line).or(Err(ParseError::Header));
|
return T::from_str(line).or(Err(ParseError::Header));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(ParseError::Header)
|
Err(ParseError::Header)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,19 +52,53 @@ where
|
|||||||
T: fmt::Display,
|
T: fmt::Display,
|
||||||
{
|
{
|
||||||
let mut iter = parts.iter();
|
let mut iter = parts.iter();
|
||||||
|
|
||||||
if let Some(part) = iter.next() {
|
if let Some(part) = iter.next() {
|
||||||
fmt::Display::fmt(part, f)?;
|
fmt::Display::fmt(part, f)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for part in iter {
|
for part in iter {
|
||||||
f.write_str(", ")?;
|
f.write_str(", ")?;
|
||||||
fmt::Display::fmt(part, f)?;
|
fmt::Display::fmt(part, f)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Percent encode a sequence of bytes with a character set defined in
|
/// Percent encode a sequence of bytes with a character set defined in [RFC 5987 §3.2].
|
||||||
/// <https://tools.ietf.org/html/rfc5987#section-3.2>
|
///
|
||||||
|
/// [RFC 5987 §3.2]: https://datatracker.ietf.org/doc/html/rfc5987#section-3.2
|
||||||
|
#[inline]
|
||||||
pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
|
pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
|
||||||
let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
|
let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
|
||||||
fmt::Display::fmt(&encoded, f)
|
fmt::Display::fmt(&encoded, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comma_delimited_parsing() {
|
||||||
|
let headers = vec![];
|
||||||
|
let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap();
|
||||||
|
assert_eq!(res, vec![0; 0]);
|
||||||
|
|
||||||
|
let headers = vec![
|
||||||
|
HeaderValue::from_static("1, 2"),
|
||||||
|
HeaderValue::from_static("3,4"),
|
||||||
|
];
|
||||||
|
let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap();
|
||||||
|
assert_eq!(res, vec![1, 2, 3, 4]);
|
||||||
|
|
||||||
|
let headers = vec![
|
||||||
|
HeaderValue::from_static(""),
|
||||||
|
HeaderValue::from_static(","),
|
||||||
|
HeaderValue::from_static(" "),
|
||||||
|
HeaderValue::from_static("1 ,"),
|
||||||
|
HeaderValue::from_static(""),
|
||||||
|
];
|
||||||
|
let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap();
|
||||||
|
assert_eq!(res, vec![1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,15 +30,25 @@ pub(crate) fn write_status_line<B: BufMut>(version: Version, n: u16, buf: &mut B
|
|||||||
/// Write out content length header.
|
/// Write out content length header.
|
||||||
///
|
///
|
||||||
/// Buffer must to contain enough space or be implicitly extendable.
|
/// Buffer must to contain enough space or be implicitly extendable.
|
||||||
pub fn write_content_length<B: BufMut>(n: u64, buf: &mut B) {
|
pub fn write_content_length<B: BufMut>(n: u64, buf: &mut B, camel_case: bool) {
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
|
if camel_case {
|
||||||
|
buf.put_slice(b"\r\nContent-Length: 0\r\n");
|
||||||
|
} else {
|
||||||
buf.put_slice(b"\r\ncontent-length: 0\r\n");
|
buf.put_slice(b"\r\ncontent-length: 0\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut buffer = itoa::Buffer::new();
|
let mut buffer = itoa::Buffer::new();
|
||||||
|
|
||||||
|
if camel_case {
|
||||||
|
buf.put_slice(b"\r\nContent-Length: ");
|
||||||
|
} else {
|
||||||
buf.put_slice(b"\r\ncontent-length: ");
|
buf.put_slice(b"\r\ncontent-length: ");
|
||||||
|
}
|
||||||
|
|
||||||
buf.put_slice(buffer.format(n).as_bytes());
|
buf.put_slice(buffer.format(n).as_bytes());
|
||||||
buf.put_slice(b"\r\n");
|
buf.put_slice(b"\r\n");
|
||||||
}
|
}
|
||||||
@@ -95,77 +105,88 @@ mod tests {
|
|||||||
fn test_write_content_length() {
|
fn test_write_content_length() {
|
||||||
let mut bytes = BytesMut::new();
|
let mut bytes = BytesMut::new();
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(0, &mut bytes);
|
write_content_length(0, &mut bytes, false);
|
||||||
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 0\r\n"[..]);
|
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 0\r\n"[..]);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(9, &mut bytes);
|
write_content_length(9, &mut bytes, false);
|
||||||
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 9\r\n"[..]);
|
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 9\r\n"[..]);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(10, &mut bytes);
|
write_content_length(10, &mut bytes, false);
|
||||||
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 10\r\n"[..]);
|
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 10\r\n"[..]);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(99, &mut bytes);
|
write_content_length(99, &mut bytes, false);
|
||||||
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 99\r\n"[..]);
|
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 99\r\n"[..]);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(100, &mut bytes);
|
write_content_length(100, &mut bytes, false);
|
||||||
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 100\r\n"[..]);
|
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 100\r\n"[..]);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(101, &mut bytes);
|
write_content_length(101, &mut bytes, false);
|
||||||
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 101\r\n"[..]);
|
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 101\r\n"[..]);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(998, &mut bytes);
|
write_content_length(998, &mut bytes, false);
|
||||||
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 998\r\n"[..]);
|
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 998\r\n"[..]);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(1000, &mut bytes);
|
write_content_length(1000, &mut bytes, false);
|
||||||
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 1000\r\n"[..]);
|
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 1000\r\n"[..]);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(1001, &mut bytes);
|
write_content_length(1001, &mut bytes, false);
|
||||||
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 1001\r\n"[..]);
|
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 1001\r\n"[..]);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(5909, &mut bytes);
|
write_content_length(5909, &mut bytes, false);
|
||||||
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 5909\r\n"[..]);
|
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 5909\r\n"[..]);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(9999, &mut bytes);
|
write_content_length(9999, &mut bytes, false);
|
||||||
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 9999\r\n"[..]);
|
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 9999\r\n"[..]);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(10001, &mut bytes);
|
write_content_length(10001, &mut bytes, false);
|
||||||
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 10001\r\n"[..]);
|
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 10001\r\n"[..]);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(59094, &mut bytes);
|
write_content_length(59094, &mut bytes, false);
|
||||||
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 59094\r\n"[..]);
|
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 59094\r\n"[..]);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(99999, &mut bytes);
|
write_content_length(99999, &mut bytes, false);
|
||||||
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 99999\r\n"[..]);
|
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 99999\r\n"[..]);
|
||||||
|
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(590947, &mut bytes);
|
write_content_length(590947, &mut bytes, false);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
bytes.split().freeze(),
|
bytes.split().freeze(),
|
||||||
b"\r\ncontent-length: 590947\r\n"[..]
|
b"\r\ncontent-length: 590947\r\n"[..]
|
||||||
);
|
);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(999999, &mut bytes);
|
write_content_length(999999, &mut bytes, false);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
bytes.split().freeze(),
|
bytes.split().freeze(),
|
||||||
b"\r\ncontent-length: 999999\r\n"[..]
|
b"\r\ncontent-length: 999999\r\n"[..]
|
||||||
);
|
);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(5909471, &mut bytes);
|
write_content_length(5909471, &mut bytes, false);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
bytes.split().freeze(),
|
bytes.split().freeze(),
|
||||||
b"\r\ncontent-length: 5909471\r\n"[..]
|
b"\r\ncontent-length: 5909471\r\n"[..]
|
||||||
);
|
);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(59094718, &mut bytes);
|
write_content_length(59094718, &mut bytes, false);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
bytes.split().freeze(),
|
bytes.split().freeze(),
|
||||||
b"\r\ncontent-length: 59094718\r\n"[..]
|
b"\r\ncontent-length: 59094718\r\n"[..]
|
||||||
);
|
);
|
||||||
bytes.reserve(50);
|
bytes.reserve(50);
|
||||||
write_content_length(4294973728, &mut bytes);
|
write_content_length(4294973728, &mut bytes, false);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
bytes.split().freeze(),
|
bytes.split().freeze(),
|
||||||
b"\r\ncontent-length: 4294973728\r\n"[..]
|
b"\r\ncontent-length: 4294973728\r\n"[..]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_content_length_camel_case() {
|
||||||
|
let mut bytes = BytesMut::new();
|
||||||
|
write_content_length(0, &mut bytes, false);
|
||||||
|
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 0\r\n"[..]);
|
||||||
|
|
||||||
|
let mut bytes = BytesMut::new();
|
||||||
|
write_content_length(0, &mut bytes, true);
|
||||||
|
assert_eq!(bytes.split().freeze(), b"\r\nContent-Length: 0\r\n"[..]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ pub trait HttpMessage: Sized {
|
|||||||
/// Message payload stream
|
/// Message payload stream
|
||||||
fn take_payload(&mut self) -> Payload<Self::Stream>;
|
fn take_payload(&mut self) -> Payload<Self::Stream>;
|
||||||
|
|
||||||
/// Request's extensions container
|
/// Returns a reference to the request-local data/extensions container.
|
||||||
fn extensions(&self) -> Ref<'_, Extensions>;
|
fn extensions(&self) -> Ref<'_, Extensions>;
|
||||||
|
|
||||||
/// Mutable reference to a the request's extensions container
|
/// Returns a mutable reference to the request-local data/extensions container.
|
||||||
fn extensions_mut(&self) -> RefMut<'_, Extensions>;
|
fn extensions_mut(&self) -> RefMut<'_, Extensions>;
|
||||||
|
|
||||||
/// Get a header.
|
/// Get a header.
|
||||||
@@ -55,7 +55,7 @@ pub trait HttpMessage: Sized {
|
|||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get content type encoding
|
/// Get content type encoding.
|
||||||
///
|
///
|
||||||
/// UTF-8 is used by default, If request charset is not set.
|
/// UTF-8 is used by default, If request charset is not set.
|
||||||
fn encoding(&self) -> Result<&'static Encoding, ContentTypeError> {
|
fn encoding(&self) -> Result<&'static Encoding, ContentTypeError> {
|
||||||
|
|||||||
84
actix-http/src/keep_alive.rs
Normal file
84
actix-http/src/keep_alive.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Connection keep-alive config.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum KeepAlive {
|
||||||
|
/// Keep-alive duration.
|
||||||
|
///
|
||||||
|
/// `KeepAlive::Timeout(Duration::ZERO)` is mapped to `KeepAlive::Disabled`.
|
||||||
|
Timeout(Duration),
|
||||||
|
|
||||||
|
/// Rely on OS to shutdown TCP connection.
|
||||||
|
///
|
||||||
|
/// Some defaults can be very long, check your OS documentation.
|
||||||
|
Os,
|
||||||
|
|
||||||
|
/// Keep-alive is disabled.
|
||||||
|
///
|
||||||
|
/// Connections will be closed immediately.
|
||||||
|
Disabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeepAlive {
|
||||||
|
pub(crate) fn enabled(&self) -> bool {
|
||||||
|
!matches!(self, Self::Disabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)] // used with `http2` feature flag
|
||||||
|
pub(crate) fn duration(&self) -> Option<Duration> {
|
||||||
|
match self {
|
||||||
|
KeepAlive::Timeout(dur) => Some(*dur),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map zero duration to disabled.
|
||||||
|
pub(crate) fn normalize(self) -> KeepAlive {
|
||||||
|
match self {
|
||||||
|
KeepAlive::Timeout(Duration::ZERO) => KeepAlive::Disabled,
|
||||||
|
ka => ka,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for KeepAlive {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Timeout(Duration::from_secs(5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Duration> for KeepAlive {
|
||||||
|
fn from(dur: Duration) -> Self {
|
||||||
|
KeepAlive::Timeout(dur).normalize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<Duration>> for KeepAlive {
|
||||||
|
fn from(ka_dur: Option<Duration>) -> Self {
|
||||||
|
match ka_dur {
|
||||||
|
Some(dur) => KeepAlive::from(dur),
|
||||||
|
None => KeepAlive::Disabled,
|
||||||
|
}
|
||||||
|
.normalize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_impls() {
|
||||||
|
let test: KeepAlive = Duration::from_secs(1).into();
|
||||||
|
assert_eq!(test, KeepAlive::Timeout(Duration::from_secs(1)));
|
||||||
|
|
||||||
|
let test: KeepAlive = Duration::from_secs(0).into();
|
||||||
|
assert_eq!(test, KeepAlive::Disabled);
|
||||||
|
|
||||||
|
let test: KeepAlive = Some(Duration::from_secs(0)).into();
|
||||||
|
assert_eq!(test, KeepAlive::Disabled);
|
||||||
|
|
||||||
|
let test: KeepAlive = None.into();
|
||||||
|
assert_eq!(test, KeepAlive::Disabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,81 +14,60 @@
|
|||||||
//! [rustls]: https://crates.io/crates/rustls
|
//! [rustls]: https://crates.io/crates/rustls
|
||||||
//! [trust-dns]: https://crates.io/crates/trust-dns
|
//! [trust-dns]: https://crates.io/crates/trust-dns
|
||||||
|
|
||||||
#![deny(rust_2018_idioms, nonstandard_style, clippy::uninit_assumed_init)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
|
#![warn(future_incompatible)]
|
||||||
#![allow(
|
#![allow(
|
||||||
clippy::type_complexity,
|
clippy::type_complexity,
|
||||||
clippy::too_many_arguments,
|
clippy::too_many_arguments,
|
||||||
clippy::new_without_default,
|
|
||||||
clippy::borrow_interior_mutable_const
|
clippy::borrow_interior_mutable_const
|
||||||
)]
|
)]
|
||||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
|
|
||||||
#[macro_use]
|
pub use ::http::{uri, uri::Uri};
|
||||||
extern crate log;
|
pub use ::http::{Method, StatusCode, Version};
|
||||||
|
|
||||||
pub mod body;
|
pub mod body;
|
||||||
mod builder;
|
mod builder;
|
||||||
pub mod client;
|
|
||||||
mod config;
|
mod config;
|
||||||
|
mod date;
|
||||||
#[cfg(feature = "__compress")]
|
#[cfg(feature = "__compress")]
|
||||||
pub mod encoding;
|
pub mod encoding;
|
||||||
|
pub mod error;
|
||||||
mod extensions;
|
mod extensions;
|
||||||
|
pub mod h1;
|
||||||
|
#[cfg(feature = "http2")]
|
||||||
|
pub mod h2;
|
||||||
pub mod header;
|
pub mod header;
|
||||||
mod helpers;
|
mod helpers;
|
||||||
mod http_message;
|
mod http_message;
|
||||||
|
mod keep_alive;
|
||||||
mod message;
|
mod message;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod notify_on_drop;
|
||||||
mod payload;
|
mod payload;
|
||||||
mod request;
|
mod requests;
|
||||||
mod response;
|
mod responses;
|
||||||
mod response_builder;
|
|
||||||
mod service;
|
mod service;
|
||||||
mod time_parser;
|
|
||||||
|
|
||||||
pub mod error;
|
|
||||||
pub mod h1;
|
|
||||||
pub mod h2;
|
|
||||||
pub mod test;
|
pub mod test;
|
||||||
|
#[cfg(feature = "ws")]
|
||||||
pub mod ws;
|
pub mod ws;
|
||||||
|
|
||||||
pub use self::builder::HttpServiceBuilder;
|
pub use self::builder::HttpServiceBuilder;
|
||||||
pub use self::config::{KeepAlive, ServiceConfig};
|
pub use self::config::ServiceConfig;
|
||||||
pub use self::error::Error;
|
pub use self::error::Error;
|
||||||
pub use self::extensions::Extensions;
|
pub use self::extensions::Extensions;
|
||||||
pub use self::header::ContentEncoding;
|
pub use self::header::ContentEncoding;
|
||||||
pub use self::http_message::HttpMessage;
|
pub use self::http_message::HttpMessage;
|
||||||
|
pub use self::keep_alive::KeepAlive;
|
||||||
pub use self::message::ConnectionType;
|
pub use self::message::ConnectionType;
|
||||||
pub use self::message::{Message, RequestHead, RequestHeadType, ResponseHead};
|
pub use self::message::Message;
|
||||||
pub use self::payload::{Payload, PayloadStream};
|
#[allow(deprecated)]
|
||||||
pub use self::request::Request;
|
pub use self::payload::{BoxedPayloadStream, Payload, PayloadStream};
|
||||||
pub use self::response::Response;
|
pub use self::requests::{Request, RequestHead, RequestHeadType};
|
||||||
pub use self::response_builder::ResponseBuilder;
|
pub use self::responses::{Response, ResponseBuilder, ResponseHead};
|
||||||
pub use self::service::HttpService;
|
pub use self::service::HttpService;
|
||||||
|
|
||||||
pub use ::http::{uri, uri::Uri};
|
|
||||||
pub use ::http::{Method, StatusCode, Version};
|
|
||||||
|
|
||||||
// TODO: deprecate this mish-mash of random items
|
|
||||||
pub mod http {
|
|
||||||
//! Various HTTP related types.
|
|
||||||
|
|
||||||
// re-exports
|
|
||||||
pub use http::header::{HeaderName, HeaderValue};
|
|
||||||
pub use http::uri::PathAndQuery;
|
|
||||||
pub use http::{uri, Error, Uri};
|
|
||||||
pub use http::{Method, StatusCode, Version};
|
|
||||||
|
|
||||||
pub use crate::header::HeaderMap;
|
|
||||||
|
|
||||||
/// A collection of HTTP headers and helpers.
|
|
||||||
pub mod header {
|
|
||||||
pub use crate::header::*;
|
|
||||||
}
|
|
||||||
pub use crate::header::ContentEncoding;
|
|
||||||
pub use crate::message::ConnectionType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A major HTTP protocol version.
|
/// A major HTTP protocol version.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
@@ -104,34 +83,18 @@ type ConnectCallback<IO> = dyn Fn(&IO, &mut Extensions);
|
|||||||
///
|
///
|
||||||
/// # Implementation Details
|
/// # Implementation Details
|
||||||
/// Uses Option to reduce necessary allocations when merging with request extensions.
|
/// Uses Option to reduce necessary allocations when merging with request extensions.
|
||||||
|
#[derive(Default)]
|
||||||
pub(crate) struct OnConnectData(Option<Extensions>);
|
pub(crate) struct OnConnectData(Option<Extensions>);
|
||||||
|
|
||||||
impl Default for OnConnectData {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OnConnectData {
|
impl OnConnectData {
|
||||||
/// Construct by calling the on-connect callback with the underlying transport I/O.
|
/// Construct by calling the on-connect callback with the underlying transport I/O.
|
||||||
pub(crate) fn from_io<T>(
|
pub(crate) fn from_io<T>(io: &T, on_connect_ext: Option<&ConnectCallback<T>>) -> Self {
|
||||||
io: &T,
|
|
||||||
on_connect_ext: Option<&ConnectCallback<T>>,
|
|
||||||
) -> Self {
|
|
||||||
let ext = on_connect_ext.map(|handler| {
|
let ext = on_connect_ext.map(|handler| {
|
||||||
let mut extensions = Extensions::new();
|
let mut extensions = Extensions::default();
|
||||||
handler(io, &mut extensions);
|
handler(io, &mut extensions);
|
||||||
extensions
|
extensions
|
||||||
});
|
});
|
||||||
|
|
||||||
Self(ext)
|
Self(ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge self into given request's extensions.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn merge_into(&mut self, req: &mut Request) {
|
|
||||||
if let Some(ref mut ext) = self.0 {
|
|
||||||
req.head.extensions.get_mut().drain_from(ext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,17 @@
|
|||||||
use std::{
|
use std::{cell::RefCell, ops, rc::Rc};
|
||||||
cell::{Ref, RefCell, RefMut},
|
|
||||||
net,
|
|
||||||
rc::Rc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
|
|
||||||
use crate::{
|
|
||||||
header::{self, HeaderMap},
|
|
||||||
Extensions, Method, StatusCode, Uri, Version,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Represents various types of connection
|
/// Represents various types of connection
|
||||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
pub enum ConnectionType {
|
pub enum ConnectionType {
|
||||||
/// Close connection after response
|
/// Close connection after response.
|
||||||
Close,
|
Close,
|
||||||
|
|
||||||
/// Keep connection alive after response
|
/// Keep connection alive after response.
|
||||||
KeepAlive,
|
KeepAlive,
|
||||||
|
|
||||||
/// Connection is upgraded to different type
|
/// Connection is upgraded to different type.
|
||||||
Upgrade,
|
Upgrade,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,308 +35,6 @@ pub trait Head: Default + 'static {
|
|||||||
F: FnOnce(&MessagePool<Self>) -> R;
|
F: FnOnce(&MessagePool<Self>) -> R;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct RequestHead {
|
|
||||||
pub uri: Uri,
|
|
||||||
pub method: Method,
|
|
||||||
pub version: Version,
|
|
||||||
pub headers: HeaderMap,
|
|
||||||
pub extensions: RefCell<Extensions>,
|
|
||||||
pub peer_addr: Option<net::SocketAddr>,
|
|
||||||
flags: Flags,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for RequestHead {
|
|
||||||
fn default() -> RequestHead {
|
|
||||||
RequestHead {
|
|
||||||
uri: Uri::default(),
|
|
||||||
method: Method::default(),
|
|
||||||
version: Version::HTTP_11,
|
|
||||||
headers: HeaderMap::with_capacity(16),
|
|
||||||
flags: Flags::empty(),
|
|
||||||
peer_addr: None,
|
|
||||||
extensions: RefCell::new(Extensions::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Head for RequestHead {
|
|
||||||
fn clear(&mut self) {
|
|
||||||
self.flags = Flags::empty();
|
|
||||||
self.headers.clear();
|
|
||||||
self.extensions.get_mut().clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_pool<F, R>(f: F) -> R
|
|
||||||
where
|
|
||||||
F: FnOnce(&MessagePool<Self>) -> R,
|
|
||||||
{
|
|
||||||
REQUEST_POOL.with(|p| f(p))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RequestHead {
|
|
||||||
/// Message extensions
|
|
||||||
#[inline]
|
|
||||||
pub fn extensions(&self) -> Ref<'_, Extensions> {
|
|
||||||
self.extensions.borrow()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mutable reference to a the message's extensions
|
|
||||||
#[inline]
|
|
||||||
pub fn extensions_mut(&self) -> RefMut<'_, Extensions> {
|
|
||||||
self.extensions.borrow_mut()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read the message headers.
|
|
||||||
pub fn headers(&self) -> &HeaderMap {
|
|
||||||
&self.headers
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mutable reference to the message headers.
|
|
||||||
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
|
||||||
&mut self.headers
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Is to uppercase headers with Camel-Case.
|
|
||||||
/// Default is `false`
|
|
||||||
#[inline]
|
|
||||||
pub fn camel_case_headers(&self) -> bool {
|
|
||||||
self.flags.contains(Flags::CAMEL_CASE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set `true` to send headers which are formatted as Camel-Case.
|
|
||||||
#[inline]
|
|
||||||
pub fn set_camel_case_headers(&mut self, val: bool) {
|
|
||||||
if val {
|
|
||||||
self.flags.insert(Flags::CAMEL_CASE);
|
|
||||||
} else {
|
|
||||||
self.flags.remove(Flags::CAMEL_CASE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Set connection type of the message
|
|
||||||
pub fn set_connection_type(&mut self, ctype: ConnectionType) {
|
|
||||||
match ctype {
|
|
||||||
ConnectionType::Close => self.flags.insert(Flags::CLOSE),
|
|
||||||
ConnectionType::KeepAlive => self.flags.insert(Flags::KEEP_ALIVE),
|
|
||||||
ConnectionType::Upgrade => self.flags.insert(Flags::UPGRADE),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Connection type
|
|
||||||
pub fn connection_type(&self) -> ConnectionType {
|
|
||||||
if self.flags.contains(Flags::CLOSE) {
|
|
||||||
ConnectionType::Close
|
|
||||||
} else if self.flags.contains(Flags::KEEP_ALIVE) {
|
|
||||||
ConnectionType::KeepAlive
|
|
||||||
} else if self.flags.contains(Flags::UPGRADE) {
|
|
||||||
ConnectionType::Upgrade
|
|
||||||
} else if self.version < Version::HTTP_11 {
|
|
||||||
ConnectionType::Close
|
|
||||||
} else {
|
|
||||||
ConnectionType::KeepAlive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Connection upgrade status
|
|
||||||
pub fn upgrade(&self) -> bool {
|
|
||||||
self.headers()
|
|
||||||
.get(header::CONNECTION)
|
|
||||||
.map(|hdr| {
|
|
||||||
if let Ok(s) = hdr.to_str() {
|
|
||||||
s.to_ascii_lowercase().contains("upgrade")
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Get response body chunking state
|
|
||||||
pub fn chunked(&self) -> bool {
|
|
||||||
!self.flags.contains(Flags::NO_CHUNKING)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn no_chunking(&mut self, val: bool) {
|
|
||||||
if val {
|
|
||||||
self.flags.insert(Flags::NO_CHUNKING);
|
|
||||||
} else {
|
|
||||||
self.flags.remove(Flags::NO_CHUNKING);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Request contains `EXPECT` header
|
|
||||||
pub fn expect(&self) -> bool {
|
|
||||||
self.flags.contains(Flags::EXPECT)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn set_expect(&mut self) {
|
|
||||||
self.flags.insert(Flags::EXPECT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum RequestHeadType {
|
|
||||||
Owned(RequestHead),
|
|
||||||
Rc(Rc<RequestHead>, Option<HeaderMap>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RequestHeadType {
|
|
||||||
pub fn extra_headers(&self) -> Option<&HeaderMap> {
|
|
||||||
match self {
|
|
||||||
RequestHeadType::Owned(_) => None,
|
|
||||||
RequestHeadType::Rc(_, headers) => headers.as_ref(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRef<RequestHead> for RequestHeadType {
|
|
||||||
fn as_ref(&self) -> &RequestHead {
|
|
||||||
match self {
|
|
||||||
RequestHeadType::Owned(head) => head,
|
|
||||||
RequestHeadType::Rc(head, _) => head.as_ref(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RequestHead> for RequestHeadType {
|
|
||||||
fn from(head: RequestHead) -> Self {
|
|
||||||
RequestHeadType::Owned(head)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ResponseHead {
|
|
||||||
pub version: Version,
|
|
||||||
pub status: StatusCode,
|
|
||||||
pub headers: HeaderMap,
|
|
||||||
pub reason: Option<&'static str>,
|
|
||||||
pub(crate) extensions: RefCell<Extensions>,
|
|
||||||
flags: Flags,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResponseHead {
|
|
||||||
/// Create new instance of `ResponseHead` type
|
|
||||||
#[inline]
|
|
||||||
pub fn new(status: StatusCode) -> ResponseHead {
|
|
||||||
ResponseHead {
|
|
||||||
status,
|
|
||||||
version: Version::default(),
|
|
||||||
headers: HeaderMap::with_capacity(12),
|
|
||||||
reason: None,
|
|
||||||
flags: Flags::empty(),
|
|
||||||
extensions: RefCell::new(Extensions::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Message extensions
|
|
||||||
#[inline]
|
|
||||||
pub fn extensions(&self) -> Ref<'_, Extensions> {
|
|
||||||
self.extensions.borrow()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mutable reference to a the message's extensions
|
|
||||||
#[inline]
|
|
||||||
pub fn extensions_mut(&self) -> RefMut<'_, Extensions> {
|
|
||||||
self.extensions.borrow_mut()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Read the message headers.
|
|
||||||
pub fn headers(&self) -> &HeaderMap {
|
|
||||||
&self.headers
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Mutable reference to the message headers.
|
|
||||||
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
|
||||||
&mut self.headers
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Set connection type of the message
|
|
||||||
pub fn set_connection_type(&mut self, ctype: ConnectionType) {
|
|
||||||
match ctype {
|
|
||||||
ConnectionType::Close => self.flags.insert(Flags::CLOSE),
|
|
||||||
ConnectionType::KeepAlive => self.flags.insert(Flags::KEEP_ALIVE),
|
|
||||||
ConnectionType::Upgrade => self.flags.insert(Flags::UPGRADE),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn connection_type(&self) -> ConnectionType {
|
|
||||||
if self.flags.contains(Flags::CLOSE) {
|
|
||||||
ConnectionType::Close
|
|
||||||
} else if self.flags.contains(Flags::KEEP_ALIVE) {
|
|
||||||
ConnectionType::KeepAlive
|
|
||||||
} else if self.flags.contains(Flags::UPGRADE) {
|
|
||||||
ConnectionType::Upgrade
|
|
||||||
} else if self.version < Version::HTTP_11 {
|
|
||||||
ConnectionType::Close
|
|
||||||
} else {
|
|
||||||
ConnectionType::KeepAlive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if keep-alive is enabled
|
|
||||||
#[inline]
|
|
||||||
pub fn keep_alive(&self) -> bool {
|
|
||||||
self.connection_type() == ConnectionType::KeepAlive
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check upgrade status of this message
|
|
||||||
#[inline]
|
|
||||||
pub fn upgrade(&self) -> bool {
|
|
||||||
self.connection_type() == ConnectionType::Upgrade
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get custom reason for the response
|
|
||||||
#[inline]
|
|
||||||
pub fn reason(&self) -> &str {
|
|
||||||
self.reason.unwrap_or_else(|| {
|
|
||||||
self.status
|
|
||||||
.canonical_reason()
|
|
||||||
.unwrap_or("<unknown status code>")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn ctype(&self) -> Option<ConnectionType> {
|
|
||||||
if self.flags.contains(Flags::CLOSE) {
|
|
||||||
Some(ConnectionType::Close)
|
|
||||||
} else if self.flags.contains(Flags::KEEP_ALIVE) {
|
|
||||||
Some(ConnectionType::KeepAlive)
|
|
||||||
} else if self.flags.contains(Flags::UPGRADE) {
|
|
||||||
Some(ConnectionType::Upgrade)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Get response body chunking state
|
|
||||||
pub fn chunked(&self) -> bool {
|
|
||||||
!self.flags.contains(Flags::NO_CHUNKING)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Set no chunking for payload
|
|
||||||
pub fn no_chunking(&mut self, val: bool) {
|
|
||||||
if val {
|
|
||||||
self.flags.insert(Flags::NO_CHUNKING);
|
|
||||||
} else {
|
|
||||||
self.flags.remove(Flags::NO_CHUNKING);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Message<T: Head> {
|
pub struct Message<T: Head> {
|
||||||
/// Rc here should not be cloned by anyone.
|
/// Rc here should not be cloned by anyone.
|
||||||
/// It's used to reuse allocation of T and no shared ownership is allowed.
|
/// It's used to reuse allocation of T and no shared ownership is allowed.
|
||||||
@@ -354,12 +43,13 @@ pub struct Message<T: Head> {
|
|||||||
|
|
||||||
impl<T: Head> Message<T> {
|
impl<T: Head> Message<T> {
|
||||||
/// Get new message from the pool of objects
|
/// Get new message from the pool of objects
|
||||||
|
#[allow(clippy::new_without_default)]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
T::with_pool(MessagePool::get_message)
|
T::with_pool(MessagePool::get_message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Head> std::ops::Deref for Message<T> {
|
impl<T: Head> ops::Deref for Message<T> {
|
||||||
type Target = T;
|
type Target = T;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
@@ -367,7 +57,7 @@ impl<T: Head> std::ops::Deref for Message<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Head> std::ops::DerefMut for Message<T> {
|
impl<T: Head> ops::DerefMut for Message<T> {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
Rc::get_mut(&mut self.head).expect("Multiple copies exist")
|
Rc::get_mut(&mut self.head).expect("Multiple copies exist")
|
||||||
}
|
}
|
||||||
@@ -379,53 +69,12 @@ impl<T: Head> Drop for Message<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct BoxedResponseHead {
|
/// Generic `Head` object pool.
|
||||||
head: Option<Box<ResponseHead>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BoxedResponseHead {
|
|
||||||
/// Get new message from the pool of objects
|
|
||||||
pub fn new(status: StatusCode) -> Self {
|
|
||||||
RESPONSE_POOL.with(|p| p.get_message(status))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Deref for BoxedResponseHead {
|
|
||||||
type Target = ResponseHead;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.head.as_ref().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::DerefMut for BoxedResponseHead {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
self.head.as_mut().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for BoxedResponseHead {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if let Some(head) = self.head.take() {
|
|
||||||
RESPONSE_POOL.with(move |p| p.release(head))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
/// Request's objects pool
|
|
||||||
pub struct MessagePool<T: Head>(RefCell<Vec<Rc<T>>>);
|
pub struct MessagePool<T: Head>(RefCell<Vec<Rc<T>>>);
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[allow(clippy::vec_box)]
|
|
||||||
/// Request's objects pool
|
|
||||||
pub struct BoxedResponsePool(RefCell<Vec<Box<ResponseHead>>>);
|
|
||||||
|
|
||||||
thread_local!(static REQUEST_POOL: MessagePool<RequestHead> = MessagePool::<RequestHead>::create());
|
|
||||||
thread_local!(static RESPONSE_POOL: BoxedResponsePool = BoxedResponsePool::create());
|
|
||||||
|
|
||||||
impl<T: Head> MessagePool<T> {
|
impl<T: Head> MessagePool<T> {
|
||||||
fn create() -> MessagePool<T> {
|
pub(crate) fn create() -> MessagePool<T> {
|
||||||
MessagePool(RefCell::new(Vec::with_capacity(128)))
|
MessagePool(RefCell::new(Vec::with_capacity(128)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,43 +96,11 @@ impl<T: Head> MessagePool<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
/// Release request instance
|
/// Release message instance
|
||||||
fn release(&self, msg: Rc<T>) {
|
fn release(&self, msg: Rc<T>) {
|
||||||
let v = &mut self.0.borrow_mut();
|
let pool = &mut self.0.borrow_mut();
|
||||||
if v.len() < 128 {
|
if pool.len() < 128 {
|
||||||
v.push(msg);
|
pool.push(msg);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BoxedResponsePool {
|
|
||||||
fn create() -> BoxedResponsePool {
|
|
||||||
BoxedResponsePool(RefCell::new(Vec::with_capacity(128)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get message from the pool
|
|
||||||
#[inline]
|
|
||||||
fn get_message(&self, status: StatusCode) -> BoxedResponseHead {
|
|
||||||
if let Some(mut head) = self.0.borrow_mut().pop() {
|
|
||||||
head.reason = None;
|
|
||||||
head.status = status;
|
|
||||||
head.headers.clear();
|
|
||||||
head.flags = Flags::empty();
|
|
||||||
BoxedResponseHead { head: Some(head) }
|
|
||||||
} else {
|
|
||||||
BoxedResponseHead {
|
|
||||||
head: Some(Box::new(ResponseHead::new(status))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Release request instance
|
|
||||||
fn release(&self, mut msg: Box<ResponseHead>) {
|
|
||||||
let v = &mut self.0.borrow_mut();
|
|
||||||
if v.len() < 128 {
|
|
||||||
msg.extensions.get_mut().clear();
|
|
||||||
v.push(msg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user