1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-03 17:41:30 +02:00

Compare commits

...

63 Commits

Author SHA1 Message Date
9668a2396f prepare actix-router release 0.5.0-rc.2 2022-01-21 17:21:46 +00:00
cb7347216c add Logger::log_target (#2594) 2022-01-21 17:19:17 +00:00
ae7f71e317 remove ambiguous HttpResponseBuilder::del_cookie (#2591) 2022-01-21 17:18:07 +00:00
bc89f0bfc2 s/example/examples 2022-01-21 16:56:33 +00:00
c959916346 fmt codegen 2022-01-20 01:54:57 +00:00
f227e880d7 refactor route codegen to be cleaner 2022-01-20 01:53:02 +00:00
68ad81f989 remove debug logs 2022-01-20 01:30:33 +00:00
f2e736719a add url_for test for conflicting named resources 2022-01-20 01:30:33 +00:00
81ef12a0fd add warn log to from_parts if given request is cloned
closes #2562
2022-01-19 22:23:53 +00:00
1bc1538118 use tokio::main in client example 2022-01-19 21:36:14 +00:00
1cc3e7b24c deprecate Path::path (#2590) 2022-01-19 20:26:33 +00:00
3dd98c308c document Path::unprocessed panic 2022-01-19 18:33:23 +00:00
cb5d9a7e64 bump deps to stable actix-server v2 2022-01-19 16:58:11 +00:00
5ee555462f add HttpResponse::add_removal_cookie (#2586) 2022-01-19 16:36:11 +00:00
ad159f5219 fix ClientResponse::body doc
fixes #2589
2022-01-19 15:52:16 +00:00
2ffc21dd4f move response extensions out of head (#2585) 2022-01-19 02:09:25 +00:00
7b8a392ef5 allow camel case response headers (#2587) 2022-01-16 03:16:26 +00:00
3c7ccf5521 update http changelog 2022-01-15 15:43:18 +00:00
e7cae5a95b migrate to brotli crate (#2538) 2022-01-15 14:03:16 +00:00
455d5c460d prepare actix-files release 0.6.0-beta.14 2022-01-14 20:01:11 +00:00
8faca783fa prepare actix-web release 4.0.0-beta.20 2022-01-14 20:00:26 +00:00
edbb9b047e prepare actix-router release 0.5.0-rc.1 2022-01-14 19:59:36 +00:00
32742d0715 support opaque app in test helpers (#2584) 2022-01-14 19:45:32 +00:00
d90c1a2331 convert error in Result extractor (#2581)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-12 18:59:22 +00:00
2a12b41456 fix support for 12 extractors (#2582)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-12 18:31:48 +00:00
6c97d448b7 update tokio-uring to 0.2 (#2583) 2022-01-12 17:53:36 +00:00
c3ce33df05 unify generics across App, Scope and Resource (#2572)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-05 15:02:28 +00:00
4431c8da65 fix bench 2022-01-05 14:10:38 +00:00
2d11ab5977 Add ServiceConfig::configure (#1988)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-05 12:31:39 +00:00
4ebf16890d add GuardContext::header (#2569) 2022-01-05 11:47:14 +00:00
fe0bbfb3da optimize PathDeserializer (#2570) 2022-01-05 10:48:20 +00:00
2462b6dd5d generalize impl Responder for HttpResponse (#2567)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-05 04:42:52 +00:00
49cfabeaf5 simplify Resource trait (#2568)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-05 04:34:13 +00:00
0f7292c69a remove readme msrv link 2022-01-05 04:24:40 +00:00
8bbf2b5052 prepare actix-test release 0.1.0-beta.11 2022-01-04 15:37:48 +00:00
8c975bcc1f prepare actix-http-test release 3.0.0-beta.11 2022-01-04 15:37:33 +00:00
742ad56d30 prepare actix-web-actors release 4.0.0-beta.10 2022-01-04 15:37:14 +00:00
bcc8d5c441 prepare actix-multipart release 0.4.0-beta.12 2022-01-04 15:36:56 +00:00
f659098d21 prepare awc release 3.0.0-beta.18 2022-01-04 15:35:21 +00:00
8621ae12f8 prepare actix-web release 4.0.0-beta.19 2022-01-04 15:35:08 +00:00
b338eb8473 prepare actix-http release 3.0.0-beta.18 2022-01-04 15:34:52 +00:00
5abd1c2c2c prepare actix-web-codegen release 0.5.0-rc.1 2022-01-04 15:34:16 +00:00
05336269f9 prepare actix-router release 0.5.0-beta.4 2022-01-04 15:33:44 +00:00
86df295ee2 fully percent decode path segments when capturing (#2566) 2022-01-04 15:19:29 +00:00
85c9b1a263 move quoter 2022-01-04 12:58:40 +00:00
577597a80a rename on-connect example 2022-01-04 12:54:20 +00:00
374dc9bfc9 files: percent-decode url path (#2398)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-04 12:54:11 +00:00
93754f307f try path config from Data as well 2022-01-04 04:08:46 +00:00
c7639bc3be document quoter 2022-01-04 03:48:12 +00:00
0bc4ae9158 remove BodyEncoding trait (#2565) 2022-01-03 18:46:04 +00:00
19a46e3925 fix doc test 2022-01-03 15:35:47 +00:00
68cd853aa2 improve docs for Compress 2022-01-03 14:59:01 +00:00
25fe1bbaa5 add double compress layer test 2022-01-03 14:05:08 +00:00
e890307091 Fix AcceptEncoding header (#2501) 2022-01-03 13:17:57 +00:00
b708924590 only run nightly checks on master ci 2021-12-31 08:38:58 +00:00
5dcb250237 fix doc test 2021-12-31 07:53:53 +00:00
b4ff6addfe use match name if possible in data debug log 2021-12-30 07:15:57 +00:00
231a24ef8d improve application data docs 2021-12-30 07:11:35 +00:00
6df4974234 prepare awc release 3.0.0-beta.17 2021-12-29 10:17:28 +00:00
a80e93d6db prepare actix-web release 4.0.0-beta.18 2021-12-29 10:17:11 +00:00
542c92c9a7 tweak changelogs 2021-12-29 10:06:36 +00:00
74738c63a7 Upgrade time dependency (via cookie) (#2555) 2021-12-29 10:03:25 +00:00
a87e01f0d1 bump msrv to 1.54 2021-12-29 08:59:15 +00:00
140 changed files with 3846 additions and 2553 deletions

View File

@ -1,8 +1,6 @@
name: Benchmark
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- master

View File

@ -5,6 +5,93 @@ on:
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

View File

@ -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 }}

View File

@ -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

View File

@ -1,6 +1,71 @@
# Changes
## Unreleased - 2021-xx-xx
### Added
- `HttpResponse::add_removal_cookie`. [#2586]
- `Logger::log_target`. [#2594]
### Removed
- `HttpRequest::req_data[_mut]()`; request-local data is still available through `.extensions()`. [#2585]
- `HttpRequestBuilder::del_cookie`. [#2591]
[#2585]: https://github.com/actix/actix-web/pull/2585
[#2586]: https://github.com/actix/actix-web/pull/2586
[#2591]: https://github.com/actix/actix-web/pull/2591
[#2594]: https://github.com/actix/actix-web/pull/2594
## 4.0.0-beta.20 - 2022-01-14
### Added
- `GuardContext::header` [#2569]
- `ServiceConfig::configure` to allow easy nesting of configuration functions. [#1988]
### Changed
- `HttpResponse` can now be used as a `Responder` with any body type. [#2567]
- `Result` extractor wrapper can now convert error types. [#2581]
- Associated types in `FromRequest` impl for `Option` and `Result` has changed. [#2581]
- Maximum number of handler extractors has increased to 12. [#2582]
- Removed bound `<B as MessageBody>::Error: Debug` in test utility functions in order to support returning opaque apps. [#2584]
[#1988]: https://github.com/actix/actix-web/pull/1988
[#2567]: https://github.com/actix/actix-web/pull/2567
[#2569]: https://github.com/actix/actix-web/pull/2569
[#2581]: https://github.com/actix/actix-web/pull/2581
[#2582]: https://github.com/actix/actix-web/pull/2582
[#2584]: https://github.com/actix/actix-web/pull/2584
## 4.0.0-beta.19 - 2022-01-04
### Added
- `impl Hash` for `http::header::Encoding`. [#2501]
- `AcceptEncoding::negotiate()`. [#2501]
### Changed
- `AcceptEncoding::preference` now returns `Option<Preference<Encoding>>`. [#2501]
- Rename methods `BodyEncoding::{encoding => encode_with, get_encoding => preferred_encoding}`. [#2501]
- `http::header::Encoding` now only represents `Content-Encoding` types. [#2501]
### Fixed
- Auto-negotiation of content encoding is more fault-tolerant when using the `Compress` middleware. [#2501]
### Removed
- `Compress::new`; restricting compression algorithm is done through feature flags. [#2501]
- `BodyEncoding` trait; signalling content encoding is now only done via the `Content-Encoding` header. [#2565]
[#2501]: https://github.com/actix/actix-web/pull/2501
[#2565]: https://github.com/actix/actix-web/pull/2565
## 4.0.0-beta.18 - 2021-12-29
### Changed
- Update `cookie` dependency (re-exported) to `0.16`. [#2555]
- Minimum supported Rust version (MSRV) is now 1.54.
### Security
- `cookie` upgrade addresses [`RUSTSEC-2020-0071`].
[#2555]: https://github.com/actix/actix-web/pull/2555
[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html
## 4.0.0-beta.17 - 2021-12-29

View File

@ -1,6 +1,6 @@
[package]
name = "actix-web"
version = "4.0.0-beta.17"
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,20 +71,20 @@ experimental-io-uring = ["actix-server/io-uring"]
[dependencies]
actix-codec = "0.4.1"
actix-macros = "0.2.3"
actix-rt = "2.3"
actix-server = "2.0.0-rc.2"
actix-rt = "2.6"
actix-server = "2"
actix-service = "2.0.0"
actix-utils = "3.0.0"
actix-tls = { version = "3.0.0", default-features = false, optional = true }
actix-http = "3.0.0-beta.17"
actix-router = "0.5.0-beta.3"
actix-web-codegen = "0.5.0-beta.6"
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"
encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false }
@ -94,7 +94,6 @@ 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"] }
@ -106,10 +105,12 @@ time = { version = "0.3", default-features = false, features = ["formatting"] }
url = "2.1"
[dev-dependencies]
actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] }
awc = { version = "3.0.0-beta.16", features = ["openssl"] }
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"
@ -117,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"
@ -155,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"]
@ -164,7 +170,7 @@ name = "uds"
required-features = ["compress-gzip"]
[[example]]
name = "on_connect"
name = "on-connect"
required-features = []
[[bench]]

View File

@ -6,13 +6,13 @@
<p>
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.17)](https://docs.rs/actix-web/4.0.0-beta.17)
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.20)](https://docs.rs/actix-web/4.0.0-beta.20)
![MSRV](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.17/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.17)
[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.20/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.20)
<br />
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)
[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)
![downloads](https://img.shields.io/crates/d/actix-web.svg)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
@ -32,7 +32,7 @@
- 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+
- Runs on stable Rust 1.54+
## Documentation

View File

@ -3,12 +3,26 @@
## 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`.
- No significant changes since `0.6.0-beta.11`.
## 0.6.0-beta.11 - 2021-12-27
* No significant changes since `0.6.0-beta.10`.
- No significant changes since `0.6.0-beta.10`.
## 0.6.0-beta.10 - 2021-12-11

View File

@ -1,6 +1,6 @@
[package]
name = "actix-files"
version = "0.6.0-beta.12"
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.17"
actix-http = "3.0.0-beta.18"
actix-service = "2"
actix-utils = "3"
actix-web = { version = "4.0.0-beta.17", 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.10"
actix-web = "4.0.0-beta.17"
actix-test = "0.1.0-beta.11"
actix-web = "4.0.0-beta.20"
tempfile = "3.2"

View File

@ -3,11 +3,11 @@
> Static file serving for Actix Web
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.12)](https://docs.rs/actix-files/0.6.0-beta.12)
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.14)](https://docs.rs/actix-files/0.6.0-beta.14)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![License](https://img.shields.io/crates/l/actix-files.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.12/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.12)
[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.14/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.14)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](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

View File

@ -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);
}
}
}
}

View File

@ -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)
};
}
// " -- &quot; & -- &amp; ' -- &#x27; < -- &lt; > -- &gt; / -- &#x2f;
/// Returns HTML entity encoded formatter.
///
/// ```plain
/// " => &quot;
/// & => &amp;
/// ' => &#x27;
/// < => &lt;
/// > => &gt;
/// / => &#x2f;
/// ```
macro_rules! encode_file_name {
($entry:ident) => {
escape_html_entity(&$entry.file_name().to_string_lossy(), Html)

View File

@ -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`

View File

@ -28,6 +28,7 @@ use crate::{
///
/// `Files` service must be registered with `App::service()` method.
///
/// # Examples
/// ```
/// use actix_web::App;
/// use actix_files::Files;

View File

@ -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]

View File

@ -1,22 +1,20 @@
use std::{
fmt,
fs::Metadata,
io,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
use actix_service::{Service, ServiceFactory};
use actix_web::{
body::{self, BoxBody, SizedStream},
dev::{
AppService, BodyEncoding, HttpServiceFactory, ResourceDef, ServiceRequest,
ServiceResponse,
self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory,
ServiceRequest, ServiceResponse,
},
http::{
header::{
self, Charset, ContentDisposition, ContentEncoding, DispositionParam,
DispositionType, ExtendedValue,
DispositionType, ExtendedValue, HeaderValue,
},
StatusCode,
},
@ -40,7 +38,7 @@ bitflags! {
impl Default for Flags {
fn default() -> Self {
Flags::from_bits_truncate(0b0000_0111)
Flags::from_bits_truncate(0b0000_1111)
}
}
@ -68,12 +66,12 @@ impl Default for Flags {
/// NamedFile::open_async("./static/index.html").await
/// }
/// ```
#[derive(Deref, DerefMut)]
#[derive(Debug, Deref, DerefMut)]
pub struct NamedFile {
path: PathBuf,
#[deref]
#[deref_mut]
file: File,
path: PathBuf,
modified: Option<SystemTime>,
pub(crate) md: Metadata,
pub(crate) flags: Flags,
@ -83,32 +81,6 @@ pub struct NamedFile {
pub(crate) encoding: Option<ContentEncoding>,
}
impl fmt::Debug for NamedFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NamedFile")
.field("path", &self.path)
.field(
"file",
#[cfg(feature = "experimental-io-uring")]
{
&"tokio_uring::File"
},
#[cfg(not(feature = "experimental-io-uring"))]
{
&self.file
},
)
.field("modified", &self.modified)
.field("md", &self.md)
.field("flags", &self.flags)
.field("status_code", &self.status_code)
.field("content_type", &self.content_type)
.field("content_disposition", &self.content_disposition)
.field("encoding", &self.encoding)
.finish()
}
}
#[cfg(not(feature = "experimental-io-uring"))]
pub(crate) use std::fs::File;
#[cfg(feature = "experimental-io-uring")]
@ -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,7 +334,7 @@ impl NamedFile {
self
}
/// Creates a etag in a format is similar to Apache's.
/// Creates an `ETag` in a format is similar to Apache's.
pub(crate) fn etag(&self) -> Option<header::EntityTag> {
self.modified.as_ref().map(|mtime| {
let ino = {
@ -386,7 +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(),
@ -405,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((
@ -420,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);
@ -478,12 +449,13 @@ impl NamedFile {
let mut res = HttpResponse::build(self.status_code);
if self.flags.contains(Flags::PREFER_UTF8) {
let ct = equiv_utf8_text(self.content_type.clone());
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
let ct = if self.flags.contains(Flags::PREFER_UTF8) {
equiv_utf8_text(self.content_type.clone())
} else {
res.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
}
self.content_type
};
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
res.insert_header((
@ -492,9 +464,8 @@ impl NamedFile {
));
}
// default compressing
if let Some(current_encoding) = self.encoding {
res.encoding(current_encoding);
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
}
if let Some(lm) = last_modified {
@ -517,7 +488,12 @@ impl NamedFile {
length = ranges[0].length;
offset = ranges[0].start;
res.encoding(ContentEncoding::Identity);
// don't allow compression middleware to modify partial content
res.insert_header((
header::CONTENT_ENCODING,
HeaderValue::from_static("identity"),
));
res.insert_header((
header::CONTENT_RANGE,
format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
@ -626,7 +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();

View File

@ -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())
}
}

View File

@ -2,7 +2,7 @@ use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc};
use actix_web::{
body::BoxBody,
dev::{Service, ServiceRequest, ServiceResponse},
dev::{self, Service, ServiceRequest, ServiceResponse},
error::Error,
guard::Guard,
http::{header, Method},
@ -98,7 +98,7 @@ impl Service<ServiceRequest> for FilesService {
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::always_ready!();
dev::always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future {
let is_method_valid = if let Some(guard) = &self.guards {
@ -114,32 +114,32 @@ impl Service<ServiceRequest> for FilesService {
Box::pin(async move {
if !is_method_valid {
return Ok(req.into_response(
actix_web::HttpResponse::MethodNotAllowed()
HttpResponse::MethodNotAllowed()
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
.body("Request did not meet this resource's requirements."),
));
}
let real_path =
match PathBufWrap::parse_path(req.match_info().path(), this.hidden_files) {
Ok(item) => item,
Err(e) => return Ok(req.error_response(e)),
};
let path_on_disk = match PathBufWrap::parse_path(
req.match_info().unprocessed(),
this.hidden_files,
) {
Ok(item) => item,
Err(err) => return Ok(req.error_response(err)),
};
if let Some(filter) = &this.path_filter {
if !filter(real_path.as_ref(), req.head()) {
if !filter(path_on_disk.as_ref(), req.head()) {
if let Some(ref default) = this.default {
return default.call(req).await;
} else {
return Ok(
req.into_response(actix_web::HttpResponse::NotFound().finish())
);
return Ok(req.into_response(HttpResponse::NotFound().finish()));
}
}
}
// full file path
let path = this.directory.join(&real_path);
let path = this.directory.join(&path_on_disk);
if let Err(err) = path.canonicalize() {
return this.handle_err(err, req).await;
}

View File

@ -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")),
);
}

View File

@ -3,6 +3,10 @@
## 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]

View File

@ -1,6 +1,6 @@
[package]
name = "actix-http-test"
version = "3.0.0-beta.10"
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"]
@ -34,8 +34,8 @@ actix-codec = "0.4.1"
actix-tls = "3.0.0"
actix-utils = "3.0.0"
actix-rt = "2.2"
actix-server = "2.0.0-rc.2"
awc = { version = "3.0.0-beta.16", default-features = false }
actix-server = "2"
awc = { version = "3.0.0-beta.18", default-features = false }
base64 = "0.13"
bytes = "1"
@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
tokio = { version = "1.8.4", features = ["sync"] }
[dev-dependencies]
actix-web = { version = "4.0.0-beta.17", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-beta.17"
actix-web = { version = "4.0.0-beta.20", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-beta.18"

View File

@ -3,15 +3,15 @@
> Various helpers for Actix applications to use during testing.
[![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test)
[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.10)](https://docs.rs/actix-http-test/3.0.0-beta.10)
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.11)](https://docs.rs/actix-http-test/3.0.0-beta.11)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
<br>
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.10)
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.11/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.11)
[![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](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

View File

@ -1,6 +1,47 @@
# 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

View File

@ -1,6 +1,6 @@
[package]
name = "actix-http"
version = "3.0.0-beta.17"
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"]
@ -74,20 +74,21 @@ smallvec = "1.6.1"
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.10", features = ["openssl"] }
actix-server = "2.0.0-rc.2"
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.17"
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"

View File

@ -3,18 +3,18 @@
> HTTP primitives for the Actix ecosystem.
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.17)](https://docs.rs/actix-http/3.0.0-beta.17)
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.18)](https://docs.rs/actix-http/3.0.0-beta.18)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.17/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.17)
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.18/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.18)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](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

View File

@ -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 101999
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 {

View File

@ -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()

View File

@ -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

View File

@ -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};
@ -47,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()),
@ -165,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")]
@ -176,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();
@ -234,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();

View File

@ -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};
@ -56,25 +53,24 @@ impl<B: MessageBody> Encoder<B> {
}
pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self {
let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|| head.status == StatusCode::SWITCHING_PROTOCOLS
|| head.status == StatusCode::NO_CONTENT
|| encoding == ContentEncoding::Identity
|| encoding == ContentEncoding::Auto);
// no need to compress an empty body
if matches!(body.size(), BodySize::None) {
return Self::none();
}
let should_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|| head.status == StatusCode::SWITCHING_PROTOCOLS
|| head.status == StatusCode::NO_CONTENT
|| encoding == ContentEncoding::Identity);
let body = match body.try_into_bytes() {
Ok(body) => EncoderBody::Full { body },
Err(body) => EncoderBody::Stream { body },
};
if can_encode {
// Modify response body only if encoder is set
if let Some(enc) = ContentEncoder::encoder(encoding) {
if should_encode {
// wrap body only if encoder is feature-enabled
if let Some(enc) = ContentEncoder::select(encoding) {
update_head(encoding, head);
return Encoder {
@ -169,6 +165,7 @@ where
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
let mut this = self.project();
loop {
if *this.eof {
return Poll::Ready(None);
@ -252,10 +249,10 @@ where
}
fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
head.headers_mut().insert(
header::CONTENT_ENCODING,
HeaderValue::from_static(encoding.as_str()),
);
head.headers_mut()
.insert(header::CONTENT_ENCODING, encoding.to_header_value());
head.headers_mut()
.insert(header::VARY, HeaderValue::from_static("accept-encoding"));
head.no_chunking(false);
}
@ -268,7 +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 {

View File

@ -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.
@ -386,6 +387,7 @@ impl StdError for DispatchError {
/// A set of error that can occur during parsing content type.
#[derive(Debug, Display, Error)]
#[cfg_attr(test, derive(PartialEq))]
#[non_exhaustive]
pub enum ContentTypeError {
/// Can not parse content type
@ -397,28 +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();

View File

@ -380,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 }
}
@ -415,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,
}
@ -463,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;
@ -488,6 +492,7 @@ impl Decoder for PayloadDecoder {
}
}
}
Kind::Eof => {
if src.is_empty() {
Ok(None)

View File

@ -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();

View File

@ -6,7 +6,7 @@ use ahash::AHashMap;
use http::header::{HeaderName, HeaderValue};
use smallvec::{smallvec, SmallVec};
use crate::header::AsHeaderName;
use super::AsHeaderName;
/// A multi-map of HTTP headers.
///
@ -605,6 +605,13 @@ impl<'a> IntoIterator for &'a HeaderMap {
}
}
/// Convert `http::HeaderMap` to our `HeaderMap`.
impl From<http::HeaderMap> for HeaderMap {
fn from(mut map: http::HeaderMap) -> HeaderMap {
HeaderMap::from_drain(map.drain())
}
}
/// Iterator over removed, owned values with the same associated name.
///
/// Returned from methods that remove or replace items. See [`HeaderMap::insert`]

View File

@ -50,20 +50,13 @@ pub use self::utils::{
/// An interface for types that already represent a valid header.
pub trait Header: TryIntoHeaderValue {
/// Returns the name of the header field
/// Returns the name of the header field.
fn name() -> HeaderName;
/// Parse a header
/// Parse the header from a HTTP message.
fn parse<M: HttpMessage>(msg: &M) -> Result<Self, ParseError>;
}
/// Convert `http::HeaderMap` to our `HeaderMap`.
impl From<http::HeaderMap> for HeaderMap {
fn from(mut map: http::HeaderMap) -> HeaderMap {
HeaderMap::from_drain(map.drain())
}
}
/// This encode set is used for HTTP header values and is defined at
/// <https://datatracker.ietf.org/doc/html/rfc5987#section-3.2>.
pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS

View File

@ -20,14 +20,16 @@ pub struct ContentEncodingParseError;
/// See [IANA HTTP Content Coding Registry].
///
/// [IANA HTTP Content Coding Registry]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ContentEncoding {
/// Automatically select encoding based on encoding negotiation.
Auto,
/// Indicates the no-op identity encoding.
///
/// I.e., no compression or modification.
Identity,
/// A format using the Brotli algorithm.
Br,
Brotli,
/// A format using the zlib structure with deflate algorithm.
Deflate,
@ -37,32 +39,36 @@ pub enum ContentEncoding {
/// Zstd algorithm.
Zstd,
/// Indicates the identity function (i.e. no compression, nor modification).
Identity,
}
impl ContentEncoding {
/// Is the content compressed?
#[inline]
pub const fn is_compression(self) -> bool {
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
}
/// Convert content encoding to string.
#[inline]
pub const fn as_str(self) -> &'static str {
match self {
ContentEncoding::Br => "br",
ContentEncoding::Brotli => "br",
ContentEncoding::Gzip => "gzip",
ContentEncoding::Deflate => "deflate",
ContentEncoding::Zstd => "zstd",
ContentEncoding::Identity | ContentEncoding::Auto => "identity",
ContentEncoding::Identity => "identity",
}
}
/// Convert content encoding to header value.
#[inline]
pub const fn to_header_value(self) -> HeaderValue {
match self {
ContentEncoding::Brotli => HeaderValue::from_static("br"),
ContentEncoding::Gzip => HeaderValue::from_static("gzip"),
ContentEncoding::Deflate => HeaderValue::from_static("deflate"),
ContentEncoding::Zstd => HeaderValue::from_static("zstd"),
ContentEncoding::Identity => HeaderValue::from_static("identity"),
}
}
}
impl Default for ContentEncoding {
#[inline]
fn default() -> Self {
Self::Identity
}
@ -71,16 +77,18 @@ impl Default for ContentEncoding {
impl FromStr for ContentEncoding {
type Err = ContentEncodingParseError;
fn from_str(val: &str) -> Result<Self, Self::Err> {
let val = val.trim();
fn from_str(enc: &str) -> Result<Self, Self::Err> {
let enc = enc.trim();
if val.eq_ignore_ascii_case("br") {
Ok(ContentEncoding::Br)
} else if val.eq_ignore_ascii_case("gzip") {
if enc.eq_ignore_ascii_case("br") {
Ok(ContentEncoding::Brotli)
} else if enc.eq_ignore_ascii_case("gzip") {
Ok(ContentEncoding::Gzip)
} else if val.eq_ignore_ascii_case("deflate") {
} else if enc.eq_ignore_ascii_case("deflate") {
Ok(ContentEncoding::Deflate)
} else if val.eq_ignore_ascii_case("zstd") {
} else if enc.eq_ignore_ascii_case("identity") {
Ok(ContentEncoding::Identity)
} else if enc.eq_ignore_ascii_case("zstd") {
Ok(ContentEncoding::Zstd)
} else {
Err(ContentEncodingParseError)

View File

@ -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.01.0 to a `Quality`.
///
@ -51,7 +55,7 @@ impl Quality {
// Check that `value` is within range should be done before calling this method.
// Just in case, this debug_assert should catch if we were forgetful.
debug_assert!(
(0.0f32..=1.0f32).contains(&value),
(0.0..=MAX_QUALITY_FLOAT).contains(&value),
"q value must be between 0.0 and 1.0"
);
@ -154,10 +158,13 @@ impl TryFrom<f32> for Quality {
/// let q1 = q(1.0);
/// assert_eq!(q1, Quality::MAX);
///
/// let q2 = q(0.0);
/// let q2 = q(0.001);
/// assert_eq!(q2, Quality::MIN);
///
/// let q3 = q(0.42);
/// let q3 = q(0.0);
/// assert_eq!(q3, Quality::ZERO);
///
/// let q4 = q(0.42);
/// ```
///
/// An out-of-range `f32` quality will panic.
@ -185,6 +192,10 @@ mod tests {
#[test]
fn display_output() {
assert_eq!(Quality::ZERO.to_string(), "0");
assert_eq!(Quality::MIN.to_string(), "0.001");
assert_eq!(Quality::MAX.to_string(), "1");
assert_eq!(q(0.0).to_string(), "0");
assert_eq!(q(1.0).to_string(), "1");
assert_eq!(q(0.001).to_string(), "0.001");

View File

@ -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),
}
}

View File

@ -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.

View File

@ -5,13 +5,13 @@ use bitflags::bitflags;
/// Represents various types of connection
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum ConnectionType {
/// Close connection after response
/// Close connection after response.
Close,
/// Keep connection alive after response
/// Keep connection alive after response.
KeepAlive,
/// Connection is upgraded to different type
/// Connection is upgraded to different type.
Upgrade,
}
@ -69,8 +69,8 @@ impl<T: Head> Drop for Message<T> {
}
}
/// Generic `Head` object pool.
#[doc(hidden)]
/// Request's objects pool
pub struct MessagePool<T: Head>(RefCell<Vec<Rc<T>>>);
impl<T: Head> MessagePool<T> {

View File

@ -142,8 +142,8 @@ impl RequestHead {
}
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum RequestHeadType {
Owned(RequestHead),
Rc(Rc<RequestHead>, Option<HeaderMap>),

View File

@ -19,7 +19,7 @@ pub struct Request<P = BoxedPayloadStream> {
pub(crate) payload: Payload<P>,
pub(crate) head: Message<RequestHead>,
pub(crate) conn_data: Option<Rc<Extensions>>,
pub(crate) req_data: RefCell<Extensions>,
pub(crate) extensions: RefCell<Extensions>,
}
impl<P> HttpMessage for Request<P> {
@ -34,16 +34,14 @@ impl<P> HttpMessage for Request<P> {
mem::replace(&mut self.payload, Payload::None)
}
/// Request extensions
#[inline]
fn extensions(&self) -> Ref<'_, Extensions> {
self.req_data.borrow()
self.extensions.borrow()
}
/// Mutable reference to a the request's extensions
#[inline]
fn extensions_mut(&self) -> RefMut<'_, Extensions> {
self.req_data.borrow_mut()
self.extensions.borrow_mut()
}
}
@ -52,7 +50,7 @@ impl From<Message<RequestHead>> for Request<BoxedPayloadStream> {
Request {
head,
payload: Payload::None,
req_data: RefCell::new(Extensions::default()),
extensions: RefCell::new(Extensions::default()),
conn_data: None,
}
}
@ -65,7 +63,7 @@ impl Request<BoxedPayloadStream> {
Request {
head: Message::new(),
payload: Payload::None,
req_data: RefCell::new(Extensions::default()),
extensions: RefCell::new(Extensions::default()),
conn_data: None,
}
}
@ -77,7 +75,7 @@ impl<P> Request<P> {
Request {
payload,
head: Message::new(),
req_data: RefCell::new(Extensions::default()),
extensions: RefCell::new(Extensions::default()),
conn_data: None,
}
}
@ -90,7 +88,7 @@ impl<P> Request<P> {
Request {
payload,
head: self.head,
req_data: self.req_data,
extensions: self.extensions,
conn_data: self.conn_data,
},
pl,
@ -195,16 +193,17 @@ impl<P> Request<P> {
.and_then(|container| container.get::<T>())
}
/// Returns the connection data container if an [on-connect] callback was registered.
/// Returns the connection-level data/extensions container if an [on-connect] callback was
/// registered, leaving an empty one in its place.
///
/// [on-connect]: crate::HttpServiceBuilder::on_connect_ext
pub fn take_conn_data(&mut self) -> Option<Rc<Extensions>> {
self.conn_data.take()
}
/// Returns the request data container, leaving an empty one in it's place.
/// Returns the request-local data/extensions container, leaving an empty one in its place.
pub fn take_req_data(&mut self) -> Extensions {
mem::take(self.req_data.get_mut())
mem::take(self.extensions.get_mut())
}
}

View File

@ -1,9 +1,6 @@
//! HTTP response builder.
use std::{
cell::{Ref, RefMut},
fmt, str,
};
use std::{cell::RefCell, fmt, str};
use crate::{
body::{EitherBody, MessageBody},
@ -202,20 +199,6 @@ impl ResponseBuilder {
self
}
/// Responses extensions
#[inline]
pub fn extensions(&self) -> Ref<'_, Extensions> {
let head = self.head.as_ref().expect("cannot reuse response builder");
head.extensions.borrow()
}
/// Mutable reference to a the response's extensions
#[inline]
pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> {
let head = self.head.as_ref().expect("cannot reuse response builder");
head.extensions.borrow_mut()
}
/// Generate response with a wrapped body.
///
/// This `ResponseBuilder` will be left in a useless state.
@ -238,7 +221,12 @@ impl ResponseBuilder {
}
let head = self.head.take().expect("cannot reuse response builder");
Ok(Response { head, body })
Ok(Response {
head,
body,
extensions: RefCell::new(Extensions::new()),
})
}
/// Generate response with an empty body.

View File

@ -1,26 +1,20 @@
//! Response head type and caching pool.
use std::{
cell::{Ref, RefCell, RefMut},
ops,
};
use std::{cell::RefCell, ops};
use crate::{
header::HeaderMap, message::Flags, ConnectionType, Extensions, StatusCode, Version,
};
use crate::{header::HeaderMap, message::Flags, ConnectionType, StatusCode, Version};
thread_local! {
static RESPONSE_POOL: BoxedResponsePool = BoxedResponsePool::create();
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ResponseHead {
pub version: Version,
pub status: StatusCode,
pub headers: HeaderMap,
pub reason: Option<&'static str>,
pub(crate) extensions: RefCell<Extensions>,
flags: Flags,
pub(crate) flags: Flags,
}
impl ResponseHead {
@ -33,36 +27,35 @@ impl ResponseHead {
headers: HeaderMap::with_capacity(12),
reason: None,
flags: Flags::empty(),
extensions: RefCell::new(Extensions::new()),
}
}
#[inline]
/// Read the message headers.
#[inline]
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
#[inline]
/// Mutable reference to the message headers.
#[inline]
pub fn headers_mut(&mut self) -> &mut HeaderMap {
&mut self.headers
}
/// Message extensions
/// Sets the flag that controls 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 extensions(&self) -> Ref<'_, Extensions> {
self.extensions.borrow()
pub fn set_camel_case_headers(&mut self, camel_case: bool) {
if camel_case {
self.flags.insert(Flags::CAMEL_CASE);
} else {
self.flags.remove(Flags::CAMEL_CASE);
}
}
/// Mutable reference to a the message's extensions
#[inline]
pub fn extensions_mut(&self) -> RefMut<'_, Extensions> {
self.extensions.borrow_mut()
}
#[inline]
/// Set connection type of the message
#[inline]
pub fn set_connection_type(&mut self, ctype: ConnectionType) {
match ctype {
ConnectionType::Close => self.flags.insert(Flags::CLOSE),
@ -121,14 +114,14 @@ impl ResponseHead {
}
}
#[inline]
/// Get response body chunking state
#[inline]
pub fn chunked(&self) -> bool {
!self.flags.contains(Flags::NO_CHUNKING)
}
#[inline]
/// Set no chunking for payload
#[inline]
pub fn no_chunking(&mut self, val: bool) {
if val {
self.flags.insert(Flags::NO_CHUNKING);
@ -171,7 +164,7 @@ impl Drop for BoxedResponseHead {
}
}
/// Request's objects pool
/// Response head object pool.
#[doc(hidden)]
pub struct BoxedResponsePool(#[allow(clippy::vec_box)] RefCell<Vec<Box<ResponseHead>>>);
@ -180,7 +173,7 @@ impl BoxedResponsePool {
BoxedResponsePool(RefCell::new(Vec::with_capacity(128)))
}
/// Get message from the pool
/// Get message from the pool.
#[inline]
fn get_message(&self, status: StatusCode) -> BoxedResponseHead {
if let Some(mut head) = self.0.borrow_mut().pop() {
@ -196,13 +189,67 @@ impl BoxedResponsePool {
}
}
/// Release request instance
/// Release request instance.
#[inline]
fn release(&self, mut msg: Box<ResponseHead>) {
fn release(&self, msg: Box<ResponseHead>) {
let pool = &mut self.0.borrow_mut();
if pool.len() < 128 {
msg.extensions.get_mut().clear();
pool.push(msg);
}
}
}
#[cfg(test)]
mod tests {
use std::{
io::{Read as _, Write as _},
net,
};
use memchr::memmem;
use crate::{
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;
}
}

View File

@ -1,7 +1,7 @@
//! HTTP response.
use std::{
cell::{Ref, RefMut},
cell::{Ref, RefCell, RefMut},
fmt, str,
};
@ -9,7 +9,7 @@ use bytes::{Bytes, BytesMut};
use bytestring::ByteString;
use crate::{
body::{BoxBody, MessageBody},
body::{BoxBody, EitherBody, MessageBody},
header::{self, HeaderMap, TryIntoHeaderValue},
responses::BoxedResponseHead,
Error, Extensions, ResponseBuilder, ResponseHead, StatusCode,
@ -19,6 +19,7 @@ use crate::{
pub struct Response<B> {
pub(crate) head: BoxedResponseHead,
pub(crate) body: B,
pub(crate) extensions: RefCell<Extensions>,
}
impl Response<BoxBody> {
@ -28,6 +29,7 @@ impl Response<BoxBody> {
Response {
head: BoxedResponseHead::new(status),
body: BoxBody::new(()),
extensions: RefCell::new(Extensions::new()),
}
}
@ -74,6 +76,7 @@ impl<B> Response<B> {
Response {
head: BoxedResponseHead::new(status),
body,
extensions: RefCell::new(Extensions::new()),
}
}
@ -120,20 +123,21 @@ impl<B> Response<B> {
}
/// Returns true if keep-alive is enabled.
#[inline]
pub fn keep_alive(&self) -> bool {
self.head.keep_alive()
}
/// Returns a reference to the extensions of this response.
/// Returns a reference to the request-local data/extensions container.
#[inline]
pub fn extensions(&self) -> Ref<'_, Extensions> {
self.head.extensions.borrow()
self.extensions.borrow()
}
/// Returns a mutable reference to the extensions of this response.
/// Returns a mutable reference to the request-local data/extensions container.
#[inline]
pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> {
self.head.extensions.borrow_mut()
self.extensions.borrow_mut()
}
/// Returns a reference to the body of this response.
@ -143,24 +147,29 @@ impl<B> Response<B> {
}
/// Sets new body.
#[inline]
pub fn set_body<B2>(self, body: B2) -> Response<B2> {
Response {
head: self.head,
body,
extensions: self.extensions,
}
}
/// Drops body and returns new response.
#[inline]
pub fn drop_body(self) -> Response<()> {
self.set_body(())
}
/// Sets new body, returning new response and previous body value.
#[inline]
pub(crate) fn replace_body<B2>(self, body: B2) -> (Response<B2>, B) {
(
Response {
head: self.head,
body,
extensions: self.extensions,
},
self.body,
)
@ -171,11 +180,15 @@ impl<B> Response<B> {
/// # Implementation Notes
/// Due to internal performance optimizations, the first element of the returned tuple is a
/// `Response` as well but only contains the head of the response this was called on.
#[inline]
pub fn into_parts(self) -> (Response<()>, B) {
self.replace_body(())
}
/// Returns new response with mapped body.
/// Map the current body type to another using a closure. 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,
@ -185,6 +198,7 @@ impl<B> Response<B> {
Response {
head: self.head,
body,
extensions: self.extensions,
}
}
@ -197,6 +211,7 @@ impl<B> Response<B> {
}
/// Returns body, consuming this response.
#[inline]
pub fn into_body(self) -> B {
self.body
}
@ -239,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()
}
}

View File

@ -3,8 +3,12 @@
## 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`.
- No significant changes since `0.4.0-beta.10`.
## 0.4.0-beta.10 - 2021-12-11

View File

@ -1,6 +1,6 @@
[package]
name = "actix-multipart"
version = "0.4.0-beta.11"
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.17", 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.17"
actix-http = "3.0.0-beta.18"
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
tokio = { version = "1.8.4", features = ["sync"] }
tokio-stream = "0.1"

View File

@ -3,15 +3,15 @@
> Multipart form support for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart)
[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.11)](https://docs.rs/actix-multipart/0.4.0-beta.11)
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.12)](https://docs.rs/actix-multipart/0.4.0-beta.12)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.11/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.11)
[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.12/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.12)
[![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](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

View File

@ -3,6 +3,27 @@
## Unreleased - 2021-xx-xx
## 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.

View File

@ -1,6 +1,6 @@
[package]
name = "actix-router"
version = "0.5.0-beta.3"
version = "0.5.0-rc.2"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",

View File

@ -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();

View File

@ -8,6 +8,7 @@
mod de;
mod path;
mod pattern;
mod quoter;
mod resource;
mod resource_path;
mod router;
@ -18,9 +19,10 @@ mod url;
pub use self::de::PathDeserializer;
pub use self::path::Path;
pub use self::pattern::{IntoPatterns, Patterns};
pub use self::quoter::Quoter;
pub use self::resource::ResourceDef;
pub use self::resource_path::{Resource, ResourcePath};
pub use self::router::{ResourceInfo, Router, RouterBuilder};
#[cfg(feature = "http")]
pub use self::url::{Quoter, Url};
pub use self::url::Url;

View File

@ -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();
}
}

219
actix-router/src/quoter.rs Normal file
View 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);
}
}

View File

@ -678,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) => {
@ -711,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,
}
@ -739,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) => &params[idx],
_ => return false,

View File

@ -2,8 +2,11 @@ use crate::Path;
// 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 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 {

View File

@ -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());

View File

@ -1,40 +1,6 @@
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"%/+");
@ -54,18 +20,20 @@ impl Url {
}
#[inline]
pub fn with_quoter(uri: http::Uri, quoter: &Quoter) -> Url {
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 {
match self.path {
@ -94,113 +62,6 @@ impl ResourcePath for Url {
}
}
/// A quoter
pub struct Quoter {
safe_table: [u8; 16],
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 i in 0..128 {
if ALLOWED.contains(&i) {
set_bit(&mut quoter.safe_table, i);
}
if QS.contains(&i) {
set_bit(&mut quoter.safe_table, i);
}
}
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
}
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;
@ -229,6 +90,16 @@ mod tests {
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
@ -239,13 +110,6 @@ mod tests {
assert_eq!(path.get("id").unwrap(), "qwe%25rty");
}
#[test]
fn 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 non_protected_ascii() {
let non_protected_ascii = ('\u{0}'..='\u{7F}')
@ -257,7 +121,7 @@ mod tests {
}
#[test]
fn 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));
@ -271,27 +135,6 @@ mod tests {
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 hex_encoding() {
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());
}
}

View File

@ -3,8 +3,12 @@
## 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`.
- No significant changes since `0.1.0-beta.9`.
## 0.1.0-beta.9 - 2021-12-17

View File

@ -1,6 +1,6 @@
[package]
name = "actix-test"
version = "0.1.0-beta.10"
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.17"
actix-http-test = "3.0.0-beta.10"
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.17", default-features = false, features = ["cookies"] }
awc = { version = "3.0.0-beta.16", 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 = [] }

View File

@ -3,8 +3,12 @@
## Unreleased - 2021-xx-xx
## 4.0.0-beta.10 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54.
## 4.0.0-beta.9 - 2021-12-27
* No significant changes since `4.0.0-beta.8`.
- No significant changes since `4.0.0-beta.8`.
## 4.0.0-beta.8 - 2021-12-11

View File

@ -1,6 +1,6 @@
[package]
name = "actix-web-actors"
version = "4.0.0-beta.9"
version = "4.0.0-beta.10"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix actors support for Actix Web"
keywords = ["actix", "http", "web", "framework", "async"]
@ -16,8 +16,8 @@ path = "src/lib.rs"
[dependencies]
actix = { version = "0.12.0", default-features = false }
actix-codec = "0.4.1"
actix-http = "3.0.0-beta.17"
actix-web = { version = "4.0.0-beta.17", default-features = false }
actix-http = "3.0.0-beta.18"
actix-web = { version = "4.0.0-beta.20", default-features = false }
bytes = "1"
bytestring = "1"
@ -27,8 +27,8 @@ tokio = { version = "1.8.4", features = ["sync"] }
[dev-dependencies]
actix-rt = "2.2"
actix-test = "0.1.0-beta.10"
awc = { version = "3.0.0-beta.16", default-features = false }
actix-test = "0.1.0-beta.11"
awc = { version = "3.0.0-beta.18", default-features = false }
env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false }

View File

@ -3,15 +3,15 @@
> Actix actors support for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors)
[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.9)](https://docs.rs/actix-web-actors/4.0.0-beta.9)
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.10)](https://docs.rs/actix-web-actors/4.0.0-beta.10)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![License](https://img.shields.io/crates/l/actix-web-actors.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.9)
[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.10)
[![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-web-actors)
- Minimum Supported Rust Version (MSRV): 1.52
- Minimum Supported Rust Version (MSRV): 1.54

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx
## 0.5.0-rc.1 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54.
## 0.5.0-beta.6 - 2021-12-11
- No significant changes since `0.5.0-beta.5`.

View File

@ -1,6 +1,6 @@
[package]
name = "actix-web-codegen"
version = "0.5.0-beta.6"
version = "0.5.0-rc.1"
description = "Routing and runtime macros for Actix Web"
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git"
@ -15,17 +15,17 @@ edition = "2018"
proc-macro = true
[dependencies]
actix-router = "0.5.0-beta.4"
proc-macro2 = "1"
quote = "1"
syn = { version = "1", features = ["full", "parsing"] }
proc-macro2 = "1"
actix-router = "0.5.0-beta.3"
[dev-dependencies]
actix-macros = "0.2.3"
actix-rt = "2.2"
actix-test = "0.1.0-beta.10"
actix-test = "0.1.0-beta.11"
actix-utils = "3.0.0"
actix-web = "4.0.0-beta.17"
actix-web = "4.0.0-beta.20"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
trybuild = "1"

View File

@ -3,18 +3,18 @@
> Routing and runtime macros for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen)
[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.6)](https://docs.rs/actix-web-codegen/0.5.0-beta.6)
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-rc.1)](https://docs.rs/actix-web-codegen/0.5.0-rc.1)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.6/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.6)
[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-rc.1/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-rc.1)
[![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-web-codegen)
- Minimum Supported Rust Version (MSRV): 1.52
- Minimum Supported Rust Version (MSRV): 1.54
## Compile Testing

View File

@ -39,13 +39,18 @@
//! ```
//! # use actix_web::HttpResponse;
//! # use actix_web_codegen::route;
//! #[route("/test", method="GET", method="HEAD")]
//! #[route("/test", method = "GET", method = "HEAD")]
//! async fn get_and_head_handler() -> HttpResponse {
//! HttpResponse::Ok().finish()
//! }
//! ```
//!
//! [actix-web attributes docs]: https://docs.rs/actix-web/*/actix_web/#attributes
//! # Multiple Path Handlers
//! There are no macros to generate multi-path handlers. Let us know in [this issue].
//!
//! [this issue]: https://github.com/actix/actix-web/issues/1709
//!
//! [actix-web attributes docs]: https://docs.rs/actix-web/latest/actix_web/#attributes
//! [GET]: macro@get
//! [POST]: macro@post
//! [PUT]: macro@put
@ -73,22 +78,23 @@ mod route;
/// ```
///
/// # Attributes
/// - `"path"` - Raw literal string with path for which to register handler.
/// - `name="resource_name"` - Specifies resource name for the handler. If not set, the function name of handler is used.
/// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. Upper-case string, "GET", "POST" for example.
/// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`
/// - `wrap="Middleware"` - Registers a resource middleware.
/// - `"path"`: Raw literal string with path for which to register handler.
/// - `name = "resource_name"`: Specifies resource name for the handler. If not set, the function
/// name of handler is used.
/// - `method = "HTTP_METHOD"`: Registers HTTP method to provide guard for. Upper-case string,
/// "GET", "POST" for example.
/// - `guard = "function_name"`: Registers function as guard using `actix_web::guard::fn_guard`.
/// - `wrap = "Middleware"`: Registers a resource middleware.
///
/// # Notes
/// Function name can be specified as any expression that is going to be accessible to the generate
/// code, e.g `my_guard` or `my_module::my_guard`.
///
/// # Example
///
/// # Examples
/// ```
/// # use actix_web::HttpResponse;
/// # use actix_web_codegen::route;
/// #[route("/test", method="GET", method="HEAD")]
/// #[route("/test", method = "GET", method = "HEAD")]
/// async fn example() -> HttpResponse {
/// HttpResponse::Ok().finish()
/// }
@ -98,69 +104,54 @@ pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(None, args, input)
}
macro_rules! doc_comment {
($x:expr; $($tt:tt)*) => {
#[doc = $x]
$($tt)*
};
}
macro_rules! method_macro {
(
$($variant:ident, $method:ident,)+
) => {
$(doc_comment! {
concat!("
Creates route handler with `actix_web::guard::", stringify!($variant), "`.
# Syntax
```plain
#[", stringify!($method), r#"("path"[, attributes])]
```
# Attributes
- `"path"` - Raw literal string with path for which to register handler.
- `name="resource_name"` - Specifies resource name for the handler. If not set, the function name of handler is used.
- `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`.
- `wrap="Middleware"` - Registers a resource middleware.
# Notes
Function name can be specified as any expression that is going to be accessible to the generate
code, e.g `my_guard` or `my_module::my_guard`.
# Example
```
# use actix_web::HttpResponse;
# use actix_web_codegen::"#, stringify!($method), ";
#[", stringify!($method), r#"("/")]
async fn example() -> HttpResponse {
HttpResponse::Ok().finish()
($variant:ident, $method:ident) => {
#[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")]
///
/// # Syntax
/// ```plain
#[doc = concat!("#[", stringify!($method), r#"("path"[, attributes])]"#)]
/// ```
///
/// # Attributes
/// - `"path"`: Raw literal string with path for which to register handler.
/// - `name = "resource_name"`: Specifies resource name for the handler. If not set, the function
/// name of handler is used.
/// - `guard = "function_name"`: Registers function as guard using `actix_web::guard::fn_guard`.
/// - `wrap = "Middleware"`: Registers a resource middleware.
///
/// # Notes
/// Function name can be specified as any expression that is going to be accessible to the
/// generate code, e.g `my_guard` or `my_module::my_guard`.
///
/// # Examples
/// ```
/// # use actix_web::HttpResponse;
#[doc = concat!("# use actix_web_codegen::", stringify!($method), ";")]
#[doc = concat!("#[", stringify!($method), r#"("/")]"#)]
/// async fn example() -> HttpResponse {
/// HttpResponse::Ok().finish()
/// }
/// ```
#[proc_macro_attribute]
pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(Some(route::MethodType::$variant), args, input)
}
```
"#);
#[proc_macro_attribute]
pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(Some(route::MethodType::$variant), args, input)
}
})+
};
}
method_macro! {
Get, get,
Post, post,
Put, put,
Delete, delete,
Head, head,
Connect, connect,
Options, options,
Trace, trace,
Patch, patch,
}
/// Marks async main function as the actix system entry-point.
method_macro!(Get, get);
method_macro!(Post, post);
method_macro!(Put, put);
method_macro!(Delete, delete);
method_macro!(Head, head);
method_macro!(Connect, connect);
method_macro!(Options, options);
method_macro!(Trace, trace);
method_macro!(Patch, patch);
/// Marks async main function as the Actix Web system entry-point.
///
/// # Examples
/// ```
/// #[actix_web::main]

View File

@ -302,13 +302,13 @@ impl ToTokens for Route {
if methods.len() > 1 {
quote! {
.guard(
actix_web::guard::Any(actix_web::guard::#first())
#(.or(actix_web::guard::#others()))*
::actix_web::guard::Any(::actix_web::guard::#first())
#(.or(::actix_web::guard::#others()))*
)
}
} else {
quote! {
.guard(actix_web::guard::#first())
.guard(::actix_web::guard::#first())
}
}
};
@ -318,17 +318,17 @@ impl ToTokens for Route {
#[allow(non_camel_case_types, missing_docs)]
pub struct #name;
impl actix_web::dev::HttpServiceFactory for #name {
impl ::actix_web::dev::HttpServiceFactory for #name {
fn register(self, __config: &mut actix_web::dev::AppService) {
#ast
let __resource = actix_web::Resource::new(#path)
let __resource = ::actix_web::Resource::new(#path)
.name(#resource_name)
#method_guards
#(.guard(actix_web::guard::fn_guard(#guards)))*
#(.guard(::actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))*
.#resource_type(#name);
actix_web::dev::HttpServiceFactory::register(__resource, __config)
::actix_web::dev::HttpServiceFactory::register(__resource, __config)
}
}
};

View File

@ -1,4 +1,4 @@
#[rustversion::stable(1.52)] // MSRV
#[rustversion::stable(1.54)] // MSRV
#[test]
fn compile_macros() {
let t = trybuild::TestCases::new();

View File

@ -1,13 +1,13 @@
error: The #[route(..)] macro requires at least one `method` attribute
--> $DIR/route-missing-method-fail.rs:3:1
--> tests/trybuild/route-missing-method-fail.rs:3:1
|
3 | #[route("/")]
| ^^^^^^^^^^^^^
|
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
= note: this error originates in the attribute macro `route` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied
--> $DIR/route-missing-method-fail.rs:12:55
--> tests/trybuild/route-missing-method-fail.rs:12:55
|
12 | let srv = actix_test::start(|| App::new().service(index));
| ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`

View File

@ -3,6 +3,21 @@
## Unreleased - 2021-xx-xx
## 3.0.0-beta.18 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54.
## 3.0.0-beta.17 - 2021-12-29
### Changed
- Update `cookie` dependency (re-exported) to `0.16`. [#2555]
### Security
- `cookie` upgrade addresses [`RUSTSEC-2020-0071`].
[#2555]: https://github.com/actix/actix-web/pull/2555
[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html
## 3.0.0-beta.16 - 2021-12-29
- `*::send_json` and `*::send_form` methods now receive `impl Serialize`. [#2553]
- `FrozenClientRequest::extra_header` now uses receives an `impl TryIntoHeaderPair`. [#2553]

View File

@ -1,6 +1,6 @@
[package]
name = "awc"
version = "3.0.0-beta.16"
version = "3.0.0-beta.18"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>",
@ -60,7 +60,7 @@ dangerous-h2c = []
[dependencies]
actix-codec = "0.4.1"
actix-service = "2.0.0"
actix-http = "3.0.0-beta.17"
actix-http = "3.0.0-beta.18"
actix-rt = { version = "2.1", default-features = false }
actix-tls = { version = "3.0.0", features = ["connect", "uri"] }
actix-utils = "3.0.0"
@ -85,7 +85,7 @@ serde_json = "1.0"
serde_urlencoded = "0.7"
tokio = { version = "1.8.4", features = ["sync"] }
cookie = { version = "0.15", features = ["percent-encode"], optional = true }
cookie = { version = "0.16", features = ["percent-encode"], optional = true }
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] }
@ -93,21 +93,24 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features
trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies]
actix-http = { version = "3.0.0-beta.17", features = ["openssl"] }
actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] }
actix-server = "2.0.0-rc.2"
actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] }
actix-http = { version = "3.0.0-beta.18", features = ["openssl"] }
actix-http-test = { version = "3.0.0-beta.11", features = ["openssl"] }
actix-server = "2"
actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] }
actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] }
actix-utils = "3.0.0"
actix-web = { version = "4.0.0-beta.17", features = ["openssl"] }
actix-web = { version = "4.0.0-beta.20", features = ["openssl"] }
brotli2 = "0.3.2"
brotli = "3.3.3"
const-str = "0.3"
env_logger = "0.9"
flate2 = "1.0.13"
futures-util = { version = "0.3.7", default-features = false }
static_assertions = "1.1"
rcgen = "0.8"
rustls-pemfile = "0.2"
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
zstd = "0.9"
[[example]]
name = "client"

View File

@ -3,16 +3,16 @@
> Async HTTP and WebSocket client library.
[![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc)
[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.16)](https://docs.rs/awc/3.0.0-beta.16)
[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.18)](https://docs.rs/awc/3.0.0-beta.18)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc)
[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.16/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.16)
[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.18/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.18)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources
- [API Documentation](https://docs.rs/awc)
- [Example Project](https://github.com/actix/examples/tree/HEAD/security/awc_https)
- Minimum Supported Rust Version (MSRV): 1.52
- Minimum Supported Rust Version (MSRV): 1.54
## Example

View File

@ -1,13 +1,13 @@
use std::error::Error as StdError;
#[actix_web::main]
#[tokio::main]
async fn main() -> Result<(), Box<dyn StdError>> {
std::env::set_var("RUST_LOG", "client=trace,awc=trace,actix_http=trace");
env_logger::init();
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
// construct request builder
let client = awc::Client::new();
// Create request builder, configure request and send
// configure request
let request = client
.get("https://www.rust-lang.org/")
.append_header(("User-Agent", "Actix-web"));
@ -16,7 +16,7 @@ async fn main() -> Result<(), Box<dyn StdError>> {
let mut response = request.send().await?;
// server http response
// server response head
println!("Response: {:?}", response);
// read response body

View File

@ -207,7 +207,7 @@ where
self
}
/// Maximum supported HTTP major version.
/// Sets maximum supported HTTP major version.
///
/// Supported versions are HTTP/1.1 and HTTP/2.
pub fn max_http_version(mut self, val: http::Version) -> Self {
@ -222,8 +222,8 @@ where
self
}
/// Indicates the initial window size (in octets) for
/// HTTP2 stream-level flow control for received data.
/// Sets the initial window size (in octets) for HTTP/2 stream-level flow control for
/// received data.
///
/// The default value is 65,535 and is good for APIs, but not for big objects.
pub fn initial_window_size(mut self, size: u32) -> Self {
@ -231,8 +231,8 @@ where
self
}
/// Indicates the initial window size (in octets) for
/// HTTP2 connection-level flow control for received data.
/// Sets the initial window size (in octets) for HTTP/2 connection-level flow control for
/// received data.
///
/// The default value is 65,535 and is good for APIs, but not for big objects.
pub fn initial_connection_window_size(mut self, size: u32) -> Self {
@ -243,6 +243,7 @@ where
/// Set total number of simultaneous connections per type of scheme.
///
/// If limit is 0, the connector has no limit.
///
/// The default limit size is 100.
pub fn limit(mut self, limit: usize) -> Self {
self.config.limit = limit;

View File

@ -67,12 +67,13 @@ impl Default for Client {
}
impl Client {
/// Create new client instance with default settings.
/// Constructs new client instance with default settings.
pub fn new() -> Client {
Client::default()
}
/// Create `Client` builder.
/// Constructs new `Client` builder.
///
/// This function is equivalent of `ClientBuilder::new()`.
pub fn builder() -> ClientBuilder<
impl Service<

View File

@ -1,5 +1,5 @@
use std::{
cell::{Ref, RefMut},
cell::{Ref, RefCell, RefMut},
fmt, mem,
pin::Pin,
task::{Context, Poll},
@ -28,6 +28,8 @@ pin_project! {
#[pin]
pub(crate) payload: Payload<S>,
pub(crate) timeout: ResponseTimeout,
pub(crate) extensions: RefCell<Extensions>,
}
}
@ -38,6 +40,7 @@ impl<S> ClientResponse<S> {
head,
payload,
timeout: ResponseTimeout::default(),
extensions: RefCell::new(Extensions::new()),
}
}
@ -64,7 +67,9 @@ impl<S> ClientResponse<S> {
&self.head().headers
}
/// Set a body and return previous body value
/// Map the current body type to another using a closure. Returns a new response.
///
/// Closure receives the response head and the current body type.
pub fn map_body<F, U>(mut self, f: F) -> ClientResponse<U>
where
F: FnOnce(&mut ResponseHead, Payload<S>) -> Payload<U>,
@ -75,6 +80,7 @@ impl<S> ClientResponse<S> {
payload,
head: self.head,
timeout: self.timeout,
extensions: self.extensions,
}
}
@ -101,6 +107,7 @@ impl<S> ClientResponse<S> {
payload: self.payload,
head: self.head,
timeout,
extensions: self.extensions,
}
}
@ -153,7 +160,6 @@ where
///
/// # Errors
/// `Future` implementation returns error if:
/// - content type is not `application/json`
/// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB)
///
/// # Examples
@ -224,11 +230,11 @@ impl<S> HttpMessage for ClientResponse<S> {
}
fn extensions(&self) -> Ref<'_, Extensions> {
self.head.extensions()
self.extensions.borrow()
}
fn extensions_mut(&self) -> RefMut<'_, Extensions> {
self.head.extensions_mut()
self.extensions.borrow_mut()
}
}

View File

@ -2,7 +2,7 @@
//!
//! Type definitions required to use [`awc::Client`](super::Client) as a WebSocket client.
//!
//! # Example
//! # Examples
//!
//! ```no_run
//! use awc::{Client, ws};

View File

@ -1,5 +1,6 @@
use std::{
collections::HashMap,
convert::Infallible,
io::{Read, Write},
net::{IpAddr, Ipv4Addr},
sync::{
@ -15,43 +16,16 @@ use cookie::Cookie;
use futures_util::stream;
use rand::Rng;
#[cfg(feature = "compress-brotli")]
use brotli2::write::BrotliEncoder;
#[cfg(feature = "compress-gzip")]
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use actix_http::{ContentEncoding, HttpService, StatusCode};
use actix_http::{HttpService, StatusCode};
use actix_http_test::test_server;
use actix_service::{fn_service, map_config, ServiceFactoryExt as _};
use actix_web::{
dev::{AppConfig, BodyEncoding},
http::header,
web, App, Error, HttpRequest, HttpResponse,
};
use actix_web::{dev::AppConfig, http::header, web, App, Error, HttpRequest, HttpResponse};
use awc::error::{JsonPayloadError, PayloadError, SendRequestError};
const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World";
mod utils;
const S: &str = "Hello World ";
const STR: &str = const_str::repeat!(S, 100);
#[actix_rt::test]
async fn test_simple() {
@ -471,15 +445,12 @@ async fn test_no_decompress() {
let srv = actix_test::start(|| {
App::new()
.wrap(actix_web::middleware::Compress::default())
.service(web::resource("/").route(web::to(|| {
let mut res = HttpResponse::Ok().body(STR);
res.encoding(header::ContentEncoding::Gzip);
res
})))
.service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))))
});
let mut res = awc::Client::new()
.get(srv.url("/"))
.insert_header((header::ACCEPT_ENCODING, "gzip"))
.no_decompress()
.send()
.await
@ -488,15 +459,12 @@ async fn test_no_decompress() {
// read response
let bytes = res.body().await.unwrap();
let mut e = GzDecoder::new(&bytes[..]);
let mut dec = Vec::new();
e.read_to_end(&mut dec).unwrap();
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
assert_eq!(utils::gzip::decode(bytes), STR.as_bytes());
// POST
let mut res = awc::Client::new()
.post(srv.url("/"))
.insert_header((header::ACCEPT_ENCODING, "gzip"))
.no_decompress()
.send()
.await
@ -504,10 +472,7 @@ async fn test_no_decompress() {
assert!(res.status().is_success());
let bytes = res.body().await.unwrap();
let mut e = GzDecoder::new(&bytes[..]);
let mut dec = Vec::new();
e.read_to_end(&mut dec).unwrap();
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
assert_eq!(utils::gzip::decode(bytes), STR.as_bytes());
}
#[cfg(feature = "compress-gzip")]
@ -515,13 +480,9 @@ async fn test_no_decompress() {
async fn test_client_gzip_encoding() {
let srv = actix_test::start(|| {
App::new().service(web::resource("/").route(web::to(|| {
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(STR.as_ref()).unwrap();
let data = e.finish().unwrap();
HttpResponse::Ok()
.insert_header(("content-encoding", "gzip"))
.body(data)
.insert_header(header::ContentEncoding::Gzip)
.body(utils::gzip::encode(STR))
})))
});
@ -531,7 +492,7 @@ async fn test_client_gzip_encoding() {
// read response
let bytes = response.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
assert_eq!(bytes, STR);
}
#[cfg(feature = "compress-gzip")]
@ -539,13 +500,9 @@ async fn test_client_gzip_encoding() {
async fn test_client_gzip_encoding_large() {
let srv = actix_test::start(|| {
App::new().service(web::resource("/").route(web::to(|| {
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(STR.repeat(10).as_ref()).unwrap();
let data = e.finish().unwrap();
HttpResponse::Ok()
.insert_header(("content-encoding", "gzip"))
.body(data)
.insert_header(header::ContentEncoding::Gzip)
.body(utils::gzip::encode(STR.repeat(10)))
})))
});
@ -555,7 +512,7 @@ async fn test_client_gzip_encoding_large() {
// read response
let bytes = response.body().await.unwrap();
assert_eq!(bytes, Bytes::from(STR.repeat(10)));
assert_eq!(bytes, STR.repeat(10));
}
#[cfg(feature = "compress-gzip")]
@ -569,12 +526,9 @@ async fn test_client_gzip_encoding_large_random() {
let srv = actix_test::start(|| {
App::new().service(web::resource("/").route(web::to(|data: Bytes| {
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(&data).unwrap();
let data = e.finish().unwrap();
HttpResponse::Ok()
.insert_header(("content-encoding", "gzip"))
.body(data)
.insert_header(header::ContentEncoding::Gzip)
.body(utils::gzip::encode(data))
})))
});
@ -584,7 +538,7 @@ async fn test_client_gzip_encoding_large_random() {
// read response
let bytes = response.body().await.unwrap();
assert_eq!(bytes, Bytes::from(data));
assert_eq!(bytes, data);
}
#[cfg(feature = "compress-brotli")]
@ -592,12 +546,9 @@ async fn test_client_gzip_encoding_large_random() {
async fn test_client_brotli_encoding() {
let srv = actix_test::start(|| {
App::new().service(web::resource("/").route(web::to(|data: Bytes| {
let mut e = BrotliEncoder::new(Vec::new(), 5);
e.write_all(&data).unwrap();
let data = e.finish().unwrap();
HttpResponse::Ok()
.insert_header(("content-encoding", "br"))
.body(data)
.body(utils::brotli::encode(data))
})))
});
@ -621,12 +572,9 @@ async fn test_client_brotli_encoding_large_random() {
let srv = actix_test::start(|| {
App::new().service(web::resource("/").route(web::to(|data: Bytes| {
let mut e = BrotliEncoder::new(Vec::new(), 5);
e.write_all(&data).unwrap();
let data = e.finish().unwrap();
HttpResponse::Ok()
.insert_header(("content-encoding", "br"))
.body(data)
.insert_header(header::ContentEncoding::Brotli)
.body(utils::brotli::encode(&data))
})))
});
@ -636,25 +584,25 @@ async fn test_client_brotli_encoding_large_random() {
// read response
let bytes = response.body().await.unwrap();
assert_eq!(bytes.len(), data.len());
assert_eq!(bytes, Bytes::from(data));
assert_eq!(bytes, data);
}
#[actix_rt::test]
async fn test_client_deflate_encoding() {
let srv = actix_test::start(|| {
App::new().default_service(web::to(|body: Bytes| {
HttpResponse::Ok().encoding(ContentEncoding::Br).body(body)
}))
App::new().default_service(web::to(|body: Bytes| HttpResponse::Ok().body(body)))
});
let req = srv.post("/").send_body(STR);
let req = srv
.post("/")
.insert_header((header::ACCEPT_ENCODING, "gzip"))
.send_body(STR);
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
assert_eq!(bytes, STR);
}
#[actix_rt::test]
@ -666,12 +614,13 @@ async fn test_client_deflate_encoding_large_random() {
.collect::<String>();
let srv = actix_test::start(|| {
App::new().default_service(web::to(|body: Bytes| {
HttpResponse::Ok().encoding(ContentEncoding::Br).body(body)
}))
App::new().default_service(web::to(|body: Bytes| HttpResponse::Ok().body(body)))
});
let req = srv.post("/").send_body(data.clone());
let req = srv
.post("/")
.insert_header((header::ACCEPT_ENCODING, "br"))
.send_body(data.clone());
let mut res = req.await.unwrap();
let bytes = res.body().await.unwrap();
@ -684,15 +633,16 @@ async fn test_client_deflate_encoding_large_random() {
async fn test_client_streaming_explicit() {
let srv = actix_test::start(|| {
App::new().default_service(web::to(|body: web::Payload| {
HttpResponse::Ok()
.encoding(ContentEncoding::Identity)
.streaming(body)
HttpResponse::Ok().streaming(body)
}))
});
let body =
stream::once(async { Ok::<_, actix_http::Error>(Bytes::from_static(STR.as_bytes())) });
let req = srv.post("/").send_stream(Box::pin(body));
let req = srv
.post("/")
.insert_header((header::ACCEPT_ENCODING, "identity"))
.send_stream(Box::pin(body));
let mut res = req.await.unwrap();
assert!(res.status().is_success());
@ -705,17 +655,16 @@ async fn test_client_streaming_explicit() {
async fn test_body_streaming_implicit() {
let srv = actix_test::start(|| {
App::new().default_service(web::to(|| {
let body = stream::once(async {
Ok::<_, actix_http::Error>(Bytes::from_static(STR.as_bytes()))
});
HttpResponse::Ok()
.encoding(ContentEncoding::Gzip)
.streaming(Box::pin(body))
let body =
stream::once(async { Ok::<_, Infallible>(Bytes::from_static(STR.as_bytes())) });
HttpResponse::Ok().streaming(body)
}))
});
let req = srv.get("/").send();
let req = srv
.get("/")
.insert_header((header::ACCEPT_ENCODING, "gzip"))
.send();
let mut res = req.await.unwrap();
assert!(res.status().is_success());

82
awc/tests/utils.rs Normal file
View File

@ -0,0 +1,82 @@
// compiling some tests will trigger unused function warnings even though other tests use them
#![allow(dead_code)]
use std::io::{Read as _, Write as _};
pub mod gzip {
use super::*;
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(bytes.as_ref()).unwrap();
encoder.finish().unwrap()
}
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut decoder = GzDecoder::new(bytes.as_ref());
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).unwrap();
buf
}
}
pub mod deflate {
use super::*;
use flate2::{read::ZlibDecoder, write::ZlibEncoder, Compression};
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(bytes.as_ref()).unwrap();
encoder.finish().unwrap()
}
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut decoder = ZlibDecoder::new(bytes.as_ref());
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).unwrap();
buf
}
}
pub mod brotli {
use super::*;
use ::brotli::{reader::Decompressor as BrotliDecoder, CompressorWriter as BrotliEncoder};
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut encoder = BrotliEncoder::new(
Vec::new(),
8 * 1024, // 32 KiB buffer
3, // BROTLI_PARAM_QUALITY
22, // BROTLI_PARAM_LGWIN
);
encoder.write_all(bytes.as_ref()).unwrap();
encoder.flush().unwrap();
encoder.into_inner()
}
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut decoder = BrotliDecoder::new(bytes.as_ref(), 8_096);
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).unwrap();
buf
}
}
pub mod zstd {
use super::*;
use ::zstd::stream::{read::Decoder, write::Encoder};
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut encoder = Encoder::new(Vec::new(), 3).unwrap();
encoder.write_all(bytes.as_ref()).unwrap();
encoder.finish().unwrap()
}
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut decoder = Decoder::new(bytes.as_ref()).unwrap();
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).unwrap();
buf
}
}

View File

@ -1 +1 @@
msrv = "1.52"
msrv = "1.54"

View File

@ -15,6 +15,7 @@ digraph {
"actix-web" -> { "actix-web-codegen" "actix-http" "actix-router" }
"awc" -> { "actix-http" }
"actix-web-codegen" -> { "actix-router" }
"actix-web-actors" -> { "actix" "actix-web" "actix-http" }
"actix-multipart" -> { "actix-web" }
"actix-files" -> { "actix-web" }

View File

@ -24,7 +24,7 @@ async fn main() -> std::io::Result<()> {
App::new()
.wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2")))
.wrap(middleware::Compress::default())
.wrap(middleware::Logger::default())
.wrap(middleware::Logger::default().log_target("http_log"))
.service(index)
.service(no_params)
.service(

View File

@ -17,12 +17,21 @@ if [ "$(uname)" = "Darwin" ]; then
fi
CARGO_MANIFEST=$DIR/Cargo.toml
CHANGELOG_FILE=$DIR/CHANGES.md
README_FILE=$DIR/README.md
# determine changelog file name
if [ -f "$DIR/CHANGES.md" ]; then
CHANGELOG_FILE=$DIR/CHANGES.md
elif [ -f "$DIR/CHANGELOG.md" ]; then
CHANGELOG_FILE=$DIR/CHANGELOG.md
else
echo "No changelog file found"
exit 1
fi
# get current version
PACKAGE_NAME="$(sed -nE 's/^name ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST" | head -n 1)"
CURRENT_VERSION="$(sed -nE 's/^version ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST")"
CURRENT_VERSION="$(sed -nE 's/^version ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST" | head -n 1)"
CHANGE_CHUNK_FILE="$(mktemp)"
echo saving changelog to $CHANGE_CHUNK_FILE
@ -40,7 +49,7 @@ cat "$CHANGELOG_FILE" |
# if word count of changelog chunk is 0 then insert filler changelog chunk
if [ "$(wc -w "$CHANGE_CHUNK_FILE" | awk '{ print $1 }')" = "0" ]; then
echo "* No significant changes since \`$CURRENT_VERSION\`." >"$CHANGE_CHUNK_FILE"
echo "- No significant changes since \`$CURRENT_VERSION\`." >"$CHANGE_CHUNK_FILE"
echo >>"$CHANGE_CHUNK_FILE"
echo >>"$CHANGE_CHUNK_FILE"
fi

View File

@ -12,16 +12,16 @@ save_exit_code() {
[ "$CMD_EXIT" = "0" ] || EXIT=$CMD_EXIT
}
save_exit_code cargo test --lib --tests -p=actix-router --all-features
save_exit_code cargo test --lib --tests -p=actix-http --all-features
save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features
save_exit_code cargo test --lib --tests -p=awc --all-features
save_exit_code cargo test --lib --tests -p=actix-http-test --all-features
save_exit_code cargo test --lib --tests -p=actix-test --all-features
save_exit_code cargo test --lib --tests -p=actix-files
save_exit_code cargo test --lib --tests -p=actix-multipart --all-features
save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features
save_exit_code cargo test --lib --tests -p=actix-router --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-http --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --nocapture --skip=test_reading_deflate_encoding_large_random_rustls
save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=awc --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-http-test --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-test --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-files -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-multipart --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features -- --nocapture
save_exit_code cargo test --workspace --doc

View File

@ -51,7 +51,10 @@ impl App<AppEntry> {
}
}
impl<T> App<T> {
impl<T> App<T>
where
T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
{
/// Set application (root level) data.
///
/// Application data stored with `App::app_data()` method is available through the
@ -109,6 +112,7 @@ impl<T> App<T> {
/// .route("/", web::get().to(handler))
/// })
/// ```
#[doc(alias = "manage")]
pub fn app_data<U: 'static>(mut self, ext: U) -> Self {
self.extensions.insert(ext);
self
@ -316,65 +320,63 @@ impl<T> App<T> {
self
}
/// Registers middleware, in the form of a middleware component (type),
/// that runs during inbound and/or outbound processing in the request
/// life-cycle (request -> response), modifying request/response as
/// necessary, across all requests managed by the *Application*.
/// Registers an app-wide middleware.
///
/// Use middleware when you need to read or modify *every* request or
/// response in some way.
/// Registers middleware, in the form of a middleware compo nen t (type), that runs during
/// inbound and/or outbound processing in the request life-cycle (request -> response),
/// modifying request/response as necessary, across all requests managed by the `App`.
///
/// Notice that the keyword for registering middleware is `wrap`. As you
/// register middleware using `wrap` in the App builder, imagine wrapping
/// layers around an inner App. The first middleware layer exposed to a
/// Request is the outermost layer-- the *last* registered in
/// the builder chain. Consequently, the *first* middleware registered
/// in the builder chain is the *last* to execute during request processing.
/// Use middleware when you need to read or modify *every* request or response in some way.
///
/// Middleware can be applied similarly to individual `Scope`s and `Resource`s.
/// See [`Scope::wrap`](crate::Scope::wrap) and [`Resource::wrap`].
///
/// # Middleware Order
/// Notice that the keyword for registering middleware is `wrap`. As you register middleware
/// using `wrap` in the App builder, imagine wrapping layers around an inner App. The first
/// middleware layer exposed to a Request is the outermost layer (i.e., the *last* registered in
/// the builder chain). Consequently, the *first* middleware registered in the builder chain is
/// the *last* to start executing during request processing.
///
/// Ordering is less obvious when wrapped services also have middleware applied. In this case,
/// middlewares are run in reverse order for `App` _and then_ in reverse order for the
/// wrapped service.
///
/// # Examples
/// ```
/// use actix_service::Service;
/// use actix_web::{middleware, web, App};
/// use actix_web::http::header::{CONTENT_TYPE, HeaderValue};
///
/// async fn index() -> &'static str {
/// "Welcome!"
/// }
///
/// fn main() {
/// let app = App::new()
/// .wrap(middleware::Logger::default())
/// .route("/index.html", web::get().to(index));
/// }
/// let app = App::new()
/// .wrap(middleware::Logger::default())
/// .route("/index.html", web::get().to(index));
/// ```
pub fn wrap<M, B, B1>(
#[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
pub fn wrap<M, B>(
self,
mw: M,
) -> App<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B1>,
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
>
where
T: ServiceFactory<
ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
Config = (),
InitError = (),
>,
B: MessageBody,
M: Transform<
T::Service,
ServiceRequest,
Response = ServiceResponse<B1>,
Error = Error,
InitError = (),
>,
B1: MessageBody,
T::Service,
ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
> + 'static,
B: MessageBody,
{
App {
endpoint: apply(mw, self.endpoint),
@ -387,61 +389,57 @@ impl<T> App<T> {
}
}
/// Registers middleware, in the form of a closure, that runs during inbound
/// and/or outbound processing in the request life-cycle (request -> response),
/// modifying request/response as necessary, across all requests managed by
/// the *Application*.
/// Registers an app-wide function middleware.
///
/// `mw` is a closure that runs during inbound and/or outbound processing in the request
/// life-cycle (request -> response), modifying request/response as necessary, across all
/// requests handled by the `App`.
///
/// Use middleware when you need to read or modify *every* request or response in some way.
///
/// Middleware can also be applied to individual `Scope`s and `Resource`s.
///
/// See [`App::wrap`] for details on how middlewares compose with each other.
///
/// # Examples
/// ```
/// use actix_service::Service;
/// use actix_web::{web, App};
/// use actix_web::{dev::Service as _, middleware, web, App};
/// use actix_web::http::header::{CONTENT_TYPE, HeaderValue};
///
/// async fn index() -> &'static str {
/// "Welcome!"
/// }
///
/// fn main() {
/// let app = App::new()
/// .wrap_fn(|req, srv| {
/// let fut = srv.call(req);
/// async {
/// let mut res = fut.await?;
/// res.headers_mut().insert(
/// CONTENT_TYPE, HeaderValue::from_static("text/plain"),
/// );
/// Ok(res)
/// }
/// })
/// .route("/index.html", web::get().to(index));
/// }
/// let app = App::new()
/// .wrap_fn(|req, srv| {
/// let fut = srv.call(req);
/// async {
/// let mut res = fut.await?;
/// res.headers_mut()
/// .insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));
/// Ok(res)
/// }
/// })
/// .route("/index.html", web::get().to(index));
/// ```
pub fn wrap_fn<F, R, B, B1>(
#[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
pub fn wrap_fn<F, R, B>(
self,
mw: F,
) -> App<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B1>,
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
>
where
T: ServiceFactory<
ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
Config = (),
InitError = (),
>,
F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static,
R: Future<Output = Result<ServiceResponse<B>, Error>>,
B: MessageBody,
F: Fn(ServiceRequest, &T::Service) -> R + Clone,
R: Future<Output = Result<ServiceResponse<B1>, Error>>,
B1: MessageBody,
{
App {
endpoint: apply_fn_factory(self.endpoint, mw),
@ -457,15 +455,14 @@ impl<T> App<T> {
impl<T, B> IntoServiceFactory<AppInit<T, B>, Request> for App<T>
where
B: MessageBody,
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
T::Future: 'static,
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
> + 'static,
B: MessageBody,
{
fn into_factory(self) -> AppInit<T, B> {
AppInit {

View File

@ -201,27 +201,29 @@ where
actix_service::forward_ready!(service);
fn call(&self, mut req: Request) -> Self::Future {
let req_data = Rc::new(RefCell::new(req.take_req_data()));
let extensions = Rc::new(RefCell::new(req.take_req_data()));
let conn_data = req.take_conn_data();
let (head, payload) = req.into_parts();
let req = if let Some(mut req) = self.app_state.pool().pop() {
let inner = Rc::get_mut(&mut req.inner).unwrap();
inner.path.get_mut().update(&head.uri);
inner.path.reset();
inner.head = head;
inner.conn_data = conn_data;
inner.req_data = req_data;
req
} else {
HttpRequest::new(
let req = match self.app_state.pool().pop() {
Some(mut req) => {
let inner = Rc::get_mut(&mut req.inner).unwrap();
inner.path.get_mut().update(&head.uri);
inner.path.reset();
inner.head = head;
inner.conn_data = conn_data;
inner.extensions = extensions;
req
}
None => HttpRequest::new(
Path::new(Url::new(head.uri.clone())),
head,
self.app_state.clone(),
self.app_data.clone(),
Rc::clone(&self.app_state),
Rc::clone(&self.app_data),
conn_data,
req_data,
)
extensions,
),
};
self.service.call(ServiceRequest::new(req, payload))

View File

@ -215,6 +215,17 @@ impl ServiceConfig {
self
}
/// Run external configuration as part of the application building process
///
/// Counterpart to [`App::configure()`](crate::App::configure) that allows for easy nesting.
pub fn configure<F>(&mut self, f: F) -> &mut Self
where
F: FnOnce(&mut ServiceConfig),
{
f(self);
self
}
/// Configure route for a specific path.
///
/// Counterpart to [`App::route()`](crate::App::route).
@ -264,7 +275,7 @@ mod tests {
use super::*;
use crate::http::{Method, StatusCode};
use crate::test::{call_service, init_service, read_body, TestRequest};
use crate::test::{assert_body_eq, call_service, init_service, read_body, TestRequest};
use crate::{web, App, HttpRequest, HttpResponse};
// allow deprecated `ServiceConfig::data`
@ -288,38 +299,6 @@ mod tests {
assert_eq!(resp.status(), StatusCode::OK);
}
// #[actix_rt::test]
// async fn test_data_factory() {
// let cfg = |cfg: &mut ServiceConfig| {
// cfg.data_factory(|| {
// sleep(std::time::Duration::from_millis(50)).then(|_| {
// println!("READY");
// Ok::<_, ()>(10usize)
// })
// });
// };
// let srv =
// init_service(App::new().configure(cfg).service(
// web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok()),
// ));
// let req = TestRequest::default().to_request();
// let resp = srv.call(req).await.unwrap();
// assert_eq!(resp.status(), StatusCode::OK);
// let cfg2 = |cfg: &mut ServiceConfig| {
// cfg.data_factory(|| Ok::<_, ()>(10u32));
// };
// let srv = init_service(
// App::new()
// .service(web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok()))
// .configure(cfg2),
// );
// let req = TestRequest::default().to_request();
// let resp = srv.call(req).await.unwrap();
// assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
// }
#[actix_rt::test]
async fn test_external_resource() {
let srv = init_service(
@ -363,4 +342,22 @@ mod tests {
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn nested_service_configure() {
fn cfg_root(cfg: &mut ServiceConfig) {
cfg.configure(cfg_sub);
}
fn cfg_sub(cfg: &mut ServiceConfig) {
cfg.route("/", web::get().to(|| async { "hello world" }));
}
let srv = init_service(App::new().configure(cfg_root)).await;
let req = TestRequest::with_uri("/").to_request();
let res = call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_body_eq!(res, b"hello world");
}
}

View File

@ -19,23 +19,32 @@ pub(crate) trait DataFactory {
pub(crate) type FnDataFactory =
Box<dyn Fn() -> LocalBoxFuture<'static, Result<Box<dyn DataFactory>, ()>>>;
/// Application data.
/// Application data wrapper and extractor.
///
/// Application level data is a piece of arbitrary data attached to the app, scope, or resource.
/// Application data is available to all routes and can be added during the application
/// configuration process via `App::data()`.
/// # Setting Data
/// Data is set using the `app_data` methods on `App`, `Scope`, and `Resource`. If data is wrapped
/// in this `Data` type for those calls, it can be used as an extractor.
///
/// Application data can be accessed by using `Data<T>` extractor where `T` is data type.
/// Note that `Data` should be constructed _outside_ the `HttpServer::new` closure if shared,
/// potentially mutable state is desired. `Data` is cheap to clone; internally, it uses an `Arc`.
///
/// **Note**: HTTP server accepts an application factory rather than an application instance. HTTP
/// server constructs an application instance for each thread, thus application data must be
/// constructed multiple times. If you want to share data between different threads, a shareable
/// object should be used, e.g. `Send + Sync`. Application data does not need to be `Send`
/// or `Sync`. Internally `Data` contains an `Arc`.
/// See also [`App::app_data`](crate::App::app_data), [`Scope::app_data`](crate::Scope::app_data),
/// and [`Resource::app_data`](crate::Resource::app_data).
///
/// # Extracting `Data`
/// Since the Actix Web router layers application data, the returned object will reference the
/// "closest" instance of the type. For example, if an `App` stores a `u32`, a nested `Scope`
/// also stores a `u32`, and the delegated request handler falls within that `Scope`, then
/// extracting a `web::<Data<u32>>` for that handler will return the `Scope`'s instance.
/// However, using the same router set up and a request that does not get captured by the `Scope`,
/// `web::<Data<u32>>` would return the `App`'s instance.
///
/// If route data is not set for a handler, using `Data<T>` extractor would cause a `500 Internal
/// Server Error` response.
///
/// See also [`HttpRequest::app_data`]
/// and [`ServiceRequest::app_data`](crate::dev::ServiceRequest::app_data).
///
/// # Unsized Data
/// For types that are unsized, most commonly `dyn T`, `Data` can wrap these types by first
/// constructing an `Arc<dyn T>` and using the `From` implementation to convert it.
@ -79,6 +88,7 @@ pub(crate) type FnDataFactory =
/// .route("/index.html", web::get().to(index))
/// .route("/index-alt.html", web::get().to(index_alt));
/// ```
#[doc(alias = "state")]
#[derive(Debug)]
pub struct Data<T: ?Sized>(Arc<T>);
@ -90,12 +100,12 @@ impl<T> Data<T> {
}
impl<T: ?Sized> Data<T> {
/// Get reference to inner app data.
/// Returns reference to inner `T`.
pub fn get_ref(&self) -> &T {
self.0.as_ref()
}
/// Convert to the internal Arc<T>
/// Unwraps to the internal `Arc<T>`
pub fn into_inner(self) -> Arc<T> {
self.0
}
@ -143,13 +153,16 @@ impl<T: ?Sized + 'static> FromRequest for Data<T> {
ok(st.clone())
} else {
log::debug!(
"Failed to construct App-level Data extractor. \
Request path: {:?} (type: {})",
req.path(),
"Failed to extract `Data<{}>` for `{}` handler. For the Data extractor to work \
correctly, wrap the data with `Data::new()` and pass it to `App::app_data()`. \
Ensure that types align in both the set and retrieve calls.",
type_name::<T>(),
req.match_name().unwrap_or_else(|| req.path())
);
err(ErrorInternalServerError(
"App data is not configured, to configure construct it with web::Data::new() and pass it to App::app_data()",
"Requested application data is not configured correctly. \
View/enable debug logs for more details.",
))
}
}

View File

@ -20,11 +20,7 @@ pub use crate::info::{ConnectionInfo, PeerAddr};
pub use crate::rmap::ResourceMap;
pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService};
pub use crate::types::form::UrlEncoded;
pub use crate::types::json::JsonBody;
pub use crate::types::readlines::Readlines;
use crate::http::header::ContentEncoding;
pub use crate::types::{JsonBody, Readlines, UrlEncoded};
use actix_router::Patterns;
@ -46,60 +42,3 @@ pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns {
patterns
}
/// Helper trait that allows to set specific encoding for response.
pub trait BodyEncoding {
/// Get content encoding
fn get_encoding(&self) -> Option<ContentEncoding>;
/// Set content encoding
///
/// Must be used with [`crate::middleware::Compress`] to take effect.
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self;
}
impl BodyEncoding for actix_http::ResponseBuilder {
fn get_encoding(&self) -> Option<ContentEncoding> {
self.extensions().get::<Enc>().map(|enc| enc.0)
}
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self {
self.extensions_mut().insert(Enc(encoding));
self
}
}
struct Enc(ContentEncoding);
impl<B> BodyEncoding for actix_http::Response<B> {
fn get_encoding(&self) -> Option<ContentEncoding> {
self.extensions().get::<Enc>().map(|enc| enc.0)
}
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self {
self.extensions_mut().insert(Enc(encoding));
self
}
}
impl BodyEncoding for crate::HttpResponseBuilder {
fn get_encoding(&self) -> Option<ContentEncoding> {
self.extensions().get::<Enc>().map(|enc| enc.0)
}
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self {
self.extensions_mut().insert(Enc(encoding));
self
}
}
impl<B> BodyEncoding for crate::HttpResponse<B> {
fn get_encoding(&self) -> Option<ContentEncoding> {
self.extensions().get::<Enc>().map(|enc| enc.0)
}
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self {
self.extensions_mut().insert(Enc(encoding));
self
}
}

View File

@ -118,15 +118,13 @@ where
macro_rules! error_helper {
($name:ident, $status:ident) => {
paste::paste! {
#[doc = "Helper function that wraps any error and generates a `" $status "` response."]
#[allow(non_snake_case)]
pub fn $name<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::$status).into()
}
#[doc = concat!("Helper function that wraps any error and generates a `", stringify!($status), "` response.")]
#[allow(non_snake_case)]
pub fn $name<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::$status).into()
}
};
}

View File

@ -3,6 +3,7 @@
use std::{
convert::Infallible,
future::Future,
marker::PhantomData,
pin::Pin,
task::{Context, Poll},
};
@ -124,12 +125,11 @@ pub trait FromRequest: Sized {
/// );
/// }
/// ```
impl<T: 'static> FromRequest for Option<T>
impl<T> FromRequest for Option<T>
where
T: FromRequest,
T::Future: 'static,
{
type Error = Error;
type Error = Infallible;
type Future = FromRequestOptFuture<T::Future>;
#[inline]
@ -152,7 +152,7 @@ where
Fut: Future<Output = Result<T, E>>,
E: Into<Error>,
{
type Output = Result<Option<T>, Error>;
type Output = Result<Option<T>, Infallible>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
@ -211,40 +211,42 @@ where
/// );
/// }
/// ```
impl<T> FromRequest for Result<T, T::Error>
impl<T, E> FromRequest for Result<T, E>
where
T: FromRequest + 'static,
T::Error: 'static,
T::Future: 'static,
T: FromRequest,
T::Error: Into<E>,
{
type Error = Error;
type Future = FromRequestResFuture<T::Future>;
type Error = Infallible;
type Future = FromRequestResFuture<T::Future, E>;
#[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
FromRequestResFuture {
fut: T::from_request(req, payload),
_phantom: PhantomData,
}
}
}
pin_project! {
pub struct FromRequestResFuture<Fut> {
pub struct FromRequestResFuture<Fut, E> {
#[pin]
fut: Fut,
_phantom: PhantomData<E>,
}
}
impl<Fut, T, E> Future for FromRequestResFuture<Fut>
impl<Fut, T, Ei, E> Future for FromRequestResFuture<Fut, E>
where
Fut: Future<Output = Result<T, E>>,
Fut: Future<Output = Result<T, Ei>>,
Ei: Into<E>,
{
type Output = Result<Result<T, E>, Error>;
type Output = Result<Result<T, E>, Infallible>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let res = ready!(this.fut.poll(cx));
Poll::Ready(Ok(res))
Poll::Ready(Ok(res.map_err(Into::into)))
}
}
@ -290,16 +292,6 @@ impl FromRequest for Method {
}
}
#[doc(hidden)]
impl FromRequest for () {
type Error = Infallible;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(())
}
}
#[doc(hidden)]
#[allow(non_snake_case)]
mod tuple_from_req {
@ -388,6 +380,15 @@ mod tuple_from_req {
}
}
impl FromRequest for () {
type Error = Infallible;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(())
}
}
tuple_from_req! { TupleFromRequest1; A }
tuple_from_req! { TupleFromRequest2; A, B }
tuple_from_req! { TupleFromRequest3; A, B, C }
@ -398,6 +399,8 @@ mod tuple_from_req {
tuple_from_req! { TupleFromRequest8; A, B, C, D, E, F, G, H }
tuple_from_req! { TupleFromRequest9; A, B, C, D, E, F, G, H, I }
tuple_from_req! { TupleFromRequest10; A, B, C, D, E, F, G, H, I, J }
tuple_from_req! { TupleFromRequest11; A, B, C, D, E, F, G, H, I, J, K }
tuple_from_req! { TupleFromRequest12; A, B, C, D, E, F, G, H, I, J, K, L }
}
#[cfg(test)]
@ -480,7 +483,14 @@ mod tests {
.set_payload(Bytes::from_static(b"bye=world"))
.to_http_parts();
let r = Result::<Form<Info>, Error>::from_request(&req, &mut pl)
struct MyError;
impl From<Error> for MyError {
fn from(_: Error) -> Self {
Self
}
}
let r = Result::<Form<Info>, MyError>::from_request(&req, &mut pl)
.await
.unwrap();
assert!(r.is_err());

View File

@ -54,7 +54,7 @@ use std::{
use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead};
use crate::service::ServiceRequest;
use crate::{http::header::Header, service::ServiceRequest, HttpMessage as _};
/// Provides access to request parts that are useful during routing.
#[derive(Debug)]
@ -69,16 +69,36 @@ impl<'a> GuardContext<'a> {
self.req.head()
}
/// Returns reference to the request-local data container.
/// Returns reference to the request-local data/extensions container.
#[inline]
pub fn req_data(&self) -> Ref<'a, Extensions> {
self.req.req_data()
self.req.extensions()
}
/// Returns mutable reference to the request-local data container.
/// Returns mutable reference to the request-local data/extensions container.
#[inline]
pub fn req_data_mut(&self) -> RefMut<'a, Extensions> {
self.req.req_data_mut()
self.req.extensions_mut()
}
/// Extracts a typed header from the request.
///
/// Returns `None` if parsing `H` fails.
///
/// # Examples
/// ```
/// use actix_web::{guard::fn_guard, http::header};
///
/// let image_accept_guard = fn_guard(|ctx| {
/// match ctx.header::<header::Accept>() {
/// Some(hdr) => hdr.preference() == "image/*",
/// None => false,
/// }
/// });
/// ```
#[inline]
pub fn header<H: Header>(&self) -> Option<H> {
H::parse(self.req).ok()
}
}
@ -270,22 +290,20 @@ impl Guard for MethodGuard {
macro_rules! method_guard {
($method_fn:ident, $method_const:ident) => {
paste::paste! {
#[doc = " Creates a guard that matches the `" $method_const "` request method."]
///
/// # Examples
#[doc = " The route in this example will only respond to `" $method_const "` requests."]
/// ```
/// use actix_web::{guard, web, HttpResponse};
///
/// web::route()
#[doc = " .guard(guard::" $method_fn "())"]
/// .to(|| HttpResponse::Ok());
/// ```
#[allow(non_snake_case)]
pub fn $method_fn() -> impl Guard {
MethodGuard(HttpMethod::$method_const)
}
#[doc = concat!("Creates a guard that matches the `", stringify!($method_const), "` request method.")]
///
/// # Examples
#[doc = concat!("The route in this example will only respond to `", stringify!($method_const), "` requests.")]
/// ```
/// use actix_web::{guard, web, HttpResponse};
///
/// web::route()
#[doc = concat!(" .guard(guard::", stringify!($method_fn), "())")]
/// .to(|| HttpResponse::Ok());
/// ```
#[allow(non_snake_case)]
pub fn $method_fn() -> impl Guard {
MethodGuard(HttpMethod::$method_const)
}
};
}

View File

@ -12,17 +12,14 @@ use crate::{
/// # What Is A Request Handler
/// A request handler has three requirements:
/// 1. It is an async function (or a function/closure that returns an appropriate future);
/// 1. The function accepts zero or more parameters that implement [`FromRequest`];
/// 1. The function parameters (up to 12) implement [`FromRequest`];
/// 1. The async function (or future) resolves to a type that can be converted into an
/// [`HttpResponse`] (i.e., it implements the [`Responder`] trait).
///
/// # Compiler Errors
/// If you get the error `the trait Handler<_> is not implemented`, then your handler does not
/// fulfill one or more of the above requirements.
///
/// Unfortunately we cannot provide a better compile error message (while keeping the trait's
/// flexibility) unless a stable alternative to [`#[rustc_on_unimplemented]`][on_unimpl] is added
/// to Rust.
/// fulfill the _first_ of the above requirements. Missing other requirements manifest as errors on
/// implementing [`FromRequest`] and [`Responder`], respectively.
///
/// # How Do Handlers Receive Variable Numbers Of Arguments
/// Rest assured there is no macro magic here; it's just traits.
@ -62,13 +59,15 @@ use crate::{
/// This is the source code for the 2-parameter implementation of `Handler` to help illustrate the
/// bounds of the handler call after argument extraction:
/// ```ignore
/// impl<Func, Arg1, Arg2, R> Handler<(Arg1, Arg2), R> for Func
/// impl<Func, Arg1, Arg2, Fut> Handler<(Arg1, Arg2)> for Func
/// where
/// Func: Fn(Arg1, Arg2) -> R + Clone + 'static,
/// R: Future,
/// R::Output: Responder,
/// Func: Fn(Arg1, Arg2) -> Fut + Clone + 'static,
/// Fut: Future,
/// {
/// fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> R {
/// type Output = Fut::Output;
/// type Future = Fut;
///
/// fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> Self::Future {
/// (self)(arg1, arg2)
/// }
/// }
@ -76,7 +75,6 @@ use crate::{
///
/// [arity]: https://en.wikipedia.org/wiki/Arity
/// [`from_request`]: FromRequest::from_request
/// [on_unimpl]: https://github.com/rust-lang/rust/issues/29628
pub trait Handler<Args>: Clone + 'static {
type Output;
type Future: Future<Output = Self::Output>;
@ -121,8 +119,9 @@ where
/// ```
macro_rules! factory_tuple ({ $($param:ident)* } => {
impl<Func, Fut, $($param,)*> Handler<($($param,)*)> for Func
where Func: Fn($($param),*) -> Fut + Clone + 'static,
Fut: Future,
where
Func: Fn($($param),*) -> Fut + Clone + 'static,
Fut: Future,
{
type Output = Fut::Output;
type Future = Fut;
@ -148,3 +147,25 @@ factory_tuple! { A B C D E F G H I }
factory_tuple! { A B C D E F G H I J }
factory_tuple! { A B C D E F G H I J K }
factory_tuple! { A B C D E F G H I J K L }
#[cfg(test)]
mod tests {
use super::*;
fn assert_impl_handler<T: FromRequest>(_: impl Handler<T>) {}
#[test]
fn arg_number() {
async fn handler_min() {}
#[rustfmt::skip]
#[allow(clippy::too_many_arguments, clippy::just_underscores_and_digits)]
async fn handler_max(
_01: (), _02: (), _03: (), _04: (), _05: (), _06: (),
_07: (), _08: (), _09: (), _10: (), _11: (), _12: (),
) {}
assert_impl_handler(handler_min);
assert_impl_handler(handler_max);
}
}

View File

@ -2,10 +2,10 @@ use std::cmp::Ordering;
use mime::Mime;
use super::QualityItem;
use super::{common_header, QualityItem};
use crate::http::header;
crate::http::header::common_header! {
common_header! {
/// `Accept` header, defined
/// in [RFC 7231 §5.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2)
///
@ -147,6 +147,39 @@ impl Accept {
Accept(vec![QualityItem::max(mime::TEXT_HTML)])
}
// TODO: method for getting best content encoding based on q-factors, available from server side
// and if none are acceptable return None
/// Extracts the most preferable mime type, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first mime type is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained
/// list is empty.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Mime {
use actix_http::header::Quality;
let mut max_item = None;
let mut max_pref = Quality::ZERO;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
max_item.unwrap_or(mime::STAR_STAR)
}
/// Returns a sorted list of mime types from highest to lowest preference, accounting for
/// [q-factor weighting] and specificity.
///
@ -196,36 +229,6 @@ impl Accept {
types.into_iter().map(|qitem| qitem.item).collect()
}
/// Extracts the most preferable mime type, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first mime type is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained
/// list is empty.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Mime {
use actix_http::header::Quality;
let mut max_item = None;
let mut max_pref = Quality::MIN;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
max_item.unwrap_or(mime::STAR_STAR)
}
}
#[cfg(test)]
@ -239,7 +242,7 @@ mod tests {
assert!(test.ranked().is_empty());
let test = Accept(vec![QualityItem::max(mime::APPLICATION_JSON)]);
assert_eq!(test.ranked(), vec!(mime::APPLICATION_JSON));
assert_eq!(test.ranked(), vec![mime::APPLICATION_JSON]);
let test = Accept(vec![
QualityItem::max(mime::TEXT_HTML),

View File

@ -1,8 +1,7 @@
use super::{Charset, QualityItem, ACCEPT_CHARSET};
use super::{common_header, Charset, QualityItem, ACCEPT_CHARSET};
crate::http::header::common_header! {
/// `Accept-Charset` header, defined in
/// [RFC 7231 §5.3.3](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.3)
common_header! {
/// `Accept-Charset` header, defined in [RFC 7231 §5.3.3].
///
/// The `Accept-Charset` header field can be sent by a user agent to
/// indicate what charsets are acceptable in textual response content.
@ -52,10 +51,12 @@ crate::http::header::common_header! {
/// AcceptCharset(vec![QualityItem::max(Charset::Ext("utf-8".to_owned()))])
/// );
/// ```
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)+
///
/// [RFC 7231 §5.3.3]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.3
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)*
test_parse_and_format {
// Test case from RFC
crate::http::header::common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]);
common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]);
}
}

View File

@ -1,17 +1,15 @@
use actix_http::header::QualityItem;
use std::collections::HashSet;
use super::{common_header, Encoding};
use super::{common_header, ContentEncoding, Encoding, Preference, Quality, QualityItem};
use crate::http::header;
common_header! {
/// `Accept-Encoding` header, defined
/// in [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4)
///
/// The `Accept-Encoding` header field can be used by user agents to
/// indicate what response content-codings are
/// acceptable in the response. An `identity` token is used as a synonym
/// for "no encoding" in order to communicate when no encoding is
/// preferred.
/// The `Accept-Encoding` header field can be used by user agents to indicate what response
/// content-codings are acceptable in the response. An `identity` token is used as a synonym
/// for "no encoding" in order to communicate when no encoding is preferred.
///
/// # ABNF
/// ```plain
@ -29,11 +27,11 @@ common_header! {
/// # Examples
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem};
/// use actix_web::http::header::{AcceptEncoding, Encoding, Preference, QualityItem};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// AcceptEncoding(vec![QualityItem::max(Encoding::Chunked)])
/// AcceptEncoding(vec![QualityItem::max(Preference::Specific(Encoding::gzip()))])
/// );
/// ```
///
@ -44,40 +42,388 @@ common_header! {
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// AcceptEncoding(vec![
/// QualityItem::max(Encoding::Chunked),
/// QualityItem::max(Encoding::Gzip),
/// QualityItem::max(Encoding::Deflate),
/// "gzip".parse().unwrap(),
/// "br".parse().unwrap(),
/// ])
/// );
/// ```
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem, q};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// AcceptEncoding(vec![
/// QualityItem::max(Encoding::Chunked),
/// QualityItem::new(Encoding::Gzip, q(0.60)),
/// QualityItem::min(Encoding::EncodingExt("*".to_owned())),
/// ])
/// );
/// ```
(AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem<Encoding>)*
(AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem<Preference<Encoding>>)*
test_parse_and_format {
// From the RFC
common_header_test!(test1, vec![b"compress, gzip"]);
common_header_test!(test2, vec![b""], Some(AcceptEncoding(vec![])));
common_header_test!(test3, vec![b"*"]);
common_header_test!(no_headers, vec![b""; 0], Some(AcceptEncoding(vec![])));
common_header_test!(empty_header, vec![b""; 1], Some(AcceptEncoding(vec![])));
common_header_test!(
order_of_appearance,
vec![b"br, gzip"],
Some(AcceptEncoding(vec![
QualityItem::max(Preference::Specific(Encoding::brotli())),
QualityItem::max(Preference::Specific(Encoding::gzip())),
]))
);
common_header_test!(any, vec![b"*"], Some(AcceptEncoding(vec![
QualityItem::max(Preference::Any),
])));
// Note: Removed quality 1 from gzip
common_header_test!(test4, vec![b"compress;q=0.5, gzip"]);
common_header_test!(implicit_quality, vec![b"gzip, identity; q=0.5, *;q=0"]);
// Note: Removed quality 1 from gzip
common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]);
common_header_test!(implicit_quality_out_of_order, vec![b"compress;q=0.5, gzip"]);
common_header_test!(
only_gzip_no_identity,
vec![b"gzip, *; q=0"],
Some(AcceptEncoding(vec![
QualityItem::max(Preference::Specific(Encoding::gzip())),
QualityItem::zero(Preference::Any),
]))
);
}
}
// TODO: shortcut for EncodingExt(*) = Any
impl AcceptEncoding {
/// Selects the most acceptable encoding according to client preference and supported types.
///
/// The "identity" encoding is not assumed and should be included in the `supported` iterator
/// if a non-encoded representation can be selected.
///
/// If `None` is returned, this indicates that none of the supported encodings are acceptable to
/// the client. The caller should generate a 406 Not Acceptable response (unencoded) that
/// includes the server's supported encodings in the body plus a [`Vary`] header.
///
/// [`Vary`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary
pub fn negotiate<'a>(
&self,
supported: impl Iterator<Item = &'a Encoding>,
) -> Option<Encoding> {
// 1. If no Accept-Encoding field is in the request, any content-coding is considered
// acceptable by the user agent.
let supported_set = supported.collect::<HashSet<_>>();
if supported_set.is_empty() {
return None;
}
if self.0.is_empty() {
// though it is not recommended to encode in this case, return identity encoding
return Some(Encoding::identity());
}
// 2. If the representation has no content-coding, then it is acceptable by default unless
// specifically excluded by the Accept-Encoding field stating either "identity;q=0" or
// "*;q=0" without a more specific entry for "identity".
let acceptable_items = self.ranked_items().collect::<Vec<_>>();
let identity_acceptable = is_identity_acceptable(&acceptable_items);
let identity_supported = supported_set.contains(&Encoding::identity());
if identity_acceptable && identity_supported && supported_set.len() == 1 {
return Some(Encoding::identity());
}
// 3. If the representation's content-coding is one of the content-codings listed in the
// Accept-Encoding field, then it is acceptable unless it is accompanied by a qvalue of 0.
// 4. If multiple content-codings are acceptable, then the acceptable content-coding with
// the highest non-zero qvalue is preferred.
let matched = acceptable_items
.into_iter()
.filter(|q| q.quality > Quality::ZERO)
// search relies on item list being in descending order of quality
.find(|q| {
let enc = &q.item;
matches!(enc, Preference::Specific(enc) if supported_set.contains(enc))
})
.map(|q| q.item);
match matched {
Some(Preference::Specific(enc)) => Some(enc),
_ if identity_acceptable => Some(Encoding::identity()),
_ => None,
}
}
/// Extracts the most preferable encoding, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first encoding is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, returns [`Preference::Any`] if acceptable list is empty. Though, if this is
/// returned, it is recommended to use an un-encoded representation.
///
/// If `None` is returned, it means that the client has signalled that no representations
/// are acceptable. This should never occur for a well behaved user-agent.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Option<Preference<Encoding>> {
// empty header indicates no preference
if self.0.is_empty() {
return Some(Preference::Any);
}
let mut max_item = None;
let mut max_pref = Quality::ZERO;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
// Return max_item if any items were above 0 quality...
max_item.or_else(|| {
// ...or else check for "*" or "identity". We can elide quality checks since
// entering this block means all items had "q=0".
match self.0.iter().find(|pref| {
matches!(
pref.item,
Preference::Any
| Preference::Specific(Encoding::Known(ContentEncoding::Identity))
)
}) {
// "identity" or "*" found so no representation is acceptable
Some(_) => None,
// implicit "identity" is acceptable
None => Some(Preference::Specific(Encoding::identity())),
}
})
}
/// Returns a sorted list of encodings from highest to lowest precedence, accounting
/// for [q-factor weighting].
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn ranked(&self) -> Vec<Preference<Encoding>> {
self.ranked_items().map(|q| q.item).collect()
}
fn ranked_items(&self) -> impl Iterator<Item = QualityItem<Preference<Encoding>>> {
if self.0.is_empty() {
return vec![].into_iter();
}
let mut types = self.0.clone();
// use stable sort so items with equal q-factor retain listed order
types.sort_by(|a, b| {
// sort by q-factor descending
b.quality.cmp(&a.quality)
});
types.into_iter()
}
}
/// Returns true if "identity" is an acceptable encoding.
///
/// Internal algorithm relies on item list being in descending order of quality.
fn is_identity_acceptable(items: &'_ [QualityItem<Preference<Encoding>>]) -> bool {
if items.is_empty() {
return true;
}
// Loop algorithm depends on items being sorted in descending order of quality. As such, it
// is sufficient to return (q > 0) when reaching either an "identity" or "*" item.
for q in items {
match (q.quality, &q.item) {
// occurrence of "identity;q=n"; return true if quality is non-zero
(q, Preference::Specific(Encoding::Known(ContentEncoding::Identity))) => {
return q > Quality::ZERO
}
// occurrence of "*;q=n"; return true if quality is non-zero
(q, Preference::Any) => return q > Quality::ZERO,
_ => {}
}
}
// implicit acceptable identity
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::header::*;
macro_rules! accept_encoding {
() => { AcceptEncoding(vec![]) };
($($q:expr),+ $(,)?) => { AcceptEncoding(vec![$($q.parse().unwrap()),+]) };
}
/// Parses an encoding string.
fn enc(enc: &str) -> Preference<Encoding> {
enc.parse().unwrap()
}
#[test]
fn detect_identity_acceptable() {
macro_rules! accept_encoding_ranked {
() => { accept_encoding!().ranked_items().collect::<Vec<_>>() };
($($q:expr),+ $(,)?) => { accept_encoding!($($q),+).ranked_items().collect::<Vec<_>>() };
}
let test = accept_encoding_ranked!();
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "br");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "*;q=0.1");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "identity;q=0.1");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "identity;q=0.1", "*;q=0");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0.1");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "*;q=0");
assert!(!is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "identity;q=0");
assert!(!is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "identity;q=0", "*;q=0");
assert!(!is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0");
assert!(!is_identity_acceptable(&test));
}
#[test]
fn encoding_negotiation() {
// no preference
let test = accept_encoding!();
assert_eq!(test.negotiate([].iter()), None);
let test = accept_encoding!();
assert_eq!(
test.negotiate([Encoding::identity()].iter()),
Some(Encoding::identity()),
);
let test = accept_encoding!("identity;q=0");
assert_eq!(test.negotiate([Encoding::identity()].iter()), None);
let test = accept_encoding!("*;q=0");
assert_eq!(test.negotiate([Encoding::identity()].iter()), None);
let test = accept_encoding!();
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::identity()),
);
let test = accept_encoding!("gzip");
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
Some(Encoding::identity()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
let test = accept_encoding!("gzip", "identity;q=0");
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
None
);
let test = accept_encoding!("gzip", "*;q=0");
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
None
);
let test = accept_encoding!("gzip", "deflate", "br");
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
Some(Encoding::brotli())
);
assert_eq!(
test.negotiate([Encoding::deflate(), Encoding::identity()].iter()),
Some(Encoding::deflate())
);
assert_eq!(
test.negotiate(
[Encoding::gzip(), Encoding::deflate(), Encoding::identity()].iter()
),
Some(Encoding::gzip())
);
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::brotli(), Encoding::identity()].iter()),
Some(Encoding::gzip())
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip())
);
}
#[test]
fn ranking_precedence() {
let test = accept_encoding!();
assert!(test.ranked().is_empty());
let test = accept_encoding!("gzip");
assert_eq!(test.ranked(), vec![enc("gzip")]);
let test = accept_encoding!("gzip;q=0.900", "*;q=0.700", "br;q=1.0");
assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]);
let test = accept_encoding!("br", "gzip", "*");
assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]);
}
#[test]
fn preference_selection() {
assert_eq!(accept_encoding!().preference(), Some(Preference::Any));
assert_eq!(accept_encoding!("identity;q=0").preference(), None);
assert_eq!(accept_encoding!("*;q=0").preference(), None);
assert_eq!(accept_encoding!("compress;q=0", "*;q=0").preference(), None);
assert_eq!(accept_encoding!("identity;q=0", "*;q=0").preference(), None);
let test = accept_encoding!("*;q=0.5");
assert_eq!(test.preference().unwrap(), enc("*"));
let test = accept_encoding!("br;q=0");
assert_eq!(test.preference().unwrap(), enc("identity"));
let test = accept_encoding!("br;q=0.900", "gzip;q=1.0", "*;q=0.500");
assert_eq!(test.preference().unwrap(), enc("gzip"));
let test = accept_encoding!("br", "gzip", "*");
assert_eq!(test.preference().unwrap(), enc("br"));
}
}

View File

@ -37,7 +37,7 @@ common_header! {
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// AcceptLanguage(vec![
/// QualityItem::max("en-US".parse().unwrap())
/// "en-US".parse().unwrap(),
/// ])
/// );
/// ```
@ -49,9 +49,9 @@ common_header! {
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// AcceptLanguage(vec![
/// QualityItem::max("da".parse().unwrap()),
/// QualityItem::new("en-GB".parse().unwrap(), q(0.8)),
/// QualityItem::new("en".parse().unwrap(), q(0.7)),
/// "da".parse().unwrap(),
/// "en-GB;q=0.8".parse().unwrap(),
/// "en;q=0.7".parse().unwrap(),
/// ])
/// );
/// ```
@ -93,6 +93,33 @@ common_header! {
}
impl AcceptLanguage {
/// Extracts the most preferable language, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first language is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, returns [`Preference::Any`] if contained list is empty.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Preference<LanguageTag> {
let mut max_item = None;
let mut max_pref = Quality::ZERO;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
max_item.unwrap_or(Preference::Any)
}
/// Returns a sorted list of languages from highest to lowest precedence, accounting
/// for [q-factor weighting].
///
@ -112,33 +139,6 @@ impl AcceptLanguage {
types.into_iter().map(|qitem| qitem.item).collect()
}
/// Extracts the most preferable language, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first language is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, returns [`Preference::Any`] if contained list is empty.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Preference<LanguageTag> {
let mut max_item = None;
let mut max_pref = Quality::MIN;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
max_item.unwrap_or(Preference::Any)
}
}
#[cfg(test)]
@ -152,7 +152,7 @@ mod tests {
assert!(test.ranked().is_empty());
let test = AcceptLanguage(vec![QualityItem::max("fr-CH".parse().unwrap())]);
assert_eq!(test.ranked(), vec!("fr-CH".parse().unwrap()));
assert_eq!(test.ranked(), vec!["fr-CH".parse().unwrap()]);
let test = AcceptLanguage(vec![
QualityItem::new("fr".parse().unwrap(), q(0.900)),

View File

@ -301,7 +301,6 @@ impl DispositionParam {
/// change to match local file system conventions if applicable, and do not use directory path
/// information that may be present.
/// See [RFC 2183 §2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3).
// TODO: think about using private fields and smallvec
#[derive(Clone, Debug, PartialEq)]
pub struct ContentDisposition {
/// The disposition type

Some files were not shown because too many files have changed in this diff Show More