mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-04 01:51:30 +02:00
Compare commits
126 Commits
files-v0.6
...
router-v0.
Author | SHA1 | Date | |
---|---|---|---|
9668a2396f | |||
cb7347216c | |||
ae7f71e317 | |||
bc89f0bfc2 | |||
c959916346 | |||
f227e880d7 | |||
68ad81f989 | |||
f2e736719a | |||
81ef12a0fd | |||
1bc1538118 | |||
1cc3e7b24c | |||
3dd98c308c | |||
cb5d9a7e64 | |||
5ee555462f | |||
ad159f5219 | |||
2ffc21dd4f | |||
7b8a392ef5 | |||
3c7ccf5521 | |||
e7cae5a95b | |||
455d5c460d | |||
8faca783fa | |||
edbb9b047e | |||
32742d0715 | |||
d90c1a2331 | |||
2a12b41456 | |||
6c97d448b7 | |||
c3ce33df05 | |||
4431c8da65 | |||
2d11ab5977 | |||
4ebf16890d | |||
fe0bbfb3da | |||
2462b6dd5d | |||
49cfabeaf5 | |||
0f7292c69a | |||
8bbf2b5052 | |||
8c975bcc1f | |||
742ad56d30 | |||
bcc8d5c441 | |||
f659098d21 | |||
8621ae12f8 | |||
b338eb8473 | |||
5abd1c2c2c | |||
05336269f9 | |||
86df295ee2 | |||
85c9b1a263 | |||
577597a80a | |||
374dc9bfc9 | |||
93754f307f | |||
c7639bc3be | |||
0bc4ae9158 | |||
19a46e3925 | |||
68cd853aa2 | |||
25fe1bbaa5 | |||
e890307091 | |||
b708924590 | |||
5dcb250237 | |||
b4ff6addfe | |||
231a24ef8d | |||
6df4974234 | |||
a80e93d6db | |||
542c92c9a7 | |||
74738c63a7 | |||
a87e01f0d1 | |||
9779010a5a | |||
11d50d792b | |||
798e9911e9 | |||
2b2de29800 | |||
0f5c876c6b | |||
96a4dc9dec | |||
4616ca8ee6 | |||
36193b0a50 | |||
76684a786e | |||
2308f8afa4 | |||
554ae7a868 | |||
ac0c4eb684 | |||
2e493cf791 | |||
5860fe5381 | |||
adf9935841 | |||
34e5c7c799 | |||
01cbfc5724 | |||
3756dfc2ce | |||
d2590fd46c | |||
1296e07c48 | |||
7b1512d863 | |||
cd025f5c0b | |||
1769812d0b | |||
324eba7e0b | |||
b3ac918d70 | |||
de20d21703 | |||
212c6926f9 | |||
1ea619f2a1 | |||
40a0162074 | |||
f8488aff1e | |||
64c2e5e1cd | |||
17f636a183 | |||
2e00776d5e | |||
7d507a41ee | |||
fb036264cc | |||
d2b9724010 | |||
5c53db1e4d | |||
84ea9e7e88 | |||
0bd5ccc432 | |||
9cd8526085 | |||
73bbe56971 | |||
8340b63b7b | |||
6c2c7b68e2 | |||
7bf47967cc | |||
ae47d96fc6 | |||
5842a3279d | |||
1d6f5ba6d6 | |||
aa31086af5 | |||
57ea322ce5 | |||
2cf27863cb | |||
5359fa56c2 | |||
a2467718ac | |||
3c0d059d92 | |||
44b7302845 | |||
a6d5776481 | |||
156cc20ac8 | |||
dd4a372613 | |||
05255c7f7c | |||
fb091b2b88 | |||
11ee8ec3ab | |||
551a0d973c | |||
cea44be670 | |||
b41b346c00 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [robjtede]
|
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -33,5 +33,5 @@ Please search on the [Actix Web issue tracker](https://github.com/actix/actix-we
|
||||
## Your Environment
|
||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||
|
||||
* Rust Version (I.e, output of `rustc -V`):
|
||||
* Actix Web Version:
|
||||
- Rust Version (I.e, output of `rustc -V`):
|
||||
- Actix Web Version:
|
||||
|
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
|
||||
|
153
.github/workflows/ci-master.yml
vendored
Normal file
153
.github/workflows/ci-master.yml
vendored
Normal file
@ -0,0 +1,153 @@
|
||||
name: CI (master only)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build_and_test_nightly:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
|
||||
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
||||
- { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
|
||||
version:
|
||||
- nightly
|
||||
|
||||
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
||||
runs-on: ${{ matrix.target.os }}
|
||||
|
||||
env:
|
||||
CI: 1
|
||||
CARGO_INCREMENTAL: 0
|
||||
VCPKGRS_DYNAMIC: 1
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# install OpenSSL on Windows
|
||||
# TODO: GitHub actions docs state that OpenSSL is
|
||||
# already installed on these Windows machines somewhere
|
||||
- name: Set vcpkg root
|
||||
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
- name: Install OpenSSL
|
||||
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||
run: vcpkg install openssl:x64-windows
|
||||
|
||||
- name: Install ${{ matrix.version }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
- name: Install cargo-hack
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-hack
|
||||
|
||||
- name: check minimal
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-min }
|
||||
|
||||
- name: check default
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-default }
|
||||
|
||||
- name: tests
|
||||
timeout-minutes: 60
|
||||
run: |
|
||||
cargo test --lib --tests -p=actix-router --all-features
|
||||
cargo test --lib --tests -p=actix-http --all-features
|
||||
cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
|
||||
cargo test --lib --tests -p=actix-web-codegen --all-features
|
||||
cargo test --lib --tests -p=awc --all-features
|
||||
cargo test --lib --tests -p=actix-http-test --all-features
|
||||
cargo test --lib --tests -p=actix-test --all-features
|
||||
cargo test --lib --tests -p=actix-files
|
||||
cargo test --lib --tests -p=actix-multipart --all-features
|
||||
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||
|
||||
- name: tests (io-uring)
|
||||
if: matrix.target.os == 'ubuntu-latest'
|
||||
timeout-minutes: 60
|
||||
run: >
|
||||
sudo bash -c "ulimit -Sl 512
|
||||
&& ulimit -Hl 512
|
||||
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
||||
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
|
||||
|
||||
- name: Clear the cargo caches
|
||||
run: |
|
||||
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
||||
|
||||
ci_feature_powerset_check:
|
||||
name: Verify Feature Combinations
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable-x86_64-unknown-linux-gnu
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
- name: Install cargo-hack
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-hack
|
||||
|
||||
- name: check feature combinations
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-all-feature-powerset }
|
||||
|
||||
- name: check feature combinations
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-all-feature-powerset-linux }
|
||||
|
||||
coverage:
|
||||
name: coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable-x86_64-unknown-linux-gnu
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
- name: Generate coverage file
|
||||
run: |
|
||||
cargo install cargo-tarpaulin --vers "^0.13"
|
||||
cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with: { file: cobertura.xml }
|
65
.github/workflows/ci.yml
vendored
65
.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 }}
|
||||
@ -96,68 +95,6 @@ jobs:
|
||||
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
||||
|
||||
ci_feature_powerset_check:
|
||||
name: Verify Feature Combinations
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable-x86_64-unknown-linux-gnu
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
- name: Install cargo-hack
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-hack
|
||||
|
||||
- name: check feature combinations
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-all-feature-powerset }
|
||||
|
||||
- name: check feature combinations
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-all-feature-powerset-linux }
|
||||
|
||||
coverage:
|
||||
name: coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable-x86_64-unknown-linux-gnu
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
- name: Generate coverage file
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
cargo install cargo-tarpaulin --vers "^0.13"
|
||||
cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose
|
||||
- name: Upload to Codecov
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: codecov/codecov-action@v1
|
||||
with: { file: cobertura.xml }
|
||||
|
||||
rustdoc:
|
||||
name: doc tests
|
||||
runs-on: ubuntu-latest
|
||||
|
11
.github/workflows/clippy-fmt.yml
vendored
11
.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,18 @@ 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
|
||||
|
653
CHANGES.md
653
CHANGES.md
File diff suppressed because it is too large
Load Diff
@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
|
45
Cargo.toml
45
Cargo.toml
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-web"
|
||||
version = "4.0.0-beta.14"
|
||||
version = "4.0.0-beta.20"
|
||||
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"]
|
||||
@ -28,15 +28,15 @@ path = "src/lib.rs"
|
||||
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",
|
||||
"awc",
|
||||
]
|
||||
|
||||
[features]
|
||||
@ -71,31 +71,29 @@ 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.1"
|
||||
actix-rt = "2.6"
|
||||
actix-server = "2"
|
||||
actix-service = "2.0.0"
|
||||
actix-utils = "3.0.0"
|
||||
actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true }
|
||||
actix-tls = { version = "3.0.0", default-features = false, optional = true }
|
||||
|
||||
actix-http = "3.0.0-beta.15"
|
||||
actix-router = "0.5.0-beta.2"
|
||||
actix-web-codegen = "0.5.0-beta.6"
|
||||
actix-http = "3.0.0-beta.18"
|
||||
actix-router = "0.5.0-rc.2"
|
||||
actix-web-codegen = "0.5.0-rc.1"
|
||||
|
||||
ahash = "0.7"
|
||||
bytes = "1"
|
||||
cfg-if = "1"
|
||||
cookie = { version = "0.15", features = ["percent-encode"], optional = true }
|
||||
cookie = { version = "0.16", features = ["percent-encode"], optional = true }
|
||||
derive_more = "0.99.5"
|
||||
either = "1.5.3"
|
||||
encoding_rs = "0.8"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
itoa = "0.4"
|
||||
itoa = "1"
|
||||
language-tags = "0.3"
|
||||
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"] }
|
||||
@ -107,10 +105,12 @@ time = { version = "0.3", default-features = false, features = ["formatting"] }
|
||||
url = "2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-test = { version = "0.1.0-beta.8", features = ["openssl", "rustls"] }
|
||||
awc = { version = "3.0.0-beta.13", features = ["openssl"] }
|
||||
actix-files = "0.6.0-beta.14"
|
||||
actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] }
|
||||
awc = { version = "3.0.0-beta.18", features = ["openssl"] }
|
||||
|
||||
brotli2 = "0.3.2"
|
||||
brotli = "3.3.3"
|
||||
const-str = "0.3"
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
env_logger = "0.9"
|
||||
flate2 = "1.0.13"
|
||||
@ -118,6 +118,7 @@ futures-util = { version = "0.3.7", default-features = false, features = ["std"]
|
||||
rand = "0.8"
|
||||
rcgen = "0.8"
|
||||
rustls-pemfile = "0.2"
|
||||
static_assertions = "1"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||
zstd = "0.9"
|
||||
@ -156,6 +157,10 @@ awc = { path = "awc" }
|
||||
name = "test_server"
|
||||
required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
|
||||
|
||||
[[test]]
|
||||
name = "compression"
|
||||
required-features = ["compress-brotli", "compress-gzip", "compress-zstd"]
|
||||
|
||||
[[example]]
|
||||
name = "basic"
|
||||
required-features = ["compress-gzip"]
|
||||
@ -165,7 +170,7 @@ name = "uds"
|
||||
required-features = ["compress-gzip"]
|
||||
|
||||
[[example]]
|
||||
name = "on_connect"
|
||||
name = "on-connect"
|
||||
required-features = []
|
||||
|
||||
[[bench]]
|
||||
|
156
MIGRATION.md
156
MIGRATION.md
@ -1,6 +1,6 @@
|
||||
## Unreleased
|
||||
|
||||
* The default `NormalizePath` behavior now strips trailing slashes by default. This was
|
||||
- The default `NormalizePath` behavior now strips trailing slashes by default. This was
|
||||
previously documented to be the case in v3 but the behavior now matches. The effect is that
|
||||
routes defined with trailing slashes will become inaccessible when
|
||||
using `NormalizePath::default()`. As such, calling `NormalizePath::default()` will log a warning.
|
||||
@ -11,9 +11,9 @@
|
||||
|
||||
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
|
||||
|
||||
* The `type Config` of `FromRequest` was removed.
|
||||
- The `type Config` of `FromRequest` was removed.
|
||||
|
||||
* Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd).
|
||||
- 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`
|
||||
@ -28,30 +28,30 @@
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to
|
||||
- The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to
|
||||
simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
|
||||
|
||||
* Cookie handling has been offloaded to the `cookie` crate:
|
||||
- Cookie handling has been offloaded to the `cookie` crate:
|
||||
* `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs.
|
||||
* Some types now require lifetime parameters.
|
||||
|
||||
* The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects
|
||||
- The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects
|
||||
any `actix-web` method previously expecting a time v0.1 input.
|
||||
|
||||
* Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now
|
||||
- Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now
|
||||
result in `SameSite=None` being sent with the response Set-Cookie header.
|
||||
To create a cookie without a SameSite attribute, remove any calls setting same_site.
|
||||
|
||||
* actix-http support for Actors messages was moved to actix-http crate and is enabled
|
||||
- actix-http support for Actors messages was moved to actix-http crate and is enabled
|
||||
with feature `actors`
|
||||
|
||||
* content_length function is removed from actix-http.
|
||||
- content_length function is removed from actix-http.
|
||||
You can set Content-Length by normally setting the response body or calling no_chunking function.
|
||||
|
||||
* `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
|
||||
- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
|
||||
`u64` instead of a `usize`.
|
||||
|
||||
* Code that was using `path.<index>` to access a `web::Path<(A, B, C)>`s elements now needs to use
|
||||
- Code that was using `path.<index>` to access a `web::Path<(A, B, C)>`s elements now needs to use
|
||||
destructuring or `.into_inner()`. For example:
|
||||
|
||||
```rust
|
||||
@ -71,35 +71,35 @@
|
||||
}
|
||||
```
|
||||
|
||||
* `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one.
|
||||
- `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one.
|
||||
It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`,
|
||||
or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`.
|
||||
|
||||
* `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`.
|
||||
- `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`.
|
||||
|
||||
* `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`.
|
||||
- `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`.
|
||||
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to
|
||||
- `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to
|
||||
`.await` on `run` method result, in that case it awaits server exit.
|
||||
|
||||
* `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`.
|
||||
- `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`.
|
||||
Stored data is available via `HttpRequest::app_data()` method at runtime.
|
||||
|
||||
* Extractor configuration must be registered with `App::app_data()` instead of `App::data()`
|
||||
- Extractor configuration must be registered with `App::app_data()` instead of `App::data()`
|
||||
|
||||
* Sync handlers has been removed. `.to_async()` method has been renamed to `.to()`
|
||||
- Sync handlers has been removed. `.to_async()` method has been renamed to `.to()`
|
||||
replace `fn` with `async fn` to convert sync handler to async
|
||||
|
||||
* `actix_http_test::TestServer` moved to `actix_web::test` module. To start
|
||||
- `actix_http_test::TestServer` moved to `actix_web::test` module. To start
|
||||
test server use `test::start()` or `test_start_with_config()` methods
|
||||
|
||||
* `ResponseError` trait has been reafctored. `ResponseError::error_response()` renders
|
||||
- `ResponseError` trait has been reafctored. `ResponseError::error_response()` renders
|
||||
http response.
|
||||
|
||||
* Feature `rust-tls` renamed to `rustls`
|
||||
- Feature `rust-tls` renamed to `rustls`
|
||||
|
||||
instead of
|
||||
|
||||
@ -113,7 +113,7 @@
|
||||
actix-web = { version = "2.0.0", features = ["rustls"] }
|
||||
```
|
||||
|
||||
* Feature `ssl` renamed to `openssl`
|
||||
- Feature `ssl` renamed to `openssl`
|
||||
|
||||
instead of
|
||||
|
||||
@ -126,11 +126,11 @@
|
||||
```rust
|
||||
actix-web = { version = "2.0.0", features = ["openssl"] }
|
||||
```
|
||||
* `Cors` builder now requires that you call `.finish()` to construct the middleware
|
||||
- `Cors` builder now requires that you call `.finish()` to construct the middleware
|
||||
|
||||
## 1.0.1
|
||||
|
||||
* Cors middleware has been moved to `actix-cors` crate
|
||||
- Cors middleware has been moved to `actix-cors` crate
|
||||
|
||||
instead of
|
||||
|
||||
@ -144,7 +144,7 @@
|
||||
use actix_cors::Cors;
|
||||
```
|
||||
|
||||
* Identity middleware has been moved to `actix-identity` crate
|
||||
- Identity middleware has been moved to `actix-identity` crate
|
||||
|
||||
instead of
|
||||
|
||||
@ -161,7 +161,7 @@
|
||||
|
||||
## 1.0.0
|
||||
|
||||
* Extractor configuration. In version 1.0 this is handled with the new `Data` mechanism for both setting and retrieving the configuration
|
||||
- Extractor configuration. In version 1.0 this is handled with the new `Data` mechanism for both setting and retrieving the configuration
|
||||
|
||||
instead of
|
||||
|
||||
@ -219,7 +219,7 @@
|
||||
)
|
||||
```
|
||||
|
||||
* Resource registration. 1.0 version uses generalized resource
|
||||
- Resource registration. 1.0 version uses generalized resource
|
||||
registration via `.service()` method.
|
||||
|
||||
instead of
|
||||
@ -239,7 +239,7 @@
|
||||
.route(web::post().to(post_handler))
|
||||
```
|
||||
|
||||
* Scope registration.
|
||||
- Scope registration.
|
||||
|
||||
instead of
|
||||
|
||||
@ -263,7 +263,7 @@
|
||||
);
|
||||
```
|
||||
|
||||
* `.with()`, `.with_async()` registration methods have been renamed to `.to()` and `.to_async()`.
|
||||
- `.with()`, `.with_async()` registration methods have been renamed to `.to()` and `.to_async()`.
|
||||
|
||||
instead of
|
||||
|
||||
@ -277,7 +277,7 @@
|
||||
App.new().service(web::resource("/welcome").to(welcome))
|
||||
```
|
||||
|
||||
* Passing arguments to handler with extractors, multiple arguments are allowed
|
||||
- Passing arguments to handler with extractors, multiple arguments are allowed
|
||||
|
||||
instead of
|
||||
|
||||
@ -295,7 +295,7 @@
|
||||
}
|
||||
```
|
||||
|
||||
* `.f()`, `.a()` and `.h()` handler registration methods have been removed.
|
||||
- `.f()`, `.a()` and `.h()` handler registration methods have been removed.
|
||||
Use `.to()` for handlers and `.to_async()` for async handlers. Handler function
|
||||
must use extractors.
|
||||
|
||||
@ -311,7 +311,7 @@
|
||||
App.new().service(web::resource("/welcome").to(welcome))
|
||||
```
|
||||
|
||||
* `HttpRequest` does not provide access to request's payload stream.
|
||||
- `HttpRequest` does not provide access to request's payload stream.
|
||||
|
||||
instead of
|
||||
|
||||
@ -341,7 +341,7 @@
|
||||
}
|
||||
```
|
||||
|
||||
* `State` is now `Data`. You register Data during the App initialization process
|
||||
- `State` is now `Data`. You register Data during the App initialization process
|
||||
and then access it from handlers either using a Data extractor or using
|
||||
HttpRequest's api.
|
||||
|
||||
@ -377,7 +377,7 @@
|
||||
```
|
||||
|
||||
|
||||
* AsyncResponder is removed, use `.to_async()` registration method and `impl Future<>` as result type.
|
||||
- AsyncResponder is removed, use `.to_async()` registration method and `impl Future<>` as result type.
|
||||
|
||||
instead of
|
||||
|
||||
@ -393,7 +393,7 @@
|
||||
.. simply omit AsyncResponder and the corresponding responder() finish method
|
||||
|
||||
|
||||
* Middleware
|
||||
- Middleware
|
||||
|
||||
instead of
|
||||
|
||||
@ -410,7 +410,7 @@
|
||||
.route("/index.html", web::get().to(index));
|
||||
```
|
||||
|
||||
* `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()`
|
||||
- `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()`
|
||||
method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead.
|
||||
|
||||
instead of
|
||||
@ -432,9 +432,9 @@
|
||||
}
|
||||
```
|
||||
|
||||
* `actix_web::server` module has been removed. To start http server use `actix_web::HttpServer` type
|
||||
- `actix_web::server` module has been removed. To start http server use `actix_web::HttpServer` type
|
||||
|
||||
* StaticFiles and NamedFile have been moved to a separate crate.
|
||||
- StaticFiles and NamedFile have been moved to a separate crate.
|
||||
|
||||
instead of `use actix_web::fs::StaticFile`
|
||||
|
||||
@ -444,20 +444,20 @@
|
||||
|
||||
use `use actix_files::NamedFile`
|
||||
|
||||
* Multipart has been moved to a separate crate.
|
||||
- Multipart has been moved to a separate crate.
|
||||
|
||||
instead of `use actix_web::multipart::Multipart`
|
||||
|
||||
use `use actix_multipart::Multipart`
|
||||
|
||||
* Response compression is not enabled by default.
|
||||
- Response compression is not enabled by default.
|
||||
To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`.
|
||||
|
||||
* Session middleware moved to actix-session crate
|
||||
- Session middleware moved to actix-session crate
|
||||
|
||||
* Actors support have been moved to `actix-web-actors` crate
|
||||
- Actors support have been moved to `actix-web-actors` crate
|
||||
|
||||
* Custom Error
|
||||
- Custom Error
|
||||
|
||||
Instead of error_response method alone, ResponseError now provides two methods: error_response and render_response respectively. Where, error_response creates the error response and render_response returns the error response to the caller.
|
||||
|
||||
@ -471,7 +471,7 @@
|
||||
|
||||
## 0.7.15
|
||||
|
||||
* The `' '` character is not percent decoded anymore before matching routes. If you need to use it in
|
||||
- The `' '` character is not percent decoded anymore before matching routes. If you need to use it in
|
||||
your routes, you should use `%20`.
|
||||
|
||||
instead of
|
||||
@ -496,18 +496,18 @@
|
||||
}
|
||||
```
|
||||
|
||||
* If you used `AsyncResult::async` you need to replace it with `AsyncResult::future`
|
||||
- If you used `AsyncResult::async` you need to replace it with `AsyncResult::future`
|
||||
|
||||
|
||||
## 0.7.4
|
||||
|
||||
* `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple
|
||||
- `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple
|
||||
even for handler with one parameter.
|
||||
|
||||
|
||||
## 0.7
|
||||
|
||||
* `HttpRequest` does not implement `Stream` anymore. If you need to read request payload
|
||||
- `HttpRequest` does not implement `Stream` anymore. If you need to read request payload
|
||||
use `HttpMessage::payload()` method.
|
||||
|
||||
instead of
|
||||
@ -533,10 +533,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
* [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html)
|
||||
- [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html)
|
||||
trait uses `&HttpRequest` instead of `&mut HttpRequest`.
|
||||
|
||||
* Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead.
|
||||
- Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead.
|
||||
|
||||
instead of
|
||||
|
||||
@ -550,17 +550,17 @@
|
||||
fn index((query, json): (Query<..>, Json<MyStruct)) -> impl Responder {}
|
||||
```
|
||||
|
||||
* `Handler::handle()` uses `&self` instead of `&mut self`
|
||||
- `Handler::handle()` uses `&self` instead of `&mut self`
|
||||
|
||||
* `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value
|
||||
- `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value
|
||||
|
||||
* Removed deprecated `HttpServer::threads()`, use
|
||||
- Removed deprecated `HttpServer::threads()`, use
|
||||
[HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead.
|
||||
|
||||
* Renamed `client::ClientConnectorError::Connector` to
|
||||
- Renamed `client::ClientConnectorError::Connector` to
|
||||
`client::ClientConnectorError::Resolver`
|
||||
|
||||
* `Route::with()` does not return `ExtractorConfig`, to configure
|
||||
- `Route::with()` does not return `ExtractorConfig`, to configure
|
||||
extractor use `Route::with_config()`
|
||||
|
||||
instead of
|
||||
@ -589,26 +589,26 @@
|
||||
}
|
||||
```
|
||||
|
||||
* `Route::with_async()` does not return `ExtractorConfig`, to configure
|
||||
- `Route::with_async()` does not return `ExtractorConfig`, to configure
|
||||
extractor use `Route::with_async_config()`
|
||||
|
||||
|
||||
## 0.6
|
||||
|
||||
* `Path<T>` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest`
|
||||
- `Path<T>` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest`
|
||||
|
||||
* `ws::Message::Close` now includes optional close reason.
|
||||
- `ws::Message::Close` now includes optional close reason.
|
||||
`ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed.
|
||||
|
||||
* `HttpServer::threads()` renamed to `HttpServer::workers()`.
|
||||
- `HttpServer::threads()` renamed to `HttpServer::workers()`.
|
||||
|
||||
* `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated.
|
||||
- `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated.
|
||||
Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead.
|
||||
|
||||
* `HttpRequest::extensions()` returns read only reference to the request's Extension
|
||||
- `HttpRequest::extensions()` returns read only reference to the request's Extension
|
||||
`HttpRequest::extensions_mut()` returns mutable reference.
|
||||
|
||||
* Instead of
|
||||
- Instead of
|
||||
|
||||
`use actix_web::middleware::{
|
||||
CookieSessionBackend, CookieSessionError, RequestSession,
|
||||
@ -619,15 +619,15 @@
|
||||
`use actix_web::middleware::session{CookieSessionBackend, CookieSessionError,
|
||||
RequestSession, Session, SessionBackend, SessionImpl, SessionStorage};`
|
||||
|
||||
* `FromRequest::from_request()` accepts mutable reference to a request
|
||||
- `FromRequest::from_request()` accepts mutable reference to a request
|
||||
|
||||
* `FromRequest::Result` has to implement `Into<Reply<Self>>`
|
||||
- `FromRequest::Result` has to implement `Into<Reply<Self>>`
|
||||
|
||||
* [`Responder::respond_to()`](
|
||||
- [`Responder::respond_to()`](
|
||||
https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to)
|
||||
is generic over `S`
|
||||
|
||||
* Use `Query` extractor instead of HttpRequest::query()`.
|
||||
- Use `Query` extractor instead of HttpRequest::query()`.
|
||||
|
||||
```rust
|
||||
fn index(q: Query<HashMap<String, String>>) -> Result<..> {
|
||||
@ -641,37 +641,37 @@
|
||||
let q = Query::<HashMap<String, String>>::extract(req);
|
||||
```
|
||||
|
||||
* Websocket operations are implemented as `WsWriter` trait.
|
||||
- Websocket operations are implemented as `WsWriter` trait.
|
||||
you need to use `use actix_web::ws::WsWriter`
|
||||
|
||||
|
||||
## 0.5
|
||||
|
||||
* `HttpResponseBuilder::body()`, `.finish()`, `.json()`
|
||||
- `HttpResponseBuilder::body()`, `.finish()`, `.json()`
|
||||
methods return `HttpResponse` instead of `Result<HttpResponse>`
|
||||
|
||||
* `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version`
|
||||
- `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version`
|
||||
moved to `actix_web::http` module
|
||||
|
||||
* `actix_web::header` moved to `actix_web::http::header`
|
||||
- `actix_web::header` moved to `actix_web::http::header`
|
||||
|
||||
* `NormalizePath` moved to `actix_web::http` module
|
||||
- `NormalizePath` moved to `actix_web::http` module
|
||||
|
||||
* `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function,
|
||||
- `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function,
|
||||
shortcut for `actix_web::server::HttpServer::new()`
|
||||
|
||||
* `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself
|
||||
- `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself
|
||||
|
||||
* `StaticFiles::new()`'s show_index parameter removed, use `show_files_listing()` method instead.
|
||||
- `StaticFiles::new()`'s show_index parameter removed, use `show_files_listing()` method instead.
|
||||
|
||||
* `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type
|
||||
- `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type
|
||||
|
||||
* `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()`
|
||||
- `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()`
|
||||
functions should be used instead
|
||||
|
||||
* `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>`
|
||||
- `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>`
|
||||
instead of `Result<_, http::Error>`
|
||||
|
||||
* `Application` renamed to a `App`
|
||||
- `Application` renamed to a `App`
|
||||
|
||||
* `actix_web::Reply`, `actix_web::Resource` moved to `actix_web::dev`
|
||||
- `actix_web::Reply`, `actix_web::Resource` moved to `actix_web::dev`
|
||||
|
70
README.md
70
README.md
@ -6,13 +6,13 @@
|
||||
<p>
|
||||
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web/4.0.0-beta.14)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||
[](https://docs.rs/actix-web/4.0.0-beta.20)
|
||||

|
||||

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

|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
@ -21,25 +21,25 @@
|
||||
|
||||
## 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+
|
||||
- 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.54+
|
||||
|
||||
## 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)
|
||||
- [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
|
||||
|
||||
@ -71,18 +71,18 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
### 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/)
|
||||
- [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.
|
||||
@ -96,9 +96,9 @@ One of the fastest web frameworks available according to the
|
||||
|
||||
This project is licensed under either of
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
||||
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
||||
[http://www.apache.org/licenses/LICENSE-2.0])
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
||||
[http://opensource.org/licenses/MIT])
|
||||
|
||||
at your option.
|
||||
|
@ -3,43 +3,65 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.6.0-beta.14 - 2022-01-14
|
||||
- The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583]
|
||||
|
||||
[#2583]: https://github.com/actix/actix-web/pull/2583
|
||||
|
||||
|
||||
## 0.6.0-beta.13 - 2022-01-04
|
||||
- The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398]
|
||||
- The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398]
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
[#2398]: https://github.com/actix/actix-web/pull/2398
|
||||
|
||||
|
||||
## 0.6.0-beta.12 - 2021-12-29
|
||||
- No significant changes since `0.6.0-beta.11`.
|
||||
|
||||
|
||||
## 0.6.0-beta.11 - 2021-12-27
|
||||
- No significant changes since `0.6.0-beta.10`.
|
||||
|
||||
|
||||
## 0.6.0-beta.10 - 2021-12-11
|
||||
* No significant changes since `0.6.0-beta.9`.
|
||||
- No significant changes since `0.6.0-beta.9`.
|
||||
|
||||
|
||||
## 0.6.0-beta.9 - 2021-11-22
|
||||
* Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408]
|
||||
* Add `NamedFile::open_async`. [#2408]
|
||||
* Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453]
|
||||
* The `Responder` impl for `NamedFile` now has a boxed future associated type. [#2408]
|
||||
* The `Service` impl for `NamedFileService` now has a boxed future associated type. [#2408]
|
||||
* Add `impl Clone` for `FilesService`. [#2408]
|
||||
- Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408]
|
||||
- Add `NamedFile::open_async`. [#2408]
|
||||
- Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453]
|
||||
- The `Responder` impl for `NamedFile` now has a boxed future associated type. [#2408]
|
||||
- The `Service` impl for `NamedFileService` now has a boxed future associated type. [#2408]
|
||||
- Add `impl Clone` for `FilesService`. [#2408]
|
||||
|
||||
[#2408]: https://github.com/actix/actix-web/pull/2408
|
||||
[#2453]: https://github.com/actix/actix-web/pull/2453
|
||||
|
||||
|
||||
## 0.6.0-beta.8 - 2021-10-20
|
||||
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
||||
|
||||
## 0.6.0-beta.7 - 2021-09-09
|
||||
* Minimum supported Rust version (MSRV) is now 1.51.
|
||||
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||
|
||||
|
||||
## 0.6.0-beta.6 - 2021-06-26
|
||||
* Added `Files::path_filter()`. [#2274]
|
||||
* `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228]
|
||||
- Added `Files::path_filter()`. [#2274]
|
||||
- `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228]
|
||||
|
||||
[#2274]: https://github.com/actix/actix-web/pull/2274
|
||||
[#2228]: https://github.com/actix/actix-web/pull/2228
|
||||
|
||||
|
||||
## 0.6.0-beta.5 - 2021-06-17
|
||||
* `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135]
|
||||
* For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156]
|
||||
* `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225]
|
||||
* `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257]
|
||||
- `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135]
|
||||
- For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156]
|
||||
- `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225]
|
||||
- `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257]
|
||||
|
||||
[#2135]: https://github.com/actix/actix-web/pull/2135
|
||||
[#2156]: https://github.com/actix/actix-web/pull/2156
|
||||
@ -48,130 +70,130 @@
|
||||
|
||||
|
||||
## 0.6.0-beta.4 - 2021-04-02
|
||||
* Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046]
|
||||
- Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046]
|
||||
|
||||
[#2046]: https://github.com/actix/actix-web/pull/2046
|
||||
|
||||
|
||||
## 0.6.0-beta.3 - 2021-03-09
|
||||
* No notable changes.
|
||||
- No notable changes.
|
||||
|
||||
|
||||
## 0.6.0-beta.2 - 2021-02-10
|
||||
* Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887]
|
||||
* Replace `v_htmlescape` with `askama_escape`. [#1953]
|
||||
- Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887]
|
||||
- Replace `v_htmlescape` with `askama_escape`. [#1953]
|
||||
|
||||
[#1887]: https://github.com/actix/actix-web/pull/1887
|
||||
[#1953]: https://github.com/actix/actix-web/pull/1953
|
||||
|
||||
|
||||
## 0.6.0-beta.1 - 2021-01-07
|
||||
* `HttpRange::parse` now has its own error type.
|
||||
* Update `bytes` to `1.0`. [#1813]
|
||||
- `HttpRange::parse` now has its own error type.
|
||||
- Update `bytes` to `1.0`. [#1813]
|
||||
|
||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||
|
||||
|
||||
## 0.5.0 - 2020-12-26
|
||||
* Optionally support hidden files/directories. [#1811]
|
||||
- Optionally support hidden files/directories. [#1811]
|
||||
|
||||
[#1811]: https://github.com/actix/actix-web/pull/1811
|
||||
|
||||
|
||||
## 0.4.1 - 2020-11-24
|
||||
* Clarify order of parameters in `Files::new` and improve docs.
|
||||
- Clarify order of parameters in `Files::new` and improve docs.
|
||||
|
||||
|
||||
## 0.4.0 - 2020-10-06
|
||||
* Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714]
|
||||
- Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714]
|
||||
|
||||
[#1714]: https://github.com/actix/actix-web/pull/1714
|
||||
|
||||
|
||||
## 0.3.0 - 2020-09-11
|
||||
* No significant changes from 0.3.0-beta.1.
|
||||
- No significant changes from 0.3.0-beta.1.
|
||||
|
||||
|
||||
## 0.3.0-beta.1 - 2020-07-15
|
||||
* Update `v_htmlescape` to 0.10
|
||||
* Update `actix-web` and `actix-http` dependencies to beta.1
|
||||
- Update `v_htmlescape` to 0.10
|
||||
- Update `actix-web` and `actix-http` dependencies to beta.1
|
||||
|
||||
|
||||
## 0.3.0-alpha.1 - 2020-05-23
|
||||
* Update `actix-web` and `actix-http` dependencies to alpha
|
||||
* Fix some typos in the docs
|
||||
* Bump minimum supported Rust version to 1.40
|
||||
* Support sending Content-Length when Content-Range is specified [#1384]
|
||||
- Update `actix-web` and `actix-http` dependencies to alpha
|
||||
- Fix some typos in the docs
|
||||
- Bump minimum supported Rust version to 1.40
|
||||
- Support sending Content-Length when Content-Range is specified [#1384]
|
||||
|
||||
[#1384]: https://github.com/actix/actix-web/pull/1384
|
||||
|
||||
|
||||
## 0.2.1 - 2019-12-22
|
||||
* Use the same format for file URLs regardless of platforms
|
||||
- Use the same format for file URLs regardless of platforms
|
||||
|
||||
|
||||
## 0.2.0 - 2019-12-20
|
||||
* Fix BodyEncoding trait import #1220
|
||||
- Fix BodyEncoding trait import #1220
|
||||
|
||||
|
||||
## 0.2.0-alpha.1 - 2019-12-07
|
||||
* Migrate to `std::future`
|
||||
- Migrate to `std::future`
|
||||
|
||||
|
||||
## 0.1.7 - 2019-11-06
|
||||
* Add an additional `filename*` param in the `Content-Disposition` header of
|
||||
- Add an additional `filename*` param in the `Content-Disposition` header of
|
||||
`actix_files::NamedFile` to be more compatible. (#1151)
|
||||
|
||||
## 0.1.6 - 2019-10-14
|
||||
* Add option to redirect to a slash-ended path `Files` #1132
|
||||
- Add option to redirect to a slash-ended path `Files` #1132
|
||||
|
||||
|
||||
## 0.1.5 - 2019-10-08
|
||||
* Bump up `mime_guess` crate version to 2.0.1
|
||||
* Bump up `percent-encoding` crate version to 2.1
|
||||
* Allow user defined request guards for `Files` #1113
|
||||
- Bump up `mime_guess` crate version to 2.0.1
|
||||
- Bump up `percent-encoding` crate version to 2.1
|
||||
- Allow user defined request guards for `Files` #1113
|
||||
|
||||
|
||||
## 0.1.4 - 2019-07-20
|
||||
* Allow to disable `Content-Disposition` header #686
|
||||
- Allow to disable `Content-Disposition` header #686
|
||||
|
||||
|
||||
## 0.1.3 - 2019-06-28
|
||||
* Do not set `Content-Length` header, let actix-http set it #930
|
||||
- Do not set `Content-Length` header, let actix-http set it #930
|
||||
|
||||
|
||||
## 0.1.2 - 2019-06-13
|
||||
* Content-Length is 0 for NamedFile HEAD request #914
|
||||
* Fix ring dependency from actix-web default features for #741
|
||||
- Content-Length is 0 for NamedFile HEAD request #914
|
||||
- Fix ring dependency from actix-web default features for #741
|
||||
|
||||
|
||||
## 0.1.1 - 2019-06-01
|
||||
* Static files are incorrectly served as both chunked and with length #812
|
||||
- Static files are incorrectly served as both chunked and with length #812
|
||||
|
||||
|
||||
## 0.1.0 - 2019-05-25
|
||||
* NamedFile last-modified check always fails due to nano-seconds in file modified date #820
|
||||
- NamedFile last-modified check always fails due to nano-seconds in file modified date #820
|
||||
|
||||
|
||||
## 0.1.0-beta.4 - 2019-05-12
|
||||
* Update actix-web to beta.4
|
||||
- Update actix-web to beta.4
|
||||
|
||||
|
||||
## 0.1.0-beta.1 - 2019-04-20
|
||||
* Update actix-web to beta.1
|
||||
- Update actix-web to beta.1
|
||||
|
||||
|
||||
## 0.1.0-alpha.6 - 2019-04-14
|
||||
* Update actix-web to alpha6
|
||||
- Update actix-web to alpha6
|
||||
|
||||
|
||||
## 0.1.0-alpha.4 - 2019-04-08
|
||||
* Update actix-web to alpha4
|
||||
- Update actix-web to alpha4
|
||||
|
||||
|
||||
## 0.1.0-alpha.2 - 2019-04-02
|
||||
* Add default handler support
|
||||
- Add default handler support
|
||||
|
||||
|
||||
## 0.1.0-alpha.1 - 2019-03-28
|
||||
* Initial impl
|
||||
- Initial impl
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-files"
|
||||
version = "0.6.0-beta.10"
|
||||
version = "0.6.0-beta.14"
|
||||
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.15"
|
||||
actix-http = "3.0.0-beta.18"
|
||||
actix-service = "2"
|
||||
actix-utils = "3"
|
||||
actix-web = { version = "4.0.0-beta.14", default-features = false }
|
||||
actix-web = { version = "4.0.0-beta.20", 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.8"
|
||||
actix-web = "4.0.0-beta.14"
|
||||
actix-test = "0.1.0-beta.11"
|
||||
actix-web = "4.0.0-beta.20"
|
||||
tempfile = "3.2"
|
||||
|
@ -3,11 +3,11 @@
|
||||
> Static file serving for Actix Web
|
||||
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://docs.rs/actix-files/0.6.0-beta.10)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||
[](https://docs.rs/actix-files/0.6.0-beta.14)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.6.0-beta.10)
|
||||
[](https://deps.rs/crate/actix-files/0.6.0-beta.14)
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
@ -15,4 +15,4 @@
|
||||
|
||||
- [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
|
||||
- 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! {
|
||||
@ -214,64 +217,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)
|
||||
|
@ -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;
|
||||
|
@ -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},
|
||||
@ -303,7 +303,7 @@ mod tests {
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
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(),
|
||||
@ -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]
|
||||
@ -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,32 +1,27 @@
|
||||
use std::{
|
||||
fmt,
|
||||
fs::Metadata,
|
||||
io,
|
||||
ops::{Deref, DerefMut},
|
||||
path::{Path, PathBuf},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
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,
|
||||
},
|
||||
Error, HttpMessage, HttpRequest, HttpResponse, Responder,
|
||||
};
|
||||
use bitflags::bitflags;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use mime_guess::from_path;
|
||||
|
||||
@ -43,7 +38,7 @@ bitflags! {
|
||||
|
||||
impl Default for Flags {
|
||||
fn default() -> Self {
|
||||
Flags::from_bits_truncate(0b0000_0111)
|
||||
Flags::from_bits_truncate(0b0000_1111)
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,9 +66,12 @@ impl Default for Flags {
|
||||
/// NamedFile::open_async("./static/index.html").await
|
||||
/// }
|
||||
/// ```
|
||||
#[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")]
|
||||
@ -224,7 +196,6 @@ impl NamedFile {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
/// Attempts to open a file in read-only mode.
|
||||
///
|
||||
/// # Examples
|
||||
@ -232,6 +203,7 @@ impl NamedFile {
|
||||
/// use actix_files::NamedFile;
|
||||
/// let file = NamedFile::open("foo.txt");
|
||||
/// ```
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
||||
let file = File::open(&path)?;
|
||||
Self::from_file(file, path)
|
||||
@ -295,23 +267,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;
|
||||
@ -337,7 +307,7 @@ impl NamedFile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether to use ETag or not.
|
||||
/// Specifies whether to return `ETag` header in response.
|
||||
///
|
||||
/// Default is true.
|
||||
#[inline]
|
||||
@ -346,7 +316,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,14 +334,18 @@ impl NamedFile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Creates an `ETag` in a format is similar to Apache's.
|
||||
pub(crate) fn etag(&self) -> Option<header::EntityTag> {
|
||||
// This etag format is similar to Apache's.
|
||||
self.modified.as_ref().map(|mtime| {
|
||||
let ino = {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt as _;
|
||||
|
||||
self.md.ino()
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
0
|
||||
@ -382,7 +356,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(),
|
||||
@ -401,12 +375,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((
|
||||
@ -416,7 +391,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);
|
||||
@ -472,36 +447,36 @@ impl NamedFile {
|
||||
false
|
||||
};
|
||||
|
||||
let mut resp = HttpResponse::build(self.status_code);
|
||||
let mut res = HttpResponse::build(self.status_code);
|
||||
|
||||
if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
let ct = equiv_utf8_text(self.content_type.clone());
|
||||
resp.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 {
|
||||
resp.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
|
||||
}
|
||||
self.content_type
|
||||
};
|
||||
|
||||
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||
|
||||
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
||||
resp.insert_header((
|
||||
res.insert_header((
|
||||
header::CONTENT_DISPOSITION,
|
||||
self.content_disposition.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// default compressing
|
||||
if let Some(current_encoding) = self.encoding {
|
||||
resp.encoding(current_encoding);
|
||||
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
|
||||
}
|
||||
|
||||
if let Some(lm) = last_modified {
|
||||
resp.insert_header((header::LAST_MODIFIED, lm.to_string()));
|
||||
res.insert_header((header::LAST_MODIFIED, lm.to_string()));
|
||||
}
|
||||
|
||||
if let Some(etag) = etag {
|
||||
resp.insert_header((header::ETAG, etag.to_string()));
|
||||
res.insert_header((header::ETAG, etag.to_string()));
|
||||
}
|
||||
|
||||
resp.insert_header((header::ACCEPT_RANGES, "bytes"));
|
||||
res.insert_header((header::ACCEPT_RANGES, "bytes"));
|
||||
|
||||
let mut length = self.md.len();
|
||||
let mut offset = 0;
|
||||
@ -513,24 +488,29 @@ impl NamedFile {
|
||||
length = ranges[0].length;
|
||||
offset = ranges[0].start;
|
||||
|
||||
resp.encoding(ContentEncoding::Identity);
|
||||
resp.insert_header((
|
||||
// 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()),
|
||||
));
|
||||
} else {
|
||||
resp.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length)));
|
||||
return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
|
||||
res.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length)));
|
||||
return res.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
|
||||
};
|
||||
} else {
|
||||
return resp.status(StatusCode::BAD_REQUEST).finish();
|
||||
return res.status(StatusCode::BAD_REQUEST).finish();
|
||||
};
|
||||
};
|
||||
|
||||
if precondition_failed {
|
||||
return resp.status(StatusCode::PRECONDITION_FAILED).finish();
|
||||
return res.status(StatusCode::PRECONDITION_FAILED).finish();
|
||||
} else if not_modified {
|
||||
return resp
|
||||
return res
|
||||
.status(StatusCode::NOT_MODIFIED)
|
||||
.body(body::None::new())
|
||||
.map_into_boxed_body();
|
||||
@ -539,10 +519,10 @@ impl NamedFile {
|
||||
let reader = chunked::new_chunked_read(length, offset, self.file);
|
||||
|
||||
if offset != 0 || length != self.md.len() {
|
||||
resp.status(StatusCode::PARTIAL_CONTENT);
|
||||
res.status(StatusCode::PARTIAL_CONTENT);
|
||||
}
|
||||
|
||||
resp.body(SizedStream::new(length, reader))
|
||||
res.body(SizedStream::new(length, reader))
|
||||
}
|
||||
}
|
||||
|
||||
@ -586,20 +566,6 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for NamedFile {
|
||||
type Target = File;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.file
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for NamedFile {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.file
|
||||
}
|
||||
}
|
||||
|
||||
impl Responder for NamedFile {
|
||||
type Body = BoxBody;
|
||||
|
||||
@ -636,7 +602,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,6 +55,7 @@ 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('\\'));
|
||||
@ -48,6 +64,12 @@ impl PathBufWrap {
|
||||
}
|
||||
}
|
||||
|
||||
// make sure we agree with stdlib parser
|
||||
for (i, component) in buf.components().enumerate() {
|
||||
assert!(matches!(component, Component::Normal(_)));
|
||||
assert!(i < segment_count);
|
||||
}
|
||||
|
||||
Ok(PathBufWrap(buf))
|
||||
}
|
||||
}
|
||||
@ -63,7 +85,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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc};
|
||||
|
||||
use actix_service::Service;
|
||||
use actix_web::{
|
||||
dev::{ServiceRequest, ServiceResponse},
|
||||
body::BoxBody,
|
||||
dev::{self, Service, ServiceRequest, ServiceResponse},
|
||||
error::Error,
|
||||
guard::Guard,
|
||||
http::{header, Method},
|
||||
@ -94,16 +94,16 @@ impl fmt::Debug for FilesService {
|
||||
}
|
||||
|
||||
impl Service<ServiceRequest> for FilesService {
|
||||
type Response = ServiceResponse;
|
||||
type Response = ServiceResponse<BoxBody>;
|
||||
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 {
|
||||
// execute user defined guards
|
||||
(**guard).check(req.head())
|
||||
(**guard).check(&req.guard_ctx())
|
||||
} else {
|
||||
// default behavior
|
||||
matches!(*req.method(), Method::HEAD | Method::GET)
|
||||
@ -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;
|
||||
}
|
||||
|
@ -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,126 +3,136 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-beta.11 - 2022-01-04
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
|
||||
## 3.0.0-beta.10 - 2021-12-27
|
||||
- Update `actix-server` to `2.0.0-rc.2`. [#2550]
|
||||
|
||||
[#2550]: https://github.com/actix/actix-web/pull/2550
|
||||
|
||||
|
||||
## 3.0.0-beta.9 - 2021-12-11
|
||||
* No significant changes since `3.0.0-beta.8`.
|
||||
- No significant changes since `3.0.0-beta.8`.
|
||||
|
||||
|
||||
## 3.0.0-beta.8 - 2021-11-30
|
||||
* Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
||||
- Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
||||
|
||||
[#2474]: https://github.com/actix/actix-web/pull/2474
|
||||
|
||||
|
||||
## 3.0.0-beta.7 - 2021-11-22
|
||||
* Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
|
||||
- Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
|
||||
|
||||
[#2408]: https://github.com/actix/actix-web/pull/2408
|
||||
|
||||
|
||||
## 3.0.0-beta.6 - 2021-11-15
|
||||
* `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
|
||||
* Update `actix-server` to `2.0.0-beta.9`. [#2442]
|
||||
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||
- `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
|
||||
- Update `actix-server` to `2.0.0-beta.9`. [#2442]
|
||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
||||
[#2442]: https://github.com/actix/actix-web/pull/2442
|
||||
|
||||
|
||||
## 3.0.0-beta.5 - 2021-09-09
|
||||
* Minimum supported Rust version (MSRV) is now 1.51.
|
||||
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||
|
||||
|
||||
## 3.0.0-beta.4 - 2021-04-02
|
||||
* Added `TestServer::client_headers` method. [#2097]
|
||||
- Added `TestServer::client_headers` method. [#2097]
|
||||
|
||||
[#2097]: https://github.com/actix/actix-web/pull/2097
|
||||
|
||||
|
||||
## 3.0.0-beta.3 - 2021-03-09
|
||||
* No notable changes.
|
||||
- No notable changes.
|
||||
|
||||
|
||||
## 3.0.0-beta.2 - 2021-02-10
|
||||
* No notable changes.
|
||||
- No notable changes.
|
||||
|
||||
|
||||
## 3.0.0-beta.1 - 2021-01-07
|
||||
* Update `bytes` to `1.0`. [#1813]
|
||||
- Update `bytes` to `1.0`. [#1813]
|
||||
|
||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||
|
||||
|
||||
## 2.1.0 - 2020-11-25
|
||||
* Add ability to set address for `TestServer`. [#1645]
|
||||
* Upgrade `base64` to `0.13`.
|
||||
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||
- Add ability to set address for `TestServer`. [#1645]
|
||||
- Upgrade `base64` to `0.13`.
|
||||
- Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||
|
||||
[#1773]: https://github.com/actix/actix-web/pull/1773
|
||||
[#1645]: https://github.com/actix/actix-web/pull/1645
|
||||
|
||||
|
||||
## 2.0.0 - 2020-09-11
|
||||
* Update actix-codec and actix-utils dependencies.
|
||||
- Update actix-codec and actix-utils dependencies.
|
||||
|
||||
|
||||
## 2.0.0-alpha.1 - 2020-05-23
|
||||
* Update the `time` dependency to 0.2.7
|
||||
* Update `actix-connect` dependency to 2.0.0-alpha.2
|
||||
* Make `test_server` `async` fn.
|
||||
* Bump minimum supported Rust version to 1.40
|
||||
* Replace deprecated `net2` crate with `socket2`
|
||||
* Update `base64` dependency to 0.12
|
||||
* Update `env_logger` dependency to 0.7
|
||||
- Update the `time` dependency to 0.2.7
|
||||
- Update `actix-connect` dependency to 2.0.0-alpha.2
|
||||
- Make `test_server` `async` fn.
|
||||
- Bump minimum supported Rust version to 1.40
|
||||
- Replace deprecated `net2` crate with `socket2`
|
||||
- Update `base64` dependency to 0.12
|
||||
- Update `env_logger` dependency to 0.7
|
||||
|
||||
## 1.0.0 - 2019-12-13
|
||||
* Replaced `TestServer::start()` with `test_server()`
|
||||
- Replaced `TestServer::start()` with `test_server()`
|
||||
|
||||
|
||||
## 1.0.0-alpha.3 - 2019-12-07
|
||||
* Migrate to `std::future`
|
||||
- Migrate to `std::future`
|
||||
|
||||
|
||||
## 0.2.5 - 2019-09-17
|
||||
* Update serde_urlencoded to "0.6.1"
|
||||
* Increase TestServerRuntime timeouts from 500ms to 3000ms
|
||||
* Do not override current `System`
|
||||
- Update serde_urlencoded to "0.6.1"
|
||||
- Increase TestServerRuntime timeouts from 500ms to 3000ms
|
||||
- Do not override current `System`
|
||||
|
||||
|
||||
## 0.2.4 - 2019-07-18
|
||||
* Update actix-server to 0.6
|
||||
- Update actix-server to 0.6
|
||||
|
||||
|
||||
## 0.2.3 - 2019-07-16
|
||||
* Add `delete`, `options`, `patch` methods to `TestServerRunner`
|
||||
- Add `delete`, `options`, `patch` methods to `TestServerRunner`
|
||||
|
||||
|
||||
## 0.2.2 - 2019-06-16
|
||||
* Add .put() and .sput() methods
|
||||
- Add .put() and .sput() methods
|
||||
|
||||
|
||||
## 0.2.1 - 2019-06-05
|
||||
* Add license files
|
||||
- Add license files
|
||||
|
||||
|
||||
## 0.2.0 - 2019-05-12
|
||||
* Update awc and actix-http deps
|
||||
- Update awc and actix-http deps
|
||||
|
||||
|
||||
## 0.1.1 - 2019-04-24
|
||||
* Always make new connection for http client
|
||||
- Always make new connection for http client
|
||||
|
||||
|
||||
## 0.1.0 - 2019-04-16
|
||||
* No changes
|
||||
- No changes
|
||||
|
||||
|
||||
## 0.1.0-alpha.3 - 2019-04-02
|
||||
* Request functions accept path #743
|
||||
- Request functions accept path #743
|
||||
|
||||
|
||||
## 0.1.0-alpha.2 - 2019-03-29
|
||||
* Added TestServerRuntime::load_body() method
|
||||
* Update actix-http and awc libraries
|
||||
- Added TestServerRuntime::load_body() method
|
||||
- Update actix-http and awc libraries
|
||||
|
||||
|
||||
## 0.1.0-alpha.1 - 2019-03-28
|
||||
* Initial impl
|
||||
- Initial impl
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http-test"
|
||||
version = "3.0.0-beta.9"
|
||||
version = "3.0.0-beta.11"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Various helpers for Actix applications to use during testing"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
@ -31,11 +31,11 @@ openssl = ["tls-openssl", "awc/openssl"]
|
||||
[dependencies]
|
||||
actix-service = "2.0.0"
|
||||
actix-codec = "0.4.1"
|
||||
actix-tls = "3.0.0-rc.1"
|
||||
actix-tls = "3.0.0"
|
||||
actix-utils = "3.0.0"
|
||||
actix-rt = "2.2"
|
||||
actix-server = "2.0.0-rc.1"
|
||||
awc = { version = "3.0.0-beta.13", default-features = false }
|
||||
actix-server = "2"
|
||||
awc = { version = "3.0.0-beta.18", default-features = false }
|
||||
|
||||
base64 = "0.13"
|
||||
bytes = "1"
|
||||
@ -48,8 +48,8 @@ serde_json = "1.0"
|
||||
slab = "0.4"
|
||||
serde_urlencoded = "0.7"
|
||||
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||
tokio = { version = "1.2", features = ["sync"] }
|
||||
tokio = { version = "1.8.4", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["cookies"] }
|
||||
actix-http = "3.0.0-beta.15"
|
||||
actix-web = { version = "4.0.0-beta.20", default-features = false, features = ["cookies"] }
|
||||
actix-http = "3.0.0-beta.18"
|
||||
|
@ -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.9)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||
[](https://docs.rs/actix-http-test/3.0.0-beta.11)
|
||||
[](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.9)
|
||||
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.11)
|
||||
[](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
|
||||
|
@ -12,7 +12,7 @@ use std::{net, thread, time::Duration};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use actix_rt::{net::TcpStream, System};
|
||||
use actix_server::{Server, ServiceFactory};
|
||||
use actix_server::{Server, ServerServiceFactory};
|
||||
use awc::{
|
||||
error::PayloadError, http::header::HeaderMap, ws, Client, ClientRequest, ClientResponse,
|
||||
Connector,
|
||||
@ -51,13 +51,13 @@ use tokio::sync::mpsc;
|
||||
/// assert!(response.status().is_success());
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer {
|
||||
pub async fn test_server<F: ServerServiceFactory<TcpStream>>(factory: F) -> TestServer {
|
||||
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
test_server_with_addr(tcp, factory).await
|
||||
}
|
||||
|
||||
/// Start [`test server`](test_server()) on an existing address binding.
|
||||
pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
|
||||
pub async fn test_server_with_addr<F: ServerServiceFactory<TcpStream>>(
|
||||
tcp: net::TcpListener,
|
||||
factory: F,
|
||||
) -> TestServer {
|
||||
@ -107,7 +107,7 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
|
||||
Connector::new()
|
||||
.conn_lifetime(Duration::from_secs(0))
|
||||
.timeout(Duration::from_millis(30000))
|
||||
.ssl(builder.build())
|
||||
.openssl(builder.build())
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "openssl"))]
|
||||
|
@ -1,46 +1,120 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
### Added
|
||||
- Response headers can be sent as camel case using `res.head_mut().set_camel_case_headers(true)`. [#2587]
|
||||
- `ResponseHead` now implements `Clone`. [#2585]
|
||||
|
||||
### Changed
|
||||
- Brotli (de)compression support is now provided by the `brotli` crate. [#2538]
|
||||
|
||||
### Removed
|
||||
- `ResponseHead::extensions[_mut]()`. [#2585]
|
||||
- `ResponseBuilder::extensions[_mut]()`. [#2585]
|
||||
|
||||
[#2538]: https://github.com/actix/actix-web/pull/2538
|
||||
[#2585]: https://github.com/actix/actix-web/pull/2585
|
||||
[#2587]: https://github.com/actix/actix-web/pull/2587
|
||||
|
||||
|
||||
## 3.0.0-beta.18 - 2022-01-04
|
||||
### Added
|
||||
- `impl Eq` for `header::ContentEncoding`. [#2501]
|
||||
- `impl Copy` for `QualityItem` where `T: Copy`. [#2501]
|
||||
- `Quality::ZERO` equivalent to `q=0`. [#2501]
|
||||
- `QualityItem::zero` that uses `Quality::ZERO`. [#2501]
|
||||
- `ContentEncoding::to_header_value()`. [#2501]
|
||||
|
||||
### Changed
|
||||
- `Quality::MIN` is now the smallest non-zero value. [#2501]
|
||||
- `QualityItem::min` semantics changed with `QualityItem::MIN`. [#2501]
|
||||
- Rename `ContentEncoding::{Br => Brotli}`. [#2501]
|
||||
- Rename `header::EntityTag::{weak => new_weak, strong => new_strong}`. [#2565]
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
### Fixed
|
||||
- `ContentEncoding::Identity` can now be parsed from a string. [#2501]
|
||||
- A `Vary` header is now correctly sent along with compressed content. [#2501]
|
||||
|
||||
### Removed
|
||||
- `ContentEncoding::Auto` variant. [#2501]
|
||||
- `ContentEncoding::is_compression()`. [#2501]
|
||||
|
||||
[#2501]: https://github.com/actix/actix-web/pull/2501
|
||||
[#2565]: https://github.com/actix/actix-web/pull/2565
|
||||
|
||||
|
||||
## 3.0.0-beta.17 - 2021-12-27
|
||||
### Changes
|
||||
- `HeaderMap::get_all` now returns a `std::slice::Iter`. [#2527]
|
||||
- `Payload` inner fields are now named. [#2545]
|
||||
- `impl Stream` for `Payload` no longer requires the `Stream` variant be `Unpin`. [#2545]
|
||||
- `impl Future` for `h1::SendResponse` no longer requires the body type be `Unpin`. [#2545]
|
||||
- `impl Stream` for `encoding::Decoder` no longer requires the stream type be `Unpin`. [#2545]
|
||||
- Rename `PayloadStream` to `BoxedPayloadStream`. [#2545]
|
||||
|
||||
### Removed
|
||||
- `h1::Payload::readany`. [#2545]
|
||||
|
||||
[#2527]: https://github.com/actix/actix-web/pull/2527
|
||||
[#2545]: https://github.com/actix/actix-web/pull/2545
|
||||
|
||||
|
||||
## 3.0.0-beta.16 - 2021-12-17
|
||||
### Added
|
||||
- New method on `MessageBody` trait, `try_into_bytes`, with default implementation, for optimizations on body types that complete in exactly one poll. Replaces `is_complete_body` and `take_complete_body`. [#2522]
|
||||
|
||||
### Changed
|
||||
- Rename trait `IntoHeaderPair => TryIntoHeaderPair`. [#2510]
|
||||
- Rename `TryIntoHeaderPair::{try_into_header_pair => try_into_pair}`. [#2510]
|
||||
- Rename trait `IntoHeaderValue => TryIntoHeaderValue`. [#2510]
|
||||
|
||||
### Removed
|
||||
- `MessageBody::{is_complete_body,take_complete_body}`. [#2522]
|
||||
|
||||
[#2510]: https://github.com/actix/actix-web/pull/2510
|
||||
[#2522]: https://github.com/actix/actix-web/pull/2522
|
||||
|
||||
|
||||
## 3.0.0-beta.15 - 2021-12-11
|
||||
### Added
|
||||
* Add timeout for canceling HTTP/2 server side connection handshake. Default to 5 seconds. [#2483]
|
||||
* HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483]
|
||||
* `Response::map_into_boxed_body`. [#2468]
|
||||
* `body::EitherBody` enum. [#2468]
|
||||
* `body::None` struct. [#2468]
|
||||
* Impl `MessageBody` for `bytestring::ByteString`. [#2468]
|
||||
* `impl Clone for ws::HandshakeError`. [#2468]
|
||||
* `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920]
|
||||
* `impl Default ` for `ws::Codec`. [#1920]
|
||||
* `header::QualityItem::{max, min}`. [#2486]
|
||||
* `header::Quality::{MAX, MIN}`. [#2486]
|
||||
* `impl Display` for `header::Quality`. [#2486]
|
||||
* Connection data set through the `on_connect_ext` callbacks is now accessible only from the new `Request::conn_data()` method. [#2491]
|
||||
* `Request::take_conn_data()`. [#2491]
|
||||
* `Request::take_req_data()`. [#2487]
|
||||
* `impl Clone` for `RequestHead`. [#2487]
|
||||
* New methods on `MessageBody` trait, `is_complete_body` and `take_complete_body`, both with default implementations, for optimisations on body types that are done in exactly one poll/chunk. [#2497]
|
||||
- Add timeout for canceling HTTP/2 server side connection handshake. Default to 5 seconds. [#2483]
|
||||
- HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483]
|
||||
- `Response::map_into_boxed_body`. [#2468]
|
||||
- `body::EitherBody` enum. [#2468]
|
||||
- `body::None` struct. [#2468]
|
||||
- Impl `MessageBody` for `bytestring::ByteString`. [#2468]
|
||||
- `impl Clone for ws::HandshakeError`. [#2468]
|
||||
- `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920]
|
||||
- `impl Default ` for `ws::Codec`. [#1920]
|
||||
- `header::QualityItem::{max, min}`. [#2486]
|
||||
- `header::Quality::{MAX, MIN}`. [#2486]
|
||||
- `impl Display` for `header::Quality`. [#2486]
|
||||
- Connection data set through the `on_connect_ext` callbacks is now accessible only from the new `Request::conn_data()` method. [#2491]
|
||||
- `Request::take_conn_data()`. [#2491]
|
||||
- `Request::take_req_data()`. [#2487]
|
||||
- `impl Clone` for `RequestHead`. [#2487]
|
||||
- New methods on `MessageBody` trait, `is_complete_body` and `take_complete_body`, both with default implementations, for optimizations on body types that are done in exactly one poll/chunk. [#2497]
|
||||
- New `boxed` method on `MessageBody` trait for wrapping body type. [#2520]
|
||||
|
||||
### Changed
|
||||
* Rename `body::BoxBody::{from_body => new}`. [#2468]
|
||||
* Body type for `Responses` returned from `Response::{new, ok, etc...}` is now `BoxBody`. [#2468]
|
||||
* The `Error` associated type on `MessageBody` type now requires `impl Error` (or similar). [#2468]
|
||||
* Error types using in service builders now require `Into<Response<BoxBody>>`. [#2468]
|
||||
* `From` implementations on error types now return a `Response<BoxBody>`. [#2468]
|
||||
* `ResponseBuilder::body(B)` now returns `Response<EitherBody<B>>`. [#2468]
|
||||
* `ResponseBuilder::finish()` now returns `Response<EitherBody<()>>`. [#2468]
|
||||
- Rename `body::BoxBody::{from_body => new}`. [#2468]
|
||||
- Body type for `Responses` returned from `Response::{new, ok, etc...}` is now `BoxBody`. [#2468]
|
||||
- The `Error` associated type on `MessageBody` type now requires `impl Error` (or similar). [#2468]
|
||||
- Error types using in service builders now require `Into<Response<BoxBody>>`. [#2468]
|
||||
- `From` implementations on error types now return a `Response<BoxBody>`. [#2468]
|
||||
- `ResponseBuilder::body(B)` now returns `Response<EitherBody<B>>`. [#2468]
|
||||
- `ResponseBuilder::finish()` now returns `Response<EitherBody<()>>`. [#2468]
|
||||
|
||||
### Removed
|
||||
* `ResponseBuilder::streaming`. [#2468]
|
||||
* `impl Future` for `ResponseBuilder`. [#2468]
|
||||
* Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468]
|
||||
* Move `body::AnyBody` to `awc`. Replaced with `EitherBody` and `BoxBody`. [#2468]
|
||||
* `impl Copy` for `ws::Codec`. [#1920]
|
||||
* `header::qitem` helper. Replaced with `header::QualityItem::max`. [#2486]
|
||||
* `impl TryFrom<u16>` for `header::Quality`. [#2486]
|
||||
* `http` module. Most everything it contained is exported at the crate root. [#2488]
|
||||
- `ResponseBuilder::streaming`. [#2468]
|
||||
- `impl Future` for `ResponseBuilder`. [#2468]
|
||||
- Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468]
|
||||
- Move `body::AnyBody` to `awc`. Replaced with `EitherBody` and `BoxBody`. [#2468]
|
||||
- `impl Copy` for `ws::Codec`. [#1920]
|
||||
- `header::qitem` helper. Replaced with `header::QualityItem::max`. [#2486]
|
||||
- `impl TryFrom<u16>` for `header::Quality`. [#2486]
|
||||
- `http` module. Most everything it contained is exported at the crate root. [#2488]
|
||||
|
||||
[#2483]: https://github.com/actix/actix-web/pull/2483
|
||||
[#2468]: https://github.com/actix/actix-web/pull/2468
|
||||
@ -50,14 +124,15 @@
|
||||
[#2488]: https://github.com/actix/actix-web/pull/2488
|
||||
[#2491]: https://github.com/actix/actix-web/pull/2491
|
||||
[#2497]: https://github.com/actix/actix-web/pull/2497
|
||||
[#2520]: https://github.com/actix/actix-web/pull/2520
|
||||
|
||||
|
||||
## 3.0.0-beta.14 - 2021-11-30
|
||||
### Changed
|
||||
* Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467]
|
||||
* Expose `header::map` module. [#2467]
|
||||
* Implement `ExactSizeIterator` and `FusedIterator` for all `HeaderMap` iterators. [#2470]
|
||||
* Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
||||
- Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467]
|
||||
- Expose `header::map` module. [#2467]
|
||||
- Implement `ExactSizeIterator` and `FusedIterator` for all `HeaderMap` iterators. [#2470]
|
||||
- Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
||||
|
||||
[#2467]: https://github.com/actix/actix-web/pull/2467
|
||||
[#2470]: https://github.com/actix/actix-web/pull/2470
|
||||
@ -66,24 +141,24 @@
|
||||
|
||||
## 3.0.0-beta.13 - 2021-11-22
|
||||
### Added
|
||||
* `body::AnyBody::empty` for quickly creating an empty body. [#2446]
|
||||
* `body::AnyBody::none` for quickly creating a "none" body. [#2456]
|
||||
* `impl Clone` for `body::AnyBody<S> where S: Clone`. [#2448]
|
||||
* `body::AnyBody::into_boxed` for quickly converting to a type-erased, boxed body type. [#2448]
|
||||
- `body::AnyBody::empty` for quickly creating an empty body. [#2446]
|
||||
- `body::AnyBody::none` for quickly creating a "none" body. [#2456]
|
||||
- `impl Clone` for `body::AnyBody<S> where S: Clone`. [#2448]
|
||||
- `body::AnyBody::into_boxed` for quickly converting to a type-erased, boxed body type. [#2448]
|
||||
|
||||
### Changed
|
||||
* Rename `body::AnyBody::{Message => Body}`. [#2446]
|
||||
* Rename `body::AnyBody::{from_message => new_boxed}`. [#2448]
|
||||
* Rename `body::AnyBody::{from_slice => copy_from_slice}`. [#2448]
|
||||
* Rename `body::{BoxAnyBody => BoxBody}`. [#2448]
|
||||
* Change representation of `AnyBody` to include a type parameter in `Body` variant. Defaults to `BoxBody`. [#2448]
|
||||
* `Encoder::response` now returns `AnyBody<Encoder<B>>`. [#2448]
|
||||
- Rename `body::AnyBody::{Message => Body}`. [#2446]
|
||||
- Rename `body::AnyBody::{from_message => new_boxed}`. [#2448]
|
||||
- Rename `body::AnyBody::{from_slice => copy_from_slice}`. [#2448]
|
||||
- Rename `body::{BoxAnyBody => BoxBody}`. [#2448]
|
||||
- Change representation of `AnyBody` to include a type parameter in `Body` variant. Defaults to `BoxBody`. [#2448]
|
||||
- `Encoder::response` now returns `AnyBody<Encoder<B>>`. [#2448]
|
||||
|
||||
### Removed
|
||||
* `body::AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446]
|
||||
* `body::BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446]
|
||||
* `EncoderError::Boxed`; it is no longer required. [#2446]
|
||||
* `body::ResponseBody`; is function is replaced by the new `body::AnyBody` enum. [#2446]
|
||||
- `body::AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446]
|
||||
- `body::BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446]
|
||||
- `EncoderError::Boxed`; it is no longer required. [#2446]
|
||||
- `body::ResponseBody`; is function is replaced by the new `body::AnyBody` enum. [#2446]
|
||||
|
||||
[#2446]: https://github.com/actix/actix-web/pull/2446
|
||||
[#2448]: https://github.com/actix/actix-web/pull/2448
|
||||
@ -92,11 +167,11 @@
|
||||
|
||||
## 3.0.0-beta.12 - 2021-11-15
|
||||
### Changed
|
||||
* Update `actix-server` to `2.0.0-beta.9`. [#2442]
|
||||
- Update `actix-server` to `2.0.0-beta.9`. [#2442]
|
||||
|
||||
### Removed
|
||||
* `client` module. [#2425]
|
||||
* `trust-dns` feature. [#2425]
|
||||
- `client` module. [#2425]
|
||||
- `trust-dns` feature. [#2425]
|
||||
|
||||
[#2425]: https://github.com/actix/actix-web/pull/2425
|
||||
[#2442]: https://github.com/actix/actix-web/pull/2442
|
||||
@ -104,21 +179,21 @@
|
||||
|
||||
## 3.0.0-beta.11 - 2021-10-20
|
||||
### Changed
|
||||
* Updated rustls to v0.20. [#2414]
|
||||
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||
- Updated rustls to v0.20. [#2414]
|
||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
||||
[#2414]: https://github.com/actix/actix-web/pull/2414
|
||||
|
||||
|
||||
## 3.0.0-beta.10 - 2021-09-09
|
||||
### Changed
|
||||
* `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377]
|
||||
* Minimum supported Rust version (MSRV) is now 1.51.
|
||||
- `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377]
|
||||
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||
|
||||
### Fixed
|
||||
* Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364]
|
||||
* Remove `Into<Error>` bound on `Encoder` body types. [#2375]
|
||||
* Fix quality parse error in Accept-Encoding header. [#2344]
|
||||
- Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364]
|
||||
- Remove `Into<Error>` bound on `Encoder` body types. [#2375]
|
||||
- Fix quality parse error in Accept-Encoding header. [#2344]
|
||||
|
||||
[#2364]: https://github.com/actix/actix-web/pull/2364
|
||||
[#2375]: https://github.com/actix/actix-web/pull/2375
|
||||
@ -128,15 +203,15 @@
|
||||
|
||||
## 3.0.0-beta.9 - 2021-08-09
|
||||
### Fixed
|
||||
* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
|
||||
- Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
|
||||
|
||||
|
||||
## 3.0.0-beta.8 - 2021-06-26
|
||||
### Changed
|
||||
* Change compression algorithm features flags. [#2250]
|
||||
- Change compression algorithm features flags. [#2250]
|
||||
|
||||
### Removed
|
||||
* `downcast` and `downcast_get_type_id` macros. [#2291]
|
||||
- `downcast` and `downcast_get_type_id` macros. [#2291]
|
||||
|
||||
[#2291]: https://github.com/actix/actix-web/pull/2291
|
||||
[#2250]: https://github.com/actix/actix-web/pull/2250
|
||||
@ -144,37 +219,37 @@
|
||||
|
||||
## 3.0.0-beta.7 - 2021-06-17
|
||||
### Added
|
||||
* Alias `body::Body` as `body::AnyBody`. [#2215]
|
||||
* `BoxAnyBody`: a boxed message body with boxed errors. [#2183]
|
||||
* Re-export `http` crate's `Error` type as `error::HttpError`. [#2171]
|
||||
* Re-export `StatusCode`, `Method`, `Version` and `Uri` at the crate root. [#2171]
|
||||
* Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171]
|
||||
* `Response::into_body` that consumes response and returns body type. [#2201]
|
||||
* `impl Default` for `Response`. [#2201]
|
||||
* Add zstd support for `ContentEncoding`. [#2244]
|
||||
- Alias `body::Body` as `body::AnyBody`. [#2215]
|
||||
- `BoxAnyBody`: a boxed message body with boxed errors. [#2183]
|
||||
- Re-export `http` crate's `Error` type as `error::HttpError`. [#2171]
|
||||
- Re-export `StatusCode`, `Method`, `Version` and `Uri` at the crate root. [#2171]
|
||||
- Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171]
|
||||
- `Response::into_body` that consumes response and returns body type. [#2201]
|
||||
- `impl Default` for `Response`. [#2201]
|
||||
- Add zstd support for `ContentEncoding`. [#2244]
|
||||
|
||||
### Changed
|
||||
* The `MessageBody` trait now has an associated `Error` type. [#2183]
|
||||
* All error trait bounds in server service builders have changed from `Into<Error>` to `Into<Response<AnyBody>>`. [#2253]
|
||||
* All error trait bounds in message body and stream impls changed from `Into<Error>` to `Into<Box<dyn std::error::Error>>`. [#2253]
|
||||
* Places in `Response` where `ResponseBody<B>` was received or returned now simply use `B`. [#2201]
|
||||
* `header` mod is now public. [#2171]
|
||||
* `uri` mod is now public. [#2171]
|
||||
* Update `language-tags` to `0.3`.
|
||||
* Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201]
|
||||
* `ResponseBuilder::message_body` now returns a `Result`. [#2201]
|
||||
* Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253]
|
||||
* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
|
||||
- The `MessageBody` trait now has an associated `Error` type. [#2183]
|
||||
- All error trait bounds in server service builders have changed from `Into<Error>` to `Into<Response<AnyBody>>`. [#2253]
|
||||
- All error trait bounds in message body and stream impls changed from `Into<Error>` to `Into<Box<dyn std::error::Error>>`. [#2253]
|
||||
- Places in `Response` where `ResponseBody<B>` was received or returned now simply use `B`. [#2201]
|
||||
- `header` mod is now public. [#2171]
|
||||
- `uri` mod is now public. [#2171]
|
||||
- Update `language-tags` to `0.3`.
|
||||
- Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201]
|
||||
- `ResponseBuilder::message_body` now returns a `Result`. [#2201]
|
||||
- Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253]
|
||||
- `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
|
||||
|
||||
### Removed
|
||||
* Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171]
|
||||
* Down-casting for `MessageBody` types. [#2183]
|
||||
* `error::Result` alias. [#2201]
|
||||
* Error field from `Response` and `Response::error`. [#2205]
|
||||
* `impl Future` for `Response`. [#2201]
|
||||
* `Response::take_body` and old `Response::into_body` method that casted body type. [#2201]
|
||||
* `InternalError` and all the error types it constructed. [#2215]
|
||||
* Conversion (`impl Into`) of `Response<Body>` and `ResponseBuilder` to `Error`. [#2215]
|
||||
- Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171]
|
||||
- Down-casting for `MessageBody` types. [#2183]
|
||||
- `error::Result` alias. [#2201]
|
||||
- Error field from `Response` and `Response::error`. [#2205]
|
||||
- `impl Future` for `Response`. [#2201]
|
||||
- `Response::take_body` and old `Response::into_body` method that casted body type. [#2201]
|
||||
- `InternalError` and all the error types it constructed. [#2215]
|
||||
- Conversion (`impl Into`) of `Response<Body>` and `ResponseBuilder` to `Error`. [#2215]
|
||||
|
||||
[#2171]: https://github.com/actix/actix-web/pull/2171
|
||||
[#2183]: https://github.com/actix/actix-web/pull/2183
|
||||
@ -189,27 +264,27 @@
|
||||
|
||||
## 3.0.0-beta.6 - 2021-04-17
|
||||
### Added
|
||||
* `impl<T: MessageBody> MessageBody for Pin<Box<T>>`. [#2152]
|
||||
* `Response::{ok, bad_request, not_found, internal_server_error}`. [#2159]
|
||||
* Helper `body::to_bytes` for async collecting message body into Bytes. [#2158]
|
||||
- `impl<T: MessageBody> MessageBody for Pin<Box<T>>`. [#2152]
|
||||
- `Response::{ok, bad_request, not_found, internal_server_error}`. [#2159]
|
||||
- Helper `body::to_bytes` for async collecting message body into Bytes. [#2158]
|
||||
|
||||
### Changes
|
||||
* 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]
|
||||
* Error enum types are marked `#[non_exhaustive]`. [#2161]
|
||||
- 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]
|
||||
- Error enum types are marked `#[non_exhaustive]`. [#2161]
|
||||
|
||||
### Removed
|
||||
* `cookies` feature flag. [#2065]
|
||||
* Top-level `cookies` mod (re-export). [#2065]
|
||||
* `HttpMessage` trait loses the `cookies` and `cookie` methods. [#2065]
|
||||
* `impl ResponseError for CookieParseError`. [#2065]
|
||||
* Deprecated methods on `ResponseBuilder`: `if_true`, `if_some`. [#2148]
|
||||
* `ResponseBuilder::json`. [#2148]
|
||||
* `ResponseBuilder::{set_header, header}`. [#2148]
|
||||
* `impl From<serde_json::Value> for Body`. [#2148]
|
||||
* `Response::build_from`. [#2159]
|
||||
* Most of the status code builders on `Response`. [#2159]
|
||||
- `cookies` feature flag. [#2065]
|
||||
- Top-level `cookies` mod (re-export). [#2065]
|
||||
- `HttpMessage` trait loses the `cookies` and `cookie` methods. [#2065]
|
||||
- `impl ResponseError for CookieParseError`. [#2065]
|
||||
- Deprecated methods on `ResponseBuilder`: `if_true`, `if_some`. [#2148]
|
||||
- `ResponseBuilder::json`. [#2148]
|
||||
- `ResponseBuilder::{set_header, header}`. [#2148]
|
||||
- `impl From<serde_json::Value> for Body`. [#2148]
|
||||
- `Response::build_from`. [#2159]
|
||||
- Most of the status code builders on `Response`. [#2159]
|
||||
|
||||
[#2065]: https://github.com/actix/actix-web/pull/2065
|
||||
[#2148]: https://github.com/actix/actix-web/pull/2148
|
||||
@ -221,16 +296,16 @@
|
||||
|
||||
## 3.0.0-beta.5 - 2021-04-02
|
||||
### Added
|
||||
* `client::Connector::handshake_timeout` method for customizing TLS connection handshake timeout. [#2081]
|
||||
* `client::ConnectorService` as `client::Connector::finish` method's return type [#2081]
|
||||
* `client::ConnectionIo` trait alias [#2081]
|
||||
- `client::Connector::handshake_timeout` method for customizing TLS connection handshake timeout. [#2081]
|
||||
- `client::ConnectorService` as `client::Connector::finish` method's return type [#2081]
|
||||
- `client::ConnectionIo` trait alias [#2081]
|
||||
|
||||
### Changed
|
||||
* `client::Connector` type now only have one generic type for `actix_service::Service`. [#2063]
|
||||
- `client::Connector` type now only have one generic type for `actix_service::Service`. [#2063]
|
||||
|
||||
### Removed
|
||||
* Common typed HTTP headers were moved to actix-web. [2094]
|
||||
* `ResponseError` impl for `actix_utils::timeout::TimeoutError`. [#2127]
|
||||
- Common typed HTTP headers were moved to actix-web. [2094]
|
||||
- `ResponseError` impl for `actix_utils::timeout::TimeoutError`. [#2127]
|
||||
|
||||
[#2063]: https://github.com/actix/actix-web/pull/2063
|
||||
[#2081]: https://github.com/actix/actix-web/pull/2081
|
||||
@ -240,13 +315,13 @@
|
||||
|
||||
## 3.0.0-beta.4 - 2021-03-08
|
||||
### Changed
|
||||
* Feature `cookies` is now optional and disabled by default. [#1981]
|
||||
* `ws::hash_key` now returns array. [#2035]
|
||||
* `ResponseBuilder::json` now takes `impl Serialize`. [#2052]
|
||||
- Feature `cookies` is now optional and disabled by default. [#1981]
|
||||
- `ws::hash_key` now returns array. [#2035]
|
||||
- `ResponseBuilder::json` now takes `impl Serialize`. [#2052]
|
||||
|
||||
### Removed
|
||||
* Re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994]
|
||||
* `ResponseError` impl for `futures_channel::oneshot::Canceled` is removed. [#1994]
|
||||
- Re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994]
|
||||
- `ResponseError` impl for `futures_channel::oneshot::Canceled` is removed. [#1994]
|
||||
|
||||
[#1981]: https://github.com/actix/actix-web/pull/1981
|
||||
[#1994]: https://github.com/actix/actix-web/pull/1994
|
||||
@ -255,48 +330,48 @@
|
||||
|
||||
|
||||
## 3.0.0-beta.3 - 2021-02-10
|
||||
* No notable changes.
|
||||
- No notable changes.
|
||||
|
||||
|
||||
## 3.0.0-beta.2 - 2021-02-10
|
||||
### Added
|
||||
* `IntoHeaderPair` trait that allows using typed and untyped headers in the same methods. [#1869]
|
||||
* `ResponseBuilder::insert_header` method which allows using typed headers. [#1869]
|
||||
* `ResponseBuilder::append_header` method which allows using typed headers. [#1869]
|
||||
* `TestRequest::insert_header` method which allows using typed headers. [#1869]
|
||||
* `ContentEncoding` implements all necessary header traits. [#1912]
|
||||
* `HeaderMap::len_keys` has the behavior of the old `len` method. [#1964]
|
||||
* `HeaderMap::drain` as an efficient draining iterator. [#1964]
|
||||
* Implement `IntoIterator` for owned `HeaderMap`. [#1964]
|
||||
* `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969]
|
||||
- `TryIntoHeaderPair` trait that allows using typed and untyped headers in the same methods. [#1869]
|
||||
- `ResponseBuilder::insert_header` method which allows using typed headers. [#1869]
|
||||
- `ResponseBuilder::append_header` method which allows using typed headers. [#1869]
|
||||
- `TestRequest::insert_header` method which allows using typed headers. [#1869]
|
||||
- `ContentEncoding` implements all necessary header traits. [#1912]
|
||||
- `HeaderMap::len_keys` has the behavior of the old `len` method. [#1964]
|
||||
- `HeaderMap::drain` as an efficient draining iterator. [#1964]
|
||||
- Implement `IntoIterator` for owned `HeaderMap`. [#1964]
|
||||
- `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969]
|
||||
|
||||
### Changed
|
||||
* `ResponseBuilder::content_type` now takes an `impl IntoHeaderValue` to support using typed
|
||||
- `ResponseBuilder::content_type` now takes an `impl TryIntoHeaderValue` to support using typed
|
||||
`mime` types. [#1894]
|
||||
* Renamed `IntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std
|
||||
- Renamed `TryIntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std
|
||||
`TryInto` trait. [#1894]
|
||||
* `Extensions::insert` returns Option of replaced item. [#1904]
|
||||
* Remove `HttpResponseBuilder::json2()`. [#1903]
|
||||
* Enable `HttpResponseBuilder::json()` to receive data by value and reference. [#1903]
|
||||
* `client::error::ConnectError` Resolver variant contains `Box<dyn std::error::Error>` type. [#1905]
|
||||
* `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905]
|
||||
* Simplify `BlockingError` type to a unit struct. It's now only triggered when blocking thread pool
|
||||
- `Extensions::insert` returns Option of replaced item. [#1904]
|
||||
- Remove `HttpResponseBuilder::json2()`. [#1903]
|
||||
- Enable `HttpResponseBuilder::json()` to receive data by value and reference. [#1903]
|
||||
- `client::error::ConnectError` Resolver variant contains `Box<dyn std::error::Error>` type. [#1905]
|
||||
- `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905]
|
||||
- Simplify `BlockingError` type to a unit struct. It's now only triggered when blocking thread pool
|
||||
is dead. [#1957]
|
||||
* `HeaderMap::len` now returns number of values instead of number of keys. [#1964]
|
||||
* `HeaderMap::insert` now returns iterator of removed values. [#1964]
|
||||
* `HeaderMap::remove` now returns iterator of removed values. [#1964]
|
||||
- `HeaderMap::len` now returns number of values instead of number of keys. [#1964]
|
||||
- `HeaderMap::insert` now returns iterator of removed values. [#1964]
|
||||
- `HeaderMap::remove` now returns iterator of removed values. [#1964]
|
||||
|
||||
### Removed
|
||||
* `ResponseBuilder::set`; use `ResponseBuilder::insert_header`. [#1869]
|
||||
* `ResponseBuilder::set_header`; use `ResponseBuilder::insert_header`. [#1869]
|
||||
* `ResponseBuilder::header`; use `ResponseBuilder::append_header`. [#1869]
|
||||
* `TestRequest::with_hdr`; use `TestRequest::default().insert_header()`. [#1869]
|
||||
* `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869]
|
||||
* `actors` optional feature. [#1969]
|
||||
* `ResponseError` impl for `actix::MailboxError`. [#1969]
|
||||
- `ResponseBuilder::set`; use `ResponseBuilder::insert_header`. [#1869]
|
||||
- `ResponseBuilder::set_header`; use `ResponseBuilder::insert_header`. [#1869]
|
||||
- `ResponseBuilder::header`; use `ResponseBuilder::append_header`. [#1869]
|
||||
- `TestRequest::with_hdr`; use `TestRequest::default().insert_header()`. [#1869]
|
||||
- `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869]
|
||||
- `actors` optional feature. [#1969]
|
||||
- `ResponseError` impl for `actix::MailboxError`. [#1969]
|
||||
|
||||
### Documentation
|
||||
* Vastly improve docs and add examples for `HeaderMap`. [#1964]
|
||||
- Vastly improve docs and add examples for `HeaderMap`. [#1964]
|
||||
|
||||
[#1869]: https://github.com/actix/actix-web/pull/1869
|
||||
[#1894]: https://github.com/actix/actix-web/pull/1894
|
||||
@ -311,24 +386,24 @@
|
||||
|
||||
## 3.0.0-beta.1 - 2021-01-07
|
||||
### Added
|
||||
* Add `Http3` to `Protocol` enum for future compatibility and also mark `#[non_exhaustive]`.
|
||||
- Add `Http3` to `Protocol` enum for future compatibility and also mark `#[non_exhaustive]`.
|
||||
|
||||
### Changed
|
||||
* Update `actix-*` dependencies to tokio `1.0` based versions. [#1813]
|
||||
* Bumped `rand` to `0.8`.
|
||||
* Update `bytes` to `1.0`. [#1813]
|
||||
* Update `h2` to `0.3`. [#1813]
|
||||
* The `ws::Message::Text` enum variant now contains a `bytestring::ByteString`. [#1864]
|
||||
- Update `actix-*` dependencies to tokio `1.0` based versions. [#1813]
|
||||
- Bumped `rand` to `0.8`.
|
||||
- Update `bytes` to `1.0`. [#1813]
|
||||
- Update `h2` to `0.3`. [#1813]
|
||||
- The `ws::Message::Text` enum variant now contains a `bytestring::ByteString`. [#1864]
|
||||
|
||||
### Removed
|
||||
* Deprecated `on_connect` methods have been removed. Prefer the new
|
||||
- Deprecated `on_connect` methods have been removed. Prefer the new
|
||||
`on_connect_ext` technique. [#1857]
|
||||
* Remove `ResponseError` impl for `actix::actors::resolver::ResolverError`
|
||||
- Remove `ResponseError` impl for `actix::actors::resolver::ResolverError`
|
||||
due to deprecate of resolver actor. [#1813]
|
||||
* Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`.
|
||||
- Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`.
|
||||
due to the removal of this type from `tokio-openssl` crate. openssl handshake
|
||||
error would return as `ConnectError::SslError`. [#1813]
|
||||
* Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`.
|
||||
- Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`.
|
||||
Due to this change `actix_threadpool::BlockingError` type is moved into
|
||||
`actix_http::error` module. [#1878]
|
||||
|
||||
@ -340,20 +415,20 @@
|
||||
|
||||
## 2.2.1 - 2021-08-09
|
||||
### Fixed
|
||||
* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
|
||||
- Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
|
||||
|
||||
|
||||
## 2.2.0 - 2020-11-25
|
||||
### Added
|
||||
* HttpResponse builders for 1xx status codes. [#1768]
|
||||
* `Accept::mime_precedence` and `Accept::mime_preference`. [#1793]
|
||||
* `TryFrom<u16>` and `TryFrom<f32>` for `http::header::Quality`. [#1797]
|
||||
- HttpResponse builders for 1xx status codes. [#1768]
|
||||
- `Accept::mime_precedence` and `Accept::mime_preference`. [#1793]
|
||||
- `TryFrom<u16>` and `TryFrom<f32>` for `http::header::Quality`. [#1797]
|
||||
|
||||
### Fixed
|
||||
* Started dropping `transfer-encoding: chunked` and `Content-Length` for 1XX and 204 responses. [#1767]
|
||||
- Started dropping `transfer-encoding: chunked` and `Content-Length` for 1XX and 204 responses. [#1767]
|
||||
|
||||
### Changed
|
||||
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||
- Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||
|
||||
[#1773]: https://github.com/actix/actix-web/pull/1773
|
||||
[#1767]: https://github.com/actix/actix-web/pull/1767
|
||||
@ -364,12 +439,12 @@
|
||||
|
||||
## 2.1.0 - 2020-10-30
|
||||
### Added
|
||||
* Added more flexible `on_connect_ext` methods for on-connect handling. [#1754]
|
||||
- Added more flexible `on_connect_ext` methods for on-connect handling. [#1754]
|
||||
|
||||
### Changed
|
||||
* Upgrade `base64` to `0.13`. [#1744]
|
||||
* Upgrade `pin-project` to `1.0`. [#1733]
|
||||
* Deprecate `ResponseBuilder::{if_some, if_true}`. [#1760]
|
||||
- Upgrade `base64` to `0.13`. [#1744]
|
||||
- Upgrade `pin-project` to `1.0`. [#1733]
|
||||
- Deprecate `ResponseBuilder::{if_some, if_true}`. [#1760]
|
||||
|
||||
[#1760]: https://github.com/actix/actix-web/pull/1760
|
||||
[#1754]: https://github.com/actix/actix-web/pull/1754
|
||||
@ -378,28 +453,28 @@
|
||||
|
||||
|
||||
## 2.0.0 - 2020-09-11
|
||||
* No significant changes from `2.0.0-beta.4`.
|
||||
- No significant changes from `2.0.0-beta.4`.
|
||||
|
||||
|
||||
## 2.0.0-beta.4 - 2020-09-09
|
||||
### Changed
|
||||
* Update actix-codec and actix-utils dependencies.
|
||||
* Update actix-connect and actix-tls dependencies.
|
||||
- Update actix-codec and actix-utils dependencies.
|
||||
- Update actix-connect and actix-tls dependencies.
|
||||
|
||||
|
||||
## 2.0.0-beta.3 - 2020-08-14
|
||||
### Fixed
|
||||
* Memory leak of `client::pool::ConnectorPoolSupport`. [#1626]
|
||||
- Memory leak of `client::pool::ConnectorPoolSupport`. [#1626]
|
||||
|
||||
[#1626]: https://github.com/actix/actix-web/pull/1626
|
||||
|
||||
|
||||
## 2.0.0-beta.2 - 2020-07-21
|
||||
### Fixed
|
||||
* Potential UB in h1 decoder using uninitialized memory. [#1614]
|
||||
- Potential UB in h1 decoder using uninitialized memory. [#1614]
|
||||
|
||||
### Changed
|
||||
* Fix illegal chunked encoding. [#1615]
|
||||
- Fix illegal chunked encoding. [#1615]
|
||||
|
||||
[#1614]: https://github.com/actix/actix-web/pull/1614
|
||||
[#1615]: https://github.com/actix/actix-web/pull/1615
|
||||
@ -407,10 +482,10 @@
|
||||
|
||||
## 2.0.0-beta.1 - 2020-07-11
|
||||
### Changed
|
||||
* Migrate cookie handling to `cookie` crate. [#1558]
|
||||
* Update `sha-1` to 0.9. [#1586]
|
||||
* Fix leak in client pool. [#1580]
|
||||
* MSRV is now 1.41.1.
|
||||
- Migrate cookie handling to `cookie` crate. [#1558]
|
||||
- Update `sha-1` to 0.9. [#1586]
|
||||
- Fix leak in client pool. [#1580]
|
||||
- MSRV is now 1.41.1.
|
||||
|
||||
[#1558]: https://github.com/actix/actix-web/pull/1558
|
||||
[#1586]: https://github.com/actix/actix-web/pull/1586
|
||||
@ -419,15 +494,15 @@
|
||||
|
||||
## 2.0.0-alpha.4 - 2020-05-21
|
||||
### Changed
|
||||
* Bump minimum supported Rust version to 1.40
|
||||
* content_length function is removed, and you can set Content-Length by calling
|
||||
- Bump minimum supported Rust version to 1.40
|
||||
- content_length function is removed, and you can set Content-Length by calling
|
||||
no_chunking function [#1439]
|
||||
* `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
|
||||
- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
|
||||
`u64` instead of a `usize`.
|
||||
* Update `base64` dependency to 0.12
|
||||
- Update `base64` dependency to 0.12
|
||||
|
||||
### Fixed
|
||||
* Support parsing of `SameSite=None` [#1503]
|
||||
- Support parsing of `SameSite=None` [#1503]
|
||||
|
||||
[#1439]: https://github.com/actix/actix-web/pull/1439
|
||||
[#1503]: https://github.com/actix/actix-web/pull/1503
|
||||
@ -435,13 +510,13 @@
|
||||
|
||||
## 2.0.0-alpha.3 - 2020-05-08
|
||||
### Fixed
|
||||
* Correct spelling of ConnectError::Unresolved [#1487]
|
||||
* Fix a mistake in the encoding of websocket continuation messages wherein
|
||||
- Correct spelling of ConnectError::Unresolved [#1487]
|
||||
- Fix a mistake in the encoding of websocket continuation messages wherein
|
||||
Item::FirstText and Item::FirstBinary are each encoded as the other.
|
||||
|
||||
### Changed
|
||||
* Implement `std::error::Error` for our custom errors [#1422]
|
||||
* Remove `failure` support for `ResponseError` since that crate
|
||||
- Implement `std::error::Error` for our custom errors [#1422]
|
||||
- Remove `failure` support for `ResponseError` since that crate
|
||||
will be deprecated in the near future.
|
||||
|
||||
[#1422]: https://github.com/actix/actix-web/pull/1422
|
||||
@ -450,12 +525,12 @@
|
||||
|
||||
## 2.0.0-alpha.2 - 2020-03-07
|
||||
### Changed
|
||||
* Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395]
|
||||
* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB
|
||||
- Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395]
|
||||
- Change default initial window size and connection window size for HTTP2 to 2MB and 1MB
|
||||
respectively to improve download speed for awc when downloading large objects. [#1394]
|
||||
* client::Connector accepts initial_window_size and initial_connection_window_size
|
||||
- client::Connector accepts initial_window_size and initial_connection_window_size
|
||||
HTTP2 configuration. [#1394]
|
||||
* client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394]
|
||||
- client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394]
|
||||
|
||||
[#1394]: https://github.com/actix/actix-web/pull/1394
|
||||
[#1395]: https://github.com/actix/actix-web/pull/1395
|
||||
@ -463,61 +538,61 @@
|
||||
|
||||
## 2.0.0-alpha.1 - 2020-02-27
|
||||
### Changed
|
||||
* Update the `time` dependency to 0.2.7.
|
||||
* Moved actors messages support from actix crate, enabled with feature `actors`.
|
||||
* Breaking change: trait MessageBody requires Unpin and accepting `Pin<&mut Self>` instead of
|
||||
- Update the `time` dependency to 0.2.7.
|
||||
- Moved actors messages support from actix crate, enabled with feature `actors`.
|
||||
- Breaking change: trait MessageBody requires Unpin and accepting `Pin<&mut Self>` instead of
|
||||
`&mut self` in the poll_next().
|
||||
* MessageBody is not implemented for &'static [u8] anymore.
|
||||
- MessageBody is not implemented for &'static [u8] anymore.
|
||||
|
||||
### Fixed
|
||||
* Allow `SameSite=None` cookies to be sent in a response.
|
||||
- Allow `SameSite=None` cookies to be sent in a response.
|
||||
|
||||
|
||||
## 1.0.1 - 2019-12-20
|
||||
### Fixed
|
||||
* Poll upgrade service's readiness from HTTP service handlers
|
||||
* Replace brotli with brotli2 #1224
|
||||
- Poll upgrade service's readiness from HTTP service handlers
|
||||
- Replace brotli with brotli2 #1224
|
||||
|
||||
|
||||
## 1.0.0 - 2019-12-13
|
||||
### Added
|
||||
* Add websockets continuation frame support
|
||||
- Add websockets continuation frame support
|
||||
|
||||
### Changed
|
||||
* Replace `flate2-xxx` features with `compress`
|
||||
- Replace `flate2-xxx` features with `compress`
|
||||
|
||||
|
||||
## 1.0.0-alpha.5 - 2019-12-09
|
||||
### Fixed
|
||||
* Check `Upgrade` service readiness before calling it
|
||||
* Fix buffer remaining capacity calculation
|
||||
- Check `Upgrade` service readiness before calling it
|
||||
- Fix buffer remaining capacity calculation
|
||||
|
||||
### Changed
|
||||
* Websockets: Ping and Pong should have binary data #1049
|
||||
- Websockets: Ping and Pong should have binary data #1049
|
||||
|
||||
|
||||
## 1.0.0-alpha.4 - 2019-12-08
|
||||
### Added
|
||||
* Add impl ResponseBuilder for Error
|
||||
- Add impl ResponseBuilder for Error
|
||||
|
||||
### Changed
|
||||
* Use rust based brotli compression library
|
||||
- Use rust based brotli compression library
|
||||
|
||||
## 1.0.0-alpha.3 - 2019-12-07
|
||||
### Changed
|
||||
* Migrate to tokio 0.2
|
||||
* Migrate to `std::future`
|
||||
- Migrate to tokio 0.2
|
||||
- Migrate to `std::future`
|
||||
|
||||
|
||||
## 0.2.11 - 2019-11-06
|
||||
### Added
|
||||
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()
|
||||
* Add an additional `filename*` param in the `Content-Disposition` header of
|
||||
- Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()
|
||||
- Add an additional `filename*` param in the `Content-Disposition` header of
|
||||
`actix_files::NamedFile` to be more compatible. (#1151)
|
||||
* Allow to use `std::convert::Infallible` as `actix_http::error::Error`
|
||||
- Allow to use `std::convert::Infallible` as `actix_http::error::Error`
|
||||
|
||||
### Fixed
|
||||
* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain;
|
||||
- To be compatible with non-English error responses, `ResponseError` rendered with `text/plain;
|
||||
charset=utf-8` header [#1118]
|
||||
|
||||
[#1878]: https://github.com/actix/actix-web/pull/1878
|
||||
@ -525,169 +600,169 @@
|
||||
|
||||
## 0.2.10 - 2019-09-11
|
||||
### Added
|
||||
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests
|
||||
- Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests
|
||||
with `RequestHead`
|
||||
|
||||
### Fixed
|
||||
* h2 will use error response #1080
|
||||
* on_connect result isn't added to request extensions for http2 requests #1009
|
||||
- h2 will use error response #1080
|
||||
- on_connect result isn't added to request extensions for http2 requests #1009
|
||||
|
||||
|
||||
## 0.2.9 - 2019-08-13
|
||||
### Changed
|
||||
* Dropped the `byteorder`-dependency in favor of `stdlib`-implementation
|
||||
* Update percent-encoding to 2.1
|
||||
* Update serde_urlencoded to 0.6.1
|
||||
- Dropped the `byteorder`-dependency in favor of `stdlib`-implementation
|
||||
- Update percent-encoding to 2.1
|
||||
- Update serde_urlencoded to 0.6.1
|
||||
|
||||
### Fixed
|
||||
* Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031)
|
||||
- Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031)
|
||||
|
||||
|
||||
## 0.2.8 - 2019-08-01
|
||||
### Added
|
||||
* Add `rustls` support
|
||||
* Add `Clone` impl for `HeaderMap`
|
||||
- Add `rustls` support
|
||||
- Add `Clone` impl for `HeaderMap`
|
||||
|
||||
### Fixed
|
||||
* awc client panic #1016
|
||||
* Invalid response with compression middleware enabled, but compression-related features
|
||||
- awc client panic #1016
|
||||
- Invalid response with compression middleware enabled, but compression-related features
|
||||
disabled #997
|
||||
|
||||
|
||||
## 0.2.7 - 2019-07-18
|
||||
### Added
|
||||
* Add support for downcasting response errors #986
|
||||
- Add support for downcasting response errors #986
|
||||
|
||||
|
||||
## 0.2.6 - 2019-07-17
|
||||
### Changed
|
||||
* Replace `ClonableService` with local copy
|
||||
* Upgrade `rand` dependency version to 0.7
|
||||
- Replace `ClonableService` with local copy
|
||||
- Upgrade `rand` dependency version to 0.7
|
||||
|
||||
|
||||
## 0.2.5 - 2019-06-28
|
||||
### Added
|
||||
* Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946
|
||||
- Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946
|
||||
|
||||
### Changed
|
||||
* Use `encoding_rs` crate instead of unmaintained `encoding` crate
|
||||
* Add `Copy` and `Clone` impls for `ws::Codec`
|
||||
- Use `encoding_rs` crate instead of unmaintained `encoding` crate
|
||||
- Add `Copy` and `Clone` impls for `ws::Codec`
|
||||
|
||||
|
||||
## 0.2.4 - 2019-06-16
|
||||
### Fixed
|
||||
* Do not compress NoContent (204) responses #918
|
||||
- Do not compress NoContent (204) responses #918
|
||||
|
||||
|
||||
## 0.2.3 - 2019-06-02
|
||||
### Added
|
||||
* Debug impl for ResponseBuilder
|
||||
* From SizedStream and BodyStream for Body
|
||||
- Debug impl for ResponseBuilder
|
||||
- From SizedStream and BodyStream for Body
|
||||
|
||||
### Changed
|
||||
* SizedStream uses u64
|
||||
- SizedStream uses u64
|
||||
|
||||
|
||||
## 0.2.2 - 2019-05-29
|
||||
### Fixed
|
||||
* Parse incoming stream before closing stream on disconnect #868
|
||||
- Parse incoming stream before closing stream on disconnect #868
|
||||
|
||||
|
||||
## 0.2.1 - 2019-05-25
|
||||
### Fixed
|
||||
* Handle socket read disconnect
|
||||
- Handle socket read disconnect
|
||||
|
||||
|
||||
## 0.2.0 - 2019-05-12
|
||||
### Changed
|
||||
* Update actix-service to 0.4
|
||||
* Expect and upgrade services accept `ServerConfig` config.
|
||||
- Update actix-service to 0.4
|
||||
- Expect and upgrade services accept `ServerConfig` config.
|
||||
|
||||
### Deleted
|
||||
* `OneRequest` service
|
||||
- `OneRequest` service
|
||||
|
||||
|
||||
## 0.1.5 - 2019-05-04
|
||||
### Fixed
|
||||
* Clean up response extensions in response pool #817
|
||||
- Clean up response extensions in response pool #817
|
||||
|
||||
|
||||
## 0.1.4 - 2019-04-24
|
||||
### Added
|
||||
* Allow to render h1 request headers in `Camel-Case`
|
||||
- Allow to render h1 request headers in `Camel-Case`
|
||||
|
||||
### Fixed
|
||||
* Read until eof for http/1.0 responses #771
|
||||
- Read until eof for http/1.0 responses #771
|
||||
|
||||
|
||||
## 0.1.3 - 2019-04-23
|
||||
### Fixed
|
||||
* Fix http client pool management
|
||||
* Fix http client wait queue management #794
|
||||
- Fix http client pool management
|
||||
- Fix http client wait queue management #794
|
||||
|
||||
|
||||
## 0.1.2 - 2019-04-23
|
||||
### Fixed
|
||||
* Fix BorrowMutError panic in client connector #793
|
||||
- Fix BorrowMutError panic in client connector #793
|
||||
|
||||
|
||||
## 0.1.1 - 2019-04-19
|
||||
### Changed
|
||||
* Cookie::max_age() accepts value in seconds
|
||||
* Cookie::max_age_time() accepts value in time::Duration
|
||||
* Allow to specify server address for client connector
|
||||
- Cookie::max_age() accepts value in seconds
|
||||
- Cookie::max_age_time() accepts value in time::Duration
|
||||
- Allow to specify server address for client connector
|
||||
|
||||
|
||||
## 0.1.0 - 2019-04-16
|
||||
### Added
|
||||
* Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr`
|
||||
- Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr`
|
||||
|
||||
### Changed
|
||||
* `actix_http::encoding` always available
|
||||
* use trust-dns-resolver 0.11.0
|
||||
- `actix_http::encoding` always available
|
||||
- use trust-dns-resolver 0.11.0
|
||||
|
||||
|
||||
## 0.1.0-alpha.5 - 2019-04-12
|
||||
### Added
|
||||
* Allow to use custom service for upgrade requests
|
||||
* Added `h1::SendResponse` future.
|
||||
- Allow to use custom service for upgrade requests
|
||||
- Added `h1::SendResponse` future.
|
||||
|
||||
### Changed
|
||||
* MessageBody::length() renamed to MessageBody::size() for consistency
|
||||
* ws handshake verification functions take RequestHead instead of Request
|
||||
- MessageBody::length() renamed to MessageBody::size() for consistency
|
||||
- ws handshake verification functions take RequestHead instead of Request
|
||||
|
||||
|
||||
## 0.1.0-alpha.4 - 2019-04-08
|
||||
### Added
|
||||
* Allow to use custom `Expect` handler
|
||||
* Add minimal `std::error::Error` impl for `Error`
|
||||
- Allow to use custom `Expect` handler
|
||||
- Add minimal `std::error::Error` impl for `Error`
|
||||
|
||||
### Changed
|
||||
* Export IntoHeaderValue
|
||||
* Render error and return as response body
|
||||
* Use thread pool for response body compression
|
||||
- Export IntoHeaderValue
|
||||
- Render error and return as response body
|
||||
- Use thread pool for response body compression
|
||||
|
||||
### Deleted
|
||||
* Removed PayloadBuffer
|
||||
- Removed PayloadBuffer
|
||||
|
||||
|
||||
## 0.1.0-alpha.3 - 2019-04-02
|
||||
### Added
|
||||
* Warn when an unsealed private cookie isn't valid UTF-8
|
||||
- Warn when an unsealed private cookie isn't valid UTF-8
|
||||
|
||||
### Fixed
|
||||
* Rust 1.31.0 compatibility
|
||||
* Preallocate read buffer for h1 codec
|
||||
* Detect socket disconnection during protocol selection
|
||||
- Rust 1.31.0 compatibility
|
||||
- Preallocate read buffer for h1 codec
|
||||
- Detect socket disconnection during protocol selection
|
||||
|
||||
|
||||
## 0.1.0-alpha.2 - 2019-03-29
|
||||
### Added
|
||||
* Added ws::Message::Nop, no-op websockets message
|
||||
- Added ws::Message::Nop, no-op websockets message
|
||||
|
||||
### Changed
|
||||
* Do not use thread pool for decompression if chunk size is smaller than 2048.
|
||||
- Do not use thread pool for decompression if chunk size is smaller than 2048.
|
||||
|
||||
|
||||
## 0.1.0-alpha.1 - 2019-03-28
|
||||
* Initial impl
|
||||
- Initial impl
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http"
|
||||
version = "3.0.0-beta.15"
|
||||
version = "3.0.0-beta.18"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "HTTP primitives for the Actix ecosystem"
|
||||
keywords = ["actix", "http", "framework", "async", "futures"]
|
||||
@ -33,7 +33,7 @@ openssl = ["actix-tls/accept", "actix-tls/openssl"]
|
||||
rustls = ["actix-tls/accept", "actix-tls/rustls"]
|
||||
|
||||
# enable compression support
|
||||
compress-brotli = ["brotli2", "__compress"]
|
||||
compress-brotli = ["brotli", "__compress"]
|
||||
compress-gzip = ["flate2", "__compress"]
|
||||
compress-zstd = ["zstd", "__compress"]
|
||||
|
||||
@ -45,7 +45,7 @@ __compress = []
|
||||
actix-service = "2.0.0"
|
||||
actix-codec = "0.4.1"
|
||||
actix-utils = "3.0.0"
|
||||
actix-rt = "2.2"
|
||||
actix-rt = { version = "2.2", default-features = false }
|
||||
|
||||
ahash = "0.7"
|
||||
base64 = "0.13"
|
||||
@ -55,40 +55,40 @@ bytestring = "1"
|
||||
derive_more = "0.99.5"
|
||||
encoding_rs = "0.8"
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
|
||||
h2 = "0.3.9"
|
||||
http = "0.2.5"
|
||||
httparse = "1.5.1"
|
||||
httpdate = "1.0.1"
|
||||
itoa = "0.4"
|
||||
itoa = "1"
|
||||
language-tags = "0.3"
|
||||
local-channel = "0.1"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
percent-encoding = "2.1"
|
||||
pin-project = "1.0.0"
|
||||
pin-project-lite = "0.2"
|
||||
rand = "0.8"
|
||||
sha-1 = "0.9"
|
||||
sha-1 = "0.10"
|
||||
smallvec = "1.6.1"
|
||||
|
||||
# tls
|
||||
actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true }
|
||||
actix-tls = { version = "3.0.0", default-features = false, optional = true }
|
||||
|
||||
# compression
|
||||
brotli2 = { version="0.3.2", optional = true }
|
||||
brotli = { version = "3.3.3", optional = true }
|
||||
flate2 = { version = "1.0.13", optional = true }
|
||||
zstd = { version = "0.9", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-http-test = { version = "3.0.0-beta.9", features = ["openssl"] }
|
||||
actix-server = "2.0.0-rc.1"
|
||||
actix-tls = { version = "3.0.0-rc.1", features = ["openssl"] }
|
||||
actix-web = "4.0.0-beta.14"
|
||||
actix-http-test = { version = "3.0.0-beta.11", features = ["openssl"] }
|
||||
actix-server = "2"
|
||||
actix-tls = { version = "3.0.0", features = ["openssl"] }
|
||||
actix-web = "4.0.0-beta.20"
|
||||
|
||||
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"
|
||||
rcgen = "0.8"
|
||||
regex = "1.3"
|
||||
rustls-pemfile = "0.2"
|
||||
@ -97,7 +97,7 @@ serde_json = "1.0"
|
||||
static_assertions = "1"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||
tokio = { version = "1.2", features = ["net", "rt", "macros"] }
|
||||
tokio = { version = "1.8.4", features = ["net", "rt", "macros"] }
|
||||
|
||||
[[example]]
|
||||
name = "ws"
|
||||
|
@ -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.15)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||
[](https://docs.rs/actix-http/3.0.0-beta.18)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.0.0-beta.15)
|
||||
[](https://deps.rs/crate/actix-http/3.0.0-beta.18)
|
||||
[](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
|
||||
|
||||
@ -54,8 +54,8 @@ async fn main() -> io::Result<()> {
|
||||
|
||||
This project is licensed under either of
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
|
||||
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
|
||||
|
||||
at your option.
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -15,6 +15,7 @@ async fn main() -> io::Result<()> {
|
||||
HttpService::build()
|
||||
.client_timeout(1000)
|
||||
.client_disconnect(1000)
|
||||
// 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
|
||||
|
@ -27,6 +27,7 @@ where
|
||||
S: Stream<Item = Result<Bytes, E>>,
|
||||
E: Into<Box<dyn StdError>> + 'static,
|
||||
{
|
||||
#[inline]
|
||||
pub fn new(stream: S) -> Self {
|
||||
BodyStream { stream }
|
||||
}
|
||||
@ -39,6 +40,7 @@ where
|
||||
{
|
||||
type Error = E;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Stream
|
||||
}
|
||||
|
@ -8,77 +8,98 @@ use std::{
|
||||
use bytes::Bytes;
|
||||
|
||||
use super::{BodySize, MessageBody, MessageBodyMapErr};
|
||||
use crate::Error;
|
||||
use crate::body;
|
||||
|
||||
/// A boxed message body with boxed errors.
|
||||
pub struct BoxBody(Pin<Box<dyn MessageBody<Error = Box<dyn StdError>>>>);
|
||||
#[derive(Debug)]
|
||||
pub struct BoxBody(BoxBodyInner);
|
||||
|
||||
enum BoxBodyInner {
|
||||
None(body::None),
|
||||
Bytes(Bytes),
|
||||
Stream(Pin<Box<dyn MessageBody<Error = Box<dyn StdError>>>>),
|
||||
}
|
||||
|
||||
impl fmt::Debug for BoxBodyInner {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::None(arg0) => f.debug_tuple("None").field(arg0).finish(),
|
||||
Self::Bytes(arg0) => f.debug_tuple("Bytes").field(arg0).finish(),
|
||||
Self::Stream(_) => f.debug_tuple("Stream").field(&"dyn MessageBody").finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BoxBody {
|
||||
/// Boxes a `MessageBody` and any errors it generates.
|
||||
/// Same as `MessageBody::boxed`.
|
||||
///
|
||||
/// If the body type to wrap is unknown or generic it is better to use [`MessageBody::boxed`] to
|
||||
/// avoid double boxing.
|
||||
#[inline]
|
||||
pub fn new<B>(body: B) -> Self
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
let body = MessageBodyMapErr::new(body, Into::into);
|
||||
Self(Box::pin(body))
|
||||
match body.size() {
|
||||
BodySize::None => Self(BoxBodyInner::None(body::None)),
|
||||
_ => match body.try_into_bytes() {
|
||||
Ok(bytes) => Self(BoxBodyInner::Bytes(bytes)),
|
||||
Err(body) => {
|
||||
let body = MessageBodyMapErr::new(body, Into::into);
|
||||
Self(BoxBodyInner::Stream(Box::pin(body)))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a mutable pinned reference to the inner message body type.
|
||||
pub fn as_pin_mut(&mut self) -> Pin<&mut (dyn MessageBody<Error = Box<dyn StdError>>)> {
|
||||
self.0.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for BoxBody {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("BoxBody(dyn MessageBody)")
|
||||
#[inline]
|
||||
pub fn as_pin_mut(&mut self) -> Pin<&mut Self> {
|
||||
Pin::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for BoxBody {
|
||||
type Error = Error;
|
||||
type Error = Box<dyn StdError>;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
self.0.size()
|
||||
match &self.0 {
|
||||
BoxBodyInner::None(none) => none.size(),
|
||||
BoxBodyInner::Bytes(bytes) => bytes.size(),
|
||||
BoxBodyInner::Stream(stream) => stream.size(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
self.0
|
||||
.as_mut()
|
||||
.poll_next(cx)
|
||||
.map_err(|err| Error::new_body().with_cause(err))
|
||||
}
|
||||
|
||||
fn is_complete_body(&self) -> bool {
|
||||
self.0.is_complete_body()
|
||||
}
|
||||
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
debug_assert!(
|
||||
self.is_complete_body(),
|
||||
"boxed type does not allow taking complete body; caller should make sure to \
|
||||
call `is_complete_body` first",
|
||||
);
|
||||
|
||||
// we do not have DerefMut access to call take_complete_body directly but since
|
||||
// is_complete_body is true we should expect the entire bytes chunk in one poll_next
|
||||
|
||||
let waker = futures_util::task::noop_waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
|
||||
match self.as_pin_mut().poll_next(&mut cx) {
|
||||
Poll::Ready(Some(Ok(data))) => data,
|
||||
_ => {
|
||||
panic!(
|
||||
"boxed type indicated it allows taking complete body but failed to \
|
||||
return Bytes when polled",
|
||||
);
|
||||
match &mut self.0 {
|
||||
BoxBodyInner::None(body) => {
|
||||
Pin::new(body).poll_next(cx).map_err(|err| match err {})
|
||||
}
|
||||
BoxBodyInner::Bytes(body) => {
|
||||
Pin::new(body).poll_next(cx).map_err(|err| match err {})
|
||||
}
|
||||
BoxBodyInner::Stream(body) => Pin::new(body).poll_next(cx),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
match self.0 {
|
||||
BoxBodyInner::None(body) => Ok(body.try_into_bytes().unwrap()),
|
||||
BoxBodyInner::Bytes(body) => Ok(body.try_into_bytes().unwrap()),
|
||||
_ => Err(self),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn boxed(self) -> BoxBody {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -23,6 +23,7 @@ pin_project! {
|
||||
|
||||
impl<L> EitherBody<L, BoxBody> {
|
||||
/// Creates new `EitherBody` using left variant and boxed right variant.
|
||||
#[inline]
|
||||
pub fn new(body: L) -> Self {
|
||||
Self::Left { body }
|
||||
}
|
||||
@ -30,11 +31,13 @@ impl<L> EitherBody<L, BoxBody> {
|
||||
|
||||
impl<L, R> EitherBody<L, R> {
|
||||
/// Creates new `EitherBody` using left variant.
|
||||
#[inline]
|
||||
pub fn left(body: L) -> Self {
|
||||
Self::Left { body }
|
||||
}
|
||||
|
||||
/// Creates new `EitherBody` using right variant.
|
||||
#[inline]
|
||||
pub fn right(body: R) -> Self {
|
||||
Self::Right { body }
|
||||
}
|
||||
@ -47,6 +50,7 @@ where
|
||||
{
|
||||
type Error = Error;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
match self {
|
||||
EitherBody::Left { body } => body.size(),
|
||||
@ -54,6 +58,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
@ -68,17 +73,23 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete_body(&self) -> bool {
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
match self {
|
||||
EitherBody::Left { body } => body.is_complete_body(),
|
||||
EitherBody::Right { body } => body.is_complete_body(),
|
||||
EitherBody::Left { body } => body
|
||||
.try_into_bytes()
|
||||
.map_err(|body| EitherBody::Left { body }),
|
||||
EitherBody::Right { body } => body
|
||||
.try_into_bytes()
|
||||
.map_err(|body| EitherBody::Right { body }),
|
||||
}
|
||||
}
|
||||
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
#[inline]
|
||||
fn boxed(self) -> BoxBody {
|
||||
match self {
|
||||
EitherBody::Left { body } => body.take_complete_body(),
|
||||
EitherBody::Right { body } => body.take_complete_body(),
|
||||
EitherBody::Left { body } => body.boxed(),
|
||||
EitherBody::Right { body } => body.boxed(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,16 +12,20 @@ use bytes::{Bytes, BytesMut};
|
||||
use futures_core::ready;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use super::BodySize;
|
||||
use super::{BodySize, BoxBody};
|
||||
|
||||
/// An interface types that can converted to bytes and used as response bodies.
|
||||
// TODO: examples
|
||||
pub trait MessageBody {
|
||||
// TODO: consider this bound to only fmt::Display since the error type is not really used
|
||||
// and there is an impl for Into<Box<StdError>> on String
|
||||
/// The type of error that will be returned if streaming body fails.
|
||||
///
|
||||
/// Since it is not appropriate to generate a response mid-stream, it only requires `Error` for
|
||||
/// internal use and logging.
|
||||
type Error: Into<Box<dyn StdError>>;
|
||||
|
||||
/// Body size hint.
|
||||
///
|
||||
/// If [`BodySize::None`] is returned, optimizations that skip reading the body are allowed.
|
||||
fn size(&self) -> BodySize;
|
||||
|
||||
/// Attempt to pull out the next chunk of body bytes.
|
||||
@ -31,51 +35,32 @@ pub trait MessageBody {
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>>;
|
||||
|
||||
/// Returns true if entire body bytes chunk is obtainable in one call to `poll_next`.
|
||||
/// Try to convert into the complete chunk of body bytes.
|
||||
///
|
||||
/// This method's implementation should agree with [`take_complete_body`] and should always be
|
||||
/// checked before taking the body.
|
||||
/// Implement this method if the entire body can be trivially extracted. This is useful for
|
||||
/// optimizations where `poll_next` calls can be avoided.
|
||||
///
|
||||
/// The default implementation returns `false.
|
||||
/// Body types with [`BodySize::None`] are allowed to return empty `Bytes`. Although, if calling
|
||||
/// this method, it is recommended to check `size` first and return early.
|
||||
///
|
||||
/// [`take_complete_body`]: MessageBody::take_complete_body
|
||||
fn is_complete_body(&self) -> bool {
|
||||
false
|
||||
/// # Errors
|
||||
/// The default implementation will error and return the original type back to the caller for
|
||||
/// further use.
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Err(self)
|
||||
}
|
||||
|
||||
/// Returns the complete chunk of body bytes.
|
||||
///
|
||||
/// Implementors of this method should note the following:
|
||||
/// - It is acceptable to skip the omit checks of [`is_complete_body`]. The responsibility of
|
||||
/// performing this check is delegated to the caller.
|
||||
/// - If the result of [`is_complete_body`] is conditional, that condition should be given
|
||||
/// equivalent attention here.
|
||||
/// - A second call call to [`take_complete_body`] should return an empty `Bytes` or panic.
|
||||
/// - A call to [`poll_next`] after calling [`take_complete_body`] should return `None` unless
|
||||
/// the chunk is guaranteed to be empty.
|
||||
///
|
||||
/// The default implementation panics unconditionally, indicating a control flow bug in the
|
||||
/// calling code.
|
||||
///
|
||||
/// # Panics
|
||||
/// With a correct implementation, panics if called without first checking [`is_complete_body`].
|
||||
///
|
||||
/// [`is_complete_body`]: MessageBody::is_complete_body
|
||||
/// [`take_complete_body`]: MessageBody::take_complete_body
|
||||
/// [`poll_next`]: MessageBody::poll_next
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
assert!(
|
||||
self.is_complete_body(),
|
||||
"type ({}) allows taking complete body but did not provide an implementation \
|
||||
of `take_complete_body`",
|
||||
std::any::type_name::<Self>()
|
||||
);
|
||||
|
||||
unimplemented!(
|
||||
"type ({}) does not allow taking complete body; caller should make sure to \
|
||||
check `is_complete_body` first",
|
||||
std::any::type_name::<Self>()
|
||||
);
|
||||
/// Converts this body into `BoxBody`.
|
||||
#[inline]
|
||||
fn boxed(self) -> BoxBody
|
||||
where
|
||||
Self: Sized + 'static,
|
||||
{
|
||||
BoxBody::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,26 +70,16 @@ mod foreign_impls {
|
||||
impl MessageBody for Infallible {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
match *self {}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
match *self {}
|
||||
}
|
||||
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
match *self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for () {
|
||||
@ -124,19 +99,14 @@ mod foreign_impls {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
Bytes::new()
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> MessageBody for Box<B>
|
||||
where
|
||||
B: MessageBody + Unpin,
|
||||
B: MessageBody + Unpin + ?Sized,
|
||||
{
|
||||
type Error = B::Error;
|
||||
|
||||
@ -152,21 +122,11 @@ mod foreign_impls {
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
Pin::new(self.get_mut().as_mut()).poll_next(cx)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_complete_body(&self) -> bool {
|
||||
self.as_ref().is_complete_body()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
self.as_mut().take_complete_body()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> MessageBody for Pin<Box<B>>
|
||||
where
|
||||
B: MessageBody,
|
||||
B: MessageBody + ?Sized,
|
||||
{
|
||||
type Error = B::Error;
|
||||
|
||||
@ -177,160 +137,126 @@ mod foreign_impls {
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
self.as_mut().poll_next(cx)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_complete_body(&self) -> bool {
|
||||
self.as_ref().is_complete_body()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
debug_assert!(
|
||||
self.is_complete_body(),
|
||||
"inner type \"{}\" does not allow taking complete body; caller should make sure to \
|
||||
call `is_complete_body` first",
|
||||
std::any::type_name::<B>(),
|
||||
);
|
||||
|
||||
// we do not have DerefMut access to call take_complete_body directly but since
|
||||
// is_complete_body is true we should expect the entire bytes chunk in one poll_next
|
||||
|
||||
let waker = futures_util::task::noop_waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
|
||||
match self.as_mut().poll_next(&mut cx) {
|
||||
Poll::Ready(Some(Ok(data))) => data,
|
||||
_ => {
|
||||
panic!(
|
||||
"inner type \"{}\" indicated it allows taking complete body but failed to \
|
||||
return Bytes when polled",
|
||||
std::any::type_name::<B>()
|
||||
);
|
||||
}
|
||||
}
|
||||
self.get_mut().as_mut().poll_next(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for &'static [u8] {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(self.take_complete_body())))
|
||||
Poll::Ready(Some(Ok(Bytes::from_static(mem::take(self.get_mut())))))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
Bytes::from_static(mem::take(self))
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::from_static(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for Bytes {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(self.take_complete_body())))
|
||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
mem::take(self)
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for BytesMut {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(self.take_complete_body())))
|
||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
mem::take(self).freeze()
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(self.freeze())
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for Vec<u8> {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(self.take_complete_body())))
|
||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()).into())))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
Bytes::from(mem::take(self))
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::from(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for &'static str {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
@ -344,22 +270,21 @@ mod foreign_impls {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
Bytes::from_static(mem::take(self).as_bytes())
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::from_static(self.as_bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for String {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
@ -372,22 +297,21 @@ mod foreign_impls {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
Bytes::from(mem::take(self))
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::from(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for bytestring::ByteString {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
@ -396,12 +320,9 @@ mod foreign_impls {
|
||||
Poll::Ready(Some(Ok(string.into_bytes())))
|
||||
}
|
||||
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
mem::take(self).into_bytes()
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(self.into_bytes())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -435,6 +356,7 @@ where
|
||||
{
|
||||
type Error = E;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
self.body.size()
|
||||
}
|
||||
@ -455,6 +377,12 @@ where
|
||||
None => Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
let Self { body, mapper } = self;
|
||||
body.try_into_bytes().map_err(|body| Self { body, mapper })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -464,6 +392,7 @@ mod tests {
|
||||
use bytes::{Bytes, BytesMut};
|
||||
|
||||
use super::*;
|
||||
use crate::body::{self, EitherBody};
|
||||
|
||||
macro_rules! assert_poll_next {
|
||||
($pin:expr, $exp:expr) => {
|
||||
@ -565,49 +494,45 @@ mod tests {
|
||||
assert_poll_next!(pl, Bytes::from("test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_string() {
|
||||
let mut data = "test".repeat(2);
|
||||
let data_bytes = Bytes::from(data.clone());
|
||||
assert!(data.is_complete_body());
|
||||
assert_eq!(data.take_complete_body(), data_bytes);
|
||||
#[actix_rt::test]
|
||||
async fn complete_body_combinators() {
|
||||
let body = Bytes::from_static(b"test");
|
||||
let body = BoxBody::new(body);
|
||||
let body = EitherBody::<_, ()>::left(body);
|
||||
let body = EitherBody::<(), _>::right(body);
|
||||
// Do not support try_into_bytes:
|
||||
// let body = Box::new(body);
|
||||
// let body = Box::pin(body);
|
||||
|
||||
let mut big_data = "test".repeat(64 * 1024);
|
||||
let data_bytes = Bytes::from(big_data.clone());
|
||||
assert!(big_data.is_complete_body());
|
||||
assert_eq!(big_data.take_complete_body(), data_bytes);
|
||||
assert_eq!(body.try_into_bytes().unwrap(), Bytes::from("test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_boxed_equivalence() {
|
||||
let mut data = Bytes::from_static(b"test");
|
||||
assert!(data.is_complete_body());
|
||||
assert_eq!(data.take_complete_body(), b"test".as_ref());
|
||||
#[actix_rt::test]
|
||||
async fn complete_body_combinators_poll() {
|
||||
let body = Bytes::from_static(b"test");
|
||||
let body = BoxBody::new(body);
|
||||
let body = EitherBody::<_, ()>::left(body);
|
||||
let body = EitherBody::<(), _>::right(body);
|
||||
let mut body = body;
|
||||
|
||||
let mut data = Box::new(Bytes::from_static(b"test"));
|
||||
assert!(data.is_complete_body());
|
||||
assert_eq!(data.take_complete_body(), b"test".as_ref());
|
||||
|
||||
let mut data = Box::pin(Bytes::from_static(b"test"));
|
||||
assert!(data.is_complete_body());
|
||||
assert_eq!(data.take_complete_body(), b"test".as_ref());
|
||||
assert_eq!(body.size(), BodySize::Sized(4));
|
||||
assert_poll_next!(Pin::new(&mut body), Bytes::from("test"));
|
||||
assert_poll_next_none!(Pin::new(&mut body));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_policy() {
|
||||
let mut data = Bytes::from_static(b"test");
|
||||
// first call returns chunk
|
||||
assert_eq!(data.take_complete_body(), b"test".as_ref());
|
||||
// second call returns empty
|
||||
assert_eq!(data.take_complete_body(), b"".as_ref());
|
||||
#[actix_rt::test]
|
||||
async fn none_body_combinators() {
|
||||
fn none_body() -> BoxBody {
|
||||
let body = body::None;
|
||||
let body = BoxBody::new(body);
|
||||
let body = EitherBody::<_, ()>::left(body);
|
||||
let body = EitherBody::<(), _>::right(body);
|
||||
body.boxed()
|
||||
}
|
||||
|
||||
let waker = futures_util::task::noop_waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
let mut data = Bytes::from_static(b"test");
|
||||
// take returns whole chunk
|
||||
assert_eq!(data.take_complete_body(), b"test".as_ref());
|
||||
// subsequent poll_next returns None
|
||||
assert_eq!(Pin::new(&mut data).poll_next(&mut cx), Poll::Ready(None));
|
||||
assert_eq!(none_body().size(), BodySize::None);
|
||||
assert_eq!(none_body().try_into_bytes().unwrap(), Bytes::new());
|
||||
assert_poll_next_none!(Pin::new(&mut none_body()));
|
||||
}
|
||||
|
||||
// down-casting used to be done with a method on MessageBody trait
|
||||
|
@ -42,12 +42,7 @@ impl MessageBody for None {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_complete_body(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
Bytes::new()
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::new())
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
/// Body size hint.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BodySize {
|
||||
/// Absence of body can be assumed from method or status code.
|
||||
/// Implicitly empty body.
|
||||
///
|
||||
/// Will skip writing Content-Length header.
|
||||
/// Will omit the Content-Length header. Used for responses to certain methods (e.g., `HEAD`) or
|
||||
/// with particular status codes (e.g., 204 No Content). Consumers that read this as a body size
|
||||
/// hint are allowed to make optimizations that skip reading or writing the payload.
|
||||
None,
|
||||
|
||||
/// Known size body.
|
||||
@ -18,6 +20,9 @@ pub enum BodySize {
|
||||
}
|
||||
|
||||
impl BodySize {
|
||||
/// Equivalent to `BodySize::Sized(0)`;
|
||||
pub const ZERO: Self = Self::Sized(0);
|
||||
|
||||
/// Returns true if size hint indicates omitted or empty body.
|
||||
///
|
||||
/// Streams will return false because it cannot be known without reading the stream.
|
||||
|
@ -27,6 +27,7 @@ where
|
||||
S: Stream<Item = Result<Bytes, E>>,
|
||||
E: Into<Box<dyn StdError>> + 'static,
|
||||
{
|
||||
#[inline]
|
||||
pub fn new(size: u64, stream: S) -> Self {
|
||||
SizedStream { size, stream }
|
||||
}
|
||||
@ -41,6 +42,7 @@ where
|
||||
{
|
||||
type Error = E;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.size as u64)
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ where
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
{
|
||||
/// Create instance of `ServiceConfigBuilder`
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
HttpServiceBuilder {
|
||||
keep_alive: KeepAlive::Timeout(5),
|
||||
|
@ -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};
|
||||
|
||||
@ -28,11 +25,14 @@ use crate::{
|
||||
|
||||
const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049;
|
||||
|
||||
pub struct Decoder<S> {
|
||||
decoder: Option<ContentDecoder>,
|
||||
stream: S,
|
||||
eof: bool,
|
||||
fut: Option<JoinHandle<Result<(Option<Bytes>, ContentDecoder), io::Error>>>,
|
||||
pin_project_lite::pin_project! {
|
||||
pub struct Decoder<S> {
|
||||
decoder: Option<ContentDecoder>,
|
||||
#[pin]
|
||||
stream: S,
|
||||
eof: bool,
|
||||
fut: Option<JoinHandle<Result<(Option<Bytes>, ContentDecoder), io::Error>>>,
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Decoder<S>
|
||||
@ -44,9 +44,9 @@ 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()),
|
||||
@ -89,42 +89,44 @@ where
|
||||
|
||||
impl<S> Stream for Decoder<S>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin,
|
||||
S: Stream<Item = Result<Bytes, PayloadError>>,
|
||||
{
|
||||
type Item = Result<Bytes, PayloadError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let mut this = self.project();
|
||||
|
||||
loop {
|
||||
if let Some(ref mut fut) = self.fut {
|
||||
if let Some(ref mut fut) = this.fut {
|
||||
let (chunk, decoder) =
|
||||
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
|
||||
|
||||
self.decoder = Some(decoder);
|
||||
self.fut.take();
|
||||
*this.decoder = Some(decoder);
|
||||
this.fut.take();
|
||||
|
||||
if let Some(chunk) = chunk {
|
||||
return Poll::Ready(Some(Ok(chunk)));
|
||||
}
|
||||
}
|
||||
|
||||
if self.eof {
|
||||
if *this.eof {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
match ready!(Pin::new(&mut self.stream).poll_next(cx)) {
|
||||
match ready!(this.stream.as_mut().poll_next(cx)) {
|
||||
Some(Err(err)) => return Poll::Ready(Some(Err(err))),
|
||||
|
||||
Some(Ok(chunk)) => {
|
||||
if let Some(mut decoder) = self.decoder.take() {
|
||||
if let Some(mut decoder) = this.decoder.take() {
|
||||
if chunk.len() < MAX_CHUNK_SIZE_DECODE_IN_PLACE {
|
||||
let chunk = decoder.feed_data(chunk)?;
|
||||
self.decoder = Some(decoder);
|
||||
*this.decoder = Some(decoder);
|
||||
|
||||
if let Some(chunk) = chunk {
|
||||
return Poll::Ready(Some(Ok(chunk)));
|
||||
}
|
||||
} else {
|
||||
self.fut = Some(spawn_blocking(move || {
|
||||
*this.fut = Some(spawn_blocking(move || {
|
||||
let chunk = decoder.feed_data(chunk)?;
|
||||
Ok((chunk, decoder))
|
||||
}));
|
||||
@ -137,9 +139,9 @@ where
|
||||
}
|
||||
|
||||
None => {
|
||||
self.eof = true;
|
||||
*this.eof = true;
|
||||
|
||||
return if let Some(mut decoder) = self.decoder.take() {
|
||||
return if let Some(mut decoder) = this.decoder.take() {
|
||||
match decoder.feed_eof() {
|
||||
Ok(Some(res)) => Poll::Ready(Some(Ok(res))),
|
||||
Ok(None) => Poll::Ready(None),
|
||||
@ -160,7 +162,7 @@ enum ContentDecoder {
|
||||
#[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")]
|
||||
@ -171,7 +173,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();
|
||||
|
||||
@ -229,7 +231,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};
|
||||
|
||||
@ -25,7 +22,7 @@ use zstd::stream::write::Encoder as ZstdEncoder;
|
||||
|
||||
use super::Writer;
|
||||
use crate::{
|
||||
body::{BodySize, MessageBody},
|
||||
body::{self, BodySize, MessageBody},
|
||||
error::BlockingError,
|
||||
header::{self, ContentEncoding, HeaderValue, CONTENT_ENCODING},
|
||||
ResponseHead, StatusCode,
|
||||
@ -46,35 +43,34 @@ pin_project! {
|
||||
impl<B: MessageBody> Encoder<B> {
|
||||
fn none() -> Self {
|
||||
Encoder {
|
||||
body: EncoderBody::None,
|
||||
body: EncoderBody::None {
|
||||
body: body::None::new(),
|
||||
},
|
||||
encoder: None,
|
||||
fut: None,
|
||||
eof: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, mut 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);
|
||||
|
||||
pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self {
|
||||
// no need to compress an empty body
|
||||
if matches!(body.size(), BodySize::None) {
|
||||
return Self::none();
|
||||
}
|
||||
|
||||
let body = if body.is_complete_body() {
|
||||
let body = body.take_complete_body();
|
||||
EncoderBody::Full { body }
|
||||
} else {
|
||||
EncoderBody::Stream { body }
|
||||
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 {
|
||||
@ -98,7 +94,7 @@ impl<B: MessageBody> Encoder<B> {
|
||||
pin_project! {
|
||||
#[project = EncoderBodyProj]
|
||||
enum EncoderBody<B> {
|
||||
None,
|
||||
None { body: body::None },
|
||||
Full { body: Bytes },
|
||||
Stream { #[pin] body: B },
|
||||
}
|
||||
@ -110,9 +106,10 @@ where
|
||||
{
|
||||
type Error = EncoderError;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
match self {
|
||||
EncoderBody::None => BodySize::None,
|
||||
EncoderBody::None { body } => body.size(),
|
||||
EncoderBody::Full { body } => body.size(),
|
||||
EncoderBody::Stream { body } => body.size(),
|
||||
}
|
||||
@ -123,7 +120,9 @@ where
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
match self.project() {
|
||||
EncoderBodyProj::None => Poll::Ready(None),
|
||||
EncoderBodyProj::None { body } => {
|
||||
Pin::new(body).poll_next(cx).map_err(|err| match err {})
|
||||
}
|
||||
EncoderBodyProj::Full { body } => {
|
||||
Pin::new(body).poll_next(cx).map_err(|err| match err {})
|
||||
}
|
||||
@ -133,21 +132,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete_body(&self) -> bool {
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match self {
|
||||
EncoderBody::None => true,
|
||||
EncoderBody::Full { .. } => true,
|
||||
EncoderBody::Stream { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
match self {
|
||||
EncoderBody::None => Bytes::new(),
|
||||
EncoderBody::Full { body } => body.take_complete_body(),
|
||||
EncoderBody::Stream { .. } => {
|
||||
panic!("EncoderBody::Stream variant cannot be taken")
|
||||
}
|
||||
EncoderBody::None { body } => Ok(body.try_into_bytes().unwrap()),
|
||||
EncoderBody::Full { body } => Ok(body.try_into_bytes().unwrap()),
|
||||
_ => Err(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -158,6 +151,7 @@ where
|
||||
{
|
||||
type Error = EncoderError;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
if self.encoder.is_some() {
|
||||
BodySize::Stream
|
||||
@ -171,6 +165,7 @@ where
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
let mut this = self.project();
|
||||
|
||||
loop {
|
||||
if *this.eof {
|
||||
return Poll::Ready(None);
|
||||
@ -234,28 +229,30 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete_body(&self) -> bool {
|
||||
#[inline]
|
||||
fn try_into_bytes(mut self) -> Result<Bytes, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if self.encoder.is_some() {
|
||||
false
|
||||
Err(self)
|
||||
} else {
|
||||
self.body.is_complete_body()
|
||||
}
|
||||
}
|
||||
|
||||
fn take_complete_body(&mut self) -> Bytes {
|
||||
if self.encoder.is_some() {
|
||||
panic!("compressed body stream cannot be taken")
|
||||
} else {
|
||||
self.body.take_complete_body()
|
||||
match self.body.try_into_bytes() {
|
||||
Ok(body) => Ok(body),
|
||||
Err(body) => {
|
||||
self.body = body;
|
||||
Err(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
|
||||
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 +265,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 +274,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 +289,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 +305,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 +321,8 @@ impl ContentEncoder {
|
||||
fn finish(self) -> Result<Bytes, io::Error> {
|
||||
match self {
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
ContentEncoder::Br(encoder) => match encoder.finish() {
|
||||
Ok(writer) => Ok(writer.buf.freeze()),
|
||||
ContentEncoder::Brotli(mut encoder) => match encoder.flush() {
|
||||
Ok(()) => Ok(encoder.into_inner().buf.freeze()),
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
|
||||
@ -354,7 +349,7 @@ impl ContentEncoder {
|
||||
fn write(&mut self, data: &[u8]) -> Result<(), io::Error> {
|
||||
match *self {
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) {
|
||||
ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
trace!("Error decoding br encoding: {}", err);
|
||||
@ -392,6 +387,16 @@ impl ContentEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
fn new_brotli_compressor() -> Box<brotli::CompressorWriter<Writer>> {
|
||||
Box::new(brotli::CompressorWriter::new(
|
||||
Writer::new(),
|
||||
8 * 1024, // 32 KiB buffer
|
||||
3, // BROTLI_PARAM_QUALITY
|
||||
22, // BROTLI_PARAM_LGWIN
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
#[non_exhaustive]
|
||||
pub enum EncoderError {
|
||||
|
@ -250,6 +250,7 @@ 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")]
|
||||
// TODO: non-exhaustive
|
||||
pub struct BlockingError;
|
||||
|
||||
/// A set of errors that can occur during payload parsing.
|
||||
@ -332,31 +333,28 @@ impl From<PayloadError> for Error {
|
||||
}
|
||||
|
||||
/// A set of errors that can occur during dispatching HTTP requests.
|
||||
#[derive(Debug, Display, Error, From)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Display, From)]
|
||||
pub enum DispatchError {
|
||||
/// Service error
|
||||
// FIXME: display and error type
|
||||
/// Service error.
|
||||
#[display(fmt = "Service Error")]
|
||||
Service(#[error(not(source))] Response<BoxBody>),
|
||||
Service(Response<BoxBody>),
|
||||
|
||||
/// Body error
|
||||
// FIXME: display and error type
|
||||
#[display(fmt = "Body Error")]
|
||||
Body(#[error(not(source))] Box<dyn StdError>),
|
||||
/// Body streaming error.
|
||||
#[display(fmt = "Body error: {}", _0)]
|
||||
Body(Box<dyn StdError>),
|
||||
|
||||
/// Upgrade service error
|
||||
/// Upgrade service error.
|
||||
Upgrade,
|
||||
|
||||
/// An `io::Error` that occurred while trying to read or write to a network stream.
|
||||
#[display(fmt = "IO error: {}", _0)]
|
||||
Io(io::Error),
|
||||
|
||||
/// Http request parse error.
|
||||
#[display(fmt = "Parse error: {}", _0)]
|
||||
/// Request parse error.
|
||||
#[display(fmt = "Request parse error: {}", _0)]
|
||||
Parse(ParseError),
|
||||
|
||||
/// Http/2 error
|
||||
/// HTTP/2 error.
|
||||
#[display(fmt = "{}", _0)]
|
||||
H2(h2::Error),
|
||||
|
||||
@ -368,25 +366,28 @@ pub enum DispatchError {
|
||||
#[display(fmt = "Connection shutdown timeout")]
|
||||
DisconnectTimeout,
|
||||
|
||||
/// Payload is not consumed
|
||||
#[display(fmt = "Task is completed but request's payload is not consumed")]
|
||||
PayloadIsNotConsumed,
|
||||
|
||||
/// Malformed request
|
||||
#[display(fmt = "Malformed request")]
|
||||
MalformedRequest,
|
||||
|
||||
/// Internal error
|
||||
/// Internal error.
|
||||
#[display(fmt = "Internal error")]
|
||||
InternalError,
|
||||
}
|
||||
|
||||
/// Unknown error
|
||||
#[display(fmt = "Unknown error")]
|
||||
Unknown,
|
||||
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),
|
||||
DispatchError::H2(err) => Some(err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
@ -398,28 +399,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();
|
||||
|
@ -5,13 +5,15 @@ use bitflags::bitflags;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use http::{Method, Version};
|
||||
|
||||
use super::decoder::{PayloadDecoder, PayloadItem, PayloadType};
|
||||
use super::{decoder, encoder, reserve_readbuf};
|
||||
use super::{Message, MessageType};
|
||||
use crate::body::BodySize;
|
||||
use crate::config::ServiceConfig;
|
||||
use crate::error::{ParseError, PayloadError};
|
||||
use crate::message::{ConnectionType, RequestHeadType, ResponseHead};
|
||||
use super::{
|
||||
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
|
||||
encoder, reserve_readbuf, Message, MessageType,
|
||||
};
|
||||
use crate::{
|
||||
body::BodySize,
|
||||
error::{ParseError, PayloadError},
|
||||
ConnectionType, RequestHeadType, ResponseHead, ServiceConfig,
|
||||
};
|
||||
|
||||
bitflags! {
|
||||
struct Flags: u8 {
|
||||
|
@ -5,15 +5,13 @@ use bitflags::bitflags;
|
||||
use bytes::BytesMut;
|
||||
use http::{Method, Version};
|
||||
|
||||
use super::decoder::{PayloadDecoder, PayloadItem, PayloadType};
|
||||
use super::{decoder, encoder};
|
||||
use super::{Message, MessageType};
|
||||
use crate::body::BodySize;
|
||||
use crate::config::ServiceConfig;
|
||||
use crate::error::ParseError;
|
||||
use crate::message::ConnectionType;
|
||||
use crate::request::Request;
|
||||
use crate::response::Response;
|
||||
use super::{
|
||||
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
|
||||
encoder, Message, MessageType,
|
||||
};
|
||||
use crate::{
|
||||
body::BodySize, error::ParseError, ConnectionType, Request, Response, ServiceConfig,
|
||||
};
|
||||
|
||||
bitflags! {
|
||||
struct Flags: u8 {
|
||||
@ -199,7 +197,7 @@ mod tests {
|
||||
use http::Method;
|
||||
|
||||
use super::*;
|
||||
use crate::HttpMessage;
|
||||
use crate::HttpMessage as _;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http_request_chunked_payload_and_next_message() {
|
||||
|
@ -2,17 +2,14 @@ use std::{convert::TryFrom, io, marker::PhantomData, mem::MaybeUninit, task::Pol
|
||||
|
||||
use actix_codec::Decoder;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use http::header::{HeaderName, HeaderValue};
|
||||
use http::{header, Method, StatusCode, Uri, Version};
|
||||
use http::{
|
||||
header::{self, HeaderName, HeaderValue},
|
||||
Method, StatusCode, Uri, Version,
|
||||
};
|
||||
use log::{debug, error, trace};
|
||||
|
||||
use super::chunked::ChunkedState;
|
||||
use crate::{
|
||||
error::ParseError,
|
||||
header::HeaderMap,
|
||||
message::{ConnectionType, ResponseHead},
|
||||
request::Request,
|
||||
};
|
||||
use crate::{error::ParseError, header::HeaderMap, ConnectionType, Request, ResponseHead};
|
||||
|
||||
pub(crate) const MAX_BUFFER_SIZE: usize = 131_072;
|
||||
const MAX_HEADERS: usize = 96;
|
||||
@ -50,7 +47,7 @@ pub(crate) enum PayloadLength {
|
||||
}
|
||||
|
||||
pub(crate) trait MessageType: Sized {
|
||||
fn set_connection_type(&mut self, ctype: Option<ConnectionType>);
|
||||
fn set_connection_type(&mut self, conn_type: Option<ConnectionType>);
|
||||
|
||||
fn set_expect(&mut self);
|
||||
|
||||
@ -193,8 +190,8 @@ pub(crate) trait MessageType: Sized {
|
||||
}
|
||||
|
||||
impl MessageType for Request {
|
||||
fn set_connection_type(&mut self, ctype: Option<ConnectionType>) {
|
||||
if let Some(ctype) = ctype {
|
||||
fn set_connection_type(&mut self, conn_type: Option<ConnectionType>) {
|
||||
if let Some(ctype) = conn_type {
|
||||
self.head_mut().set_connection_type(ctype);
|
||||
}
|
||||
}
|
||||
@ -278,8 +275,8 @@ impl MessageType for Request {
|
||||
}
|
||||
|
||||
impl MessageType for ResponseHead {
|
||||
fn set_connection_type(&mut self, ctype: Option<ConnectionType>) {
|
||||
if let Some(ctype) = ctype {
|
||||
fn set_connection_type(&mut self, conn_type: Option<ConnectionType>) {
|
||||
if let Some(ctype) = conn_type {
|
||||
ResponseHead::set_connection_type(self, ctype);
|
||||
}
|
||||
}
|
||||
@ -383,34 +380,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 }
|
||||
}
|
||||
@ -418,25 +417,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,
|
||||
}
|
||||
|
||||
@ -466,6 +466,7 @@ impl Decoder for PayloadDecoder {
|
||||
Ok(Some(PayloadItem::Chunk(buf)))
|
||||
}
|
||||
}
|
||||
|
||||
Kind::Chunked(ref mut state, ref mut size) => {
|
||||
loop {
|
||||
let mut buf = None;
|
||||
@ -491,6 +492,7 @@ impl Decoder for PayloadDecoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kind::Eof => {
|
||||
if src.is_empty() {
|
||||
Ok(None)
|
||||
|
@ -15,14 +15,14 @@ use bitflags::bitflags;
|
||||
use bytes::{Buf, BytesMut};
|
||||
use futures_core::ready;
|
||||
use log::{error, trace};
|
||||
use pin_project::pin_project;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use crate::{
|
||||
body::{BodySize, BoxBody, MessageBody},
|
||||
config::ServiceConfig,
|
||||
error::{DispatchError, ParseError, PayloadError},
|
||||
service::HttpFlow,
|
||||
Extensions, OnConnectData, Request, Response, StatusCode,
|
||||
Error, Extensions, OnConnectData, Request, Response, StatusCode,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@ -46,79 +46,111 @@ bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project]
|
||||
/// Dispatcher for HTTP/1.1 protocol
|
||||
pub struct Dispatcher<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
// there's 2 versions of Dispatcher state because of:
|
||||
// https://github.com/taiki-e/pin-project-lite/issues/3
|
||||
//
|
||||
// tl;dr: pin-project-lite doesn't play well with other attribute macros
|
||||
|
||||
B: MessageBody,
|
||||
#[cfg(not(test))]
|
||||
pin_project! {
|
||||
/// Dispatcher for HTTP/1.1 protocol
|
||||
pub struct Dispatcher<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
B: MessageBody,
|
||||
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
#[pin]
|
||||
inner: DispatcherState<T, S, B, X, U>,
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
#[cfg(test)]
|
||||
poll_count: u64,
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
#[pin]
|
||||
inner: DispatcherState<T, S, B, X, U>,
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project(project = DispatcherStateProj)]
|
||||
enum DispatcherState<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
#[cfg(test)]
|
||||
pin_project! {
|
||||
/// Dispatcher for HTTP/1.1 protocol
|
||||
pub struct Dispatcher<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
|
||||
B: MessageBody,
|
||||
B: MessageBody,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
Normal(#[pin] InnerDispatcher<T, S, B, X, U>),
|
||||
Upgrade(#[pin] U::Future),
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
#[pin]
|
||||
inner: DispatcherState<T, S, B, X, U>,
|
||||
|
||||
// used in tests
|
||||
poll_count: u64,
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project(project = InnerDispatcherProj)]
|
||||
struct InnerDispatcher<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
pin_project! {
|
||||
#[project = DispatcherStateProj]
|
||||
enum DispatcherState<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
|
||||
B: MessageBody,
|
||||
B: MessageBody,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
flow: Rc<HttpFlow<S, X, U>>,
|
||||
flags: Flags,
|
||||
peer_addr: Option<net::SocketAddr>,
|
||||
conn_data: Option<Rc<Extensions>>,
|
||||
error: Option<DispatchError>,
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
Normal { #[pin] inner: InnerDispatcher<T, S, B, X, U> },
|
||||
Upgrade { #[pin] fut: U::Future },
|
||||
}
|
||||
}
|
||||
|
||||
#[pin]
|
||||
state: State<S, B, X>,
|
||||
payload: Option<PayloadSender>,
|
||||
messages: VecDeque<DispatcherMessage>,
|
||||
pin_project! {
|
||||
#[project = InnerDispatcherProj]
|
||||
struct InnerDispatcher<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
|
||||
ka_expire: Instant,
|
||||
#[pin]
|
||||
ka_timer: Option<Sleep>,
|
||||
B: MessageBody,
|
||||
|
||||
io: Option<T>,
|
||||
read_buf: BytesMut,
|
||||
write_buf: BytesMut,
|
||||
codec: Codec,
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
flow: Rc<HttpFlow<S, X, U>>,
|
||||
flags: Flags,
|
||||
peer_addr: Option<net::SocketAddr>,
|
||||
conn_data: Option<Rc<Extensions>>,
|
||||
error: Option<DispatchError>,
|
||||
|
||||
#[pin]
|
||||
state: State<S, B, X>,
|
||||
payload: Option<PayloadSender>,
|
||||
messages: VecDeque<DispatcherMessage>,
|
||||
|
||||
ka_expire: Instant,
|
||||
#[pin]
|
||||
ka_timer: Option<Sleep>,
|
||||
|
||||
io: Option<T>,
|
||||
read_buf: BytesMut,
|
||||
write_buf: BytesMut,
|
||||
codec: Codec,
|
||||
}
|
||||
}
|
||||
|
||||
enum DispatcherMessage {
|
||||
@ -127,19 +159,21 @@ enum DispatcherMessage {
|
||||
Error(Response<()>),
|
||||
}
|
||||
|
||||
#[pin_project(project = StateProj)]
|
||||
enum State<S, B, X>
|
||||
where
|
||||
S: Service<Request>,
|
||||
X: Service<Request, Response = Request>,
|
||||
pin_project! {
|
||||
#[project = StateProj]
|
||||
enum State<S, B, X>
|
||||
where
|
||||
S: Service<Request>,
|
||||
X: Service<Request, Response = Request>,
|
||||
|
||||
B: MessageBody,
|
||||
{
|
||||
None,
|
||||
ExpectCall(#[pin] X::Future),
|
||||
ServiceCall(#[pin] S::Future),
|
||||
SendPayload(#[pin] B),
|
||||
SendErrorPayload(#[pin] BoxBody),
|
||||
B: MessageBody,
|
||||
{
|
||||
None,
|
||||
ExpectCall { #[pin] fut: X::Future },
|
||||
ServiceCall { #[pin] fut: S::Future },
|
||||
SendPayload { #[pin] body: B },
|
||||
SendErrorPayload { #[pin] body: BoxBody },
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, B, X> State<S, B, X>
|
||||
@ -198,25 +232,27 @@ where
|
||||
};
|
||||
|
||||
Dispatcher {
|
||||
inner: DispatcherState::Normal(InnerDispatcher {
|
||||
flow,
|
||||
flags,
|
||||
peer_addr,
|
||||
conn_data: conn_data.0.map(Rc::new),
|
||||
error: None,
|
||||
inner: DispatcherState::Normal {
|
||||
inner: InnerDispatcher {
|
||||
flow,
|
||||
flags,
|
||||
peer_addr,
|
||||
conn_data: conn_data.0.map(Rc::new),
|
||||
error: None,
|
||||
|
||||
state: State::None,
|
||||
payload: None,
|
||||
messages: VecDeque::new(),
|
||||
state: State::None,
|
||||
payload: None,
|
||||
messages: VecDeque::new(),
|
||||
|
||||
ka_expire,
|
||||
ka_timer,
|
||||
ka_expire,
|
||||
ka_timer,
|
||||
|
||||
io: Some(io),
|
||||
read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
|
||||
write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
|
||||
codec: Codec::new(config),
|
||||
}),
|
||||
io: Some(io),
|
||||
read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
|
||||
write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
|
||||
codec: Codec::new(config),
|
||||
},
|
||||
},
|
||||
|
||||
#[cfg(test)]
|
||||
poll_count: 0,
|
||||
@ -316,7 +352,7 @@ where
|
||||
let size = self.as_mut().send_response_inner(message, &body)?;
|
||||
let state = match size {
|
||||
BodySize::None | BodySize::Sized(0) => State::None,
|
||||
_ => State::SendPayload(body),
|
||||
_ => State::SendPayload { body },
|
||||
};
|
||||
self.project().state.set(state);
|
||||
Ok(())
|
||||
@ -330,7 +366,7 @@ where
|
||||
let size = self.as_mut().send_response_inner(message, &body)?;
|
||||
let state = match size {
|
||||
BodySize::None | BodySize::Sized(0) => State::None,
|
||||
_ => State::SendErrorPayload(body),
|
||||
_ => State::SendErrorPayload { body },
|
||||
};
|
||||
self.project().state.set(state);
|
||||
Ok(())
|
||||
@ -356,12 +392,12 @@ where
|
||||
// Handle `EXPECT: 100-Continue` header
|
||||
if req.head().expect() {
|
||||
// set InnerDispatcher state and continue loop to poll it.
|
||||
let task = this.flow.expect.call(req);
|
||||
this.state.set(State::ExpectCall(task));
|
||||
let fut = this.flow.expect.call(req);
|
||||
this.state.set(State::ExpectCall { fut });
|
||||
} else {
|
||||
// the same as expect call.
|
||||
let task = this.flow.service.call(req);
|
||||
this.state.set(State::ServiceCall(task));
|
||||
let fut = this.flow.service.call(req);
|
||||
this.state.set(State::ServiceCall { fut });
|
||||
};
|
||||
}
|
||||
|
||||
@ -381,7 +417,7 @@ where
|
||||
// all messages are dealt with.
|
||||
None => return Ok(PollResponse::DoNothing),
|
||||
},
|
||||
StateProj::ServiceCall(fut) => match fut.poll(cx) {
|
||||
StateProj::ServiceCall { fut } => match fut.poll(cx) {
|
||||
// service call resolved. send response.
|
||||
Poll::Ready(Ok(res)) => {
|
||||
let (res, body) = res.into().replace_body(());
|
||||
@ -407,11 +443,11 @@ where
|
||||
}
|
||||
},
|
||||
|
||||
StateProj::SendPayload(mut stream) => {
|
||||
StateProj::SendPayload { mut body } => {
|
||||
// keep populate writer buffer until buffer size limit hit,
|
||||
// get blocked or finished.
|
||||
while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE {
|
||||
match stream.as_mut().poll_next(cx) {
|
||||
match body.as_mut().poll_next(cx) {
|
||||
Poll::Ready(Some(Ok(item))) => {
|
||||
this.codec
|
||||
.encode(Message::Chunk(Some(item)), this.write_buf)?;
|
||||
@ -437,13 +473,13 @@ where
|
||||
return Ok(PollResponse::DrainWriteBuf);
|
||||
}
|
||||
|
||||
StateProj::SendErrorPayload(mut stream) => {
|
||||
StateProj::SendErrorPayload { mut body } => {
|
||||
// TODO: de-dupe impl with SendPayload
|
||||
|
||||
// keep populate writer buffer until buffer size limit hit,
|
||||
// get blocked or finished.
|
||||
while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE {
|
||||
match stream.as_mut().poll_next(cx) {
|
||||
match body.as_mut().poll_next(cx) {
|
||||
Poll::Ready(Some(Ok(item))) => {
|
||||
this.codec
|
||||
.encode(Message::Chunk(Some(item)), this.write_buf)?;
|
||||
@ -458,7 +494,9 @@ where
|
||||
}
|
||||
|
||||
Poll::Ready(Some(Err(err))) => {
|
||||
return Err(DispatchError::Service(err.into()))
|
||||
return Err(DispatchError::Body(
|
||||
Error::new_body().with_cause(err).into(),
|
||||
))
|
||||
}
|
||||
|
||||
Poll::Pending => return Ok(PollResponse::DoNothing),
|
||||
@ -469,14 +507,14 @@ where
|
||||
return Ok(PollResponse::DrainWriteBuf);
|
||||
}
|
||||
|
||||
StateProj::ExpectCall(fut) => match fut.poll(cx) {
|
||||
StateProj::ExpectCall { fut } => match fut.poll(cx) {
|
||||
// expect resolved. write continue to buffer and set InnerDispatcher state
|
||||
// to service call.
|
||||
Poll::Ready(Ok(req)) => {
|
||||
this.write_buf
|
||||
.extend_from_slice(b"HTTP/1.1 100 Continue\r\n\r\n");
|
||||
let fut = this.flow.service.call(req);
|
||||
this.state.set(State::ServiceCall(fut));
|
||||
this.state.set(State::ServiceCall { fut });
|
||||
}
|
||||
|
||||
// send expect error as response
|
||||
@ -502,25 +540,25 @@ where
|
||||
let mut this = self.as_mut().project();
|
||||
if req.head().expect() {
|
||||
// set dispatcher state so the future is pinned.
|
||||
let task = this.flow.expect.call(req);
|
||||
this.state.set(State::ExpectCall(task));
|
||||
let fut = this.flow.expect.call(req);
|
||||
this.state.set(State::ExpectCall { fut });
|
||||
} else {
|
||||
// the same as above.
|
||||
let task = this.flow.service.call(req);
|
||||
this.state.set(State::ServiceCall(task));
|
||||
let fut = this.flow.service.call(req);
|
||||
this.state.set(State::ServiceCall { fut });
|
||||
};
|
||||
|
||||
// eagerly poll the future for once(or twice if expect is resolved immediately).
|
||||
loop {
|
||||
match self.as_mut().project().state.project() {
|
||||
StateProj::ExpectCall(fut) => {
|
||||
StateProj::ExpectCall { fut } => {
|
||||
match fut.poll(cx) {
|
||||
// expect is resolved. continue loop and poll the service call branch.
|
||||
Poll::Ready(Ok(req)) => {
|
||||
self.as_mut().send_continue();
|
||||
let mut this = self.as_mut().project();
|
||||
let task = this.flow.service.call(req);
|
||||
this.state.set(State::ServiceCall(task));
|
||||
let fut = this.flow.service.call(req);
|
||||
this.state.set(State::ServiceCall { fut });
|
||||
continue;
|
||||
}
|
||||
// future is pending. return Ok(()) to notify that a new state is
|
||||
@ -536,7 +574,7 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
StateProj::ServiceCall(fut) => {
|
||||
StateProj::ServiceCall { fut } => {
|
||||
// return no matter the service call future's result.
|
||||
return match fut.poll(cx) {
|
||||
// future is resolved. send response and return a result. On success
|
||||
@ -608,10 +646,11 @@ where
|
||||
Payload is attached to Request and passed to Service::call
|
||||
where the state can be collected and consumed.
|
||||
*/
|
||||
let (ps, pl) = Payload::create(false);
|
||||
let (req1, _) = req.replace_payload(crate::Payload::H1(pl));
|
||||
let (sender, payload) = Payload::create(false);
|
||||
let (req1, _) =
|
||||
req.replace_payload(crate::Payload::H1 { payload });
|
||||
req = req1;
|
||||
*this.payload = Some(ps);
|
||||
*this.payload = Some(sender);
|
||||
}
|
||||
|
||||
// Request has no payload.
|
||||
@ -901,7 +940,7 @@ where
|
||||
}
|
||||
|
||||
match this.inner.project() {
|
||||
DispatcherStateProj::Normal(mut inner) => {
|
||||
DispatcherStateProj::Normal { mut inner } => {
|
||||
inner.as_mut().poll_keepalive(cx)?;
|
||||
|
||||
if inner.flags.contains(Flags::SHUTDOWN) {
|
||||
@ -941,7 +980,7 @@ where
|
||||
self.as_mut()
|
||||
.project()
|
||||
.inner
|
||||
.set(DispatcherState::Upgrade(upgrade));
|
||||
.set(DispatcherState::Upgrade { fut: upgrade });
|
||||
return self.poll(cx);
|
||||
}
|
||||
};
|
||||
@ -993,8 +1032,8 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
DispatcherStateProj::Upgrade(fut) => fut.poll(cx).map_err(|e| {
|
||||
error!("Upgrade handler error: {}", e);
|
||||
DispatcherStateProj::Upgrade { fut: upgrade } => upgrade.poll(cx).map_err(|err| {
|
||||
error!("Upgrade handler error: {}", err);
|
||||
DispatchError::Upgrade
|
||||
}),
|
||||
}
|
||||
@ -1088,7 +1127,7 @@ mod tests {
|
||||
Poll::Ready(res) => assert!(res.is_err()),
|
||||
}
|
||||
|
||||
if let DispatcherStateProj::Normal(inner) = h1.project().inner.project() {
|
||||
if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() {
|
||||
assert!(inner.flags.contains(Flags::READ_DISCONNECT));
|
||||
assert_eq!(
|
||||
&inner.project().io.take().unwrap().write_buf[..26],
|
||||
@ -1123,7 +1162,7 @@ mod tests {
|
||||
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||
|
||||
match h1.as_mut().poll(cx) {
|
||||
Poll::Pending => panic!("first poll should not be pending"),
|
||||
@ -1133,7 +1172,7 @@ mod tests {
|
||||
// polls: initial => shutdown
|
||||
assert_eq!(h1.poll_count, 2);
|
||||
|
||||
if let DispatcherStateProj::Normal(inner) = h1.project().inner.project() {
|
||||
if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() {
|
||||
let res = &mut inner.project().io.take().unwrap().write_buf[..];
|
||||
stabilize_date_header(res);
|
||||
|
||||
@ -1177,7 +1216,7 @@ mod tests {
|
||||
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||
|
||||
match h1.as_mut().poll(cx) {
|
||||
Poll::Pending => panic!("first poll should not be pending"),
|
||||
@ -1187,7 +1226,7 @@ mod tests {
|
||||
// polls: initial => shutdown
|
||||
assert_eq!(h1.poll_count, 1);
|
||||
|
||||
if let DispatcherStateProj::Normal(inner) = h1.project().inner.project() {
|
||||
if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() {
|
||||
let res = &mut inner.project().io.take().unwrap().write_buf[..];
|
||||
stabilize_date_header(res);
|
||||
|
||||
@ -1237,13 +1276,13 @@ mod tests {
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
assert!(h1.as_mut().poll(cx).is_pending());
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||
|
||||
// polls: manual
|
||||
assert_eq!(h1.poll_count, 1);
|
||||
eprintln!("poll count: {}", h1.poll_count);
|
||||
|
||||
if let DispatcherState::Normal(ref inner) = h1.inner {
|
||||
if let DispatcherState::Normal { ref inner } = h1.inner {
|
||||
let io = inner.io.as_ref().unwrap();
|
||||
let res = &io.write_buf()[..];
|
||||
assert_eq!(
|
||||
@ -1258,7 +1297,7 @@ mod tests {
|
||||
// polls: manual manual shutdown
|
||||
assert_eq!(h1.poll_count, 3);
|
||||
|
||||
if let DispatcherState::Normal(ref inner) = h1.inner {
|
||||
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);
|
||||
@ -1309,12 +1348,12 @@ mod tests {
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
assert!(h1.as_mut().poll(cx).is_ready());
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
|
||||
assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
|
||||
|
||||
// polls: manual shutdown
|
||||
assert_eq!(h1.poll_count, 2);
|
||||
|
||||
if let DispatcherState::Normal(ref inner) = h1.inner {
|
||||
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);
|
||||
@ -1386,7 +1425,7 @@ mod tests {
|
||||
actix_rt::pin!(h1);
|
||||
|
||||
assert!(h1.as_mut().poll(cx).is_ready());
|
||||
assert!(matches!(&h1.inner, DispatcherState::Upgrade(_)));
|
||||
assert!(matches!(&h1.inner, DispatcherState::Upgrade { .. }));
|
||||
|
||||
// polls: manual shutdown
|
||||
assert_eq!(h1.poll_count, 2);
|
||||
|
@ -1,19 +1,19 @@
|
||||
use std::io::Write;
|
||||
use std::marker::PhantomData;
|
||||
use std::ptr::copy_nonoverlapping;
|
||||
use std::slice::from_raw_parts_mut;
|
||||
use std::{cmp, io};
|
||||
use std::{
|
||||
cmp,
|
||||
io::{self, Write as _},
|
||||
marker::PhantomData,
|
||||
ptr::copy_nonoverlapping,
|
||||
slice::from_raw_parts_mut,
|
||||
};
|
||||
|
||||
use bytes::{BufMut, BytesMut};
|
||||
|
||||
use crate::{
|
||||
body::BodySize,
|
||||
config::ServiceConfig,
|
||||
header::{map::Value, HeaderMap, HeaderName},
|
||||
header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING},
|
||||
helpers,
|
||||
message::{ConnectionType, RequestHeadType},
|
||||
Response, StatusCode, Version,
|
||||
header::{
|
||||
map::Value, HeaderMap, HeaderName, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING,
|
||||
},
|
||||
helpers, ConnectionType, RequestHeadType, Response, ServiceConfig, StatusCode, Version,
|
||||
};
|
||||
|
||||
const AVERAGE_HEADER_SIZE: usize = 30;
|
||||
@ -258,6 +258,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();
|
||||
|
@ -1,8 +1,7 @@
|
||||
use actix_service::{Service, ServiceFactory};
|
||||
use actix_utils::future::{ready, Ready};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::request::Request;
|
||||
use crate::{Error, Request};
|
||||
|
||||
pub struct ExpectHandler;
|
||||
|
||||
|
@ -59,7 +59,7 @@ pub(crate) fn reserve_readbuf(src: &mut BytesMut) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::request::Request;
|
||||
use crate::Request;
|
||||
|
||||
impl Message<Request> {
|
||||
pub fn message(self) -> Request {
|
||||
|
@ -1,9 +1,12 @@
|
||||
//! Payload stream
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::pin::Pin;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::task::{Context, Poll, Waker};
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::VecDeque,
|
||||
pin::Pin,
|
||||
rc::{Rc, Weak},
|
||||
task::{Context, Poll, Waker},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_core::Stream;
|
||||
@ -22,39 +25,32 @@ pub enum PayloadStatus {
|
||||
|
||||
/// Buffered stream of bytes chunks
|
||||
///
|
||||
/// Payload stores chunks in a vector. First chunk can be received with
|
||||
/// `.readany()` method. Payload stream is not thread safe. Payload does not
|
||||
/// notify current task when new data is available.
|
||||
/// Payload stores chunks in a vector. First chunk can be received with `poll_next`. Payload does
|
||||
/// not notify current task when new data is available.
|
||||
///
|
||||
/// Payload stream can be used as `Response` body stream.
|
||||
/// Payload can be used as `Response` body stream.
|
||||
#[derive(Debug)]
|
||||
pub struct Payload {
|
||||
inner: Rc<RefCell<Inner>>,
|
||||
}
|
||||
|
||||
impl Payload {
|
||||
/// Create payload stream.
|
||||
/// Creates a payload stream.
|
||||
///
|
||||
/// This method construct two objects responsible for bytes stream
|
||||
/// generation.
|
||||
///
|
||||
/// * `PayloadSender` - *Sender* side of the stream
|
||||
///
|
||||
/// * `Payload` - *Receiver* side of the stream
|
||||
/// This method construct two objects responsible for bytes stream generation:
|
||||
/// - `PayloadSender` - *Sender* side of the stream
|
||||
/// - `Payload` - *Receiver* side of the stream
|
||||
pub fn create(eof: bool) -> (PayloadSender, Payload) {
|
||||
let shared = Rc::new(RefCell::new(Inner::new(eof)));
|
||||
|
||||
(
|
||||
PayloadSender {
|
||||
inner: Rc::downgrade(&shared),
|
||||
},
|
||||
PayloadSender::new(Rc::downgrade(&shared)),
|
||||
Payload { inner: shared },
|
||||
)
|
||||
}
|
||||
|
||||
/// Create empty payload
|
||||
#[doc(hidden)]
|
||||
pub fn empty() -> Payload {
|
||||
/// Creates an empty payload.
|
||||
pub(crate) fn empty() -> Payload {
|
||||
Payload {
|
||||
inner: Rc::new(RefCell::new(Inner::new(true))),
|
||||
}
|
||||
@ -77,14 +73,6 @@ impl Payload {
|
||||
pub fn unread_data(&mut self, data: Bytes) {
|
||||
self.inner.borrow_mut().unread_data(data);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn readany(
|
||||
&mut self,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, PayloadError>>> {
|
||||
self.inner.borrow_mut().readany(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for Payload {
|
||||
@ -94,7 +82,7 @@ impl Stream for Payload {
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, PayloadError>>> {
|
||||
self.inner.borrow_mut().readany(cx)
|
||||
Pin::new(&mut *self.inner.borrow_mut()).poll_next(cx)
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,6 +92,10 @@ pub struct PayloadSender {
|
||||
}
|
||||
|
||||
impl PayloadSender {
|
||||
fn new(inner: Weak<RefCell<Inner>>) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_error(&mut self, err: PayloadError) {
|
||||
if let Some(shared) = self.inner.upgrade() {
|
||||
@ -227,7 +219,10 @@ impl Inner {
|
||||
self.len
|
||||
}
|
||||
|
||||
fn readany(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<Bytes, PayloadError>>> {
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, PayloadError>>> {
|
||||
if let Some(data) = self.items.pop_front() {
|
||||
self.len -= data.len();
|
||||
self.need_read = self.len < MAX_BUFFER_SIZE;
|
||||
@ -257,8 +252,18 @@ impl Inner {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||
|
||||
use actix_utils::future::poll_fn;
|
||||
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
||||
|
||||
use super::*;
|
||||
|
||||
assert_impl_all!(Payload: Unpin);
|
||||
assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe);
|
||||
|
||||
assert_impl_all!(Inner: Unpin, Send, Sync);
|
||||
assert_not_impl_any!(Inner: UnwindSafe, RefUnwindSafe);
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_unread_data() {
|
||||
@ -270,7 +275,10 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
Bytes::from("data"),
|
||||
poll_fn(|cx| payload.readany(cx)).await.unwrap().unwrap()
|
||||
poll_fn(|cx| Pin::new(&mut payload).poll_next(cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -356,9 +356,9 @@ where
|
||||
type Future = Dispatcher<T, S, B, X, U>;
|
||||
|
||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self._poll_ready(cx).map_err(|e| {
|
||||
log::error!("HTTP/1 service readiness error: {:?}", e);
|
||||
DispatchError::Service(e)
|
||||
self._poll_ready(cx).map_err(|err| {
|
||||
log::error!("HTTP/1 service readiness error: {:?}", err);
|
||||
DispatchError::Service(err)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -2,9 +2,7 @@ use actix_codec::Framed;
|
||||
use actix_service::{Service, ServiceFactory};
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::h1::Codec;
|
||||
use crate::request::Request;
|
||||
use crate::{h1::Codec, Error, Request};
|
||||
|
||||
pub struct UpgradeHandler;
|
||||
|
||||
|
@ -9,9 +9,8 @@ use pin_project_lite::pin_project;
|
||||
|
||||
use crate::{
|
||||
body::{BodySize, MessageBody},
|
||||
error::Error,
|
||||
h1::{Codec, Message},
|
||||
response::Response,
|
||||
Error, Response,
|
||||
};
|
||||
|
||||
pin_project! {
|
||||
@ -46,7 +45,7 @@ where
|
||||
impl<T, B> Future for SendResponse<T, B>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
B: MessageBody + Unpin,
|
||||
B: MessageBody,
|
||||
B::Error: Into<Error>,
|
||||
{
|
||||
type Output = Result<Framed<T, Codec>, Error>;
|
||||
@ -82,7 +81,7 @@ where
|
||||
// body is done when item is None
|
||||
body_done = item.is_none();
|
||||
if body_done {
|
||||
let _ = this.body.take();
|
||||
this.body.set(None);
|
||||
}
|
||||
let framed = this.framed.as_mut().as_pin_mut().unwrap();
|
||||
framed
|
||||
|
@ -108,8 +108,8 @@ where
|
||||
match Pin::new(&mut this.connection).poll_accept(cx)? {
|
||||
Poll::Ready(Some((req, tx))) => {
|
||||
let (parts, body) = req.into_parts();
|
||||
let pl = crate::h2::Payload::new(body);
|
||||
let pl = Payload::H2(pl);
|
||||
let payload = crate::h2::Payload::new(body);
|
||||
let pl = Payload::H2 { payload };
|
||||
let mut req = Request::with_payload(pl);
|
||||
|
||||
let head = req.head_mut();
|
||||
@ -288,9 +288,11 @@ fn prepare_response(
|
||||
let _ = match size {
|
||||
BodySize::None | BodySize::Stream => None,
|
||||
|
||||
BodySize::Sized(0) => res
|
||||
.headers_mut()
|
||||
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
|
||||
BodySize::Sized(0) => {
|
||||
#[allow(clippy::declare_interior_mutable_const)]
|
||||
const HV_ZERO: HeaderValue = HeaderValue::from_static("0");
|
||||
res.headers_mut().insert(CONTENT_LENGTH, HV_ZERO)
|
||||
}
|
||||
|
||||
BodySize::Sized(len) => {
|
||||
let mut buf = itoa::Buffer::new();
|
||||
|
@ -98,3 +98,14 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||
|
||||
use static_assertions::assert_impl_all;
|
||||
|
||||
use super::*;
|
||||
|
||||
assert_impl_all!(Payload: Unpin, Send, Sync, UnwindSafe, RefUnwindSafe);
|
||||
}
|
||||
|
@ -270,10 +270,10 @@ where
|
||||
type Future = H2ServiceHandlerResponse<T, S, B>;
|
||||
|
||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.flow.service.poll_ready(cx).map_err(|e| {
|
||||
let e = e.into();
|
||||
error!("Service readiness error: {:?}", e);
|
||||
DispatchError::Service(e)
|
||||
self.flow.service.poll_ready(cx).map_err(|err| {
|
||||
let err = err.into();
|
||||
error!("Service readiness error: {:?}", err);
|
||||
DispatchError::Service(err)
|
||||
})
|
||||
}
|
||||
|
||||
@ -297,7 +297,6 @@ where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S::Future: 'static,
|
||||
{
|
||||
Incoming(Dispatcher<T, S, B, (), ()>),
|
||||
Handshake(
|
||||
Option<Rc<HttpFlow<S, (), ()>>>,
|
||||
Option<ServiceConfig>,
|
||||
@ -305,6 +304,7 @@ where
|
||||
OnConnectData,
|
||||
HandshakeWithTimeout<T>,
|
||||
),
|
||||
Established(Dispatcher<T, S, B, (), ()>),
|
||||
}
|
||||
|
||||
pub struct H2ServiceHandlerResponse<T, S, B>
|
||||
@ -332,7 +332,6 @@ where
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.state {
|
||||
State::Incoming(ref mut disp) => Pin::new(disp).poll(cx),
|
||||
State::Handshake(
|
||||
ref mut srv,
|
||||
ref mut config,
|
||||
@ -343,7 +342,7 @@ where
|
||||
Ok((conn, timer)) => {
|
||||
let on_connect_data = mem::take(conn_data);
|
||||
|
||||
self.state = State::Incoming(Dispatcher::new(
|
||||
self.state = State::Established(Dispatcher::new(
|
||||
conn,
|
||||
srv.take().unwrap(),
|
||||
config.take().unwrap(),
|
||||
@ -360,6 +359,8 @@ where
|
||||
Poll::Ready(Err(err))
|
||||
}
|
||||
},
|
||||
|
||||
State::Established(ref mut disp) => Pin::new(disp).poll(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ pub trait Sealed {
|
||||
}
|
||||
|
||||
impl Sealed for HeaderName {
|
||||
#[inline]
|
||||
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||
Ok(Cow::Borrowed(self))
|
||||
}
|
||||
@ -23,6 +24,7 @@ impl Sealed for HeaderName {
|
||||
impl AsHeaderName for HeaderName {}
|
||||
|
||||
impl Sealed for &HeaderName {
|
||||
#[inline]
|
||||
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||
Ok(Cow::Borrowed(*self))
|
||||
}
|
||||
@ -30,6 +32,7 @@ impl Sealed for &HeaderName {
|
||||
impl AsHeaderName for &HeaderName {}
|
||||
|
||||
impl Sealed for &str {
|
||||
#[inline]
|
||||
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||
HeaderName::from_str(self).map(Cow::Owned)
|
||||
}
|
||||
@ -37,6 +40,7 @@ impl Sealed for &str {
|
||||
impl AsHeaderName for &str {}
|
||||
|
||||
impl Sealed for String {
|
||||
#[inline]
|
||||
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||
HeaderName::from_str(self).map(Cow::Owned)
|
||||
}
|
||||
@ -44,6 +48,7 @@ impl Sealed for String {
|
||||
impl AsHeaderName for String {}
|
||||
|
||||
impl Sealed for &String {
|
||||
#[inline]
|
||||
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||
HeaderName::from_str(self).map(Cow::Owned)
|
||||
}
|
||||
|
@ -1,22 +1,20 @@
|
||||
//! [`IntoHeaderPair`] trait and implementations.
|
||||
//! [`TryIntoHeaderPair`] trait and implementations.
|
||||
|
||||
use std::convert::TryFrom as _;
|
||||
|
||||
use http::{
|
||||
header::{HeaderName, InvalidHeaderName, InvalidHeaderValue},
|
||||
Error as HttpError, HeaderValue,
|
||||
use super::{
|
||||
Header, HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, TryIntoHeaderValue,
|
||||
};
|
||||
use crate::error::HttpError;
|
||||
|
||||
use super::{Header, IntoHeaderValue};
|
||||
|
||||
/// An interface for types that can be converted into a [`HeaderName`]/[`HeaderValue`] pair for
|
||||
/// An interface for types that can be converted into a [`HeaderName`] + [`HeaderValue`] pair for
|
||||
/// insertion into a [`HeaderMap`].
|
||||
///
|
||||
/// [`HeaderMap`]: super::HeaderMap
|
||||
pub trait IntoHeaderPair: Sized {
|
||||
pub trait TryIntoHeaderPair: Sized {
|
||||
type Error: Into<HttpError>;
|
||||
|
||||
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error>;
|
||||
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -34,14 +32,14 @@ impl From<InvalidHeaderPart> for HttpError {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> IntoHeaderPair for (HeaderName, V)
|
||||
impl<V> TryIntoHeaderPair for (HeaderName, V)
|
||||
where
|
||||
V: IntoHeaderValue,
|
||||
V: TryIntoHeaderValue,
|
||||
V::Error: Into<InvalidHeaderValue>,
|
||||
{
|
||||
type Error = InvalidHeaderPart;
|
||||
|
||||
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||
let (name, value) = self;
|
||||
let value = value
|
||||
.try_into_value()
|
||||
@ -50,14 +48,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> IntoHeaderPair for (&HeaderName, V)
|
||||
impl<V> TryIntoHeaderPair for (&HeaderName, V)
|
||||
where
|
||||
V: IntoHeaderValue,
|
||||
V: TryIntoHeaderValue,
|
||||
V::Error: Into<InvalidHeaderValue>,
|
||||
{
|
||||
type Error = InvalidHeaderPart;
|
||||
|
||||
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||
let (name, value) = self;
|
||||
let value = value
|
||||
.try_into_value()
|
||||
@ -66,14 +64,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> IntoHeaderPair for (&[u8], V)
|
||||
impl<V> TryIntoHeaderPair for (&[u8], V)
|
||||
where
|
||||
V: IntoHeaderValue,
|
||||
V: TryIntoHeaderValue,
|
||||
V::Error: Into<InvalidHeaderValue>,
|
||||
{
|
||||
type Error = InvalidHeaderPart;
|
||||
|
||||
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||
let (name, value) = self;
|
||||
let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?;
|
||||
let value = value
|
||||
@ -83,14 +81,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> IntoHeaderPair for (&str, V)
|
||||
impl<V> TryIntoHeaderPair for (&str, V)
|
||||
where
|
||||
V: IntoHeaderValue,
|
||||
V: TryIntoHeaderValue,
|
||||
V::Error: Into<InvalidHeaderValue>,
|
||||
{
|
||||
type Error = InvalidHeaderPart;
|
||||
|
||||
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||
let (name, value) = self;
|
||||
let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?;
|
||||
let value = value
|
||||
@ -100,23 +98,25 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> IntoHeaderPair for (String, V)
|
||||
impl<V> TryIntoHeaderPair for (String, V)
|
||||
where
|
||||
V: IntoHeaderValue,
|
||||
V: TryIntoHeaderValue,
|
||||
V::Error: Into<InvalidHeaderValue>,
|
||||
{
|
||||
type Error = InvalidHeaderPart;
|
||||
|
||||
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||
#[inline]
|
||||
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||
let (name, value) = self;
|
||||
(name.as_str(), value).try_into_header_pair()
|
||||
(name.as_str(), value).try_into_pair()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Header> IntoHeaderPair for T {
|
||||
type Error = <T as IntoHeaderValue>::Error;
|
||||
impl<T: Header> TryIntoHeaderPair for T {
|
||||
type Error = <T as TryIntoHeaderValue>::Error;
|
||||
|
||||
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||
#[inline]
|
||||
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||
Ok((T::name(), self.try_into_value()?))
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
//! [`IntoHeaderValue`] trait and implementations.
|
||||
//! [`TryIntoHeaderValue`] trait and implementations.
|
||||
|
||||
use std::convert::TryFrom as _;
|
||||
|
||||
@ -7,7 +7,7 @@ use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue};
|
||||
use mime::Mime;
|
||||
|
||||
/// An interface for types that can be converted into a [`HeaderValue`].
|
||||
pub trait IntoHeaderValue: Sized {
|
||||
pub trait TryIntoHeaderValue: Sized {
|
||||
/// The type returned in the event of a conversion error.
|
||||
type Error: Into<HttpError>;
|
||||
|
||||
@ -15,7 +15,7 @@ pub trait IntoHeaderValue: Sized {
|
||||
fn try_into_value(self) -> Result<HeaderValue, Self::Error>;
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for HeaderValue {
|
||||
impl TryIntoHeaderValue for HeaderValue {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
@ -24,7 +24,7 @@ impl IntoHeaderValue for HeaderValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for &HeaderValue {
|
||||
impl TryIntoHeaderValue for &HeaderValue {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
@ -33,7 +33,7 @@ impl IntoHeaderValue for &HeaderValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for &str {
|
||||
impl TryIntoHeaderValue for &str {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
@ -42,7 +42,7 @@ impl IntoHeaderValue for &str {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for &[u8] {
|
||||
impl TryIntoHeaderValue for &[u8] {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
@ -51,7 +51,7 @@ impl IntoHeaderValue for &[u8] {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for Bytes {
|
||||
impl TryIntoHeaderValue for Bytes {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
@ -60,7 +60,7 @@ impl IntoHeaderValue for Bytes {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for Vec<u8> {
|
||||
impl TryIntoHeaderValue for Vec<u8> {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
@ -69,7 +69,7 @@ impl IntoHeaderValue for Vec<u8> {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for String {
|
||||
impl TryIntoHeaderValue for String {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
@ -78,7 +78,7 @@ impl IntoHeaderValue for String {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for usize {
|
||||
impl TryIntoHeaderValue for usize {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
@ -87,7 +87,7 @@ impl IntoHeaderValue for usize {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for i64 {
|
||||
impl TryIntoHeaderValue for i64 {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
@ -96,7 +96,7 @@ impl IntoHeaderValue for i64 {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for u64 {
|
||||
impl TryIntoHeaderValue for u64 {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
@ -105,7 +105,7 @@ impl IntoHeaderValue for u64 {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for i32 {
|
||||
impl TryIntoHeaderValue for i32 {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
@ -114,7 +114,7 @@ impl IntoHeaderValue for i32 {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for u32 {
|
||||
impl TryIntoHeaderValue for u32 {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
@ -123,7 +123,7 @@ impl IntoHeaderValue for u32 {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for Mime {
|
||||
impl TryIntoHeaderValue for Mime {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
|
@ -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.
|
||||
///
|
||||
@ -306,8 +306,11 @@ impl HeaderMap {
|
||||
/// assert_eq!(set_cookies_iter.next().unwrap(), "two=2");
|
||||
/// assert!(set_cookies_iter.next().is_none());
|
||||
/// ```
|
||||
pub fn get_all(&self, key: impl AsHeaderName) -> GetAll<'_> {
|
||||
GetAll::new(self.get_value(key))
|
||||
pub fn get_all(&self, key: impl AsHeaderName) -> std::slice::Iter<'_, HeaderValue> {
|
||||
match self.get_value(key) {
|
||||
Some(value) => value.iter(),
|
||||
None => (&[]).iter(),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: get_all_mut ?
|
||||
@ -333,7 +336,7 @@ impl HeaderMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a name-value pair into the map.
|
||||
/// Inserts (overrides) a name-value pair in the map.
|
||||
///
|
||||
/// If the map already contained this key, the new value is associated with the key and all
|
||||
/// previous values are removed and returned as a `Removed` iterator. The key is not updated;
|
||||
@ -372,7 +375,7 @@ impl HeaderMap {
|
||||
Removed::new(value)
|
||||
}
|
||||
|
||||
/// Inserts a name-value pair into the map.
|
||||
/// Appends a name-value pair to the map.
|
||||
///
|
||||
/// If the map already contained this key, the new value is added to the list of values
|
||||
/// currently associated with the key. The key is not updated; this matters for types that can
|
||||
@ -602,52 +605,13 @@ impl<'a> IntoIterator for &'a HeaderMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over borrowed values with the same associated name.
|
||||
///
|
||||
/// See [`HeaderMap::get_all`].
|
||||
#[derive(Debug)]
|
||||
pub struct GetAll<'a> {
|
||||
idx: usize,
|
||||
value: Option<&'a Value>,
|
||||
}
|
||||
|
||||
impl<'a> GetAll<'a> {
|
||||
fn new(value: Option<&'a Value>) -> Self {
|
||||
Self { idx: 0, value }
|
||||
/// Convert `http::HeaderMap` to our `HeaderMap`.
|
||||
impl From<http::HeaderMap> for HeaderMap {
|
||||
fn from(mut map: http::HeaderMap) -> HeaderMap {
|
||||
HeaderMap::from_drain(map.drain())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for GetAll<'a> {
|
||||
type Item = &'a HeaderValue;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let val = self.value?;
|
||||
|
||||
match val.get(self.idx) {
|
||||
Some(val) => {
|
||||
self.idx += 1;
|
||||
Some(val)
|
||||
}
|
||||
None => {
|
||||
// current index is none; remove value to fast-path future next calls
|
||||
self.value = None;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
match self.value {
|
||||
Some(val) => (val.len(), Some(val.len())),
|
||||
None => (0, Some(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for GetAll<'_> {}
|
||||
|
||||
impl iter::FusedIterator for GetAll<'_> {}
|
||||
|
||||
/// Iterator over removed, owned values with the same associated name.
|
||||
///
|
||||
/// Returned from methods that remove or replace items. See [`HeaderMap::insert`]
|
||||
@ -895,7 +859,7 @@ mod tests {
|
||||
|
||||
assert_impl_all!(HeaderMap: IntoIterator);
|
||||
assert_impl_all!(Keys<'_>: Iterator, ExactSizeIterator, FusedIterator);
|
||||
assert_impl_all!(GetAll<'_>: Iterator, ExactSizeIterator, FusedIterator);
|
||||
assert_impl_all!(std::slice::Iter<'_, HeaderValue>: Iterator, ExactSizeIterator, FusedIterator);
|
||||
assert_impl_all!(Removed: Iterator, ExactSizeIterator, FusedIterator);
|
||||
assert_impl_all!(Iter<'_>: Iterator, ExactSizeIterator, FusedIterator);
|
||||
assert_impl_all!(IntoIter: Iterator, ExactSizeIterator, FusedIterator);
|
||||
|
@ -37,8 +37,8 @@ mod shared;
|
||||
mod utils;
|
||||
|
||||
pub use self::as_name::AsHeaderName;
|
||||
pub use self::into_pair::IntoHeaderPair;
|
||||
pub use self::into_value::IntoHeaderValue;
|
||||
pub use self::into_pair::TryIntoHeaderPair;
|
||||
pub use self::into_value::TryIntoHeaderValue;
|
||||
pub use self::map::HeaderMap;
|
||||
pub use self::shared::{
|
||||
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, LanguageTag,
|
||||
@ -49,21 +49,14 @@ pub use self::utils::{
|
||||
};
|
||||
|
||||
/// An interface for types that already represent a valid header.
|
||||
pub trait Header: IntoHeaderValue {
|
||||
/// Returns the name of the header field
|
||||
pub trait Header: TryIntoHeaderValue {
|
||||
/// 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
|
||||
|
@ -5,7 +5,7 @@ use http::header::InvalidHeaderValue;
|
||||
|
||||
use crate::{
|
||||
error::ParseError,
|
||||
header::{self, from_one_raw_str, Header, HeaderName, HeaderValue, IntoHeaderValue},
|
||||
header::{self, from_one_raw_str, Header, HeaderName, HeaderValue, TryIntoHeaderValue},
|
||||
HttpMessage,
|
||||
};
|
||||
|
||||
@ -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 fn is_compression(self) -> bool {
|
||||
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
|
||||
}
|
||||
|
||||
/// Convert content encoding to string.
|
||||
#[inline]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
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)
|
||||
@ -96,7 +104,7 @@ impl TryFrom<&str> for ContentEncoding {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for ContentEncoding {
|
||||
impl TryIntoHeaderValue for ContentEncoding {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
fn try_into_value(self) -> Result<http::HeaderValue, Self::Error> {
|
||||
|
@ -4,7 +4,8 @@ use bytes::BytesMut;
|
||||
use http::header::{HeaderValue, InvalidHeaderValue};
|
||||
|
||||
use crate::{
|
||||
config::DATE_VALUE_LENGTH, error::ParseError, header::IntoHeaderValue, helpers::MutWriter,
|
||||
config::DATE_VALUE_LENGTH, error::ParseError, header::TryIntoHeaderValue,
|
||||
helpers::MutWriter,
|
||||
};
|
||||
|
||||
/// A timestamp with HTTP-style formatting and parsing.
|
||||
@ -29,7 +30,7 @@ impl fmt::Display for HttpDate {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for HttpDate {
|
||||
impl TryIntoHeaderValue for HttpDate {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||
|
@ -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"
|
||||
);
|
||||
|
||||
@ -87,7 +91,7 @@ impl fmt::Display for Quality {
|
||||
|
||||
// 0 is already handled so it's not possible to have a trailing 0 in this range
|
||||
// we can just write the integer
|
||||
itoa::fmt(f, x)
|
||||
itoa_fmt(f, x)
|
||||
} else if x < 100 {
|
||||
// x in is range 10–99
|
||||
|
||||
@ -95,21 +99,21 @@ impl fmt::Display for Quality {
|
||||
|
||||
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 100–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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -117,6 +121,12 @@ impl fmt::Display for Quality {
|
||||
}
|
||||
}
|
||||
|
||||
/// Write integer to a `fmt::Write`.
|
||||
pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Result {
|
||||
let mut buf = itoa::Buffer::new();
|
||||
wr.write_str(buf.format(value))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Display, Error)]
|
||||
#[display(fmt = "quality out of bounds")]
|
||||
#[non_exhaustive]
|
||||
@ -148,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.
|
||||
@ -179,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),
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -19,7 +19,6 @@
|
||||
#![allow(
|
||||
clippy::type_complexity,
|
||||
clippy::too_many_arguments,
|
||||
clippy::new_without_default,
|
||||
clippy::borrow_interior_mutable_const
|
||||
)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
@ -28,26 +27,26 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub use ::http::{uri, uri::Uri};
|
||||
pub use ::http::{Method, StatusCode, Version};
|
||||
|
||||
pub mod body;
|
||||
mod builder;
|
||||
mod config;
|
||||
|
||||
#[cfg(feature = "__compress")]
|
||||
pub mod encoding;
|
||||
pub mod error;
|
||||
mod extensions;
|
||||
pub mod h1;
|
||||
pub mod h2;
|
||||
pub mod header;
|
||||
mod helpers;
|
||||
mod http_message;
|
||||
mod message;
|
||||
mod payload;
|
||||
mod request;
|
||||
mod response;
|
||||
mod response_builder;
|
||||
mod requests;
|
||||
mod responses;
|
||||
mod service;
|
||||
|
||||
pub mod error;
|
||||
pub mod h1;
|
||||
pub mod h2;
|
||||
pub mod test;
|
||||
pub mod ws;
|
||||
|
||||
@ -58,16 +57,13 @@ pub use self::extensions::Extensions;
|
||||
pub use self::header::ContentEncoding;
|
||||
pub use self::http_message::HttpMessage;
|
||||
pub use self::message::ConnectionType;
|
||||
pub use self::message::{Message, RequestHead, RequestHeadType, ResponseHead};
|
||||
pub use self::payload::{Payload, PayloadStream};
|
||||
pub use self::request::Request;
|
||||
pub use self::response::Response;
|
||||
pub use self::response_builder::ResponseBuilder;
|
||||
pub use self::message::Message;
|
||||
#[allow(deprecated)]
|
||||
pub use self::payload::{BoxedPayloadStream, Payload, PayloadStream};
|
||||
pub use self::requests::{Request, RequestHead, RequestHeadType};
|
||||
pub use self::responses::{Response, ResponseBuilder, ResponseHead};
|
||||
pub use self::service::HttpService;
|
||||
|
||||
pub use ::http::{uri, uri::Uri};
|
||||
pub use ::http::{Method, StatusCode, Version};
|
||||
|
||||
/// A major HTTP protocol version.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[non_exhaustive]
|
||||
|
@ -1,26 +1,17 @@
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
net,
|
||||
rc::Rc,
|
||||
};
|
||||
use std::{cell::RefCell, ops, rc::Rc};
|
||||
|
||||
use bitflags::bitflags;
|
||||
|
||||
use crate::{
|
||||
header::{self, HeaderMap},
|
||||
Extensions, Method, StatusCode, Uri, Version,
|
||||
};
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@ -44,294 +35,6 @@ pub trait Head: Default + 'static {
|
||||
F: FnOnce(&MessagePool<Self>) -> R;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RequestHead {
|
||||
pub method: Method,
|
||||
pub uri: Uri,
|
||||
pub version: Version,
|
||||
pub headers: HeaderMap,
|
||||
pub peer_addr: Option<net::SocketAddr>,
|
||||
flags: Flags,
|
||||
}
|
||||
|
||||
impl Default for RequestHead {
|
||||
fn default() -> RequestHead {
|
||||
RequestHead {
|
||||
method: Method::default(),
|
||||
uri: Uri::default(),
|
||||
version: Version::HTTP_11,
|
||||
headers: HeaderMap::with_capacity(16),
|
||||
peer_addr: None,
|
||||
flags: Flags::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Head for RequestHead {
|
||||
fn clear(&mut self) {
|
||||
self.flags = Flags::empty();
|
||||
self.headers.clear();
|
||||
}
|
||||
|
||||
fn with_pool<F, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce(&MessagePool<Self>) -> R,
|
||||
{
|
||||
REQUEST_POOL.with(|p| f(p))
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHead {
|
||||
/// Read the message headers.
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Mutable reference to the message headers.
|
||||
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Is to uppercase headers with Camel-Case.
|
||||
/// Default is `false`
|
||||
#[inline]
|
||||
pub fn camel_case_headers(&self) -> bool {
|
||||
self.flags.contains(Flags::CAMEL_CASE)
|
||||
}
|
||||
|
||||
/// Set `true` to send headers which are formatted as Camel-Case.
|
||||
#[inline]
|
||||
pub fn set_camel_case_headers(&mut self, val: bool) {
|
||||
if val {
|
||||
self.flags.insert(Flags::CAMEL_CASE);
|
||||
} else {
|
||||
self.flags.remove(Flags::CAMEL_CASE);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Set connection type of the message
|
||||
pub fn set_connection_type(&mut self, ctype: ConnectionType) {
|
||||
match ctype {
|
||||
ConnectionType::Close => self.flags.insert(Flags::CLOSE),
|
||||
ConnectionType::KeepAlive => self.flags.insert(Flags::KEEP_ALIVE),
|
||||
ConnectionType::Upgrade => self.flags.insert(Flags::UPGRADE),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Connection type
|
||||
pub fn connection_type(&self) -> ConnectionType {
|
||||
if self.flags.contains(Flags::CLOSE) {
|
||||
ConnectionType::Close
|
||||
} else if self.flags.contains(Flags::KEEP_ALIVE) {
|
||||
ConnectionType::KeepAlive
|
||||
} else if self.flags.contains(Flags::UPGRADE) {
|
||||
ConnectionType::Upgrade
|
||||
} else if self.version < Version::HTTP_11 {
|
||||
ConnectionType::Close
|
||||
} else {
|
||||
ConnectionType::KeepAlive
|
||||
}
|
||||
}
|
||||
|
||||
/// Connection upgrade status
|
||||
pub fn upgrade(&self) -> bool {
|
||||
self.headers()
|
||||
.get(header::CONNECTION)
|
||||
.map(|hdr| {
|
||||
if let Ok(s) = hdr.to_str() {
|
||||
s.to_ascii_lowercase().contains("upgrade")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Get response body chunking state
|
||||
pub fn chunked(&self) -> bool {
|
||||
!self.flags.contains(Flags::NO_CHUNKING)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn no_chunking(&mut self, val: bool) {
|
||||
if val {
|
||||
self.flags.insert(Flags::NO_CHUNKING);
|
||||
} else {
|
||||
self.flags.remove(Flags::NO_CHUNKING);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Request contains `EXPECT` header
|
||||
pub fn expect(&self) -> bool {
|
||||
self.flags.contains(Flags::EXPECT)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn set_expect(&mut self) {
|
||||
self.flags.insert(Flags::EXPECT);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum RequestHeadType {
|
||||
Owned(RequestHead),
|
||||
Rc(Rc<RequestHead>, Option<HeaderMap>),
|
||||
}
|
||||
|
||||
impl RequestHeadType {
|
||||
pub fn extra_headers(&self) -> Option<&HeaderMap> {
|
||||
match self {
|
||||
RequestHeadType::Owned(_) => None,
|
||||
RequestHeadType::Rc(_, headers) => headers.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<RequestHead> for RequestHeadType {
|
||||
fn as_ref(&self) -> &RequestHead {
|
||||
match self {
|
||||
RequestHeadType::Owned(head) => head,
|
||||
RequestHeadType::Rc(head, _) => head.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RequestHead> for RequestHeadType {
|
||||
fn from(head: RequestHead) -> Self {
|
||||
RequestHeadType::Owned(head)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ResponseHead {
|
||||
pub version: Version,
|
||||
pub status: StatusCode,
|
||||
pub headers: HeaderMap,
|
||||
pub reason: Option<&'static str>,
|
||||
pub(crate) extensions: RefCell<Extensions>,
|
||||
flags: Flags,
|
||||
}
|
||||
|
||||
impl ResponseHead {
|
||||
/// Create new instance of `ResponseHead` type
|
||||
#[inline]
|
||||
pub fn new(status: StatusCode) -> ResponseHead {
|
||||
ResponseHead {
|
||||
status,
|
||||
version: Version::default(),
|
||||
headers: HeaderMap::with_capacity(12),
|
||||
reason: None,
|
||||
flags: Flags::empty(),
|
||||
extensions: RefCell::new(Extensions::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Message extensions
|
||||
#[inline]
|
||||
pub fn extensions(&self) -> Ref<'_, Extensions> {
|
||||
self.extensions.borrow()
|
||||
}
|
||||
|
||||
/// Mutable reference to a the message's extensions
|
||||
#[inline]
|
||||
pub fn extensions_mut(&self) -> RefMut<'_, Extensions> {
|
||||
self.extensions.borrow_mut()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Read the message headers.
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Mutable reference to the message headers.
|
||||
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Set connection type of the message
|
||||
pub fn set_connection_type(&mut self, ctype: ConnectionType) {
|
||||
match ctype {
|
||||
ConnectionType::Close => self.flags.insert(Flags::CLOSE),
|
||||
ConnectionType::KeepAlive => self.flags.insert(Flags::KEEP_ALIVE),
|
||||
ConnectionType::Upgrade => self.flags.insert(Flags::UPGRADE),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn connection_type(&self) -> ConnectionType {
|
||||
if self.flags.contains(Flags::CLOSE) {
|
||||
ConnectionType::Close
|
||||
} else if self.flags.contains(Flags::KEEP_ALIVE) {
|
||||
ConnectionType::KeepAlive
|
||||
} else if self.flags.contains(Flags::UPGRADE) {
|
||||
ConnectionType::Upgrade
|
||||
} else if self.version < Version::HTTP_11 {
|
||||
ConnectionType::Close
|
||||
} else {
|
||||
ConnectionType::KeepAlive
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if keep-alive is enabled
|
||||
#[inline]
|
||||
pub fn keep_alive(&self) -> bool {
|
||||
self.connection_type() == ConnectionType::KeepAlive
|
||||
}
|
||||
|
||||
/// Check upgrade status of this message
|
||||
#[inline]
|
||||
pub fn upgrade(&self) -> bool {
|
||||
self.connection_type() == ConnectionType::Upgrade
|
||||
}
|
||||
|
||||
/// Get custom reason for the response
|
||||
#[inline]
|
||||
pub fn reason(&self) -> &str {
|
||||
self.reason.unwrap_or_else(|| {
|
||||
self.status
|
||||
.canonical_reason()
|
||||
.unwrap_or("<unknown status code>")
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn conn_type(&self) -> Option<ConnectionType> {
|
||||
if self.flags.contains(Flags::CLOSE) {
|
||||
Some(ConnectionType::Close)
|
||||
} else if self.flags.contains(Flags::KEEP_ALIVE) {
|
||||
Some(ConnectionType::KeepAlive)
|
||||
} else if self.flags.contains(Flags::UPGRADE) {
|
||||
Some(ConnectionType::Upgrade)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Get response body chunking state
|
||||
pub fn chunked(&self) -> bool {
|
||||
!self.flags.contains(Flags::NO_CHUNKING)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Set no chunking for payload
|
||||
pub fn no_chunking(&mut self, val: bool) {
|
||||
if val {
|
||||
self.flags.insert(Flags::NO_CHUNKING);
|
||||
} else {
|
||||
self.flags.remove(Flags::NO_CHUNKING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Message<T: Head> {
|
||||
/// Rc here should not be cloned by anyone.
|
||||
/// It's used to reuse allocation of T and no shared ownership is allowed.
|
||||
@ -340,12 +43,13 @@ pub struct Message<T: Head> {
|
||||
|
||||
impl<T: Head> Message<T> {
|
||||
/// Get new message from the pool of objects
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
T::with_pool(MessagePool::get_message)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Head> std::ops::Deref for Message<T> {
|
||||
impl<T: Head> ops::Deref for Message<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
@ -353,7 +57,7 @@ impl<T: Head> std::ops::Deref for Message<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Head> std::ops::DerefMut for Message<T> {
|
||||
impl<T: Head> ops::DerefMut for Message<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
Rc::get_mut(&mut self.head).expect("Multiple copies exist")
|
||||
}
|
||||
@ -365,53 +69,12 @@ impl<T: Head> Drop for Message<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct BoxedResponseHead {
|
||||
head: Option<Box<ResponseHead>>,
|
||||
}
|
||||
|
||||
impl BoxedResponseHead {
|
||||
/// Get new message from the pool of objects
|
||||
pub fn new(status: StatusCode) -> Self {
|
||||
RESPONSE_POOL.with(|p| p.get_message(status))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for BoxedResponseHead {
|
||||
type Target = ResponseHead;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.head.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for BoxedResponseHead {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.head.as_mut().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BoxedResponseHead {
|
||||
fn drop(&mut self) {
|
||||
if let Some(head) = self.head.take() {
|
||||
RESPONSE_POOL.with(move |p| p.release(head))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic `Head` object pool.
|
||||
#[doc(hidden)]
|
||||
/// Request's objects pool
|
||||
pub struct MessagePool<T: Head>(RefCell<Vec<Rc<T>>>);
|
||||
|
||||
#[doc(hidden)]
|
||||
#[allow(clippy::vec_box)]
|
||||
/// Request's objects pool
|
||||
pub struct BoxedResponsePool(RefCell<Vec<Box<ResponseHead>>>);
|
||||
|
||||
thread_local!(static REQUEST_POOL: MessagePool<RequestHead> = MessagePool::<RequestHead>::create());
|
||||
thread_local!(static RESPONSE_POOL: BoxedResponsePool = BoxedResponsePool::create());
|
||||
|
||||
impl<T: Head> MessagePool<T> {
|
||||
fn create() -> MessagePool<T> {
|
||||
pub(crate) fn create() -> MessagePool<T> {
|
||||
MessagePool(RefCell::new(Vec::with_capacity(128)))
|
||||
}
|
||||
|
||||
@ -433,43 +96,11 @@ impl<T: Head> MessagePool<T> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Release request instance
|
||||
/// Release message instance
|
||||
fn release(&self, msg: Rc<T>) {
|
||||
let v = &mut self.0.borrow_mut();
|
||||
if v.len() < 128 {
|
||||
v.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BoxedResponsePool {
|
||||
fn create() -> BoxedResponsePool {
|
||||
BoxedResponsePool(RefCell::new(Vec::with_capacity(128)))
|
||||
}
|
||||
|
||||
/// Get message from the pool
|
||||
#[inline]
|
||||
fn get_message(&self, status: StatusCode) -> BoxedResponseHead {
|
||||
if let Some(mut head) = self.0.borrow_mut().pop() {
|
||||
head.reason = None;
|
||||
head.status = status;
|
||||
head.headers.clear();
|
||||
head.flags = Flags::empty();
|
||||
BoxedResponseHead { head: Some(head) }
|
||||
} else {
|
||||
BoxedResponseHead {
|
||||
head: Some(Box::new(ResponseHead::new(status))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Release request instance
|
||||
fn release(&self, mut msg: Box<ResponseHead>) {
|
||||
let v = &mut self.0.borrow_mut();
|
||||
if v.len() < 128 {
|
||||
msg.extensions.get_mut().clear();
|
||||
v.push(msg);
|
||||
let pool = &mut self.0.borrow_mut();
|
||||
if pool.len() < 128 {
|
||||
pool.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,67 +1,89 @@
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{
|
||||
mem,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_core::Stream;
|
||||
use h2::RecvStream;
|
||||
|
||||
use crate::error::PayloadError;
|
||||
|
||||
/// Type represent boxed payload
|
||||
pub type PayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadError>>>>;
|
||||
/// A boxed payload stream.
|
||||
pub type BoxedPayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadError>>>>;
|
||||
|
||||
/// Type represent streaming payload
|
||||
pub enum Payload<S = PayloadStream> {
|
||||
None,
|
||||
H1(crate::h1::Payload),
|
||||
H2(crate::h2::Payload),
|
||||
Stream(S),
|
||||
#[deprecated(since = "4.0.0", note = "Renamed to `BoxedPayloadStream`.")]
|
||||
pub type PayloadStream = BoxedPayloadStream;
|
||||
|
||||
pin_project_lite::pin_project! {
|
||||
/// A streaming payload.
|
||||
#[project = PayloadProj]
|
||||
pub enum Payload<S = BoxedPayloadStream> {
|
||||
None,
|
||||
H1 { payload: crate::h1::Payload },
|
||||
H2 { payload: crate::h2::Payload },
|
||||
Stream { #[pin] payload: S },
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> From<crate::h1::Payload> for Payload<S> {
|
||||
fn from(v: crate::h1::Payload) -> Self {
|
||||
Payload::H1(v)
|
||||
fn from(payload: crate::h1::Payload) -> Self {
|
||||
Payload::H1 { payload }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> From<crate::h2::Payload> for Payload<S> {
|
||||
fn from(v: crate::h2::Payload) -> Self {
|
||||
Payload::H2(v)
|
||||
fn from(payload: crate::h2::Payload) -> Self {
|
||||
Payload::H2 { payload }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> From<RecvStream> for Payload<S> {
|
||||
fn from(v: RecvStream) -> Self {
|
||||
Payload::H2(crate::h2::Payload::new(v))
|
||||
impl<S> From<h2::RecvStream> for Payload<S> {
|
||||
fn from(stream: h2::RecvStream) -> Self {
|
||||
Payload::H2 {
|
||||
payload: crate::h2::Payload::new(stream),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PayloadStream> for Payload {
|
||||
fn from(pl: PayloadStream) -> Self {
|
||||
Payload::Stream(pl)
|
||||
impl From<BoxedPayloadStream> for Payload {
|
||||
fn from(payload: BoxedPayloadStream) -> Self {
|
||||
Payload::Stream { payload }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Payload<S> {
|
||||
/// Takes current payload and replaces it with `None` value
|
||||
pub fn take(&mut self) -> Payload<S> {
|
||||
std::mem::replace(self, Payload::None)
|
||||
mem::replace(self, Payload::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Stream for Payload<S>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin,
|
||||
S: Stream<Item = Result<Bytes, PayloadError>>,
|
||||
{
|
||||
type Item = Result<Bytes, PayloadError>;
|
||||
|
||||
#[inline]
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match self.get_mut() {
|
||||
Payload::None => Poll::Ready(None),
|
||||
Payload::H1(ref mut pl) => pl.readany(cx),
|
||||
Payload::H2(ref mut pl) => Pin::new(pl).poll_next(cx),
|
||||
Payload::Stream(ref mut pl) => Pin::new(pl).poll_next(cx),
|
||||
match self.project() {
|
||||
PayloadProj::None => Poll::Ready(None),
|
||||
PayloadProj::H1 { payload } => Pin::new(payload).poll_next(cx),
|
||||
PayloadProj::H2 { payload } => Pin::new(payload).poll_next(cx),
|
||||
PayloadProj::Stream { payload } => payload.poll_next(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||
|
||||
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
||||
|
||||
use super::*;
|
||||
|
||||
assert_impl_all!(Payload: Unpin);
|
||||
assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe);
|
||||
}
|
||||
|
174
actix-http/src/requests/head.rs
Normal file
174
actix-http/src/requests/head.rs
Normal file
@ -0,0 +1,174 @@
|
||||
use std::{net, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
header::{self, HeaderMap},
|
||||
message::{Flags, Head, MessagePool},
|
||||
ConnectionType, Method, Uri, Version,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static REQUEST_POOL: MessagePool<RequestHead> = MessagePool::<RequestHead>::create()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RequestHead {
|
||||
pub method: Method,
|
||||
pub uri: Uri,
|
||||
pub version: Version,
|
||||
pub headers: HeaderMap,
|
||||
pub peer_addr: Option<net::SocketAddr>,
|
||||
flags: Flags,
|
||||
}
|
||||
|
||||
impl Default for RequestHead {
|
||||
fn default() -> RequestHead {
|
||||
RequestHead {
|
||||
method: Method::default(),
|
||||
uri: Uri::default(),
|
||||
version: Version::HTTP_11,
|
||||
headers: HeaderMap::with_capacity(16),
|
||||
peer_addr: None,
|
||||
flags: Flags::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Head for RequestHead {
|
||||
fn clear(&mut self) {
|
||||
self.flags = Flags::empty();
|
||||
self.headers.clear();
|
||||
}
|
||||
|
||||
fn with_pool<F, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce(&MessagePool<Self>) -> R,
|
||||
{
|
||||
REQUEST_POOL.with(|p| f(p))
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestHead {
|
||||
/// Read the message headers.
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Mutable reference to the message headers.
|
||||
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Is to uppercase headers with Camel-Case.
|
||||
/// Default is `false`
|
||||
#[inline]
|
||||
pub fn camel_case_headers(&self) -> bool {
|
||||
self.flags.contains(Flags::CAMEL_CASE)
|
||||
}
|
||||
|
||||
/// Set `true` to send headers which are formatted as Camel-Case.
|
||||
#[inline]
|
||||
pub fn set_camel_case_headers(&mut self, val: bool) {
|
||||
if val {
|
||||
self.flags.insert(Flags::CAMEL_CASE);
|
||||
} else {
|
||||
self.flags.remove(Flags::CAMEL_CASE);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Set connection type of the message
|
||||
pub fn set_connection_type(&mut self, ctype: ConnectionType) {
|
||||
match ctype {
|
||||
ConnectionType::Close => self.flags.insert(Flags::CLOSE),
|
||||
ConnectionType::KeepAlive => self.flags.insert(Flags::KEEP_ALIVE),
|
||||
ConnectionType::Upgrade => self.flags.insert(Flags::UPGRADE),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Connection type
|
||||
pub fn connection_type(&self) -> ConnectionType {
|
||||
if self.flags.contains(Flags::CLOSE) {
|
||||
ConnectionType::Close
|
||||
} else if self.flags.contains(Flags::KEEP_ALIVE) {
|
||||
ConnectionType::KeepAlive
|
||||
} else if self.flags.contains(Flags::UPGRADE) {
|
||||
ConnectionType::Upgrade
|
||||
} else if self.version < Version::HTTP_11 {
|
||||
ConnectionType::Close
|
||||
} else {
|
||||
ConnectionType::KeepAlive
|
||||
}
|
||||
}
|
||||
|
||||
/// Connection upgrade status
|
||||
pub fn upgrade(&self) -> bool {
|
||||
self.headers()
|
||||
.get(header::CONNECTION)
|
||||
.map(|hdr| {
|
||||
if let Ok(s) = hdr.to_str() {
|
||||
s.to_ascii_lowercase().contains("upgrade")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Get response body chunking state
|
||||
pub fn chunked(&self) -> bool {
|
||||
!self.flags.contains(Flags::NO_CHUNKING)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn no_chunking(&mut self, val: bool) {
|
||||
if val {
|
||||
self.flags.insert(Flags::NO_CHUNKING);
|
||||
} else {
|
||||
self.flags.remove(Flags::NO_CHUNKING);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Request contains `EXPECT` header
|
||||
pub fn expect(&self) -> bool {
|
||||
self.flags.contains(Flags::EXPECT)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn set_expect(&mut self) {
|
||||
self.flags.insert(Flags::EXPECT);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum RequestHeadType {
|
||||
Owned(RequestHead),
|
||||
Rc(Rc<RequestHead>, Option<HeaderMap>),
|
||||
}
|
||||
|
||||
impl RequestHeadType {
|
||||
pub fn extra_headers(&self) -> Option<&HeaderMap> {
|
||||
match self {
|
||||
RequestHeadType::Owned(_) => None,
|
||||
RequestHeadType::Rc(_, headers) => headers.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<RequestHead> for RequestHeadType {
|
||||
fn as_ref(&self) -> &RequestHead {
|
||||
match self {
|
||||
RequestHeadType::Owned(head) => head,
|
||||
RequestHeadType::Rc(head, _) => head.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RequestHead> for RequestHeadType {
|
||||
fn from(head: RequestHead) -> Self {
|
||||
RequestHeadType::Owned(head)
|
||||
}
|
||||
}
|
7
actix-http/src/requests/mod.rs
Normal file
7
actix-http/src/requests/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
//! HTTP requests.
|
||||
|
||||
mod head;
|
||||
mod request;
|
||||
|
||||
pub use self::head::{RequestHead, RequestHeadType};
|
||||
pub use self::request::Request;
|
@ -10,19 +10,16 @@ use std::{
|
||||
use http::{header, Method, Uri, Version};
|
||||
|
||||
use crate::{
|
||||
extensions::Extensions,
|
||||
header::HeaderMap,
|
||||
message::{Message, RequestHead},
|
||||
payload::{Payload, PayloadStream},
|
||||
HttpMessage,
|
||||
header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, Message, Payload,
|
||||
RequestHead,
|
||||
};
|
||||
|
||||
/// An HTTP request.
|
||||
pub struct Request<P = PayloadStream> {
|
||||
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> {
|
||||
@ -37,37 +34,36 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Message<RequestHead>> for Request<PayloadStream> {
|
||||
impl From<Message<RequestHead>> for Request<BoxedPayloadStream> {
|
||||
fn from(head: Message<RequestHead>) -> Self {
|
||||
Request {
|
||||
head,
|
||||
payload: Payload::None,
|
||||
req_data: RefCell::new(Extensions::default()),
|
||||
extensions: RefCell::new(Extensions::default()),
|
||||
conn_data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Request<PayloadStream> {
|
||||
impl Request<BoxedPayloadStream> {
|
||||
/// Create new Request instance
|
||||
pub fn new() -> Request<PayloadStream> {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Request<BoxedPayloadStream> {
|
||||
Request {
|
||||
head: Message::new(),
|
||||
payload: Payload::None,
|
||||
req_data: RefCell::new(Extensions::default()),
|
||||
extensions: RefCell::new(Extensions::default()),
|
||||
conn_data: None,
|
||||
}
|
||||
}
|
||||
@ -79,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,
|
||||
}
|
||||
}
|
||||
@ -92,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,
|
||||
@ -197,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(&mut self.req_data.get_mut())
|
||||
mem::take(self.extensions.get_mut())
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,13 @@
|
||||
//! HTTP response builder.
|
||||
|
||||
use std::{
|
||||
cell::{Ref, RefMut},
|
||||
fmt, str,
|
||||
};
|
||||
use std::{cell::RefCell, fmt, str};
|
||||
|
||||
use crate::{
|
||||
body::{EitherBody, MessageBody},
|
||||
error::{Error, HttpError},
|
||||
header::{self, IntoHeaderPair, IntoHeaderValue},
|
||||
message::{BoxedResponseHead, ConnectionType, ResponseHead},
|
||||
Extensions, Response, StatusCode,
|
||||
header::{self, TryIntoHeaderPair, TryIntoHeaderValue},
|
||||
responses::{BoxedResponseHead, ResponseHead},
|
||||
ConnectionType, Extensions, Response, StatusCode,
|
||||
};
|
||||
|
||||
/// An HTTP response builder.
|
||||
@ -90,12 +87,9 @@ impl ResponseBuilder {
|
||||
/// assert!(res.headers().contains_key("content-type"));
|
||||
/// assert!(res.headers().contains_key("x-test"));
|
||||
/// ```
|
||||
pub fn insert_header<H>(&mut self, header: H) -> &mut Self
|
||||
where
|
||||
H: IntoHeaderPair,
|
||||
{
|
||||
pub fn insert_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
|
||||
if let Some(parts) = self.inner() {
|
||||
match header.try_into_header_pair() {
|
||||
match header.try_into_pair() {
|
||||
Ok((key, value)) => {
|
||||
parts.headers.insert(key, value);
|
||||
}
|
||||
@ -121,12 +115,9 @@ impl ResponseBuilder {
|
||||
/// assert_eq!(res.headers().get_all("content-type").count(), 1);
|
||||
/// assert_eq!(res.headers().get_all("x-test").count(), 2);
|
||||
/// ```
|
||||
pub fn append_header<H>(&mut self, header: H) -> &mut Self
|
||||
where
|
||||
H: IntoHeaderPair,
|
||||
{
|
||||
pub fn append_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
|
||||
if let Some(parts) = self.inner() {
|
||||
match header.try_into_header_pair() {
|
||||
match header.try_into_pair() {
|
||||
Ok((key, value)) => parts.headers.append(key, value),
|
||||
Err(e) => self.err = Some(e.into()),
|
||||
};
|
||||
@ -157,7 +148,7 @@ impl ResponseBuilder {
|
||||
#[inline]
|
||||
pub fn upgrade<V>(&mut self, value: V) -> &mut Self
|
||||
where
|
||||
V: IntoHeaderValue,
|
||||
V: TryIntoHeaderValue,
|
||||
{
|
||||
if let Some(parts) = self.inner() {
|
||||
parts.set_connection_type(ConnectionType::Upgrade);
|
||||
@ -195,7 +186,7 @@ impl ResponseBuilder {
|
||||
#[inline]
|
||||
pub fn content_type<V>(&mut self, value: V) -> &mut Self
|
||||
where
|
||||
V: IntoHeaderValue,
|
||||
V: TryIntoHeaderValue,
|
||||
{
|
||||
if let Some(parts) = self.inner() {
|
||||
match value.try_into_value() {
|
||||
@ -208,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.
|
||||
@ -244,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.
|
255
actix-http/src/responses/head.rs
Normal file
255
actix-http/src/responses/head.rs
Normal file
@ -0,0 +1,255 @@
|
||||
//! Response head type and caching pool.
|
||||
|
||||
use std::{cell::RefCell, ops};
|
||||
|
||||
use crate::{header::HeaderMap, message::Flags, ConnectionType, StatusCode, Version};
|
||||
|
||||
thread_local! {
|
||||
static RESPONSE_POOL: BoxedResponsePool = BoxedResponsePool::create();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResponseHead {
|
||||
pub version: Version,
|
||||
pub status: StatusCode,
|
||||
pub headers: HeaderMap,
|
||||
pub reason: Option<&'static str>,
|
||||
pub(crate) flags: Flags,
|
||||
}
|
||||
|
||||
impl ResponseHead {
|
||||
/// Create new instance of `ResponseHead` type
|
||||
#[inline]
|
||||
pub fn new(status: StatusCode) -> ResponseHead {
|
||||
ResponseHead {
|
||||
status,
|
||||
version: Version::HTTP_11,
|
||||
headers: HeaderMap::with_capacity(12),
|
||||
reason: None,
|
||||
flags: Flags::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the message headers.
|
||||
#[inline]
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Mutable reference to the message headers.
|
||||
#[inline]
|
||||
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Sets the flag that controls wether to send headers formatted as Camel-Case.
|
||||
///
|
||||
/// Only applicable to HTTP/1.x responses; HTTP/2 header names are always lowercase.
|
||||
#[inline]
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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),
|
||||
ConnectionType::KeepAlive => self.flags.insert(Flags::KEEP_ALIVE),
|
||||
ConnectionType::Upgrade => self.flags.insert(Flags::UPGRADE),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn connection_type(&self) -> ConnectionType {
|
||||
if self.flags.contains(Flags::CLOSE) {
|
||||
ConnectionType::Close
|
||||
} else if self.flags.contains(Flags::KEEP_ALIVE) {
|
||||
ConnectionType::KeepAlive
|
||||
} else if self.flags.contains(Flags::UPGRADE) {
|
||||
ConnectionType::Upgrade
|
||||
} else if self.version < Version::HTTP_11 {
|
||||
ConnectionType::Close
|
||||
} else {
|
||||
ConnectionType::KeepAlive
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if keep-alive is enabled
|
||||
#[inline]
|
||||
pub fn keep_alive(&self) -> bool {
|
||||
self.connection_type() == ConnectionType::KeepAlive
|
||||
}
|
||||
|
||||
/// Check upgrade status of this message
|
||||
#[inline]
|
||||
pub fn upgrade(&self) -> bool {
|
||||
self.connection_type() == ConnectionType::Upgrade
|
||||
}
|
||||
|
||||
/// Get custom reason for the response
|
||||
#[inline]
|
||||
pub fn reason(&self) -> &str {
|
||||
self.reason.unwrap_or_else(|| {
|
||||
self.status
|
||||
.canonical_reason()
|
||||
.unwrap_or("<unknown status code>")
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn conn_type(&self) -> Option<ConnectionType> {
|
||||
if self.flags.contains(Flags::CLOSE) {
|
||||
Some(ConnectionType::Close)
|
||||
} else if self.flags.contains(Flags::KEEP_ALIVE) {
|
||||
Some(ConnectionType::KeepAlive)
|
||||
} else if self.flags.contains(Flags::UPGRADE) {
|
||||
Some(ConnectionType::Upgrade)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get response body chunking state
|
||||
#[inline]
|
||||
pub fn chunked(&self) -> bool {
|
||||
!self.flags.contains(Flags::NO_CHUNKING)
|
||||
}
|
||||
|
||||
/// Set no chunking for payload
|
||||
#[inline]
|
||||
pub fn no_chunking(&mut self, val: bool) {
|
||||
if val {
|
||||
self.flags.insert(Flags::NO_CHUNKING);
|
||||
} else {
|
||||
self.flags.remove(Flags::NO_CHUNKING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct BoxedResponseHead {
|
||||
head: Option<Box<ResponseHead>>,
|
||||
}
|
||||
|
||||
impl BoxedResponseHead {
|
||||
/// Get new message from the pool of objects
|
||||
pub fn new(status: StatusCode) -> Self {
|
||||
RESPONSE_POOL.with(|p| p.get_message(status))
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for BoxedResponseHead {
|
||||
type Target = ResponseHead;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.head.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::DerefMut for BoxedResponseHead {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.head.as_mut().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BoxedResponseHead {
|
||||
fn drop(&mut self) {
|
||||
if let Some(head) = self.head.take() {
|
||||
RESPONSE_POOL.with(move |p| p.release(head))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Response head object pool.
|
||||
#[doc(hidden)]
|
||||
pub struct BoxedResponsePool(#[allow(clippy::vec_box)] RefCell<Vec<Box<ResponseHead>>>);
|
||||
|
||||
impl BoxedResponsePool {
|
||||
fn create() -> BoxedResponsePool {
|
||||
BoxedResponsePool(RefCell::new(Vec::with_capacity(128)))
|
||||
}
|
||||
|
||||
/// Get message from the pool.
|
||||
#[inline]
|
||||
fn get_message(&self, status: StatusCode) -> BoxedResponseHead {
|
||||
if let Some(mut head) = self.0.borrow_mut().pop() {
|
||||
head.reason = None;
|
||||
head.status = status;
|
||||
head.headers.clear();
|
||||
head.flags = Flags::empty();
|
||||
BoxedResponseHead { head: Some(head) }
|
||||
} else {
|
||||
BoxedResponseHead {
|
||||
head: Some(Box::new(ResponseHead::new(status))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Release request instance.
|
||||
#[inline]
|
||||
fn release(&self, msg: Box<ResponseHead>) {
|
||||
let pool = &mut self.0.borrow_mut();
|
||||
|
||||
if pool.len() < 128 {
|
||||
pool.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io::{Read as _, Write as _},
|
||||
net,
|
||||
};
|
||||
|
||||
use memchr::memmem;
|
||||
|
||||
use crate::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
Error, HttpService, Request, Response,
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn camel_case_headers() {
|
||||
let mut srv = actix_http_test::test_server(|| {
|
||||
HttpService::new(|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");
|
||||
let mut data = vec![0; 1024];
|
||||
let _ = stream.read(&mut data);
|
||||
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_some());
|
||||
|
||||
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");
|
||||
let mut data = vec![0; 1024];
|
||||
let _ = stream.read(&mut data);
|
||||
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_some());
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
}
|
11
actix-http/src/responses/mod.rs
Normal file
11
actix-http/src/responses/mod.rs
Normal file
@ -0,0 +1,11 @@
|
||||
//! HTTP response.
|
||||
|
||||
mod builder;
|
||||
mod head;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod response;
|
||||
|
||||
pub use self::builder::ResponseBuilder;
|
||||
pub(crate) use self::head::BoxedResponseHead;
|
||||
pub use self::head::ResponseHead;
|
||||
pub use self::response::Response;
|
@ -1,7 +1,7 @@
|
||||
//! HTTP response.
|
||||
|
||||
use std::{
|
||||
cell::{Ref, RefMut},
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
fmt, str,
|
||||
};
|
||||
|
||||
@ -9,17 +9,17 @@ use bytes::{Bytes, BytesMut};
|
||||
use bytestring::ByteString;
|
||||
|
||||
use crate::{
|
||||
body::{BoxBody, MessageBody},
|
||||
extensions::Extensions,
|
||||
header::{self, HeaderMap, IntoHeaderValue},
|
||||
message::{BoxedResponseHead, ResponseHead},
|
||||
Error, ResponseBuilder, StatusCode,
|
||||
body::{BoxBody, EitherBody, MessageBody},
|
||||
header::{self, HeaderMap, TryIntoHeaderValue},
|
||||
responses::BoxedResponseHead,
|
||||
Error, Extensions, ResponseBuilder, ResponseHead, StatusCode,
|
||||
};
|
||||
|
||||
/// An HTTP response.
|
||||
pub struct Response<B> {
|
||||
pub(crate) head: BoxedResponseHead,
|
||||
pub(crate) body: B,
|
||||
pub(crate) extensions: RefCell<Extensions>,
|
||||
}
|
||||
|
||||
impl Response<BoxBody> {
|
||||
@ -29,6 +29,7 @@ impl Response<BoxBody> {
|
||||
Response {
|
||||
head: BoxedResponseHead::new(status),
|
||||
body: BoxBody::new(()),
|
||||
extensions: RefCell::new(Extensions::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,6 +76,7 @@ impl<B> Response<B> {
|
||||
Response {
|
||||
head: BoxedResponseHead::new(status),
|
||||
body,
|
||||
extensions: RefCell::new(Extensions::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,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.
|
||||
@ -144,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,
|
||||
)
|
||||
@ -170,13 +178,17 @@ impl<B> Response<B> {
|
||||
/// Returns split head and body.
|
||||
///
|
||||
/// # Implementation Notes
|
||||
/// Due to internal performance optimisations, the first element of the returned tuple is a
|
||||
/// 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. Returns 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,
|
||||
@ -186,6 +198,7 @@ impl<B> Response<B> {
|
||||
Response {
|
||||
head: self.head,
|
||||
body,
|
||||
extensions: self.extensions,
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,10 +207,11 @@ impl<B> Response<B> {
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
self.map_body(|_, body| BoxBody::new(body))
|
||||
self.map_body(|_, body| body.boxed())
|
||||
}
|
||||
|
||||
/// Returns body, consuming this response.
|
||||
#[inline]
|
||||
pub fn into_body(self) -> B {
|
||||
self.body
|
||||
}
|
||||
@ -240,9 +254,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()
|
||||
}
|
||||
}
|
||||
|
@ -493,9 +493,9 @@ where
|
||||
type Future = HttpServiceHandlerResponse<T, S, B, X, U>;
|
||||
|
||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self._poll_ready(cx).map_err(|e| {
|
||||
log::error!("HTTP service readiness error: {:?}", e);
|
||||
DispatchError::Service(e)
|
||||
self._poll_ready(cx).map_err(|err| {
|
||||
log::error!("HTTP service readiness error: {:?}", err);
|
||||
DispatchError::Service(err)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ use bytes::{Bytes, BytesMut};
|
||||
use http::{Method, Uri, Version};
|
||||
|
||||
use crate::{
|
||||
header::{HeaderMap, IntoHeaderPair},
|
||||
header::{HeaderMap, TryIntoHeaderPair},
|
||||
payload::Payload,
|
||||
Request,
|
||||
};
|
||||
@ -92,11 +92,8 @@ impl TestRequest {
|
||||
}
|
||||
|
||||
/// Insert a header, replacing any that were set with an equivalent field name.
|
||||
pub fn insert_header<H>(&mut self, header: H) -> &mut Self
|
||||
where
|
||||
H: IntoHeaderPair,
|
||||
{
|
||||
match header.try_into_header_pair() {
|
||||
pub fn insert_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
|
||||
match header.try_into_pair() {
|
||||
Ok((key, value)) => {
|
||||
parts(&mut self.0).headers.insert(key, value);
|
||||
}
|
||||
@ -109,11 +106,8 @@ impl TestRequest {
|
||||
}
|
||||
|
||||
/// Append a header, keeping any that were set with an equivalent field name.
|
||||
pub fn append_header<H>(&mut self, header: H) -> &mut Self
|
||||
where
|
||||
H: IntoHeaderPair,
|
||||
{
|
||||
match header.try_into_header_pair() {
|
||||
pub fn append_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
|
||||
match header.try_into_pair() {
|
||||
Ok((key, value)) => {
|
||||
parts(&mut self.0).headers.append(key, value);
|
||||
}
|
||||
@ -126,7 +120,7 @@ impl TestRequest {
|
||||
}
|
||||
|
||||
/// Set request payload.
|
||||
pub fn set_payload<B: Into<Bytes>>(&mut self, data: B) -> &mut Self {
|
||||
pub fn set_payload(&mut self, data: impl Into<Bytes>) -> &mut Self {
|
||||
let mut payload = crate::h1::Payload::empty();
|
||||
payload.unread_data(data.into());
|
||||
parts(&mut self.0).payload = Some(payload.into());
|
||||
@ -270,7 +264,7 @@ impl TestSeqBuffer {
|
||||
|
||||
/// Create new empty `TestBuffer` instance.
|
||||
pub fn empty() -> Self {
|
||||
Self::new("")
|
||||
Self::new(BytesMut::new())
|
||||
}
|
||||
|
||||
pub fn read_buf(&self) -> Ref<'_, BytesMut> {
|
||||
|
@ -9,7 +9,7 @@ use derive_more::{Display, Error, From};
|
||||
use http::{header, Method, StatusCode};
|
||||
|
||||
use crate::body::BoxBody;
|
||||
use crate::{header::HeaderValue, message::RequestHead, response::Response, ResponseBuilder};
|
||||
use crate::{header::HeaderValue, RequestHead, Response, ResponseBuilder};
|
||||
|
||||
mod codec;
|
||||
mod dispatcher;
|
||||
@ -99,8 +99,9 @@ impl From<HandshakeError> for Response<BoxBody> {
|
||||
match err {
|
||||
HandshakeError::GetMethodRequired => {
|
||||
let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED);
|
||||
res.headers_mut()
|
||||
.insert(header::ALLOW, HeaderValue::from_static("GET"));
|
||||
#[allow(clippy::declare_interior_mutable_const)]
|
||||
const HV_GET: HeaderValue = HeaderValue::from_static("GET");
|
||||
res.headers_mut().insert(header::ALLOW, HV_GET);
|
||||
res
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ use std::{
|
||||
io::{self, BufReader, Write},
|
||||
net::{SocketAddr, TcpStream as StdTcpStream},
|
||||
sync::Arc,
|
||||
task::Poll,
|
||||
};
|
||||
|
||||
use actix_http::{
|
||||
@ -16,25 +17,37 @@ use actix_http::{
|
||||
Error, HttpService, Method, Request, Response, StatusCode, Version,
|
||||
};
|
||||
use actix_http_test::test_server;
|
||||
use actix_rt::pin;
|
||||
use actix_service::{fn_factory_with_config, fn_service};
|
||||
use actix_tls::connect::rustls::webpki_roots_cert_store;
|
||||
use actix_utils::future::{err, ok};
|
||||
use actix_utils::future::{err, ok, poll_fn};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use derive_more::{Display, Error};
|
||||
use futures_core::Stream;
|
||||
use futures_util::stream::{once, StreamExt as _};
|
||||
use futures_core::{ready, Stream};
|
||||
use futures_util::stream::once;
|
||||
use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig, ServerName};
|
||||
use rustls_pemfile::{certs, pkcs8_private_keys};
|
||||
|
||||
async fn load_body<S>(mut stream: S) -> Result<BytesMut, PayloadError>
|
||||
async fn load_body<S>(stream: S) -> Result<BytesMut, PayloadError>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin,
|
||||
S: Stream<Item = Result<Bytes, PayloadError>>,
|
||||
{
|
||||
let mut body = BytesMut::new();
|
||||
while let Some(item) = stream.next().await {
|
||||
body.extend_from_slice(&item?)
|
||||
}
|
||||
Ok(body)
|
||||
let mut buf = BytesMut::new();
|
||||
|
||||
pin!(stream);
|
||||
|
||||
poll_fn(|cx| loop {
|
||||
let body = stream.as_mut();
|
||||
|
||||
match ready!(body.poll_next(cx)) {
|
||||
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
|
||||
None => return Poll::Ready(Ok(())),
|
||||
Some(Err(err)) => return Poll::Ready(Err(err)),
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn tls_config() -> RustlsServerConfig {
|
||||
|
@ -3,120 +3,128 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 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`.
|
||||
|
||||
|
||||
## 0.4.0-beta.10 - 2021-12-11
|
||||
* No significant changes since `0.4.0-beta.9`.
|
||||
- No significant changes since `0.4.0-beta.9`.
|
||||
|
||||
|
||||
## 0.4.0-beta.9 - 2021-12-01
|
||||
* Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463]
|
||||
- Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463]
|
||||
|
||||
[#2463]: https://github.com/actix/actix-web/pull/2463
|
||||
|
||||
|
||||
## 0.4.0-beta.8 - 2021-11-22
|
||||
* Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451]
|
||||
* Added `MultipartError::NoContentDisposition` variant. [#2451]
|
||||
* Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451]
|
||||
* Added `Field::name` method for getting the field name. [#2451]
|
||||
* `MultipartError` now marks variants with inner errors as the source. [#2451]
|
||||
* `MultipartError` is now marked as non-exhaustive. [#2451]
|
||||
- Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451]
|
||||
- Added `MultipartError::NoContentDisposition` variant. [#2451]
|
||||
- Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451]
|
||||
- Added `Field::name` method for getting the field name. [#2451]
|
||||
- `MultipartError` now marks variants with inner errors as the source. [#2451]
|
||||
- `MultipartError` is now marked as non-exhaustive. [#2451]
|
||||
|
||||
[#2451]: https://github.com/actix/actix-web/pull/2451
|
||||
|
||||
|
||||
## 0.4.0-beta.7 - 2021-10-20
|
||||
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
||||
|
||||
## 0.4.0-beta.6 - 2021-09-09
|
||||
* Minimum supported Rust version (MSRV) is now 1.51.
|
||||
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||
|
||||
|
||||
## 0.4.0-beta.5 - 2021-06-17
|
||||
* No notable changes.
|
||||
- No notable changes.
|
||||
|
||||
|
||||
## 0.4.0-beta.4 - 2021-04-02
|
||||
* No notable changes.
|
||||
- No notable changes.
|
||||
|
||||
|
||||
## 0.4.0-beta.3 - 2021-03-09
|
||||
* No notable changes.
|
||||
- No notable changes.
|
||||
|
||||
|
||||
## 0.4.0-beta.2 - 2021-02-10
|
||||
* No notable changes.
|
||||
- No notable changes.
|
||||
|
||||
|
||||
## 0.4.0-beta.1 - 2021-01-07
|
||||
* Fix multipart consuming payload before header checks. [#1513]
|
||||
* Update `bytes` to `1.0`. [#1813]
|
||||
- Fix multipart consuming payload before header checks. [#1513]
|
||||
- Update `bytes` to `1.0`. [#1813]
|
||||
|
||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||
[#1513]: https://github.com/actix/actix-web/pull/1513
|
||||
|
||||
|
||||
## 0.3.0 - 2020-09-11
|
||||
* No significant changes from `0.3.0-beta.2`.
|
||||
- No significant changes from `0.3.0-beta.2`.
|
||||
|
||||
|
||||
## 0.3.0-beta.2 - 2020-09-10
|
||||
* Update `actix-*` dependencies to latest versions.
|
||||
- Update `actix-*` dependencies to latest versions.
|
||||
|
||||
|
||||
## 0.3.0-beta.1 - 2020-07-15
|
||||
* Update `actix-web` to 3.0.0-beta.1
|
||||
- Update `actix-web` to 3.0.0-beta.1
|
||||
|
||||
|
||||
## 0.3.0-alpha.1 - 2020-05-25
|
||||
* Update `actix-web` to 3.0.0-alpha.3
|
||||
* Bump minimum supported Rust version to 1.40
|
||||
* Minimize `futures` dependencies
|
||||
* Remove the unused `time` dependency
|
||||
* Fix missing `std::error::Error` implement for `MultipartError`.
|
||||
- Update `actix-web` to 3.0.0-alpha.3
|
||||
- Bump minimum supported Rust version to 1.40
|
||||
- Minimize `futures` dependencies
|
||||
- Remove the unused `time` dependency
|
||||
- Fix missing `std::error::Error` implement for `MultipartError`.
|
||||
|
||||
## [0.2.0] - 2019-12-20
|
||||
|
||||
* Release
|
||||
- Release
|
||||
|
||||
## [0.2.0-alpha.4] - 2019-12-xx
|
||||
|
||||
* Multipart handling now handles Pending during read of boundary #1205
|
||||
- Multipart handling now handles Pending during read of boundary #1205
|
||||
|
||||
## [0.2.0-alpha.2] - 2019-12-03
|
||||
|
||||
* Migrate to `std::future`
|
||||
- Migrate to `std::future`
|
||||
|
||||
## [0.1.4] - 2019-09-12
|
||||
|
||||
* Multipart handling now parses requests which do not end in CRLF #1038
|
||||
- Multipart handling now parses requests which do not end in CRLF #1038
|
||||
|
||||
## [0.1.3] - 2019-08-18
|
||||
|
||||
* Fix ring dependency from actix-web default features for #741.
|
||||
- Fix ring dependency from actix-web default features for #741.
|
||||
|
||||
## [0.1.2] - 2019-06-02
|
||||
|
||||
* Fix boundary parsing #876
|
||||
- Fix boundary parsing #876
|
||||
|
||||
## [0.1.1] - 2019-05-25
|
||||
|
||||
* Fix disconnect handling #834
|
||||
- Fix disconnect handling #834
|
||||
|
||||
## [0.1.0] - 2019-05-18
|
||||
|
||||
* Release
|
||||
- Release
|
||||
|
||||
## [0.1.0-beta.4] - 2019-05-12
|
||||
|
||||
* Handle cancellation of uploads #736
|
||||
- Handle cancellation of uploads #736
|
||||
|
||||
* Upgrade to actix-web 1.0.0-beta.4
|
||||
- Upgrade to actix-web 1.0.0-beta.4
|
||||
|
||||
## [0.1.0-beta.1] - 2019-04-21
|
||||
|
||||
* Do not support nested multipart
|
||||
- Do not support nested multipart
|
||||
|
||||
* Split multipart support to separate crate
|
||||
- Split multipart support to separate crate
|
||||
|
||||
* Optimize multipart handling #634, #769
|
||||
- Optimize multipart handling #634, #769
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-multipart"
|
||||
version = "0.4.0-beta.10"
|
||||
version = "0.4.0-beta.12"
|
||||
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.14", default-features = false }
|
||||
actix-web = { version = "4.0.0-beta.20", 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.15"
|
||||
actix-http = "3.0.0-beta.18"
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
tokio = { version = "1", features = ["sync"] }
|
||||
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.10)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||
[](https://docs.rs/actix-multipart/0.4.0-beta.12)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-multipart/0.4.0-beta.10)
|
||||
[](https://deps.rs/crate/actix-multipart/0.4.0-beta.12)
|
||||
[](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
|
||||
|
@ -1233,7 +1233,7 @@ mod tests {
|
||||
|
||||
// and should not consume the payload
|
||||
match payload {
|
||||
actix_web::dev::Payload::H1(_) => {} //expected
|
||||
actix_web::dev::Payload::H1 { .. } => {} //expected
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,44 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
||||
|
||||
## 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]
|
||||
* 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]
|
||||
* `ResourceDef::pattern` now returns the first pattern in multi-pattern resources. [#2356]
|
||||
* Support `build_resource_path` on multi-pattern resources. [#2356]
|
||||
* Minimum supported Rust version (MSRV) is now 1.51.
|
||||
- Introduce `ResourceDef::join`. [#380]
|
||||
- Disallow prefix routes with tail segments. [#379]
|
||||
- Enforce path separators on dynamic prefixes. [#378]
|
||||
- Improve malformed path error message. [#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]
|
||||
- `ResourceDef::pattern` now returns the first pattern in multi-pattern resources. [#2356]
|
||||
- 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
|
||||
@ -25,23 +49,23 @@
|
||||
|
||||
|
||||
## 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]
|
||||
- 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]
|
||||
|
||||
[#368]: https://github.com/actix/actix-net/pull/368
|
||||
[#366]: https://github.com/actix/actix-net/pull/366
|
||||
@ -53,10 +77,10 @@
|
||||
|
||||
|
||||
## 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]
|
||||
- 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]
|
||||
|
||||
[#345]: https://github.com/actix/actix-net/pull/345
|
||||
[#357]: https://github.com/actix/actix-net/pull/357
|
||||
@ -65,68 +89,68 @@
|
||||
|
||||
|
||||
## 0.3.0 - 2019-12-31
|
||||
* Version was yanked previously. See https://crates.io/crates/actix-router/0.3.0
|
||||
- Version was yanked previously. See https://crates.io/crates/actix-router/0.3.0
|
||||
|
||||
|
||||
## 0.2.7 - 2021-02-06
|
||||
* Add `Router::recognize_checked` [#247]
|
||||
- Add `Router::recognize_checked` [#247]
|
||||
|
||||
[#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]
|
||||
|
||||
[#246]: https://github.com/actix/actix-net/pull/246
|
||||
|
||||
|
||||
## 0.2.5 - 2020-09-20
|
||||
* Fix `from_hex()` method
|
||||
- Fix `from_hex()` method
|
||||
|
||||
|
||||
## 0.2.4 - 2019-12-31
|
||||
* Add `ResourceDef::resource_path_named()` path generation method
|
||||
- Add `ResourceDef::resource_path_named()` path generation method
|
||||
|
||||
|
||||
## 0.2.3 - 2019-12-25
|
||||
* Add impl `IntoPattern` for `&String`
|
||||
- Add impl `IntoPattern` for `&String`
|
||||
|
||||
|
||||
## 0.2.2 - 2019-12-25
|
||||
* Use `IntoPattern` for `RouterBuilder::path()`
|
||||
- Use `IntoPattern` for `RouterBuilder::path()`
|
||||
|
||||
|
||||
## 0.2.1 - 2019-12-25
|
||||
* Add `IntoPattern` trait
|
||||
* Add multi-pattern resources
|
||||
- Add `IntoPattern` trait
|
||||
- Add multi-pattern resources
|
||||
|
||||
|
||||
## 0.2.0 - 2019-12-07
|
||||
* Update http to 0.2
|
||||
* Update regex to 1.3
|
||||
* Use bytestring instead of string
|
||||
- Update http to 0.2
|
||||
- Update regex to 1.3
|
||||
- Use bytestring instead of string
|
||||
|
||||
|
||||
## 0.1.5 - 2019-05-15
|
||||
* Remove debug prints
|
||||
- Remove debug prints
|
||||
|
||||
|
||||
## 0.1.4 - 2019-05-15
|
||||
* Fix checked resource match
|
||||
- Fix checked resource match
|
||||
|
||||
|
||||
## 0.1.3 - 2019-04-22
|
||||
* Added support for `remainder match` (i.e "/path/{tail}*")
|
||||
- Added support for `remainder match` (i.e "/path/{tail}*")
|
||||
|
||||
|
||||
## 0.1.2 - 2019-04-07
|
||||
* Export `Quoter` type
|
||||
* Allow to reset `Path` instance
|
||||
- Export `Quoter` type
|
||||
- Allow to reset `Path` instance
|
||||
|
||||
|
||||
## 0.1.1 - 2019-04-03
|
||||
* Get dynamic segment by name instead of iterator.
|
||||
- Get dynamic segment by name instead of iterator.
|
||||
|
||||
|
||||
## 0.1.0 - 2019-03-09
|
||||
* Initial release
|
||||
- Initial release
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-router"
|
||||
version = "0.5.0-beta.2"
|
||||
version = "0.5.0-rc.2"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",
|
||||
@ -21,7 +21,7 @@ default = ["http"]
|
||||
|
||||
[dependencies]
|
||||
bytestring = ">=0.1.5, <2"
|
||||
firestorm = "0.4"
|
||||
firestorm = "0.5"
|
||||
http = { version = "0.2.3", optional = true }
|
||||
log = "0.4"
|
||||
regex = "1.5"
|
||||
@ -29,7 +29,7 @@ serde = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
firestorm = { version = "0.4", features = ["enable_system_time"] }
|
||||
firestorm = { version = "0.5", features = ["enable_system_time"] }
|
||||
http = "0.2.5"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
|
@ -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(self.value.as_bytes()))
|
||||
.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(self.value.as_bytes())) {
|
||||
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(self.value.as_bytes())) {
|
||||
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();
|
||||
|
@ -7,144 +7,22 @@
|
||||
|
||||
mod de;
|
||||
mod path;
|
||||
mod pattern;
|
||||
mod quoter;
|
||||
mod resource;
|
||||
mod resource_path;
|
||||
mod router;
|
||||
|
||||
pub use self::de::PathDeserializer;
|
||||
pub use self::path::Path;
|
||||
pub use self::resource::ResourceDef;
|
||||
pub use self::router::{ResourceInfo, Router, RouterBuilder};
|
||||
|
||||
// TODO: this trait is necessary, document it
|
||||
// see impl Resource for ServiceRequest
|
||||
pub trait Resource<T: ResourcePath> {
|
||||
fn resource_path(&mut self) -> &mut Path<T>;
|
||||
}
|
||||
|
||||
pub trait ResourcePath {
|
||||
fn path(&self) -> &str;
|
||||
}
|
||||
|
||||
impl ResourcePath for String {
|
||||
fn path(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ResourcePath for &'a str {
|
||||
fn path(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourcePath for bytestring::ByteString {
|
||||
fn path(&self) -> &str {
|
||||
&*self
|
||||
}
|
||||
}
|
||||
|
||||
/// One or many patterns.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Patterns {
|
||||
Single(String),
|
||||
List(Vec<String>),
|
||||
}
|
||||
|
||||
impl Patterns {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Patterns::Single(_) => false,
|
||||
Patterns::List(pats) => pats.is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper trait for type that could be converted to one or more path pattern.
|
||||
pub trait IntoPatterns {
|
||||
fn patterns(&self) -> Patterns;
|
||||
}
|
||||
|
||||
impl IntoPatterns for String {
|
||||
fn patterns(&self) -> Patterns {
|
||||
Patterns::Single(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoPatterns for &'a String {
|
||||
fn patterns(&self) -> Patterns {
|
||||
Patterns::Single((*self).clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoPatterns for &'a str {
|
||||
fn patterns(&self) -> Patterns {
|
||||
Patterns::Single((*self).to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPatterns for bytestring::ByteString {
|
||||
fn patterns(&self) -> Patterns {
|
||||
Patterns::Single(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPatterns for Patterns {
|
||||
fn patterns(&self) -> Patterns {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> IntoPatterns for Vec<T> {
|
||||
fn patterns(&self) -> Patterns {
|
||||
let mut patterns = self.iter().map(|v| v.as_ref().to_owned());
|
||||
|
||||
match patterns.size_hint() {
|
||||
(1, _) => Patterns::Single(patterns.next().unwrap()),
|
||||
_ => Patterns::List(patterns.collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! array_patterns_single (($tp:ty) => {
|
||||
impl IntoPatterns for [$tp; 1] {
|
||||
fn patterns(&self) -> Patterns {
|
||||
Patterns::Single(self[0].to_owned())
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
macro_rules! array_patterns_multiple (($tp:ty, $str_fn:expr, $($num:tt) +) => {
|
||||
// for each array length specified in $num
|
||||
$(
|
||||
impl IntoPatterns for [$tp; $num] {
|
||||
fn patterns(&self) -> Patterns {
|
||||
Patterns::List(self.iter().map($str_fn).collect())
|
||||
}
|
||||
}
|
||||
)+
|
||||
});
|
||||
|
||||
array_patterns_single!(&str);
|
||||
array_patterns_multiple!(&str, |&v| v.to_owned(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16);
|
||||
|
||||
array_patterns_single!(String);
|
||||
array_patterns_multiple!(String, |v| v.clone(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16);
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
mod url;
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
pub use self::url::{Quoter, 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};
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
mod http_impls {
|
||||
use http::Uri;
|
||||
|
||||
use super::ResourcePath;
|
||||
|
||||
impl ResourcePath for Uri {
|
||||
fn path(&self) -> &str {
|
||||
self.path()
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
92
actix-router/src/pattern.rs
Normal file
92
actix-router/src/pattern.rs
Normal file
@ -0,0 +1,92 @@
|
||||
/// One or many patterns.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Patterns {
|
||||
Single(String),
|
||||
List(Vec<String>),
|
||||
}
|
||||
|
||||
impl Patterns {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Patterns::Single(_) => false,
|
||||
Patterns::List(pats) => pats.is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper trait for type that could be converted to one or more path patterns.
|
||||
pub trait IntoPatterns {
|
||||
fn patterns(&self) -> Patterns;
|
||||
}
|
||||
|
||||
impl IntoPatterns for String {
|
||||
fn patterns(&self) -> Patterns {
|
||||
Patterns::Single(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPatterns for &String {
|
||||
fn patterns(&self) -> Patterns {
|
||||
(*self).patterns()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPatterns for str {
|
||||
fn patterns(&self) -> Patterns {
|
||||
Patterns::Single(self.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPatterns for &str {
|
||||
fn patterns(&self) -> Patterns {
|
||||
(*self).patterns()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPatterns for bytestring::ByteString {
|
||||
fn patterns(&self) -> Patterns {
|
||||
Patterns::Single(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPatterns for Patterns {
|
||||
fn patterns(&self) -> Patterns {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> IntoPatterns for Vec<T> {
|
||||
fn patterns(&self) -> Patterns {
|
||||
let mut patterns = self.iter().map(|v| v.as_ref().to_owned());
|
||||
|
||||
match patterns.size_hint() {
|
||||
(1, _) => Patterns::Single(patterns.next().unwrap()),
|
||||
_ => Patterns::List(patterns.collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! array_patterns_single (($tp:ty) => {
|
||||
impl IntoPatterns for [$tp; 1] {
|
||||
fn patterns(&self) -> Patterns {
|
||||
Patterns::Single(self[0].to_owned())
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
macro_rules! array_patterns_multiple (($tp:ty, $str_fn:expr, $($num:tt) +) => {
|
||||
// for each array length specified in space-separated $num
|
||||
$(
|
||||
impl IntoPatterns for [$tp; $num] {
|
||||
fn patterns(&self) -> Patterns {
|
||||
Patterns::List(self.iter().map($str_fn).collect())
|
||||
}
|
||||
}
|
||||
)+
|
||||
});
|
||||
|
||||
array_patterns_single!(&str);
|
||||
array_patterns_multiple!(&str, |&v| v.to_owned(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16);
|
||||
|
||||
array_patterns_single!(String);
|
||||
array_patterns_multiple!(String, |v| v.clone(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16);
|
219
actix-router/src/quoter.rs
Normal file
219
actix-router/src/quoter.rs
Normal file
@ -0,0 +1,219 @@
|
||||
#[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
|
||||
}
|
||||
|
||||
/// Re-quotes... ?
|
||||
///
|
||||
/// Returns `None` when no modification to the original string was required.
|
||||
pub fn requote(&self, val: &[u8]) -> Option<String> {
|
||||
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.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(), "/a%c");
|
||||
assert_eq!(q.requote(b"/a%2Bc").unwrap(), "/a%2Bc");
|
||||
|
||||
let q = Quoter::new(b"%+", b"/");
|
||||
assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), "/a%b+c");
|
||||
assert_eq!(q.requote(b"/a%2fb").unwrap(), "/a%2fb");
|
||||
assert_eq!(q.requote(b"/a%2Fb").unwrap(), "/a%2Fb");
|
||||
assert_eq!(q.requote(b"/a%0Ab").unwrap(), "/a\nb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoter_no_modification() {
|
||||
let q = Quoter::new(b"", b"");
|
||||
assert_eq!(q.requote(b"/abc/../efg"), None);
|
||||
}
|
||||
}
|
@ -29,26 +29,25 @@ const REGEX_FLAGS: &str = "(?s-m)";
|
||||
///
|
||||
///
|
||||
/// # Pattern Format and Matching Behavior
|
||||
///
|
||||
/// Resource pattern is defined as a string of zero or more _segments_ where each segment is
|
||||
/// preceded by a slash `/`.
|
||||
///
|
||||
/// This means that pattern string __must__ either be empty or begin with a slash (`/`).
|
||||
/// This also implies that a trailing slash in pattern defines an empty segment.
|
||||
/// For example, the pattern `"/user/"` has two segments: `["user", ""]`
|
||||
/// This means that pattern string __must__ either be empty or begin with a slash (`/`). This also
|
||||
/// implies that a trailing slash in pattern defines an empty segment. For example, the pattern
|
||||
/// `"/user/"` has two segments: `["user", ""]`
|
||||
///
|
||||
/// A key point to underhand is that `ResourceDef` matches segments, not strings.
|
||||
/// It matches segments individually.
|
||||
/// For example, the pattern `/user/` is not considered a prefix for the path `/user/123/456`,
|
||||
/// because the second segment doesn't match: `["user", ""]` vs `["user", "123", "456"]`.
|
||||
/// A key point to understand is that `ResourceDef` matches segments, not strings. Segments are
|
||||
/// matched individually. For example, the pattern `/user/` is not considered a prefix for the path
|
||||
/// `/user/123/456`, because the second segment doesn't match: `["user", ""]`
|
||||
/// vs `["user", "123", "456"]`.
|
||||
///
|
||||
/// This definition is consistent with the definition of absolute URL path in
|
||||
/// [RFC 3986 (section 3.3)](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)
|
||||
/// [RFC 3986 §3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)
|
||||
///
|
||||
///
|
||||
/// # Static Resources
|
||||
/// A static resource is the most basic type of definition. Pass a pattern to
|
||||
/// [new][Self::new]. Conforming paths must match the pattern exactly.
|
||||
/// A static resource is the most basic type of definition. Pass a pattern to [new][Self::new].
|
||||
/// Conforming paths must match the pattern exactly.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
@ -63,7 +62,6 @@ const REGEX_FLAGS: &str = "(?s-m)";
|
||||
/// assert!(!resource.is_match("/search"));
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// # Dynamic Segments
|
||||
/// Also known as "path parameters". Resources can define sections of a pattern that be extracted
|
||||
/// from a conforming path, if it conforms to (one of) the resource pattern(s).
|
||||
@ -102,15 +100,15 @@ const REGEX_FLAGS: &str = "(?s-m)";
|
||||
/// assert_eq!(path.get("id").unwrap(), "123");
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// # Prefix Resources
|
||||
/// A prefix resource is defined as pattern that can match just the start of a path, up to a
|
||||
/// segment boundary.
|
||||
///
|
||||
/// Prefix patterns with a trailing slash may have an unexpected, though correct, behavior.
|
||||
/// They define and therefore require an empty segment in order to match. Examples are given below.
|
||||
/// They define and therefore require an empty segment in order to match. It is easier to understand
|
||||
/// this behavior after reading the [matching behavior section]. Examples are given below.
|
||||
///
|
||||
/// Empty pattern matches any path as a prefix.
|
||||
/// The empty pattern (`""`), as a prefix, matches any path.
|
||||
///
|
||||
/// Prefix resources can contain dynamic segments.
|
||||
///
|
||||
@ -130,7 +128,6 @@ const REGEX_FLAGS: &str = "(?s-m)";
|
||||
/// assert!(!resource.is_match("/user/123"));
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// # Custom Regex Segments
|
||||
/// Dynamic segments can be customised to only match a specific regular expression. It can be
|
||||
/// helpful to do this if resource definitions would otherwise conflict and cause one to
|
||||
@ -158,7 +155,6 @@ const REGEX_FLAGS: &str = "(?s-m)";
|
||||
/// assert!(!resource.is_match("/user/abc"));
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// # Tail Segments
|
||||
/// As a shortcut to defining a custom regex for matching _all_ remaining characters (not just those
|
||||
/// up until a `/` character), there is a special pattern to match (and capture) the remaining
|
||||
@ -179,7 +175,6 @@ const REGEX_FLAGS: &str = "(?s-m)";
|
||||
/// assert_eq!(path.get("tail").unwrap(), "main/LICENSE");
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// # Multi-Pattern Resources
|
||||
/// For resources that can map to multiple distinct paths, it may be suitable to use
|
||||
/// multi-pattern resources by passing an array/vec to [`new`][Self::new]. They will be combined
|
||||
@ -198,7 +193,6 @@ const REGEX_FLAGS: &str = "(?s-m)";
|
||||
/// assert!(resource.is_match("/index"));
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// # Trailing Slashes
|
||||
/// It should be noted that this library takes no steps to normalize intra-path or trailing slashes.
|
||||
/// As such, all resource definitions implicitly expect a pre-processing step to normalize paths if
|
||||
@ -212,6 +206,8 @@ const REGEX_FLAGS: &str = "(?s-m)";
|
||||
/// assert!(!ResourceDef::new("/root/").is_match("/root"));
|
||||
/// assert!(!ResourceDef::prefix("/root/").is_match("/root"));
|
||||
/// ```
|
||||
///
|
||||
/// [matching behavior section]: #pattern-format-and-matching-behavior
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ResourceDef {
|
||||
id: u16,
|
||||
@ -279,7 +275,7 @@ impl ResourceDef {
|
||||
/// ```
|
||||
pub fn new<T: IntoPatterns>(paths: T) -> Self {
|
||||
profile_method!(new);
|
||||
Self::new2(paths, false)
|
||||
Self::construct(paths, false)
|
||||
}
|
||||
|
||||
/// Constructs a new resource definition using a pattern that performs prefix matching.
|
||||
@ -292,7 +288,7 @@ impl ResourceDef {
|
||||
/// resource definition with a tail segment; use [`new`][Self::new] in this case.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if path regex pattern is malformed.
|
||||
/// Panics if path pattern is malformed.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
@ -307,14 +303,14 @@ impl ResourceDef {
|
||||
/// ```
|
||||
pub fn prefix<T: IntoPatterns>(paths: T) -> Self {
|
||||
profile_method!(prefix);
|
||||
ResourceDef::new2(paths, true)
|
||||
ResourceDef::construct(paths, true)
|
||||
}
|
||||
|
||||
/// Constructs a new resource definition using a string pattern that performs prefix matching,
|
||||
/// inserting a `/` to beginning of the pattern if absent and pattern is not empty.
|
||||
/// ensuring a leading `/` if pattern is not empty.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if path regex pattern is malformed.
|
||||
/// Panics if path pattern is malformed.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
@ -515,8 +511,8 @@ impl ResourceDef {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match patterns.len() {
|
||||
1 => ResourceDef::new2(&patterns[0], other.is_prefix()),
|
||||
_ => ResourceDef::new2(patterns, other.is_prefix()),
|
||||
1 => ResourceDef::construct(&patterns[0], other.is_prefix()),
|
||||
_ => ResourceDef::construct(patterns, other.is_prefix()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -682,22 +678,21 @@ impl ResourceDef {
|
||||
/// assert!(!try_match(&resource, &mut path));
|
||||
/// assert_eq!(path.unprocessed(), "/user/admin/stars");
|
||||
/// ```
|
||||
pub fn capture_match_info_fn<R, T, F, U>(
|
||||
pub fn capture_match_info_fn<R, F, U>(
|
||||
&self,
|
||||
resource: &mut R,
|
||||
check_fn: F,
|
||||
user_data: U,
|
||||
) -> bool
|
||||
where
|
||||
R: Resource<T>,
|
||||
T: ResourcePath,
|
||||
R: Resource,
|
||||
F: FnOnce(&R, U) -> bool,
|
||||
{
|
||||
profile_method!(capture_match_info_fn);
|
||||
|
||||
let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default();
|
||||
let path = resource.resource_path();
|
||||
let path_str = path.path();
|
||||
let path_str = path.unprocessed();
|
||||
|
||||
let (matched_len, matched_vars) = match &self.pat_type {
|
||||
PatternType::Static(pattern) => {
|
||||
@ -715,7 +710,7 @@ impl ResourceDef {
|
||||
let captures = {
|
||||
profile_section!(pattern_dynamic_regex_exec);
|
||||
|
||||
match re.captures(path.path()) {
|
||||
match re.captures(path.unprocessed()) {
|
||||
Some(captures) => captures,
|
||||
_ => return false,
|
||||
}
|
||||
@ -743,7 +738,7 @@ impl ResourceDef {
|
||||
PatternType::DynamicSet(re, params) => {
|
||||
profile_section!(pattern_dynamic_set);
|
||||
|
||||
let path = path.path();
|
||||
let path = path.unprocessed();
|
||||
let (pattern, names) = match re.matches(path).into_iter().next() {
|
||||
Some(idx) => ¶ms[idx],
|
||||
_ => return false,
|
||||
@ -881,8 +876,8 @@ impl ResourceDef {
|
||||
}
|
||||
}
|
||||
|
||||
fn new2<T: IntoPatterns>(paths: T, is_prefix: bool) -> Self {
|
||||
profile_method!(new2);
|
||||
fn construct<T: IntoPatterns>(paths: T, is_prefix: bool) -> Self {
|
||||
profile_method!(construct);
|
||||
|
||||
let patterns = paths.patterns();
|
||||
let (pat_type, segments) = match &patterns {
|
||||
@ -1814,7 +1809,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn prefix_plus_tail_match_is_allowed() {
|
||||
fn prefix_plus_tail_match_disallowed() {
|
||||
ResourceDef::prefix("/user/{id}*");
|
||||
}
|
||||
}
|
||||
|
39
actix-router/src/resource_path.rs
Normal file
39
actix-router/src/resource_path.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use crate::Path;
|
||||
|
||||
// TODO: this trait is necessary, document it
|
||||
// see impl Resource for ServiceRequest
|
||||
pub trait Resource {
|
||||
/// Type of resource's path returned in `resource_path`.
|
||||
type Path: ResourcePath;
|
||||
|
||||
fn resource_path(&mut self) -> &mut Path<Self::Path>;
|
||||
}
|
||||
|
||||
pub trait ResourcePath {
|
||||
fn path(&self) -> &str;
|
||||
}
|
||||
|
||||
impl ResourcePath for String {
|
||||
fn path(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ResourcePath for &'a str {
|
||||
fn path(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourcePath for bytestring::ByteString {
|
||||
fn path(&self) -> &str {
|
||||
&*self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
impl ResourcePath for http::Uri {
|
||||
fn path(&self) -> &str {
|
||||
self.path()
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
use firestorm::profile_method;
|
||||
|
||||
use crate::{IntoPatterns, Resource, ResourceDef, ResourcePath};
|
||||
use crate::{IntoPatterns, Resource, ResourceDef};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct ResourceId(pub u16);
|
||||
@ -26,10 +26,9 @@ impl<T, U> Router<T, U> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recognize<R, P>(&self, resource: &mut R) -> Option<(&T, ResourceId)>
|
||||
pub fn recognize<R>(&self, resource: &mut R) -> Option<(&T, ResourceId)>
|
||||
where
|
||||
R: Resource<P>,
|
||||
P: ResourcePath,
|
||||
R: Resource,
|
||||
{
|
||||
profile_method!(recognize);
|
||||
|
||||
@ -42,10 +41,9 @@ impl<T, U> Router<T, U> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn recognize_mut<R, P>(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)>
|
||||
pub fn recognize_mut<R>(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)>
|
||||
where
|
||||
R: Resource<P>,
|
||||
P: ResourcePath,
|
||||
R: Resource,
|
||||
{
|
||||
profile_method!(recognize_mut);
|
||||
|
||||
@ -58,11 +56,10 @@ impl<T, U> Router<T, U> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn recognize_fn<R, P, F>(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)>
|
||||
pub fn recognize_fn<R, F>(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)>
|
||||
where
|
||||
F: Fn(&R, &Option<U>) -> bool,
|
||||
R: Resource<P>,
|
||||
P: ResourcePath,
|
||||
R: Resource,
|
||||
{
|
||||
profile_method!(recognize_checked);
|
||||
|
||||
@ -75,15 +72,14 @@ impl<T, U> Router<T, U> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn recognize_mut_fn<R, P, F>(
|
||||
pub fn recognize_mut_fn<R, F>(
|
||||
&mut self,
|
||||
resource: &mut R,
|
||||
check: F,
|
||||
) -> Option<(&mut T, ResourceId)>
|
||||
where
|
||||
F: Fn(&R, &Option<U>) -> bool,
|
||||
R: Resource<P>,
|
||||
P: ResourcePath,
|
||||
R: Resource,
|
||||
{
|
||||
profile_method!(recognize_mut_checked);
|
||||
|
||||
@ -260,6 +256,7 @@ mod tests {
|
||||
router.path("/name/{val}", 11);
|
||||
let mut router = router.finish();
|
||||
|
||||
// test skip beyond path length
|
||||
let mut path = Path::new("/name");
|
||||
path.skip(6);
|
||||
assert!(router.recognize_mut(&mut path).is_none());
|
||||
|
@ -1,68 +1,44 @@
|
||||
use crate::ResourcePath;
|
||||
|
||||
#[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";
|
||||
|
||||
#[inline]
|
||||
fn bit_at(array: &[u8], ch: u8) -> bool {
|
||||
array[(ch >> 3) as usize] & (1 << (ch & 7)) != 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_bit(array: &mut [u8], ch: u8) {
|
||||
array[(ch >> 3) as usize] |= 1 << (ch & 7)
|
||||
}
|
||||
use crate::Quoter;
|
||||
|
||||
thread_local! {
|
||||
static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+");
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Url {
|
||||
uri: http::Uri,
|
||||
path: Option<String>,
|
||||
}
|
||||
|
||||
impl Url {
|
||||
#[inline]
|
||||
pub fn new(uri: http::Uri) -> Url {
|
||||
let path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes()));
|
||||
|
||||
Url { uri, path }
|
||||
}
|
||||
|
||||
pub fn with_quoter(uri: http::Uri, quoter: &Quoter) -> Url {
|
||||
#[inline]
|
||||
pub fn new_with_quoter(uri: http::Uri, quoter: &Quoter) -> Url {
|
||||
Url {
|
||||
path: quoter.requote(uri.path().as_bytes()),
|
||||
uri,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns URI.
|
||||
#[inline]
|
||||
pub fn uri(&self) -> &http::Uri {
|
||||
&self.uri
|
||||
}
|
||||
|
||||
/// Returns path.
|
||||
#[inline]
|
||||
pub fn path(&self) -> &str {
|
||||
if let Some(ref s) = self.path {
|
||||
s
|
||||
} else {
|
||||
self.uri.path()
|
||||
match self.path {
|
||||
Some(ref path) => path,
|
||||
_ => self.uri.path(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,112 +62,6 @@ impl ResourcePath for Url {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Quoter {
|
||||
safe_table: [u8; 16],
|
||||
protected_table: [u8; 16],
|
||||
}
|
||||
|
||||
impl Quoter {
|
||||
pub fn new(safe: &[u8], protected: &[u8]) -> Quoter {
|
||||
let mut q = Quoter {
|
||||
safe_table: [0; 16],
|
||||
protected_table: [0; 16],
|
||||
};
|
||||
|
||||
// prepare safe table
|
||||
for i in 0..128 {
|
||||
if ALLOWED.contains(&i) {
|
||||
set_bit(&mut q.safe_table, i);
|
||||
}
|
||||
if QS.contains(&i) {
|
||||
set_bit(&mut q.safe_table, i);
|
||||
}
|
||||
}
|
||||
|
||||
for ch in safe {
|
||||
set_bit(&mut q.safe_table, *ch)
|
||||
}
|
||||
|
||||
// prepare protected table
|
||||
for ch in protected {
|
||||
set_bit(&mut q.safe_table, *ch);
|
||||
set_bit(&mut q.protected_table, *ch);
|
||||
}
|
||||
|
||||
q
|
||||
}
|
||||
|
||||
pub fn requote(&self, val: &[u8]) -> Option<String> {
|
||||
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) = restore_ch(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.map(|data| String::from_utf8_lossy(&data).into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn from_hex(v: u8) -> Option<u8> {
|
||||
if (b'0'..=b'9').contains(&v) {
|
||||
Some(v - 0x30) // ord('0') == 0x30
|
||||
} else if (b'A'..=b'F').contains(&v) {
|
||||
Some(v - 0x41 + 10) // ord('A') == 0x41
|
||||
} else if (b'a'..=b'f').contains(&v) {
|
||||
Some(v - 0x61 + 10) // ord('a') == 0x61
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn restore_ch(d1: u8, d2: u8) -> Option<u8> {
|
||||
from_hex(d1).and_then(|d1| from_hex(d2).map(move |d2| d1 << 4 | d2))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use http::Uri;
|
||||
@ -215,11 +85,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_url() {
|
||||
fn parse_url() {
|
||||
let re = "/user/{id}/test";
|
||||
|
||||
let path = match_url(re, "/user/2345/test");
|
||||
assert_eq!(path.get("id").unwrap(), "2345");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protected_chars() {
|
||||
let re = "/user/{id}/test";
|
||||
|
||||
let encoded = percent_encode(PROTECTED);
|
||||
let path = match_url(re, format!("/user/{}/test", encoded));
|
||||
// characters in captured segment remain unencoded
|
||||
assert_eq!(path.get("id").unwrap(), &encoded);
|
||||
|
||||
// "%25" should never be decoded into '%' to guarantee the output is a valid
|
||||
// percent-encoded format
|
||||
@ -231,24 +111,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_protected_chars() {
|
||||
let encoded = percent_encode(PROTECTED);
|
||||
let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded));
|
||||
assert_eq!(path.get("id").unwrap(), &encoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_protecteed_ascii() {
|
||||
let nonprotected_ascii = ('\u{0}'..='\u{7F}')
|
||||
fn non_protected_ascii() {
|
||||
let non_protected_ascii = ('\u{0}'..='\u{7F}')
|
||||
.filter(|&c| c.is_ascii() && !PROTECTED.contains(&(c as u8)))
|
||||
.collect::<String>();
|
||||
let encoded = percent_encode(nonprotected_ascii.as_bytes());
|
||||
let encoded = percent_encode(non_protected_ascii.as_bytes());
|
||||
let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded));
|
||||
assert_eq!(path.get("id").unwrap(), &nonprotected_ascii);
|
||||
assert_eq!(path.get("id").unwrap(), &non_protected_ascii);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_utf8_multibyte() {
|
||||
fn valid_utf8_multi_byte() {
|
||||
let test = ('\u{FF00}'..='\u{FFFF}').collect::<String>();
|
||||
let encoded = percent_encode(test.as_bytes());
|
||||
let path = match_url("/a/{id}/b", format!("/a/{}/b", &encoded));
|
||||
@ -256,33 +129,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_utf8() {
|
||||
fn invalid_utf8() {
|
||||
let invalid_utf8 = percent_encode((0x80..=0xff).collect::<Vec<_>>().as_slice());
|
||||
let uri = Uri::try_from(format!("/{}", invalid_utf8)).unwrap();
|
||||
let path = Path::new(Url::new(uri));
|
||||
|
||||
// We should always get a valid utf8 string
|
||||
assert!(String::from_utf8(path.path().as_bytes().to_owned()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_hex() {
|
||||
let hex = b"0123456789abcdefABCDEF";
|
||||
|
||||
for i in 0..256 {
|
||||
let c = i as u8;
|
||||
if hex.contains(&c) {
|
||||
assert!(from_hex(c).is_some())
|
||||
} else {
|
||||
assert!(from_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_hex(hex[i]).unwrap(), expected[i]);
|
||||
}
|
||||
assert!(String::from_utf8(path.as_str().as_bytes().to_owned()).is_ok());
|
||||
}
|
||||
}
|
||||
|
@ -3,40 +3,55 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.1.0-beta.11 - 2022-01-04
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
|
||||
## 0.1.0-beta.10 - 2021-12-27
|
||||
- No significant changes since `0.1.0-beta.9`.
|
||||
|
||||
|
||||
## 0.1.0-beta.9 - 2021-12-17
|
||||
- Re-export `actix_http::body::to_bytes`. [#2518]
|
||||
- Update `actix_web::test` re-exports. [#2518]
|
||||
|
||||
[#2518]: https://github.com/actix/actix-web/pull/2518
|
||||
|
||||
|
||||
## 0.1.0-beta.8 - 2021-12-11
|
||||
* No significant changes since `0.1.0-beta.7`.
|
||||
- No significant changes since `0.1.0-beta.7`.
|
||||
|
||||
|
||||
## 0.1.0-beta.7 - 2021-11-22
|
||||
* Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
|
||||
- Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
|
||||
|
||||
[#2408]: https://github.com/actix/actix-web/pull/2408
|
||||
|
||||
|
||||
## 0.1.0-beta.6 - 2021-11-15
|
||||
* No significant changes from `0.1.0-beta.5`.
|
||||
- No significant changes from `0.1.0-beta.5`.
|
||||
|
||||
|
||||
## 0.1.0-beta.5 - 2021-10-20
|
||||
* Updated rustls to v0.20. [#2414]
|
||||
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||
- Updated rustls to v0.20. [#2414]
|
||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
||||
[#2414]: https://github.com/actix/actix-web/pull/2414
|
||||
|
||||
|
||||
## 0.1.0-beta.4 - 2021-09-09
|
||||
* Minimum supported Rust version (MSRV) is now 1.51.
|
||||
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||
|
||||
|
||||
## 0.1.0-beta.3 - 2021-06-20
|
||||
* No significant changes from `0.1.0-beta.2`.
|
||||
- No significant changes from `0.1.0-beta.2`.
|
||||
|
||||
|
||||
## 0.1.0-beta.2 - 2021-04-17
|
||||
* No significant changes from `0.1.0-beta.1`.
|
||||
- No significant changes from `0.1.0-beta.1`.
|
||||
|
||||
|
||||
## 0.1.0-beta.1 - 2021-04-02
|
||||
* Move integration testing structs from `actix-web`. [#2112]
|
||||
- Move integration testing structs from `actix-web`. [#2112]
|
||||
|
||||
[#2112]: https://github.com/actix/actix-web/pull/2112
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-test"
|
||||
version = "0.1.0-beta.8"
|
||||
version = "0.1.0-beta.11"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.4.1"
|
||||
actix-http = "3.0.0-beta.15"
|
||||
actix-http-test = "3.0.0-beta.9"
|
||||
actix-http = "3.0.0-beta.18"
|
||||
actix-http-test = "3.0.0-beta.11"
|
||||
actix-rt = "2.1"
|
||||
actix-service = "2.0.0"
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["cookies"] }
|
||||
awc = { version = "3.0.0-beta.13", default-features = false, features = ["cookies"] }
|
||||
actix-web = { version = "4.0.0-beta.20", default-features = false, features = ["cookies"] }
|
||||
awc = { version = "3.0.0-beta.18", default-features = false, features = ["cookies"] }
|
||||
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = [] }
|
||||
@ -45,4 +45,4 @@ serde_json = "1"
|
||||
serde_urlencoded = "0.7"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0", optional = true }
|
||||
tokio = { version = "1.2", features = ["sync"] }
|
||||
tokio = { version = "1.8.4", features = ["sync"] }
|
||||
|
@ -37,9 +37,14 @@ extern crate tls_rustls as rustls;
|
||||
use std::{fmt, net, thread, time::Duration};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
pub use actix_http::test::TestBuffer;
|
||||
pub use actix_http::{body::to_bytes, test::TestBuffer};
|
||||
use actix_http::{header::HeaderMap, ws, HttpService, Method, Request, Response};
|
||||
pub use actix_http_test::unused_addr;
|
||||
use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _};
|
||||
pub use actix_web::test::{
|
||||
call_and_read_body, call_and_read_body_json, call_service, init_service, ok_service,
|
||||
read_body, read_body_json, simple_service, TestRequest,
|
||||
};
|
||||
use actix_web::{
|
||||
body::MessageBody,
|
||||
dev::{AppConfig, Server, ServerHandle, Service},
|
||||
@ -48,12 +53,6 @@ use actix_web::{
|
||||
};
|
||||
use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector};
|
||||
use futures_core::Stream;
|
||||
|
||||
pub use actix_http_test::unused_addr;
|
||||
pub use actix_web::test::{
|
||||
call_service, default_service, init_service, load_stream, ok_service, read_body,
|
||||
read_body_json, read_response, read_response_json, TestRequest,
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Start default [`TestServer`].
|
||||
@ -341,7 +340,7 @@ where
|
||||
Connector::new()
|
||||
.conn_lifetime(Duration::from_secs(0))
|
||||
.timeout(Duration::from_millis(30000))
|
||||
.ssl(builder.build())
|
||||
.openssl(builder.build())
|
||||
}
|
||||
#[cfg(not(feature = "openssl"))]
|
||||
{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user