mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-03 01:34:32 +02:00
Compare commits
274 Commits
files-v0.6
...
web-v4.2.0
Author | SHA1 | Date | |
---|---|---|---|
7767cf3071 | |||
b59a96d9d7 | |||
037740bf62 | |||
386258c285 | |||
99bf774e94 | |||
35b0fd1a85 | |||
0b5b4dcbf3 | |||
c993055fc8 | |||
679f61cf37 | |||
056de320f0 | |||
f220719fae | |||
c9f91796df | |||
ea764b1d57 | |||
19aa14a9d6 | |||
10746fb2fb | |||
4bbe60b609 | |||
8ff489aa90 | |||
e0a88cea8d | |||
d78ff283af | |||
ce6d520215 | |||
3e25742a41 | |||
20f4cfe6b5 | |||
6408291ab0 | |||
8d260e599f | |||
14bcf72ec1 | |||
6485434a33 | |||
16c7c16463 | |||
9b0fdca6e9 | |||
8759d79b03 | |||
c0d5d7bdb5 | |||
40eab1f091 | |||
75517cce82 | |||
9b51624b27 | |||
8e2ae8cd40 | |||
9a2f8450e0 | |||
23ef51609e | |||
f7d629a61a | |||
e0845d9ad9 | |||
2f79daec16 | |||
f3f41a0cc7 | |||
987067698b | |||
b62f1b4ef7 | |||
df5257c373 | |||
226ea696ce | |||
e524fc86ea | |||
7e990e423f | |||
8f9a12ed5d | |||
c6eba2da9b | |||
06c7945801 | |||
0dba6310c6 | |||
f7d7d92984 | |||
3d6ea7fe9b | |||
8dbf7da89f | |||
de92b3be2e | |||
5d0e8138ee | |||
6b7196225e | |||
265fa0d050 | |||
062127a210 | |||
3926416580 | |||
43671ae4aa | |||
264a703d94 | |||
498fb954b3 | |||
2253eae2bb | |||
8e76a1c775 | |||
dce57a79c9 | |||
6a5b370206 | |||
b1c85ba85b | |||
9aab911600 | |||
017e40f733 | |||
45592b37b6 | |||
8abcb94512 | |||
f2cacc4c9d | |||
56b9c0d08e | |||
de9e41484a | |||
2fed978597 | |||
40048a5811 | |||
e942d3e3b1 | |||
09cffc093c | |||
c58f287044 | |||
7b27493e4c | |||
478b33b8a3 | |||
592b40f914 | |||
fe5279c77a | |||
80d222aa78 | |||
a03a2a0076 | |||
745e738955 | |||
1fd90f0b10 | |||
a35804b89f | |||
5611b98c0d | |||
dce9438518 | |||
be986d96b3 | |||
8ddb24b49b | |||
87f627cd5d | |||
03456b8a33 | |||
8c2fad3164 | |||
62fbd225bc | |||
0fa4d999d9 | |||
da4c849f62 | |||
49cd303c3b | |||
955c3ac0c4 | |||
56e5c19b85 | |||
3f03af1c59 | |||
25c0673278 | |||
e7a05f9892 | |||
2f13e5f675 | |||
9f964751f6 | |||
fcca515387 | |||
075932d823 | |||
cb379c0e0c | |||
d4a5d450de | |||
542200cbc2 | |||
d0c08dbb7d | |||
d0b5fb18d2 | |||
12fb3412a5 | |||
2665357a0c | |||
693271e571 | |||
10ef9b0751 | |||
ce00c88963 | |||
75e6ffb057 | |||
ad38973767 | |||
1c1d6477ef | |||
53509a5361 | |||
a6f27baff1 | |||
218e34ee17 | |||
11bfa84926 | |||
5aa6f713c7 | |||
151a15da74 | |||
1ce58ecb30 | |||
f940653981 | |||
b291e29882 | |||
f843776f36 | |||
52f7d96358 | |||
51e573b888 | |||
38e015432b | |||
f5895d5eff | |||
a0c4bf8d1b | |||
594e3a6ef1 | |||
a808a26d8c | |||
de62e8b025 | |||
3486edabcf | |||
4c59a34513 | |||
1b706b3069 | |||
a9f445875a | |||
e0f02c1d9e | |||
092dbba5b9 | |||
ff4b2d251f | |||
98faa61afe | |||
3f2db9e75c | |||
074d18209d | |||
593fbde46a | |||
161861997c | |||
3d621677a5 | |||
0c144054cb | |||
b0fbe0dfd8 | |||
b653bf557f | |||
1d1a65282f | |||
b0a363a7ae | |||
b4d3c2394d | |||
5ca42df89a | |||
fc5ecdc30b | |||
7fe800c3ff | |||
075df88a07 | |||
391d8a744a | |||
5b6cb681b9 | |||
0957ec40b4 | |||
ccf430d74a | |||
c84c1f0f15 | |||
e9279dfbb8 | |||
a68239adaa | |||
40a4b1ccd5 | |||
7f5a8c0851 | |||
bcdde1d4ea | |||
30aa64ea32 | |||
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 |
@ -6,9 +6,12 @@ lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dcli
|
||||
ci-check-min = "hack --workspace check --no-default-features"
|
||||
ci-check-default = "hack --workspace check"
|
||||
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="hack --workspace --feature-powerset --skip=__compress,experimental-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"
|
||||
|
||||
# compile docs as docs.rs would
|
||||
# RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace
|
||||
|
2
.github/workflows/bench.yml
vendored
2
.github/workflows/bench.yml
vendored
@ -1,8 +1,6 @@
|
||||
name: Benchmark
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
66
.github/workflows/ci-master.yml
vendored
66
.github/workflows/ci-master.yml
vendored
@ -1,66 +0,0 @@
|
||||
name: CI (master only)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
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 }
|
150
.github/workflows/ci-post-merge.yml
vendored
Normal file
150
.github/workflows/ci-post-merge.yml
vendored
Normal file
@ -0,0 +1,150 @@
|
||||
name: CI (post-merge)
|
||||
|
||||
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
|
||||
CARGO_UNSTABLE_SPARSE_REGISTRY: true
|
||||
|
||||
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: Install cargo-hack
|
||||
uses: taiki-e/install-action@cargo-hack
|
||||
|
||||
- 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 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: Clear the cargo caches
|
||||
run: |
|
||||
cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
||||
|
||||
ci_feature_powerset_check:
|
||||
name: Verify Feature Combinations
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
CI: 1
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
||||
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: Install cargo-hack
|
||||
uses: taiki-e/install-action@cargo-hack
|
||||
|
||||
- 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 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 }
|
||||
|
||||
nextest:
|
||||
name: nextest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
CI: 1
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.3.0
|
||||
|
||||
- name: Test with cargo-nextest
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: nextest
|
||||
args: run
|
51
.github/workflows/ci.yml
vendored
51
.github/workflows/ci.yml
vendored
@ -16,9 +16,8 @@ jobs:
|
||||
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
||||
- { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
|
||||
version:
|
||||
- 1.52.0 # MSRV
|
||||
- 1.59.0 # MSRV
|
||||
- stable
|
||||
- nightly
|
||||
|
||||
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
||||
runs-on: ${{ matrix.target.os }}
|
||||
@ -48,18 +47,22 @@ jobs:
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Install cargo-hack
|
||||
uses: taiki-e/install-action@cargo-hack
|
||||
|
||||
- name: workaround MSRV issues
|
||||
if: matrix.version != 'stable'
|
||||
run: |
|
||||
cargo install cargo-edit --version=0.8.0
|
||||
cargo add const-str@0.3 --dev -p=actix-web
|
||||
cargo add const-str@0.3 --dev -p=awc
|
||||
|
||||
- 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 }
|
||||
@ -82,19 +85,37 @@ jobs:
|
||||
cargo test --lib --tests -p=actix-multipart --all-features
|
||||
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||
|
||||
- name: Clear the cargo caches
|
||||
run: |
|
||||
cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
||||
|
||||
io-uring:
|
||||
name: io-uring tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
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.3.0
|
||||
|
||||
- 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
|
||||
&& RUSTUP_TOOLCHAIN=stable cargo test --lib --tests -p=actix-files --all-features"
|
||||
|
||||
rustdoc:
|
||||
name: doc tests
|
||||
|
29
.github/workflows/clippy-fmt.yml
vendored
29
.github/workflows/clippy-fmt.yml
vendored
@ -14,6 +14,7 @@ jobs:
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
components: rustfmt
|
||||
- name: Check with rustfmt
|
||||
uses: actions-rs/cargo@v1
|
||||
@ -30,10 +31,36 @@ jobs:
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
components: clippy
|
||||
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
|
||||
uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
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
|
||||
|
36
.github/workflows/coverage.yml
vendored
Normal file
36
.github/workflows/coverage.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
# disabled because `cargo tarpaulin` currently segfaults
|
||||
|
||||
name: Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
# job currently (1st Feb 2022) segfaults
|
||||
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 }
|
7
.github/workflows/upload-doc.yml
vendored
7
.github/workflows/upload-doc.yml
vendored
@ -28,8 +28,7 @@ jobs:
|
||||
run: echo '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > target/doc/index.html
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: JamesIves/github-pages-deploy-action@3.7.1
|
||||
uses: JamesIves/github-pages-deploy-action@v4.4.0
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BRANCH: gh-pages
|
||||
FOLDER: target/doc
|
||||
folder: target/doc
|
||||
single-commit: true
|
||||
|
3
.prettierrc.json
Normal file
3
.prettierrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"proseWrap": "never"
|
||||
}
|
953
CHANGES.md
953
CHANGES.md
@ -1,952 +1,5 @@
|
||||
# Changes
|
||||
# Changelog
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
Changelogs are kept separately for each crate in this repo.
|
||||
|
||||
|
||||
## 4.0.0-beta.17 - 2021-12-29
|
||||
### Added
|
||||
- `guard::GuardContext` for use with the `Guard` trait. [#2552]
|
||||
- `ServiceRequest::guard_ctx` for obtaining a guard context. [#2552]
|
||||
|
||||
### Changed
|
||||
- `Guard` trait now receives a `&GuardContext`. [#2552]
|
||||
- `guard::fn_guard` functions now receives a `&GuardContext`. [#2552]
|
||||
- Some guards now return `impl Guard` and their concrete types are made private: `guard::Header` and all the method guards. [#2552]
|
||||
- The `Not` guard is now generic over the type of guard it wraps. [#2552]
|
||||
|
||||
### Fixed
|
||||
- Rename `ConnectionInfo::{remote_addr => peer_addr}`, deprecating the old name. [#2554]
|
||||
- `ConnectionInfo::peer_addr` will not return the port number. [#2554]
|
||||
- `ConnectionInfo::realip_remote_addr` will not return the port number if sourcing the IP from the peer's socket address. [#2554]
|
||||
|
||||
[#2552]: https://github.com/actix/actix-web/pull/2552
|
||||
[#2554]: https://github.com/actix/actix-web/pull/2554
|
||||
|
||||
|
||||
## 4.0.0-beta.16 - 2021-12-27
|
||||
### Changed
|
||||
- No longer require `Scope` service body type to be boxed. [#2523]
|
||||
- No longer require `Resource` service body type to be boxed. [#2526]
|
||||
|
||||
[#2523]: https://github.com/actix/actix-web/pull/2523
|
||||
[#2526]: https://github.com/actix/actix-web/pull/2526
|
||||
|
||||
|
||||
## 4.0.0-beta.15 - 2021-12-17
|
||||
### Added
|
||||
- Method on `Responder` trait (`customize`) for customizing responders and `CustomizeResponder` struct. [#2510]
|
||||
- Implement `Debug` for `DefaultHeaders`. [#2510]
|
||||
|
||||
### Changed
|
||||
- Align `DefaultHeader` method terminology, deprecating previous methods. [#2510]
|
||||
- Response service types in `ErrorHandlers` middleware now use `ServiceResponse<EitherBody<B>>` to allow changing the body type. [#2515]
|
||||
- Both variants in `ErrorHandlerResponse` now use `ServiceResponse<EitherBody<B>>`. [#2515]
|
||||
- Rename `test::{default_service => simple_service}`. Old name is deprecated. [#2518]
|
||||
- Rename `test::{read_response_json => call_and_read_body_json}`. Old name is deprecated. [#2518]
|
||||
- Rename `test::{read_response => call_and_read_body}`. Old name is deprecated. [#2518]
|
||||
- Relax body type and error bounds on test utilities. [#2518]
|
||||
|
||||
### Removed
|
||||
- Top-level `EitherExtractError` export. [#2510]
|
||||
- Conversion implementations for `either` crate. [#2516]
|
||||
- `test::load_stream` and `test::load_body`; replace usage with `body::to_bytes`. [#2518]
|
||||
|
||||
[#2510]: https://github.com/actix/actix-web/pull/2510
|
||||
[#2515]: https://github.com/actix/actix-web/pull/2515
|
||||
[#2516]: https://github.com/actix/actix-web/pull/2516
|
||||
[#2518]: https://github.com/actix/actix-web/pull/2518
|
||||
|
||||
|
||||
## 4.0.0-beta.14 - 2021-12-11
|
||||
### Added
|
||||
- Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480]
|
||||
- `AcceptEncoding` typed header. [#2482]
|
||||
- `Range` typed header. [#2485]
|
||||
- `HttpResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468]
|
||||
- `ServiceResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468]
|
||||
- Connection data set through the `HttpServer::on_connect` callback is now accessible only from the new `HttpRequest::conn_data()` and `ServiceRequest::conn_data()` methods. [#2491]
|
||||
- `HttpRequest::{req_data,req_data_mut}`. [#2487]
|
||||
- `ServiceResponse::into_parts`. [#2499]
|
||||
|
||||
### Changed
|
||||
- Rename `Accept::{mime_precedence => ranked}`. [#2480]
|
||||
- Rename `Accept::{mime_preference => preference}`. [#2480]
|
||||
- Un-deprecate `App::data_factory`. [#2484]
|
||||
- `HttpRequest::url_for` no longer constructs URLs with query or fragment components. [#2430]
|
||||
- Remove `B` (body) type parameter on `App`. [#2493]
|
||||
- Add `B` (body) type parameter on `Scope`. [#2492]
|
||||
- Request-local data container is no longer part of a `RequestHead`. Instead it is a distinct part of a `Request`. [#2487]
|
||||
|
||||
### Fixed
|
||||
- Accept wildcard `*` items in `AcceptLanguage`. [#2480]
|
||||
- Re-exports `dev::{BodySize, MessageBody, SizedStream}`. They are exposed through the `body` module. [#2468]
|
||||
- Typed headers containing lists that require one or more items now enforce this minimum. [#2482]
|
||||
|
||||
### Removed
|
||||
- `ConnectionInfo::get`. [#2487]
|
||||
|
||||
[#2430]: https://github.com/actix/actix-web/pull/2430
|
||||
[#2468]: https://github.com/actix/actix-web/pull/2468
|
||||
[#2480]: https://github.com/actix/actix-web/pull/2480
|
||||
[#2482]: https://github.com/actix/actix-web/pull/2482
|
||||
[#2484]: https://github.com/actix/actix-web/pull/2484
|
||||
[#2485]: https://github.com/actix/actix-web/pull/2485
|
||||
[#2487]: https://github.com/actix/actix-web/pull/2487
|
||||
[#2491]: https://github.com/actix/actix-web/pull/2491
|
||||
[#2492]: https://github.com/actix/actix-web/pull/2492
|
||||
[#2493]: https://github.com/actix/actix-web/pull/2493
|
||||
[#2499]: https://github.com/actix/actix-web/pull/2499
|
||||
|
||||
|
||||
## 4.0.0-beta.13 - 2021-11-30
|
||||
### Changed
|
||||
- Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
||||
|
||||
[#2474]: https://github.com/actix/actix-web/pull/2474
|
||||
|
||||
|
||||
## 4.0.0-beta.12 - 2021-11-22
|
||||
### Changed
|
||||
- Compress middleware's response type is now `AnyBody<Encoder<B>>`. [#2448]
|
||||
|
||||
### Fixed
|
||||
- Relax `Unpin` bound on `S` (stream) parameter of `HttpResponseBuilder::streaming`. [#2448]
|
||||
|
||||
### Removed
|
||||
- `dev::ResponseBody` re-export; is function is replaced by the new `dev::AnyBody` enum. [#2446]
|
||||
|
||||
[#2446]: https://github.com/actix/actix-web/pull/2446
|
||||
[#2448]: https://github.com/actix/actix-web/pull/2448
|
||||
|
||||
|
||||
## 4.0.0-beta.11 - 2021-11-15
|
||||
### Added
|
||||
- Re-export `dev::ServerHandle` from `actix-server`. [#2442]
|
||||
|
||||
### Changed
|
||||
- `ContentType::html` now produces `text/html; charset=utf-8` instead of `text/html`. [#2423]
|
||||
- Update `actix-server` to `2.0.0-beta.9`. [#2442]
|
||||
|
||||
[#2423]: https://github.com/actix/actix-web/pull/2423
|
||||
[#2442]: https://github.com/actix/actix-web/pull/2442
|
||||
|
||||
|
||||
## 4.0.0-beta.10 - 2021-10-20
|
||||
### Added
|
||||
- Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362]
|
||||
- `#[actix_web::test]` macro for setting up tests with a runtime. [#2409]
|
||||
|
||||
### Changed
|
||||
- Associated type `FromRequest::Config` was removed. [#2233]
|
||||
- Inner field made private on `web::Payload`. [#2384]
|
||||
- `Data::into_inner` and `Data::get_ref` no longer requires `T: Sized`. [#2403]
|
||||
- Updated rustls to v0.20. [#2414]
|
||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
||||
### Removed
|
||||
- Useless `ServiceResponse::checked_expr` method. [#2401]
|
||||
|
||||
[#2233]: https://github.com/actix/actix-web/pull/2233
|
||||
[#2362]: https://github.com/actix/actix-web/pull/2362
|
||||
[#2384]: https://github.com/actix/actix-web/pull/2384
|
||||
[#2401]: https://github.com/actix/actix-web/pull/2401
|
||||
[#2403]: https://github.com/actix/actix-web/pull/2403
|
||||
[#2409]: https://github.com/actix/actix-web/pull/2409
|
||||
[#2414]: https://github.com/actix/actix-web/pull/2414
|
||||
|
||||
|
||||
## 4.0.0-beta.9 - 2021-09-09
|
||||
### Added
|
||||
- Re-export actix-service `ServiceFactory` in `dev` module. [#2325]
|
||||
|
||||
### Changed
|
||||
- Compress middleware will return 406 Not Acceptable when no content encoding is acceptable to the client. [#2344]
|
||||
- Move `BaseHttpResponse` to `dev::Response`. [#2379]
|
||||
- Enable `TestRequest::param` to accept more than just static strings. [#2172]
|
||||
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||
|
||||
### Fixed
|
||||
- Fix quality parse error in Accept-Encoding header. [#2344]
|
||||
- Re-export correct type at `web::HttpResponse`. [#2379]
|
||||
|
||||
[#2172]: https://github.com/actix/actix-web/pull/2172
|
||||
[#2325]: https://github.com/actix/actix-web/pull/2325
|
||||
[#2344]: https://github.com/actix/actix-web/pull/2344
|
||||
[#2379]: https://github.com/actix/actix-web/pull/2379
|
||||
|
||||
|
||||
## 4.0.0-beta.8 - 2021-06-26
|
||||
### Added
|
||||
- Add `ServiceRequest::parts_mut`. [#2177]
|
||||
- Add extractors for `Uri` and `Method`. [#2263]
|
||||
- Add extractors for `ConnectionInfo` and `PeerAddr`. [#2263]
|
||||
- Add `Route::service` for using hand-written services as handlers. [#2262]
|
||||
|
||||
### Changed
|
||||
- Change compression algorithm features flags. [#2250]
|
||||
- Deprecate `App::data` and `App::data_factory`. [#2271]
|
||||
- Smarter extraction of `ConnectionInfo` parts. [#2282]
|
||||
|
||||
### Fixed
|
||||
- Scope and Resource middleware can access data items set on their own layer. [#2288]
|
||||
|
||||
[#2177]: https://github.com/actix/actix-web/pull/2177
|
||||
[#2250]: https://github.com/actix/actix-web/pull/2250
|
||||
[#2271]: https://github.com/actix/actix-web/pull/2271
|
||||
[#2262]: https://github.com/actix/actix-web/pull/2262
|
||||
[#2263]: https://github.com/actix/actix-web/pull/2263
|
||||
[#2282]: https://github.com/actix/actix-web/pull/2282
|
||||
[#2288]: https://github.com/actix/actix-web/pull/2288
|
||||
|
||||
|
||||
## 4.0.0-beta.7 - 2021-06-17
|
||||
### Added
|
||||
- `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200]
|
||||
|
||||
### Changed
|
||||
- Adjusted default JSON payload limit to 2MB (from 32kb) and included size and limits in the `JsonPayloadError::Overflow` error variant. [#2162]
|
||||
[#2162]: (https://github.com/actix/actix-web/pull/2162)
|
||||
- `ServiceResponse::error_response` now uses body type of `Body`. [#2201]
|
||||
- `ServiceResponse::checked_expr` now returns a `Result`. [#2201]
|
||||
- Update `language-tags` to `0.3`.
|
||||
- `ServiceResponse::take_body`. [#2201]
|
||||
- `ServiceResponse::map_body` closure receives and returns `B` instead of `ResponseBody<B>` types. [#2201]
|
||||
- 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]
|
||||
- `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
|
||||
- `middleware::normalize` now will not try to normalize URIs with no valid path [#2246]
|
||||
|
||||
### Removed
|
||||
- `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201]
|
||||
|
||||
[#2200]: https://github.com/actix/actix-web/pull/2200
|
||||
[#2201]: https://github.com/actix/actix-web/pull/2201
|
||||
[#2253]: https://github.com/actix/actix-web/pull/2253
|
||||
[#2246]: https://github.com/actix/actix-web/pull/2246
|
||||
|
||||
|
||||
## 4.0.0-beta.6 - 2021-04-17
|
||||
### Added
|
||||
- `HttpResponse` and `HttpResponseBuilder` structs. [#2065]
|
||||
|
||||
### Changed
|
||||
- Most error types are now marked `#[non_exhaustive]`. [#2148]
|
||||
- Methods on `ContentDisposition` that took `T: AsRef<str>` now take `impl AsRef<str>`.
|
||||
|
||||
[#2065]: https://github.com/actix/actix-web/pull/2065
|
||||
[#2148]: https://github.com/actix/actix-web/pull/2148
|
||||
|
||||
|
||||
## 4.0.0-beta.5 - 2021-04-02
|
||||
### Added
|
||||
- `Header` extractor for extracting common HTTP headers in handlers. [#2094]
|
||||
- Added `TestServer::client_headers` method. [#2097]
|
||||
|
||||
### Fixed
|
||||
- Double ampersand in Logger format is escaped correctly. [#2067]
|
||||
|
||||
### Changed
|
||||
- `CustomResponder` would return error as `HttpResponse` when `CustomResponder::with_header` failed
|
||||
instead of skipping. (Only the first error is kept when multiple error occur) [#2093]
|
||||
|
||||
### Removed
|
||||
- The `client` mod was removed. Clients should now use `awc` directly.
|
||||
[871ca5e4](https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7)
|
||||
- Integration testing was moved to new `actix-test` crate. Namely these items from the `test`
|
||||
module: `TestServer`, `TestServerConfig`, `start`, `start_with`, and `unused_addr`. [#2112]
|
||||
|
||||
[#2067]: https://github.com/actix/actix-web/pull/2067
|
||||
[#2093]: https://github.com/actix/actix-web/pull/2093
|
||||
[#2094]: https://github.com/actix/actix-web/pull/2094
|
||||
[#2097]: https://github.com/actix/actix-web/pull/2097
|
||||
[#2112]: https://github.com/actix/actix-web/pull/2112
|
||||
|
||||
|
||||
## 4.0.0-beta.4 - 2021-03-09
|
||||
### Changed
|
||||
- Feature `cookies` is now optional and enabled by default. [#1981]
|
||||
- `JsonBody::new` returns a default limit of 32kB to be consistent with `JsonConfig` and the default
|
||||
behaviour of the `web::Json<T>` extractor. [#2010]
|
||||
|
||||
[#1981]: https://github.com/actix/actix-web/pull/1981
|
||||
[#2010]: https://github.com/actix/actix-web/pull/2010
|
||||
|
||||
|
||||
## 4.0.0-beta.3 - 2021-02-10
|
||||
- Update `actix-web-codegen` to `0.5.0-beta.1`.
|
||||
|
||||
|
||||
## 4.0.0-beta.2 - 2021-02-10
|
||||
### Added
|
||||
- The method `Either<web::Json<T>, web::Form<T>>::into_inner()` which returns the inner type for
|
||||
whichever variant was created. Also works for `Either<web::Form<T>, web::Json<T>>`. [#1894]
|
||||
- Add `services!` macro for helping register multiple services to `App`. [#1933]
|
||||
- Enable registering a vec of services of the same type to `App` [#1933]
|
||||
|
||||
### Changed
|
||||
- Rework `Responder` trait to be sync and returns `Response`/`HttpResponse` directly.
|
||||
Making it simpler and more performant. [#1891]
|
||||
- `ServiceRequest::into_parts` and `ServiceRequest::from_parts` can no longer fail. [#1893]
|
||||
- `ServiceRequest::from_request` can no longer fail. [#1893]
|
||||
- Our `Either` type now uses `Left`/`Right` variants (instead of `A`/`B`) [#1894]
|
||||
- `test::{call_service, read_response, read_response_json, send_request}` take `&Service`
|
||||
in argument [#1905]
|
||||
- `App::wrap_fn`, `Resource::wrap_fn` and `Scope::wrap_fn` provide `&Service` in closure
|
||||
argument. [#1905]
|
||||
- `web::block` no longer requires the output is a Result. [#1957]
|
||||
|
||||
### Fixed
|
||||
- Multiple calls to `App::data` with the same type now keeps the latest call's data. [#1906]
|
||||
|
||||
### Removed
|
||||
- Public field of `web::Path` has been made private. [#1894]
|
||||
- Public field of `web::Query` has been made private. [#1894]
|
||||
- `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869]
|
||||
- `AppService::set_service_data`; for custom HTTP service factories adding application data, use the
|
||||
layered data model by calling `ServiceRequest::add_data_container` when handling
|
||||
requests instead. [#1906]
|
||||
|
||||
[#1891]: https://github.com/actix/actix-web/pull/1891
|
||||
[#1893]: https://github.com/actix/actix-web/pull/1893
|
||||
[#1894]: https://github.com/actix/actix-web/pull/1894
|
||||
[#1869]: https://github.com/actix/actix-web/pull/1869
|
||||
[#1905]: https://github.com/actix/actix-web/pull/1905
|
||||
[#1906]: https://github.com/actix/actix-web/pull/1906
|
||||
[#1933]: https://github.com/actix/actix-web/pull/1933
|
||||
[#1957]: https://github.com/actix/actix-web/pull/1957
|
||||
|
||||
|
||||
## 4.0.0-beta.1 - 2021-01-07
|
||||
### Added
|
||||
- `Compat` middleware enabling generic response body/error type of middlewares like `Logger` and
|
||||
`Compress` to be used in `middleware::Condition` and `Resource`, `Scope` services. [#1865]
|
||||
|
||||
### Changed
|
||||
- Update `actix-*` dependencies to tokio `1.0` based versions. [#1813]
|
||||
- Bumped `rand` to `0.8`.
|
||||
- Update `rust-tls` to `0.19`. [#1813]
|
||||
- Rename `Handler` to `HandlerService` and rename `Factory` to `Handler`. [#1852]
|
||||
- The default `TrailingSlash` is now `Trim`, in line with existing documentation. See migration
|
||||
guide for implications. [#1875]
|
||||
- Rename `DefaultHeaders::{content_type => add_content_type}`. [#1875]
|
||||
- MSRV is now 1.46.0.
|
||||
|
||||
### Fixed
|
||||
- Added the underlying parse error to `test::read_body_json`'s panic message. [#1812]
|
||||
|
||||
### Removed
|
||||
- Public modules `middleware::{normalize, err_handlers}`. All necessary middleware structs are now
|
||||
exposed directly by the `middleware` module.
|
||||
- Remove `actix-threadpool` as dependency. `actix_threadpool::BlockingError` error type can be imported
|
||||
from `actix_web::error` module. [#1878]
|
||||
|
||||
[#1812]: https://github.com/actix/actix-web/pull/1812
|
||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||
[#1852]: https://github.com/actix/actix-web/pull/1852
|
||||
[#1865]: https://github.com/actix/actix-web/pull/1865
|
||||
[#1875]: https://github.com/actix/actix-web/pull/1875
|
||||
[#1878]: https://github.com/actix/actix-web/pull/1878
|
||||
|
||||
|
||||
## 3.3.3 - 2021-12-18
|
||||
### Changed
|
||||
- Soft-deprecate `NormalizePath::default()`, noting upcoming behavior change in v4. [#2529]
|
||||
|
||||
[#2529]: https://github.com/actix/actix-web/pull/2529
|
||||
|
||||
|
||||
## 3.3.2 - 2020-12-01
|
||||
### Fixed
|
||||
- Removed an occasional `unwrap` on `None` panic in `NormalizePathNormalization`. [#1762]
|
||||
- Fix `match_pattern()` returning `None` for scope with empty path resource. [#1798]
|
||||
- Increase minimum `socket2` version. [#1803]
|
||||
|
||||
[#1762]: https://github.com/actix/actix-web/pull/1762
|
||||
[#1798]: https://github.com/actix/actix-web/pull/1798
|
||||
[#1803]: https://github.com/actix/actix-web/pull/1803
|
||||
|
||||
|
||||
## 3.3.1 - 2020-11-29
|
||||
- Ensure `actix-http` dependency uses same `serde_urlencoded`.
|
||||
|
||||
|
||||
## 3.3.0 - 2020-11-25
|
||||
### Added
|
||||
- Add `Either<A, B>` extractor helper. [#1788]
|
||||
|
||||
### Changed
|
||||
- Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||
|
||||
[#1773]: https://github.com/actix/actix-web/pull/1773
|
||||
[#1788]: https://github.com/actix/actix-web/pull/1788
|
||||
|
||||
|
||||
## 3.2.0 - 2020-10-30
|
||||
### Added
|
||||
- Implement `exclude_regex` for Logger middleware. [#1723]
|
||||
- Add request-local data extractor `web::ReqData`. [#1748]
|
||||
- Add ability to register closure for request middleware logging. [#1749]
|
||||
- Add `app_data` to `ServiceConfig`. [#1757]
|
||||
- Expose `on_connect` for access to the connection stream before request is handled. [#1754]
|
||||
|
||||
### Changed
|
||||
- Updated actix-web-codegen dependency for access to new `#[route(...)]` multi-method macro.
|
||||
- Print non-configured `Data<T>` type when attempting extraction. [#1743]
|
||||
- Re-export bytes::Buf{Mut} in web module. [#1750]
|
||||
- Upgrade `pin-project` to `1.0`.
|
||||
|
||||
[#1723]: https://github.com/actix/actix-web/pull/1723
|
||||
[#1743]: https://github.com/actix/actix-web/pull/1743
|
||||
[#1748]: https://github.com/actix/actix-web/pull/1748
|
||||
[#1750]: https://github.com/actix/actix-web/pull/1750
|
||||
[#1754]: https://github.com/actix/actix-web/pull/1754
|
||||
[#1749]: https://github.com/actix/actix-web/pull/1749
|
||||
|
||||
|
||||
## 3.1.0 - 2020-09-29
|
||||
### Changed
|
||||
- Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allows `NormalizePath`
|
||||
to retain any trailing slashes. [#1695]
|
||||
- Remove bound `std::marker::Sized` from `web::Data` to support storing `Arc<dyn Trait>`
|
||||
via `web::Data::from` [#1710]
|
||||
|
||||
### Fixed
|
||||
- `ResourceMap` debug printing is no longer infinitely recursive. [#1708]
|
||||
|
||||
[#1695]: https://github.com/actix/actix-web/pull/1695
|
||||
[#1708]: https://github.com/actix/actix-web/pull/1708
|
||||
[#1710]: https://github.com/actix/actix-web/pull/1710
|
||||
|
||||
|
||||
## 3.0.2 - 2020-09-15
|
||||
### Fixed
|
||||
- `NormalizePath` when used with `TrailingSlash::Trim` no longer trims the root path "/". [#1678]
|
||||
|
||||
[#1678]: https://github.com/actix/actix-web/pull/1678
|
||||
|
||||
|
||||
## 3.0.1 - 2020-09-13
|
||||
### Changed
|
||||
- `middleware::normalize::TrailingSlash` enum is now accessible. [#1673]
|
||||
|
||||
[#1673]: https://github.com/actix/actix-web/pull/1673
|
||||
|
||||
|
||||
## 3.0.0 - 2020-09-11
|
||||
- No significant changes from `3.0.0-beta.4`.
|
||||
|
||||
|
||||
## 3.0.0-beta.4 - 2020-09-09
|
||||
### Added
|
||||
- `middleware::NormalizePath` now has configurable behavior for either always having a trailing
|
||||
slash, or as the new addition, always trimming trailing slashes. [#1639]
|
||||
|
||||
### Changed
|
||||
- Update actix-codec and actix-utils dependencies. [#1634]
|
||||
- `FormConfig` and `JsonConfig` configurations are now also considered when set
|
||||
using `App::data`. [#1641]
|
||||
- `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. [#1655]
|
||||
- `HttpServer::maxconnrate` is renamed to the more expressive
|
||||
`HttpServer::max_connection_rate`. [#1655]
|
||||
|
||||
[#1639]: https://github.com/actix/actix-web/pull/1639
|
||||
[#1641]: https://github.com/actix/actix-web/pull/1641
|
||||
[#1634]: https://github.com/actix/actix-web/pull/1634
|
||||
[#1655]: https://github.com/actix/actix-web/pull/1655
|
||||
|
||||
## 3.0.0-beta.3 - 2020-08-17
|
||||
### Changed
|
||||
- Update `rustls` to 0.18
|
||||
|
||||
|
||||
## 3.0.0-beta.2 - 2020-08-17
|
||||
### Changed
|
||||
- `PayloadConfig` is now also considered in `Bytes` and `String` extractors when set
|
||||
using `App::data`. [#1610]
|
||||
- `web::Path` now has a public representation: `web::Path(pub T)` that enables
|
||||
destructuring. [#1594]
|
||||
- `ServiceRequest::app_data` allows retrieval of non-Data data without splitting into parts to
|
||||
access `HttpRequest` which already allows this. [#1618]
|
||||
- Re-export all error types from `awc`. [#1621]
|
||||
- MSRV is now 1.42.0.
|
||||
|
||||
### Fixed
|
||||
- Memory leak of app data in pooled requests. [#1609]
|
||||
|
||||
[#1594]: https://github.com/actix/actix-web/pull/1594
|
||||
[#1609]: https://github.com/actix/actix-web/pull/1609
|
||||
[#1610]: https://github.com/actix/actix-web/pull/1610
|
||||
[#1618]: https://github.com/actix/actix-web/pull/1618
|
||||
[#1621]: https://github.com/actix/actix-web/pull/1621
|
||||
|
||||
|
||||
## 3.0.0-beta.1 - 2020-07-13
|
||||
### Added
|
||||
- Re-export `actix_rt::main` as `actix_web::main`.
|
||||
- `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched
|
||||
resource pattern.
|
||||
- `HttpRequest::match_name` and `ServiceRequest::match_name` for extracting matched resource name.
|
||||
|
||||
### Changed
|
||||
- Fix actix_http::h1::dispatcher so it returns when HW_BUFFER_SIZE is reached. Should reduce peak memory consumption during large uploads. [#1550]
|
||||
- Migrate cookie handling to `cookie` crate. Actix-web no longer requires `ring` dependency.
|
||||
- MSRV is now 1.41.1
|
||||
|
||||
### Fixed
|
||||
- `NormalizePath` improved consistency when path needs slashes added _and_ removed.
|
||||
|
||||
|
||||
## 3.0.0-alpha.3 - 2020-05-21
|
||||
### Added
|
||||
- Add option to create `Data<T>` from `Arc<T>` [#1509]
|
||||
|
||||
### Changed
|
||||
- Resources and Scopes can now access non-overridden data types set on App (or containing scopes) when setting their own data. [#1486]
|
||||
- Fix audit issue logging by default peer address [#1485]
|
||||
- Bump minimum supported Rust version to 1.40
|
||||
- Replace deprecated `net2` crate with `socket2`
|
||||
|
||||
[#1485]: https://github.com/actix/actix-web/pull/1485
|
||||
[#1509]: https://github.com/actix/actix-web/pull/1509
|
||||
|
||||
## [3.0.0-alpha.2] - 2020-05-08
|
||||
|
||||
### Changed
|
||||
|
||||
- `{Resource,Scope}::default_service(f)` handlers now support app data extraction. [#1452]
|
||||
- Implement `std::error::Error` for our custom errors [#1422]
|
||||
- NormalizePath middleware now appends trailing / so that routes of form /example/ respond to /example requests. [#1433]
|
||||
- Remove the `failure` feature and support.
|
||||
|
||||
[#1422]: https://github.com/actix/actix-web/pull/1422
|
||||
[#1433]: https://github.com/actix/actix-web/pull/1433
|
||||
[#1452]: https://github.com/actix/actix-web/pull/1452
|
||||
[#1486]: https://github.com/actix/actix-web/pull/1486
|
||||
|
||||
|
||||
## [3.0.0-alpha.1] - 2020-03-11
|
||||
|
||||
### Added
|
||||
|
||||
- Add helper function for creating routes with `TRACE` method guard `web::trace()`
|
||||
- Add convenience functions `test::read_body_json()` and `test::TestRequest::send_request()` for testing.
|
||||
|
||||
### Changed
|
||||
|
||||
- Use `sha-1` crate instead of unmaintained `sha1` crate
|
||||
- Skip empty chunks when returning response from a `Stream` [#1308]
|
||||
- Update the `time` dependency to 0.2.7
|
||||
- Update `actix-tls` dependency to 2.0.0-alpha.1
|
||||
- Update `rustls` dependency to 0.17
|
||||
|
||||
[#1308]: https://github.com/actix/actix-web/pull/1308
|
||||
|
||||
## [2.0.0] - 2019-12-25
|
||||
|
||||
### Changed
|
||||
|
||||
- Rename `HttpServer::start()` to `HttpServer::run()`
|
||||
|
||||
- Allow to gracefully stop test server via `TestServer::stop()`
|
||||
|
||||
- Allow to specify multi-patterns for resources
|
||||
|
||||
## [2.0.0-rc] - 2019-12-20
|
||||
|
||||
### Changed
|
||||
|
||||
- Move `BodyEncoding` to `dev` module #1220
|
||||
|
||||
- Allow to set `peer_addr` for TestRequest #1074
|
||||
|
||||
- Make web::Data deref to Arc<T> #1214
|
||||
|
||||
- Rename `App::register_data()` to `App::app_data()`
|
||||
|
||||
- `HttpRequest::app_data<T>()` returns `Option<&T>` instead of `Option<&Data<T>>`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix `AppConfig::secure()` is always false. #1202
|
||||
|
||||
|
||||
## [2.0.0-alpha.6] - 2019-12-15
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed compilation with default features off
|
||||
|
||||
## [2.0.0-alpha.5] - 2019-12-13
|
||||
|
||||
### Added
|
||||
|
||||
- Add test server, `test::start()` and `test::start_with()`
|
||||
|
||||
## [2.0.0-alpha.4] - 2019-12-08
|
||||
|
||||
### Deleted
|
||||
|
||||
- Delete HttpServer::run(), it is not useful with async/await
|
||||
|
||||
## [2.0.0-alpha.3] - 2019-12-07
|
||||
|
||||
### Changed
|
||||
|
||||
- Migrate to tokio 0.2
|
||||
|
||||
|
||||
## [2.0.0-alpha.1] - 2019-11-22
|
||||
|
||||
### Changed
|
||||
|
||||
- Migrated to `std::future`
|
||||
|
||||
- Remove implementation of `Responder` for `()`. (#1167)
|
||||
|
||||
|
||||
## [1.0.9] - 2019-11-14
|
||||
|
||||
### Added
|
||||
|
||||
- Add `Payload::into_inner` method and make stored `def::Payload` public. (#1110)
|
||||
|
||||
### Changed
|
||||
|
||||
- Support `Host` guards when the `Host` header is unset (e.g. HTTP/2 requests) (#1129)
|
||||
|
||||
|
||||
## [1.0.8] - 2019-09-25
|
||||
|
||||
### Added
|
||||
|
||||
- Add `Scope::register_data` and `Resource::register_data` methods, parallel to
|
||||
`App::register_data`.
|
||||
|
||||
- Add `middleware::Condition` that conditionally enables another middleware
|
||||
|
||||
- Allow to re-construct `ServiceRequest` from `HttpRequest` and `Payload`
|
||||
|
||||
- Add `HttpServer::listen_uds` for ability to listen on UDS FD rather than path,
|
||||
which is useful for example with systemd.
|
||||
|
||||
### Changed
|
||||
|
||||
- Make UrlEncodedError::Overflow more informative
|
||||
|
||||
- Use actix-testing for testing utils
|
||||
|
||||
|
||||
## [1.0.7] - 2019-08-29
|
||||
|
||||
### Fixed
|
||||
|
||||
- Request Extensions leak #1062
|
||||
|
||||
|
||||
## [1.0.6] - 2019-08-28
|
||||
|
||||
### Added
|
||||
|
||||
- Re-implement Host predicate (#989)
|
||||
|
||||
- Form implements Responder, returning a `application/x-www-form-urlencoded` response
|
||||
|
||||
- Add `into_inner` to `Data`
|
||||
|
||||
- Add `test::TestRequest::set_form()` convenience method to automatically serialize data and set
|
||||
the header in test requests.
|
||||
|
||||
### Changed
|
||||
|
||||
- `Query` payload made `pub`. Allows user to pattern-match the payload.
|
||||
|
||||
- Enable `rust-tls` feature for client #1045
|
||||
|
||||
- Update serde_urlencoded to 0.6.1
|
||||
|
||||
- Update url to 2.1
|
||||
|
||||
|
||||
## [1.0.5] - 2019-07-18
|
||||
|
||||
### Added
|
||||
|
||||
- Unix domain sockets (HttpServer::bind_uds) #92
|
||||
|
||||
- Actix now logs errors resulting in "internal server error" responses always, with the `error`
|
||||
logging level
|
||||
|
||||
### Fixed
|
||||
|
||||
- Restored logging of errors through the `Logger` middleware
|
||||
|
||||
|
||||
## [1.0.4] - 2019-07-17
|
||||
|
||||
### Added
|
||||
|
||||
- Add `Responder` impl for `(T, StatusCode) where T: Responder`
|
||||
|
||||
- Allow to access app's resource map via
|
||||
`ServiceRequest::resource_map()` and `HttpRequest::resource_map()` methods.
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade `rand` dependency version to 0.7
|
||||
|
||||
|
||||
## [1.0.3] - 2019-06-28
|
||||
|
||||
### Added
|
||||
|
||||
- Support asynchronous data factories #850
|
||||
|
||||
### Changed
|
||||
|
||||
- Use `encoding_rs` crate instead of unmaintained `encoding` crate
|
||||
|
||||
|
||||
## [1.0.2] - 2019-06-17
|
||||
|
||||
### Changed
|
||||
|
||||
- Move cors middleware to `actix-cors` crate.
|
||||
|
||||
- Move identity middleware to `actix-identity` crate.
|
||||
|
||||
|
||||
## [1.0.1] - 2019-06-17
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for PathConfig #903
|
||||
|
||||
- Add `middleware::identity::RequestIdentity` trait to `get_identity` from `HttpMessage`.
|
||||
|
||||
### Changed
|
||||
|
||||
- Move cors middleware to `actix-cors` crate.
|
||||
|
||||
- Move identity middleware to `actix-identity` crate.
|
||||
|
||||
- Disable default feature `secure-cookies`.
|
||||
|
||||
- Allow to test an app that uses async actors #897
|
||||
|
||||
- Re-apply patch from #637 #894
|
||||
|
||||
### Fixed
|
||||
|
||||
- HttpRequest::url_for is broken with nested scopes #915
|
||||
|
||||
|
||||
## [1.0.0] - 2019-06-05
|
||||
|
||||
### Added
|
||||
|
||||
- Add `Scope::configure()` method.
|
||||
|
||||
- Add `ServiceRequest::set_payload()` method.
|
||||
|
||||
- Add `test::TestRequest::set_json()` convenience method to automatically
|
||||
serialize data and set header in test requests.
|
||||
|
||||
- Add macros for head, options, trace, connect and patch http methods
|
||||
|
||||
### Changed
|
||||
|
||||
- Drop an unnecessary `Option<_>` indirection around `ServerBuilder` from `HttpServer`. #863
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Logger request time format, and use rfc3339. #867
|
||||
|
||||
- Clear http requests pool on app service drop #860
|
||||
|
||||
|
||||
## [1.0.0-rc] - 2019-05-18
|
||||
|
||||
### Added
|
||||
|
||||
- Add `Query<T>::from_query()` to extract parameters from a query string. #846
|
||||
- `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors.
|
||||
|
||||
### Changed
|
||||
|
||||
- `JsonConfig` is now `Send + Sync`, this implies that `error_handler` must be `Send + Sync` too.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Codegen with parameters in the path only resolves the first registered endpoint #841
|
||||
|
||||
|
||||
## [1.0.0-beta.4] - 2019-05-12
|
||||
|
||||
### Added
|
||||
|
||||
- Allow to set/override app data on scope level
|
||||
|
||||
### Changed
|
||||
|
||||
- `App::configure` take an `FnOnce` instead of `Fn`
|
||||
- Upgrade actix-net crates
|
||||
|
||||
|
||||
## [1.0.0-beta.3] - 2019-05-04
|
||||
|
||||
### Added
|
||||
|
||||
- Add helper function for executing futures `test::block_fn()`
|
||||
|
||||
### Changed
|
||||
|
||||
- Extractor configuration could be registered with `App::data()`
|
||||
or with `Resource::data()` #775
|
||||
|
||||
- Route data is unified with app data, `Route::data()` moved to resource
|
||||
level to `Resource::data()`
|
||||
|
||||
- CORS handling without headers #702
|
||||
|
||||
- Allow constructing `Data` instances to avoid double `Arc` for `Send + Sync` types.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix `NormalizePath` middleware impl #806
|
||||
|
||||
### Deleted
|
||||
|
||||
- `App::data_factory()` is deleted.
|
||||
|
||||
|
||||
## [1.0.0-beta.2] - 2019-04-24
|
||||
|
||||
### Added
|
||||
|
||||
- Add raw services support via `web::service()`
|
||||
|
||||
- Add helper functions for reading response body `test::read_body()`
|
||||
|
||||
- Add support for `remainder match` (i.e "/path/{tail}*")
|
||||
|
||||
- Extend `Responder` trait, allow to override status code and headers.
|
||||
|
||||
- Store visit and login timestamp in the identity cookie #502
|
||||
|
||||
### Changed
|
||||
|
||||
- `.to_async()` handler can return `Responder` type #792
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix async web::Data factory handling
|
||||
|
||||
|
||||
## [1.0.0-beta.1] - 2019-04-20
|
||||
|
||||
### Added
|
||||
|
||||
- Add helper functions for reading test response body,
|
||||
`test::read_response()` and test::read_response_json()`
|
||||
|
||||
- Add `.peer_addr()` #744
|
||||
|
||||
- Add `NormalizePath` middleware
|
||||
|
||||
### Changed
|
||||
|
||||
- Rename `RouterConfig` to `ServiceConfig`
|
||||
|
||||
- Rename `test::call_success` to `test::call_service`
|
||||
|
||||
- Removed `ServiceRequest::from_parts()` as it is unsafe to create from parts.
|
||||
|
||||
- `CookieIdentityPolicy::max_age()` accepts value in seconds
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `TestRequest::app_data()`
|
||||
|
||||
|
||||
## [1.0.0-alpha.6] - 2019-04-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Allow using any service as default service.
|
||||
|
||||
- Remove generic type for request payload, always use default.
|
||||
|
||||
- Removed `Decompress` middleware. Bytes, String, Json, Form extractors
|
||||
automatically decompress payload.
|
||||
|
||||
- Make extractor config type explicit. Add `FromRequest::Config` associated type.
|
||||
|
||||
|
||||
## [1.0.0-alpha.5] - 2019-04-12
|
||||
|
||||
### Added
|
||||
|
||||
- Added async io `TestBuffer` for testing.
|
||||
|
||||
### Deleted
|
||||
|
||||
- Removed native-tls support
|
||||
|
||||
|
||||
## [1.0.0-alpha.4] - 2019-04-08
|
||||
|
||||
### Added
|
||||
|
||||
- `App::configure()` allow to offload app configuration to different methods
|
||||
|
||||
- Added `URLPath` option for logger
|
||||
|
||||
- Added `ServiceRequest::app_data()`, returns `Data<T>`
|
||||
|
||||
- Added `ServiceFromRequest::app_data()`, returns `Data<T>`
|
||||
|
||||
### Changed
|
||||
|
||||
- `FromRequest` trait refactoring
|
||||
|
||||
- Move multipart support to actix-multipart crate
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix body propagation in Response::from_error. #760
|
||||
|
||||
|
||||
## [1.0.0-alpha.3] - 2019-04-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Renamed `TestRequest::to_service()` to `TestRequest::to_srv_request()`
|
||||
|
||||
- Renamed `TestRequest::to_response()` to `TestRequest::to_srv_response()`
|
||||
|
||||
- Removed `Deref` impls
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed unused `actix_web::web::md()`
|
||||
|
||||
|
||||
## [1.0.0-alpha.2] - 2019-03-29
|
||||
|
||||
### Added
|
||||
|
||||
- Rustls support
|
||||
|
||||
### Changed
|
||||
|
||||
- Use forked cookie
|
||||
|
||||
- Multipart::Field renamed to MultipartField
|
||||
|
||||
## [1.0.0-alpha.1] - 2019-03-28
|
||||
|
||||
### Changed
|
||||
|
||||
- Complete architecture re-design.
|
||||
|
||||
- Return 405 response if no matching route found within resource #538
|
||||
Actix Web changelog [is now here →](./actix-web/CHANGES.md).
|
||||
|
150
Cargo.toml
150
Cargo.toml
@ -1,126 +1,18 @@
|
||||
[package]
|
||||
name = "actix-web"
|
||||
version = "4.0.0-beta.17"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
||||
keywords = ["actix", "http", "web", "framework", "async"]
|
||||
categories = [
|
||||
"network-programming",
|
||||
"asynchronous",
|
||||
"web-programming::http-server",
|
||||
"web-programming::websocket"
|
||||
]
|
||||
homepage = "https://actix.rs"
|
||||
repository = "https://github.com/actix/actix-web.git"
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
# features that docs.rs will build with
|
||||
features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lib]
|
||||
name = "actix_web"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
".",
|
||||
"awc",
|
||||
"actix-http",
|
||||
"actix-files",
|
||||
"actix-http-test",
|
||||
"actix-http",
|
||||
"actix-multipart",
|
||||
"actix-router",
|
||||
"actix-test",
|
||||
"actix-web-actors",
|
||||
"actix-web-codegen",
|
||||
"actix-http-test",
|
||||
"actix-test",
|
||||
"actix-router",
|
||||
"actix-web",
|
||||
"awc",
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
|
||||
|
||||
# Brotli algorithm content-encoding support
|
||||
compress-brotli = ["actix-http/compress-brotli", "__compress"]
|
||||
# Gzip and deflate algorithms content-encoding support
|
||||
compress-gzip = ["actix-http/compress-gzip", "__compress"]
|
||||
# Zstd algorithm content-encoding support
|
||||
compress-zstd = ["actix-http/compress-zstd", "__compress"]
|
||||
|
||||
# support for cookies
|
||||
cookies = ["cookie"]
|
||||
|
||||
# secure cookies feature
|
||||
secure-cookies = ["cookie/secure"]
|
||||
|
||||
# openssl
|
||||
openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
|
||||
|
||||
# rustls
|
||||
rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"]
|
||||
|
||||
# Internal (PRIVATE!) features used to aid testing and checking feature status.
|
||||
# Don't rely on these whatsoever. They may disappear at anytime.
|
||||
__compress = []
|
||||
|
||||
# io-uring feature only avaiable for Linux OSes.
|
||||
experimental-io-uring = ["actix-server/io-uring"]
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.4.1"
|
||||
actix-macros = "0.2.3"
|
||||
actix-rt = "2.3"
|
||||
actix-server = "2.0.0-rc.2"
|
||||
actix-service = "2.0.0"
|
||||
actix-utils = "3.0.0"
|
||||
actix-tls = { version = "3.0.0", default-features = false, optional = true }
|
||||
|
||||
actix-http = "3.0.0-beta.17"
|
||||
actix-router = "0.5.0-beta.3"
|
||||
actix-web-codegen = "0.5.0-beta.6"
|
||||
|
||||
ahash = "0.7"
|
||||
bytes = "1"
|
||||
cfg-if = "1"
|
||||
cookie = { version = "0.15", features = ["percent-encode"], optional = true }
|
||||
derive_more = "0.99.5"
|
||||
encoding_rs = "0.8"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
itoa = "1"
|
||||
language-tags = "0.3"
|
||||
once_cell = "1.5"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
paste = "1"
|
||||
pin-project-lite = "0.2.7"
|
||||
regex = "1.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_urlencoded = "0.7"
|
||||
smallvec = "1.6.1"
|
||||
socket2 = "0.4.0"
|
||||
time = { version = "0.3", default-features = false, features = ["formatting"] }
|
||||
url = "2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] }
|
||||
awc = { version = "3.0.0-beta.16", features = ["openssl"] }
|
||||
|
||||
brotli2 = "0.3.2"
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
env_logger = "0.9"
|
||||
flate2 = "1.0.13"
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
|
||||
rand = "0.8"
|
||||
rcgen = "0.8"
|
||||
rustls-pemfile = "0.2"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||
zstd = "0.9"
|
||||
|
||||
[profile.dev]
|
||||
# Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much.
|
||||
debug = 0
|
||||
@ -137,7 +29,7 @@ actix-http-test = { path = "actix-http-test" }
|
||||
actix-multipart = { path = "actix-multipart" }
|
||||
actix-router = { path = "actix-router" }
|
||||
actix-test = { path = "actix-test" }
|
||||
actix-web = { path = "." }
|
||||
actix-web = { path = "actix-web" }
|
||||
actix-web-actors = { path = "actix-web-actors" }
|
||||
actix-web-codegen = { path = "actix-web-codegen" }
|
||||
awc = { path = "awc" }
|
||||
@ -150,31 +42,3 @@ awc = { path = "awc" }
|
||||
# actix-utils = { path = "../actix-net/actix-utils" }
|
||||
# actix-tls = { path = "../actix-net/actix-tls" }
|
||||
# actix-server = { path = "../actix-net/actix-server" }
|
||||
|
||||
[[test]]
|
||||
name = "test_server"
|
||||
required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
|
||||
|
||||
[[example]]
|
||||
name = "basic"
|
||||
required-features = ["compress-gzip"]
|
||||
|
||||
[[example]]
|
||||
name = "uds"
|
||||
required-features = ["compress-gzip"]
|
||||
|
||||
[[example]]
|
||||
name = "on_connect"
|
||||
required-features = []
|
||||
|
||||
[[bench]]
|
||||
name = "server"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "service"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "responder"
|
||||
harness = false
|
||||
|
677
MIGRATION.md
677
MIGRATION.md
@ -1,677 +0,0 @@
|
||||
## Unreleased
|
||||
|
||||
- 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
|
||||
routes defined with trailing slashes will become inaccessible when
|
||||
using `NormalizePath::default()`. As such, calling `NormalizePath::default()` will log a warning.
|
||||
It is advised that the `new` method be used instead.
|
||||
|
||||
Before: `#[get("/test/")]`
|
||||
After: `#[get("/test")]`
|
||||
|
||||
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
|
||||
|
||||
- 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.
|
||||
To select algorithm you want to include with `middleware::Compress` use following flags:
|
||||
- `compress-brotli`
|
||||
- `compress-gzip`
|
||||
- `compress-zstd`
|
||||
If you have set in your `Cargo.toml` dedicated `actix-web` features and you still want
|
||||
to have compression enabled. Please change features selection like bellow:
|
||||
|
||||
Before: `"compress"`
|
||||
After: `"compress-brotli", "compress-gzip", "compress-zstd"`
|
||||
|
||||
|
||||
## 3.0.0
|
||||
|
||||
- The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to
|
||||
simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
|
||||
|
||||
- Cookie handling has been offloaded to the `cookie` crate:
|
||||
* `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs.
|
||||
* Some types now require lifetime parameters.
|
||||
|
||||
- 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.
|
||||
|
||||
- Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now
|
||||
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.
|
||||
|
||||
- actix-http support for Actors messages was moved to actix-http crate and is enabled
|
||||
with feature `actors`
|
||||
|
||||
- content_length function is removed from actix-http.
|
||||
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
|
||||
`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
|
||||
destructuring or `.into_inner()`. For example:
|
||||
|
||||
```rust
|
||||
// Previously:
|
||||
async fn some_route(path: web::Path<(String, String)>) -> String {
|
||||
format!("Hello, {} {}", path.0, path.1)
|
||||
}
|
||||
|
||||
// Now (this also worked before):
|
||||
async fn some_route(path: web::Path<(String, String)>) -> String {
|
||||
let (first_name, last_name) = path.into_inner();
|
||||
format!("Hello, {} {}", first_name, last_name)
|
||||
}
|
||||
// Or (this wasn't previously supported):
|
||||
async fn some_route(web::Path((first_name, last_name)): web::Path<(String, String)>) -> String {
|
||||
format!("Hello, {} {}", first_name, last_name)
|
||||
}
|
||||
```
|
||||
|
||||
- `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(...)`,
|
||||
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::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`.
|
||||
|
||||
|
||||
## 2.0.0
|
||||
|
||||
- `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to
|
||||
`.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`.
|
||||
Stored data is available via `HttpRequest::app_data()` method at runtime.
|
||||
|
||||
- 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()`
|
||||
replace `fn` with `async fn` to convert sync handler to async
|
||||
|
||||
- `actix_http_test::TestServer` moved to `actix_web::test` module. To start
|
||||
test server use `test::start()` or `test_start_with_config()` methods
|
||||
|
||||
- `ResponseError` trait has been reafctored. `ResponseError::error_response()` renders
|
||||
http response.
|
||||
|
||||
- Feature `rust-tls` renamed to `rustls`
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
actix-web = { version = "2.0.0", features = ["rust-tls"] }
|
||||
```
|
||||
|
||||
use
|
||||
|
||||
```rust
|
||||
actix-web = { version = "2.0.0", features = ["rustls"] }
|
||||
```
|
||||
|
||||
- Feature `ssl` renamed to `openssl`
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
actix-web = { version = "2.0.0", features = ["ssl"] }
|
||||
```
|
||||
|
||||
use
|
||||
|
||||
```rust
|
||||
actix-web = { version = "2.0.0", features = ["openssl"] }
|
||||
```
|
||||
- `Cors` builder now requires that you call `.finish()` to construct the middleware
|
||||
|
||||
## 1.0.1
|
||||
|
||||
- Cors middleware has been moved to `actix-cors` crate
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
use actix_web::middleware::cors::Cors;
|
||||
```
|
||||
|
||||
use
|
||||
|
||||
```rust
|
||||
use actix_cors::Cors;
|
||||
```
|
||||
|
||||
- Identity middleware has been moved to `actix-identity` crate
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
use actix_web::middleware::identity::{Identity, CookieIdentityPolicy, IdentityService};
|
||||
```
|
||||
|
||||
use
|
||||
|
||||
```rust
|
||||
use actix_identity::{Identity, CookieIdentityPolicy, IdentityService};
|
||||
```
|
||||
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Extractor configuration. In version 1.0 this is handled with the new `Data` mechanism for both setting and retrieving the configuration
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
|
||||
#[derive(Default)]
|
||||
struct ExtractorConfig {
|
||||
config: String,
|
||||
}
|
||||
|
||||
impl FromRequest for YourExtractor {
|
||||
type Config = ExtractorConfig;
|
||||
type Result = Result<YourExtractor, Error>;
|
||||
|
||||
fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result {
|
||||
println!("use the config: {:?}", cfg.config);
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
App::new().resource("/route_with_config", |r| {
|
||||
r.post().with_config(handler_fn, |cfg| {
|
||||
cfg.0.config = "test".to_string();
|
||||
})
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
use the HttpRequest to get the configuration like any other `Data` with `req.app_data::<C>()` and set it with the `data()` method on the `resource`
|
||||
|
||||
```rust
|
||||
#[derive(Default)]
|
||||
struct ExtractorConfig {
|
||||
config: String,
|
||||
}
|
||||
|
||||
impl FromRequest for YourExtractor {
|
||||
type Error = Error;
|
||||
type Future = Result<Self, Self::Error>;
|
||||
type Config = ExtractorConfig;
|
||||
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
let cfg = req.app_data::<ExtractorConfig>();
|
||||
println!("config data?: {:?}", cfg.unwrap().role);
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
App::new().service(
|
||||
resource("/route_with_config")
|
||||
.data(ExtractorConfig {
|
||||
config: "test".to_string(),
|
||||
})
|
||||
.route(post().to(handler_fn)),
|
||||
)
|
||||
```
|
||||
|
||||
- Resource registration. 1.0 version uses generalized resource
|
||||
registration via `.service()` method.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
App.new().resource("/welcome", |r| r.f(welcome))
|
||||
```
|
||||
|
||||
use App's or Scope's `.service()` method. `.service()` method accepts
|
||||
object that implements `HttpServiceFactory` trait. By default
|
||||
actix-web provides `Resource` and `Scope` services.
|
||||
|
||||
```rust
|
||||
App.new().service(
|
||||
web::resource("/welcome")
|
||||
.route(web::get().to(welcome))
|
||||
.route(web::post().to(post_handler))
|
||||
```
|
||||
|
||||
- Scope registration.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
let app = App::new().scope("/{project_id}", |scope| {
|
||||
scope
|
||||
.resource("/path1", |r| r.f(|_| HttpResponse::Ok()))
|
||||
.resource("/path2", |r| r.f(|_| HttpResponse::Ok()))
|
||||
.resource("/path3", |r| r.f(|_| HttpResponse::MethodNotAllowed()))
|
||||
});
|
||||
```
|
||||
|
||||
use `.service()` for registration and `web::scope()` as scope object factory.
|
||||
|
||||
```rust
|
||||
let app = App::new().service(
|
||||
web::scope("/{project_id}")
|
||||
.service(web::resource("/path1").to(|| HttpResponse::Ok()))
|
||||
.service(web::resource("/path2").to(|| HttpResponse::Ok()))
|
||||
.service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed()))
|
||||
);
|
||||
```
|
||||
|
||||
- `.with()`, `.with_async()` registration methods have been renamed to `.to()` and `.to_async()`.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
App.new().resource("/welcome", |r| r.with(welcome))
|
||||
```
|
||||
|
||||
use `.to()` or `.to_async()` methods
|
||||
|
||||
```rust
|
||||
App.new().service(web::resource("/welcome").to(welcome))
|
||||
```
|
||||
|
||||
- Passing arguments to handler with extractors, multiple arguments are allowed
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
fn welcome((body, req): (Bytes, HttpRequest)) -> ... {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
use multiple arguments
|
||||
|
||||
```rust
|
||||
fn welcome(body: Bytes, req: HttpRequest) -> ... {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
- `.f()`, `.a()` and `.h()` handler registration methods have been removed.
|
||||
Use `.to()` for handlers and `.to_async()` for async handlers. Handler function
|
||||
must use extractors.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
App.new().resource("/welcome", |r| r.f(welcome))
|
||||
```
|
||||
|
||||
use App's `to()` or `to_async()` methods
|
||||
|
||||
```rust
|
||||
App.new().service(web::resource("/welcome").to(welcome))
|
||||
```
|
||||
|
||||
- `HttpRequest` does not provide access to request's payload stream.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
fn index(req: &HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
req
|
||||
.payload()
|
||||
.from_err()
|
||||
.fold((), |_, chunk| {
|
||||
...
|
||||
})
|
||||
.map(|_| HttpResponse::Ok().finish())
|
||||
.responder()
|
||||
}
|
||||
```
|
||||
|
||||
use `Payload` extractor
|
||||
|
||||
```rust
|
||||
fn index(stream: web::Payload) -> impl Future<Item=HttpResponse, Error=Error> {
|
||||
stream
|
||||
.from_err()
|
||||
.fold((), |_, chunk| {
|
||||
...
|
||||
})
|
||||
.map(|_| HttpResponse::Ok().finish())
|
||||
}
|
||||
```
|
||||
|
||||
- `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
|
||||
HttpRequest's api.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
App.with_state(T)
|
||||
```
|
||||
|
||||
use App's `data` method
|
||||
|
||||
```rust
|
||||
App.new()
|
||||
.data(T)
|
||||
```
|
||||
|
||||
and either use the Data extractor within your handler
|
||||
|
||||
```rust
|
||||
use actix_web::web::Data;
|
||||
|
||||
fn endpoint_handler(Data<T>)){
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
.. or access your Data element from the HttpRequest
|
||||
|
||||
```rust
|
||||
fn endpoint_handler(req: HttpRequest) {
|
||||
let data: Option<Data<T>> = req.app_data::<T>();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
- AsyncResponder is removed, use `.to_async()` registration method and `impl Future<>` as result type.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
use actix_web::AsyncResponder;
|
||||
|
||||
fn endpoint_handler(...) -> impl Future<Item=HttpResponse, Error=Error>{
|
||||
...
|
||||
.responder()
|
||||
}
|
||||
```
|
||||
|
||||
.. simply omit AsyncResponder and the corresponding responder() finish method
|
||||
|
||||
|
||||
- Middleware
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
let app = App::new()
|
||||
.middleware(middleware::Logger::default())
|
||||
```
|
||||
|
||||
use `.wrap()` method
|
||||
|
||||
```rust
|
||||
let app = App::new()
|
||||
.wrap(middleware::Logger::default())
|
||||
.route("/index.html", web::get().to(index));
|
||||
```
|
||||
|
||||
- `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()`
|
||||
method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
fn index(req: &HttpRequest) -> Responder {
|
||||
req.body()
|
||||
.and_then(|body| {
|
||||
...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
use
|
||||
|
||||
```rust
|
||||
fn index(body: Bytes) -> Responder {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
- `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.
|
||||
|
||||
instead of `use actix_web::fs::StaticFile`
|
||||
|
||||
use `use actix_files::Files`
|
||||
|
||||
instead of `use actix_web::fs::Namedfile`
|
||||
|
||||
use `use actix_files::NamedFile`
|
||||
|
||||
- Multipart has been moved to a separate crate.
|
||||
|
||||
instead of `use actix_web::multipart::Multipart`
|
||||
|
||||
use `use actix_multipart::Multipart`
|
||||
|
||||
- Response compression is not enabled by default.
|
||||
To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`.
|
||||
|
||||
- Session middleware moved to actix-session crate
|
||||
|
||||
- Actors support have been moved to `actix-web-actors` crate
|
||||
|
||||
- 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.
|
||||
|
||||
Simplest migration from 0.7 to 1.0 shall include below method to the custom implementation of ResponseError:
|
||||
|
||||
```rust
|
||||
fn render_response(&self) -> HttpResponse {
|
||||
self.error_response()
|
||||
}
|
||||
```
|
||||
|
||||
## 0.7.15
|
||||
|
||||
- The `' '` character is not percent decoded anymore before matching routes. If you need to use it in
|
||||
your routes, you should use `%20`.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let app = App::new().resource("/my index", |r| {
|
||||
r.method(http::Method::GET)
|
||||
.with(index);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
use
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let app = App::new().resource("/my%20index", |r| {
|
||||
r.method(http::Method::GET)
|
||||
.with(index);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- If you used `AsyncResult::async` you need to replace it with `AsyncResult::future`
|
||||
|
||||
|
||||
## 0.7.4
|
||||
|
||||
- `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple
|
||||
even for handler with one parameter.
|
||||
|
||||
|
||||
## 0.7
|
||||
|
||||
- `HttpRequest` does not implement `Stream` anymore. If you need to read request payload
|
||||
use `HttpMessage::payload()` method.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
fn index(req: HttpRequest) -> impl Responder {
|
||||
req
|
||||
.from_err()
|
||||
.fold(...)
|
||||
....
|
||||
}
|
||||
```
|
||||
|
||||
use `.payload()`
|
||||
|
||||
```rust
|
||||
fn index(req: HttpRequest) -> impl Responder {
|
||||
req
|
||||
.payload() // <- get request payload stream
|
||||
.from_err()
|
||||
.fold(...)
|
||||
....
|
||||
}
|
||||
```
|
||||
|
||||
- [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html)
|
||||
trait uses `&HttpRequest` instead of `&mut HttpRequest`.
|
||||
|
||||
- Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
fn index(query: Query<..>, info: Json<MyStruct) -> impl Responder {}
|
||||
```
|
||||
|
||||
use tuple of extractors and use `.with()` for registration:
|
||||
|
||||
```rust
|
||||
fn index((query, json): (Query<..>, Json<MyStruct)) -> impl Responder {}
|
||||
```
|
||||
|
||||
- `Handler::handle()` uses `&self` instead of `&mut self`
|
||||
|
||||
- `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value
|
||||
|
||||
- Removed deprecated `HttpServer::threads()`, use
|
||||
[HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead.
|
||||
|
||||
- Renamed `client::ClientConnectorError::Connector` to
|
||||
`client::ClientConnectorError::Resolver`
|
||||
|
||||
- `Route::with()` does not return `ExtractorConfig`, to configure
|
||||
extractor use `Route::with_config()`
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let app = App::new().resource("/index.html", |r| {
|
||||
r.method(http::Method::GET)
|
||||
.with(index)
|
||||
.limit(4096); // <- limit size of the payload
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
use
|
||||
|
||||
```rust
|
||||
|
||||
fn main() {
|
||||
let app = App::new().resource("/index.html", |r| {
|
||||
r.method(http::Method::GET)
|
||||
.with_config(index, |cfg| { // <- register handler
|
||||
cfg.limit(4096); // <- limit size of the payload
|
||||
})
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- `Route::with_async()` does not return `ExtractorConfig`, to configure
|
||||
extractor use `Route::with_async_config()`
|
||||
|
||||
|
||||
## 0.6
|
||||
|
||||
- `Path<T>` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest`
|
||||
|
||||
- `ws::Message::Close` now includes optional close reason.
|
||||
`ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed.
|
||||
|
||||
- `HttpServer::threads()` renamed to `HttpServer::workers()`.
|
||||
|
||||
- `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated.
|
||||
Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead.
|
||||
|
||||
- `HttpRequest::extensions()` returns read only reference to the request's Extension
|
||||
`HttpRequest::extensions_mut()` returns mutable reference.
|
||||
|
||||
- Instead of
|
||||
|
||||
`use actix_web::middleware::{
|
||||
CookieSessionBackend, CookieSessionError, RequestSession,
|
||||
Session, SessionBackend, SessionImpl, SessionStorage};`
|
||||
|
||||
use `actix_web::middleware::session`
|
||||
|
||||
`use actix_web::middleware::session{CookieSessionBackend, CookieSessionError,
|
||||
RequestSession, Session, SessionBackend, SessionImpl, SessionStorage};`
|
||||
|
||||
- `FromRequest::from_request()` accepts mutable reference to a request
|
||||
|
||||
- `FromRequest::Result` has to implement `Into<Reply<Self>>`
|
||||
|
||||
- [`Responder::respond_to()`](
|
||||
https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to)
|
||||
is generic over `S`
|
||||
|
||||
- Use `Query` extractor instead of HttpRequest::query()`.
|
||||
|
||||
```rust
|
||||
fn index(q: Query<HashMap<String, String>>) -> Result<..> {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```rust
|
||||
let q = Query::<HashMap<String, String>>::extract(req);
|
||||
```
|
||||
|
||||
- Websocket operations are implemented as `WsWriter` trait.
|
||||
you need to use `use actix_web::ws::WsWriter`
|
||||
|
||||
|
||||
## 0.5
|
||||
|
||||
- `HttpResponseBuilder::body()`, `.finish()`, `.json()`
|
||||
methods return `HttpResponse` instead of `Result<HttpResponse>`
|
||||
|
||||
- `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version`
|
||||
moved to `actix_web::http` module
|
||||
|
||||
- `actix_web::header` moved to `actix_web::http::header`
|
||||
|
||||
- `NormalizePath` moved to `actix_web::http` module
|
||||
|
||||
- `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function,
|
||||
shortcut for `actix_web::server::HttpServer::new()`
|
||||
|
||||
- `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.
|
||||
|
||||
- `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type
|
||||
|
||||
- `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()`
|
||||
functions should be used instead
|
||||
|
||||
- `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>`
|
||||
instead of `Result<_, http::Error>`
|
||||
|
||||
- `Application` renamed to a `App`
|
||||
|
||||
- `actix_web::Reply`, `actix_web::Resource` moved to `actix_web::dev`
|
109
README.md
109
README.md
@ -1,109 +0,0 @@
|
||||
<div align="center">
|
||||
<h1>Actix Web</h1>
|
||||
<p>
|
||||
<strong>Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust</strong>
|
||||
</p>
|
||||
<p>
|
||||
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web/4.0.0-beta.17)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||

|
||||
[](https://deps.rs/crate/actix-web/4.0.0-beta.17)
|
||||
<br />
|
||||
[](https://github.com/actix/actix-web/actions)
|
||||
[](https://codecov.io/gh/actix/actix-web)
|
||||

|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## Features
|
||||
|
||||
- Supports *HTTP/1.x* and *HTTP/2*
|
||||
- Streaming and pipelining
|
||||
- Keep-alive and slow requests handling
|
||||
- Client/server [WebSockets](https://actix.rs/docs/websockets/) support
|
||||
- Transparent content compression/decompression (br, gzip, deflate, zstd)
|
||||
- Powerful [request routing](https://actix.rs/docs/url-dispatch/)
|
||||
- Multipart streams
|
||||
- Static assets
|
||||
- SSL support using OpenSSL or Rustls
|
||||
- Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
|
||||
- Includes an async [HTTP client](https://docs.rs/awc/)
|
||||
- Runs on stable Rust 1.52+
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Website & User Guide](https://actix.rs)
|
||||
- [Examples Repository](https://github.com/actix/examples)
|
||||
- [API Documentation](https://docs.rs/actix-web)
|
||||
- [API Documentation (master branch)](https://actix.rs/actix-web/actix_web)
|
||||
|
||||
## Example
|
||||
|
||||
Dependencies:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
actix-web = "3"
|
||||
```
|
||||
|
||||
Code:
|
||||
|
||||
```rust
|
||||
use actix_web::{get, web, App, HttpServer, Responder};
|
||||
|
||||
#[get("/{id}/{name}/index.html")]
|
||||
async fn index(web::Path((id, name)): web::Path<(u32, String)>) -> impl Responder {
|
||||
format!("Hello {}! id:{}", name, id)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
HttpServer::new(|| App::new().service(index))
|
||||
.bind("127.0.0.1:8080")?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
```
|
||||
|
||||
### More examples
|
||||
|
||||
- [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics/)
|
||||
- [Application State](https://github.com/actix/examples/tree/master/basics/state/)
|
||||
- [JSON Handling](https://github.com/actix/examples/tree/master/json/json/)
|
||||
- [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart/)
|
||||
- [Diesel Integration](https://github.com/actix/examples/tree/master/database_interactions/diesel/)
|
||||
- [r2d2 Integration](https://github.com/actix/examples/tree/master/database_interactions/r2d2/)
|
||||
- [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets/websocket/)
|
||||
- [Tera Templates](https://github.com/actix/examples/tree/master/template_engines/tera/)
|
||||
- [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 OpenSSL](https://github.com/actix/examples/tree/master/security/openssl/)
|
||||
- [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat/)
|
||||
|
||||
You may consider checking out
|
||||
[this directory](https://github.com/actix/examples/tree/master/) for more examples.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
One of the fastest web frameworks available according to the
|
||||
[TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite).
|
||||
|
||||
## License
|
||||
|
||||
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])
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
||||
[http://opensource.org/licenses/MIT])
|
||||
|
||||
at your option.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Contribution to the actix-web repo is organized under the terms of the Contributor Covenant.
|
||||
The Actix team promises to intervene to uphold that code of conduct.
|
@ -1,14 +1,58 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
## Unreleased - 2022-xx-xx
|
||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||
|
||||
|
||||
## 0.6.2 - 2022-07-23
|
||||
- Allow partial range responses for video content to start streaming sooner. [#2817]
|
||||
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||
|
||||
[#2817]: https://github.com/actix/actix-web/pull/2817
|
||||
|
||||
|
||||
## 0.6.1 - 2022-06-11
|
||||
- Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021]
|
||||
- Update `tokio-uring` dependency to `0.3`.
|
||||
- Audio files now use `Content-Disposition: inline` instead of `attachment`. [#2645]
|
||||
- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency.
|
||||
|
||||
[#2021]: https://github.com/actix/actix-web/pull/2021
|
||||
[#2645]: https://github.com/actix/actix-web/pull/2645
|
||||
|
||||
|
||||
## 0.6.0 - 2022-02-25
|
||||
- No significant changes since `0.6.0-beta.16`.
|
||||
|
||||
|
||||
## 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`.
|
||||
- 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`.
|
||||
- No significant changes since `0.6.0-beta.10`.
|
||||
|
||||
|
||||
## 0.6.0-beta.10 - 2021-12-11
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-files"
|
||||
version = "0.6.0-beta.12"
|
||||
version = "0.6.2"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"fakeshadow <24548779@qq.com>",
|
||||
@ -22,12 +22,11 @@ path = "src/lib.rs"
|
||||
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
|
||||
|
||||
[dependencies]
|
||||
actix-http = "3.0.0-beta.17"
|
||||
actix-http = "3"
|
||||
actix-service = "2"
|
||||
actix-utils = "3"
|
||||
actix-web = { version = "4.0.0-beta.17", default-features = false }
|
||||
actix-web = { version = "4", default-features = false }
|
||||
|
||||
askama_escape = "0.10"
|
||||
bitflags = "1"
|
||||
bytes = "1"
|
||||
derive_more = "0.99.5"
|
||||
@ -38,10 +37,15 @@ mime = "0.3"
|
||||
mime_guess = "2.0.1"
|
||||
percent-encoding = "2.1"
|
||||
pin-project-lite = "0.2.7"
|
||||
v_htmlescape= "0.15"
|
||||
|
||||
tokio-uring = { version = "0.1", optional = true }
|
||||
# experimental-io-uring
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
tokio-uring = { version = "0.3", optional = true, features = ["bytes"] }
|
||||
actix-server = { version = "2.1", optional = true } # ensure matching tokio-uring versions
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-test = "0.1.0-beta.10"
|
||||
actix-web = "4.0.0-beta.17"
|
||||
actix-rt = "2.7"
|
||||
actix-test = "0.1"
|
||||
actix-web = "4"
|
||||
tempfile = "3.2"
|
||||
|
@ -3,16 +3,16 @@
|
||||
> Static file serving for Actix Web
|
||||
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://docs.rs/actix-files/0.6.0-beta.12)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||
[](https://docs.rs/actix-files/0.6.2)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.6.0-beta.12)
|
||||
[](https://deps.rs/crate/actix-files/0.6.2)
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-files/)
|
||||
- [Example Project](https://github.com/actix/examples/tree/master/basics/static_index)
|
||||
- Minimum Supported Rust Version (MSRV): 1.52
|
||||
- [API Documentation](https://docs.rs/actix-files)
|
||||
- [Example Project](https://github.com/actix/examples/tree/master/basics/static-files)
|
||||
- Minimum Supported Rust Version (MSRV): 1.54
|
||||
|
@ -10,6 +10,9 @@ use actix_web::{error::Error, web::Bytes};
|
||||
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! {
|
||||
@ -78,7 +81,7 @@ async fn chunked_read_file_callback(
|
||||
) -> Result<(File, Bytes), Error> {
|
||||
use io::{Read as _, Seek as _};
|
||||
|
||||
let res = actix_web::rt::task::spawn_blocking(move || {
|
||||
let res = actix_web::web::block(move || {
|
||||
let mut buf = Vec::with_capacity(max_bytes);
|
||||
|
||||
file.seek(io::SeekFrom::Start(offset))?;
|
||||
@ -91,8 +94,7 @@ async fn chunked_read_file_callback(
|
||||
Ok((file, Bytes::from(buf)))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| actix_web::error::BlockingError)??;
|
||||
.await??;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
@ -214,64 +216,3 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
use bytes_mut::BytesMut;
|
||||
|
||||
// TODO: remove new type and use bytes::BytesMut directly
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
mod bytes_mut {
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use tokio_uring::buf::{IoBuf, IoBufMut};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BytesMut(bytes::BytesMut);
|
||||
|
||||
impl BytesMut {
|
||||
pub(super) fn new() -> Self {
|
||||
Self(bytes::BytesMut::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for BytesMut {
|
||||
type Target = bytes::BytesMut;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for BytesMut {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl IoBuf for BytesMut {
|
||||
fn stable_ptr(&self) -> *const u8 {
|
||||
self.0.as_ptr()
|
||||
}
|
||||
|
||||
fn bytes_init(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
fn bytes_total(&self) -> usize {
|
||||
self.0.capacity()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl IoBufMut for BytesMut {
|
||||
fn stable_mut_ptr(&mut self) -> *mut u8 {
|
||||
self.0.as_mut_ptr()
|
||||
}
|
||||
|
||||
unsafe fn set_init(&mut self, init_len: usize) {
|
||||
if self.len() < init_len {
|
||||
self.0.set_len(init_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf};
|
||||
|
||||
use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
|
||||
use askama_escape::{escape as escape_html_entity, Html};
|
||||
use percent_encoding::{utf8_percent_encode, CONTROLS};
|
||||
use v_htmlescape::escape as escape_html_entity;
|
||||
|
||||
/// A directory; responds with the generated directory listing.
|
||||
#[derive(Debug)]
|
||||
@ -40,17 +40,26 @@ impl Directory {
|
||||
pub(crate) type DirectoryRenderer =
|
||||
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 {
|
||||
($path:ident) => {
|
||||
utf8_percent_encode(&$path, CONTROLS)
|
||||
};
|
||||
}
|
||||
|
||||
// " -- " & -- & ' -- ' < -- < > -- > / -- /
|
||||
/// Returns HTML entity encoded formatter.
|
||||
///
|
||||
/// ```plain
|
||||
/// " => "
|
||||
/// & => &
|
||||
/// ' => '
|
||||
/// < => <
|
||||
/// > => >
|
||||
/// / => /
|
||||
/// ```
|
||||
macro_rules! encode_file_name {
|
||||
($entry:ident) => {
|
||||
escape_html_entity(&$entry.file_name().to_string_lossy(), Html)
|
||||
escape_html_entity(&$entry.file_name().to_string_lossy())
|
||||
};
|
||||
}
|
||||
|
||||
@ -66,7 +75,7 @@ pub(crate) fn directory_listing(
|
||||
if dir.is_visible(&entry) {
|
||||
let entry = entry.unwrap();
|
||||
let p = match entry.path().strip_prefix(&dir.path) {
|
||||
Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace("\\", "/"),
|
||||
Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace('\\', "/"),
|
||||
Ok(p) => base.join(p).to_string_lossy().into_owned(),
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ use actix_web::{http::StatusCode, ResponseError};
|
||||
use derive_more::Display;
|
||||
|
||||
/// Errors which can occur when serving static files.
|
||||
#[derive(Display, Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Eq, Display)]
|
||||
pub enum FilesError {
|
||||
/// Path is not a directory
|
||||
#[allow(dead_code)]
|
||||
@ -22,17 +22,24 @@ impl ResponseError for FilesError {
|
||||
}
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Display, Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Eq, Display)]
|
||||
#[non_exhaustive]
|
||||
pub enum UriSegmentError {
|
||||
/// The segment started with the wrapped invalid character.
|
||||
#[display(fmt = "The segment started with the wrapped invalid character")]
|
||||
BadStart(char),
|
||||
|
||||
/// The segment contained the wrapped invalid character.
|
||||
#[display(fmt = "The segment contained the wrapped invalid character")]
|
||||
BadChar(char),
|
||||
|
||||
/// The segment ended with the wrapped invalid character.
|
||||
#[display(fmt = "The segment ended with the wrapped invalid character")]
|
||||
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`
|
||||
|
@ -28,6 +28,7 @@ use crate::{
|
||||
///
|
||||
/// `Files` service must be registered with `App::service()` method.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::App;
|
||||
/// use actix_files::Files;
|
||||
@ -36,7 +37,7 @@ use crate::{
|
||||
/// .service(Files::new("/static", "."));
|
||||
/// ```
|
||||
pub struct Files {
|
||||
path: String,
|
||||
mount_path: String,
|
||||
directory: PathBuf,
|
||||
index: Option<String>,
|
||||
show_index: bool,
|
||||
@ -67,7 +68,7 @@ impl Clone for Files {
|
||||
default: self.default.clone(),
|
||||
renderer: self.renderer.clone(),
|
||||
file_flags: self.file_flags,
|
||||
path: self.path.clone(),
|
||||
mount_path: self.mount_path.clone(),
|
||||
mime_override: self.mime_override.clone(),
|
||||
path_filter: self.path_filter.clone(),
|
||||
use_guards: self.use_guards.clone(),
|
||||
@ -106,7 +107,7 @@ impl Files {
|
||||
};
|
||||
|
||||
Files {
|
||||
path: mount_path.trim_end_matches('/').to_owned(),
|
||||
mount_path: mount_path.trim_end_matches('/').to_owned(),
|
||||
directory: dir,
|
||||
index: None,
|
||||
show_index: false,
|
||||
@ -341,9 +342,9 @@ impl HttpServiceFactory for Files {
|
||||
}
|
||||
|
||||
let rdef = if config.is_root() {
|
||||
ResourceDef::root_prefix(&self.path)
|
||||
ResourceDef::root_prefix(&self.mount_path)
|
||||
} else {
|
||||
ResourceDef::prefix(&self.path)
|
||||
ResourceDef::prefix(&self.mount_path)
|
||||
};
|
||||
|
||||
config.register_service(rdef, guards, self, None)
|
||||
|
@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! Provides a non-blocking service for serving static files from disk.
|
||||
//!
|
||||
//! # Example
|
||||
//! # Examples
|
||||
//! ```
|
||||
//! use actix_web::App;
|
||||
//! use actix_files::Files;
|
||||
@ -67,8 +67,8 @@ mod tests {
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use actix_service::ServiceFactory;
|
||||
use actix_web::{
|
||||
dev::ServiceFactory,
|
||||
guard,
|
||||
http::{
|
||||
header::{self, ContentDisposition, DispositionParam, DispositionType},
|
||||
@ -106,7 +106,7 @@ mod tests {
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::IF_MODIFIED_SINCE, since))
|
||||
.to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
|
||||
}
|
||||
|
||||
@ -118,7 +118,7 @@ mod tests {
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::IF_MODIFIED_SINCE, since))
|
||||
.to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
|
||||
}
|
||||
|
||||
@ -131,7 +131,7 @@ mod tests {
|
||||
.insert_header((header::IF_NONE_MATCH, "miss_etag"))
|
||||
.insert_header((header::IF_MODIFIED_SINCE, since))
|
||||
.to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_ne!(resp.status(), StatusCode::NOT_MODIFIED);
|
||||
}
|
||||
|
||||
@ -143,7 +143,7 @@ mod tests {
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::IF_UNMODIFIED_SINCE, since))
|
||||
.to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
@ -155,7 +155,7 @@ mod tests {
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::IF_UNMODIFIED_SINCE, since))
|
||||
.to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
@ -172,7 +172,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"text/x-toml"
|
||||
@ -196,7 +196,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||
"inline; filename=\"Cargo.toml\""
|
||||
@ -207,7 +207,7 @@ mod tests {
|
||||
.unwrap()
|
||||
.disable_content_disposition();
|
||||
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());
|
||||
}
|
||||
|
||||
@ -235,7 +235,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"text/x-toml"
|
||||
@ -261,7 +261,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"text/xml"
|
||||
@ -284,7 +284,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"image/png"
|
||||
@ -300,10 +300,10 @@ mod tests {
|
||||
let file = NamedFile::open_async("tests/test.js").await.unwrap();
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"application/javascript"
|
||||
"application/javascript; charset=utf-8"
|
||||
);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||
@ -330,7 +330,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"image/png"
|
||||
@ -353,7 +353,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"application/octet-stream"
|
||||
@ -364,22 +364,45 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_status_code_text() {
|
||||
let mut file = NamedFile::open_async("Cargo.toml")
|
||||
async fn status_code_customize_same_output() {
|
||||
let file1 = NamedFile::open_async("Cargo.toml")
|
||||
.await
|
||||
.unwrap()
|
||||
.set_status_code(StatusCode::NOT_FOUND);
|
||||
|
||||
let file2 = NamedFile::open_async("Cargo.toml")
|
||||
.await
|
||||
.unwrap()
|
||||
.customize()
|
||||
.with_status(StatusCode::NOT_FOUND);
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let res1 = file1.respond_to(&req);
|
||||
let res2 = file2.respond_to(&req);
|
||||
|
||||
assert_eq!(res1.status(), StatusCode::NOT_FOUND);
|
||||
assert_eq!(res2.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_status_code_text() {
|
||||
let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||
|
||||
{
|
||||
file.file();
|
||||
let _f: &File = &file;
|
||||
}
|
||||
|
||||
{
|
||||
let _f: &mut File = &mut file;
|
||||
}
|
||||
|
||||
let file = file.customize().with_status(StatusCode::NOT_FOUND);
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
let resp = file.respond_to(&req);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"text/x-toml"
|
||||
@ -597,7 +620,8 @@ mod tests {
|
||||
.to_request();
|
||||
let res = test::call_service(&srv, request).await;
|
||||
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]
|
||||
@ -632,7 +656,7 @@ mod tests {
|
||||
async fn test_named_file_allowed_method() {
|
||||
let req = TestRequest::default().method(Method::GET).to_http_request();
|
||||
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);
|
||||
}
|
||||
|
||||
@ -802,6 +826,38 @@ mod tests {
|
||||
let req = TestRequest::get().uri("/test/%43argo.toml").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
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]
|
||||
|
@ -1,22 +1,20 @@
|
||||
use std::{
|
||||
fmt,
|
||||
fs::Metadata,
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use actix_service::{Service, ServiceFactory};
|
||||
use actix_web::{
|
||||
body::{self, BoxBody, SizedStream},
|
||||
dev::{
|
||||
AppService, BodyEncoding, HttpServiceFactory, ResourceDef, ServiceRequest,
|
||||
ServiceResponse,
|
||||
self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory,
|
||||
ServiceRequest, ServiceResponse,
|
||||
},
|
||||
http::{
|
||||
header::{
|
||||
self, Charset, ContentDisposition, ContentEncoding, DispositionParam,
|
||||
DispositionType, ExtendedValue,
|
||||
DispositionType, ExtendedValue, HeaderValue,
|
||||
},
|
||||
StatusCode,
|
||||
},
|
||||
@ -25,6 +23,7 @@ use actix_web::{
|
||||
use bitflags::bitflags;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use mime::Mime;
|
||||
use mime_guess::from_path;
|
||||
|
||||
use crate::{encoding::equiv_utf8_text, range::HttpRange};
|
||||
@ -40,7 +39,7 @@ bitflags! {
|
||||
|
||||
impl Default for Flags {
|
||||
fn default() -> Self {
|
||||
Flags::from_bits_truncate(0b0000_0111)
|
||||
Flags::from_bits_truncate(0b0000_1111)
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,47 +67,21 @@ impl Default for Flags {
|
||||
/// NamedFile::open_async("./static/index.html").await
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Deref, DerefMut)]
|
||||
#[derive(Debug, Deref, DerefMut)]
|
||||
pub struct NamedFile {
|
||||
path: PathBuf,
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
file: File,
|
||||
path: PathBuf,
|
||||
modified: Option<SystemTime>,
|
||||
pub(crate) md: Metadata,
|
||||
pub(crate) flags: Flags,
|
||||
pub(crate) status_code: StatusCode,
|
||||
pub(crate) content_type: mime::Mime,
|
||||
pub(crate) content_disposition: header::ContentDisposition,
|
||||
pub(crate) content_type: Mime,
|
||||
pub(crate) content_disposition: ContentDisposition,
|
||||
pub(crate) encoding: Option<ContentEncoding>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for NamedFile {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("NamedFile")
|
||||
.field("path", &self.path)
|
||||
.field(
|
||||
"file",
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
{
|
||||
&"tokio_uring::File"
|
||||
},
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
{
|
||||
&self.file
|
||||
},
|
||||
)
|
||||
.field("modified", &self.modified)
|
||||
.field("md", &self.md)
|
||||
.field("flags", &self.flags)
|
||||
.field("status_code", &self.status_code)
|
||||
.field("content_type", &self.content_type)
|
||||
.field("content_disposition", &self.content_disposition)
|
||||
.field("encoding", &self.encoding)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
pub(crate) use std::fs::File;
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
@ -124,18 +97,18 @@ impl NamedFile {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// use std::{
|
||||
/// io::{self, Write as _},
|
||||
/// env,
|
||||
/// fs::File
|
||||
/// };
|
||||
/// use actix_files::NamedFile;
|
||||
/// use std::io::{self, Write};
|
||||
/// use std::env;
|
||||
/// use std::fs::File;
|
||||
///
|
||||
/// fn main() -> io::Result<()> {
|
||||
/// let mut file = File::create("foo.txt")?;
|
||||
/// file.write_all(b"Hello, world!")?;
|
||||
/// let named_file = NamedFile::from_file(file, "bar.txt")?;
|
||||
/// # std::fs::remove_file("foo.txt");
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// let mut file = File::create("foo.txt")?;
|
||||
/// file.write_all(b"Hello, world!")?;
|
||||
/// let named_file = NamedFile::from_file(file, "bar.txt")?;
|
||||
/// # std::fs::remove_file("foo.txt");
|
||||
/// Ok(())
|
||||
/// ```
|
||||
pub fn from_file<P: AsRef<Path>>(file: File, path: P) -> io::Result<NamedFile> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
@ -156,7 +129,7 @@ impl NamedFile {
|
||||
let ct = from_path(&path).first_or_octet_stream();
|
||||
|
||||
let disposition = match ct.type_() {
|
||||
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
|
||||
mime::IMAGE | mime::TEXT | mime::AUDIO | mime::VIDEO => DispositionType::Inline,
|
||||
mime::APPLICATION => match ct.subtype() {
|
||||
mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
|
||||
name if name == "wasm" => DispositionType::Inline,
|
||||
@ -224,7 +197,6 @@ impl NamedFile {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
/// Attempts to open a file in read-only mode.
|
||||
///
|
||||
/// # Examples
|
||||
@ -232,6 +204,7 @@ impl NamedFile {
|
||||
/// use actix_files::NamedFile;
|
||||
/// let file = NamedFile::open("foo.txt");
|
||||
/// ```
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
||||
let file = File::open(&path)?;
|
||||
Self::from_file(file, path)
|
||||
@ -239,8 +212,8 @@ impl NamedFile {
|
||||
|
||||
/// 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].
|
||||
/// When the `experimental-io-uring` crate feature is enabled, this will be async. Otherwise, it
|
||||
/// will behave just like `open`.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
@ -265,13 +238,13 @@ impl NamedFile {
|
||||
Self::from_file(file, path)
|
||||
}
|
||||
|
||||
/// Returns reference to the underlying `File` object.
|
||||
/// Returns reference to the underlying file object.
|
||||
#[inline]
|
||||
pub fn file(&self) -> &File {
|
||||
&self.file
|
||||
}
|
||||
|
||||
/// Retrieve the path of this file.
|
||||
/// Returns the filesystem path to this file.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
@ -289,55 +262,92 @@ impl NamedFile {
|
||||
self.path.as_path()
|
||||
}
|
||||
|
||||
/// Set response **Status Code**
|
||||
/// Returns the time the file was last modified.
|
||||
///
|
||||
/// Returns `None` only on unsupported platforms; see [`std::fs::Metadata::modified()`].
|
||||
/// Therefore, it is usually safe to unwrap this.
|
||||
#[inline]
|
||||
pub fn modified(&self) -> Option<SystemTime> {
|
||||
self.modified
|
||||
}
|
||||
|
||||
/// Returns the filesystem metadata associated with this file.
|
||||
#[inline]
|
||||
pub fn metadata(&self) -> &Metadata {
|
||||
&self.md
|
||||
}
|
||||
|
||||
/// Returns the `Content-Type` header that will be used when serving this file.
|
||||
#[inline]
|
||||
pub fn content_type(&self) -> &Mime {
|
||||
&self.content_type
|
||||
}
|
||||
|
||||
/// Returns the `Content-Disposition` that will be used when serving this file.
|
||||
#[inline]
|
||||
pub fn content_disposition(&self) -> &ContentDisposition {
|
||||
&self.content_disposition
|
||||
}
|
||||
|
||||
/// Returns the `Content-Encoding` that will be used when serving this file.
|
||||
///
|
||||
/// A return value of `None` indicates that the content is not already using a compressed
|
||||
/// representation and may be subject to compression downstream.
|
||||
#[inline]
|
||||
pub fn content_encoding(&self) -> Option<ContentEncoding> {
|
||||
self.encoding
|
||||
}
|
||||
|
||||
/// Set response status code.
|
||||
#[deprecated(since = "0.7.0", note = "Prefer `Responder::customize()`.")]
|
||||
pub fn set_status_code(mut self, status: StatusCode) -> Self {
|
||||
self.status_code = status;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the MIME Content-Type for serving this file. By default
|
||||
/// the Content-Type is inferred from the filename extension.
|
||||
/// Sets the `Content-Type` header that will be used when serving this file. By default the
|
||||
/// `Content-Type` is inferred from the filename extension.
|
||||
#[inline]
|
||||
pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self {
|
||||
pub fn set_content_type(mut self, mime_type: Mime) -> Self {
|
||||
self.content_type = mime_type;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Content-Disposition for serving this file. This allows
|
||||
/// changing the inline/attachment disposition as well as the filename
|
||||
/// sent to the peer.
|
||||
/// Set the Content-Disposition for serving this file. This allows changing the
|
||||
/// `inline/attachment` disposition as well as the filename sent to the peer.
|
||||
///
|
||||
/// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and
|
||||
/// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise,
|
||||
/// and the filename is taken from the path provided in the `open` method
|
||||
/// after converting it to UTF-8 using.
|
||||
/// [`std::ffi::OsStr::to_string_lossy`]
|
||||
/// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise, and the
|
||||
/// filename is taken from the path provided in the `open` method after converting it to UTF-8
|
||||
/// (using `to_string_lossy`).
|
||||
#[inline]
|
||||
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
|
||||
pub fn set_content_disposition(mut self, cd: ContentDisposition) -> Self {
|
||||
self.content_disposition = cd;
|
||||
self.flags.insert(Flags::CONTENT_DISPOSITION);
|
||||
self
|
||||
}
|
||||
|
||||
/// Disable `Content-Disposition` header.
|
||||
/// Disables `Content-Disposition` header.
|
||||
///
|
||||
/// By default Content-Disposition` header is enabled.
|
||||
/// By default, the `Content-Disposition` header is sent.
|
||||
#[inline]
|
||||
pub fn disable_content_disposition(mut self) -> Self {
|
||||
self.flags.remove(Flags::CONTENT_DISPOSITION);
|
||||
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]
|
||||
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
|
||||
self.encoding = Some(enc);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether to use ETag or not.
|
||||
/// Specifies whether to return `ETag` header in response.
|
||||
///
|
||||
/// Default is true.
|
||||
#[inline]
|
||||
@ -346,7 +356,7 @@ impl NamedFile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether to use Last-Modified or not.
|
||||
/// Specifies whether to return `Last-Modified` header in response.
|
||||
///
|
||||
/// Default is true.
|
||||
#[inline]
|
||||
@ -364,7 +374,7 @@ impl NamedFile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Creates a etag in a format is similar to Apache's.
|
||||
/// Creates an `ETag` in a format is similar to Apache's.
|
||||
pub(crate) fn etag(&self) -> Option<header::EntityTag> {
|
||||
self.modified.as_ref().map(|mtime| {
|
||||
let ino = {
|
||||
@ -386,7 +396,7 @@ impl NamedFile {
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("modification time must be after epoch");
|
||||
|
||||
header::EntityTag::strong(format!(
|
||||
header::EntityTag::new_strong(format!(
|
||||
"{:x}:{:x}:{:x}:{:x}",
|
||||
ino,
|
||||
self.md.len(),
|
||||
@ -405,12 +415,13 @@ impl NamedFile {
|
||||
if self.status_code != StatusCode::OK {
|
||||
let mut res = HttpResponse::build(self.status_code);
|
||||
|
||||
if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
let ct = equiv_utf8_text(self.content_type.clone());
|
||||
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||
let ct = if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
equiv_utf8_text(self.content_type.clone())
|
||||
} 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) {
|
||||
res.insert_header((
|
||||
@ -420,7 +431,7 @@ impl NamedFile {
|
||||
}
|
||||
|
||||
if let Some(current_encoding) = self.encoding {
|
||||
res.encoding(current_encoding);
|
||||
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
|
||||
}
|
||||
|
||||
let reader = chunked::new_chunked_read(self.md.len(), 0, self.file);
|
||||
@ -478,12 +489,13 @@ impl NamedFile {
|
||||
|
||||
let mut res = HttpResponse::build(self.status_code);
|
||||
|
||||
if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
let ct = equiv_utf8_text(self.content_type.clone());
|
||||
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||
let ct = if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
equiv_utf8_text(self.content_type.clone())
|
||||
} 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) {
|
||||
res.insert_header((
|
||||
@ -492,9 +504,8 @@ impl NamedFile {
|
||||
));
|
||||
}
|
||||
|
||||
// default compressing
|
||||
if let Some(current_encoding) = self.encoding {
|
||||
res.encoding(current_encoding);
|
||||
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
|
||||
}
|
||||
|
||||
if let Some(lm) = last_modified {
|
||||
@ -517,7 +528,27 @@ impl NamedFile {
|
||||
length = ranges[0].length;
|
||||
offset = ranges[0].start;
|
||||
|
||||
res.encoding(ContentEncoding::Identity);
|
||||
// When a Content-Encoding header is present in a 206 partial content response
|
||||
// for video content, it prevents browser video players from starting playback
|
||||
// before loading the whole video and also prevents seeking.
|
||||
//
|
||||
// See: https://github.com/actix/actix-web/issues/2815
|
||||
//
|
||||
// The assumption of this fix is that the video player knows to not send an
|
||||
// Accept-Encoding header for this request and that downstream middleware will
|
||||
// not attempt compression for requests without it.
|
||||
//
|
||||
// TODO: Solve question around what to do if self.encoding is set and partial
|
||||
// range is requested. Reject request? Ignoring self.encoding seems wrong, too.
|
||||
// In practice, it should not come up.
|
||||
if req.headers().contains_key(&header::ACCEPT_ENCODING) {
|
||||
// don't allow compression middleware to modify partial content
|
||||
res.insert_header((
|
||||
header::CONTENT_ENCODING,
|
||||
HeaderValue::from_static("identity"),
|
||||
));
|
||||
}
|
||||
|
||||
res.insert_header((
|
||||
header::CONTENT_RANGE,
|
||||
format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
|
||||
@ -626,7 +657,7 @@ impl Service<ServiceRequest> for NamedFileService {
|
||||
type Error = Error;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
actix_service::always_ready!();
|
||||
dev::always_ready!();
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
let (req, _) = req.into_parts();
|
||||
|
@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
path::{Component, Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
@ -26,8 +26,23 @@ impl PathBufWrap {
|
||||
pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
|
||||
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('/') {
|
||||
if segment == ".." {
|
||||
segment_count -= 1;
|
||||
buf.pop();
|
||||
} else if !hidden_files && segment.starts_with('.') {
|
||||
return Err(UriSegmentError::BadStart('.'));
|
||||
@ -40,14 +55,27 @@ impl PathBufWrap {
|
||||
} else if segment.ends_with('<') {
|
||||
return Err(UriSegmentError::BadEnd('<'));
|
||||
} else if segment.is_empty() {
|
||||
segment_count -= 1;
|
||||
continue;
|
||||
} else if cfg!(windows) && segment.contains('\\') {
|
||||
return Err(UriSegmentError::BadChar('\\'));
|
||||
} else if cfg!(windows) && segment.contains(':') {
|
||||
return Err(UriSegmentError::BadChar(':'));
|
||||
} else {
|
||||
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))
|
||||
}
|
||||
}
|
||||
@ -63,7 +91,7 @@ impl FromRequest for PathBufWrap {
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
ready(req.match_info().path().parse())
|
||||
ready(req.match_info().unprocessed().parse())
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,4 +165,26 @@ mod tests {
|
||||
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"])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc};
|
||||
|
||||
use actix_web::{
|
||||
body::BoxBody,
|
||||
dev::{Service, ServiceRequest, ServiceResponse},
|
||||
dev::{self, Service, ServiceRequest, ServiceResponse},
|
||||
error::Error,
|
||||
guard::Guard,
|
||||
http::{header, Method},
|
||||
@ -98,7 +98,7 @@ impl Service<ServiceRequest> for FilesService {
|
||||
type Error = Error;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
actix_service::always_ready!();
|
||||
dev::always_ready!();
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
let is_method_valid = if let Some(guard) = &self.guards {
|
||||
@ -114,32 +114,32 @@ impl Service<ServiceRequest> for FilesService {
|
||||
Box::pin(async move {
|
||||
if !is_method_valid {
|
||||
return Ok(req.into_response(
|
||||
actix_web::HttpResponse::MethodNotAllowed()
|
||||
HttpResponse::MethodNotAllowed()
|
||||
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
|
||||
.body("Request did not meet this resource's requirements."),
|
||||
));
|
||||
}
|
||||
|
||||
let real_path =
|
||||
match PathBufWrap::parse_path(req.match_info().path(), this.hidden_files) {
|
||||
Ok(item) => item,
|
||||
Err(e) => return Ok(req.error_response(e)),
|
||||
};
|
||||
let path_on_disk = match PathBufWrap::parse_path(
|
||||
req.match_info().unprocessed(),
|
||||
this.hidden_files,
|
||||
) {
|
||||
Ok(item) => item,
|
||||
Err(err) => return Ok(req.error_response(err)),
|
||||
};
|
||||
|
||||
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) = this.default {
|
||||
return default.call(req).await;
|
||||
} else {
|
||||
return Ok(
|
||||
req.into_response(actix_web::HttpResponse::NotFound().finish())
|
||||
);
|
||||
return Ok(req.into_response(HttpResponse::NotFound().finish()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// full file path
|
||||
let path = this.directory.join(&real_path);
|
||||
let path = this.directory.join(&path_on_disk);
|
||||
if let Err(err) = path.canonicalize() {
|
||||
return this.handle_err(err, req).await;
|
||||
}
|
||||
@ -168,7 +168,7 @@ impl Service<ServiceRequest> for FilesService {
|
||||
}
|
||||
}
|
||||
None if this.show_index => Ok(this.show_index(req, path)),
|
||||
_ => Ok(ServiceResponse::from_err(
|
||||
None => Ok(ServiceResponse::from_err(
|
||||
FilesError::IsDirectory,
|
||||
req.into_parts().0,
|
||||
)),
|
||||
|
@ -1,11 +1,11 @@
|
||||
use actix_files::Files;
|
||||
use actix_files::{Files, NamedFile};
|
||||
use actix_web::{
|
||||
http::{
|
||||
header::{self, HeaderValue},
|
||||
StatusCode,
|
||||
},
|
||||
test::{self, TestRequest},
|
||||
App,
|
||||
web, App,
|
||||
};
|
||||
|
||||
#[actix_web::test]
|
||||
@ -19,12 +19,12 @@ async fn test_utf8_file_contents() {
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
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 =
|
||||
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;
|
||||
|
||||
let req = TestRequest::with_uri("/utf8.txt").to_request();
|
||||
@ -33,6 +33,34 @@ async fn test_utf8_file_contents() {
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
res.headers().get(header::CONTENT_TYPE),
|
||||
Some(&HeaderValue::from_static("text/plain; charset=utf-8")),
|
||||
Some(&HeaderValue::from_static("text/plain")),
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn partial_range_response_encoding() {
|
||||
let srv = test::init_service(App::new().default_service(web::to(|| async {
|
||||
NamedFile::open_async("./tests/test.binary").await.unwrap()
|
||||
})))
|
||||
.await;
|
||||
|
||||
// range request without accept-encoding returns no content-encoding header
|
||||
let req = TestRequest::with_uri("/")
|
||||
.append_header((header::RANGE, "bytes=10-20"))
|
||||
.to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::PARTIAL_CONTENT);
|
||||
assert!(!res.headers().contains_key(header::CONTENT_ENCODING));
|
||||
|
||||
// range request with accept-encoding returns a content-encoding header
|
||||
let req = TestRequest::with_uri("/")
|
||||
.append_header((header::RANGE, "bytes=10-20"))
|
||||
.append_header((header::ACCEPT_ENCODING, "identity"))
|
||||
.to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::PARTIAL_CONTENT);
|
||||
assert_eq!(
|
||||
res.headers().get(header::CONTENT_ENCODING).unwrap(),
|
||||
"identity"
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,35 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
## Unreleased - 2022-xx-xx
|
||||
- Minimum supported Rust version (MSRV) is now 1.59.
|
||||
|
||||
|
||||
## 3.0.0 - 2022-07-24
|
||||
- `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
|
||||
- Added `TestServer::client_headers` method. [#2097]
|
||||
- Update `actix-server` dependency to `2`.
|
||||
- Update `actix-tls` dependency to `3`.
|
||||
- Update `bytes` to `1.0`. [#1813]
|
||||
- Minimum supported Rust version (MSRV) is now 1.57.
|
||||
|
||||
[#2442]: https://github.com/actix/actix-web/pull/2442
|
||||
[#2097]: https://github.com/actix/actix-web/pull/2097
|
||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||
|
||||
|
||||
<details>
|
||||
<summary>3.0.0 Pre-Releases</summary>
|
||||
|
||||
## 3.0.0-beta.13 - 2022-02-16
|
||||
- No significant changes since `3.0.0-beta.12`.
|
||||
|
||||
|
||||
## 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
|
||||
@ -56,6 +85,7 @@
|
||||
|
||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||
|
||||
</details>
|
||||
|
||||
## 2.1.0 - 2020-11-25
|
||||
- Add ability to set address for `TestServer`. [#1645]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http-test"
|
||||
version = "3.0.0-beta.10"
|
||||
version = "3.0.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Various helpers for Actix applications to use during testing"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
@ -29,13 +29,13 @@ default = []
|
||||
openssl = ["tls-openssl", "awc/openssl"]
|
||||
|
||||
[dependencies]
|
||||
actix-service = "2.0.0"
|
||||
actix-codec = "0.4.1"
|
||||
actix-tls = "3.0.0"
|
||||
actix-utils = "3.0.0"
|
||||
actix-service = "2"
|
||||
actix-codec = "0.5"
|
||||
actix-tls = "3"
|
||||
actix-utils = "3"
|
||||
actix-rt = "2.2"
|
||||
actix-server = "2.0.0-rc.2"
|
||||
awc = { version = "3.0.0-beta.16", default-features = false }
|
||||
actix-server = "2"
|
||||
awc = { version = "3", default-features = false }
|
||||
|
||||
base64 = "0.13"
|
||||
bytes = "1"
|
||||
@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||
tokio = { version = "1.8.4", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = { version = "4.0.0-beta.17", default-features = false, features = ["cookies"] }
|
||||
actix-http = "3.0.0-beta.17"
|
||||
actix-web = { version = "4", default-features = false, features = ["cookies"] }
|
||||
actix-http = "3"
|
||||
|
@ -3,15 +3,15 @@
|
||||
> Various helpers for Actix applications to use during testing.
|
||||
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://docs.rs/actix-http-test/3.0.0-beta.10)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||
[](https://docs.rs/actix-http-test/3.0.0)
|
||||

|
||||

|
||||
<br>
|
||||
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.10)
|
||||
[](https://deps.rs/crate/actix-http-test/3.0.0)
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-http-test)
|
||||
- Minimum Supported Rust Version (MSRV): 1.52
|
||||
- Minimum Supported Rust Version (MSRV): 1.54
|
||||
|
@ -1,10 +1,460 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
## Unreleased - 2022-xx-xx
|
||||
|
||||
|
||||
## 3.2.2 - 2022-09-11
|
||||
### Changed
|
||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||
|
||||
### Fixed
|
||||
- Avoid possibility of dispatcher getting stuck while back-pressuring I/O. [#2369]
|
||||
|
||||
[#2369]: https://github.com/actix/actix-web/pull/2369
|
||||
|
||||
|
||||
## 3.2.1 - 2022-07-02
|
||||
### Fixed
|
||||
- Fix parsing ambiguity in Transfer-Encoding and Content-Length headers for HTTP/1.0 requests. [#2794]
|
||||
|
||||
[#2794]: https://github.com/actix/actix-web/pull/2794
|
||||
|
||||
|
||||
## 3.2.0 - 2022-06-30
|
||||
### Changed
|
||||
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||
|
||||
### Fixed
|
||||
- Websocket parser no longer throws endless overflow errors after receiving an oversized frame. [#2790]
|
||||
- Retain previously set Vary headers when using compression encoder. [#2798]
|
||||
|
||||
[#2790]: https://github.com/actix/actix-web/pull/2790
|
||||
[#2798]: https://github.com/actix/actix-web/pull/2798
|
||||
|
||||
|
||||
## 3.1.0 - 2022-06-11
|
||||
### Changed
|
||||
- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency.
|
||||
|
||||
### Fixed
|
||||
- Revert broken fix in [#2624] that caused erroneous 500 error responses. Temporarily re-introduces [#2357] bug. [#2779]
|
||||
|
||||
[#2624]: https://github.com/actix/actix-web/pull/2624
|
||||
[#2357]: https://github.com/actix/actix-web/issues/2357
|
||||
[#2779]: https://github.com/actix/actix-web/pull/2779
|
||||
|
||||
|
||||
## 3.0.4 - 2022-03-09
|
||||
### Fixed
|
||||
- Document on docs.rs with `ws` feature enabled.
|
||||
|
||||
|
||||
## 3.0.3 - 2022-03-08
|
||||
### Fixed
|
||||
- Allow spaces between header name and colon when parsing responses. [#2684]
|
||||
|
||||
[#2684]: https://github.com/actix/actix-web/pull/2684
|
||||
|
||||
|
||||
## 3.0.2 - 2022-03-05
|
||||
### Fixed
|
||||
- Fix encoding camel-case header names with more than one hyphen. [#2683]
|
||||
|
||||
[#2683]: https://github.com/actix/actix-web/pull/2683
|
||||
|
||||
|
||||
## 3.0.1 - 2022-03-04
|
||||
- Fix panic in H1 dispatcher when pipelining is used with keep-alive. [#2678]
|
||||
|
||||
[#2678]: https://github.com/actix/actix-web/issues/2678
|
||||
|
||||
## 3.0.0 - 2022-02-25
|
||||
### Dependencies
|
||||
- Updated `actix-*` to Tokio v1-based versions. [#1813]
|
||||
- Updated `bytes` to `1.0`. [#1813]
|
||||
- Updated `h2` to `0.3`. [#1813]
|
||||
- Updated `rustls` to `0.20.0`. [#2414]
|
||||
- Updated `language-tags` to `0.3`.
|
||||
- Updated `tokio` to `1`.
|
||||
|
||||
### Added
|
||||
- Crate Features:
|
||||
- `ws`; disabled by default. [#2618]
|
||||
- `http2`; disabled by default. [#2618]
|
||||
- `compress-brotli`; disabled by default. [#2618]
|
||||
- `compress-gzip`; disabled by default. [#2618]
|
||||
- `compress-zstd`; disabled by default. [#2618]
|
||||
- Functions:
|
||||
- `body::to_bytes` for async collecting message body into Bytes. [#2158]
|
||||
- Traits:
|
||||
- `TryIntoHeaderPair`; allows using typed and untyped headers in the same methods. [#1869]
|
||||
- Types:
|
||||
- `body::BoxBody`; a boxed message body with boxed errors. [#2183]
|
||||
- `body::EitherBody` enum. [#2468]
|
||||
- `body::None` struct. [#2468]
|
||||
- Re-export `http` crate's `Error` type as `error::HttpError`. [#2171]
|
||||
- Variants:
|
||||
- `ContentEncoding::Zstd` along with . [#2244]
|
||||
- `Protocol::Http3` for future compatibility and also mark `#[non_exhaustive]`. [00ba8d55]
|
||||
- Methods:
|
||||
- `ContentEncoding::to_header_value()`. [#2501]
|
||||
- `header::QualityItem::{max, min}()`. [#2486]
|
||||
- `header::QualityItem::zero()` that uses `Quality::ZERO`. [#2501]
|
||||
- `HeaderMap::drain()` as an efficient draining iterator. [#1964]
|
||||
- `HeaderMap::len_keys()` has the behavior of the old `len` method. [#1964]
|
||||
- `MessageBody::boxed` trait method for wrapping boxing types efficiently. [#2520]
|
||||
- `MessageBody::try_into_bytes` trait method, with default implementation, for optimizations on body types that complete in exactly one poll. [#2522]
|
||||
- `Request::conn_data()`. [#2491]
|
||||
- `Request::take_conn_data()`. [#2491]
|
||||
- `Request::take_req_data()`. [#2487]
|
||||
- `Response::{ok, bad_request, not_found, internal_server_error}()`. [#2159]
|
||||
- `Response::into_body()` that consumes response and returns body type. [#2201]
|
||||
- `Response::map_into_boxed_body()`. [#2468]
|
||||
- `ResponseBuilder::append_header()` method which allows using typed and untyped headers. [#1869]
|
||||
- `ResponseBuilder::insert_header()` method which allows using typed and untyped headers. [#1869]
|
||||
- `ResponseHead::set_camel_case_headers()`. [#2587]
|
||||
- `TestRequest::insert_header()` method which allows using typed and untyped headers. [#1869]
|
||||
- Implementations:
|
||||
- Implement `Clone for ws::HandshakeError`. [#2468]
|
||||
- Implement `Clone` for `body::AnyBody<S> where S: Clone`. [#2448]
|
||||
- Implement `Clone` for `RequestHead`. [#2487]
|
||||
- Implement `Clone` for `ResponseHead`. [#2585]
|
||||
- Implement `Copy` for `QualityItem<T> where T: Copy`. [#2501]
|
||||
- Implement `Default` for `ContentEncoding`. [#1912]
|
||||
- Implement `Default` for `HttpServiceBuilder`. [#2611]
|
||||
- Implement `Default` for `KeepAlive`. [#2611]
|
||||
- Implement `Default` for `Response`. [#2201]
|
||||
- Implement `Default` for `ws::Codec`. [#1920]
|
||||
- Implement `Display` for `header::Quality`. [#2486]
|
||||
- Implement `Eq` for `header::ContentEncoding`. [#2501]
|
||||
- Implement `ExactSizeIterator` and `FusedIterator` for all `HeaderMap` iterators. [#2470]
|
||||
- Implement `From<Duration>` for `KeepAlive`. [#2611]
|
||||
- Implement `From<Option<Duration>>` for `KeepAlive`. [#2611]
|
||||
- Implement `From<Vec<u8>>` for `Response<Vec<u8>>`. [#2625]
|
||||
- Implement `FromStr` for `ContentEncoding`. [#1912]
|
||||
- Implement `Header` for `ContentEncoding`. [#1912]
|
||||
- Implement `IntoHeaderValue` for `ContentEncoding`. [#1912]
|
||||
- Implement `IntoIterator` for `HeaderMap`. [#1964]
|
||||
- Implement `MessageBody` for `bytestring::ByteString`. [#2468]
|
||||
- Implement `MessageBody` for `Pin<Box<T>> where T: MessageBody`. [#2152]
|
||||
- Misc:
|
||||
- Re-export `StatusCode`, `Method`, `Version` and `Uri` at the crate root. [#2171]
|
||||
- Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171]
|
||||
- `Quality::ZERO` associated constant equivalent to `q=0`. [#2501]
|
||||
- `header::Quality::{MAX, MIN}` associated constants equivalent to `q=1` and `q=0.001`, respectively. [#2486]
|
||||
- Timeout for canceling HTTP/2 server side connection handshake. Configurable with `ServiceConfig::client_timeout`; defaults to 5 seconds. [#2483]
|
||||
- `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920]
|
||||
|
||||
### Changed
|
||||
- Traits:
|
||||
- Rename `IntoHeaderValue => TryIntoHeaderValue`. [#2510]
|
||||
- `MessageBody` now has an associated `Error` type. [#2183]
|
||||
- Types:
|
||||
- `Protocol` enum is now marked `#[non_exhaustive]`.
|
||||
- `error::DispatcherError` enum is now marked `#[non_exhaustive]`. [#2624]
|
||||
- `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377]
|
||||
- Error enums are marked `#[non_exhaustive]`. [#2161]
|
||||
- Rename `PayloadStream` to `BoxedPayloadStream`. [#2545]
|
||||
- The body type parameter of `Response` no longer has a default. [#2152]
|
||||
- Enum Variants:
|
||||
- Rename `ContentEncoding::{Br => Brotli}`. [#2501]
|
||||
- `Payload` inner fields are now named. [#2545]
|
||||
- `ws::Message::Text` now contains a `bytestring::ByteString`. [#1864]
|
||||
- Methods:
|
||||
- Rename `ServiceConfig::{client_timer_expire => client_request_deadline}`. [#2611]
|
||||
- Rename `ServiceConfig::{client_disconnect_timer => client_disconnect_deadline}`. [#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]
|
||||
- Rename `header::EntityTag::{weak => new_weak, strong => new_strong}`. [#2565]
|
||||
- Rename `TryIntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std `TryInto` trait. [#1894]
|
||||
- Deadline methods in `ServiceConfig` now return `std::time::Instant`s instead of Tokio's wrapper type. [#2611]
|
||||
- Places in `Response` where `ResponseBody<B>` was received or returned now simply use `B`. [#2201]
|
||||
- `encoding::Encoder::response` now returns `AnyBody<Encoder<B>>`. [#2448]
|
||||
- `Extensions::insert` returns replaced item. [#1904]
|
||||
- `HeaderMap::get_all` now returns a `std::slice::Iter`. [#2527]
|
||||
- `HeaderMap::insert` now returns iterator of removed values. [#1964]
|
||||
- `HeaderMap::len` now returns number of values instead of number of keys. [#1964]
|
||||
- `HeaderMap::remove` now returns iterator of removed values. [#1964]
|
||||
- `ResponseBuilder::body(B)` now returns `Response<EitherBody<B>>`. [#2468]
|
||||
- `ResponseBuilder::content_type` now takes an `impl TryIntoHeaderValue` to support using typed `mime` types. [#1894]
|
||||
- `ResponseBuilder::finish()` now returns `Response<EitherBody<()>>`. [#2468]
|
||||
- `ResponseBuilder::json` now takes `impl Serialize`. [#2052]
|
||||
- `ResponseBuilder::message_body` now returns a `Result`. [#2201]∑
|
||||
- `ServiceConfig::keep_alive` now returns a `KeepAlive`. [#2611]
|
||||
- `ws::hash_key` now returns array. [#2035]
|
||||
- Trait Implementations:
|
||||
- Implementation of `Stream` for `Payload` no longer requires the `Stream` variant be `Unpin`. [#2545]
|
||||
- Implementation of `Future` for `h1::SendResponse` no longer requires the body type be `Unpin`. [#2545]
|
||||
- Implementation of `Stream` for `encoding::Decoder` no longer requires the stream type be `Unpin`. [#2545]
|
||||
- Implementation of `From` for error types now return a `Response<BoxBody>`. [#2468]
|
||||
- Misc:
|
||||
- `header` module is now public. [#2171]
|
||||
- `uri` module is now public. [#2171]
|
||||
- Request-local data container is no longer part of a `RequestHead`. Instead it is a distinct part of a `Request`. [#2487]
|
||||
- All error trait bounds in server service builders have changed from `Into<Error>` to `Into<Response<BoxBody>>`. [#2253]
|
||||
- All error trait bounds in message body and stream impls changed from `Into<Error>` to `Into<Box<dyn std::error::Error>>`. [#2253]
|
||||
- Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467]
|
||||
- Connection data set through the `on_connect_ext` callbacks is now accessible only from the new `Request::conn_data()` method. [#2491]
|
||||
- Brotli (de)compression support is now provided by the `brotli` crate. [#2538]
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
### Fixed
|
||||
- A `Vary` header is now correctly sent along with compressed content. [#2501]
|
||||
- HTTP/1.1 dispatcher correctly uses client request timeout. [#2611]
|
||||
- Fixed issue where handlers that took payload but then dropped without reading it to EOF it would cause keep-alive connections to become stuck. [#2624]
|
||||
- `ContentEncoding`'s `Identity` variant can now be parsed from a string. [#2501]
|
||||
- `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuration parameter. [#2226]
|
||||
- Remove unnecessary `Into<Error>` bound on `Encoder` body types. [#2375]
|
||||
- Remove unnecessary `Unpin` bound on `ResponseBuilder::streaming`. [#2253]
|
||||
- `BodyStream` and `SizedStream` are no longer restricted to `Unpin` types. [#2152]
|
||||
- Fixed slice creation pointing to potential uninitialized data on h1 encoder. [#2364]
|
||||
- Fixed quality parse error in Accept-Encoding header. [#2344]
|
||||
|
||||
### Removed
|
||||
- Crate Features:
|
||||
- `compress` feature. [#2065]
|
||||
- `cookies` feature. [#2065]
|
||||
- `trust-dns` feature. [#2425]
|
||||
- `actors` optional feature and trait implementation for `actix` types. [#1969]
|
||||
- Functions:
|
||||
- `header::qitem` helper. Replaced with `header::QualityItem::max`. [#2486]
|
||||
- Types:
|
||||
- `body::Body`; replaced with `EitherBody` and `BoxBody`. [#2468]
|
||||
- `body::ResponseBody`. [#2446]
|
||||
- `ConnectError::SslHandshakeError` and re-export of `HandshakeError`. Due to the removal of this type from `tokio-openssl` crate. OpenSSL handshake error now returns `ConnectError::SslError`. [#1813]
|
||||
- `error::Canceled` re-export. [#1994]
|
||||
- `error::Result` type alias. [#2201]
|
||||
- `error::BlockingError` [#2660]
|
||||
- `InternalError` and all the error types it constructed were moved up to `actix-web`. [#2215]
|
||||
- Typed HTTP headers; they have moved up to `actix-web`. [2094]
|
||||
- Re-export of `http` crate's `HeaderMap` types in addition to ours. [#2171]
|
||||
- Enum Variants:
|
||||
- `body::BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446]
|
||||
- `ContentEncoding::Auto`. [#2501]
|
||||
- `EncoderError::Boxed`. [#2446]
|
||||
- Methods:
|
||||
- `ContentEncoding::is_compression()`. [#2501]
|
||||
- `h1::Payload::readany()`. [#2545]
|
||||
- `HttpMessage::cookie[s]()` trait methods. [#2065]
|
||||
- `HttpServiceBuilder::new()`; use `default` instead. [#2611]
|
||||
- `on_connect` (previously deprecated) methods have been removed; use `on_connect_ext`. [#1857]
|
||||
- `Response::build_from()`. [#2159]
|
||||
- `Response::error()` [#2205]
|
||||
- `Response::take_body()` and old `Response::into_body()` method that casted body type. [#2201]
|
||||
- `Response`'s status code builders. [#2159]
|
||||
- `ResponseBuilder::{if_true, if_some}()` (previously deprecated). [#2148]
|
||||
- `ResponseBuilder::{set, set_header}()`; use `ResponseBuilder::insert_header()`. [#1869]
|
||||
- `ResponseBuilder::extensions[_mut]()`. [#2585]
|
||||
- `ResponseBuilder::header()`; use `ResponseBuilder::append_header()`. [#1869]
|
||||
- `ResponseBuilder::json()`. [#2148]
|
||||
- `ResponseBuilder::json2()`. [#1903]
|
||||
- `ResponseBuilder::streaming()`. [#2468]
|
||||
- `ResponseHead::extensions[_mut]()`. [#2585]
|
||||
- `ServiceConfig::{client_timer, keep_alive_timer}()`. [#2611]
|
||||
- `TestRequest::with_hdr()`; use `TestRequest::default().insert_header()`. [#1869]
|
||||
- `TestRequest::with_header()`; use `TestRequest::default().insert_header()`. [#1869]
|
||||
- Trait implementations:
|
||||
- Implementation of `Copy` for `ws::Codec`. [#1920]
|
||||
- Implementation of `From<Option<usize>> for KeepAlive`; use `Duration`s instead. [#2611]
|
||||
- Implementation of `From<serde_json::Value>` for `Body`. [#2148]
|
||||
- Implementation of `From<usize> for KeepAlive`; use `Duration`s instead. [#2611]
|
||||
- Implementation of `Future` for `Response`. [#2201]
|
||||
- Implementation of `Future` for `ResponseBuilder`. [#2468]
|
||||
- Implementation of `Into<Error>` for `Response<Body>`. [#2215]
|
||||
- Implementation of `Into<Error>` for `ResponseBuilder`. [#2215]
|
||||
- Implementation of `ResponseError` for `actix_utils::timeout::TimeoutError`. [#2127]
|
||||
- Implementation of `ResponseError` for `CookieParseError`. [#2065]
|
||||
- Implementation of `TryFrom<u16>` for `header::Quality`. [#2486]
|
||||
- Misc:
|
||||
- `http` module; most everything it contained is exported at the crate root. [#2488]
|
||||
- `cookies` module (re-export). [#2065]
|
||||
- `client` module. Connector types now live in `awc`. [#2425]
|
||||
- `error` field from `Response`. [#2205]
|
||||
- `downcast` and `downcast_get_type_id` macros. [#2291]
|
||||
- Down-casting for `MessageBody` types; use standard `Any` trait. [#2183]
|
||||
|
||||
|
||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||
[#1845]: https://github.com/actix/actix-web/pull/1845
|
||||
[#1857]: https://github.com/actix/actix-web/pull/1857
|
||||
[#1864]: https://github.com/actix/actix-web/pull/1864
|
||||
[#1869]: https://github.com/actix/actix-web/pull/1869
|
||||
[#1878]: https://github.com/actix/actix-web/pull/1878
|
||||
[#1894]: https://github.com/actix/actix-web/pull/1894
|
||||
[#1903]: https://github.com/actix/actix-web/pull/1903
|
||||
[#1904]: https://github.com/actix/actix-web/pull/1904
|
||||
[#1912]: https://github.com/actix/actix-web/pull/1912
|
||||
[#1920]: https://github.com/actix/actix-web/pull/1920
|
||||
[#1964]: https://github.com/actix/actix-web/pull/1964
|
||||
[#1969]: https://github.com/actix/actix-web/pull/1969
|
||||
[#1981]: https://github.com/actix/actix-web/pull/1981
|
||||
[#1994]: https://github.com/actix/actix-web/pull/1994
|
||||
[#2035]: https://github.com/actix/actix-web/pull/2035
|
||||
[#2052]: https://github.com/actix/actix-web/pull/2052
|
||||
[#2065]: https://github.com/actix/actix-web/pull/2065
|
||||
[#2094]: https://github.com/actix/actix-web/pull/2094
|
||||
[#2127]: https://github.com/actix/actix-web/pull/2127
|
||||
[#2148]: https://github.com/actix/actix-web/pull/2148
|
||||
[#2152]: https://github.com/actix/actix-web/pull/2152
|
||||
[#2158]: https://github.com/actix/actix-web/pull/2158
|
||||
[#2159]: https://github.com/actix/actix-web/pull/2159
|
||||
[#2161]: https://github.com/actix/actix-web/pull/2161
|
||||
[#2171]: https://github.com/actix/actix-web/pull/2171
|
||||
[#2183]: https://github.com/actix/actix-web/pull/2183
|
||||
[#2196]: https://github.com/actix/actix-web/pull/2196
|
||||
[#2201]: https://github.com/actix/actix-web/pull/2201
|
||||
[#2205]: https://github.com/actix/actix-web/pull/2205
|
||||
[#2215]: https://github.com/actix/actix-web/pull/2215
|
||||
[#2244]: https://github.com/actix/actix-web/pull/2244
|
||||
[#2250]: https://github.com/actix/actix-web/pull/2250
|
||||
[#2253]: https://github.com/actix/actix-web/pull/2253
|
||||
[#2291]: https://github.com/actix/actix-web/pull/2291
|
||||
[#2344]: https://github.com/actix/actix-web/pull/2344
|
||||
[#2364]: https://github.com/actix/actix-web/pull/2364
|
||||
[#2375]: https://github.com/actix/actix-web/pull/2375
|
||||
[#2377]: https://github.com/actix/actix-web/pull/2377
|
||||
[#2414]: https://github.com/actix/actix-web/pull/2414
|
||||
[#2425]: https://github.com/actix/actix-web/pull/2425
|
||||
[#2442]: https://github.com/actix/actix-web/pull/2442
|
||||
[#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
|
||||
[#2467]: https://github.com/actix/actix-web/pull/2467
|
||||
[#2468]: https://github.com/actix/actix-web/pull/2468
|
||||
[#2470]: https://github.com/actix/actix-web/pull/2470
|
||||
[#2474]: https://github.com/actix/actix-web/pull/2474
|
||||
[#2483]: https://github.com/actix/actix-web/pull/2483
|
||||
[#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
|
||||
[#2501]: https://github.com/actix/actix-web/pull/2501
|
||||
[#2510]: https://github.com/actix/actix-web/pull/2510
|
||||
[#2520]: https://github.com/actix/actix-web/pull/2520
|
||||
[#2522]: https://github.com/actix/actix-web/pull/2522
|
||||
[#2527]: https://github.com/actix/actix-web/pull/2527
|
||||
[#2538]: https://github.com/actix/actix-web/pull/2538
|
||||
[#2545]: https://github.com/actix/actix-web/pull/2545
|
||||
[#2565]: https://github.com/actix/actix-web/pull/2565
|
||||
[#2585]: https://github.com/actix/actix-web/pull/2585
|
||||
[#2587]: https://github.com/actix/actix-web/pull/2587
|
||||
[#2611]: https://github.com/actix/actix-web/pull/2611
|
||||
[#2618]: https://github.com/actix/actix-web/pull/2618
|
||||
[#2624]: https://github.com/actix/actix-web/pull/2624
|
||||
[#2625]: https://github.com/actix/actix-web/pull/2625
|
||||
[#2660]: https://github.com/actix/actix-web/pull/2660
|
||||
[00ba8d55]: https://github.com/actix/actix-web/commit/00ba8d55492284581695d824648590715a8bd386
|
||||
|
||||
|
||||
<details>
|
||||
<summary>3.0.0 Pre-Releases</summary>
|
||||
|
||||
## 3.0.0-rc.4 - 2022-02-22
|
||||
### Fixed
|
||||
- Fix h1 dispatcher panic. [1ce58ecb]
|
||||
|
||||
[1ce58ecb]: https://github.com/actix/actix-web/commit/1ce58ecb305c60e51db06e6c913b7a1344e229ca
|
||||
|
||||
|
||||
## 3.0.0-rc.3 - 2022-02-16
|
||||
- No significant changes since `3.0.0-rc.2`.
|
||||
|
||||
|
||||
## 3.0.0-rc.2 - 2022-02-08
|
||||
### Added
|
||||
- Implement `From<Vec<u8>>` for `Response<Vec<u8>>`. [#2625]
|
||||
|
||||
### Changed
|
||||
- `error::DispatcherError` enum is now marked `#[non_exhaustive]`. [#2624]
|
||||
|
||||
### Fixed
|
||||
- Issue where handlers that took payload but then dropped without reading it to EOF it would cause keep-alive connections to become stuck. [#2624]
|
||||
|
||||
[#2624]: https://github.com/actix/actix-web/pull/2624
|
||||
[#2625]: https://github.com/actix/actix-web/pull/2625
|
||||
|
||||
|
||||
## 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
|
||||
- Rename `ServiceConfig::{client_timer_expire => client_request_deadline}`. [#2611]
|
||||
- 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
|
||||
- HTTP/1.1 dispatcher correctly uses client request timeout. [#2611]
|
||||
|
||||
### 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
|
||||
### Changed
|
||||
- `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]
|
||||
@ -227,7 +677,7 @@
|
||||
- `Response::{ok, bad_request, not_found, internal_server_error}`. [#2159]
|
||||
- Helper `body::to_bytes` for async collecting message body into Bytes. [#2158]
|
||||
|
||||
### Changes
|
||||
### Changed
|
||||
- The type parameter of `Response` no longer has a default. [#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]
|
||||
@ -360,10 +810,10 @@
|
||||
- Remove `ResponseError` impl for `actix::actors::resolver::ResolverError`
|
||||
due to deprecate of resolver actor. [#1813]
|
||||
- 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]
|
||||
- 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]
|
||||
|
||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||
@ -371,6 +821,15 @@
|
||||
[#1864]: https://github.com/actix/actix-web/pull/1864
|
||||
[#1878]: https://github.com/actix/actix-web/pull/1878
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## 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
|
||||
### Fixed
|
||||
|
@ -1,7 +1,10 @@
|
||||
[package]
|
||||
name = "actix-http"
|
||||
version = "3.0.0-beta.17"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
version = "3.2.2"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
]
|
||||
description = "HTTP primitives for the Actix ecosystem"
|
||||
keywords = ["actix", "http", "framework", "async", "futures"]
|
||||
homepage = "https://actix.rs"
|
||||
@ -17,7 +20,7 @@ edition = "2018"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
# features that docs.rs will build with
|
||||
features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"]
|
||||
features = ["http2", "ws", "openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"]
|
||||
|
||||
[lib]
|
||||
name = "actix_http"
|
||||
@ -26,71 +29,89 @@ path = "src/lib.rs"
|
||||
[features]
|
||||
default = []
|
||||
|
||||
# openssl
|
||||
# HTTP/2 protocol support
|
||||
http2 = ["h2"]
|
||||
|
||||
# WebSocket protocol implementation
|
||||
ws = [
|
||||
"local-channel",
|
||||
"base64",
|
||||
"rand",
|
||||
"sha1",
|
||||
]
|
||||
|
||||
# TLS via OpenSSL
|
||||
openssl = ["actix-tls/accept", "actix-tls/openssl"]
|
||||
|
||||
# rustls support
|
||||
# TLS via Rustls
|
||||
rustls = ["actix-tls/accept", "actix-tls/rustls"]
|
||||
|
||||
# enable compression support
|
||||
compress-brotli = ["brotli2", "__compress"]
|
||||
compress-gzip = ["flate2", "__compress"]
|
||||
compress-zstd = ["zstd", "__compress"]
|
||||
# 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.
|
||||
# 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 = []
|
||||
|
||||
[dependencies]
|
||||
actix-service = "2.0.0"
|
||||
actix-codec = "0.4.1"
|
||||
actix-utils = "3.0.0"
|
||||
actix-service = "2"
|
||||
actix-codec = "0.5"
|
||||
actix-utils = "3"
|
||||
actix-rt = { version = "2.2", default-features = false }
|
||||
|
||||
ahash = "0.7"
|
||||
base64 = "0.13"
|
||||
bitflags = "1.2"
|
||||
bytes = "1"
|
||||
bytestring = "1"
|
||||
derive_more = "0.99.5"
|
||||
encoding_rs = "0.8"
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
h2 = "0.3.9"
|
||||
http = "0.2.5"
|
||||
httparse = "1.5.1"
|
||||
httpdate = "1.0.1"
|
||||
itoa = "1"
|
||||
language-tags = "0.3"
|
||||
local-channel = "0.1"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
percent-encoding = "2.1"
|
||||
pin-project-lite = "0.2"
|
||||
rand = "0.8"
|
||||
sha-1 = "0.10"
|
||||
smallvec = "1.6.1"
|
||||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||
|
||||
# tls
|
||||
actix-tls = { version = "3.0.0", default-features = false, optional = true }
|
||||
# http2
|
||||
h2 = { version = "0.3.9", optional = true }
|
||||
|
||||
# compression
|
||||
brotli2 = { version="0.3.2", optional = true }
|
||||
# websockets
|
||||
local-channel = { version = "0.1", optional = true }
|
||||
base64 = { version = "0.13", optional = true }
|
||||
rand = { version = "0.8", optional = true }
|
||||
sha1 = { version = "0.10", optional = true }
|
||||
|
||||
# openssl/rustls
|
||||
actix-tls = { version = "3", default-features = false, optional = true }
|
||||
|
||||
# compress-*
|
||||
brotli = { version = "3.3.3", optional = true }
|
||||
flate2 = { version = "1.0.13", optional = true }
|
||||
zstd = { version = "0.9", optional = true }
|
||||
zstd = { version = "0.11", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] }
|
||||
actix-server = "2.0.0-rc.2"
|
||||
actix-tls = { version = "3.0.0", features = ["openssl"] }
|
||||
actix-web = "4.0.0-beta.17"
|
||||
actix-http-test = { version = "3", features = ["openssl"] }
|
||||
actix-server = "2"
|
||||
actix-tls = { version = "3", features = ["openssl"] }
|
||||
actix-web = "4"
|
||||
|
||||
async-stream = "0.3"
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
env_logger = "0.9"
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
rcgen = "0.8"
|
||||
memchr = "2.4"
|
||||
once_cell = "1.9"
|
||||
rcgen = "0.9"
|
||||
regex = "1.3"
|
||||
rustls-pemfile = "0.2"
|
||||
rustversion = "1"
|
||||
rustls-pemfile = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
static_assertions = "1"
|
||||
@ -100,7 +121,7 @@ tokio = { version = "1.8.4", features = ["net", "rt", "macros"] }
|
||||
|
||||
[[example]]
|
||||
name = "ws"
|
||||
required-features = ["rustls"]
|
||||
required-features = ["ws", "rustls"]
|
||||
|
||||
[[bench]]
|
||||
name = "write-camel-case"
|
||||
|
@ -3,18 +3,18 @@
|
||||
> HTTP primitives for the Actix ecosystem.
|
||||
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/3.0.0-beta.17)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||
[](https://docs.rs/actix-http/3.2.2)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.0.0-beta.17)
|
||||
[](https://deps.rs/crate/actix-http/3.2.2)
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-http)
|
||||
- Minimum Supported Rust Version (MSRV): 1.52
|
||||
- Minimum Supported Rust Version (MSRV): 1.54
|
||||
|
||||
## Example
|
||||
|
||||
@ -25,7 +25,7 @@ use actix_http::{HttpService, Response};
|
||||
use actix_server::Server;
|
||||
use futures_util::future;
|
||||
use http::header::HeaderValue;
|
||||
use log::info;
|
||||
use tracing::info;
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
|
@ -42,32 +42,37 @@ mod _new {
|
||||
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)
|
||||
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)
|
||||
itoa_fmt(f, x / 10)
|
||||
} else {
|
||||
itoa::fmt(f, x)
|
||||
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)
|
||||
itoa_fmt(f, x / 100)
|
||||
} else if x % 10 == 0 {
|
||||
// one trailing 0, divide by 10 and write
|
||||
itoa::fmt(f, x / 10)
|
||||
itoa_fmt(f, x / 10)
|
||||
} else {
|
||||
itoa::fmt(f, x)
|
||||
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 {
|
||||
|
@ -114,11 +114,12 @@ mod _original {
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
||||
#![allow(clippy::uninit_assumed_init)]
|
||||
#![allow(invalid_value, clippy::uninit_assumed_init)]
|
||||
|
||||
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
|
||||
#[allow(invalid_value)]
|
||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
|
||||
|
@ -18,7 +18,8 @@ async fn main() -> std::io::Result<()> {
|
||||
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()))
|
||||
// h1 will configure server to only use HTTP/1.1
|
||||
.h1(map_config(app, |_| AppConfig::default()))
|
||||
.tcp()
|
||||
})?
|
||||
.run()
|
||||
|
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,10 +1,11 @@
|
||||
use std::io;
|
||||
use std::{io, time::Duration};
|
||||
|
||||
use actix_http::{Error, HttpService, Request, Response, StatusCode};
|
||||
use actix_server::Server;
|
||||
use bytes::BytesMut;
|
||||
use futures_util::StreamExt as _;
|
||||
use http::header::HeaderValue;
|
||||
use tracing::info;
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
@ -13,22 +14,24 @@ async fn main() -> io::Result<()> {
|
||||
Server::build()
|
||||
.bind("echo", ("127.0.0.1", 8080), || {
|
||||
HttpService::build()
|
||||
.client_timeout(1000)
|
||||
.client_disconnect(1000)
|
||||
.client_request_timeout(Duration::from_secs(1))
|
||||
.client_disconnect_timeout(Duration::from_secs(1))
|
||||
// handles HTTP/1.1 and HTTP/2
|
||||
.finish(|mut req: Request| async move {
|
||||
let mut body = BytesMut::new();
|
||||
while let Some(item) = req.payload().next().await {
|
||||
body.extend_from_slice(&item?);
|
||||
}
|
||||
|
||||
log::info!("request body: {:?}", body);
|
||||
info!("request body: {:?}", body);
|
||||
|
||||
Ok::<_, Error>(
|
||||
Response::build(StatusCode::OK)
|
||||
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
|
||||
.body(body),
|
||||
)
|
||||
let res = Response::build(StatusCode::OK)
|
||||
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
|
||||
.body(body);
|
||||
|
||||
Ok::<_, Error>(res)
|
||||
})
|
||||
// No TLS
|
||||
.tcp()
|
||||
})?
|
||||
.run()
|
||||
|
@ -1,32 +1,34 @@
|
||||
use std::io;
|
||||
|
||||
use actix_http::{
|
||||
body::MessageBody, header::HeaderValue, Error, HttpService, Request, Response, StatusCode,
|
||||
body::{BodyStream, MessageBody},
|
||||
header, Error, HttpMessage, HttpService, Request, Response, StatusCode,
|
||||
};
|
||||
use actix_server::Server;
|
||||
use bytes::BytesMut;
|
||||
use futures_util::StreamExt as _;
|
||||
|
||||
async fn handle_request(mut req: Request) -> Result<Response<impl MessageBody>, Error> {
|
||||
let mut body = BytesMut::new();
|
||||
while let Some(item) = req.payload().next().await {
|
||||
body.extend_from_slice(&item?)
|
||||
let mut res = Response::build(StatusCode::OK);
|
||||
|
||||
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)
|
||||
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
|
||||
.body(body))
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
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), || {
|
||||
HttpService::build().finish(handle_request).tcp()
|
||||
HttpService::build()
|
||||
// handles HTTP/1.1 only
|
||||
.h1(handle_request)
|
||||
// No TLS
|
||||
.tcp()
|
||||
})?
|
||||
.run()
|
||||
.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,9 +1,8 @@
|
||||
use std::{convert::Infallible, io};
|
||||
use std::{convert::Infallible, io, time::Duration};
|
||||
|
||||
use actix_http::{
|
||||
header::HeaderValue, HttpMessage, HttpService, Request, Response, StatusCode,
|
||||
};
|
||||
use actix_http::{header::HeaderValue, HttpService, Request, Response, StatusCode};
|
||||
use actix_server::Server;
|
||||
use tracing::info;
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
@ -12,18 +11,18 @@ async fn main() -> io::Result<()> {
|
||||
Server::build()
|
||||
.bind("hello-world", ("127.0.0.1", 8080), || {
|
||||
HttpService::build()
|
||||
.client_timeout(1000)
|
||||
.client_disconnect(1000)
|
||||
.client_request_timeout(Duration::from_secs(1))
|
||||
.client_disconnect_timeout(Duration::from_secs(1))
|
||||
.on_connect_ext(|_, ext| {
|
||||
ext.insert(42u32);
|
||||
})
|
||||
.finish(|req: Request| async move {
|
||||
log::info!("{:?}", req);
|
||||
info!("{:?}", req);
|
||||
|
||||
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();
|
||||
let forty_two = req.conn_data::<u32>().unwrap().to_string();
|
||||
res.insert_header((
|
||||
"x-forty-two",
|
||||
HeaderValue::from_str(&forty_two).unwrap(),
|
||||
|
@ -12,6 +12,7 @@ use actix_http::{body::BodyStream, HttpService, Response};
|
||||
use actix_server::Server;
|
||||
use async_stream::stream;
|
||||
use bytes::Bytes;
|
||||
use tracing::info;
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
@ -21,7 +22,7 @@ async fn main() -> io::Result<()> {
|
||||
.bind("streaming-error", ("127.0.0.1", 8080), || {
|
||||
HttpService::build()
|
||||
.finish(|req| async move {
|
||||
log::info!("{:?}", req);
|
||||
info!("{:?}", req);
|
||||
let res = Response::ok();
|
||||
|
||||
Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! {
|
||||
|
@ -17,6 +17,7 @@ use actix_server::Server;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use bytestring::ByteString;
|
||||
use futures_core::{ready, Stream};
|
||||
use tracing::{info, trace};
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
@ -34,13 +35,13 @@ async fn main() -> io::Result<()> {
|
||||
}
|
||||
|
||||
async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error> {
|
||||
log::info!("handshaking");
|
||||
info!("handshaking");
|
||||
let mut res = ws::handshake(req.head())?;
|
||||
|
||||
// handshake will always fail under HTTP/2
|
||||
|
||||
log::info!("responding");
|
||||
Ok(res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))?)
|
||||
info!("responding");
|
||||
res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))
|
||||
}
|
||||
|
||||
struct Heartbeat {
|
||||
@ -61,7 +62,7 @@ impl Stream for Heartbeat {
|
||||
type Item = Result<Bytes, Error>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
log::trace!("poll");
|
||||
trace!("poll");
|
||||
|
||||
ready!(self.as_mut().interval.poll_tick(cx));
|
||||
|
||||
|
@ -80,7 +80,7 @@ mod tests {
|
||||
use futures_core::ready;
|
||||
use futures_util::{stream, FutureExt as _};
|
||||
use pin_project_lite::pin_project;
|
||||
use static_assertions::{assert_impl_all, assert_not_impl_all};
|
||||
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
||||
|
||||
use super::*;
|
||||
use crate::body::to_bytes;
|
||||
@ -91,10 +91,10 @@ mod tests {
|
||||
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);
|
||||
assert_not_impl_any!(BodyStream<stream::Empty<Bytes>>: MessageBody);
|
||||
assert_not_impl_any!(BodyStream<stream::Repeat<Bytes>>: MessageBody);
|
||||
// crate::Error is not Clone
|
||||
assert_not_impl_all!(BodyStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
|
||||
assert_not_impl_any!(BodyStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn skips_empty_chunks() {
|
||||
|
@ -31,7 +31,7 @@ impl fmt::Debug for BoxBodyInner {
|
||||
}
|
||||
|
||||
impl BoxBody {
|
||||
/// Same as `MessageBody::boxed`.
|
||||
/// 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.
|
||||
@ -105,14 +105,13 @@ impl MessageBody for BoxBody {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use static_assertions::{assert_impl_all, assert_not_impl_all};
|
||||
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
||||
|
||||
use super::*;
|
||||
use crate::body::to_bytes;
|
||||
|
||||
assert_impl_all!(BoxBody: MessageBody, fmt::Debug, Unpin);
|
||||
|
||||
assert_not_impl_all!(BoxBody: Send, Sync, Unpin);
|
||||
assert_impl_all!(BoxBody: fmt::Debug, MessageBody, Unpin);
|
||||
assert_not_impl_any!(BoxBody: Send, Sync);
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn nested_boxed_body() {
|
||||
|
@ -10,6 +10,17 @@ use super::{BodySize, BoxBody, MessageBody};
|
||||
use crate::Error;
|
||||
|
||||
pin_project! {
|
||||
/// An "either" type specialized for body types.
|
||||
///
|
||||
/// It is common, in middleware especially, to conditionally return an inner service's unknown/
|
||||
/// generic body `B` type or return early with a new response. This type's "right" variant
|
||||
/// defaults to `BoxBody` since error responses are the common case.
|
||||
///
|
||||
/// For example, middleware will often have `type Response = ServiceResponse<EitherBody<B>>`.
|
||||
/// This means that the inner service's response body type maps to the `Left` variant and the
|
||||
/// middleware's own error responses use the default `Right` variant of `BoxBody`. Of course,
|
||||
/// there's no reason it couldn't use `EitherBody<B, String>` instead if its alternative
|
||||
/// responses have a known type.
|
||||
#[project = EitherBodyProj]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EitherBody<L, R = BoxBody> {
|
||||
@ -22,7 +33,10 @@ pin_project! {
|
||||
}
|
||||
|
||||
impl<L> EitherBody<L, BoxBody> {
|
||||
/// Creates new `EitherBody` using left variant and boxed right variant.
|
||||
/// Creates new `EitherBody` left variant with a boxed right variant.
|
||||
///
|
||||
/// If the expected `R` type will be inferred and is not `BoxBody` then use the
|
||||
/// [`left`](Self::left) constructor instead.
|
||||
#[inline]
|
||||
pub fn new(body: L) -> Self {
|
||||
Self::Left { body }
|
||||
|
@ -14,8 +14,44 @@ use pin_project_lite::pin_project;
|
||||
|
||||
use super::{BodySize, BoxBody};
|
||||
|
||||
/// An interface types that can converted to bytes and used as response bodies.
|
||||
// TODO: examples
|
||||
/// 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`](https://docs.rs/bytestring/1).
|
||||
/// - 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 {
|
||||
/// The type of error that will be returned if streaming body fails.
|
||||
///
|
||||
@ -29,7 +65,22 @@ pub trait MessageBody {
|
||||
fn size(&self) -> BodySize;
|
||||
|
||||
/// Attempt to pull out the next chunk of body bytes.
|
||||
// TODO: expand documentation
|
||||
///
|
||||
/// # 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(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
@ -37,7 +88,7 @@ pub trait MessageBody {
|
||||
|
||||
/// Try to convert into the complete chunk of body bytes.
|
||||
///
|
||||
/// Implement this method if the entire body can be trivially extracted. This is useful for
|
||||
/// 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
|
||||
@ -54,7 +105,11 @@ pub trait MessageBody {
|
||||
Err(self)
|
||||
}
|
||||
|
||||
/// Converts this body into `BoxBody`.
|
||||
/// 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
|
||||
@ -426,6 +481,7 @@ mod tests {
|
||||
assert_poll_next_none!(pl);
|
||||
}
|
||||
|
||||
#[allow(clippy::let_unit_value)]
|
||||
#[actix_rt::test]
|
||||
async fn test_unit() {
|
||||
let pl = ();
|
||||
|
@ -1,4 +1,9 @@
|
||||
//! Traits and structures to aid consuming and writing HTTP payloads.
|
||||
//!
|
||||
//! "Body" and "payload" are used somewhat interchangeably in this documentation.
|
||||
|
||||
// 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.
|
||||
|
||||
mod body_stream;
|
||||
mod boxed;
|
||||
|
@ -10,9 +10,12 @@ use super::{BodySize, MessageBody};
|
||||
|
||||
/// Body type for responses that forbid payloads.
|
||||
///
|
||||
/// Distinct from an empty response which would contain a Content-Length header.
|
||||
///
|
||||
/// 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;
|
||||
|
@ -76,7 +76,7 @@ mod tests {
|
||||
use actix_rt::pin;
|
||||
use actix_utils::future::poll_fn;
|
||||
use futures_util::stream;
|
||||
use static_assertions::{assert_impl_all, assert_not_impl_all};
|
||||
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
||||
|
||||
use super::*;
|
||||
use crate::body::to_bytes;
|
||||
@ -87,10 +87,10 @@ mod tests {
|
||||
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);
|
||||
assert_not_impl_any!(SizedStream<stream::Empty<Bytes>>: MessageBody);
|
||||
assert_not_impl_any!(SizedStream<stream::Repeat<Bytes>>: MessageBody);
|
||||
// crate::Error is not Clone
|
||||
assert_not_impl_all!(SizedStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
|
||||
assert_not_impl_any!(SizedStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn skips_empty_chunks() {
|
||||
|
@ -1,25 +1,22 @@
|
||||
use std::{fmt, marker::PhantomData, net, rc::Rc};
|
||||
use std::{fmt, marker::PhantomData, net, rc::Rc, time::Duration};
|
||||
|
||||
use actix_codec::Framed;
|
||||
use actix_service::{IntoServiceFactory, Service, ServiceFactory};
|
||||
|
||||
use crate::{
|
||||
body::{BoxBody, MessageBody},
|
||||
config::{KeepAlive, ServiceConfig},
|
||||
h1::{self, ExpectHandler, H1Service, UpgradeHandler},
|
||||
h2::H2Service,
|
||||
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
|
||||
/// builder-like pattern.
|
||||
/// This type can construct an instance of [`HttpService`] through a builder-like pattern.
|
||||
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
|
||||
keep_alive: KeepAlive,
|
||||
client_timeout: u64,
|
||||
client_disconnect: u64,
|
||||
client_request_timeout: Duration,
|
||||
client_disconnect_timeout: Duration,
|
||||
secure: bool,
|
||||
local_addr: Option<net::SocketAddr>,
|
||||
expect: X,
|
||||
@ -28,22 +25,23 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
|
||||
_phantom: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
|
||||
impl<T, S> Default for HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
{
|
||||
/// Create instance of `ServiceConfigBuilder`
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
fn default() -> Self {
|
||||
HttpServiceBuilder {
|
||||
keep_alive: KeepAlive::Timeout(5),
|
||||
client_timeout: 5000,
|
||||
client_disconnect: 0,
|
||||
// ServiceConfig parts (make sure defaults match)
|
||||
keep_alive: KeepAlive::default(),
|
||||
client_request_timeout: Duration::from_secs(5),
|
||||
client_disconnect_timeout: Duration::ZERO,
|
||||
secure: false,
|
||||
local_addr: None,
|
||||
|
||||
// dispatcher parts
|
||||
expect: ExpectHandler,
|
||||
upgrade: None,
|
||||
on_connect_ext: None,
|
||||
@ -65,9 +63,11 @@ where
|
||||
U::Error: fmt::Display,
|
||||
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 {
|
||||
self.keep_alive = val.into();
|
||||
self
|
||||
@ -85,33 +85,45 @@ where
|
||||
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
|
||||
/// the entire set headers within this time, the request is terminated with
|
||||
/// the 408 (Request Time-out) error.
|
||||
/// Defines a timeout for reading client request header. If the client does not transmit the
|
||||
/// request head within this duration, the connection is terminated with a `408 Request Timeout`
|
||||
/// 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.
|
||||
pub fn client_timeout(mut self, val: u64) -> Self {
|
||||
self.client_timeout = val;
|
||||
/// By default, the client timeout is 5 seconds.
|
||||
pub fn client_request_timeout(mut self, dur: Duration) -> Self {
|
||||
self.client_request_timeout = dur;
|
||||
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
|
||||
/// 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.
|
||||
pub fn client_disconnect(mut self, val: u64) -> Self {
|
||||
self.client_disconnect = val;
|
||||
/// By default, the disconnect timeout is disabled.
|
||||
pub fn client_disconnect_timeout(mut self, dur: Duration) -> Self {
|
||||
self.client_disconnect_timeout = dur;
|
||||
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.
|
||||
///
|
||||
/// Service get called with request that contains `EXPECT` header.
|
||||
@ -126,8 +138,8 @@ where
|
||||
{
|
||||
HttpServiceBuilder {
|
||||
keep_alive: self.keep_alive,
|
||||
client_timeout: self.client_timeout,
|
||||
client_disconnect: self.client_disconnect,
|
||||
client_request_timeout: self.client_request_timeout,
|
||||
client_disconnect_timeout: self.client_disconnect_timeout,
|
||||
secure: self.secure,
|
||||
local_addr: self.local_addr,
|
||||
expect: expect.into_factory(),
|
||||
@ -150,8 +162,8 @@ where
|
||||
{
|
||||
HttpServiceBuilder {
|
||||
keep_alive: self.keep_alive,
|
||||
client_timeout: self.client_timeout,
|
||||
client_disconnect: self.client_disconnect,
|
||||
client_request_timeout: self.client_request_timeout,
|
||||
client_disconnect_timeout: self.client_disconnect_timeout,
|
||||
secure: self.secure,
|
||||
local_addr: self.local_addr,
|
||||
expect: self.expect,
|
||||
@ -185,8 +197,8 @@ where
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
self.client_timeout,
|
||||
self.client_disconnect,
|
||||
self.client_request_timeout,
|
||||
self.client_disconnect_timeout,
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
);
|
||||
@ -198,7 +210,9 @@ where
|
||||
}
|
||||
|
||||
/// 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")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
||||
pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>
|
||||
where
|
||||
F: IntoServiceFactory<S, Request>,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
@ -209,13 +223,14 @@ where
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
self.client_timeout,
|
||||
self.client_disconnect,
|
||||
self.client_request_timeout,
|
||||
self.client_disconnect_timeout,
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
);
|
||||
|
||||
H2Service::with_config(cfg, service.into_factory()).on_connect_ext(self.on_connect_ext)
|
||||
crate::h2::H2Service::with_config(cfg, service.into_factory())
|
||||
.on_connect_ext(self.on_connect_ext)
|
||||
}
|
||||
|
||||
/// Finish service configuration and create `HttpService` instance.
|
||||
@ -230,8 +245,8 @@ where
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
self.client_timeout,
|
||||
self.client_disconnect,
|
||||
self.client_request_timeout,
|
||||
self.client_disconnect_timeout,
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
);
|
||||
|
@ -1,106 +1,59 @@
|
||||
use std::{
|
||||
cell::Cell,
|
||||
fmt::{self, Write},
|
||||
net,
|
||||
rc::Rc,
|
||||
time::{Duration, SystemTime},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use actix_rt::{
|
||||
task::JoinHandle,
|
||||
time::{interval, sleep_until, Instant, Sleep},
|
||||
};
|
||||
use bytes::BytesMut;
|
||||
|
||||
/// "Sun, 06 Nov 1994 08:49:37 GMT".len()
|
||||
pub(crate) const DATE_VALUE_LENGTH: usize = 29;
|
||||
use crate::{date::DateService, KeepAlive};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
/// Server keep-alive setting
|
||||
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
|
||||
/// HTTP service configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServiceConfig(Rc<Inner>);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Inner {
|
||||
keep_alive: Option<Duration>,
|
||||
client_timeout: u64,
|
||||
client_disconnect: u64,
|
||||
ka_enabled: bool,
|
||||
keep_alive: KeepAlive,
|
||||
client_request_timeout: Duration,
|
||||
client_disconnect_timeout: Duration,
|
||||
secure: bool,
|
||||
local_addr: Option<std::net::SocketAddr>,
|
||||
date_service: DateService,
|
||||
}
|
||||
|
||||
impl Clone for ServiceConfig {
|
||||
fn clone(&self) -> Self {
|
||||
ServiceConfig(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ServiceConfig {
|
||||
fn default() -> Self {
|
||||
Self::new(KeepAlive::Timeout(5), 0, 0, false, None)
|
||||
Self::new(
|
||||
KeepAlive::default(),
|
||||
Duration::from_secs(5),
|
||||
Duration::ZERO,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceConfig {
|
||||
/// Create instance of `ServiceConfig`
|
||||
/// Create instance of `ServiceConfig`.
|
||||
pub fn new(
|
||||
keep_alive: KeepAlive,
|
||||
client_timeout: u64,
|
||||
client_disconnect: u64,
|
||||
client_request_timeout: Duration,
|
||||
client_disconnect_timeout: Duration,
|
||||
secure: bool,
|
||||
local_addr: Option<net::SocketAddr>,
|
||||
) -> 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 {
|
||||
keep_alive,
|
||||
ka_enabled,
|
||||
client_timeout,
|
||||
client_disconnect,
|
||||
keep_alive: keep_alive.normalize(),
|
||||
client_request_timeout,
|
||||
client_disconnect_timeout,
|
||||
secure,
|
||||
local_addr,
|
||||
date_service: DateService::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Returns true if connection is secure (HTTPS)
|
||||
/// Returns `true` if connection is secure (i.e., using TLS / HTTPS).
|
||||
#[inline]
|
||||
pub fn secure(&self) -> bool {
|
||||
self.0.secure
|
||||
@ -114,235 +67,97 @@ impl ServiceConfig {
|
||||
self.0.local_addr
|
||||
}
|
||||
|
||||
/// Keep alive duration if configured.
|
||||
/// Connection keep-alive setting.
|
||||
#[inline]
|
||||
pub fn keep_alive(&self) -> Option<Duration> {
|
||||
pub fn keep_alive(&self) -> KeepAlive {
|
||||
self.0.keep_alive
|
||||
}
|
||||
|
||||
/// Return state of connection keep-alive functionality
|
||||
#[inline]
|
||||
pub fn keep_alive_enabled(&self) -> bool {
|
||||
self.0.ka_enabled
|
||||
}
|
||||
|
||||
/// Client timeout for first request.
|
||||
#[inline]
|
||||
pub fn client_timer(&self) -> Option<Sleep> {
|
||||
let delay_time = self.0.client_timeout;
|
||||
if delay_time != 0 {
|
||||
Some(sleep_until(self.now() + Duration::from_millis(delay_time)))
|
||||
} else {
|
||||
None
|
||||
/// Creates a time object representing the deadline for this connection's keep-alive period, if
|
||||
/// 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() {
|
||||
KeepAlive::Timeout(dur) => Some(self.now() + dur),
|
||||
KeepAlive::Os => None,
|
||||
KeepAlive::Disabled => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Client timeout for first request.
|
||||
pub fn client_timer_expire(&self) -> Option<Instant> {
|
||||
let delay = self.0.client_timeout;
|
||||
if delay != 0 {
|
||||
Some(self.now() + Duration::from_millis(delay))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
/// Creates a time object representing the deadline for the client to finish sending the head of
|
||||
/// its first request.
|
||||
///
|
||||
/// Returns `None` if this `ServiceConfig was` constructed with `client_request_timeout: 0`.
|
||||
pub fn client_request_deadline(&self) -> Option<Instant> {
|
||||
let timeout = self.0.client_request_timeout;
|
||||
(timeout != Duration::ZERO).then(|| self.now() + timeout)
|
||||
}
|
||||
|
||||
/// Client disconnect timer
|
||||
pub fn client_disconnect_timer(&self) -> Option<Instant> {
|
||||
let delay = self.0.client_disconnect;
|
||||
if delay != 0 {
|
||||
Some(self.now() + Duration::from_millis(delay))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
/// Creates a time object representing the deadline for the client to disconnect.
|
||||
pub fn client_disconnect_deadline(&self) -> Option<Instant> {
|
||||
let timeout = self.0.client_disconnect_timeout;
|
||||
(timeout != Duration::ZERO).then(|| self.now() + timeout)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
self.0.date_service.now()
|
||||
}
|
||||
|
||||
/// Writes date header to `dst` buffer.
|
||||
///
|
||||
/// Low-level method that utilizes the built-in efficient date service, requiring fewer syscalls
|
||||
/// than normal. Note that a CRLF (`\r\n`) is included in what is written.
|
||||
#[doc(hidden)]
|
||||
pub fn set_date(&self, dst: &mut BytesMut) {
|
||||
let mut buf: [u8; 39] = [0; 39];
|
||||
buf[..6].copy_from_slice(b"date: ");
|
||||
pub fn write_date_header(&self, dst: &mut BytesMut, camel_case: bool) {
|
||||
let mut buf: [u8; 37] = [0; 37];
|
||||
|
||||
buf[..6].copy_from_slice(if camel_case { b"Date: " } else { b"date: " });
|
||||
|
||||
self.0
|
||||
.date_service
|
||||
.set_date(|date| buf[6..35].copy_from_slice(&date.bytes));
|
||||
buf[35..].copy_from_slice(b"\r\n\r\n");
|
||||
.with_date(|date| buf[6..35].copy_from_slice(&date.bytes));
|
||||
|
||||
buf[35..].copy_from_slice(b"\r\n");
|
||||
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
|
||||
.date_service
|
||||
.set_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, "{}", 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.
|
||||
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.
|
||||
/// 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
|
||||
#[cfg(test)]
|
||||
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.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
.with_date(|date| dst.extend_from_slice(&date.bytes));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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]
|
||||
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;
|
||||
|
||||
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();
|
||||
|
||||
sleep_until(Instant::now() + Duration::from_secs(2)).await;
|
||||
sleep_until((Instant::now() + Duration::from_secs(2)).into()).await;
|
||||
yield_now().await;
|
||||
|
||||
let now2 = settings.now();
|
||||
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);
|
||||
|
||||
@ -395,11 +210,27 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
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);
|
||||
settings.set_date(&mut buf1);
|
||||
settings.write_date_header(&mut buf1, false);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#[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 futures_core::{ready, Stream};
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
use brotli2::write::BrotliDecoder;
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
use flate2::write::{GzDecoder, ZlibDecoder};
|
||||
|
||||
@ -22,7 +19,7 @@ use zstd::stream::write::Decoder as ZstdDecoder;
|
||||
|
||||
use crate::{
|
||||
encoding::Writer,
|
||||
error::{BlockingError, PayloadError},
|
||||
error::PayloadError,
|
||||
header::{ContentEncoding, HeaderMap, CONTENT_ENCODING},
|
||||
};
|
||||
|
||||
@ -47,17 +44,20 @@ where
|
||||
pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> {
|
||||
let decoder = match encoding {
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
ContentEncoding::Br => Some(ContentDecoder::Br(Box::new(BrotliDecoder::new(
|
||||
Writer::new(),
|
||||
)))),
|
||||
ContentEncoding::Brotli => Some(ContentDecoder::Brotli(Box::new(
|
||||
brotli::DecompressorWriter::new(Writer::new(), 8_096),
|
||||
))),
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
|
||||
ZlibDecoder::new(Writer::new()),
|
||||
))),
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(GzDecoder::new(
|
||||
Writer::new(),
|
||||
)))),
|
||||
|
||||
#[cfg(feature = "compress-zstd")]
|
||||
ContentEncoding::Zstd => Some(ContentDecoder::Zstd(Box::new(
|
||||
ZstdDecoder::new(Writer::new()).expect(
|
||||
@ -101,8 +101,12 @@ where
|
||||
|
||||
loop {
|
||||
if let Some(ref mut fut) = this.fut {
|
||||
let (chunk, decoder) =
|
||||
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
|
||||
let (chunk, decoder) = ready!(Pin::new(fut).poll(cx)).map_err(|_| {
|
||||
PayloadError::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Blocking task was cancelled unexpectedly",
|
||||
))
|
||||
})??;
|
||||
|
||||
*this.decoder = Some(decoder);
|
||||
this.fut.take();
|
||||
@ -162,10 +166,13 @@ where
|
||||
enum ContentDecoder {
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
Deflate(Box<ZlibDecoder<Writer>>),
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
Gzip(Box<GzDecoder<Writer>>),
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
Br(Box<BrotliDecoder<Writer>>),
|
||||
Brotli(Box<brotli::DecompressorWriter<Writer>>),
|
||||
|
||||
// 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`
|
||||
#[cfg(feature = "compress-zstd")]
|
||||
@ -176,7 +183,7 @@ impl ContentDecoder {
|
||||
fn feed_eof(&mut self) -> io::Result<Option<Bytes>> {
|
||||
match self {
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
ContentDecoder::Br(ref mut decoder) => match decoder.flush() {
|
||||
ContentDecoder::Brotli(ref mut decoder) => match decoder.flush() {
|
||||
Ok(()) => {
|
||||
let b = decoder.get_mut().take();
|
||||
|
||||
@ -234,7 +241,7 @@ impl ContentDecoder {
|
||||
fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> {
|
||||
match self {
|
||||
#[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(_) => {
|
||||
decoder.flush()?;
|
||||
let b = decoder.get_mut().take();
|
||||
|
@ -14,19 +14,16 @@ use derive_more::Display;
|
||||
use futures_core::ready;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
use brotli2::write::BrotliEncoder;
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
use flate2::write::{GzEncoder, ZlibEncoder};
|
||||
|
||||
use tracing::trace;
|
||||
#[cfg(feature = "compress-zstd")]
|
||||
use zstd::stream::write::Encoder as ZstdEncoder;
|
||||
|
||||
use super::Writer;
|
||||
use crate::{
|
||||
body::{self, BodySize, MessageBody},
|
||||
error::BlockingError,
|
||||
header::{self, ContentEncoding, HeaderValue, CONTENT_ENCODING},
|
||||
ResponseHead, StatusCode,
|
||||
};
|
||||
@ -56,25 +53,24 @@ impl<B: MessageBody> Encoder<B> {
|
||||
}
|
||||
|
||||
pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self {
|
||||
let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|
||||
|| head.status == StatusCode::SWITCHING_PROTOCOLS
|
||||
|| head.status == StatusCode::NO_CONTENT
|
||||
|| encoding == ContentEncoding::Identity
|
||||
|| encoding == ContentEncoding::Auto);
|
||||
|
||||
// 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::NO_CONTENT
|
||||
|| encoding == ContentEncoding::Identity);
|
||||
|
||||
let body = match body.try_into_bytes() {
|
||||
Ok(body) => EncoderBody::Full { body },
|
||||
Err(body) => EncoderBody::Stream { body },
|
||||
};
|
||||
|
||||
if can_encode {
|
||||
// Modify response body only if encoder is set
|
||||
if let Some(enc) = ContentEncoder::encoder(encoding) {
|
||||
if should_encode {
|
||||
// wrap body only if encoder is feature-enabled
|
||||
if let Some(enc) = ContentEncoder::select(encoding) {
|
||||
update_head(encoding, head);
|
||||
|
||||
return Encoder {
|
||||
@ -169,6 +165,7 @@ where
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
let mut this = self.project();
|
||||
|
||||
loop {
|
||||
if *this.eof {
|
||||
return Poll::Ready(None);
|
||||
@ -176,7 +173,12 @@ where
|
||||
|
||||
if let Some(ref mut fut) = this.fut {
|
||||
let mut encoder = ready!(Pin::new(fut).poll(cx))
|
||||
.map_err(|_| EncoderError::Blocking(BlockingError))?
|
||||
.map_err(|_| {
|
||||
EncoderError::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Blocking task was cancelled unexpectedly",
|
||||
))
|
||||
})?
|
||||
.map_err(EncoderError::Io)?;
|
||||
|
||||
let chunk = encoder.take();
|
||||
@ -252,10 +254,10 @@ where
|
||||
}
|
||||
|
||||
fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
|
||||
head.headers_mut().insert(
|
||||
header::CONTENT_ENCODING,
|
||||
HeaderValue::from_static(encoding.as_str()),
|
||||
);
|
||||
head.headers_mut()
|
||||
.insert(header::CONTENT_ENCODING, encoding.to_header_value());
|
||||
head.headers_mut()
|
||||
.append(header::VARY, HeaderValue::from_static("accept-encoding"));
|
||||
|
||||
head.no_chunking(false);
|
||||
}
|
||||
@ -268,7 +270,7 @@ enum ContentEncoder {
|
||||
Gzip(GzEncoder<Writer>),
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
Br(BrotliEncoder<Writer>),
|
||||
Brotli(Box<brotli::CompressorWriter<Writer>>),
|
||||
|
||||
// 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`.
|
||||
@ -277,7 +279,7 @@ enum ContentEncoder {
|
||||
}
|
||||
|
||||
impl ContentEncoder {
|
||||
fn encoder(encoding: ContentEncoding) -> Option<Self> {
|
||||
fn select(encoding: ContentEncoding) -> Option<Self> {
|
||||
match encoding {
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new(
|
||||
@ -292,9 +294,7 @@ impl ContentEncoder {
|
||||
))),
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
ContentEncoding::Br => {
|
||||
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3)))
|
||||
}
|
||||
ContentEncoding::Brotli => Some(ContentEncoder::Brotli(new_brotli_compressor())),
|
||||
|
||||
#[cfg(feature = "compress-zstd")]
|
||||
ContentEncoding::Zstd => {
|
||||
@ -310,7 +310,7 @@ impl ContentEncoder {
|
||||
pub(crate) fn take(&mut self) -> Bytes {
|
||||
match *self {
|
||||
#[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")]
|
||||
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
|
||||
@ -326,8 +326,8 @@ impl ContentEncoder {
|
||||
fn finish(self) -> Result<Bytes, io::Error> {
|
||||
match self {
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
ContentEncoder::Br(encoder) => match encoder.finish() {
|
||||
Ok(writer) => Ok(writer.buf.freeze()),
|
||||
ContentEncoder::Brotli(mut encoder) => match encoder.flush() {
|
||||
Ok(()) => Ok(encoder.into_inner().buf.freeze()),
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
|
||||
@ -354,7 +354,7 @@ impl ContentEncoder {
|
||||
fn write(&mut self, data: &[u8]) -> Result<(), io::Error> {
|
||||
match *self {
|
||||
#[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(()),
|
||||
Err(err) => {
|
||||
trace!("Error decoding br encoding: {}", err);
|
||||
@ -392,15 +392,24 @@ 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)]
|
||||
#[non_exhaustive]
|
||||
pub enum EncoderError {
|
||||
/// Wrapped body stream error.
|
||||
#[display(fmt = "body")]
|
||||
Body(Box<dyn StdError>),
|
||||
|
||||
#[display(fmt = "blocking")]
|
||||
Blocking(BlockingError),
|
||||
|
||||
/// Generic I/O error.
|
||||
#[display(fmt = "io")]
|
||||
Io(io::Error),
|
||||
}
|
||||
@ -409,7 +418,6 @@ impl StdError for EncoderError {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match self {
|
||||
EncoderError::Body(err) => Some(&**err),
|
||||
EncoderError::Blocking(err) => Some(err),
|
||||
EncoderError::Io(err) => Some(err),
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Err
|
||||
use derive_more::{Display, Error, From};
|
||||
use http::{uri::InvalidUri, StatusCode};
|
||||
|
||||
use crate::{body::BoxBody, ws, Response};
|
||||
use crate::{body::BoxBody, Response};
|
||||
|
||||
pub use http::Error as HttpError;
|
||||
|
||||
@ -51,7 +51,7 @@ impl Error {
|
||||
Self::new(Kind::SendResponse)
|
||||
}
|
||||
|
||||
#[allow(unused)] // reserved for future use (TODO: remove allow when being used)
|
||||
#[allow(unused)] // available for future use
|
||||
pub(crate) fn new_io() -> Self {
|
||||
Self::new(Kind::Io)
|
||||
}
|
||||
@ -61,6 +61,7 @@ impl Error {
|
||||
Self::new(Kind::Encoder)
|
||||
}
|
||||
|
||||
#[allow(unused)] // used with `ws` feature flag
|
||||
pub(crate) fn new_ws() -> Self {
|
||||
Self::new(Kind::Ws)
|
||||
}
|
||||
@ -107,8 +108,10 @@ pub(crate) enum Kind {
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// TODO: more detail
|
||||
f.write_str("actix_http::Error")
|
||||
f.debug_struct("actix_http::Error")
|
||||
.field("kind", &self.inner.kind)
|
||||
.field("cause", &self.inner.cause)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,14 +142,16 @@ impl From<HttpError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ws::HandshakeError> for Error {
|
||||
fn from(err: ws::HandshakeError) -> Self {
|
||||
#[cfg(feature = "ws")]
|
||||
impl From<crate::ws::HandshakeError> for Error {
|
||||
fn from(err: crate::ws::HandshakeError) -> Self {
|
||||
Self::new_ws().with_cause(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ws::ProtocolError> for Error {
|
||||
fn from(err: ws::ProtocolError) -> Self {
|
||||
#[cfg(feature = "ws")]
|
||||
impl From<crate::ws::ProtocolError> for Error {
|
||||
fn from(err: crate::ws::ProtocolError) -> Self {
|
||||
Self::new_ws().with_cause(err)
|
||||
}
|
||||
}
|
||||
@ -247,11 +252,6 @@ impl From<ParseError> for Response<BoxBody> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of errors that can occur running blocking tasks in thread pool.
|
||||
#[derive(Debug, Display, Error)]
|
||||
#[display(fmt = "Blocking thread pool is gone")]
|
||||
pub struct BlockingError;
|
||||
|
||||
/// A set of errors that can occur during payload parsing.
|
||||
#[derive(Debug, Display)]
|
||||
#[non_exhaustive]
|
||||
@ -276,8 +276,9 @@ pub enum PayloadError {
|
||||
UnknownLength,
|
||||
|
||||
/// HTTP/2 payload error.
|
||||
#[cfg(feature = "http2")]
|
||||
#[display(fmt = "{}", _0)]
|
||||
Http2Payload(h2::Error),
|
||||
Http2Payload(::h2::Error),
|
||||
|
||||
/// Generic I/O error.
|
||||
#[display(fmt = "{}", _0)]
|
||||
@ -288,18 +289,21 @@ impl std::error::Error for PayloadError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
PayloadError::Incomplete(None) => None,
|
||||
PayloadError::Incomplete(Some(err)) => Some(err as &dyn std::error::Error),
|
||||
PayloadError::Incomplete(Some(err)) => Some(err),
|
||||
PayloadError::EncodingCorrupted => None,
|
||||
PayloadError::Overflow => None,
|
||||
PayloadError::UnknownLength => None,
|
||||
PayloadError::Http2Payload(err) => Some(err as &dyn std::error::Error),
|
||||
PayloadError::Io(err) => Some(err as &dyn std::error::Error),
|
||||
#[cfg(feature = "http2")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
||||
PayloadError::Http2Payload(err) => Some(err),
|
||||
PayloadError::Io(err) => Some(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<h2::Error> for PayloadError {
|
||||
fn from(err: h2::Error) -> Self {
|
||||
#[cfg(feature = "http2")]
|
||||
impl From<::h2::Error> for PayloadError {
|
||||
fn from(err: ::h2::Error) -> Self {
|
||||
PayloadError::Http2Payload(err)
|
||||
}
|
||||
}
|
||||
@ -316,15 +320,6 @@ impl From<io::Error> for PayloadError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlockingError> for PayloadError {
|
||||
fn from(_: BlockingError) -> Self {
|
||||
PayloadError::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Operation is canceled",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PayloadError> for Error {
|
||||
fn from(err: PayloadError) -> Self {
|
||||
Self::new_payload().with_cause(err)
|
||||
@ -333,6 +328,7 @@ impl From<PayloadError> for Error {
|
||||
|
||||
/// A set of errors that can occur during dispatching HTTP requests.
|
||||
#[derive(Debug, Display, From)]
|
||||
#[non_exhaustive]
|
||||
pub enum DispatchError {
|
||||
/// Service error.
|
||||
#[display(fmt = "Service Error")]
|
||||
@ -355,6 +351,8 @@ pub enum DispatchError {
|
||||
|
||||
/// HTTP/2 error.
|
||||
#[display(fmt = "{}", _0)]
|
||||
#[cfg(feature = "http2")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
||||
H2(h2::Error),
|
||||
|
||||
/// The first request did not complete within the specified timeout.
|
||||
@ -365,6 +363,10 @@ pub enum DispatchError {
|
||||
#[display(fmt = "Connection shutdown timeout")]
|
||||
DisconnectTimeout,
|
||||
|
||||
/// Handler dropped payload before reading EOF.
|
||||
#[display(fmt = "Handler dropped payload before reading EOF")]
|
||||
HandlerDroppedPayload,
|
||||
|
||||
/// Internal error.
|
||||
#[display(fmt = "Internal error")]
|
||||
InternalError,
|
||||
@ -373,12 +375,14 @@ pub enum DispatchError {
|
||||
impl StdError for DispatchError {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
@ -386,6 +390,7 @@ impl StdError for DispatchError {
|
||||
|
||||
/// A set of error that can occur during parsing content type.
|
||||
#[derive(Debug, Display, Error)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[non_exhaustive]
|
||||
pub enum ContentTypeError {
|
||||
/// Can not parse content type
|
||||
@ -397,28 +402,14 @@ pub enum ContentTypeError {
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http::{Error as HttpError, StatusCode};
|
||||
use std::io;
|
||||
|
||||
use http::{Error as HttpError, StatusCode};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_into_response() {
|
||||
let resp: Response<BoxBody> = ParseError::Incomplete.into();
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::{io, task::Poll};
|
||||
|
||||
use bytes::{Buf as _, Bytes, BytesMut};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
macro_rules! byte (
|
||||
($rdr:ident) => ({
|
||||
@ -14,7 +15,7 @@ macro_rules! byte (
|
||||
})
|
||||
);
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum ChunkedState {
|
||||
Size,
|
||||
SizeLws,
|
||||
@ -76,7 +77,7 @@ impl ChunkedState {
|
||||
Poll::Ready(Ok(ChunkedState::Size))
|
||||
}
|
||||
None => {
|
||||
log::debug!("chunk size would overflow u64");
|
||||
debug!("chunk size would overflow u64");
|
||||
Poll::Ready(Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Invalid chunk size line: Size is too big",
|
||||
@ -124,7 +125,7 @@ impl ChunkedState {
|
||||
rem: &mut u64,
|
||||
buf: &mut Option<Bytes>,
|
||||
) -> Poll<Result<ChunkedState, io::Error>> {
|
||||
log::trace!("Chunked read, remaining={:?}", rem);
|
||||
trace!("Chunked read, remaining={:?}", rem);
|
||||
|
||||
let len = rdr.len() as u64;
|
||||
if len == 0 {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::io;
|
||||
use std::{fmt, io};
|
||||
|
||||
use actix_codec::{Decoder, Encoder};
|
||||
use bitflags::bitflags;
|
||||
@ -17,9 +17,9 @@ use crate::{
|
||||
|
||||
bitflags! {
|
||||
struct Flags: u8 {
|
||||
const HEAD = 0b0000_0001;
|
||||
const KEEPALIVE_ENABLED = 0b0000_1000;
|
||||
const STREAM = 0b0001_0000;
|
||||
const HEAD = 0b0000_0001;
|
||||
const KEEP_ALIVE_ENABLED = 0b0000_1000;
|
||||
const STREAM = 0b0001_0000;
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ struct ClientCodecInner {
|
||||
decoder: decoder::MessageDecoder<ResponseHead>,
|
||||
payload: Option<PayloadDecoder>,
|
||||
version: Version,
|
||||
ctype: ConnectionType,
|
||||
conn_type: ConnectionType,
|
||||
|
||||
// encoder part
|
||||
flags: Flags,
|
||||
@ -51,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 {
|
||||
/// Create HTTP/1 codec.
|
||||
///
|
||||
/// `keepalive_enabled` how response `connection` header get generated.
|
||||
pub fn new(config: ServiceConfig) -> Self {
|
||||
let flags = if config.keep_alive_enabled() {
|
||||
Flags::KEEPALIVE_ENABLED
|
||||
let flags = if config.keep_alive().enabled() {
|
||||
Flags::KEEP_ALIVE_ENABLED
|
||||
} else {
|
||||
Flags::empty()
|
||||
};
|
||||
|
||||
ClientCodec {
|
||||
inner: ClientCodecInner {
|
||||
config,
|
||||
decoder: decoder::MessageDecoder::default(),
|
||||
payload: None,
|
||||
version: Version::HTTP_11,
|
||||
ctype: ConnectionType::Close,
|
||||
conn_type: ConnectionType::Close,
|
||||
|
||||
flags,
|
||||
encoder: encoder::MessageEncoder::default(),
|
||||
@ -77,12 +86,12 @@ impl ClientCodec {
|
||||
|
||||
/// Check if request is upgrade
|
||||
pub fn upgrade(&self) -> bool {
|
||||
self.inner.ctype == ConnectionType::Upgrade
|
||||
self.inner.conn_type == ConnectionType::Upgrade
|
||||
}
|
||||
|
||||
/// Check if last response is keep-alive
|
||||
pub fn keepalive(&self) -> bool {
|
||||
self.inner.ctype == ConnectionType::KeepAlive
|
||||
pub fn keep_alive(&self) -> bool {
|
||||
self.inner.conn_type == ConnectionType::KeepAlive
|
||||
}
|
||||
|
||||
/// Check last request's message type
|
||||
@ -104,8 +113,8 @@ impl ClientCodec {
|
||||
|
||||
impl ClientPayloadCodec {
|
||||
/// Check if last response is keep-alive
|
||||
pub fn keepalive(&self) -> bool {
|
||||
self.inner.ctype == ConnectionType::KeepAlive
|
||||
pub fn keep_alive(&self) -> bool {
|
||||
self.inner.conn_type == ConnectionType::KeepAlive
|
||||
}
|
||||
|
||||
/// Transform payload codec to a message codec
|
||||
@ -119,15 +128,18 @@ impl Decoder for ClientCodec {
|
||||
type Error = ParseError;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set");
|
||||
debug_assert!(
|
||||
self.inner.payload.is_none(),
|
||||
"Payload decoder should not be set"
|
||||
);
|
||||
|
||||
if let Some((req, payload)) = self.inner.decoder.decode(src)? {
|
||||
if let Some(ctype) = req.conn_type() {
|
||||
if let Some(conn_type) = req.conn_type() {
|
||||
// do not use peer's keep-alive
|
||||
self.inner.ctype = if ctype == ConnectionType::KeepAlive {
|
||||
self.inner.ctype
|
||||
self.inner.conn_type = if conn_type == ConnectionType::KeepAlive {
|
||||
self.inner.conn_type
|
||||
} else {
|
||||
ctype
|
||||
conn_type
|
||||
};
|
||||
}
|
||||
|
||||
@ -192,9 +204,9 @@ impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
|
||||
.set(Flags::HEAD, head.as_ref().method == Method::HEAD);
|
||||
|
||||
// connection status
|
||||
inner.ctype = match head.as_ref().connection_type() {
|
||||
inner.conn_type = match head.as_ref().connection_type() {
|
||||
ConnectionType::KeepAlive => {
|
||||
if inner.flags.contains(Flags::KEEPALIVE_ENABLED) {
|
||||
if inner.flags.contains(Flags::KEEP_ALIVE_ENABLED) {
|
||||
ConnectionType::KeepAlive
|
||||
} else {
|
||||
ConnectionType::Close
|
||||
@ -211,7 +223,7 @@ impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
|
||||
false,
|
||||
inner.version,
|
||||
length,
|
||||
inner.ctype,
|
||||
inner.conn_type,
|
||||
&inner.config,
|
||||
)?;
|
||||
}
|
||||
|
@ -15,9 +15,9 @@ use crate::{
|
||||
|
||||
bitflags! {
|
||||
struct Flags: u8 {
|
||||
const HEAD = 0b0000_0001;
|
||||
const KEEPALIVE_ENABLED = 0b0000_0010;
|
||||
const STREAM = 0b0000_0100;
|
||||
const HEAD = 0b0000_0001;
|
||||
const KEEP_ALIVE_ENABLED = 0b0000_0010;
|
||||
const STREAM = 0b0000_0100;
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,7 +42,9 @@ impl Default for Codec {
|
||||
|
||||
impl fmt::Debug for Codec {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,8 +53,8 @@ impl Codec {
|
||||
///
|
||||
/// `keepalive_enabled` how response `connection` header get generated.
|
||||
pub fn new(config: ServiceConfig) -> Self {
|
||||
let flags = if config.keep_alive_enabled() {
|
||||
Flags::KEEPALIVE_ENABLED
|
||||
let flags = if config.keep_alive().enabled() {
|
||||
Flags::KEEP_ALIVE_ENABLED
|
||||
} else {
|
||||
Flags::empty()
|
||||
};
|
||||
@ -76,14 +78,14 @@ impl Codec {
|
||||
|
||||
/// Check if last response is keep-alive.
|
||||
#[inline]
|
||||
pub fn keepalive(&self) -> bool {
|
||||
pub fn keep_alive(&self) -> bool {
|
||||
self.conn_type == ConnectionType::KeepAlive
|
||||
}
|
||||
|
||||
/// Check if keep-alive enabled on server level.
|
||||
#[inline]
|
||||
pub fn keepalive_enabled(&self) -> bool {
|
||||
self.flags.contains(Flags::KEEPALIVE_ENABLED)
|
||||
pub fn keep_alive_enabled(&self) -> bool {
|
||||
self.flags.contains(Flags::KEEP_ALIVE_ENABLED)
|
||||
}
|
||||
|
||||
/// Check last request's message type.
|
||||
@ -123,11 +125,13 @@ impl Decoder for Codec {
|
||||
self.flags.set(Flags::HEAD, head.method == Method::HEAD);
|
||||
self.version = head.version;
|
||||
self.conn_type = head.connection_type();
|
||||
|
||||
if self.conn_type == ConnectionType::KeepAlive
|
||||
&& !self.flags.contains(Flags::KEEPALIVE_ENABLED)
|
||||
&& !self.flags.contains(Flags::KEEP_ALIVE_ENABLED)
|
||||
{
|
||||
self.conn_type = ConnectionType::Close
|
||||
}
|
||||
|
||||
match payload {
|
||||
PayloadType::None => self.payload = None,
|
||||
PayloadType::Payload(pl) => self.payload = Some(pl),
|
||||
@ -179,9 +183,11 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
|
||||
&self.config,
|
||||
)?;
|
||||
}
|
||||
|
||||
Message::Chunk(Some(bytes)) => {
|
||||
self.encoder.encode_chunk(bytes.as_ref(), dst)?;
|
||||
}
|
||||
|
||||
Message::Chunk(None) => {
|
||||
self.encoder.encode_eof(dst)?;
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ use http::{
|
||||
header::{self, HeaderName, HeaderValue},
|
||||
Method, StatusCode, Uri, Version,
|
||||
};
|
||||
use log::{debug, error, trace};
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
use super::chunked::ChunkedState;
|
||||
use crate::{error::ParseError, header::HeaderMap, ConnectionType, Request, ResponseHead};
|
||||
@ -46,6 +46,23 @@ pub(crate) enum PayloadLength {
|
||||
None,
|
||||
}
|
||||
|
||||
impl PayloadLength {
|
||||
/// Returns true if variant is `None`.
|
||||
fn is_none(&self) -> bool {
|
||||
matches!(self, Self::None)
|
||||
}
|
||||
|
||||
/// Returns true if variant is represents zero-length (not none) payload.
|
||||
fn is_zero(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
PayloadLength::Payload(PayloadType::Payload(PayloadDecoder {
|
||||
kind: Kind::Length(0)
|
||||
}))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait MessageType: Sized {
|
||||
fn set_connection_type(&mut self, conn_type: Option<ConnectionType>);
|
||||
|
||||
@ -59,6 +76,7 @@ pub(crate) trait MessageType: Sized {
|
||||
&mut self,
|
||||
slice: &Bytes,
|
||||
raw_headers: &[HeaderIndex],
|
||||
version: Version,
|
||||
) -> Result<PayloadLength, ParseError> {
|
||||
let mut ka = None;
|
||||
let mut has_upgrade_websocket = false;
|
||||
@ -87,21 +105,23 @@ pub(crate) trait MessageType: Sized {
|
||||
return Err(ParseError::Header);
|
||||
}
|
||||
|
||||
header::CONTENT_LENGTH => match value.to_str() {
|
||||
Ok(s) if s.trim().starts_with('+') => {
|
||||
debug!("illegal Content-Length: {:?}", s);
|
||||
header::CONTENT_LENGTH => match value.to_str().map(str::trim) {
|
||||
Ok(val) if val.starts_with('+') => {
|
||||
debug!("illegal Content-Length: {:?}", val);
|
||||
return Err(ParseError::Header);
|
||||
}
|
||||
Ok(s) => {
|
||||
if let Ok(len) = s.parse::<u64>() {
|
||||
if len != 0 {
|
||||
content_length = Some(len);
|
||||
}
|
||||
|
||||
Ok(val) => {
|
||||
if let Ok(len) = val.parse::<u64>() {
|
||||
// accept 0 lengths here and remove them in `decode` after all
|
||||
// headers have been processed to prevent request smuggling issues
|
||||
content_length = Some(len);
|
||||
} else {
|
||||
debug!("illegal Content-Length: {:?}", s);
|
||||
debug!("illegal Content-Length: {:?}", val);
|
||||
return Err(ParseError::Header);
|
||||
}
|
||||
}
|
||||
|
||||
Err(_) => {
|
||||
debug!("illegal Content-Length: {:?}", value);
|
||||
return Err(ParseError::Header);
|
||||
@ -114,22 +134,23 @@ pub(crate) trait MessageType: Sized {
|
||||
return Err(ParseError::Header);
|
||||
}
|
||||
|
||||
header::TRANSFER_ENCODING => {
|
||||
header::TRANSFER_ENCODING if version == Version::HTTP_11 => {
|
||||
seen_te = true;
|
||||
|
||||
if let Ok(s) = value.to_str().map(str::trim) {
|
||||
if s.eq_ignore_ascii_case("chunked") {
|
||||
if let Ok(val) = value.to_str().map(str::trim) {
|
||||
if val.eq_ignore_ascii_case("chunked") {
|
||||
chunked = true;
|
||||
} else if s.eq_ignore_ascii_case("identity") {
|
||||
} else if val.eq_ignore_ascii_case("identity") {
|
||||
// allow silently since multiple TE headers are already checked
|
||||
} else {
|
||||
debug!("illegal Transfer-Encoding: {:?}", s);
|
||||
debug!("illegal Transfer-Encoding: {:?}", val);
|
||||
return Err(ParseError::Header);
|
||||
}
|
||||
} else {
|
||||
return Err(ParseError::Header);
|
||||
}
|
||||
}
|
||||
|
||||
// connection keep-alive state
|
||||
header::CONNECTION => {
|
||||
ka = if let Ok(conn) = value.to_str().map(str::trim) {
|
||||
@ -146,6 +167,7 @@ pub(crate) trait MessageType: Sized {
|
||||
None
|
||||
};
|
||||
}
|
||||
|
||||
header::UPGRADE => {
|
||||
if let Ok(val) = value.to_str().map(str::trim) {
|
||||
if val.eq_ignore_ascii_case("websocket") {
|
||||
@ -153,19 +175,23 @@ pub(crate) trait MessageType: Sized {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header::EXPECT => {
|
||||
let bytes = value.as_bytes();
|
||||
if bytes.len() >= 4 && &bytes[0..4] == b"100-" {
|
||||
expect = true;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
headers.append(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
self.set_connection_type(ka);
|
||||
|
||||
if expect {
|
||||
self.set_expect()
|
||||
}
|
||||
@ -209,15 +235,16 @@ impl MessageType for Request {
|
||||
|
||||
let (len, method, uri, ver, h_len) = {
|
||||
// SAFETY:
|
||||
// Create an uninitialized array of `MaybeUninit`. The `assume_init` is
|
||||
// safe because the type we are claiming to have initialized here is a
|
||||
// bunch of `MaybeUninit`s, which do not require initialization.
|
||||
// Create an uninitialized array of `MaybeUninit`. The `assume_init` is safe because the
|
||||
// type we are claiming to have initialized here is a bunch of `MaybeUninit`s, which
|
||||
// do not require initialization.
|
||||
let mut parsed = unsafe {
|
||||
MaybeUninit::<[MaybeUninit<httparse::Header<'_>>; MAX_HEADERS]>::uninit()
|
||||
.assume_init()
|
||||
};
|
||||
|
||||
let mut req = httparse::Request::new(&mut []);
|
||||
|
||||
match req.parse_with_uninit_headers(src, &mut parsed)? {
|
||||
httparse::Status::Complete(len) => {
|
||||
let method = Method::from_bytes(req.method.unwrap().as_bytes())
|
||||
@ -232,6 +259,7 @@ impl MessageType for Request {
|
||||
|
||||
(len, method, uri, version, req.headers.len())
|
||||
}
|
||||
|
||||
httparse::Status::Partial => {
|
||||
return if src.len() >= MAX_BUFFER_SIZE {
|
||||
trace!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
||||
@ -247,7 +275,22 @@ impl MessageType for Request {
|
||||
let mut msg = Request::new();
|
||||
|
||||
// convert headers
|
||||
let length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?;
|
||||
let mut length =
|
||||
msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?;
|
||||
|
||||
// disallow HTTP/1.0 POST requests that do not contain a Content-Length headers
|
||||
// see https://datatracker.ietf.org/doc/html/rfc1945#section-7.2.2
|
||||
if ver == Version::HTTP_10 && method == Method::POST && length.is_none() {
|
||||
debug!("no Content-Length specified for HTTP/1.0 POST request");
|
||||
return Err(ParseError::Header);
|
||||
}
|
||||
|
||||
// Remove CL value if 0 now that all headers and HTTP/1.0 special cases are processed.
|
||||
// Protects against some request smuggling attacks.
|
||||
// See https://github.com/actix/actix-web/issues/2767.
|
||||
if length.is_zero() {
|
||||
length = PayloadLength::None;
|
||||
}
|
||||
|
||||
// payload decoder
|
||||
let decoder = match length {
|
||||
@ -291,22 +334,35 @@ impl MessageType for ResponseHead {
|
||||
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
||||
|
||||
let (len, ver, status, h_len) = {
|
||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
|
||||
// SAFETY:
|
||||
// Create an uninitialized array of `MaybeUninit`. The `assume_init` is safe because the
|
||||
// type we are claiming to have initialized here is a bunch of `MaybeUninit`s, which
|
||||
// do not require initialization.
|
||||
let mut parsed = unsafe {
|
||||
MaybeUninit::<[MaybeUninit<httparse::Header<'_>>; MAX_HEADERS]>::uninit()
|
||||
.assume_init()
|
||||
};
|
||||
|
||||
let mut res = httparse::Response::new(&mut parsed);
|
||||
match res.parse(src)? {
|
||||
let mut res = httparse::Response::new(&mut []);
|
||||
|
||||
let mut config = httparse::ParserConfig::default();
|
||||
config.allow_spaces_after_header_name_in_responses(true);
|
||||
|
||||
match config.parse_response_with_uninit_headers(&mut res, src, &mut parsed)? {
|
||||
httparse::Status::Complete(len) => {
|
||||
let version = if res.version.unwrap() == 1 {
|
||||
Version::HTTP_11
|
||||
} else {
|
||||
Version::HTTP_10
|
||||
};
|
||||
|
||||
let status = StatusCode::from_u16(res.code.unwrap())
|
||||
.map_err(|_| ParseError::Status)?;
|
||||
HeaderIndex::record(src, res.headers, &mut headers);
|
||||
|
||||
(len, version, status, res.headers.len())
|
||||
}
|
||||
|
||||
httparse::Status::Partial => {
|
||||
return if src.len() >= MAX_BUFFER_SIZE {
|
||||
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
||||
@ -322,7 +378,15 @@ impl MessageType for ResponseHead {
|
||||
msg.version = ver;
|
||||
|
||||
// convert headers
|
||||
let length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?;
|
||||
let mut length =
|
||||
msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?;
|
||||
|
||||
// Remove CL value if 0 now that all headers and HTTP/1.0 special cases are processed.
|
||||
// Protects against some request smuggling attacks.
|
||||
// See https://github.com/actix/actix-web/issues/2767.
|
||||
if length.is_zero() {
|
||||
length = PayloadLength::None;
|
||||
}
|
||||
|
||||
// message payload
|
||||
let decoder = if let PayloadLength::Payload(pl) = length {
|
||||
@ -358,9 +422,6 @@ pub(crate) const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex {
|
||||
pub(crate) const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] =
|
||||
[EMPTY_HEADER_INDEX; MAX_HEADERS];
|
||||
|
||||
pub(crate) const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] =
|
||||
[httparse::EMPTY_HEADER; MAX_HEADERS];
|
||||
|
||||
impl HeaderIndex {
|
||||
pub(crate) fn record(
|
||||
bytes: &[u8],
|
||||
@ -379,61 +440,64 @@ impl HeaderIndex {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// Http payload item
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Chunk type yielded while decoding a payload.
|
||||
pub enum PayloadItem {
|
||||
Chunk(Bytes),
|
||||
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*
|
||||
/// include a Content-Length header.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// If a message body does not use `Transfer-Encoding`, it should include a `Content-Length`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PayloadDecoder {
|
||||
kind: Kind,
|
||||
}
|
||||
|
||||
impl PayloadDecoder {
|
||||
/// Constructs a fixed-length payload decoder.
|
||||
pub fn length(x: u64) -> PayloadDecoder {
|
||||
PayloadDecoder {
|
||||
kind: Kind::Length(x),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a chunked encoding decoder.
|
||||
pub fn chunked() -> PayloadDecoder {
|
||||
PayloadDecoder {
|
||||
kind: Kind::Chunked(ChunkedState::Size, 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an decoder that yields chunks until the stream returns EOF.
|
||||
pub fn eof() -> PayloadDecoder {
|
||||
PayloadDecoder { kind: Kind::Eof }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum Kind {
|
||||
/// A Reader used when a Content-Length header is passed with a positive
|
||||
/// integer.
|
||||
/// A reader used when a `Content-Length` header is passed with a positive integer.
|
||||
Length(u64),
|
||||
/// A Reader used when Transfer-Encoding is `chunked`.
|
||||
|
||||
/// A reader used when `Transfer-Encoding` is `chunked`.
|
||||
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
|
||||
/// `Request` to be made with both `Content-Length` and
|
||||
/// `Transfer-Encoding: chunked` missing, as explained from the spec:
|
||||
/// Note: This should only used for `Response`s. It is illegal for a `Request` to be made
|
||||
/// without either of `Content-Length` and `Transfer-Encoding: chunked` missing, as explained
|
||||
/// in [RFC 7230 §3.3.3]:
|
||||
///
|
||||
/// > If a Transfer-Encoding header field is present in a response and
|
||||
/// > the chunked transfer coding is not the final encoding, the
|
||||
/// > message body length is determined by reading the connection until
|
||||
/// > it is closed by the server. If a Transfer-Encoding header field
|
||||
/// > is present in a request and the chunked transfer coding is not
|
||||
/// > the final encoding, the message body length cannot be determined
|
||||
/// > reliably; the server MUST respond with the 400 (Bad Request)
|
||||
/// > status code and then close the connection.
|
||||
/// > If a Transfer-Encoding header field is present in a response and the chunked transfer
|
||||
/// > coding is not the final encoding, the message body length is determined by reading the
|
||||
/// > connection until it is closed by the server. If a Transfer-Encoding header field is
|
||||
/// > present in a request and the chunked transfer coding is not the final encoding, the
|
||||
/// > message body length cannot be determined 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,
|
||||
}
|
||||
|
||||
@ -463,6 +527,7 @@ impl Decoder for PayloadDecoder {
|
||||
Ok(Some(PayloadItem::Chunk(buf)))
|
||||
}
|
||||
}
|
||||
|
||||
Kind::Chunked(ref mut state, ref mut size) => {
|
||||
loop {
|
||||
let mut buf = None;
|
||||
@ -488,6 +553,7 @@ impl Decoder for PayloadDecoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kind::Eof => {
|
||||
if src.is_empty() {
|
||||
Ok(None)
|
||||
@ -589,14 +655,100 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_post() {
|
||||
let mut buf = BytesMut::from("POST /test2 HTTP/1.0\r\n\r\n");
|
||||
fn parse_h09_reject() {
|
||||
let mut buf = BytesMut::from(
|
||||
"GET /test1 HTTP/0.9\r\n\
|
||||
\r\n",
|
||||
);
|
||||
|
||||
let mut reader = MessageDecoder::<Request>::default();
|
||||
reader.decode(&mut buf).unwrap_err();
|
||||
|
||||
let mut buf = BytesMut::from(
|
||||
"POST /test2 HTTP/0.9\r\n\
|
||||
Content-Length: 3\r\n\
|
||||
\r\n
|
||||
abc",
|
||||
);
|
||||
|
||||
let mut reader = MessageDecoder::<Request>::default();
|
||||
reader.decode(&mut buf).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_h10_get() {
|
||||
let mut buf = BytesMut::from(
|
||||
"GET /test1 HTTP/1.0\r\n\
|
||||
\r\n",
|
||||
);
|
||||
|
||||
let mut reader = MessageDecoder::<Request>::default();
|
||||
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
||||
assert_eq!(req.version(), Version::HTTP_10);
|
||||
assert_eq!(*req.method(), Method::GET);
|
||||
assert_eq!(req.path(), "/test1");
|
||||
|
||||
let mut buf = BytesMut::from(
|
||||
"GET /test2 HTTP/1.0\r\n\
|
||||
Content-Length: 0\r\n\
|
||||
\r\n",
|
||||
);
|
||||
|
||||
let mut reader = MessageDecoder::<Request>::default();
|
||||
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
||||
assert_eq!(req.version(), Version::HTTP_10);
|
||||
assert_eq!(*req.method(), Method::GET);
|
||||
assert_eq!(req.path(), "/test2");
|
||||
|
||||
let mut buf = BytesMut::from(
|
||||
"GET /test3 HTTP/1.0\r\n\
|
||||
Content-Length: 3\r\n\
|
||||
\r\n
|
||||
abc",
|
||||
);
|
||||
|
||||
let mut reader = MessageDecoder::<Request>::default();
|
||||
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
||||
assert_eq!(req.version(), Version::HTTP_10);
|
||||
assert_eq!(*req.method(), Method::GET);
|
||||
assert_eq!(req.path(), "/test3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_h10_post() {
|
||||
let mut buf = BytesMut::from(
|
||||
"POST /test1 HTTP/1.0\r\n\
|
||||
Content-Length: 3\r\n\
|
||||
\r\n\
|
||||
abc",
|
||||
);
|
||||
|
||||
let mut reader = MessageDecoder::<Request>::default();
|
||||
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
||||
assert_eq!(req.version(), Version::HTTP_10);
|
||||
assert_eq!(*req.method(), Method::POST);
|
||||
assert_eq!(req.path(), "/test1");
|
||||
|
||||
let mut buf = BytesMut::from(
|
||||
"POST /test2 HTTP/1.0\r\n\
|
||||
Content-Length: 0\r\n\
|
||||
\r\n",
|
||||
);
|
||||
|
||||
let mut reader = MessageDecoder::<Request>::default();
|
||||
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
||||
assert_eq!(req.version(), Version::HTTP_10);
|
||||
assert_eq!(*req.method(), Method::POST);
|
||||
assert_eq!(req.path(), "/test2");
|
||||
|
||||
let mut buf = BytesMut::from(
|
||||
"POST /test3 HTTP/1.0\r\n\
|
||||
\r\n",
|
||||
);
|
||||
|
||||
let mut reader = MessageDecoder::<Request>::default();
|
||||
let err = reader.decode(&mut buf).unwrap_err();
|
||||
assert!(err.to_string().contains("Header"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -692,121 +844,98 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_conn_default_1_0() {
|
||||
let mut buf = BytesMut::from("GET /test HTTP/1.0\r\n\r\n");
|
||||
let req = parse_ready!(&mut buf);
|
||||
|
||||
let req = parse_ready!(&mut BytesMut::from("GET /test HTTP/1.0\r\n\r\n"));
|
||||
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conn_default_1_1() {
|
||||
let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n");
|
||||
let req = parse_ready!(&mut buf);
|
||||
|
||||
let req = parse_ready!(&mut BytesMut::from("GET /test HTTP/1.1\r\n\r\n"));
|
||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conn_close() {
|
||||
let mut buf = BytesMut::from(
|
||||
let req = parse_ready!(&mut BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
connection: close\r\n\r\n",
|
||||
);
|
||||
let req = parse_ready!(&mut buf);
|
||||
|
||||
));
|
||||
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
||||
|
||||
let mut buf = BytesMut::from(
|
||||
let req = parse_ready!(&mut BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
connection: Close\r\n\r\n",
|
||||
);
|
||||
let req = parse_ready!(&mut buf);
|
||||
|
||||
));
|
||||
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conn_close_1_0() {
|
||||
let mut buf = BytesMut::from(
|
||||
let req = parse_ready!(&mut BytesMut::from(
|
||||
"GET /test HTTP/1.0\r\n\
|
||||
connection: close\r\n\r\n",
|
||||
);
|
||||
|
||||
let req = parse_ready!(&mut buf);
|
||||
|
||||
));
|
||||
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conn_keep_alive_1_0() {
|
||||
let mut buf = BytesMut::from(
|
||||
let req = parse_ready!(&mut BytesMut::from(
|
||||
"GET /test HTTP/1.0\r\n\
|
||||
connection: keep-alive\r\n\r\n",
|
||||
);
|
||||
let req = parse_ready!(&mut buf);
|
||||
|
||||
));
|
||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||
|
||||
let mut buf = BytesMut::from(
|
||||
let req = parse_ready!(&mut BytesMut::from(
|
||||
"GET /test HTTP/1.0\r\n\
|
||||
connection: Keep-Alive\r\n\r\n",
|
||||
);
|
||||
let req = parse_ready!(&mut buf);
|
||||
|
||||
));
|
||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conn_keep_alive_1_1() {
|
||||
let mut buf = BytesMut::from(
|
||||
let req = parse_ready!(&mut BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
connection: keep-alive\r\n\r\n",
|
||||
);
|
||||
let req = parse_ready!(&mut buf);
|
||||
|
||||
));
|
||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conn_other_1_0() {
|
||||
let mut buf = BytesMut::from(
|
||||
let req = parse_ready!(&mut BytesMut::from(
|
||||
"GET /test HTTP/1.0\r\n\
|
||||
connection: other\r\n\r\n",
|
||||
);
|
||||
let req = parse_ready!(&mut buf);
|
||||
|
||||
));
|
||||
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conn_other_1_1() {
|
||||
let mut buf = BytesMut::from(
|
||||
let req = parse_ready!(&mut BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
connection: other\r\n\r\n",
|
||||
);
|
||||
let req = parse_ready!(&mut buf);
|
||||
|
||||
));
|
||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conn_upgrade() {
|
||||
let mut buf = BytesMut::from(
|
||||
let req = parse_ready!(&mut BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
upgrade: websockets\r\n\
|
||||
connection: upgrade\r\n\r\n",
|
||||
);
|
||||
let req = parse_ready!(&mut buf);
|
||||
));
|
||||
|
||||
assert!(req.upgrade());
|
||||
assert_eq!(req.head().connection_type(), ConnectionType::Upgrade);
|
||||
|
||||
let mut buf = BytesMut::from(
|
||||
let req = parse_ready!(&mut BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
upgrade: Websockets\r\n\
|
||||
connection: Upgrade\r\n\r\n",
|
||||
);
|
||||
let req = parse_ready!(&mut buf);
|
||||
));
|
||||
|
||||
assert!(req.upgrade());
|
||||
assert_eq!(req.head().connection_type(), ConnectionType::Upgrade);
|
||||
@ -814,59 +943,62 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_conn_upgrade_connect_method() {
|
||||
let mut buf = BytesMut::from(
|
||||
let req = parse_ready!(&mut BytesMut::from(
|
||||
"CONNECT /test HTTP/1.1\r\n\
|
||||
content-type: text/plain\r\n\r\n",
|
||||
);
|
||||
let req = parse_ready!(&mut buf);
|
||||
));
|
||||
|
||||
assert!(req.upgrade());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_headers_content_length_err_1() {
|
||||
let mut buf = BytesMut::from(
|
||||
fn test_headers_bad_content_length() {
|
||||
// string CL
|
||||
expect_parse_err!(&mut BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
content-length: line\r\n\r\n",
|
||||
);
|
||||
));
|
||||
|
||||
expect_parse_err!(&mut buf)
|
||||
// negative CL
|
||||
expect_parse_err!(&mut BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
content-length: -1\r\n\r\n",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_headers_content_length_err_2() {
|
||||
fn octal_ish_cl_parsed_as_decimal() {
|
||||
let mut buf = BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
content-length: -1\r\n\r\n",
|
||||
"POST /test HTTP/1.1\r\n\
|
||||
content-length: 011\r\n\r\n",
|
||||
);
|
||||
|
||||
expect_parse_err!(&mut buf);
|
||||
let mut reader = MessageDecoder::<Request>::default();
|
||||
let (_req, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
||||
assert!(matches!(
|
||||
pl,
|
||||
PayloadType::Payload(pl) if pl == PayloadDecoder::length(11)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_header() {
|
||||
let mut buf = BytesMut::from(
|
||||
expect_parse_err!(&mut BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
test line\r\n\r\n",
|
||||
);
|
||||
|
||||
expect_parse_err!(&mut buf);
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_name() {
|
||||
let mut buf = BytesMut::from(
|
||||
expect_parse_err!(&mut BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
test[]: line\r\n\r\n",
|
||||
);
|
||||
|
||||
expect_parse_err!(&mut buf);
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_request_bad_status_line() {
|
||||
let mut buf = BytesMut::from("getpath \r\n\r\n");
|
||||
expect_parse_err!(&mut buf);
|
||||
expect_parse_err!(&mut BytesMut::from("getpath \r\n\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -906,11 +1038,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_http_request_parser_utf8() {
|
||||
let mut buf = BytesMut::from(
|
||||
let req = parse_ready!(&mut BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
x-test: тест\r\n\r\n",
|
||||
);
|
||||
let req = parse_ready!(&mut buf);
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
req.headers().get("x-test").unwrap().as_bytes(),
|
||||
@ -920,24 +1051,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_http_request_parser_two_slashes() {
|
||||
let mut buf = BytesMut::from("GET //path HTTP/1.1\r\n\r\n");
|
||||
let req = parse_ready!(&mut buf);
|
||||
|
||||
let req = parse_ready!(&mut BytesMut::from("GET //path HTTP/1.1\r\n\r\n"));
|
||||
assert_eq!(req.path(), "//path");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_request_parser_bad_method() {
|
||||
let mut buf = BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n");
|
||||
|
||||
expect_parse_err!(&mut buf);
|
||||
expect_parse_err!(&mut BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_request_parser_bad_version() {
|
||||
let mut buf = BytesMut::from("GET //get HT/11\r\n\r\n");
|
||||
|
||||
expect_parse_err!(&mut buf);
|
||||
expect_parse_err!(&mut BytesMut::from("GET //get HT/11\r\n\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -954,29 +1079,66 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn hrs_multiple_content_length() {
|
||||
let mut buf = BytesMut::from(
|
||||
expect_parse_err!(&mut BytesMut::from(
|
||||
"GET / HTTP/1.1\r\n\
|
||||
Host: example.com\r\n\
|
||||
Content-Length: 4\r\n\
|
||||
Content-Length: 2\r\n\
|
||||
\r\n\
|
||||
abcd",
|
||||
);
|
||||
));
|
||||
|
||||
expect_parse_err!(&mut buf);
|
||||
expect_parse_err!(&mut BytesMut::from(
|
||||
"GET / HTTP/1.1\r\n\
|
||||
Host: example.com\r\n\
|
||||
Content-Length: 0\r\n\
|
||||
Content-Length: 2\r\n\
|
||||
\r\n\
|
||||
ab",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hrs_content_length_plus() {
|
||||
let mut buf = BytesMut::from(
|
||||
expect_parse_err!(&mut BytesMut::from(
|
||||
"GET / HTTP/1.1\r\n\
|
||||
Host: example.com\r\n\
|
||||
Content-Length: +3\r\n\
|
||||
\r\n\
|
||||
000",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hrs_te_http10() {
|
||||
// in HTTP/1.0 transfer encoding is ignored and must therefore contain a CL header
|
||||
|
||||
expect_parse_err!(&mut BytesMut::from(
|
||||
"POST / HTTP/1.0\r\n\
|
||||
Host: example.com\r\n\
|
||||
Transfer-Encoding: chunked\r\n\
|
||||
\r\n\
|
||||
3\r\n\
|
||||
aaa\r\n\
|
||||
0\r\n\
|
||||
",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hrs_cl_and_te_http10() {
|
||||
// in HTTP/1.0 transfer encoding is simply ignored so it's fine to have both
|
||||
|
||||
let mut buf = BytesMut::from(
|
||||
"GET / HTTP/1.0\r\n\
|
||||
Host: example.com\r\n\
|
||||
Content-Length: 3\r\n\
|
||||
Transfer-Encoding: chunked\r\n\
|
||||
\r\n\
|
||||
000",
|
||||
);
|
||||
|
||||
expect_parse_err!(&mut buf);
|
||||
parse_ready!(&mut buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
File diff suppressed because it is too large
Load Diff
976
actix-http/src/h1/dispatcher_tests.rs
Normal file
976
actix-http/src/h1/dispatcher_tests.rs
Normal file
@ -0,0 +1,976 @@
|
||||
use std::{future::Future, str, task::Poll, time::Duration};
|
||||
|
||||
use actix_rt::{pin, 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 drop_payload_service(
|
||||
) -> impl Service<Request, Response = Response<&'static str>, Error = Error> {
|
||||
fn_service(|mut req: Request| async move {
|
||||
let _ = req.take_payload();
|
||||
Ok::<_, Error>(Response::with_body(StatusCode::OK, "payload dropped"))
|
||||
})
|
||||
}
|
||||
|
||||
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(),
|
||||
);
|
||||
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(),
|
||||
);
|
||||
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 = http_msg(
|
||||
r"
|
||||
HTTP/1.1 200 OK
|
||||
content-length: 5
|
||||
connection: close
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC
|
||||
|
||||
/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(),
|
||||
);
|
||||
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(),
|
||||
);
|
||||
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(),
|
||||
);
|
||||
|
||||
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(),
|
||||
);
|
||||
|
||||
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(),
|
||||
);
|
||||
|
||||
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\
|
||||
",
|
||||
);
|
||||
|
||||
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\
|
||||
",
|
||||
);
|
||||
|
||||
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\
|
||||
",
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// fix in #2624 reverted temporarily
|
||||
// complete fix tracked in #2745
|
||||
#[ignore]
|
||||
#[actix_rt::test]
|
||||
async fn handler_drop_payload() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let mut buf = TestBuffer::new(http_msg(
|
||||
r"
|
||||
POST /drop-payload HTTP/1.1
|
||||
Content-Length: 3
|
||||
|
||||
abc
|
||||
",
|
||||
));
|
||||
|
||||
let services = HttpFlow::new(
|
||||
drop_payload_service(),
|
||||
ExpectHandler,
|
||||
None::<UpgradeHandler>,
|
||||
);
|
||||
|
||||
let h1 = Dispatcher::new(
|
||||
buf.clone(),
|
||||
services,
|
||||
ServiceConfig::default(),
|
||||
None,
|
||||
OnConnectData::default(),
|
||||
);
|
||||
pin!(h1);
|
||||
|
||||
lazy(|cx| {
|
||||
assert!(h1.as_mut().poll(cx).is_pending());
|
||||
|
||||
// polls: manual
|
||||
assert_eq!(h1.poll_count, 1);
|
||||
|
||||
let mut res = BytesMut::from(buf.take_write_buf().as_ref());
|
||||
stabilize_date_header(&mut res);
|
||||
let res = &res[..];
|
||||
|
||||
let exp = http_msg(
|
||||
r"
|
||||
HTTP/1.1 200 OK
|
||||
content-length: 15
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC
|
||||
|
||||
payload dropped
|
||||
",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
exp,
|
||||
"\nexpected response not in write buffer:\n\
|
||||
response: {:?}\n\
|
||||
expected: {:?}",
|
||||
String::from_utf8_lossy(res),
|
||||
String::from_utf8_lossy(&exp)
|
||||
);
|
||||
|
||||
if let DispatcherStateProj::Normal { inner } = h1.as_mut().project().inner.project() {
|
||||
assert!(inner.state.is_none());
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
lazy(|cx| {
|
||||
// add message that claims to have payload longer than provided
|
||||
buf.extend_read_buf(http_msg(
|
||||
r"
|
||||
POST /drop-payload HTTP/1.1
|
||||
Content-Length: 200
|
||||
|
||||
abc
|
||||
",
|
||||
));
|
||||
|
||||
assert!(h1.as_mut().poll(cx).is_pending());
|
||||
|
||||
// polls: manual => manual
|
||||
assert_eq!(h1.poll_count, 2);
|
||||
|
||||
let mut res = BytesMut::from(buf.take_write_buf().as_ref());
|
||||
stabilize_date_header(&mut res);
|
||||
let res = &res[..];
|
||||
|
||||
// expect response immediately even though request side has not finished reading payload
|
||||
let exp = http_msg(
|
||||
r"
|
||||
HTTP/1.1 200 OK
|
||||
content-length: 15
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC
|
||||
|
||||
payload dropped
|
||||
",
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
lazy(|cx| {
|
||||
assert!(h1.as_mut().poll(cx).is_ready());
|
||||
|
||||
// polls: manual => manual => manual
|
||||
assert_eq!(h1.poll_count, 3);
|
||||
|
||||
let mut res = BytesMut::from(buf.take_write_buf().as_ref());
|
||||
stabilize_date_header(&mut res);
|
||||
let res = &res[..];
|
||||
|
||||
// expect that unrequested error response is sent back since connection could not be cleaned
|
||||
let exp = http_msg(
|
||||
r"
|
||||
HTTP/1.1 500 Internal Server Error
|
||||
content-length: 0
|
||||
connection: close
|
||||
date: Thu, 01 Jan 1970 12:34:56 UTC
|
||||
|
||||
",
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
fn http_msg(msg: impl AsRef<str>) -> BytesMut {
|
||||
let mut msg = msg
|
||||
.as_ref()
|
||||
.trim()
|
||||
.split('\n')
|
||||
.into_iter()
|
||||
.map(|line| [line.trim_start(), "\r"].concat())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
// remove trailing \r
|
||||
msg.pop();
|
||||
|
||||
if !msg.is_empty() && !msg.contains("\r\n\r\n") {
|
||||
msg.push_str("\r\n\r\n");
|
||||
}
|
||||
|
||||
BytesMut::from(msg.as_bytes())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_msg_creates_msg() {
|
||||
assert_eq!(http_msg(r""), "");
|
||||
|
||||
assert_eq!(
|
||||
http_msg(
|
||||
r"
|
||||
POST / HTTP/1.1
|
||||
Content-Length: 3
|
||||
|
||||
abc
|
||||
"
|
||||
),
|
||||
"POST / HTTP/1.1\r\nContent-Length: 3\r\n\r\nabc"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
http_msg(
|
||||
r"
|
||||
GET / HTTP/1.1
|
||||
Content-Length: 3
|
||||
|
||||
"
|
||||
),
|
||||
"GET / HTTP/1.1\r\nContent-Length: 3\r\n\r\n"
|
||||
);
|
||||
}
|
@ -105,7 +105,7 @@ pub(crate) trait MessageType: Sized {
|
||||
}
|
||||
BodySize::Sized(0) if camel_case => dst.put_slice(b"\r\nContent-Length: 0\r\n"),
|
||||
BodySize::Sized(0) => dst.put_slice(b"\r\ncontent-length: 0\r\n"),
|
||||
BodySize::Sized(len) => helpers::write_content_length(len, dst),
|
||||
BodySize::Sized(len) => helpers::write_content_length(len, dst, camel_case),
|
||||
BodySize::None => dst.put_slice(b"\r\n"),
|
||||
}
|
||||
|
||||
@ -152,7 +152,6 @@ pub(crate) trait MessageType: Sized {
|
||||
let k = key.as_str().as_bytes();
|
||||
let k_len = k.len();
|
||||
|
||||
// TODO: drain?
|
||||
for val in value.iter() {
|
||||
let v = val.as_ref();
|
||||
let v_len = v.len();
|
||||
@ -211,14 +210,14 @@ pub(crate) trait MessageType: Sized {
|
||||
dst.advance_mut(pos);
|
||||
}
|
||||
|
||||
// optimized date header, set_date writes \r\n
|
||||
if !has_date {
|
||||
config.set_date(dst);
|
||||
} else {
|
||||
// msg eof
|
||||
dst.extend_from_slice(b"\r\n");
|
||||
// optimized date header, write_date_header writes its own \r\n
|
||||
config.write_date_header(dst, camel_case);
|
||||
}
|
||||
|
||||
// end-of-headers marker
|
||||
dst.extend_from_slice(b"\r\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -258,6 +257,12 @@ impl MessageType for Response<()> {
|
||||
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<()> {
|
||||
let head = self.head();
|
||||
let reason = head.reason().as_bytes();
|
||||
@ -313,16 +318,17 @@ impl MessageType for RequestHeadType {
|
||||
}
|
||||
|
||||
impl<T: MessageType> MessageEncoder<T> {
|
||||
/// Encode message
|
||||
/// Encode chunk.
|
||||
pub fn encode_chunk(&mut self, msg: &[u8], buf: &mut BytesMut) -> io::Result<bool> {
|
||||
self.te.encode(msg, buf)
|
||||
}
|
||||
|
||||
/// Encode eof
|
||||
/// Encode EOF.
|
||||
pub fn encode_eof(&mut self, buf: &mut BytesMut) -> io::Result<()> {
|
||||
self.te.encode_eof(buf)
|
||||
}
|
||||
|
||||
/// Encode message.
|
||||
pub fn encode(
|
||||
&mut self,
|
||||
dst: &mut BytesMut,
|
||||
@ -511,6 +517,7 @@ unsafe fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) {
|
||||
if let Some(c @ b'a'..=b'z') = iter.next() {
|
||||
buffer[index] = c & 0b1101_1111;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
index += 1;
|
||||
@ -522,7 +529,7 @@ mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use bytes::Bytes;
|
||||
use http::header::AUTHORIZATION;
|
||||
use http::header::{AUTHORIZATION, UPGRADE_INSECURE_REQUESTS};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
@ -553,6 +560,9 @@ mod tests {
|
||||
head.headers
|
||||
.insert(CONTENT_TYPE, HeaderValue::from_static("plain/text"));
|
||||
|
||||
head.headers
|
||||
.insert(UPGRADE_INSECURE_REQUESTS, HeaderValue::from_static("1"));
|
||||
|
||||
let mut head = RequestHeadType::Owned(head);
|
||||
|
||||
let _ = head.encode_headers(
|
||||
@ -568,6 +578,7 @@ mod tests {
|
||||
assert!(data.contains("Connection: close\r\n"));
|
||||
assert!(data.contains("Content-Type: plain/text\r\n"));
|
||||
assert!(data.contains("Date: date\r\n"));
|
||||
assert!(data.contains("Upgrade-Insecure-Requests: 1\r\n"));
|
||||
|
||||
let _ = head.encode_headers(
|
||||
&mut bytes,
|
||||
|
@ -7,10 +7,13 @@ mod client;
|
||||
mod codec;
|
||||
mod decoder;
|
||||
mod dispatcher;
|
||||
#[cfg(test)]
|
||||
mod dispatcher_tests;
|
||||
mod encoder;
|
||||
mod expect;
|
||||
mod payload;
|
||||
mod service;
|
||||
mod timer;
|
||||
mod upgrade;
|
||||
mod utils;
|
||||
|
||||
@ -26,9 +29,10 @@ pub use self::utils::SendResponse;
|
||||
#[derive(Debug)]
|
||||
/// Codec message
|
||||
pub enum Message<T> {
|
||||
/// Http message
|
||||
/// HTTP message.
|
||||
Item(T),
|
||||
/// Payload chunk
|
||||
|
||||
/// Payload chunk.
|
||||
Chunk(Option<Bytes>),
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ use crate::error::PayloadError;
|
||||
/// max buffer size 32k
|
||||
pub(crate) const MAX_BUFFER_SIZE: usize = 32_768;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum PayloadStatus {
|
||||
Read,
|
||||
Pause,
|
||||
@ -252,18 +252,15 @@ impl Inner {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||
|
||||
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_not_impl_any!(Payload: Send, Sync);
|
||||
|
||||
assert_impl_all!(Inner: Unpin, Send, Sync);
|
||||
assert_not_impl_any!(Inner: UnwindSafe, RefUnwindSafe);
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_unread_data() {
|
||||
|
@ -13,6 +13,7 @@ use actix_service::{
|
||||
};
|
||||
use actix_utils::future::ready;
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
body::{BoxBody, MessageBody},
|
||||
@ -133,6 +134,7 @@ mod openssl {
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Create OpenSSL based service.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
|
||||
pub fn openssl(
|
||||
self,
|
||||
acceptor: SslAcceptor,
|
||||
@ -195,6 +197,7 @@ mod rustls {
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Create Rustls based service.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
pub fn rustls(
|
||||
self,
|
||||
config: ServerConfig,
|
||||
@ -305,13 +308,13 @@ where
|
||||
Box::pin(async move {
|
||||
let expect = expect
|
||||
.await
|
||||
.map_err(|e| log::error!("Init http expect service error: {:?}", e))?;
|
||||
.map_err(|e| error!("Init http expect service error: {:?}", e))?;
|
||||
|
||||
let upgrade = match upgrade {
|
||||
Some(upgrade) => {
|
||||
let upgrade = upgrade
|
||||
.await
|
||||
.map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?;
|
||||
.map_err(|e| error!("Init http upgrade service error: {:?}", e))?;
|
||||
Some(upgrade)
|
||||
}
|
||||
None => None,
|
||||
@ -319,7 +322,7 @@ where
|
||||
|
||||
let service = service
|
||||
.await
|
||||
.map_err(|e| log::error!("Init http service error: {:?}", e))?;
|
||||
.map_err(|e| error!("Init http service error: {:?}", e))?;
|
||||
|
||||
Ok(H1ServiceHandler::new(
|
||||
cfg,
|
||||
@ -357,7 +360,7 @@ where
|
||||
|
||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self._poll_ready(cx).map_err(|err| {
|
||||
log::error!("HTTP/1 service readiness error: {:?}", err);
|
||||
error!("HTTP/1 service readiness error: {:?}", err);
|
||||
DispatchError::Service(err)
|
||||
})
|
||||
}
|
||||
|
81
actix-http/src/h1/timer.rs
Normal file
81
actix-http/src/h1/timer.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use std::{fmt, future::Future, pin::Pin, task::Context};
|
||||
|
||||
use actix_rt::time::{Instant, Sleep};
|
||||
use tracing::trace;
|
||||
|
||||
#[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) {
|
||||
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) {
|
||||
trace!("trying to clear a disabled timer from line {}", line);
|
||||
}
|
||||
|
||||
if matches!(self, Self::Inactive) {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,13 +19,15 @@ use h2::{
|
||||
server::{Connection, SendResponse},
|
||||
Ping, PingPong,
|
||||
};
|
||||
use log::{error, trace};
|
||||
use pin_project_lite::pin_project;
|
||||
use tracing::{error, trace, warn};
|
||||
|
||||
use crate::{
|
||||
body::{BodySize, BoxBody, MessageBody},
|
||||
config::ServiceConfig,
|
||||
header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING},
|
||||
header::{
|
||||
HeaderName, HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE,
|
||||
},
|
||||
service::HttpFlow,
|
||||
Extensions, OnConnectData, Payload, Request, Response, ResponseHead,
|
||||
};
|
||||
@ -57,15 +59,15 @@ where
|
||||
conn_data: OnConnectData,
|
||||
timer: Option<Pin<Box<Sleep>>>,
|
||||
) -> Self {
|
||||
let ping_pong = config.keep_alive().map(|dur| H2PingPong {
|
||||
let ping_pong = config.keep_alive().duration().map(|dur| H2PingPong {
|
||||
timer: timer
|
||||
.map(|mut timer| {
|
||||
// reset timer if it's received from new function.
|
||||
timer.as_mut().reset(config.now() + dur);
|
||||
// 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,
|
||||
in_flight: false,
|
||||
ping_pong: conn.ping_pong().unwrap(),
|
||||
});
|
||||
|
||||
@ -82,9 +84,14 @@ where
|
||||
}
|
||||
|
||||
struct H2PingPong {
|
||||
timer: Pin<Box<Sleep>>,
|
||||
on_flight: bool,
|
||||
/// Handle to send ping frames from the peer.
|
||||
ping_pong: PingPong,
|
||||
|
||||
/// True when a ping has been sent and is waiting for a reply.
|
||||
in_flight: bool,
|
||||
|
||||
/// Timeout for pong response.
|
||||
timer: Pin<Box<Sleep>>,
|
||||
}
|
||||
|
||||
impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U>
|
||||
@ -150,34 +157,36 @@ 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.
|
||||
if ping_pong.in_flight {
|
||||
// When there is an in-flight ping-pong, poll pong and and keep-alive
|
||||
// timer. On successful 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;
|
||||
ping_pong.in_flight = false;
|
||||
|
||||
let dead_line = this.config.keep_alive_expire().unwrap();
|
||||
ping_pong.timer.as_mut().reset(dead_line);
|
||||
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(()))
|
||||
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.
|
||||
// When there is no in-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_expire().unwrap();
|
||||
ping_pong.timer.as_mut().reset(dead_line);
|
||||
let dead_line = this.config.keep_alive_deadline().unwrap();
|
||||
ping_pong.timer.as_mut().reset(dead_line.into());
|
||||
|
||||
ping_pong.on_flight = true;
|
||||
ping_pong.in_flight = true;
|
||||
}
|
||||
},
|
||||
None => return Poll::Pending,
|
||||
@ -285,13 +294,13 @@ fn prepare_response(
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = match size {
|
||||
BodySize::None | BodySize::Stream => None,
|
||||
match size {
|
||||
BodySize::None | BodySize::Stream => {}
|
||||
|
||||
BodySize::Sized(0) => {
|
||||
#[allow(clippy::declare_interior_mutable_const)]
|
||||
const HV_ZERO: HeaderValue = HeaderValue::from_static("0");
|
||||
res.headers_mut().insert(CONTENT_LENGTH, HV_ZERO)
|
||||
res.headers_mut().insert(CONTENT_LENGTH, HV_ZERO);
|
||||
}
|
||||
|
||||
BodySize::Sized(len) => {
|
||||
@ -300,19 +309,28 @@ fn prepare_response(
|
||||
res.headers_mut().insert(
|
||||
CONTENT_LENGTH,
|
||||
HeaderValue::from_str(buf.format(*len)).unwrap(),
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// copy headers
|
||||
for (key, value) in head.headers.iter() {
|
||||
match *key {
|
||||
// TODO: consider skipping other headers according to:
|
||||
// https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2
|
||||
// omit HTTP/1.x only headers
|
||||
CONNECTION | TRANSFER_ENCODING => continue,
|
||||
CONTENT_LENGTH if skip_len => continue,
|
||||
DATE => has_date = true,
|
||||
match key {
|
||||
// omit HTTP/1.x only headers according to:
|
||||
// https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2
|
||||
&CONNECTION | &TRANSFER_ENCODING | &UPGRADE => continue,
|
||||
|
||||
&CONTENT_LENGTH if skip_len => continue,
|
||||
&DATE => has_date = true,
|
||||
|
||||
// omit HTTP/1.x only headers according to:
|
||||
// https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2
|
||||
hdr if hdr == HeaderName::from_static("keep-alive")
|
||||
|| hdr == HeaderName::from_static("proxy-connection") =>
|
||||
{
|
||||
continue
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@ -322,7 +340,7 @@ fn prepare_response(
|
||||
// set date header
|
||||
if !has_date {
|
||||
let mut bytes = BytesMut::with_capacity(29);
|
||||
config.set_date_header(&mut bytes);
|
||||
config.write_date_header_value(&mut bytes);
|
||||
res.headers_mut().insert(
|
||||
DATE,
|
||||
// SAFETY: serialized date-times are known ASCII strings
|
||||
|
@ -7,7 +7,7 @@ use std::{
|
||||
};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_rt::time::Sleep;
|
||||
use actix_rt::time::{sleep_until, Sleep};
|
||||
use bytes::Bytes;
|
||||
use futures_core::{ready, Stream};
|
||||
use h2::{
|
||||
@ -15,17 +15,17 @@ use h2::{
|
||||
RecvStream,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::ServiceConfig,
|
||||
error::{DispatchError, PayloadError},
|
||||
};
|
||||
|
||||
mod dispatcher;
|
||||
mod service;
|
||||
|
||||
pub use self::dispatcher::Dispatcher;
|
||||
pub use self::service::H2Service;
|
||||
|
||||
use crate::{
|
||||
config::ServiceConfig,
|
||||
error::{DispatchError, PayloadError},
|
||||
};
|
||||
|
||||
/// HTTP/2 peer stream.
|
||||
pub struct Payload {
|
||||
stream: RecvStream,
|
||||
@ -67,7 +67,9 @@ where
|
||||
{
|
||||
HandshakeWithTimeout {
|
||||
handshake: handshake(io),
|
||||
timer: config.client_timer().map(Box::pin),
|
||||
timer: config
|
||||
.client_request_deadline()
|
||||
.map(|deadline| Box::pin(sleep_until(deadline.into()))),
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,7 +88,7 @@ where
|
||||
let this = self.get_mut();
|
||||
|
||||
match Pin::new(&mut this.handshake).poll(cx)? {
|
||||
// return the timer on success handshake. It can be re-used for h2 ping-pong.
|
||||
// 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) => {
|
||||
@ -101,11 +103,9 @@ where
|
||||
|
||||
#[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);
|
||||
assert_impl_all!(Payload: Unpin, Send, Sync);
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ use actix_service::{
|
||||
};
|
||||
use actix_utils::future::ready;
|
||||
use futures_core::{future::LocalBoxFuture, ready};
|
||||
use log::error;
|
||||
use tracing::{error, trace};
|
||||
|
||||
use crate::{
|
||||
body::{BoxBody, MessageBody},
|
||||
@ -117,6 +117,7 @@ mod openssl {
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
/// Create OpenSSL based service.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
|
||||
pub fn openssl(
|
||||
self,
|
||||
acceptor: SslAcceptor,
|
||||
@ -164,6 +165,7 @@ mod rustls {
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
/// Create Rustls based service.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
pub fn rustls(
|
||||
self,
|
||||
mut config: ServerConfig,
|
||||
|
@ -6,7 +6,7 @@ use ahash::AHashMap;
|
||||
use http::header::{HeaderName, HeaderValue};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
||||
use crate::header::AsHeaderName;
|
||||
use super::AsHeaderName;
|
||||
|
||||
/// A multi-map of HTTP headers.
|
||||
///
|
||||
@ -605,6 +605,13 @@ impl<'a> IntoIterator for &'a HeaderMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Returned from methods that remove or replace items. See [`HeaderMap::insert`]
|
||||
@ -623,7 +630,7 @@ impl Removed {
|
||||
/// Returns true if iterator contains no elements, without consuming it.
|
||||
///
|
||||
/// If called immediately after [`HeaderMap::insert`] or [`HeaderMap::remove`], it will indicate
|
||||
/// wether any items were actually replaced or removed, respectively.
|
||||
/// 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
|
||||
|
@ -50,20 +50,13 @@ pub use self::utils::{
|
||||
|
||||
/// An interface for types that already represent a valid header.
|
||||
pub trait Header: TryIntoHeaderValue {
|
||||
/// Returns the name of the header field
|
||||
/// Returns the name of the header field.
|
||||
fn name() -> HeaderName;
|
||||
|
||||
/// Parse a header
|
||||
/// Parse the header from a HTTP message.
|
||||
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
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc5987#section-3.2>.
|
||||
pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
|
||||
|
@ -20,14 +20,16 @@ pub struct ContentEncodingParseError;
|
||||
/// 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)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum ContentEncoding {
|
||||
/// Automatically select encoding based on encoding negotiation.
|
||||
Auto,
|
||||
/// Indicates the no-op identity encoding.
|
||||
///
|
||||
/// I.e., no compression or modification.
|
||||
Identity,
|
||||
|
||||
/// A format using the Brotli algorithm.
|
||||
Br,
|
||||
Brotli,
|
||||
|
||||
/// A format using the zlib structure with deflate algorithm.
|
||||
Deflate,
|
||||
@ -37,32 +39,36 @@ pub enum ContentEncoding {
|
||||
|
||||
/// Zstd algorithm.
|
||||
Zstd,
|
||||
|
||||
/// Indicates the identity function (i.e. no compression, nor modification).
|
||||
Identity,
|
||||
}
|
||||
|
||||
impl ContentEncoding {
|
||||
/// Is the content compressed?
|
||||
#[inline]
|
||||
pub const fn is_compression(self) -> bool {
|
||||
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
|
||||
}
|
||||
|
||||
/// Convert content encoding to string.
|
||||
#[inline]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
ContentEncoding::Br => "br",
|
||||
ContentEncoding::Brotli => "br",
|
||||
ContentEncoding::Gzip => "gzip",
|
||||
ContentEncoding::Deflate => "deflate",
|
||||
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 {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self::Identity
|
||||
}
|
||||
@ -71,16 +77,18 @@ impl Default for ContentEncoding {
|
||||
impl FromStr for ContentEncoding {
|
||||
type Err = ContentEncodingParseError;
|
||||
|
||||
fn from_str(val: &str) -> Result<Self, Self::Err> {
|
||||
let val = val.trim();
|
||||
fn from_str(enc: &str) -> Result<Self, Self::Err> {
|
||||
let enc = enc.trim();
|
||||
|
||||
if val.eq_ignore_ascii_case("br") {
|
||||
Ok(ContentEncoding::Br)
|
||||
} else if val.eq_ignore_ascii_case("gzip") {
|
||||
if enc.eq_ignore_ascii_case("br") {
|
||||
Ok(ContentEncoding::Brotli)
|
||||
} else if enc.eq_ignore_ascii_case("gzip") {
|
||||
Ok(ContentEncoding::Gzip)
|
||||
} else if val.eq_ignore_ascii_case("deflate") {
|
||||
} else if enc.eq_ignore_ascii_case("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)
|
||||
} else {
|
||||
Err(ContentEncodingParseError)
|
||||
|
@ -12,7 +12,7 @@ use crate::header::{Charset, HTTP_VALUE};
|
||||
/// - A character sequence representing the actual value (`value`), separated by single quotes.
|
||||
///
|
||||
/// 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, Eq)]
|
||||
pub struct ExtendedValue {
|
||||
/// The character set that is used to encode the `value` to a string.
|
||||
pub charset: Charset,
|
||||
|
@ -4,8 +4,7 @@ use bytes::BytesMut;
|
||||
use http::header::{HeaderValue, InvalidHeaderValue};
|
||||
|
||||
use crate::{
|
||||
config::DATE_VALUE_LENGTH, error::ParseError, header::TryIntoHeaderValue,
|
||||
helpers::MutWriter,
|
||||
date::DATE_VALUE_LENGTH, error::ParseError, header::TryIntoHeaderValue, helpers::MutWriter,
|
||||
};
|
||||
|
||||
/// A timestamp with HTTP-style formatting and parsing.
|
||||
|
@ -27,7 +27,8 @@ const MAX_QUALITY_FLOAT: f32 = 1.0;
|
||||
///
|
||||
/// assert_eq!(q(0.42).to_string(), "0.42");
|
||||
/// assert_eq!(q(1.0).to_string(), "1");
|
||||
/// assert_eq!(Quality::MIN.to_string(), "0");
|
||||
/// 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
|
||||
@ -38,8 +39,11 @@ impl Quality {
|
||||
/// The maximum quality value, equivalent to `q=1.0`.
|
||||
pub const MAX: Quality = Quality(MAX_QUALITY_INT);
|
||||
|
||||
/// The minimum quality value, equivalent to `q=0.0`.
|
||||
pub const MIN: Quality = Quality(0);
|
||||
/// 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`.
|
||||
///
|
||||
@ -51,7 +55,7 @@ impl Quality {
|
||||
// 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),
|
||||
(0.0..=MAX_QUALITY_FLOAT).contains(&value),
|
||||
"q value must be between 0.0 and 1.0"
|
||||
);
|
||||
|
||||
@ -154,10 +158,13 @@ impl TryFrom<f32> for Quality {
|
||||
/// let q1 = q(1.0);
|
||||
/// assert_eq!(q1, Quality::MAX);
|
||||
///
|
||||
/// let q2 = q(0.0);
|
||||
/// let q2 = q(0.001);
|
||||
/// assert_eq!(q2, Quality::MIN);
|
||||
///
|
||||
/// let q3 = q(0.42);
|
||||
/// let q3 = q(0.0);
|
||||
/// assert_eq!(q3, Quality::ZERO);
|
||||
///
|
||||
/// let q4 = q(0.42);
|
||||
/// ```
|
||||
///
|
||||
/// An out-of-range `f32` quality will panic.
|
||||
@ -185,6 +192,10 @@ mod tests {
|
||||
|
||||
#[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");
|
||||
|
@ -31,7 +31,7 @@ use super::Quality;
|
||||
/// let q_item_fallback: QualityItem<String> = "abc;q=0.1".parse().unwrap();
|
||||
/// assert!(q_item > q_item_fallback);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct QualityItem<T> {
|
||||
/// The wrapped contents of the field.
|
||||
pub item: T,
|
||||
@ -53,10 +53,15 @@ impl<T> QualityItem<T> {
|
||||
Self::new(item, Quality::MAX)
|
||||
}
|
||||
|
||||
/// Constructs a new `QualityItem` from an item, using the minimum q-value.
|
||||
/// 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> PartialOrd for QualityItem<T> {
|
||||
@ -73,7 +78,10 @@ impl<T: fmt::Display> fmt::Display for QualityItem<T> {
|
||||
// q-factor value is implied for max value
|
||||
Quality::MAX => Ok(()),
|
||||
|
||||
Quality::MIN => f.write_str("; q=0"),
|
||||
// fast path for zero
|
||||
Quality::ZERO => f.write_str("; q=0"),
|
||||
|
||||
// quality formatting is already using itoa
|
||||
q => write!(f, "; q={}", q),
|
||||
}
|
||||
}
|
||||
@ -139,7 +147,7 @@ mod tests {
|
||||
|
||||
// copy of encoding from actix-web headers
|
||||
#[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Encoding {
|
||||
Chunked,
|
||||
Brotli,
|
||||
|
@ -30,15 +30,25 @@ pub(crate) fn write_status_line<B: BufMut>(version: Version, n: u16, buf: &mut B
|
||||
/// Write out content length header.
|
||||
///
|
||||
/// 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 {
|
||||
buf.put_slice(b"\r\ncontent-length: 0\r\n");
|
||||
if camel_case {
|
||||
buf.put_slice(b"\r\nContent-Length: 0\r\n");
|
||||
} else {
|
||||
buf.put_slice(b"\r\ncontent-length: 0\r\n");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let mut buffer = itoa::Buffer::new();
|
||||
|
||||
buf.put_slice(b"\r\ncontent-length: ");
|
||||
if camel_case {
|
||||
buf.put_slice(b"\r\nContent-Length: ");
|
||||
} else {
|
||||
buf.put_slice(b"\r\ncontent-length: ");
|
||||
}
|
||||
|
||||
buf.put_slice(buffer.format(n).as_bytes());
|
||||
buf.put_slice(b"\r\n");
|
||||
}
|
||||
@ -95,77 +105,88 @@ mod tests {
|
||||
fn test_write_content_length() {
|
||||
let mut bytes = BytesMut::new();
|
||||
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"[..]);
|
||||
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"[..]);
|
||||
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"[..]);
|
||||
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"[..]);
|
||||
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"[..]);
|
||||
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"[..]);
|
||||
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"[..]);
|
||||
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"[..]);
|
||||
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"[..]);
|
||||
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"[..]);
|
||||
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"[..]);
|
||||
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"[..]);
|
||||
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"[..]);
|
||||
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"[..]);
|
||||
|
||||
bytes.reserve(50);
|
||||
write_content_length(590947, &mut bytes);
|
||||
write_content_length(590947, &mut bytes, false);
|
||||
assert_eq!(
|
||||
bytes.split().freeze(),
|
||||
b"\r\ncontent-length: 590947\r\n"[..]
|
||||
);
|
||||
bytes.reserve(50);
|
||||
write_content_length(999999, &mut bytes);
|
||||
write_content_length(999999, &mut bytes, false);
|
||||
assert_eq!(
|
||||
bytes.split().freeze(),
|
||||
b"\r\ncontent-length: 999999\r\n"[..]
|
||||
);
|
||||
bytes.reserve(50);
|
||||
write_content_length(5909471, &mut bytes);
|
||||
write_content_length(5909471, &mut bytes, false);
|
||||
assert_eq!(
|
||||
bytes.split().freeze(),
|
||||
b"\r\ncontent-length: 5909471\r\n"[..]
|
||||
);
|
||||
bytes.reserve(50);
|
||||
write_content_length(59094718, &mut bytes);
|
||||
write_content_length(59094718, &mut bytes, false);
|
||||
assert_eq!(
|
||||
bytes.split().freeze(),
|
||||
b"\r\ncontent-length: 59094718\r\n"[..]
|
||||
);
|
||||
bytes.reserve(50);
|
||||
write_content_length(4294973728, &mut bytes);
|
||||
write_content_length(4294973728, &mut bytes, false);
|
||||
assert_eq!(
|
||||
bytes.split().freeze(),
|
||||
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
|
||||
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>;
|
||||
|
||||
/// 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>;
|
||||
|
||||
/// 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.
|
||||
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);
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
//! ## Crate Features
|
||||
//! | Feature | Functionality |
|
||||
//! | ------------------- | ------------------------------------------- |
|
||||
//! | `http2` | HTTP/2 support via [h2]. |
|
||||
//! | `openssl` | TLS support via [OpenSSL]. |
|
||||
//! | `rustls` | TLS support via [rustls]. |
|
||||
//! | `compress-brotli` | Payload compression support: Brotli. |
|
||||
@ -10,6 +11,7 @@
|
||||
//! | `compress-zstd` | Payload compression support: Zstd. |
|
||||
//! | `trust-dns` | Use [trust-dns] as the client DNS resolver. |
|
||||
//!
|
||||
//! [h2]: https://crates.io/crates/h2
|
||||
//! [OpenSSL]: https://crates.io/crates/openssl
|
||||
//! [rustls]: https://crates.io/crates/rustls
|
||||
//! [trust-dns]: https://crates.io/crates/trust-dns
|
||||
@ -23,9 +25,7 @@
|
||||
)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
pub use ::http::{uri, uri::Uri};
|
||||
pub use ::http::{Method, StatusCode, Version};
|
||||
@ -33,29 +33,38 @@ pub use ::http::{Method, StatusCode, Version};
|
||||
pub mod body;
|
||||
mod builder;
|
||||
mod config;
|
||||
mod date;
|
||||
#[cfg(feature = "__compress")]
|
||||
pub mod encoding;
|
||||
pub mod error;
|
||||
mod extensions;
|
||||
pub mod h1;
|
||||
#[cfg(feature = "http2")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
||||
pub mod h2;
|
||||
pub mod header;
|
||||
mod helpers;
|
||||
mod http_message;
|
||||
mod keep_alive;
|
||||
mod message;
|
||||
#[cfg(test)]
|
||||
mod notify_on_drop;
|
||||
mod payload;
|
||||
mod requests;
|
||||
mod responses;
|
||||
mod service;
|
||||
pub mod test;
|
||||
#[cfg(feature = "ws")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
|
||||
pub mod ws;
|
||||
|
||||
pub use self::builder::HttpServiceBuilder;
|
||||
pub use self::config::{KeepAlive, ServiceConfig};
|
||||
pub use self::config::ServiceConfig;
|
||||
pub use self::error::Error;
|
||||
pub use self::extensions::Extensions;
|
||||
pub use self::header::ContentEncoding;
|
||||
pub use self::http_message::HttpMessage;
|
||||
pub use self::keep_alive::KeepAlive;
|
||||
pub use self::message::ConnectionType;
|
||||
pub use self::message::Message;
|
||||
#[allow(deprecated)]
|
||||
@ -63,6 +72,9 @@ pub use self::payload::{BoxedPayloadStream, Payload, PayloadStream};
|
||||
pub use self::requests::{Request, RequestHead, RequestHeadType};
|
||||
pub use self::responses::{Response, ResponseBuilder, ResponseHead};
|
||||
pub use self::service::HttpService;
|
||||
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))]
|
||||
pub use self::service::TlsAcceptorConfig;
|
||||
|
||||
/// A major HTTP protocol version.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
|
@ -3,15 +3,15 @@ use std::{cell::RefCell, ops, rc::Rc};
|
||||
use bitflags::bitflags;
|
||||
|
||||
/// Represents various types of connection
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConnectionType {
|
||||
/// Close connection after response
|
||||
/// Close connection after response.
|
||||
Close,
|
||||
|
||||
/// Keep connection alive after response
|
||||
/// Keep connection alive after response.
|
||||
KeepAlive,
|
||||
|
||||
/// Connection is upgraded to different type
|
||||
/// Connection is upgraded to different type.
|
||||
Upgrade,
|
||||
}
|
||||
|
||||
@ -69,8 +69,8 @@ impl<T: Head> Drop for Message<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic `Head` object pool.
|
||||
#[doc(hidden)]
|
||||
/// Request's objects pool
|
||||
pub struct MessagePool<T: Head>(RefCell<Vec<Rc<T>>>);
|
||||
|
||||
impl<T: Head> MessagePool<T> {
|
||||
|
49
actix-http/src/notify_on_drop.rs
Normal file
49
actix-http/src/notify_on_drop.rs
Normal file
@ -0,0 +1,49 @@
|
||||
/// 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
|
||||
use std::cell::RefCell;
|
||||
|
||||
thread_local! {
|
||||
static NOTIFY_DROPPED: RefCell<Option<bool>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
/// Check if the spawned task is dropped.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics 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 {
|
||||
/// # Panics
|
||||
/// Panics hen 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -6,16 +6,30 @@ use std::{
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_core::Stream;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use crate::error::PayloadError;
|
||||
|
||||
/// A boxed payload stream.
|
||||
pub type BoxedPayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadError>>>>;
|
||||
|
||||
#[deprecated(since = "4.0.0", note = "Renamed to `BoxedPayloadStream`.")]
|
||||
#[doc(hidden)]
|
||||
#[deprecated(since = "3.0.0", note = "Renamed to `BoxedPayloadStream`.")]
|
||||
pub type PayloadStream = BoxedPayloadStream;
|
||||
|
||||
pin_project_lite::pin_project! {
|
||||
#[cfg(not(feature = "http2"))]
|
||||
pin_project! {
|
||||
/// A streaming payload.
|
||||
#[project = PayloadProj]
|
||||
pub enum Payload<S = BoxedPayloadStream> {
|
||||
None,
|
||||
H1 { payload: crate::h1::Payload },
|
||||
Stream { #[pin] payload: S },
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http2")]
|
||||
pin_project! {
|
||||
/// A streaming payload.
|
||||
#[project = PayloadProj]
|
||||
pub enum Payload<S = BoxedPayloadStream> {
|
||||
@ -32,14 +46,16 @@ impl<S> From<crate::h1::Payload> for Payload<S> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http2")]
|
||||
impl<S> From<crate::h2::Payload> for Payload<S> {
|
||||
fn from(payload: crate::h2::Payload) -> Self {
|
||||
Payload::H2 { payload }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> From<h2::RecvStream> for Payload<S> {
|
||||
fn from(stream: h2::RecvStream) -> Self {
|
||||
#[cfg(feature = "http2")]
|
||||
impl<S> From<::h2::RecvStream> for Payload<S> {
|
||||
fn from(stream: ::h2::RecvStream) -> Self {
|
||||
Payload::H2 {
|
||||
payload: crate::h2::Payload::new(stream),
|
||||
}
|
||||
@ -70,7 +86,10 @@ where
|
||||
match self.project() {
|
||||
PayloadProj::None => Poll::Ready(None),
|
||||
PayloadProj::H1 { payload } => Pin::new(payload).poll_next(cx),
|
||||
|
||||
#[cfg(feature = "http2")]
|
||||
PayloadProj::H2 { payload } => Pin::new(payload).poll_next(cx),
|
||||
|
||||
PayloadProj::Stream { payload } => payload.poll_next(cx),
|
||||
}
|
||||
}
|
||||
@ -78,12 +97,10 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||
|
||||
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_not_impl_any!(Payload: Send, Sync);
|
||||
}
|
||||
|
@ -130,8 +130,8 @@ impl RequestHead {
|
||||
}
|
||||
}
|
||||
|
||||
/// Request contains `EXPECT` header.
|
||||
#[inline]
|
||||
/// Request contains `EXPECT` header
|
||||
pub fn expect(&self) -> bool {
|
||||
self.flags.contains(Flags::EXPECT)
|
||||
}
|
||||
@ -142,8 +142,8 @@ impl RequestHead {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum RequestHeadType {
|
||||
Owned(RequestHead),
|
||||
Rc(Rc<RequestHead>, Option<HeaderMap>),
|
||||
|
@ -19,7 +19,7 @@ pub struct Request<P = BoxedPayloadStream> {
|
||||
pub(crate) payload: Payload<P>,
|
||||
pub(crate) head: Message<RequestHead>,
|
||||
pub(crate) conn_data: Option<Rc<Extensions>>,
|
||||
pub(crate) req_data: RefCell<Extensions>,
|
||||
pub(crate) extensions: RefCell<Extensions>,
|
||||
}
|
||||
|
||||
impl<P> HttpMessage for Request<P> {
|
||||
@ -34,16 +34,14 @@ impl<P> HttpMessage for Request<P> {
|
||||
mem::replace(&mut self.payload, Payload::None)
|
||||
}
|
||||
|
||||
/// Request extensions
|
||||
#[inline]
|
||||
fn extensions(&self) -> Ref<'_, Extensions> {
|
||||
self.req_data.borrow()
|
||||
self.extensions.borrow()
|
||||
}
|
||||
|
||||
/// Mutable reference to a the request's extensions
|
||||
#[inline]
|
||||
fn extensions_mut(&self) -> RefMut<'_, Extensions> {
|
||||
self.req_data.borrow_mut()
|
||||
self.extensions.borrow_mut()
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,7 +50,7 @@ impl From<Message<RequestHead>> for Request<BoxedPayloadStream> {
|
||||
Request {
|
||||
head,
|
||||
payload: Payload::None,
|
||||
req_data: RefCell::new(Extensions::default()),
|
||||
extensions: RefCell::new(Extensions::default()),
|
||||
conn_data: None,
|
||||
}
|
||||
}
|
||||
@ -65,7 +63,7 @@ impl Request<BoxedPayloadStream> {
|
||||
Request {
|
||||
head: Message::new(),
|
||||
payload: Payload::None,
|
||||
req_data: RefCell::new(Extensions::default()),
|
||||
extensions: RefCell::new(Extensions::default()),
|
||||
conn_data: None,
|
||||
}
|
||||
}
|
||||
@ -77,7 +75,7 @@ impl<P> Request<P> {
|
||||
Request {
|
||||
payload,
|
||||
head: Message::new(),
|
||||
req_data: RefCell::new(Extensions::default()),
|
||||
extensions: RefCell::new(Extensions::default()),
|
||||
conn_data: None,
|
||||
}
|
||||
}
|
||||
@ -90,7 +88,7 @@ impl<P> Request<P> {
|
||||
Request {
|
||||
payload,
|
||||
head: self.head,
|
||||
req_data: self.req_data,
|
||||
extensions: self.extensions,
|
||||
conn_data: self.conn_data,
|
||||
},
|
||||
pl,
|
||||
@ -195,16 +193,17 @@ impl<P> Request<P> {
|
||||
.and_then(|container| container.get::<T>())
|
||||
}
|
||||
|
||||
/// Returns the connection data container if an [on-connect] callback was registered.
|
||||
/// Returns the connection-level data/extensions container if an [on-connect] callback was
|
||||
/// registered, leaving an empty one in its place.
|
||||
///
|
||||
/// [on-connect]: crate::HttpServiceBuilder::on_connect_ext
|
||||
pub fn take_conn_data(&mut self) -> Option<Rc<Extensions>> {
|
||||
self.conn_data.take()
|
||||
}
|
||||
|
||||
/// Returns the request data container, leaving an empty one in it's place.
|
||||
/// Returns the request-local data/extensions container, leaving an empty one in its place.
|
||||
pub fn take_req_data(&mut self) -> Extensions {
|
||||
mem::take(self.req_data.get_mut())
|
||||
mem::take(self.extensions.get_mut())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,6 @@
|
||||
//! HTTP response builder.
|
||||
|
||||
use std::{
|
||||
cell::{Ref, RefMut},
|
||||
fmt, str,
|
||||
};
|
||||
use std::{cell::RefCell, fmt, str};
|
||||
|
||||
use crate::{
|
||||
body::{EitherBody, MessageBody},
|
||||
@ -147,7 +144,7 @@ impl ResponseBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set connection type to Upgrade
|
||||
/// Set connection type to `Upgrade`.
|
||||
#[inline]
|
||||
pub fn upgrade<V>(&mut self, value: V) -> &mut Self
|
||||
where
|
||||
@ -164,7 +161,7 @@ impl ResponseBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Force close connection, even if it is marked as keep-alive
|
||||
/// Force-close connection, even if it is marked as keep-alive.
|
||||
#[inline]
|
||||
pub fn force_close(&mut self) -> &mut Self {
|
||||
if let Some(parts) = self.inner() {
|
||||
@ -202,20 +199,6 @@ impl ResponseBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Responses extensions
|
||||
#[inline]
|
||||
pub fn extensions(&self) -> Ref<'_, Extensions> {
|
||||
let head = self.head.as_ref().expect("cannot reuse response builder");
|
||||
head.extensions.borrow()
|
||||
}
|
||||
|
||||
/// Mutable reference to a the response's extensions
|
||||
#[inline]
|
||||
pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> {
|
||||
let head = self.head.as_ref().expect("cannot reuse response builder");
|
||||
head.extensions.borrow_mut()
|
||||
}
|
||||
|
||||
/// Generate response with a wrapped body.
|
||||
///
|
||||
/// This `ResponseBuilder` will be left in a useless state.
|
||||
@ -238,7 +221,12 @@ impl ResponseBuilder {
|
||||
}
|
||||
|
||||
let head = self.head.take().expect("cannot reuse response builder");
|
||||
Ok(Response { head, body })
|
||||
|
||||
Ok(Response {
|
||||
head,
|
||||
body,
|
||||
extensions: RefCell::new(Extensions::new()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate response with an empty body.
|
||||
|
@ -1,26 +1,20 @@
|
||||
//! Response head type and caching pool.
|
||||
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
ops,
|
||||
};
|
||||
use std::{cell::RefCell, ops};
|
||||
|
||||
use crate::{
|
||||
header::HeaderMap, message::Flags, ConnectionType, Extensions, StatusCode, Version,
|
||||
};
|
||||
use crate::{header::HeaderMap, message::Flags, ConnectionType, StatusCode, Version};
|
||||
|
||||
thread_local! {
|
||||
static RESPONSE_POOL: BoxedResponsePool = BoxedResponsePool::create();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResponseHead {
|
||||
pub version: Version,
|
||||
pub status: StatusCode,
|
||||
pub headers: HeaderMap,
|
||||
pub reason: Option<&'static str>,
|
||||
pub(crate) extensions: RefCell<Extensions>,
|
||||
flags: Flags,
|
||||
pub(crate) flags: Flags,
|
||||
}
|
||||
|
||||
impl ResponseHead {
|
||||
@ -33,36 +27,35 @@ impl ResponseHead {
|
||||
headers: HeaderMap::with_capacity(12),
|
||||
reason: None,
|
||||
flags: Flags::empty(),
|
||||
extensions: RefCell::new(Extensions::new()),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Read the message headers.
|
||||
#[inline]
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Mutable reference to the message headers.
|
||||
#[inline]
|
||||
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Message extensions
|
||||
/// Sets the flag that controls whether to send headers formatted as Camel-Case.
|
||||
///
|
||||
/// Only applicable to HTTP/1.x responses; HTTP/2 header names are always lowercase.
|
||||
#[inline]
|
||||
pub fn extensions(&self) -> Ref<'_, Extensions> {
|
||||
self.extensions.borrow()
|
||||
pub fn set_camel_case_headers(&mut self, camel_case: bool) {
|
||||
if camel_case {
|
||||
self.flags.insert(Flags::CAMEL_CASE);
|
||||
} else {
|
||||
self.flags.remove(Flags::CAMEL_CASE);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mutable reference to a the message's extensions
|
||||
#[inline]
|
||||
pub fn extensions_mut(&self) -> RefMut<'_, Extensions> {
|
||||
self.extensions.borrow_mut()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Set connection type of the message
|
||||
#[inline]
|
||||
pub fn set_connection_type(&mut self, ctype: ConnectionType) {
|
||||
match ctype {
|
||||
ConnectionType::Close => self.flags.insert(Flags::CLOSE),
|
||||
@ -121,14 +114,14 @@ impl ResponseHead {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Get response body chunking state
|
||||
#[inline]
|
||||
pub fn chunked(&self) -> bool {
|
||||
!self.flags.contains(Flags::NO_CHUNKING)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Set no chunking for payload
|
||||
#[inline]
|
||||
pub fn no_chunking(&mut self, val: bool) {
|
||||
if val {
|
||||
self.flags.insert(Flags::NO_CHUNKING);
|
||||
@ -171,7 +164,7 @@ impl Drop for BoxedResponseHead {
|
||||
}
|
||||
}
|
||||
|
||||
/// Request's objects pool
|
||||
/// Response head object pool.
|
||||
#[doc(hidden)]
|
||||
pub struct BoxedResponsePool(#[allow(clippy::vec_box)] RefCell<Vec<Box<ResponseHead>>>);
|
||||
|
||||
@ -180,7 +173,7 @@ impl BoxedResponsePool {
|
||||
BoxedResponsePool(RefCell::new(Vec::with_capacity(128)))
|
||||
}
|
||||
|
||||
/// Get message from the pool
|
||||
/// Get message from the pool.
|
||||
#[inline]
|
||||
fn get_message(&self, status: StatusCode) -> BoxedResponseHead {
|
||||
if let Some(mut head) = self.0.borrow_mut().pop() {
|
||||
@ -196,13 +189,81 @@ impl BoxedResponsePool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Release request instance
|
||||
/// Release request instance.
|
||||
#[inline]
|
||||
fn release(&self, mut msg: Box<ResponseHead>) {
|
||||
fn release(&self, msg: Box<ResponseHead>) {
|
||||
let pool = &mut self.0.borrow_mut();
|
||||
|
||||
if pool.len() < 128 {
|
||||
msg.extensions.get_mut().clear();
|
||||
pool.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io::{Read as _, Write as _},
|
||||
net,
|
||||
};
|
||||
|
||||
use memchr::memmem;
|
||||
|
||||
use crate::{
|
||||
h1::H1Service,
|
||||
header::{HeaderName, HeaderValue},
|
||||
Error, Request, Response, ServiceConfig,
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn camel_case_headers() {
|
||||
let mut srv = actix_http_test::test_server(|| {
|
||||
H1Service::with_config(ServiceConfig::default(), |req: Request| async move {
|
||||
let mut res = Response::ok();
|
||||
|
||||
if req.path().contains("camel") {
|
||||
res.head_mut().set_camel_case_headers(true);
|
||||
}
|
||||
|
||||
res.headers_mut().insert(
|
||||
HeaderName::from_static("foo-bar"),
|
||||
HeaderValue::from_static("baz"),
|
||||
);
|
||||
|
||||
Ok::<_, Error>(res)
|
||||
})
|
||||
.tcp()
|
||||
})
|
||||
.await;
|
||||
|
||||
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
||||
stream
|
||||
.write_all(b"GET /camel HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
||||
.unwrap();
|
||||
let mut data = vec![];
|
||||
let _ = stream.read_to_end(&mut data).unwrap();
|
||||
assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n");
|
||||
assert!(memmem::find(&data, b"Foo-Bar").is_some());
|
||||
assert!(memmem::find(&data, b"foo-bar").is_none());
|
||||
assert!(memmem::find(&data, b"Date").is_some());
|
||||
assert!(memmem::find(&data, b"date").is_none());
|
||||
assert!(memmem::find(&data, b"Content-Length").is_some());
|
||||
assert!(memmem::find(&data, b"content-length").is_none());
|
||||
|
||||
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
||||
stream
|
||||
.write_all(b"GET /lower HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
||||
.unwrap();
|
||||
let mut data = vec![];
|
||||
let _ = stream.read_to_end(&mut data).unwrap();
|
||||
assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n");
|
||||
assert!(memmem::find(&data, b"Foo-Bar").is_none());
|
||||
assert!(memmem::find(&data, b"foo-bar").is_some());
|
||||
assert!(memmem::find(&data, b"Date").is_none());
|
||||
assert!(memmem::find(&data, b"date").is_some());
|
||||
assert!(memmem::find(&data, b"Content-Length").is_none());
|
||||
assert!(memmem::find(&data, b"content-length").is_some());
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
//! HTTP response.
|
||||
|
||||
use std::{
|
||||
cell::{Ref, RefMut},
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
fmt, str,
|
||||
};
|
||||
|
||||
@ -9,7 +9,7 @@ use bytes::{Bytes, BytesMut};
|
||||
use bytestring::ByteString;
|
||||
|
||||
use crate::{
|
||||
body::{BoxBody, MessageBody},
|
||||
body::{BoxBody, EitherBody, MessageBody},
|
||||
header::{self, HeaderMap, TryIntoHeaderValue},
|
||||
responses::BoxedResponseHead,
|
||||
Error, Extensions, ResponseBuilder, ResponseHead, StatusCode,
|
||||
@ -19,6 +19,7 @@ use crate::{
|
||||
pub struct Response<B> {
|
||||
pub(crate) head: BoxedResponseHead,
|
||||
pub(crate) body: B,
|
||||
pub(crate) extensions: RefCell<Extensions>,
|
||||
}
|
||||
|
||||
impl Response<BoxBody> {
|
||||
@ -28,6 +29,7 @@ impl Response<BoxBody> {
|
||||
Response {
|
||||
head: BoxedResponseHead::new(status),
|
||||
body: BoxBody::new(()),
|
||||
extensions: RefCell::new(Extensions::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,6 +76,7 @@ impl<B> Response<B> {
|
||||
Response {
|
||||
head: BoxedResponseHead::new(status),
|
||||
body,
|
||||
extensions: RefCell::new(Extensions::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,20 +123,21 @@ impl<B> Response<B> {
|
||||
}
|
||||
|
||||
/// Returns true if keep-alive is enabled.
|
||||
#[inline]
|
||||
pub fn keep_alive(&self) -> bool {
|
||||
self.head.keep_alive()
|
||||
}
|
||||
|
||||
/// Returns a reference to the extensions of this response.
|
||||
/// Returns a reference to the request-local data/extensions container.
|
||||
#[inline]
|
||||
pub fn extensions(&self) -> Ref<'_, Extensions> {
|
||||
self.head.extensions.borrow()
|
||||
self.extensions.borrow()
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the extensions of this response.
|
||||
/// Returns a mutable reference to the request-local data/extensions container.
|
||||
#[inline]
|
||||
pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> {
|
||||
self.head.extensions.borrow_mut()
|
||||
self.extensions.borrow_mut()
|
||||
}
|
||||
|
||||
/// Returns a reference to the body of this response.
|
||||
@ -143,24 +147,29 @@ impl<B> Response<B> {
|
||||
}
|
||||
|
||||
/// Sets new body.
|
||||
#[inline]
|
||||
pub fn set_body<B2>(self, body: B2) -> Response<B2> {
|
||||
Response {
|
||||
head: self.head,
|
||||
body,
|
||||
extensions: self.extensions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Drops body and returns new response.
|
||||
#[inline]
|
||||
pub fn drop_body(self) -> Response<()> {
|
||||
self.set_body(())
|
||||
}
|
||||
|
||||
/// Sets new body, returning new response and previous body value.
|
||||
#[inline]
|
||||
pub(crate) fn replace_body<B2>(self, body: B2) -> (Response<B2>, B) {
|
||||
(
|
||||
Response {
|
||||
head: self.head,
|
||||
body,
|
||||
extensions: self.extensions,
|
||||
},
|
||||
self.body,
|
||||
)
|
||||
@ -171,11 +180,15 @@ impl<B> Response<B> {
|
||||
/// # Implementation Notes
|
||||
/// Due to internal performance optimizations, the first element of the returned tuple is a
|
||||
/// `Response` as well but only contains the head of the response this was called on.
|
||||
#[inline]
|
||||
pub fn into_parts(self) -> (Response<()>, B) {
|
||||
self.replace_body(())
|
||||
}
|
||||
|
||||
/// Returns new response with mapped body.
|
||||
/// Map the current body type to another using a closure, returning a new response.
|
||||
///
|
||||
/// Closure receives the response head and the current body type.
|
||||
#[inline]
|
||||
pub fn map_body<F, B2>(mut self, f: F) -> Response<B2>
|
||||
where
|
||||
F: FnOnce(&mut ResponseHead, B) -> B2,
|
||||
@ -185,9 +198,11 @@ impl<B> Response<B> {
|
||||
Response {
|
||||
head: self.head,
|
||||
body,
|
||||
extensions: self.extensions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Map the current body to a type-erased `BoxBody`.
|
||||
#[inline]
|
||||
pub fn map_into_boxed_body(self) -> Response<BoxBody>
|
||||
where
|
||||
@ -196,7 +211,8 @@ impl<B> Response<B> {
|
||||
self.map_body(|_, body| body.boxed())
|
||||
}
|
||||
|
||||
/// Returns body, consuming this response.
|
||||
/// Returns the response body, dropping all other parts.
|
||||
#[inline]
|
||||
pub fn into_body(self) -> B {
|
||||
self.body
|
||||
}
|
||||
@ -239,9 +255,9 @@ impl<I: Into<Response<BoxBody>>, E: Into<Error>> From<Result<I, E>> for Response
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResponseBuilder> for Response<BoxBody> {
|
||||
impl From<ResponseBuilder> for Response<EitherBody<()>> {
|
||||
fn from(mut builder: ResponseBuilder) -> Self {
|
||||
builder.finish().map_into_boxed_body()
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@ -269,6 +285,24 @@ impl From<&'static [u8]> for Response<&'static [u8]> {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Response<Vec<u8>> {
|
||||
fn from(val: Vec<u8>) -> Self {
|
||||
let mut res = Response::with_body(StatusCode::OK, val);
|
||||
let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap();
|
||||
res.headers_mut().insert(header::CONTENT_TYPE, mime);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Vec<u8>> for Response<Vec<u8>> {
|
||||
fn from(val: &Vec<u8>) -> Self {
|
||||
let mut res = Response::with_body(StatusCode::OK, val.clone());
|
||||
let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap();
|
||||
res.headers_mut().insert(header::CONTENT_TYPE, mime);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Response<String> {
|
||||
fn from(val: String) -> Self {
|
||||
let mut res = Response::with_body(StatusCode::OK, val);
|
||||
|
@ -15,13 +15,13 @@ use actix_service::{
|
||||
};
|
||||
use futures_core::{future::LocalBoxFuture, ready};
|
||||
use pin_project_lite::pin_project;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
body::{BoxBody, MessageBody},
|
||||
builder::HttpServiceBuilder,
|
||||
config::{KeepAlive, ServiceConfig},
|
||||
error::DispatchError,
|
||||
h1, h2, ConnectCallback, OnConnectData, Protocol, Request, Response,
|
||||
h1, ConnectCallback, OnConnectData, Protocol, Request, Response, ServiceConfig,
|
||||
};
|
||||
|
||||
/// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol.
|
||||
@ -43,9 +43,9 @@ where
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
/// Create builder for `HttpService` instance.
|
||||
/// Constructs builder for `HttpService` instance.
|
||||
pub fn build() -> HttpServiceBuilder<T, S> {
|
||||
HttpServiceBuilder::new()
|
||||
HttpServiceBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,12 +58,10 @@ where
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
/// Create new `HttpService` instance.
|
||||
/// Constructs new `HttpService` instance from service with default config.
|
||||
pub fn new<F: IntoServiceFactory<S, Request>>(service: F) -> Self {
|
||||
let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0, false, None);
|
||||
|
||||
HttpService {
|
||||
cfg,
|
||||
cfg: ServiceConfig::default(),
|
||||
srv: service.into_factory(),
|
||||
expect: h1::ExpectHandler,
|
||||
upgrade: None,
|
||||
@ -72,7 +70,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new `HttpService` instance with config.
|
||||
/// Constructs new `HttpService` instance from config and service.
|
||||
pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
|
||||
cfg: ServiceConfig,
|
||||
service: F,
|
||||
@ -97,11 +95,10 @@ where
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody,
|
||||
{
|
||||
/// Provide service for `EXPECT: 100-Continue` support.
|
||||
/// Sets service for `Expect: 100-Continue` handling.
|
||||
///
|
||||
/// Service get called with request that contains `EXPECT` header.
|
||||
/// Service must return request in case of success, in that case
|
||||
/// request will be forwarded to main service.
|
||||
/// An expect service is called with requests that contain an `Expect` header. A successful
|
||||
/// response type is also a request which will be forwarded to the main service.
|
||||
pub fn expect<X1>(self, expect: X1) -> HttpService<T, S, B, X1, U>
|
||||
where
|
||||
X1: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
@ -118,10 +115,10 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide service for custom `Connection: UPGRADE` support.
|
||||
/// Sets service for custom `Connection: Upgrade` handling.
|
||||
///
|
||||
/// If service is provided then normal requests handling get halted
|
||||
/// and this service get called with original request and framed object.
|
||||
/// If service is provided then normal requests handling get halted and this service get called
|
||||
/// with original request and framed object.
|
||||
pub fn upgrade<U1>(self, upgrade: Option<U1>) -> HttpService<T, S, B, X, U1>
|
||||
where
|
||||
U1: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
|
||||
@ -184,6 +181,25 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration options used when accepting TLS connection.
|
||||
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TlsAcceptorConfig {
|
||||
pub(crate) handshake_timeout: Option<std::time::Duration>,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
||||
impl TlsAcceptorConfig {
|
||||
/// Set TLS handshake timeout duration.
|
||||
pub fn handshake_timeout(self, dur: std::time::Duration) -> Self {
|
||||
Self {
|
||||
handshake_timeout: Some(dur),
|
||||
// ..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
mod openssl {
|
||||
use actix_service::ServiceFactoryExt as _;
|
||||
@ -223,6 +239,7 @@ mod openssl {
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Create OpenSSL based service.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
|
||||
pub fn openssl(
|
||||
self,
|
||||
acceptor: SslAcceptor,
|
||||
@ -233,7 +250,29 @@ mod openssl {
|
||||
Error = TlsError<SslError, DispatchError>,
|
||||
InitError = (),
|
||||
> {
|
||||
Acceptor::new(acceptor)
|
||||
self.openssl_with_config(acceptor, TlsAcceptorConfig::default())
|
||||
}
|
||||
|
||||
/// Create OpenSSL based service with custom TLS acceptor configuration.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
|
||||
pub fn openssl_with_config(
|
||||
self,
|
||||
acceptor: SslAcceptor,
|
||||
tls_acceptor_config: TlsAcceptorConfig,
|
||||
) -> impl ServiceFactory<
|
||||
TcpStream,
|
||||
Config = (),
|
||||
Response = (),
|
||||
Error = TlsError<SslError, DispatchError>,
|
||||
InitError = (),
|
||||
> {
|
||||
let mut acceptor = Acceptor::new(acceptor);
|
||||
|
||||
if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout {
|
||||
acceptor.set_handshake_timeout(handshake_timeout);
|
||||
}
|
||||
|
||||
acceptor
|
||||
.map_init_err(|_| {
|
||||
unreachable!("TLS acceptor service factory does not error on init")
|
||||
})
|
||||
@ -295,9 +334,26 @@ mod rustls {
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Create Rustls based service.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
pub fn rustls(
|
||||
self,
|
||||
config: ServerConfig,
|
||||
) -> impl ServiceFactory<
|
||||
TcpStream,
|
||||
Config = (),
|
||||
Response = (),
|
||||
Error = TlsError<io::Error, DispatchError>,
|
||||
InitError = (),
|
||||
> {
|
||||
self.rustls_with_config(config, TlsAcceptorConfig::default())
|
||||
}
|
||||
|
||||
/// Create Rustls based service with custom TLS acceptor configuration.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
pub fn rustls_with_config(
|
||||
self,
|
||||
mut config: ServerConfig,
|
||||
tls_acceptor_config: TlsAcceptorConfig,
|
||||
) -> impl ServiceFactory<
|
||||
TcpStream,
|
||||
Config = (),
|
||||
@ -309,7 +365,13 @@ mod rustls {
|
||||
protos.extend_from_slice(&config.alpn_protocols);
|
||||
config.alpn_protocols = protos;
|
||||
|
||||
Acceptor::new(config)
|
||||
let mut acceptor = Acceptor::new(config);
|
||||
|
||||
if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout {
|
||||
acceptor.set_handshake_timeout(handshake_timeout);
|
||||
}
|
||||
|
||||
acceptor
|
||||
.map_init_err(|_| {
|
||||
unreachable!("TLS acceptor service factory does not error on init")
|
||||
})
|
||||
@ -373,13 +435,13 @@ where
|
||||
Box::pin(async move {
|
||||
let expect = expect
|
||||
.await
|
||||
.map_err(|e| log::error!("Init http expect service error: {:?}", e))?;
|
||||
.map_err(|e| error!("Init http expect service error: {:?}", e))?;
|
||||
|
||||
let upgrade = match upgrade {
|
||||
Some(upgrade) => {
|
||||
let upgrade = upgrade
|
||||
.await
|
||||
.map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?;
|
||||
.map_err(|e| error!("Init http upgrade service error: {:?}", e))?;
|
||||
Some(upgrade)
|
||||
}
|
||||
None => None,
|
||||
@ -387,7 +449,7 @@ where
|
||||
|
||||
let service = service
|
||||
.await
|
||||
.map_err(|e| log::error!("Init http service error: {:?}", e))?;
|
||||
.map_err(|e| error!("Init http service error: {:?}", e))?;
|
||||
|
||||
Ok(HttpServiceHandler::new(
|
||||
cfg,
|
||||
@ -494,7 +556,7 @@ where
|
||||
|
||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self._poll_ready(cx).map_err(|err| {
|
||||
log::error!("HTTP service readiness error: {:?}", err);
|
||||
error!("HTTP service readiness error: {:?}", err);
|
||||
DispatchError::Service(err)
|
||||
})
|
||||
}
|
||||
@ -506,10 +568,11 @@ where
|
||||
let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
|
||||
|
||||
match proto {
|
||||
#[cfg(feature = "http2")]
|
||||
Protocol::Http2 => HttpServiceHandlerResponse {
|
||||
state: State::H2Handshake {
|
||||
handshake: Some((
|
||||
h2::handshake_with_timeout(io, &self.cfg),
|
||||
crate::h2::handshake_with_timeout(io, &self.cfg),
|
||||
self.cfg.clone(),
|
||||
self.flow.clone(),
|
||||
conn_data,
|
||||
@ -518,6 +581,11 @@ where
|
||||
},
|
||||
},
|
||||
|
||||
#[cfg(not(feature = "http2"))]
|
||||
Protocol::Http2 => {
|
||||
panic!("HTTP/2 support is disabled (enable with the `http2` feature flag)")
|
||||
}
|
||||
|
||||
Protocol::Http1 => HttpServiceHandlerResponse {
|
||||
state: State::H1 {
|
||||
dispatcher: h1::Dispatcher::new(
|
||||
@ -535,6 +603,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "http2"))]
|
||||
pin_project! {
|
||||
#[project = StateProj]
|
||||
enum State<T, S, B, X, U>
|
||||
@ -556,10 +625,37 @@ pin_project! {
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
H1 { #[pin] dispatcher: h1::Dispatcher<T, S, B, X, U> },
|
||||
H2 { #[pin] dispatcher: h2::Dispatcher<T, S, B, X, U> },
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http2")]
|
||||
pin_project! {
|
||||
#[project = StateProj]
|
||||
enum State<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead,
|
||||
T: AsyncWrite,
|
||||
T: Unpin,
|
||||
|
||||
S: Service<Request>,
|
||||
S::Future: 'static,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
|
||||
B: MessageBody,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
H1 { #[pin] dispatcher: h1::Dispatcher<T, S, B, X, U> },
|
||||
|
||||
H2 { #[pin] dispatcher: crate::h2::Dispatcher<T, S, B, X, U> },
|
||||
|
||||
H2Handshake {
|
||||
handshake: Option<(
|
||||
h2::HandshakeWithTimeout<T>,
|
||||
crate::h2::HandshakeWithTimeout<T>,
|
||||
ServiceConfig,
|
||||
Rc<HttpFlow<S, X, U>>,
|
||||
OnConnectData,
|
||||
@ -618,21 +714,25 @@ where
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.as_mut().project().state.project() {
|
||||
StateProj::H1 { dispatcher } => dispatcher.poll(cx),
|
||||
|
||||
#[cfg(feature = "http2")]
|
||||
StateProj::H2 { dispatcher } => dispatcher.poll(cx),
|
||||
|
||||
#[cfg(feature = "http2")]
|
||||
StateProj::H2Handshake { handshake: data } => {
|
||||
match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) {
|
||||
Ok((conn, timer)) => {
|
||||
let (_, config, flow, conn_data, peer_addr) = data.take().unwrap();
|
||||
|
||||
self.as_mut().project().state.set(State::H2 {
|
||||
dispatcher: h2::Dispatcher::new(
|
||||
dispatcher: crate::h2::Dispatcher::new(
|
||||
conn, flow, config, peer_addr, conn_data, timer,
|
||||
),
|
||||
});
|
||||
self.poll(cx)
|
||||
}
|
||||
Err(err) => {
|
||||
trace!("H2 handshake error: {}", err);
|
||||
tracing::trace!("H2 handshake error: {}", err);
|
||||
Poll::Ready(Err(err))
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
//! Various testing helpers for use in internal and app tests.
|
||||
|
||||
use std::{
|
||||
cell::{Ref, RefCell},
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
io::{self, Read, Write},
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
@ -19,29 +19,7 @@ use crate::{
|
||||
Request,
|
||||
};
|
||||
|
||||
/// Test `Request` builder
|
||||
///
|
||||
/// ```ignore
|
||||
/// # use http::{header, StatusCode};
|
||||
/// # use actix_web::*;
|
||||
/// use actix_web::test::TestRequest;
|
||||
///
|
||||
/// fn index(req: &HttpRequest) -> Response {
|
||||
/// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) {
|
||||
/// Response::Ok().into()
|
||||
/// } else {
|
||||
/// Response::BadRequest().into()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let resp = TestRequest::default().insert_header("content-type", "text/plain")
|
||||
/// .run(&index)
|
||||
/// .unwrap();
|
||||
/// assert_eq!(resp.status(), StatusCode::OK);
|
||||
///
|
||||
/// let resp = TestRequest::default().run(&index).unwrap();
|
||||
/// assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
/// ```
|
||||
/// Test `Request` builder.
|
||||
pub struct TestRequest(Option<Inner>);
|
||||
|
||||
struct Inner {
|
||||
@ -157,10 +135,11 @@ fn parts(parts: &mut Option<Inner>) -> &mut Inner {
|
||||
}
|
||||
|
||||
/// Async I/O test buffer.
|
||||
#[derive(Debug)]
|
||||
pub struct TestBuffer {
|
||||
pub read_buf: BytesMut,
|
||||
pub write_buf: BytesMut,
|
||||
pub err: Option<io::Error>,
|
||||
pub read_buf: Rc<RefCell<BytesMut>>,
|
||||
pub write_buf: Rc<RefCell<BytesMut>>,
|
||||
pub err: Option<Rc<io::Error>>,
|
||||
}
|
||||
|
||||
impl TestBuffer {
|
||||
@ -170,34 +149,69 @@ impl TestBuffer {
|
||||
T: Into<BytesMut>,
|
||||
{
|
||||
Self {
|
||||
read_buf: data.into(),
|
||||
write_buf: BytesMut::new(),
|
||||
read_buf: Rc::new(RefCell::new(data.into())),
|
||||
write_buf: Rc::new(RefCell::new(BytesMut::new())),
|
||||
err: None,
|
||||
}
|
||||
}
|
||||
|
||||
// intentionally not using Clone trait
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn clone(&self) -> Self {
|
||||
Self {
|
||||
read_buf: self.read_buf.clone(),
|
||||
write_buf: self.write_buf.clone(),
|
||||
err: self.err.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new empty `TestBuffer` instance.
|
||||
pub fn empty() -> Self {
|
||||
Self::new("")
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn read_buf_slice(&self) -> Ref<'_, [u8]> {
|
||||
Ref::map(self.read_buf.borrow(), |b| b.as_ref())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn read_buf_slice_mut(&self) -> RefMut<'_, [u8]> {
|
||||
RefMut::map(self.read_buf.borrow_mut(), |b| b.as_mut())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn write_buf_slice(&self) -> Ref<'_, [u8]> {
|
||||
Ref::map(self.write_buf.borrow(), |b| b.as_ref())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn write_buf_slice_mut(&self) -> RefMut<'_, [u8]> {
|
||||
RefMut::map(self.write_buf.borrow_mut(), |b| b.as_mut())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn take_write_buf(&self) -> Bytes {
|
||||
self.write_buf.borrow_mut().split().freeze()
|
||||
}
|
||||
|
||||
/// Add data to read buffer.
|
||||
pub fn extend_read_buf<T: AsRef<[u8]>>(&mut self, data: T) {
|
||||
self.read_buf.extend_from_slice(data.as_ref())
|
||||
self.read_buf.borrow_mut().extend_from_slice(data.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl io::Read for TestBuffer {
|
||||
fn read(&mut self, dst: &mut [u8]) -> Result<usize, io::Error> {
|
||||
if self.read_buf.is_empty() {
|
||||
if self.read_buf.borrow().is_empty() {
|
||||
if self.err.is_some() {
|
||||
Err(self.err.take().unwrap())
|
||||
Err(Rc::try_unwrap(self.err.take().unwrap()).unwrap())
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::WouldBlock, ""))
|
||||
}
|
||||
} else {
|
||||
let size = std::cmp::min(self.read_buf.len(), dst.len());
|
||||
let b = self.read_buf.split_to(size);
|
||||
let size = std::cmp::min(self.read_buf.borrow().len(), dst.len());
|
||||
let b = self.read_buf.borrow_mut().split_to(size);
|
||||
dst[..size].copy_from_slice(&b);
|
||||
Ok(size)
|
||||
}
|
||||
@ -206,7 +220,7 @@ impl io::Read for TestBuffer {
|
||||
|
||||
impl io::Write for TestBuffer {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.write_buf.extend(buf);
|
||||
self.write_buf.borrow_mut().extend(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
|
@ -2,13 +2,16 @@ use actix_codec::{Decoder, Encoder};
|
||||
use bitflags::bitflags;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use bytestring::ByteString;
|
||||
use tracing::error;
|
||||
|
||||
use super::frame::Parser;
|
||||
use super::proto::{CloseReason, OpCode};
|
||||
use super::ProtocolError;
|
||||
use super::{
|
||||
frame::Parser,
|
||||
proto::{CloseReason, OpCode},
|
||||
ProtocolError,
|
||||
};
|
||||
|
||||
/// A WebSocket message.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Message {
|
||||
/// Text message.
|
||||
Text(ByteString),
|
||||
@ -33,7 +36,7 @@ pub enum Message {
|
||||
}
|
||||
|
||||
/// A WebSocket frame.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Frame {
|
||||
/// Text frame. Note that the codec does not validate UTF-8 encoding.
|
||||
Text(Bytes),
|
||||
@ -55,7 +58,7 @@ pub enum Frame {
|
||||
}
|
||||
|
||||
/// A WebSocket continuation item.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Item {
|
||||
FirstText(Bytes),
|
||||
FirstBinary(Bytes),
|
||||
|
@ -1,6 +1,8 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use actix_service::{IntoService, Service};
|
||||
@ -71,8 +73,8 @@ mod inner {
|
||||
use actix_service::{IntoService, Service};
|
||||
use futures_core::stream::Stream;
|
||||
use local_channel::mpsc;
|
||||
use log::debug;
|
||||
use pin_project_lite::pin_project;
|
||||
use tracing::debug;
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed};
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use log::debug;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::ws::mask::apply_mask;
|
||||
use crate::ws::proto::{CloseCode, CloseReason, OpCode};
|
||||
use crate::ws::ProtocolError;
|
||||
use super::{
|
||||
mask::apply_mask,
|
||||
proto::{CloseCode, CloseReason, OpCode},
|
||||
ProtocolError,
|
||||
};
|
||||
|
||||
/// A struct representing a WebSocket frame.
|
||||
#[derive(Debug)]
|
||||
@ -15,7 +17,6 @@ impl Parser {
|
||||
fn parse_metadata(
|
||||
src: &[u8],
|
||||
server: bool,
|
||||
max_size: usize,
|
||||
) -> Result<Option<(usize, bool, OpCode, usize, Option<[u8; 4]>)>, ProtocolError> {
|
||||
let chunk_len = src.len();
|
||||
|
||||
@ -58,20 +59,12 @@ impl Parser {
|
||||
return Ok(None);
|
||||
}
|
||||
let len = u64::from_be_bytes(TryFrom::try_from(&src[idx..idx + 8]).unwrap());
|
||||
if len > max_size as u64 {
|
||||
return Err(ProtocolError::Overflow);
|
||||
}
|
||||
idx += 8;
|
||||
len as usize
|
||||
} else {
|
||||
len as usize
|
||||
};
|
||||
|
||||
// check for max allowed size
|
||||
if length > max_size {
|
||||
return Err(ProtocolError::Overflow);
|
||||
}
|
||||
|
||||
let mask = if server {
|
||||
if chunk_len < idx + 4 {
|
||||
return Ok(None);
|
||||
@ -96,11 +89,10 @@ impl Parser {
|
||||
max_size: usize,
|
||||
) -> Result<Option<(bool, OpCode, Option<BytesMut>)>, ProtocolError> {
|
||||
// try to parse ws frame metadata
|
||||
let (idx, finished, opcode, length, mask) =
|
||||
match Parser::parse_metadata(src, server, max_size)? {
|
||||
None => return Ok(None),
|
||||
Some(res) => res,
|
||||
};
|
||||
let (idx, finished, opcode, length, mask) = match Parser::parse_metadata(src, server)? {
|
||||
None => return Ok(None),
|
||||
Some(res) => res,
|
||||
};
|
||||
|
||||
// not enough data
|
||||
if src.len() < idx + length {
|
||||
@ -110,6 +102,13 @@ impl Parser {
|
||||
// remove prefix
|
||||
src.advance(idx);
|
||||
|
||||
// check for max allowed size
|
||||
if length > max_size {
|
||||
// drop the payload
|
||||
src.advance(length);
|
||||
return Err(ProtocolError::Overflow);
|
||||
}
|
||||
|
||||
// no need for body
|
||||
if length == 0 {
|
||||
return Ok(Some((finished, opcode, None)));
|
||||
@ -337,6 +336,30 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_frame_max_size_recoverability() {
|
||||
let mut buf = BytesMut::new();
|
||||
// The first text frame with length == 2, payload doesn't matter.
|
||||
buf.extend(&[0b0000_0001u8, 0b0000_0010u8, 0b0000_0000u8, 0b0000_0000u8]);
|
||||
// Next binary frame with length == 2 and payload == `[0x1111_1111u8, 0x1111_1111u8]`.
|
||||
buf.extend(&[0b0000_0010u8, 0b0000_0010u8, 0b1111_1111u8, 0b1111_1111u8]);
|
||||
|
||||
assert_eq!(buf.len(), 8);
|
||||
assert!(matches!(
|
||||
Parser::parse(&mut buf, false, 1),
|
||||
Err(ProtocolError::Overflow)
|
||||
));
|
||||
assert_eq!(buf.len(), 4);
|
||||
let frame = extract(Parser::parse(&mut buf, false, 2));
|
||||
assert!(!frame.finished);
|
||||
assert_eq!(frame.opcode, OpCode::Binary);
|
||||
assert_eq!(
|
||||
frame.payload,
|
||||
Bytes::from(vec![0b1111_1111u8, 0b1111_1111u8])
|
||||
);
|
||||
assert_eq!(buf.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ping_frame() {
|
||||
let mut buf = BytesMut::new();
|
||||
|
@ -47,40 +47,6 @@ pub fn apply_mask_fast32(buf: &mut [u8], mask: [u8; 4]) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// legacy test from old apply mask test. kept for now for back compat test.
|
||||
// TODO: remove it and favor the other test.
|
||||
#[test]
|
||||
fn test_apply_mask_legacy() {
|
||||
let mask = [0x6d, 0xb6, 0xb2, 0x80];
|
||||
|
||||
let unmasked = vec![
|
||||
0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, 0x74, 0xf9,
|
||||
0x12, 0x03,
|
||||
];
|
||||
|
||||
// Check masking with proper alignment.
|
||||
{
|
||||
let mut masked = unmasked.clone();
|
||||
apply_mask_fallback(&mut masked, mask);
|
||||
|
||||
let mut masked_fast = unmasked.clone();
|
||||
apply_mask(&mut masked_fast, mask);
|
||||
|
||||
assert_eq!(masked, masked_fast);
|
||||
}
|
||||
|
||||
// Check masking without alignment.
|
||||
{
|
||||
let mut masked = unmasked.clone();
|
||||
apply_mask_fallback(&mut masked[1..], mask);
|
||||
|
||||
let mut masked_fast = unmasked;
|
||||
apply_mask(&mut masked_fast[1..], mask);
|
||||
|
||||
assert_eq!(masked, masked_fast);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_mask() {
|
||||
let mask = [0x6d, 0xb6, 0xb2, 0x80];
|
||||
|
@ -67,7 +67,7 @@ pub enum ProtocolError {
|
||||
}
|
||||
|
||||
/// WebSocket handshake errors
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Display, Error)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)]
|
||||
pub enum HandshakeError {
|
||||
/// Only get method is allowed.
|
||||
#[display(fmt = "Method not allowed.")]
|
||||
|
@ -3,6 +3,8 @@ use std::{
|
||||
fmt,
|
||||
};
|
||||
|
||||
use tracing::error;
|
||||
|
||||
/// Operation codes defined in [RFC 6455 §11.8].
|
||||
///
|
||||
/// [RFC 6455]: https://datatracker.ietf.org/doc/html/rfc6455#section-11.8
|
||||
@ -58,7 +60,7 @@ impl From<OpCode> for u8 {
|
||||
Ping => 9,
|
||||
Pong => 10,
|
||||
Bad => {
|
||||
log::error!("Attempted to convert invalid opcode to u8. This is a bug.");
|
||||
error!("Attempted to convert invalid opcode to u8. This is a bug.");
|
||||
8 // if this somehow happens, a close frame will help us tear down quickly
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World";
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_v2() {
|
||||
async fn h1_v2() {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -59,7 +59,7 @@ async fn test_h1_v2() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_connection_close() {
|
||||
async fn connection_close() {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -73,7 +73,7 @@ async fn test_connection_close() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_with_query_parameter() {
|
||||
async fn with_query_parameter() {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.finish(|req: Request| async move {
|
||||
@ -104,7 +104,7 @@ impl From<ExpectFailed> for Response<BoxBody> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_expect() {
|
||||
async fn h1_expect() {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.expect(|req: Request| async {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::io;
|
||||
use std::{io, time::Duration};
|
||||
|
||||
use actix_http::{error::Error, HttpService, Response};
|
||||
use actix_server::Server;
|
||||
@ -19,7 +19,7 @@ async fn h2_ping_pong() -> io::Result<()> {
|
||||
.workers(1)
|
||||
.listen("h2_ping_pong", lst, || {
|
||||
HttpService::build()
|
||||
.keep_alive(3)
|
||||
.keep_alive(Duration::from_secs(3))
|
||||
.h2(|_| async { Ok::<_, Error>(Response::ok()) })
|
||||
.tcp()
|
||||
})?
|
||||
@ -92,10 +92,10 @@ async fn h2_handshake_timeout() -> io::Result<()> {
|
||||
.workers(1)
|
||||
.listen("h2_ping_pong", lst, || {
|
||||
HttpService::build()
|
||||
.keep_alive(30)
|
||||
.keep_alive(Duration::from_secs(30))
|
||||
// set first request timeout to 5 seconds.
|
||||
// this is the timeout used for http2 handshake.
|
||||
.client_timeout(5000)
|
||||
.client_request_timeout(Duration::from_secs(5))
|
||||
.h2(|_| async { Ok::<_, Error>(Response::ok()) })
|
||||
.tcp()
|
||||
})?
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
extern crate tls_openssl as openssl;
|
||||
|
||||
use std::{convert::Infallible, io};
|
||||
use std::{convert::Infallible, io, time::Duration};
|
||||
|
||||
use actix_http::{
|
||||
body::{BodyStream, BoxBody, SizedStream},
|
||||
error::PayloadError,
|
||||
header::{self, HeaderValue},
|
||||
Error, HttpService, Method, Request, Response, StatusCode, Version,
|
||||
Error, HttpService, Method, Request, Response, StatusCode, TlsAcceptorConfig, Version,
|
||||
};
|
||||
use actix_http_test::test_server;
|
||||
use actix_service::{fn_service, ServiceFactoryExt};
|
||||
@ -66,7 +66,7 @@ fn tls_config() -> SslAcceptor {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2() -> io::Result<()> {
|
||||
async fn h2() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| ok::<_, Error>(Response::ok()))
|
||||
@ -81,7 +81,7 @@ async fn test_h2() -> io::Result<()> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_1() -> io::Result<()> {
|
||||
async fn h2_1() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.finish(|req: Request| {
|
||||
@ -89,7 +89,10 @@ async fn test_h2_1() -> io::Result<()> {
|
||||
assert_eq!(req.version(), Version::HTTP_2);
|
||||
ok::<_, Error>(Response::ok())
|
||||
})
|
||||
.openssl(tls_config())
|
||||
.openssl_with_config(
|
||||
tls_config(),
|
||||
TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)),
|
||||
)
|
||||
.map_err(|_| ())
|
||||
})
|
||||
.await;
|
||||
@ -100,7 +103,7 @@ async fn test_h2_1() -> io::Result<()> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_body() -> io::Result<()> {
|
||||
async fn h2_body() -> io::Result<()> {
|
||||
let data = "HELLOWORLD".to_owned().repeat(64 * 1024); // 640 KiB
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
@ -122,7 +125,7 @@ async fn test_h2_body() -> io::Result<()> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_content_length() {
|
||||
async fn h2_content_length() {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|req: Request| {
|
||||
@ -164,7 +167,7 @@ async fn test_h2_content_length() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_headers() {
|
||||
async fn h2_headers() {
|
||||
let data = STR.repeat(10);
|
||||
let data2 = data.clone();
|
||||
|
||||
@ -229,7 +232,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World";
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_body2() {
|
||||
async fn h2_body2() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -247,7 +250,7 @@ async fn test_h2_body2() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_head_empty() {
|
||||
async fn h2_head_empty() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -271,7 +274,7 @@ async fn test_h2_head_empty() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_head_binary() {
|
||||
async fn h2_head_binary() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -294,7 +297,7 @@ async fn test_h2_head_binary() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_head_binary2() {
|
||||
async fn h2_head_binary2() {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -313,7 +316,7 @@ async fn test_h2_head_binary2() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_body_length() {
|
||||
async fn h2_body_length() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| async {
|
||||
@ -338,7 +341,7 @@ async fn test_h2_body_length() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_body_chunked_explicit() {
|
||||
async fn h2_body_chunked_explicit() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| {
|
||||
@ -366,7 +369,7 @@ async fn test_h2_body_chunked_explicit() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_response_http_error_handling() {
|
||||
async fn h2_response_http_error_handling() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(fn_service(|_| {
|
||||
@ -406,7 +409,7 @@ impl From<BadRequest> for Response<BoxBody> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_service_error() {
|
||||
async fn h2_service_error() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| err::<Response<BoxBody>, _>(BadRequest))
|
||||
@ -424,7 +427,7 @@ async fn test_h2_service_error() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_on_connect() {
|
||||
async fn h2_on_connect() {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.on_connect_ext(|_, data| {
|
||||
|
@ -8,13 +8,14 @@ use std::{
|
||||
net::{SocketAddr, TcpStream as StdTcpStream},
|
||||
sync::Arc,
|
||||
task::Poll,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use actix_http::{
|
||||
body::{BodyStream, BoxBody, SizedStream},
|
||||
error::PayloadError,
|
||||
header::{self, HeaderName, HeaderValue},
|
||||
Error, HttpService, Method, Request, Response, StatusCode, Version,
|
||||
Error, HttpService, Method, Request, Response, StatusCode, TlsAcceptorConfig, Version,
|
||||
};
|
||||
use actix_http_test::test_server;
|
||||
use actix_rt::pin;
|
||||
@ -106,7 +107,7 @@ pub fn get_negotiated_alpn_protocol(
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1() -> io::Result<()> {
|
||||
async fn h1() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Error>(Response::ok()))
|
||||
@ -120,7 +121,7 @@ async fn test_h1() -> io::Result<()> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2() -> io::Result<()> {
|
||||
async fn h2() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| ok::<_, Error>(Response::ok()))
|
||||
@ -134,7 +135,7 @@ async fn test_h2() -> io::Result<()> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_1() -> io::Result<()> {
|
||||
async fn h1_1() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h1(|req: Request| {
|
||||
@ -152,7 +153,7 @@ async fn test_h1_1() -> io::Result<()> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_1() -> io::Result<()> {
|
||||
async fn h2_1() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.finish(|req: Request| {
|
||||
@ -160,7 +161,10 @@ async fn test_h2_1() -> io::Result<()> {
|
||||
assert_eq!(req.version(), Version::HTTP_2);
|
||||
ok::<_, Error>(Response::ok())
|
||||
})
|
||||
.rustls(tls_config())
|
||||
.rustls_with_config(
|
||||
tls_config(),
|
||||
TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)),
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
@ -170,7 +174,7 @@ async fn test_h2_1() -> io::Result<()> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_body1() -> io::Result<()> {
|
||||
async fn h2_body1() -> io::Result<()> {
|
||||
let data = "HELLOWORLD".to_owned().repeat(64 * 1024);
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
@ -191,7 +195,7 @@ async fn test_h2_body1() -> io::Result<()> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_content_length() {
|
||||
async fn h2_content_length() {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|req: Request| {
|
||||
@ -212,6 +216,7 @@ async fn test_h2_content_length() {
|
||||
let value = HeaderValue::from_static("0");
|
||||
|
||||
{
|
||||
#[allow(clippy::single_element_loop)]
|
||||
for &i in &[0] {
|
||||
let req = srv
|
||||
.request(Method::HEAD, srv.surl(&format!("/{}", i)))
|
||||
@ -226,6 +231,7 @@ async fn test_h2_content_length() {
|
||||
// assert_eq!(response.headers().get(&header), None);
|
||||
}
|
||||
|
||||
#[allow(clippy::single_element_loop)]
|
||||
for &i in &[1] {
|
||||
let req = srv
|
||||
.request(Method::GET, srv.surl(&format!("/{}", i)))
|
||||
@ -245,7 +251,7 @@ async fn test_h2_content_length() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_headers() {
|
||||
async fn h2_headers() {
|
||||
let data = STR.repeat(10);
|
||||
let data2 = data.clone();
|
||||
|
||||
@ -309,7 +315,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World";
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_body2() {
|
||||
async fn h2_body2() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -326,7 +332,7 @@ async fn test_h2_body2() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_head_empty() {
|
||||
async fn h2_head_empty() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -352,7 +358,7 @@ async fn test_h2_head_empty() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_head_binary() {
|
||||
async fn h2_head_binary() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -377,7 +383,7 @@ async fn test_h2_head_binary() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_head_binary2() {
|
||||
async fn h2_head_binary2() {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -398,7 +404,7 @@ async fn test_h2_head_binary2() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_body_length() {
|
||||
async fn h2_body_length() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| {
|
||||
@ -420,7 +426,7 @@ async fn test_h2_body_length() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_body_chunked_explicit() {
|
||||
async fn h2_body_chunked_explicit() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| {
|
||||
@ -447,7 +453,7 @@ async fn test_h2_body_chunked_explicit() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_response_http_error_handling() {
|
||||
async fn h2_response_http_error_handling() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(fn_factory_with_config(|_: ()| {
|
||||
@ -486,7 +492,7 @@ impl From<BadRequest> for Response<BoxBody> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_service_error() {
|
||||
async fn h2_service_error() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| err::<Response<BoxBody>, _>(BadRequest))
|
||||
@ -503,7 +509,7 @@ async fn test_h2_service_error() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_service_error() {
|
||||
async fn h1_service_error() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h1(|_| err::<Response<BoxBody>, _>(BadRequest))
|
||||
@ -524,7 +530,7 @@ const HTTP1_1_ALPN_PROTOCOL: &[u8] = b"http/1.1";
|
||||
const CUSTOM_ALPN_PROTOCOL: &[u8] = b"custom";
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_alpn_h1() -> io::Result<()> {
|
||||
async fn alpn_h1() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
let mut config = tls_config();
|
||||
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
|
||||
@ -546,7 +552,7 @@ async fn test_alpn_h1() -> io::Result<()> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_alpn_h2() -> io::Result<()> {
|
||||
async fn alpn_h2() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
let mut config = tls_config();
|
||||
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
|
||||
@ -572,7 +578,7 @@ async fn test_alpn_h2() -> io::Result<()> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_alpn_h2_1() -> io::Result<()> {
|
||||
async fn alpn_h2_1() -> io::Result<()> {
|
||||
let srv = test_server(move || {
|
||||
let mut config = tls_config();
|
||||
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user