mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-03 01:34:32 +02:00
Compare commits
164 Commits
files-v0.6
...
codegen-v4
Author | SHA1 | Date | |
---|---|---|---|
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 }
|
185
.github/workflows/ci-post-merge.yml
vendored
Normal file
185
.github/workflows/ci-post-merge.yml
vendored
Normal file
@ -0,0 +1,185 @@
|
||||
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
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# install OpenSSL on Windows
|
||||
# TODO: GitHub actions docs state that OpenSSL is
|
||||
# already installed on these Windows machines somewhere
|
||||
- name: Set vcpkg root
|
||||
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
- name: Install OpenSSL
|
||||
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||
run: vcpkg install openssl:x64-windows
|
||||
|
||||
- name: Install ${{ matrix.version }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
- name: Install cargo-hack
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-hack
|
||||
|
||||
- name: check minimal
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-min }
|
||||
|
||||
- name: check default
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-default }
|
||||
|
||||
- name: tests
|
||||
timeout-minutes: 60
|
||||
run: |
|
||||
cargo test --lib --tests -p=actix-router --all-features
|
||||
cargo test --lib --tests -p=actix-http --all-features
|
||||
cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
|
||||
cargo test --lib --tests -p=actix-web-codegen --all-features
|
||||
cargo test --lib --tests -p=awc --all-features
|
||||
cargo test --lib --tests -p=actix-http-test --all-features
|
||||
cargo test --lib --tests -p=actix-test --all-features
|
||||
cargo test --lib --tests -p=actix-files
|
||||
cargo test --lib --tests -p=actix-multipart --all-features
|
||||
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||
|
||||
- name: tests (io-uring)
|
||||
if: matrix.target.os == 'ubuntu-latest'
|
||||
timeout-minutes: 60
|
||||
run: >
|
||||
sudo bash -c "ulimit -Sl 512
|
||||
&& ulimit -Hl 512
|
||||
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
||||
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
|
||||
|
||||
- name: Clear the cargo caches
|
||||
run: |
|
||||
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
||||
|
||||
ci_feature_powerset_check:
|
||||
name: Verify Feature Combinations
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable-x86_64-unknown-linux-gnu
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
- name: Install cargo-hack
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-hack
|
||||
|
||||
- name: check feature combinations
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-all-feature-powerset }
|
||||
|
||||
- name: check feature combinations
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-all-feature-powerset-linux }
|
||||
|
||||
# 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 }
|
||||
|
||||
nextest:
|
||||
name: nextest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
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: Install cargo-nextest
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-nextest
|
||||
|
||||
- name: Test with cargo-nextest
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: nextest
|
||||
args: run
|
3
.github/workflows/ci.yml
vendored
3
.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.54.0 # MSRV
|
||||
- stable
|
||||
- nightly
|
||||
|
||||
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
||||
runs-on: ${{ matrix.target.os }}
|
||||
|
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
|
||||
|
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.
|
@ -3,12 +3,34 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.6.0-beta.16 - 2022-01-31
|
||||
- No significant changes since `0.6.0-beta.15`.
|
||||
|
||||
|
||||
## 0.6.0-beta.15 - 2022-01-21
|
||||
- No significant changes since `0.6.0-beta.14`.
|
||||
|
||||
|
||||
## 0.6.0-beta.14 - 2022-01-14
|
||||
- The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583]
|
||||
|
||||
[#2583]: https://github.com/actix/actix-web/pull/2583
|
||||
|
||||
|
||||
## 0.6.0-beta.13 - 2022-01-04
|
||||
- The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398]
|
||||
- The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398]
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
[#2398]: https://github.com/actix/actix-web/pull/2398
|
||||
|
||||
|
||||
## 0.6.0-beta.12 - 2021-12-29
|
||||
* No significant changes since `0.6.0-beta.11`.
|
||||
- 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.0-beta.16"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"fakeshadow <24548779@qq.com>",
|
||||
@ -22,10 +22,10 @@ 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.0.0"
|
||||
actix-service = "2"
|
||||
actix-utils = "3"
|
||||
actix-web = { version = "4.0.0-beta.17", default-features = false }
|
||||
actix-web = { version = "4.0.0", default-features = false }
|
||||
|
||||
askama_escape = "0.10"
|
||||
bitflags = "1"
|
||||
@ -39,9 +39,10 @@ mime_guess = "2.0.1"
|
||||
percent-encoding = "2.1"
|
||||
pin-project-lite = "0.2.7"
|
||||
|
||||
tokio-uring = { version = "0.1", optional = true }
|
||||
tokio-uring = { version = "0.2", optional = true, features = ["bytes"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-test = "0.1.0-beta.10"
|
||||
actix-web = "4.0.0-beta.17"
|
||||
actix-test = "0.1.0-beta.13"
|
||||
actix-web = "4.0.0"
|
||||
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.0-beta.16)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.6.0-beta.12)
|
||||
[](https://deps.rs/crate/actix-files/0.6.0-beta.16)
|
||||
[](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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,14 +40,23 @@ 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)
|
||||
@ -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,
|
||||
};
|
||||
|
@ -23,16 +23,23 @@ impl ResponseError for FilesError {
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Display, Debug, PartialEq)]
|
||||
#[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"
|
||||
@ -379,7 +379,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"
|
||||
@ -597,7 +597,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 +633,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 +803,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,
|
||||
},
|
||||
@ -40,7 +38,7 @@ bitflags! {
|
||||
|
||||
impl Default for Flags {
|
||||
fn default() -> Self {
|
||||
Flags::from_bits_truncate(0b0000_0111)
|
||||
Flags::from_bits_truncate(0b0000_1111)
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,12 +66,12 @@ 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,
|
||||
@ -83,32 +81,6 @@ pub struct NamedFile {
|
||||
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 +96,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();
|
||||
@ -224,7 +196,6 @@ impl NamedFile {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
/// Attempts to open a file in read-only mode.
|
||||
///
|
||||
/// # Examples
|
||||
@ -232,11 +203,13 @@ 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)
|
||||
}
|
||||
|
||||
#[allow(rustdoc::broken_intra_doc_links)]
|
||||
/// Attempts to open a file asynchronously in read-only mode.
|
||||
///
|
||||
/// When the `experimental-io-uring` crate feature is enabled, this will be async.
|
||||
@ -295,23 +268,21 @@ impl NamedFile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the MIME Content-Type for serving this file. By default
|
||||
/// the Content-Type is inferred from the filename extension.
|
||||
/// Set the MIME Content-Type for 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 {
|
||||
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 {
|
||||
self.content_disposition = cd;
|
||||
@ -328,16 +299,18 @@ impl NamedFile {
|
||||
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 +319,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 +337,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 +359,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 +378,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 +394,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 +452,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 +467,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 +491,12 @@ impl NamedFile {
|
||||
length = ranges[0].length;
|
||||
offset = ranges[0].start;
|
||||
|
||||
res.encoding(ContentEncoding::Identity);
|
||||
// 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 +605,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,
|
||||
)),
|
||||
|
@ -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,6 @@ 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")),
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,18 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 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
|
||||
- Update `actix-server` to `2.0.0-rc.2`. [#2550]
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http-test"
|
||||
version = "3.0.0-beta.10"
|
||||
version = "3.0.0-beta.13"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Various helpers for Actix applications to use during testing"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
@ -30,12 +30,12 @@ openssl = ["tls-openssl", "awc/openssl"]
|
||||
|
||||
[dependencies]
|
||||
actix-service = "2.0.0"
|
||||
actix-codec = "0.4.1"
|
||||
actix-tls = "3.0.0"
|
||||
actix-codec = "0.5"
|
||||
actix-tls = "3"
|
||||
actix-utils = "3.0.0"
|
||||
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.0.0-beta.21", 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.0.0", default-features = false, features = ["cookies"] }
|
||||
actix-http = "3.0.0"
|
||||
|
@ -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-beta.13)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br>
|
||||
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.10)
|
||||
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.13)
|
||||
[](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
|
||||
|
@ -3,8 +3,393 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 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 +612,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]
|
||||
@ -371,6 +756,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.0.0"
|
||||
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", "openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"]
|
||||
|
||||
[lib]
|
||||
name = "actix_http"
|
||||
@ -26,68 +29,85 @@ path = "src/lib.rs"
|
||||
[features]
|
||||
default = []
|
||||
|
||||
# openssl
|
||||
# HTTP/2 protocol support
|
||||
http2 = ["h2"]
|
||||
|
||||
# WebSocket protocol implementation
|
||||
ws = [
|
||||
"local-channel",
|
||||
"base64",
|
||||
"rand",
|
||||
"sha-1",
|
||||
]
|
||||
|
||||
# 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"
|
||||
|
||||
# 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 }
|
||||
sha-1 = { 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.10", 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.0.0-beta.13", features = ["openssl"] }
|
||||
actix-server = "2"
|
||||
actix-tls = { version = "3", features = ["openssl"] }
|
||||
actix-web = "4.0.0"
|
||||
|
||||
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"] }
|
||||
memchr = "2.4"
|
||||
once_cell = "1.9"
|
||||
rcgen = "0.8"
|
||||
regex = "1.3"
|
||||
rustls-pemfile = "0.2"
|
||||
|
@ -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.0.0)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.0.0-beta.17)
|
||||
[](https://deps.rs/crate/actix-http/3.0.0)
|
||||
[](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
|
||||
|
||||
|
@ -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 {
|
||||
|
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,4 +1,4 @@
|
||||
use std::io;
|
||||
use std::{io, time::Duration};
|
||||
|
||||
use actix_http::{Error, HttpService, Request, Response, StatusCode};
|
||||
use actix_server::Server;
|
||||
@ -13,8 +13,9 @@ 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 {
|
||||
@ -23,12 +24,13 @@ async fn main() -> io::Result<()> {
|
||||
|
||||
log::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,4 +1,4 @@
|
||||
use std::{convert::Infallible, io};
|
||||
use std::{convert::Infallible, io, time::Duration};
|
||||
|
||||
use actix_http::{
|
||||
header::HeaderValue, HttpMessage, HttpService, Request, Response, StatusCode,
|
||||
@ -12,8 +12,8 @@ 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);
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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,8 @@ 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")]
|
||||
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 +222,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 +244,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,71 +1,36 @@
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,34 +38,22 @@ impl 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,9 +14,6 @@ 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};
|
||||
|
||||
@ -26,7 +23,6 @@ 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 +52,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 +164,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 +172,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 +253,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()
|
||||
.insert(header::VARY, HeaderValue::from_static("accept-encoding"));
|
||||
|
||||
head.no_chunking(false);
|
||||
}
|
||||
@ -268,7 +269,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 +278,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 +293,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 +309,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 +325,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,10 +353,10 @@ 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);
|
||||
log::trace!("Error decoding br encoding: {}", err);
|
||||
Err(err)
|
||||
}
|
||||
},
|
||||
@ -366,7 +365,7 @@ impl ContentEncoder {
|
||||
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
trace!("Error decoding gzip encoding: {}", err);
|
||||
log::trace!("Error decoding gzip encoding: {}", err);
|
||||
Err(err)
|
||||
}
|
||||
},
|
||||
@ -375,7 +374,7 @@ impl ContentEncoder {
|
||||
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
trace!("Error decoding deflate encoding: {}", err);
|
||||
log::trace!("Error decoding deflate encoding: {}", err);
|
||||
Err(err)
|
||||
}
|
||||
},
|
||||
@ -384,7 +383,7 @@ impl ContentEncoder {
|
||||
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
trace!("Error decoding ztsd encoding: {}", err);
|
||||
log::trace!("Error decoding ztsd encoding: {}", err);
|
||||
Err(err)
|
||||
}
|
||||
},
|
||||
@ -392,15 +391,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 +417,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,20 @@ 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")]
|
||||
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 +319,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 +327,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 +350,7 @@ pub enum DispatchError {
|
||||
|
||||
/// HTTP/2 error.
|
||||
#[display(fmt = "{}", _0)]
|
||||
#[cfg(feature = "http2")]
|
||||
H2(h2::Error),
|
||||
|
||||
/// The first request did not complete within the specified timeout.
|
||||
@ -365,6 +361,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 +373,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 +388,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))]
|
||||
#[non_exhaustive]
|
||||
pub enum ContentTypeError {
|
||||
/// Can not parse content type
|
||||
@ -397,28 +400,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,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)?;
|
||||
}
|
||||
|
@ -209,15 +209,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 +233,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");
|
||||
@ -380,34 +382,36 @@ impl HeaderIndex {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// Http payload item
|
||||
/// 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.
|
||||
/// If a message body does not use `Transfer-Encoding`, it should include a `Content-Length`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
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 }
|
||||
}
|
||||
@ -415,25 +419,26 @@ impl PayloadDecoder {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
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 +468,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 +494,7 @@ impl Decoder for PayloadDecoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kind::Eof => {
|
||||
if src.is_empty() {
|
||||
Ok(None)
|
||||
|
File diff suppressed because it is too large
Load Diff
973
actix-http/src/h1/dispatcher_tests.rs
Normal file
973
actix-http/src/h1/dispatcher_tests.rs
Normal file
@ -0,0 +1,973 @@
|
||||
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;
|
||||
}
|
||||
|
||||
#[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,
|
||||
|
@ -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>),
|
||||
}
|
||||
|
||||
|
80
actix-http/src/h1/timer.rs
Normal file
80
actix-http/src/h1/timer.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use std::{fmt, future::Future, pin::Pin, task::Context};
|
||||
|
||||
use actix_rt::time::{Instant, Sleep};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) enum TimerState {
|
||||
Disabled,
|
||||
Inactive,
|
||||
Active { timer: Pin<Box<Sleep>> },
|
||||
}
|
||||
|
||||
impl TimerState {
|
||||
pub(super) fn new(enabled: bool) -> Self {
|
||||
if enabled {
|
||||
Self::Inactive
|
||||
} else {
|
||||
Self::Disabled
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_enabled(&self) -> bool {
|
||||
matches!(self, Self::Active { .. } | Self::Inactive)
|
||||
}
|
||||
|
||||
pub(super) fn set(&mut self, timer: Sleep, line: u32) {
|
||||
if matches!(self, Self::Disabled) {
|
||||
log::trace!("setting disabled timer from line {}", line);
|
||||
}
|
||||
|
||||
*self = Self::Active {
|
||||
timer: Box::pin(timer),
|
||||
};
|
||||
}
|
||||
|
||||
pub(super) fn set_and_init(&mut self, cx: &mut Context<'_>, timer: Sleep, line: u32) {
|
||||
self.set(timer, line);
|
||||
self.init(cx);
|
||||
}
|
||||
|
||||
pub(super) fn clear(&mut self, line: u32) {
|
||||
if matches!(self, Self::Disabled) {
|
||||
log::trace!("trying to clear a disabled timer from line {}", line);
|
||||
}
|
||||
|
||||
if matches!(self, Self::Inactive) {
|
||||
log::trace!("trying to clear an inactive timer from line {}", line);
|
||||
}
|
||||
|
||||
*self = Self::Inactive;
|
||||
}
|
||||
|
||||
pub(super) fn init(&mut self, cx: &mut Context<'_>) {
|
||||
if let TimerState::Active { timer } = self {
|
||||
let _ = timer.as_mut().poll(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TimerState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
TimerState::Disabled => f.write_str("timer is disabled"),
|
||||
TimerState::Inactive => f.write_str("timer is inactive"),
|
||||
TimerState::Active { timer } => {
|
||||
let deadline = timer.deadline();
|
||||
let now = Instant::now();
|
||||
|
||||
if deadline < now {
|
||||
f.write_str("timer is active and has reached deadline")
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"timer is active and due to expire in {} milliseconds",
|
||||
((deadline - now).as_secs_f32() * 1000.0)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -25,7 +25,9 @@ use pin_project_lite::pin_project;
|
||||
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,11 +59,11 @@ 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))),
|
||||
@ -141,7 +143,7 @@ where
|
||||
DispatchError::SendResponse(err) => {
|
||||
trace!("Error sending HTTP/2 response: {:?}", err)
|
||||
}
|
||||
DispatchError::SendData(err) => warn!("{:?}", err),
|
||||
DispatchError::SendData(err) => log::warn!("{:?}", err),
|
||||
DispatchError::ResponseBody(err) => {
|
||||
error!("Response payload stream error: {:?}", err)
|
||||
}
|
||||
@ -160,8 +162,8 @@ where
|
||||
Poll::Ready(_) => {
|
||||
ping_pong.on_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(()))
|
||||
@ -174,8 +176,8 @@ where
|
||||
|
||||
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;
|
||||
}
|
||||
@ -306,13 +308,22 @@ fn prepare_response(
|
||||
|
||||
// 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 +333,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) => {
|
||||
|
@ -355,7 +355,7 @@ where
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
trace!("H2 handshake error: {}", err);
|
||||
log::trace!("H2 handshake error: {}", err);
|
||||
Poll::Ready(Err(err))
|
||||
}
|
||||
},
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -24,38 +26,42 @@
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub use ::http::{uri, uri::Uri};
|
||||
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")]
|
||||
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")]
|
||||
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)]
|
||||
|
@ -5,13 +5,13 @@ use bitflags::bitflags;
|
||||
/// Represents various types of connection
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
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,6 +6,7 @@ use std::{
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_core::Stream;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use crate::error::PayloadError;
|
||||
|
||||
@ -15,7 +16,19 @@ pub type BoxedPayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadErr
|
||||
#[deprecated(since = "4.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 +45,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 +85,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),
|
||||
}
|
||||
}
|
||||
|
@ -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},
|
||||
@ -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();
|
||||
let _ = 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();
|
||||
let _ = 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);
|
||||
|
@ -19,9 +19,8 @@ use pin_project_lite::pin_project;
|
||||
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 +42,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 +57,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 +69,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 +94,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 +114,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 = ()>,
|
||||
@ -506,10 +502,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 +515,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 +537,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "http2"))]
|
||||
pin_project! {
|
||||
#[project = StateProj]
|
||||
enum State<T, S, B, X, U>
|
||||
@ -556,10 +559,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 +648,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);
|
||||
log::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,
|
||||
@ -157,10 +157,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 +171,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 +242,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())
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,11 @@ use bitflags::bitflags;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use bytestring::ByteString;
|
||||
|
||||
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)]
|
||||
@ -251,7 +253,7 @@ impl Decoder for Codec {
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
error!("Unfinished fragment {:?}", opcode);
|
||||
log::error!("Unfinished fragment {:?}", opcode);
|
||||
Err(ProtocolError::ContinuationFragment(opcode))
|
||||
}
|
||||
};
|
||||
|
@ -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};
|
||||
|
@ -3,9 +3,11 @@ use std::convert::TryFrom;
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use log::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)]
|
||||
|
@ -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];
|
||||
|
@ -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()
|
||||
})?
|
||||
|
@ -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| {
|
||||
@ -100,7 +100,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 +122,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 +164,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 +229,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 +247,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 +271,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 +294,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 +313,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 +338,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 +366,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 +406,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 +424,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| {
|
||||
|
@ -106,7 +106,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 +120,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 +134,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 +152,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| {
|
||||
@ -170,7 +170,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 +191,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| {
|
||||
@ -245,7 +245,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 +309,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 +326,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 +352,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 +377,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 +398,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 +420,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 +447,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 +486,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 +503,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 +524,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 +546,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 +572,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());
|
||||
|
@ -2,7 +2,7 @@ use std::{
|
||||
convert::Infallible,
|
||||
io::{Read, Write},
|
||||
net, thread,
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use actix_http::{
|
||||
@ -22,12 +22,12 @@ use futures_util::{
|
||||
use regex::Regex;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1() {
|
||||
async fn h1_basic() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.keep_alive(KeepAlive::Disabled)
|
||||
.client_timeout(1000)
|
||||
.client_disconnect(1000)
|
||||
.client_request_timeout(Duration::from_secs(1))
|
||||
.client_disconnect_timeout(Duration::from_secs(1))
|
||||
.h1(|req: Request| {
|
||||
assert!(req.peer_addr().is_some());
|
||||
ok::<_, Infallible>(Response::ok())
|
||||
@ -43,12 +43,12 @@ async fn test_h1() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_2() {
|
||||
async fn h1_2() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.keep_alive(KeepAlive::Disabled)
|
||||
.client_timeout(1000)
|
||||
.client_disconnect(1000)
|
||||
.client_request_timeout(Duration::from_secs(1))
|
||||
.client_disconnect_timeout(Duration::from_secs(1))
|
||||
.finish(|req: Request| {
|
||||
assert!(req.peer_addr().is_some());
|
||||
assert_eq!(req.version(), http::Version::HTTP_11);
|
||||
@ -75,7 +75,7 @@ impl From<ExpectFailed> for Response<BoxBody> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_expect_continue() {
|
||||
async fn expect_continue() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.expect(fn_service(|req: Request| {
|
||||
@ -106,7 +106,7 @@ async fn test_expect_continue() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_expect_continue_h1() {
|
||||
async fn expect_continue_h1() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.expect(fn_service(|req: Request| {
|
||||
@ -139,7 +139,7 @@ async fn test_expect_continue_h1() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_chunked_payload() {
|
||||
async fn chunked_payload() {
|
||||
let chunk_sizes = vec![32768, 32, 32768];
|
||||
let total_size: usize = chunk_sizes.iter().sum();
|
||||
|
||||
@ -197,26 +197,43 @@ async fn test_chunked_payload() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_slow_request() {
|
||||
async fn slow_request_408() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.client_timeout(100)
|
||||
.client_request_timeout(Duration::from_millis(200))
|
||||
.keep_alive(Duration::from_secs(2))
|
||||
.finish(|_| ok::<_, Infallible>(Response::ok()))
|
||||
.tcp()
|
||||
})
|
||||
.await;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
||||
let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n");
|
||||
let _ = stream.write_all(b"GET /test HTTP/1.1\r\n");
|
||||
let mut data = String::new();
|
||||
let _ = stream.read_to_string(&mut data);
|
||||
assert!(data.starts_with("HTTP/1.1 408 Request Timeout"));
|
||||
assert!(
|
||||
data.starts_with("HTTP/1.1 408 Request Timeout"),
|
||||
"response was not 408: {}",
|
||||
data
|
||||
);
|
||||
|
||||
let diff = start.elapsed();
|
||||
|
||||
if diff < Duration::from_secs(1) {
|
||||
// test success
|
||||
} else if diff < Duration::from_secs(3) {
|
||||
panic!("request seems to have wrongly timed-out according to keep-alive");
|
||||
} else {
|
||||
panic!("request took way too long to time out");
|
||||
}
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http1_malformed_request() {
|
||||
async fn http1_malformed_request() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
@ -234,7 +251,7 @@ async fn test_http1_malformed_request() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http1_keepalive() {
|
||||
async fn http1_keepalive() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
@ -257,23 +274,25 @@ async fn test_http1_keepalive() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http1_keepalive_timeout() {
|
||||
async fn http1_keepalive_timeout() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.keep_alive(1)
|
||||
.keep_alive(Duration::from_secs(1))
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
.tcp()
|
||||
})
|
||||
.await;
|
||||
|
||||
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
||||
let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n\r\n");
|
||||
let mut data = vec![0; 1024];
|
||||
|
||||
let _ = stream.write_all(b"GET /test HTTP/1.1\r\n\r\n");
|
||||
let mut data = vec![0; 256];
|
||||
let _ = stream.read(&mut data);
|
||||
assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n");
|
||||
|
||||
thread::sleep(Duration::from_millis(1100));
|
||||
|
||||
let mut data = vec![0; 1024];
|
||||
let mut data = vec![0; 256];
|
||||
let res = stream.read(&mut data).unwrap();
|
||||
assert_eq!(res, 0);
|
||||
|
||||
@ -281,7 +300,7 @@ async fn test_http1_keepalive_timeout() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http1_keepalive_close() {
|
||||
async fn http1_keepalive_close() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
@ -303,7 +322,7 @@ async fn test_http1_keepalive_close() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http10_keepalive_default_close() {
|
||||
async fn http10_keepalive_default_close() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
@ -325,7 +344,7 @@ async fn test_http10_keepalive_default_close() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http10_keepalive() {
|
||||
async fn http10_keepalive() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
@ -354,7 +373,7 @@ async fn test_http10_keepalive() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http1_keepalive_disabled() {
|
||||
async fn http1_keepalive_disabled() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.keep_alive(KeepAlive::Disabled)
|
||||
@ -377,7 +396,7 @@ async fn test_http1_keepalive_disabled() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_content_length() {
|
||||
async fn content_length() {
|
||||
use actix_http::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
StatusCode,
|
||||
@ -426,7 +445,7 @@ async fn test_content_length() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_headers() {
|
||||
async fn h1_headers() {
|
||||
let data = STR.repeat(10);
|
||||
let data2 = data.clone();
|
||||
|
||||
@ -492,7 +511,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_body() {
|
||||
async fn h1_body() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -511,7 +530,7 @@ async fn test_h1_body() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_head_empty() {
|
||||
async fn h1_head_empty() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -538,7 +557,7 @@ async fn test_h1_head_empty() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_head_binary() {
|
||||
async fn h1_head_binary() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -565,7 +584,7 @@ async fn test_h1_head_binary() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_head_binary2() {
|
||||
async fn h1_head_binary2() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
@ -588,7 +607,7 @@ async fn test_h1_head_binary2() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_body_length() {
|
||||
async fn h1_body_length() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| {
|
||||
@ -612,7 +631,7 @@ async fn test_h1_body_length() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_body_chunked_explicit() {
|
||||
async fn h1_body_chunked_explicit() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| {
|
||||
@ -649,7 +668,7 @@ async fn test_h1_body_chunked_explicit() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_body_chunked_implicit() {
|
||||
async fn h1_body_chunked_implicit() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| {
|
||||
@ -680,7 +699,7 @@ async fn test_h1_body_chunked_implicit() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_response_http_error_handling() {
|
||||
async fn h1_response_http_error_handling() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(fn_service(|_| {
|
||||
@ -719,7 +738,7 @@ impl From<BadRequest> for Response<BoxBody> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_service_error() {
|
||||
async fn h1_service_error() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| err::<Response<()>, _>(BadRequest))
|
||||
@ -738,7 +757,7 @@ async fn test_h1_service_error() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_on_connect() {
|
||||
async fn h1_on_connect() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.on_connect_ext(|_, data| {
|
||||
@ -761,7 +780,7 @@ async fn test_h1_on_connect() {
|
||||
/// Tests compliance with 304 Not Modified spec in RFC 7232 §4.1.
|
||||
/// https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
|
||||
#[actix_rt::test]
|
||||
async fn test_not_modified_spec_h1() {
|
||||
async fn not_modified_spec_h1() {
|
||||
// TODO: this test needing a few seconds to complete reveals some weirdness with either the
|
||||
// dispatcher or the client, though similar hangs occur on other tests in this file, only
|
||||
// succeeding, it seems, because of the keepalive timer
|
||||
@ -831,7 +850,8 @@ async fn test_not_modified_spec_h1() {
|
||||
Some(&header::HeaderValue::from_static("4")),
|
||||
);
|
||||
// server does not prevent payload from being sent but clients may choose not to read it
|
||||
// TODO: this is probably a bug, especially since CL header can differ in length from the body
|
||||
// TODO: this is probably a bug in the client, especially since CL header can differ in length
|
||||
// from the body
|
||||
assert!(!srv.load_body(res).await.unwrap().is_empty());
|
||||
|
||||
// TODO: add stream response tests
|
||||
|
@ -109,7 +109,7 @@ async fn service(msg: Frame) -> Result<Message, Error> {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_simple() {
|
||||
async fn simple() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.upgrade(fn_factory(|| async {
|
||||
|
@ -3,8 +3,16 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.4.0-beta.13 - 2022-01-31
|
||||
- No significant changes since `0.4.0-beta.12`.
|
||||
|
||||
|
||||
## 0.4.0-beta.12 - 2022-01-04
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
|
||||
## 0.4.0-beta.11 - 2021-12-27
|
||||
* No significant changes since `0.4.0-beta.10`.
|
||||
- No significant changes since `0.4.0-beta.10`.
|
||||
|
||||
|
||||
## 0.4.0-beta.10 - 2021-12-11
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-multipart"
|
||||
version = "0.4.0-beta.11"
|
||||
version = "0.4.0-beta.13"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Multipart form support for Actix Web"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
@ -15,7 +15,7 @@ path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = { version = "4.0.0-beta.17", default-features = false }
|
||||
actix-web = { version = "4.0.0", default-features = false }
|
||||
|
||||
bytes = "1"
|
||||
derive_more = "0.99.5"
|
||||
@ -28,7 +28,7 @@ twoway = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-http = "3.0.0-beta.17"
|
||||
actix-http = "3.0.0"
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
tokio = { version = "1.8.4", features = ["sync"] }
|
||||
tokio-stream = "0.1"
|
||||
|
@ -3,15 +3,15 @@
|
||||
> Multipart form support for Actix Web.
|
||||
|
||||
[](https://crates.io/crates/actix-multipart)
|
||||
[](https://docs.rs/actix-multipart/0.4.0-beta.11)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||
[](https://docs.rs/actix-multipart/0.4.0-beta.13)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-multipart/0.4.0-beta.11)
|
||||
[](https://deps.rs/crate/actix-multipart/0.4.0-beta.13)
|
||||
[](https://crates.io/crates/actix-multipart)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-multipart)
|
||||
- Minimum Supported Rust Version (MSRV): 1.52
|
||||
- Minimum Supported Rust Version (MSRV): 1.54
|
||||
|
@ -3,15 +3,119 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.5.0 - 2022-02-22
|
||||
### Added
|
||||
- Add `Path::as_str`. [#2590]
|
||||
- Add `ResourceDef::set_name`. [#373][net#373]
|
||||
- Add `RouterBuilder::push`. [#2612]
|
||||
- Implement `IntoPatterns` for `bytestring::ByteString`. [#372][net#372]
|
||||
- Introduce `ResourceDef::join`. [#380][net#380]
|
||||
- Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373][net#373]
|
||||
- `Resource` is now implemented for `&mut Path<_>` and `RefMut<Path<_>>`. [#2568]
|
||||
- Support `build_resource_path` on multi-pattern resources. [#2356]
|
||||
- Support multi-pattern prefixes and joins. [#2356]
|
||||
|
||||
### Changed
|
||||
- Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612]
|
||||
- Deprecate `Path::path`. [#2590]
|
||||
- Disallow prefix routes with tail segments. [#379][net#379]
|
||||
- Enforce path separators on dynamic prefixes. [#378][net#378]
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
- Prefix segments now always end with with a segment delimiter or end-of-input. [#2355]
|
||||
- Prefix segments with trailing slashes define a trailing empty segment. [#2355]
|
||||
- `Quoter::requote` now returns `Option<Vec<u8>>`. [#2613]
|
||||
- Re-work `IntoPatterns` trait, adding a `Patterns` enum. [#372][net#372]
|
||||
- Rename `Path::{len => segment_count}` to be more descriptive of its purpose. [#370][net#370]
|
||||
- Rename `ResourceDef::{is_prefix_match => find_match}`. [#373][net#373]
|
||||
- Rename `ResourceDef::{match_path => capture_match_info}`. [#373][net#373]
|
||||
- Rename `ResourceDef::{match_path_checked => capture_match_info_fn}`. [#373][net#373]
|
||||
- Rename `ResourceDef::{resource_path => resource_path_from_iter}`. [#371][net#371]
|
||||
- Rename `ResourceDef::{resource_path_named => resource_path_from_map}`. [#371][net#371]
|
||||
- Rename `Router::{*_checked => *_fn}`. [#373][net#373]
|
||||
- Replace `Option<U>` with `U` in `Router` API. [#2612]
|
||||
- `Resource` trait now uses an associated type, `Path`, instead of a generic parameter. [#2568]
|
||||
- `ResourceDef::pattern` now returns the first pattern in multi-pattern resources. [#2356]
|
||||
- `ResourceDef::resource_path_from_iter` now takes an `IntoIterator`. [#373][net#373]
|
||||
- Return type of `ResourceDef::name` is now `Option<&str>`. [#373][net#373]
|
||||
- Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373][net#373]
|
||||
|
||||
### Fixed
|
||||
- Fix `ResourceDef`'s `PartialEq` implementation. [#373][net#373]
|
||||
- Fix segment interpolation leaving `Path` in unintended state after matching. [#368][net#368]
|
||||
- Improve malformed path error message. [#384][net#384]
|
||||
- `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566]
|
||||
- Relax bounds on `Router::recognize*` and `ResourceDef::capture_match_info`. [#2612]
|
||||
- Static patterns in multi-patterns are no longer interpreted as regex. [#366][net#366]
|
||||
|
||||
### Removed
|
||||
- `ResourceDef::name_mut`. [#373][net#373]
|
||||
- Unused `ResourceInfo`. [#2612]
|
||||
|
||||
[#2355]: https://github.com/actix/actix-web/pull/2355
|
||||
[#2356]: https://github.com/actix/actix-web/pull/2356
|
||||
[#2566]: https://github.com/actix/actix-net/pull/2566
|
||||
[#2568]: https://github.com/actix/actix-web/pull/2568
|
||||
[#2590]: https://github.com/actix/actix-web/pull/2590
|
||||
[#2612]: https://github.com/actix/actix-web/pull/2612
|
||||
[#2613]: https://github.com/actix/actix-web/pull/2613
|
||||
[net#366]: https://github.com/actix/actix-net/pull/366
|
||||
[net#368]: https://github.com/actix/actix-net/pull/368
|
||||
[net#368]: https://github.com/actix/actix-net/pull/368
|
||||
[net#370]: https://github.com/actix/actix-net/pull/370
|
||||
[net#371]: https://github.com/actix/actix-net/pull/371
|
||||
[net#372]: https://github.com/actix/actix-net/pull/372
|
||||
[net#373]: https://github.com/actix/actix-net/pull/373
|
||||
[net#378]: https://github.com/actix/actix-net/pull/378
|
||||
[net#379]: https://github.com/actix/actix-net/pull/379
|
||||
[net#380]: https://github.com/actix/actix-net/pull/380
|
||||
[net#384]: https://github.com/actix/actix-net/pull/384
|
||||
|
||||
|
||||
<details>
|
||||
<summary>0.5.0 Pre-Releases</summary>
|
||||
|
||||
## 0.5.0-rc.3 - 2022-01-31
|
||||
- Remove unused `ResourceInfo`. [#2612]
|
||||
- Add `RouterBuilder::push`. [#2612]
|
||||
- Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612]
|
||||
- Replace `Option<U>` with `U` in `Router` API. [#2612]
|
||||
- Relax bounds on `Router::recognize*` and `ResourceDef::capture_match_info`. [#2612]
|
||||
- `Quoter::requote` now returns `Option<Vec<u8>>`. [#2613]
|
||||
|
||||
[#2612]: https://github.com/actix/actix-web/pull/2612
|
||||
[#2613]: https://github.com/actix/actix-web/pull/2613
|
||||
|
||||
|
||||
## 0.5.0-rc.2 - 2022-01-21
|
||||
- Add `Path::as_str`. [#2590]
|
||||
- Deprecate `Path::path`. [#2590]
|
||||
|
||||
[#2590]: https://github.com/actix/actix-web/pull/2590
|
||||
|
||||
|
||||
## 0.5.0-rc.1 - 2022-01-14
|
||||
- `Resource` trait now have an associated type, `Path`, instead of the generic parameter. [#2568]
|
||||
- `Resource` is now implemented for `&mut Path<_>` and `RefMut<Path<_>>`. [#2568]
|
||||
|
||||
[#2568]: https://github.com/actix/actix-web/pull/2568
|
||||
|
||||
|
||||
## 0.5.0-beta.4 - 2022-01-04
|
||||
- `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566]
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
[#2566]: https://github.com/actix/actix-net/pull/2566
|
||||
|
||||
|
||||
## 0.5.0-beta.3 - 2021-12-17
|
||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
||||
|
||||
## 0.5.0-beta.2 - 2021-09-09
|
||||
- Introduce `ResourceDef::join`. [#380]
|
||||
- Disallow prefix routes with tail segments. [#379]
|
||||
- Enforce path separators on dynamic prefixes. [#378]
|
||||
- Improve malformed path error message. [#384]
|
||||
- Introduce `ResourceDef::join`. [#380][net#380]
|
||||
- Disallow prefix routes with tail segments. [#379][net#379]
|
||||
- Enforce path separators on dynamic prefixes. [#378][net#378]
|
||||
- Improve malformed path error message. [#384][net#384]
|
||||
- Prefix segments now always end with with a segment delimiter or end-of-input. [#2355]
|
||||
- Prefix segments with trailing slashes define a trailing empty segment. [#2355]
|
||||
- Support multi-pattern prefixes and joins. [#2356]
|
||||
@ -19,52 +123,54 @@
|
||||
- Support `build_resource_path` on multi-pattern resources. [#2356]
|
||||
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||
|
||||
[#378]: https://github.com/actix/actix-net/pull/378
|
||||
[#379]: https://github.com/actix/actix-net/pull/379
|
||||
[#380]: https://github.com/actix/actix-net/pull/380
|
||||
[#384]: https://github.com/actix/actix-net/pull/384
|
||||
[net#378]: https://github.com/actix/actix-net/pull/378
|
||||
[net#379]: https://github.com/actix/actix-net/pull/379
|
||||
[net#380]: https://github.com/actix/actix-net/pull/380
|
||||
[net#384]: https://github.com/actix/actix-net/pull/384
|
||||
[#2355]: https://github.com/actix/actix-web/pull/2355
|
||||
[#2356]: https://github.com/actix/actix-web/pull/2356
|
||||
|
||||
|
||||
## 0.5.0-beta.1 - 2021-07-20
|
||||
- Fix a bug in multi-patterns where static patterns are interpreted as regex. [#366]
|
||||
- Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373]
|
||||
- Fix segment interpolation leaving `Path` in unintended state after matching. [#368]
|
||||
- Fix `ResourceDef` `PartialEq` implementation. [#373]
|
||||
- Re-work `IntoPatterns` trait, adding a `Patterns` enum. [#372]
|
||||
- Implement `IntoPatterns` for `bytestring::ByteString`. [#372]
|
||||
- Rename `Path::{len => segment_count}` to be more descriptive of it's purpose. [#370]
|
||||
- Rename `ResourceDef::{resource_path => resource_path_from_iter}`. [#371]
|
||||
- `ResourceDef::resource_path_from_iter` now takes an `IntoIterator`. [#373]
|
||||
- Rename `ResourceDef::{resource_path_named => resource_path_from_map}`. [#371]
|
||||
- Rename `ResourceDef::{is_prefix_match => find_match}`. [#373]
|
||||
- Rename `ResourceDef::{match_path => capture_match_info}`. [#373]
|
||||
- Rename `ResourceDef::{match_path_checked => capture_match_info_fn}`. [#373]
|
||||
- Remove `ResourceDef::name_mut` and introduce `ResourceDef::set_name`. [#373]
|
||||
- Rename `Router::{*_checked => *_fn}`. [#373]
|
||||
- Return type of `ResourceDef::name` is now `Option<&str>`. [#373]
|
||||
- Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373]
|
||||
- Fix a bug in multi-patterns where static patterns are interpreted as regex. [#366][net#366]
|
||||
- Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373][net#373]
|
||||
- Fix segment interpolation leaving `Path` in unintended state after matching. [#368][net#368]
|
||||
- Fix `ResourceDef` `PartialEq` implementation. [#373][net#373]
|
||||
- Re-work `IntoPatterns` trait, adding a `Patterns` enum. [#372][net#372]
|
||||
- Implement `IntoPatterns` for `bytestring::ByteString`. [#372][net#372]
|
||||
- Rename `Path::{len => segment_count}` to be more descriptive of it's purpose. [#370][net#370]
|
||||
- Rename `ResourceDef::{resource_path => resource_path_from_iter}`. [#371][net#371]
|
||||
- `ResourceDef::resource_path_from_iter` now takes an `IntoIterator`. [#373][net#373]
|
||||
- Rename `ResourceDef::{resource_path_named => resource_path_from_map}`. [#371][net#371]
|
||||
- Rename `ResourceDef::{is_prefix_match => find_match}`. [#373][net#373]
|
||||
- Rename `ResourceDef::{match_path => capture_match_info}`. [#373][net#373]
|
||||
- Rename `ResourceDef::{match_path_checked => capture_match_info_fn}`. [#373][net#373]
|
||||
- Remove `ResourceDef::name_mut` and introduce `ResourceDef::set_name`. [#373][net#373]
|
||||
- Rename `Router::{*_checked => *_fn}`. [#373][net#373]
|
||||
- Return type of `ResourceDef::name` is now `Option<&str>`. [#373][net#373]
|
||||
- Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373][net#373]
|
||||
|
||||
[#368]: https://github.com/actix/actix-net/pull/368
|
||||
[#366]: https://github.com/actix/actix-net/pull/366
|
||||
[#368]: https://github.com/actix/actix-net/pull/368
|
||||
[#370]: https://github.com/actix/actix-net/pull/370
|
||||
[#371]: https://github.com/actix/actix-net/pull/371
|
||||
[#372]: https://github.com/actix/actix-net/pull/372
|
||||
[#373]: https://github.com/actix/actix-net/pull/373
|
||||
[net#368]: https://github.com/actix/actix-net/pull/368
|
||||
[net#366]: https://github.com/actix/actix-net/pull/366
|
||||
[net#368]: https://github.com/actix/actix-net/pull/368
|
||||
[net#370]: https://github.com/actix/actix-net/pull/370
|
||||
[net#371]: https://github.com/actix/actix-net/pull/371
|
||||
[net#372]: https://github.com/actix/actix-net/pull/372
|
||||
[net#373]: https://github.com/actix/actix-net/pull/373
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## 0.4.0 - 2021-06-06
|
||||
- When matching path parameters, `%25` is now kept in the percent-encoded form; no longer decoded to `%`. [#357]
|
||||
- Path tail patterns now match new lines (`\n`) in request URL. [#360]
|
||||
- Fixed a safety bug where `Path` could return a malformed string after percent decoding. [#359]
|
||||
- Methods `Path::{add, add_static}` now take `impl Into<Cow<'static, str>>`. [#345]
|
||||
- When matching path parameters, `%25` is now kept in the percent-encoded form; no longer decoded to `%`. [#357][net#357]
|
||||
- Path tail patterns now match new lines (`\n`) in request URL. [#360][net#360]
|
||||
- Fixed a safety bug where `Path` could return a malformed string after percent decoding. [#359][net#359]
|
||||
- Methods `Path::{add, add_static}` now take `impl Into<Cow<'static, str>>`. [#345][net#345]
|
||||
|
||||
[#345]: https://github.com/actix/actix-net/pull/345
|
||||
[#357]: https://github.com/actix/actix-net/pull/357
|
||||
[#359]: https://github.com/actix/actix-net/pull/359
|
||||
[#360]: https://github.com/actix/actix-net/pull/360
|
||||
[net#345]: https://github.com/actix/actix-net/pull/345
|
||||
[net#357]: https://github.com/actix/actix-net/pull/357
|
||||
[net#359]: https://github.com/actix/actix-net/pull/359
|
||||
[net#360]: https://github.com/actix/actix-net/pull/360
|
||||
|
||||
|
||||
## 0.3.0 - 2019-12-31
|
||||
@ -72,15 +178,15 @@
|
||||
|
||||
|
||||
## 0.2.7 - 2021-02-06
|
||||
- Add `Router::recognize_checked` [#247]
|
||||
- Add `Router::recognize_checked` [#247][net#247]
|
||||
|
||||
[#247]: https://github.com/actix/actix-net/pull/247
|
||||
[net#247]: https://github.com/actix/actix-net/pull/247
|
||||
|
||||
|
||||
## 0.2.6 - 2021-01-09
|
||||
- Use `bytestring` version range compatible with Bytes v1.0. [#246]
|
||||
- Use `bytestring` version range compatible with Bytes v1.0. [#246][net#246]
|
||||
|
||||
[#246]: https://github.com/actix/actix-net/pull/246
|
||||
[net#246]: https://github.com/actix/actix-net/pull/246
|
||||
|
||||
|
||||
## 0.2.5 - 2020-09-20
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-router"
|
||||
version = "0.5.0-beta.3"
|
||||
version = "0.5.0"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",
|
||||
|
@ -145,7 +145,8 @@ macro_rules! register {
|
||||
concat!("/user/keys"),
|
||||
concat!("/user/keys/", $p1),
|
||||
];
|
||||
std::array::IntoIter::new(arr)
|
||||
|
||||
IntoIterator::into_iter(arr)
|
||||
}};
|
||||
}
|
||||
|
||||
@ -158,7 +159,7 @@ fn call() -> impl Iterator<Item = &'static str> {
|
||||
"/repos/rust-lang/rust/releases/1.51.0",
|
||||
];
|
||||
|
||||
std::array::IntoIter::new(arr)
|
||||
IntoIterator::into_iter(arr)
|
||||
}
|
||||
|
||||
fn compare_routers(c: &mut Criterion) {
|
||||
|
@ -1,8 +1,14 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use serde::de::{self, Deserializer, Error as DeError, Visitor};
|
||||
use serde::forward_to_deserialize_any;
|
||||
|
||||
use crate::path::{Path, PathIter};
|
||||
use crate::ResourcePath;
|
||||
use crate::{Quoter, ResourcePath};
|
||||
|
||||
thread_local! {
|
||||
static FULL_QUOTER: Quoter = Quoter::new(b"+/%", b"");
|
||||
}
|
||||
|
||||
macro_rules! unsupported_type {
|
||||
($trait_fn:ident, $name:expr) => {
|
||||
@ -10,16 +16,13 @@ macro_rules! unsupported_type {
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
Err(de::value::Error::custom(concat!(
|
||||
"unsupported type: ",
|
||||
$name
|
||||
)))
|
||||
Err(de::Error::custom(concat!("unsupported type: ", $name)))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! parse_single_value {
|
||||
($trait_fn:ident, $visit_fn:ident, $tp:tt) => {
|
||||
($trait_fn:ident) => {
|
||||
fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
@ -33,18 +36,35 @@ macro_rules! parse_single_value {
|
||||
.as_str(),
|
||||
))
|
||||
} else {
|
||||
let v = self.path[0].parse().map_err(|_| {
|
||||
de::value::Error::custom(format!(
|
||||
"can not parse {:?} to a {}",
|
||||
&self.path[0], $tp
|
||||
))
|
||||
})?;
|
||||
visitor.$visit_fn(v)
|
||||
Value {
|
||||
value: &self.path[0],
|
||||
}
|
||||
.$trait_fn(visitor)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! parse_value {
|
||||
($trait_fn:ident, $visit_fn:ident, $tp:tt) => {
|
||||
fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
let decoded = FULL_QUOTER
|
||||
.with(|q| q.requote_str_lossy(self.value))
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or(Cow::Borrowed(self.value));
|
||||
|
||||
let v = decoded.parse().map_err(|_| {
|
||||
de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $tp))
|
||||
})?;
|
||||
|
||||
visitor.$visit_fn(v)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub struct PathDeserializer<'de, T: ResourcePath> {
|
||||
path: &'de Path<T>,
|
||||
}
|
||||
@ -172,23 +192,6 @@ impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T>
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
if self.path.segment_count() != 1 {
|
||||
Err(de::value::Error::custom(
|
||||
format!(
|
||||
"wrong number of parameters: {} expected 1",
|
||||
self.path.segment_count()
|
||||
)
|
||||
.as_str(),
|
||||
))
|
||||
} else {
|
||||
visitor.visit_str(&self.path[0])
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
@ -199,25 +202,26 @@ impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T>
|
||||
}
|
||||
|
||||
unsupported_type!(deserialize_any, "'any'");
|
||||
unsupported_type!(deserialize_bytes, "bytes");
|
||||
unsupported_type!(deserialize_option, "Option<T>");
|
||||
unsupported_type!(deserialize_identifier, "identifier");
|
||||
unsupported_type!(deserialize_ignored_any, "ignored_any");
|
||||
|
||||
parse_single_value!(deserialize_bool, visit_bool, "bool");
|
||||
parse_single_value!(deserialize_i8, visit_i8, "i8");
|
||||
parse_single_value!(deserialize_i16, visit_i16, "i16");
|
||||
parse_single_value!(deserialize_i32, visit_i32, "i32");
|
||||
parse_single_value!(deserialize_i64, visit_i64, "i64");
|
||||
parse_single_value!(deserialize_u8, visit_u8, "u8");
|
||||
parse_single_value!(deserialize_u16, visit_u16, "u16");
|
||||
parse_single_value!(deserialize_u32, visit_u32, "u32");
|
||||
parse_single_value!(deserialize_u64, visit_u64, "u64");
|
||||
parse_single_value!(deserialize_f32, visit_f32, "f32");
|
||||
parse_single_value!(deserialize_f64, visit_f64, "f64");
|
||||
parse_single_value!(deserialize_string, visit_string, "String");
|
||||
parse_single_value!(deserialize_byte_buf, visit_string, "String");
|
||||
parse_single_value!(deserialize_char, visit_char, "char");
|
||||
parse_single_value!(deserialize_bool);
|
||||
parse_single_value!(deserialize_i8);
|
||||
parse_single_value!(deserialize_i16);
|
||||
parse_single_value!(deserialize_i32);
|
||||
parse_single_value!(deserialize_i64);
|
||||
parse_single_value!(deserialize_u8);
|
||||
parse_single_value!(deserialize_u16);
|
||||
parse_single_value!(deserialize_u32);
|
||||
parse_single_value!(deserialize_u64);
|
||||
parse_single_value!(deserialize_f32);
|
||||
parse_single_value!(deserialize_f64);
|
||||
parse_single_value!(deserialize_str);
|
||||
parse_single_value!(deserialize_string);
|
||||
parse_single_value!(deserialize_bytes);
|
||||
parse_single_value!(deserialize_byte_buf);
|
||||
parse_single_value!(deserialize_char);
|
||||
}
|
||||
|
||||
struct ParamsDeserializer<'de, T: ResourcePath> {
|
||||
@ -279,20 +283,6 @@ impl<'de> Deserializer<'de> for Key<'de> {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! parse_value {
|
||||
($trait_fn:ident, $visit_fn:ident, $tp:tt) => {
|
||||
fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
let v = self.value.parse().map_err(|_| {
|
||||
de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $tp))
|
||||
})?;
|
||||
visitor.$visit_fn(v)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct Value<'de> {
|
||||
value: &'de str,
|
||||
}
|
||||
@ -311,8 +301,6 @@ impl<'de> Deserializer<'de> for Value<'de> {
|
||||
parse_value!(deserialize_u64, visit_u64, "u64");
|
||||
parse_value!(deserialize_f32, visit_f32, "f32");
|
||||
parse_value!(deserialize_f64, visit_f64, "f64");
|
||||
parse_value!(deserialize_string, visit_string, "String");
|
||||
parse_value!(deserialize_byte_buf, visit_string, "String");
|
||||
parse_value!(deserialize_char, visit_char, "char");
|
||||
|
||||
fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
@ -340,18 +328,38 @@ impl<'de> Deserializer<'de> for Value<'de> {
|
||||
visitor.visit_unit()
|
||||
}
|
||||
|
||||
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
visitor.visit_borrowed_bytes(self.value.as_bytes())
|
||||
}
|
||||
|
||||
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
visitor.visit_borrowed_str(self.value)
|
||||
match FULL_QUOTER.with(|q| q.requote_str_lossy(self.value)) {
|
||||
Some(s) => visitor.visit_string(s),
|
||||
None => visitor.visit_borrowed_str(self.value),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
match FULL_QUOTER.with(|q| q.requote_str_lossy(self.value)) {
|
||||
Some(s) => visitor.visit_byte_buf(s.into()),
|
||||
None => visitor.visit_borrowed_bytes(self.value.as_bytes()),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
self.deserialize_bytes(visitor)
|
||||
}
|
||||
|
||||
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
self.deserialize_str(visitor)
|
||||
}
|
||||
|
||||
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
@ -497,6 +505,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::path::Path;
|
||||
use crate::router::Router;
|
||||
use crate::ResourceDef;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MyStruct {
|
||||
@ -657,6 +666,79 @@ mod tests {
|
||||
assert!(format!("{:?}", s).contains("can not parse"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_path_decode_string() {
|
||||
let rdef = ResourceDef::new("/{key}");
|
||||
|
||||
let mut path = Path::new("/%25");
|
||||
rdef.capture_match_info(&mut path);
|
||||
let de = PathDeserializer::new(&path);
|
||||
let segment: String = serde::Deserialize::deserialize(de).unwrap();
|
||||
assert_eq!(segment, "%");
|
||||
|
||||
let mut path = Path::new("/%2F");
|
||||
rdef.capture_match_info(&mut path);
|
||||
let de = PathDeserializer::new(&path);
|
||||
let segment: String = serde::Deserialize::deserialize(de).unwrap();
|
||||
assert_eq!(segment, "/")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_path_decode_seq() {
|
||||
let rdef = ResourceDef::new("/{key}/{value}");
|
||||
|
||||
let mut path = Path::new("/%30%25/%30%2F");
|
||||
rdef.capture_match_info(&mut path);
|
||||
let de = PathDeserializer::new(&path);
|
||||
let segment: (String, String) = serde::Deserialize::deserialize(de).unwrap();
|
||||
assert_eq!(segment.0, "0%");
|
||||
assert_eq!(segment.1, "0/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_path_decode_map() {
|
||||
#[derive(Deserialize)]
|
||||
struct Vals {
|
||||
key: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
let rdef = ResourceDef::new("/{key}/{value}");
|
||||
|
||||
let mut path = Path::new("/%25/%2F");
|
||||
rdef.capture_match_info(&mut path);
|
||||
let de = PathDeserializer::new(&path);
|
||||
let vals: Vals = serde::Deserialize::deserialize(de).unwrap();
|
||||
assert_eq!(vals.key, "%");
|
||||
assert_eq!(vals.value, "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_borrowed() {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Params<'a> {
|
||||
val: &'a str,
|
||||
}
|
||||
|
||||
let rdef = ResourceDef::new("/{val}");
|
||||
|
||||
let mut path = Path::new("/X");
|
||||
rdef.capture_match_info(&mut path);
|
||||
let de = PathDeserializer::new(&path);
|
||||
let params: Params<'_> = serde::Deserialize::deserialize(de).unwrap();
|
||||
assert_eq!(params.val, "X");
|
||||
let de = PathDeserializer::new(&path);
|
||||
let params: &str = serde::Deserialize::deserialize(de).unwrap();
|
||||
assert_eq!(params, "X");
|
||||
|
||||
let mut path = Path::new("/%2F");
|
||||
rdef.capture_match_info(&mut path);
|
||||
let de = PathDeserializer::new(&path);
|
||||
assert!(<Params<'_> as serde::Deserialize>::deserialize(de).is_err());
|
||||
let de = PathDeserializer::new(&path);
|
||||
assert!(<&str as serde::Deserialize>::deserialize(de).is_err());
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn test_extract_path_decode() {
|
||||
// let mut router = Router::<()>::default();
|
||||
|
@ -8,6 +8,7 @@
|
||||
mod de;
|
||||
mod path;
|
||||
mod pattern;
|
||||
mod quoter;
|
||||
mod resource;
|
||||
mod resource_path;
|
||||
mod router;
|
||||
@ -18,9 +19,10 @@ mod url;
|
||||
pub use self::de::PathDeserializer;
|
||||
pub use self::path::Path;
|
||||
pub use self::pattern::{IntoPatterns, Patterns};
|
||||
pub use self::quoter::Quoter;
|
||||
pub use self::resource::ResourceDef;
|
||||
pub use self::resource_path::{Resource, ResourcePath};
|
||||
pub use self::router::{ResourceInfo, Router, RouterBuilder};
|
||||
pub use self::router::{ResourceId, Router, RouterBuilder};
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
pub use self::url::{Quoter, Url};
|
||||
pub use self::url::Url;
|
||||
|
@ -1,5 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
use std::ops::Index;
|
||||
use std::ops::{DerefMut, Index};
|
||||
|
||||
use firestorm::profile_method;
|
||||
use serde::de;
|
||||
@ -37,19 +37,39 @@ impl<T: ResourcePath> Path<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get reference to inner path instance.
|
||||
/// Returns reference to inner path instance.
|
||||
#[inline]
|
||||
pub fn get_ref(&self) -> &T {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Get mutable reference to inner path instance.
|
||||
/// Returns mutable reference to inner path instance.
|
||||
#[inline]
|
||||
pub fn get_mut(&mut self) -> &mut T {
|
||||
&mut self.path
|
||||
}
|
||||
|
||||
/// Path.
|
||||
/// Returns full path as a string.
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
profile_method!(as_str);
|
||||
self.path.path()
|
||||
}
|
||||
|
||||
/// Returns unprocessed part of the path.
|
||||
///
|
||||
/// Returns empty string if no more is to be processed.
|
||||
#[inline]
|
||||
pub fn unprocessed(&self) -> &str {
|
||||
profile_method!(unprocessed);
|
||||
// clamp skip to path length
|
||||
let skip = (self.skip as usize).min(self.as_str().len());
|
||||
&self.path.path()[skip..]
|
||||
}
|
||||
|
||||
/// Returns unprocessed part of the path.
|
||||
#[doc(hidden)]
|
||||
#[deprecated(since = "0.6.0", note = "Use `.as_str()` or `.unprocessed()`.")]
|
||||
#[inline]
|
||||
pub fn path(&self) -> &str {
|
||||
profile_method!(path);
|
||||
@ -66,6 +86,8 @@ impl<T: ResourcePath> Path<T> {
|
||||
/// Set new path.
|
||||
#[inline]
|
||||
pub fn set(&mut self, path: T) {
|
||||
profile_method!(set);
|
||||
|
||||
self.skip = 0;
|
||||
self.path = path;
|
||||
self.segments.clear();
|
||||
@ -74,6 +96,8 @@ impl<T: ResourcePath> Path<T> {
|
||||
/// Reset state.
|
||||
#[inline]
|
||||
pub fn reset(&mut self) {
|
||||
profile_method!(reset);
|
||||
|
||||
self.skip = 0;
|
||||
self.segments.clear();
|
||||
}
|
||||
@ -81,6 +105,7 @@ impl<T: ResourcePath> Path<T> {
|
||||
/// Skip first `n` chars in path.
|
||||
#[inline]
|
||||
pub fn skip(&mut self, n: u16) {
|
||||
profile_method!(skip);
|
||||
self.skip += n;
|
||||
}
|
||||
|
||||
@ -102,6 +127,8 @@ impl<T: ResourcePath> Path<T> {
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
value: impl Into<Cow<'static, str>>,
|
||||
) {
|
||||
profile_method!(add_static);
|
||||
|
||||
self.segments
|
||||
.push((name.into(), PathItem::Static(value.into())));
|
||||
}
|
||||
@ -136,11 +163,6 @@ impl<T: ResourcePath> Path<T> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Get unprocessed part of the path
|
||||
pub fn unprocessed(&self) -> &str {
|
||||
&self.path.path()[(self.skip as usize)..]
|
||||
}
|
||||
|
||||
/// Get matched parameter by name.
|
||||
///
|
||||
/// If keyed parameter is not available empty string is used as default value.
|
||||
@ -213,8 +235,38 @@ impl<T: ResourcePath> Index<usize> for Path<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ResourcePath> Resource<T> for Path<T> {
|
||||
fn resource_path(&mut self) -> &mut Self {
|
||||
impl<T: ResourcePath> Resource for Path<T> {
|
||||
type Path = T;
|
||||
|
||||
fn resource_path(&mut self) -> &mut Path<Self::Path> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, P> Resource for T
|
||||
where
|
||||
T: DerefMut<Target = Path<P>>,
|
||||
P: ResourcePath,
|
||||
{
|
||||
type Path = P;
|
||||
|
||||
fn resource_path(&mut self) -> &mut Path<Self::Path> {
|
||||
&mut *self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cell::RefCell;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deref_impls() {
|
||||
let mut foo = Path::new("/foo");
|
||||
let _ = (&mut foo).resource_path();
|
||||
|
||||
let foo = RefCell::new(foo);
|
||||
let _ = foo.borrow_mut().resource_path();
|
||||
}
|
||||
}
|
||||
|
244
actix-router/src/quoter.rs
Normal file
244
actix-router/src/quoter.rs
Normal file
@ -0,0 +1,244 @@
|
||||
#[allow(dead_code)]
|
||||
const GEN_DELIMS: &[u8] = b":/?#[]@";
|
||||
|
||||
#[allow(dead_code)]
|
||||
const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,";
|
||||
|
||||
#[allow(dead_code)]
|
||||
const SUB_DELIMS: &[u8] = b"!$'()*,+?=;";
|
||||
|
||||
#[allow(dead_code)]
|
||||
const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;";
|
||||
|
||||
#[allow(dead_code)]
|
||||
const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
1234567890
|
||||
-._~";
|
||||
|
||||
const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
1234567890
|
||||
-._~
|
||||
!$'()*,";
|
||||
|
||||
const QS: &[u8] = b"+&=;b";
|
||||
|
||||
/// A quoter
|
||||
pub struct Quoter {
|
||||
/// Simple bit-map of safe values in the 0-127 ASCII range.
|
||||
safe_table: [u8; 16],
|
||||
|
||||
/// Simple bit-map of protected values in the 0-127 ASCII range.
|
||||
protected_table: [u8; 16],
|
||||
}
|
||||
|
||||
impl Quoter {
|
||||
pub fn new(safe: &[u8], protected: &[u8]) -> Quoter {
|
||||
let mut quoter = Quoter {
|
||||
safe_table: [0; 16],
|
||||
protected_table: [0; 16],
|
||||
};
|
||||
|
||||
// prepare safe table
|
||||
for ch in 0..128 {
|
||||
if ALLOWED.contains(&ch) {
|
||||
set_bit(&mut quoter.safe_table, ch);
|
||||
}
|
||||
|
||||
if QS.contains(&ch) {
|
||||
set_bit(&mut quoter.safe_table, ch);
|
||||
}
|
||||
}
|
||||
|
||||
for &ch in safe {
|
||||
set_bit(&mut quoter.safe_table, ch)
|
||||
}
|
||||
|
||||
// prepare protected table
|
||||
for &ch in protected {
|
||||
set_bit(&mut quoter.safe_table, ch);
|
||||
set_bit(&mut quoter.protected_table, ch);
|
||||
}
|
||||
|
||||
quoter
|
||||
}
|
||||
|
||||
/// Decodes safe percent-encoded sequences from `val`.
|
||||
///
|
||||
/// Returns `None` when no modification to the original byte string was required.
|
||||
///
|
||||
/// Non-ASCII bytes are accepted as valid input.
|
||||
///
|
||||
/// Behavior for invalid/incomplete percent-encoding sequences is unspecified and may include
|
||||
/// removing the invalid sequence from the output or passing it as-is.
|
||||
pub fn requote(&self, val: &[u8]) -> Option<Vec<u8>> {
|
||||
let mut has_pct = 0;
|
||||
let mut pct = [b'%', 0, 0];
|
||||
let mut idx = 0;
|
||||
let mut cloned: Option<Vec<u8>> = None;
|
||||
|
||||
let len = val.len();
|
||||
|
||||
while idx < len {
|
||||
let ch = val[idx];
|
||||
|
||||
if has_pct != 0 {
|
||||
pct[has_pct] = val[idx];
|
||||
has_pct += 1;
|
||||
|
||||
if has_pct == 3 {
|
||||
has_pct = 0;
|
||||
let buf = cloned.as_mut().unwrap();
|
||||
|
||||
if let Some(ch) = hex_pair_to_char(pct[1], pct[2]) {
|
||||
if ch < 128 {
|
||||
if bit_at(&self.protected_table, ch) {
|
||||
buf.extend_from_slice(&pct);
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if bit_at(&self.safe_table, ch) {
|
||||
buf.push(ch);
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
buf.push(ch);
|
||||
} else {
|
||||
buf.extend_from_slice(&pct[..]);
|
||||
}
|
||||
}
|
||||
} else if ch == b'%' {
|
||||
has_pct = 1;
|
||||
|
||||
if cloned.is_none() {
|
||||
let mut c = Vec::with_capacity(len);
|
||||
c.extend_from_slice(&val[..idx]);
|
||||
cloned = Some(c);
|
||||
}
|
||||
} else if let Some(ref mut cloned) = cloned {
|
||||
cloned.push(ch)
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
cloned
|
||||
}
|
||||
|
||||
pub(crate) fn requote_str_lossy(&self, val: &str) -> Option<String> {
|
||||
self.requote(val.as_bytes())
|
||||
.map(|data| String::from_utf8_lossy(&data).into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an ASCII character in the hex-encoded set (`0-9`, `A-F`, `a-f`) to its integer
|
||||
/// representation from `0x0`–`0xF`.
|
||||
///
|
||||
/// - `0x30 ('0') => 0x0`
|
||||
/// - `0x39 ('9') => 0x9`
|
||||
/// - `0x41 ('a') => 0xA`
|
||||
/// - `0x61 ('A') => 0xA`
|
||||
/// - `0x46 ('f') => 0xF`
|
||||
/// - `0x66 ('F') => 0xF`
|
||||
fn from_ascii_hex(v: u8) -> Option<u8> {
|
||||
match v {
|
||||
b'0'..=b'9' => Some(v - 0x30), // ord('0') == 0x30
|
||||
b'A'..=b'F' => Some(v - 0x41 + 10), // ord('A') == 0x41
|
||||
b'a'..=b'f' => Some(v - 0x61 + 10), // ord('a') == 0x61
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode a ASCII hex-encoded pair to an integer.
|
||||
///
|
||||
/// Returns `None` if either portion of the decoded pair does not evaluate to a valid hex value.
|
||||
///
|
||||
/// - `0x33 ('3'), 0x30 ('0') => 0x30 ('0')`
|
||||
/// - `0x34 ('4'), 0x31 ('1') => 0x41 ('A')`
|
||||
/// - `0x36 ('6'), 0x31 ('1') => 0x61 ('a')`
|
||||
fn hex_pair_to_char(d1: u8, d2: u8) -> Option<u8> {
|
||||
let (d_high, d_low) = (from_ascii_hex(d1)?, from_ascii_hex(d2)?);
|
||||
|
||||
// left shift high nibble by 4 bits
|
||||
Some(d_high << 4 | d_low)
|
||||
}
|
||||
|
||||
/// Sets bit in given bit-map to 1=true.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if `ch` index is out of bounds.
|
||||
fn set_bit(array: &mut [u8], ch: u8) {
|
||||
array[(ch >> 3) as usize] |= 0b1 << (ch & 0b111)
|
||||
}
|
||||
|
||||
/// Returns true if bit to true in given bit-map.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if `ch` index is out of bounds.
|
||||
fn bit_at(array: &[u8], ch: u8) -> bool {
|
||||
array[(ch >> 3) as usize] & (0b1 << (ch & 0b111)) != 0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hex_encoding() {
|
||||
let hex = b"0123456789abcdefABCDEF";
|
||||
|
||||
for i in 0..256 {
|
||||
let c = i as u8;
|
||||
if hex.contains(&c) {
|
||||
assert!(from_ascii_hex(c).is_some())
|
||||
} else {
|
||||
assert!(from_ascii_hex(c).is_none())
|
||||
}
|
||||
}
|
||||
|
||||
let expected = [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 10, 11, 12, 13, 14, 15,
|
||||
];
|
||||
for i in 0..hex.len() {
|
||||
assert_eq!(from_ascii_hex(hex[i]).unwrap(), expected[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_quoter() {
|
||||
let q = Quoter::new(b"", b"+");
|
||||
assert_eq!(q.requote(b"/a%25c").unwrap(), b"/a%c");
|
||||
assert_eq!(q.requote(b"/a%2Bc").unwrap(), b"/a%2Bc");
|
||||
|
||||
let q = Quoter::new(b"%+", b"/");
|
||||
assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), b"/a%b+c");
|
||||
assert_eq!(q.requote(b"/a%2fb").unwrap(), b"/a%2fb");
|
||||
assert_eq!(q.requote(b"/a%2Fb").unwrap(), b"/a%2Fb");
|
||||
assert_eq!(q.requote(b"/a%0Ab").unwrap(), b"/a\nb");
|
||||
assert_eq!(q.requote(b"/a%FE\xffb").unwrap(), b"/a\xfe\xffb");
|
||||
assert_eq!(q.requote(b"/a\xfe\xffb"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_ascii() {
|
||||
let q = Quoter::new(b"%+", b"/");
|
||||
assert_eq!(q.requote(b"/a%FE\xffb").unwrap(), b"/a\xfe\xffb");
|
||||
assert_eq!(q.requote(b"/a\xfe\xffb"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_sequences() {
|
||||
let q = Quoter::new(b"%+", b"/");
|
||||
assert_eq!(q.requote(b"/a%2x%2X%%").unwrap(), b"/a%2x%2X");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoter_no_modification() {
|
||||
let q = Quoter::new(b"", b"");
|
||||
assert_eq!(q.requote(b"/abc/../efg"), None);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user