mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-04 01:51:30 +02:00
Compare commits
51 Commits
codegen-v4
...
files-v0.6
Author | SHA1 | Date | |
---|---|---|---|
c15016dafb | |||
74688843ba | |||
845156da85 | |||
98752c053c | |||
df6fde883c | |||
8d4cb8c69a | |||
dd9ac4d9b8 | |||
72c80f9107 | |||
b00fe72cf6 | |||
2f0b8a264a | |||
b9f0faafde | |||
6627109984 | |||
b9f54c8796 | |||
cfd40b4f15 | |||
08c2cdf641 | |||
fbd0e5dd0a | |||
7b936bc443 | |||
d2364c80c4 | |||
77459ec415 | |||
6f0a6bd1bb | |||
06c3513bc0 | |||
29bd6a1dd5 | |||
17f7cd2aae | |||
ede645ee4e | |||
6d48593a60 | |||
3c69d078b2 | |||
e7c34f2e45 | |||
d708a4de6d | |||
d97bd7ec17 | |||
fcd06c9896 | |||
1065043528 | |||
45b77c6819 | |||
a2e2c30d59 | |||
83cd061c86 | |||
068909f1b3 | |||
f8cb71e789 | |||
73b94e902d | |||
ad7e67f940 | |||
1519ae7772 | |||
cc7145d41d | |||
172c4c7a0a | |||
fd63305859 | |||
ef64d6a27c | |||
4d3689db5e | |||
894effb856 | |||
07a7290432 | |||
bd5c0af0a6 | |||
c73fba16ce | |||
909461087c | |||
40f7ab38d2 | |||
a9e44bcf07 |
3
.github/workflows/bench.yml
vendored
3
.github/workflows/bench.yml
vendored
@ -5,6 +5,9 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
check_benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
|
34
.github/workflows/ci-post-merge.yml
vendored
34
.github/workflows/ci-post-merge.yml
vendored
@ -4,6 +4,9 @@ on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
build_and_test_nightly:
|
||||
strategy:
|
||||
@ -92,29 +95,21 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable-x86_64-unknown-linux-gnu
|
||||
profile: minimal
|
||||
override: true
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install cargo-hack
|
||||
uses: taiki-e/install-action@cargo-hack
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
run: cargo generate-lockfile
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
- name: check feature combinations
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-all-feature-powerset }
|
||||
run: cargo ci-check-all-feature-powerset
|
||||
|
||||
- name: check feature combinations
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-all-feature-powerset-linux }
|
||||
run: cargo ci-check-all-feature-powerset-linux
|
||||
|
||||
nextest:
|
||||
name: nextest
|
||||
@ -127,24 +122,15 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
run: cargo generate-lockfile
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.3.0
|
||||
|
||||
- name: Test with cargo-nextest
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: nextest
|
||||
args: run
|
||||
run: cargo nextest run
|
||||
|
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@ -6,6 +6,9 @@ on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
strategy:
|
||||
@ -63,6 +66,11 @@ jobs:
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
- name: workaround MSRV issues
|
||||
if: matrix.version != 'stable'
|
||||
run: |
|
||||
cargo update -p=zstd-sys --precise=2.0.1+zstd.1.5.2
|
||||
|
||||
- name: check minimal
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-min }
|
||||
@ -96,16 +104,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable-x86_64-unknown-linux-gnu
|
||||
profile: minimal
|
||||
override: true
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
run: cargo generate-lockfile
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.3.0
|
||||
|
||||
@ -123,20 +125,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust (nightly)
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly-x86_64-unknown-linux-gnu
|
||||
profile: minimal
|
||||
override: true
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
run: cargo generate-lockfile
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.3.0
|
||||
|
||||
- name: doc tests
|
||||
uses: actions-rs/cargo@v1
|
||||
run: cargo ci-doctest
|
||||
timeout-minutes: 60
|
||||
with: { command: ci-doctest }
|
||||
|
39
.github/workflows/clippy-fmt.yml
vendored
39
.github/workflows/clippy-fmt.yml
vendored
@ -9,54 +9,37 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
components: rustfmt
|
||||
- name: Check with rustfmt
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
with: { components: rustfmt }
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
components: clippy
|
||||
override: true
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with: { components: clippy }
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
run: cargo 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 --tests --examples --all-features
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
lint-docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
components: rust-docs
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with: { components: rust-docs }
|
||||
|
||||
- name: Check for broken intra-doc links
|
||||
uses: actions-rs/cargo@v1
|
||||
env:
|
||||
|
20
.github/workflows/upload-doc.yml
vendored
20
.github/workflows/upload-doc.yml
vendored
@ -4,31 +4,29 @@ on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: write # to push changes in repo (jamesives/github-pages-deploy-action)
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly-x86_64-unknown-linux-gnu
|
||||
profile: minimal
|
||||
override: true
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
|
||||
- name: Build Docs
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: doc
|
||||
args: --workspace --all-features --no-deps
|
||||
run: cargo +nightly doc --no-deps --workspace --all-features
|
||||
env:
|
||||
RUSTDOCFLAGS: --cfg=docsrs
|
||||
|
||||
- name: Tweak HTML
|
||||
run: echo '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > target/doc/index.html
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: JamesIves/github-pages-deploy-action@v4.4.0
|
||||
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
||||
with:
|
||||
folder: target/doc
|
||||
single-commit: true
|
||||
|
@ -1,9 +1,15 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||
|
||||
|
||||
## 0.6.3 - 2023-01-21
|
||||
- XHTML files now use `Content-Disposition: inline` instead of `attachment`. [#2903]
|
||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||
- Update `tokio-uring` dependency to `0.4`.
|
||||
|
||||
[#2903]: https://github.com/actix/actix-web/pull/2903
|
||||
|
||||
## 0.6.2 - 2022-07-23
|
||||
- Allow partial range responses for video content to start streaming sooner. [#2817]
|
||||
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||
|
@ -1,9 +1,8 @@
|
||||
[package]
|
||||
name = "actix-files"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"fakeshadow <24548779@qq.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
]
|
||||
description = "Static file serving for Actix Web"
|
||||
@ -30,7 +29,7 @@ actix-web = { version = "4", default-features = false }
|
||||
bitflags = "1"
|
||||
bytes = "1"
|
||||
derive_more = "0.99.5"
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||
http-range = "0.1.4"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
@ -41,8 +40,8 @@ v_htmlescape= "0.15"
|
||||
|
||||
# experimental-io-uring
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
tokio-uring = { version = "0.3", optional = true, features = ["bytes"] }
|
||||
actix-server = { version = "2.1", optional = true } # ensure matching tokio-uring versions
|
||||
tokio-uring = { version = "0.4", optional = true, features = ["bytes"] }
|
||||
actix-server = { version = "2.2", optional = true } # ensure matching tokio-uring versions
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.7"
|
||||
|
@ -3,11 +3,11 @@
|
||||
> Static file serving for Actix Web
|
||||
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://docs.rs/actix-files/0.6.2)
|
||||
[](https://docs.rs/actix-files/0.6.3)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.6.2)
|
||||
[](https://deps.rs/crate/actix-files/0.6.3)
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use actix_service::boxed::{BoxService, BoxServiceFactory};
|
||||
use actix_web::{
|
||||
|
@ -132,7 +132,7 @@ impl NamedFile {
|
||||
mime::IMAGE | mime::TEXT | mime::AUDIO | mime::VIDEO => DispositionType::Inline,
|
||||
mime::APPLICATION => match ct.subtype() {
|
||||
mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
|
||||
name if name == "wasm" => DispositionType::Inline,
|
||||
name if name == "wasm" || name == "xhtml" => DispositionType::Inline,
|
||||
_ => DispositionType::Attachment,
|
||||
},
|
||||
_ => DispositionType::Attachment,
|
||||
|
@ -23,7 +23,7 @@ impl Deref for FilesService {
|
||||
type Target = FilesServiceInner;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
|
||||
|
||||
## 3.1.0 - 2023-01-21
|
||||
- Minimum supported Rust version (MSRV) is now 1.59.
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http-test"
|
||||
version = "3.0.0"
|
||||
version = "3.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Various helpers for Actix applications to use during testing"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
@ -37,9 +37,8 @@ actix-rt = "2.2"
|
||||
actix-server = "2"
|
||||
awc = { version = "3", default-features = false }
|
||||
|
||||
base64 = "0.13"
|
||||
bytes = "1"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-core = { version = "0.3.17", default-features = false }
|
||||
http = "0.2.5"
|
||||
log = "0.4"
|
||||
socket2 = "0.4"
|
||||
@ -48,7 +47,7 @@ serde_json = "1.0"
|
||||
slab = "0.4"
|
||||
serde_urlencoded = "0.7"
|
||||
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||
tokio = { version = "1.8.4", features = ["sync"] }
|
||||
tokio = { version = "1.18.4", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = { version = "4", default-features = false, features = ["cookies"] }
|
||||
|
@ -3,11 +3,11 @@
|
||||
> Various helpers for Actix applications to use during testing.
|
||||
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://docs.rs/actix-http-test/3.0.0)
|
||||
[](https://docs.rs/actix-http-test/3.1.0)
|
||||

|
||||

|
||||
<br>
|
||||
[](https://deps.rs/crate/actix-http-test/3.0.0)
|
||||
[](https://deps.rs/crate/actix-http-test/3.1.0)
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
||||
@ -87,6 +88,7 @@ pub async fn test_server_with_addr<F: ServerServiceFactory<TcpStream>>(
|
||||
|
||||
// notify TestServer that server and system have shut down
|
||||
// all thread managed resources should be dropped at this point
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = thread_stop_tx.send(());
|
||||
});
|
||||
|
||||
@ -294,6 +296,7 @@ impl Drop for TestServer {
|
||||
// without needing to await anything
|
||||
|
||||
// signal server to stop
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = self.server.stop(true);
|
||||
|
||||
// signal system to stop
|
||||
|
@ -3,6 +3,39 @@
|
||||
## Unreleased - 2022-xx-xx
|
||||
|
||||
|
||||
## 3.3.0 - 2023-01-21
|
||||
### Added
|
||||
- Implement `MessageBody` for `Cow<'static, str>` and `Cow<'static, [u8]>`. [#2959]
|
||||
- Implement `MessageBody` for `&mut B` where `B: MessageBody + Unpin`. [#2868]
|
||||
- Implement `MessageBody` for `Pin<B>` where `B::Target: MessageBody`. [#2868]
|
||||
- Automatic h2c detection via new service finalizer `HttpService::tcp_auto_h2c()`. [#2957]
|
||||
- `HeaderMap::retain()` [#2955].
|
||||
- Header name constants in `header` module. [#2956] [#2968]
|
||||
- `CACHE_STATUS`
|
||||
- `CDN_CACHE_CONTROL`
|
||||
- `CROSS_ORIGIN_EMBEDDER_POLICY`
|
||||
- `CROSS_ORIGIN_OPENER_POLICY`
|
||||
- `PERMISSIONS_POLICY`
|
||||
- `X_FORWARDED_FOR`
|
||||
- `X_FORWARDED_HOST`
|
||||
- `X_FORWARDED_PROTO`
|
||||
|
||||
### Fixed
|
||||
- Fix non-empty body of HTTP/2 HEAD responses. [#2920]
|
||||
|
||||
### Performance
|
||||
- Improve overall performance of operations on `Extensions`. [#2890]
|
||||
|
||||
[#2959]: https://github.com/actix/actix-web/pull/2959
|
||||
[#2868]: https://github.com/actix/actix-web/pull/2868
|
||||
[#2890]: https://github.com/actix/actix-web/pull/2890
|
||||
[#2920]: https://github.com/actix/actix-web/pull/2920
|
||||
[#2957]: https://github.com/actix/actix-web/pull/2957
|
||||
[#2955]: https://github.com/actix/actix-web/pull/2955
|
||||
[#2956]: https://github.com/actix/actix-web/pull/2956
|
||||
[#2968]: https://github.com/actix/actix-web/pull/2968
|
||||
|
||||
|
||||
## 3.2.2 - 2022-09-11
|
||||
### Changed
|
||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http"
|
||||
version = "3.2.2"
|
||||
version = "3.3.0"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
@ -67,7 +67,7 @@ bytes = "1"
|
||||
bytestring = "1"
|
||||
derive_more = "0.99.5"
|
||||
encoding_rs = "0.8"
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||
http = "0.2.5"
|
||||
httparse = "1.5.1"
|
||||
httpdate = "1.0.1"
|
||||
@ -77,6 +77,8 @@ mime = "0.3"
|
||||
percent-encoding = "2.1"
|
||||
pin-project-lite = "0.2"
|
||||
smallvec = "1.6.1"
|
||||
tokio = { version = "1.18.4", features = [] }
|
||||
tokio-util = { version = "0.7", features = ["io", "codec"] }
|
||||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||
|
||||
# http2
|
||||
@ -84,7 +86,7 @@ h2 = { version = "0.3.9", optional = true }
|
||||
|
||||
# websockets
|
||||
local-channel = { version = "0.1", optional = true }
|
||||
base64 = { version = "0.13", optional = true }
|
||||
base64 = { version = "0.21", optional = true }
|
||||
rand = { version = "0.8", optional = true }
|
||||
sha1 = { version = "0.10", optional = true }
|
||||
|
||||
@ -94,7 +96,7 @@ actix-tls = { version = "3", default-features = false, optional = true }
|
||||
# compress-*
|
||||
brotli = { version = "3.3.3", optional = true }
|
||||
flate2 = { version = "1.0.13", optional = true }
|
||||
zstd = { version = "0.11", optional = true }
|
||||
zstd = { version = "0.12", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-http-test = { version = "3", features = ["openssl"] }
|
||||
@ -103,9 +105,9 @@ actix-tls = { version = "3", features = ["openssl"] }
|
||||
actix-web = "4"
|
||||
|
||||
async-stream = "0.3"
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
criterion = { version = "0.4", features = ["html_reports"] }
|
||||
env_logger = "0.9"
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||
memchr = "2.4"
|
||||
once_cell = "1.9"
|
||||
rcgen = "0.9"
|
||||
@ -117,7 +119,7 @@ serde_json = "1.0"
|
||||
static_assertions = "1"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||
tokio = { version = "1.8.4", features = ["net", "rt", "macros"] }
|
||||
tokio = { version = "1.18.4", features = ["net", "rt", "macros"] }
|
||||
|
||||
[[example]]
|
||||
name = "ws"
|
||||
|
@ -3,11 +3,11 @@
|
||||
> HTTP primitives for the Actix ecosystem.
|
||||
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/3.2.2)
|
||||
[](https://docs.rs/actix-http/3.3.0)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.2.2)
|
||||
[](https://deps.rs/crate/actix-http/3.3.0)
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
|
||||
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
|
||||
|
29
actix-http/examples/h2c-detect.rs
Normal file
29
actix-http/examples/h2c-detect.rs
Normal file
@ -0,0 +1,29 @@
|
||||
//! An example that supports automatic selection of plaintext h1/h2c connections.
|
||||
//!
|
||||
//! Notably, both the following commands will work.
|
||||
//! ```console
|
||||
//! $ curl --http1.1 'http://localhost:8080/'
|
||||
//! $ curl --http2-prior-knowledge 'http://localhost:8080/'
|
||||
//! ```
|
||||
|
||||
use std::{convert::Infallible, io};
|
||||
|
||||
use actix_http::{HttpService, Request, Response, StatusCode};
|
||||
use actix_server::Server;
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> io::Result<()> {
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
|
||||
Server::build()
|
||||
.bind("h2c-detect", ("127.0.0.1", 8080), || {
|
||||
HttpService::build()
|
||||
.finish(|_req: Request| async move {
|
||||
Ok::<_, Infallible>(Response::build(StatusCode::OK).body("Hello!"))
|
||||
})
|
||||
.tcp_auto_h2c()
|
||||
})?
|
||||
.workers(2)
|
||||
.run()
|
||||
.await
|
||||
}
|
@ -10,13 +10,13 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use actix_codec::Encoder;
|
||||
use actix_http::{body::BodyStream, error::Error, ws, HttpService, Request, Response};
|
||||
use actix_rt::time::{interval, Interval};
|
||||
use actix_server::Server;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use bytestring::ByteString;
|
||||
use futures_core::{ready, Stream};
|
||||
use tokio_util::codec::Encoder;
|
||||
use tracing::{info, trace};
|
||||
|
||||
#[actix_rt::main]
|
||||
|
@ -120,8 +120,28 @@ pub trait MessageBody {
|
||||
}
|
||||
|
||||
mod foreign_impls {
|
||||
use std::{borrow::Cow, ops::DerefMut};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl<B> MessageBody for &mut B
|
||||
where
|
||||
B: MessageBody + Unpin + ?Sized,
|
||||
{
|
||||
type Error = B::Error;
|
||||
|
||||
fn size(&self) -> BodySize {
|
||||
(**self).size()
|
||||
}
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
Pin::new(&mut **self).poll_next(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for Infallible {
|
||||
type Error = Infallible;
|
||||
|
||||
@ -179,8 +199,9 @@ mod foreign_impls {
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> MessageBody for Pin<Box<B>>
|
||||
impl<T, B> MessageBody for Pin<T>
|
||||
where
|
||||
T: DerefMut<Target = B> + Unpin,
|
||||
B: MessageBody + ?Sized,
|
||||
{
|
||||
type Error = B::Error;
|
||||
@ -303,6 +324,39 @@ mod foreign_impls {
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for Cow<'static, [u8]> {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
let bytes = match mem::take(self.get_mut()) {
|
||||
Cow::Borrowed(b) => Bytes::from_static(b),
|
||||
Cow::Owned(b) => Bytes::from(b),
|
||||
};
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
match self {
|
||||
Cow::Borrowed(b) => Ok(Bytes::from_static(b)),
|
||||
Cow::Owned(b) => Ok(Bytes::from(b)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for &'static str {
|
||||
type Error = Infallible;
|
||||
|
||||
@ -358,6 +412,39 @@ mod foreign_impls {
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for Cow<'static, str> {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
let bytes = match mem::take(self.get_mut()) {
|
||||
Cow::Borrowed(s) => Bytes::from_static(s.as_bytes()),
|
||||
Cow::Owned(s) => Bytes::from(s.into_bytes()),
|
||||
};
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
match self {
|
||||
Cow::Borrowed(s) => Ok(Bytes::from_static(s.as_bytes())),
|
||||
Cow::Owned(s) => Ok(Bytes::from(s.into_bytes())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for bytestring::ByteString {
|
||||
type Error = Infallible;
|
||||
|
||||
@ -445,6 +532,7 @@ mod tests {
|
||||
use actix_rt::pin;
|
||||
use actix_utils::future::poll_fn;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_util::stream;
|
||||
|
||||
use super::*;
|
||||
use crate::body::{self, EitherBody};
|
||||
@ -481,6 +569,34 @@ mod tests {
|
||||
assert_poll_next_none!(pl);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn mut_equivalence() {
|
||||
assert_eq!(().size(), BodySize::Sized(0));
|
||||
assert_eq!(().size(), (&(&mut ())).size());
|
||||
|
||||
let pl = &mut ();
|
||||
pin!(pl);
|
||||
assert_poll_next_none!(pl);
|
||||
|
||||
let pl = &mut Box::new(());
|
||||
pin!(pl);
|
||||
assert_poll_next_none!(pl);
|
||||
|
||||
let mut body = body::SizedStream::new(
|
||||
8,
|
||||
stream::iter([
|
||||
Ok::<_, std::io::Error>(Bytes::from("1234")),
|
||||
Ok(Bytes::from("5678")),
|
||||
]),
|
||||
);
|
||||
let body = &mut body;
|
||||
assert_eq!(body.size(), BodySize::Sized(8));
|
||||
pin!(body);
|
||||
assert_poll_next!(body, Bytes::from_static(b"1234"));
|
||||
assert_poll_next!(body, Bytes::from_static(b"5678"));
|
||||
assert_poll_next_none!(body);
|
||||
}
|
||||
|
||||
#[allow(clippy::let_unit_value)]
|
||||
#[actix_rt::test]
|
||||
async fn test_unit() {
|
||||
@ -607,4 +723,18 @@ mod tests {
|
||||
let not_body = resp_body.downcast_ref::<()>();
|
||||
assert!(not_body.is_none());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn non_owning_to_bytes() {
|
||||
let mut body = BoxBody::new(());
|
||||
let bytes = body::to_bytes(&mut body).await.unwrap();
|
||||
assert_eq!(bytes, Bytes::new());
|
||||
|
||||
let mut body = body::BodyStream::new(stream::iter([
|
||||
Ok::<_, std::io::Error>(Bytes::from("1234")),
|
||||
Ok(Bytes::from("5678")),
|
||||
]));
|
||||
let bytes = body::to_bytes(&mut body).await.unwrap();
|
||||
assert_eq!(bytes, Bytes::from_static(b"12345678"));
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ where
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.size as u64)
|
||||
BodySize::Sized(self.size)
|
||||
}
|
||||
|
||||
/// Attempts to pull out the next value of the underlying [`Stream`].
|
||||
|
@ -42,7 +42,7 @@ pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
|
||||
let body = body.as_mut();
|
||||
|
||||
match ready!(body.poll_next(cx)) {
|
||||
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
|
||||
Some(Ok(bytes)) => buf.extend_from_slice(&bytes),
|
||||
None => return Poll::Ready(Ok(())),
|
||||
Some(Err(err)) => return Poll::Ready(Err(err)),
|
||||
}
|
||||
|
@ -186,7 +186,7 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
/// Finish service configuration and create a HTTP Service for HTTP/1 protocol.
|
||||
/// Finish service configuration and create a service for the HTTP/1 protocol.
|
||||
pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
|
||||
where
|
||||
B: MessageBody,
|
||||
@ -209,7 +209,7 @@ where
|
||||
.on_connect_ext(self.on_connect_ext)
|
||||
}
|
||||
|
||||
/// Finish service configuration and create a HTTP service for HTTP/2 protocol.
|
||||
/// Finish service configuration and create a service for the HTTP/2 protocol.
|
||||
#[cfg(feature = "http2")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
||||
pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>
|
||||
|
@ -1,9 +1,30 @@
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
hash::{BuildHasherDefault, Hasher},
|
||||
};
|
||||
|
||||
use ahash::AHashMap;
|
||||
/// A hasher for `TypeId`s that takes advantage of its known characteristics.
|
||||
///
|
||||
/// Author of `anymap` crate has done research on the topic:
|
||||
/// https://github.com/chris-morgan/anymap/blob/2e9a5704/src/lib.rs#L599
|
||||
#[derive(Debug, Default)]
|
||||
struct NoOpHasher(u64);
|
||||
|
||||
impl Hasher for NoOpHasher {
|
||||
fn write(&mut self, _bytes: &[u8]) {
|
||||
unimplemented!("This NoOpHasher can only handle u64s")
|
||||
}
|
||||
|
||||
fn write_u64(&mut self, i: u64) {
|
||||
self.0 = i;
|
||||
}
|
||||
|
||||
fn finish(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A type map for request extensions.
|
||||
///
|
||||
@ -11,7 +32,7 @@ use ahash::AHashMap;
|
||||
#[derive(Default)]
|
||||
pub struct Extensions {
|
||||
/// Use AHasher with a std HashMap with for faster lookups on the small `TypeId` keys.
|
||||
map: AHashMap<TypeId, Box<dyn Any>>,
|
||||
map: HashMap<TypeId, Box<dyn Any>, BuildHasherDefault<NoOpHasher>>,
|
||||
}
|
||||
|
||||
impl Extensions {
|
||||
@ -19,7 +40,7 @@ impl Extensions {
|
||||
#[inline]
|
||||
pub fn new() -> Extensions {
|
||||
Extensions {
|
||||
map: AHashMap::new(),
|
||||
map: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,7 @@ impl ChunkedState {
|
||||
|
||||
match size.checked_mul(radix) {
|
||||
Some(n) => {
|
||||
*size = n as u64;
|
||||
*size = n;
|
||||
*size += rem as u64;
|
||||
|
||||
Poll::Ready(Ok(ChunkedState::Size))
|
||||
|
@ -1,9 +1,9 @@
|
||||
use std::{fmt, io};
|
||||
|
||||
use actix_codec::{Decoder, Encoder};
|
||||
use bitflags::bitflags;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use http::{Method, Version};
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
use super::{
|
||||
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
|
||||
|
@ -1,9 +1,9 @@
|
||||
use std::{fmt, io};
|
||||
|
||||
use actix_codec::{Decoder, Encoder};
|
||||
use bitflags::bitflags;
|
||||
use bytes::BytesMut;
|
||||
use http::{Method, Version};
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
use super::{
|
||||
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
|
||||
|
@ -8,13 +8,15 @@ use std::{
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Decoder as _, Encoder as _, Framed, FramedParts};
|
||||
use actix_codec::{Framed, FramedParts};
|
||||
use actix_rt::time::sleep_until;
|
||||
use actix_service::Service;
|
||||
use bitflags::bitflags;
|
||||
use bytes::{Buf, BytesMut};
|
||||
use futures_core::ready;
|
||||
use pin_project_lite::pin_project;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_util::codec::{Decoder as _, Encoder as _};
|
||||
use tracing::{error, trace};
|
||||
|
||||
use crate::{
|
||||
@ -1004,7 +1006,7 @@ where
|
||||
this.read_buf.reserve(HW_BUFFER_SIZE - remaining);
|
||||
}
|
||||
|
||||
match actix_codec::poll_read_buf(io.as_mut(), cx, this.read_buf) {
|
||||
match tokio_util::io::poll_read_buf(io.as_mut(), cx, this.read_buf) {
|
||||
Poll::Ready(Ok(n)) => {
|
||||
this.flags.remove(Flags::FINISHED);
|
||||
|
||||
|
@ -64,7 +64,7 @@ fn drop_payload_service(
|
||||
fn echo_payload_service() -> impl Service<Request, Response = Response<Bytes>, Error = Error> {
|
||||
fn_service(|mut req: Request| {
|
||||
Box::pin(async move {
|
||||
use futures_util::stream::StreamExt as _;
|
||||
use futures_util::StreamExt as _;
|
||||
|
||||
let mut pl = req.take_payload();
|
||||
let mut body = BytesMut::new();
|
||||
@ -637,7 +637,7 @@ async fn expect_handling() {
|
||||
|
||||
if let DispatcherState::Normal { ref inner } = h1.inner {
|
||||
let io = inner.io.as_ref().unwrap();
|
||||
let mut res = (&io.write_buf()[..]).to_owned();
|
||||
let mut res = io.write_buf()[..].to_owned();
|
||||
stabilize_date_header(&mut res);
|
||||
|
||||
assert_eq!(
|
||||
@ -699,7 +699,7 @@ async fn expect_eager() {
|
||||
|
||||
if let DispatcherState::Normal { ref inner } = h1.inner {
|
||||
let io = inner.io.as_ref().unwrap();
|
||||
let mut res = (&io.write_buf()[..]).to_owned();
|
||||
let mut res = io.write_buf()[..].to_owned();
|
||||
stabilize_date_header(&mut res);
|
||||
|
||||
// Despite the content-length header and even though the request payload has not
|
||||
|
@ -450,7 +450,7 @@ impl TransferEncoding {
|
||||
|
||||
buf.extend_from_slice(&msg[..len as usize]);
|
||||
|
||||
*remaining -= len as u64;
|
||||
*remaining -= len;
|
||||
Ok(*remaining == 0)
|
||||
} else {
|
||||
Ok(true)
|
||||
|
@ -29,7 +29,7 @@ use crate::{
|
||||
HeaderName, HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE,
|
||||
},
|
||||
service::HttpFlow,
|
||||
Extensions, OnConnectData, Payload, Request, Response, ResponseHead,
|
||||
Extensions, Method, OnConnectData, Payload, Request, Response, ResponseHead,
|
||||
};
|
||||
|
||||
const CHUNK_SIZE: usize = 16_384;
|
||||
@ -118,6 +118,7 @@ where
|
||||
let payload = crate::h2::Payload::new(body);
|
||||
let pl = Payload::H2 { payload };
|
||||
let mut req = Request::with_payload(pl);
|
||||
let head_req = parts.method == Method::HEAD;
|
||||
|
||||
let head = req.head_mut();
|
||||
head.uri = parts.uri;
|
||||
@ -135,10 +136,10 @@ where
|
||||
actix_rt::spawn(async move {
|
||||
// resolve service call and send response.
|
||||
let res = match fut.await {
|
||||
Ok(res) => handle_response(res.into(), tx, config).await,
|
||||
Ok(res) => handle_response(res.into(), tx, config, head_req).await,
|
||||
Err(err) => {
|
||||
let res: Response<BoxBody> = err.into();
|
||||
handle_response(res, tx, config).await
|
||||
handle_response(res, tx, config, head_req).await
|
||||
}
|
||||
};
|
||||
|
||||
@ -206,6 +207,7 @@ async fn handle_response<B>(
|
||||
res: Response<B>,
|
||||
mut tx: SendResponse<Bytes>,
|
||||
config: ServiceConfig,
|
||||
head_req: bool,
|
||||
) -> Result<(), DispatchError>
|
||||
where
|
||||
B: MessageBody,
|
||||
@ -215,14 +217,14 @@ where
|
||||
// prepare response.
|
||||
let mut size = body.size();
|
||||
let res = prepare_response(config, res.head(), &mut size);
|
||||
let eof = size.is_eof();
|
||||
let eof_or_head = size.is_eof() || head_req;
|
||||
|
||||
// send response head and return on eof.
|
||||
let mut stream = tx
|
||||
.send_response(res, eof)
|
||||
.send_response(res, eof_or_head)
|
||||
.map_err(DispatchError::SendResponse)?;
|
||||
|
||||
if eof {
|
||||
if eof_or_head {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
51
actix-http/src/header/common.rs
Normal file
51
actix-http/src/header/common.rs
Normal file
@ -0,0 +1,51 @@
|
||||
//! Common header names not defined in [`http`].
|
||||
//!
|
||||
//! Any headers added to this file will need to be re-exported from the list at `crate::headers`.
|
||||
|
||||
use http::header::HeaderName;
|
||||
|
||||
/// Response header field that indicates how caches have handled that response and its corresponding
|
||||
/// request.
|
||||
///
|
||||
/// See [RFC 9211](https://www.rfc-editor.org/rfc/rfc9211) for full semantics.
|
||||
pub const CACHE_STATUS: HeaderName = HeaderName::from_static("cache-status");
|
||||
|
||||
/// Response header field that allows origin servers to control the behavior of CDN caches
|
||||
/// interposed between them and clients separately from other caches that might handle the response.
|
||||
///
|
||||
/// See [RFC 9213](https://www.rfc-editor.org/rfc/rfc9213) for full semantics.
|
||||
pub const CDN_CACHE_CONTROL: HeaderName = HeaderName::from_static("cdn-cache-control");
|
||||
|
||||
/// Response header that prevents a document from loading any cross-origin resources that don't
|
||||
/// explicitly grant the document permission (using [CORP] or [CORS]).
|
||||
///
|
||||
/// [CORP]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy_(CORP)
|
||||
/// [CORS]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
||||
pub const CROSS_ORIGIN_EMBEDDER_POLICY: HeaderName =
|
||||
HeaderName::from_static("cross-origin-embedder-policy");
|
||||
|
||||
/// Response header that allows you to ensure a top-level document does not share a browsing context
|
||||
/// group with cross-origin documents.
|
||||
pub const CROSS_ORIGIN_OPENER_POLICY: HeaderName =
|
||||
HeaderName::from_static("cross-origin-opener-policy");
|
||||
|
||||
/// Response header that conveys a desire that the browser blocks no-cors cross-origin/cross-site
|
||||
/// requests to the given resource.
|
||||
pub const CROSS_ORIGIN_RESOURCE_POLICY: HeaderName =
|
||||
HeaderName::from_static("cross-origin-resource-policy");
|
||||
|
||||
/// Response header that provides a mechanism to allow and deny the use of browser features in a
|
||||
/// document or within any `<iframe>` elements in the document.
|
||||
pub const PERMISSIONS_POLICY: HeaderName = HeaderName::from_static("permissions-policy");
|
||||
|
||||
/// Request header (de-facto standard) for identifying the originating IP address of a client
|
||||
/// connecting to a web server through a proxy server.
|
||||
pub const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
|
||||
|
||||
/// Request header (de-facto standard) for identifying the original host requested by the client in
|
||||
/// the `Host` HTTP request header.
|
||||
pub const X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
|
||||
|
||||
/// Request header (de-facto standard) for identifying the protocol that a client used to connect to
|
||||
/// your proxy or load balancer.
|
||||
pub const X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
|
@ -150,9 +150,7 @@ impl HeaderMap {
|
||||
/// assert_eq!(map.len(), 3);
|
||||
/// ```
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner
|
||||
.iter()
|
||||
.fold(0, |acc, (_, values)| acc + values.len())
|
||||
self.inner.values().map(|vals| vals.len()).sum()
|
||||
}
|
||||
|
||||
/// Returns the number of _keys_ stored in the map.
|
||||
@ -309,7 +307,7 @@ impl HeaderMap {
|
||||
pub fn get_all(&self, key: impl AsHeaderName) -> std::slice::Iter<'_, HeaderValue> {
|
||||
match self.get_value(key) {
|
||||
Some(value) => value.iter(),
|
||||
None => (&[]).iter(),
|
||||
None => [].iter(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -552,6 +550,39 @@ impl HeaderMap {
|
||||
Keys(self.inner.keys())
|
||||
}
|
||||
|
||||
/// Retains only the headers specified by the predicate.
|
||||
///
|
||||
/// In other words, removes all headers `(name, val)` for which `retain_fn(&name, &mut val)`
|
||||
/// returns false.
|
||||
///
|
||||
/// The order in which headers are visited should be considered arbitrary.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||
/// let mut map = HeaderMap::new();
|
||||
///
|
||||
/// map.append(header::HOST, HeaderValue::from_static("duck.com"));
|
||||
/// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1"));
|
||||
/// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2"));
|
||||
///
|
||||
/// map.retain(|name, val| val.as_bytes().starts_with(b"one"));
|
||||
///
|
||||
/// assert_eq!(map.len(), 1);
|
||||
/// assert!(map.contains_key(&header::SET_COOKIE));
|
||||
/// ```
|
||||
pub fn retain<F>(&mut self, mut retain_fn: F)
|
||||
where
|
||||
F: FnMut(&HeaderName, &mut HeaderValue) -> bool,
|
||||
{
|
||||
self.inner.retain(|name, vals| {
|
||||
vals.inner.retain(|val| retain_fn(name, val));
|
||||
|
||||
// invariant: make sure newly empty value lists are removed
|
||||
!vals.is_empty()
|
||||
})
|
||||
}
|
||||
|
||||
/// Clears the map, returning all name-value sets as an iterator.
|
||||
///
|
||||
/// Header names will only be yielded for the first value in each set. All items that are
|
||||
@ -943,6 +974,55 @@ mod tests {
|
||||
assert!(map.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retain() {
|
||||
let mut map = HeaderMap::new();
|
||||
|
||||
map.append(header::LOCATION, HeaderValue::from_static("/test"));
|
||||
map.append(header::HOST, HeaderValue::from_static("duck.com"));
|
||||
map.append(header::COOKIE, HeaderValue::from_static("one=1"));
|
||||
map.append(header::COOKIE, HeaderValue::from_static("two=2"));
|
||||
|
||||
assert_eq!(map.len(), 4);
|
||||
|
||||
// by value
|
||||
map.retain(|_, val| !val.as_bytes().contains(&b'/'));
|
||||
assert_eq!(map.len(), 3);
|
||||
|
||||
// by name
|
||||
map.retain(|name, _| name.as_str() != "cookie");
|
||||
assert_eq!(map.len(), 1);
|
||||
|
||||
// keep but mutate value
|
||||
map.retain(|_, val| {
|
||||
*val = HeaderValue::from_static("replaced");
|
||||
true
|
||||
});
|
||||
assert_eq!(map.len(), 1);
|
||||
assert_eq!(map.get("host").unwrap(), "replaced");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retain_removes_empty_value_lists() {
|
||||
let mut map = HeaderMap::with_capacity(3);
|
||||
|
||||
map.append(header::HOST, HeaderValue::from_static("duck.com"));
|
||||
map.append(header::HOST, HeaderValue::from_static("duck.com"));
|
||||
|
||||
assert_eq!(map.len(), 2);
|
||||
assert_eq!(map.len_keys(), 1);
|
||||
assert_eq!(map.inner.len(), 1);
|
||||
assert_eq!(map.capacity(), 3);
|
||||
|
||||
// remove everything
|
||||
map.retain(|_n, _v| false);
|
||||
|
||||
assert_eq!(map.len(), 0);
|
||||
assert_eq!(map.len_keys(), 0);
|
||||
assert_eq!(map.inner.len(), 0);
|
||||
assert_eq!(map.capacity(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entries_into_iter() {
|
||||
let mut map = HeaderMap::new();
|
||||
|
@ -1,14 +1,18 @@
|
||||
//! Pre-defined `HeaderName`s, traits for parsing and conversion, and other header utility methods.
|
||||
|
||||
// declaring new header consts will yield this error
|
||||
#![allow(clippy::declare_interior_mutable_const)]
|
||||
|
||||
use percent_encoding::{AsciiSet, CONTROLS};
|
||||
|
||||
// re-export from http except header map related items
|
||||
pub use http::header::{
|
||||
pub use ::http::header::{
|
||||
HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, ToStrError,
|
||||
};
|
||||
|
||||
// re-export const header names
|
||||
pub use http::header::{
|
||||
// re-export const header names, list is explicit so that any updates to `common` module do not
|
||||
// conflict with this set
|
||||
pub use ::http::header::{
|
||||
ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES,
|
||||
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
|
||||
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS,
|
||||
@ -30,22 +34,30 @@ pub use http::header::{
|
||||
use crate::{error::ParseError, HttpMessage};
|
||||
|
||||
mod as_name;
|
||||
mod common;
|
||||
mod into_pair;
|
||||
mod into_value;
|
||||
pub mod map;
|
||||
mod shared;
|
||||
mod utils;
|
||||
|
||||
pub use self::as_name::AsHeaderName;
|
||||
pub use self::into_pair::TryIntoHeaderPair;
|
||||
pub use self::into_value::TryIntoHeaderValue;
|
||||
pub use self::map::HeaderMap;
|
||||
pub use self::shared::{
|
||||
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, LanguageTag,
|
||||
Quality, QualityItem,
|
||||
pub use self::{
|
||||
as_name::AsHeaderName,
|
||||
into_pair::TryIntoHeaderPair,
|
||||
into_value::TryIntoHeaderValue,
|
||||
map::HeaderMap,
|
||||
shared::{
|
||||
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate,
|
||||
LanguageTag, Quality, QualityItem,
|
||||
},
|
||||
utils::{fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode},
|
||||
};
|
||||
pub use self::utils::{
|
||||
fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode,
|
||||
|
||||
// re-export list is explicit so that any updates to `http` do not conflict with this set
|
||||
pub use self::common::{
|
||||
CACHE_STATUS, CDN_CACHE_CONTROL, CROSS_ORIGIN_EMBEDDER_POLICY, CROSS_ORIGIN_OPENER_POLICY,
|
||||
CROSS_ORIGIN_RESOURCE_POLICY, PERMISSIONS_POLICY, X_FORWARDED_FOR, X_FORWARDED_HOST,
|
||||
X_FORWARDED_PROTO,
|
||||
};
|
||||
|
||||
/// An interface for types that already represent a valid header.
|
||||
|
@ -21,7 +21,8 @@
|
||||
#![allow(
|
||||
clippy::type_complexity,
|
||||
clippy::too_many_arguments,
|
||||
clippy::borrow_interior_mutable_const
|
||||
clippy::borrow_interior_mutable_const,
|
||||
clippy::uninlined_format_args
|
||||
)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
@ -113,14 +113,14 @@ impl<P> Request<P> {
|
||||
#[inline]
|
||||
/// Http message part of the request
|
||||
pub fn head(&self) -> &RequestHead {
|
||||
&*self.head
|
||||
&self.head
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[doc(hidden)]
|
||||
/// Mutable reference to a HTTP message part of the request
|
||||
pub fn head_mut(&mut self) -> &mut RequestHead {
|
||||
&mut *self.head
|
||||
&mut self.head
|
||||
}
|
||||
|
||||
/// Mutable reference to the message's headers.
|
||||
|
@ -83,13 +83,13 @@ impl<B> Response<B> {
|
||||
/// Returns a reference to the head of this response.
|
||||
#[inline]
|
||||
pub fn head(&self) -> &ResponseHead {
|
||||
&*self.head
|
||||
&self.head
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the head of this response.
|
||||
#[inline]
|
||||
pub fn head_mut(&mut self) -> &mut ResponseHead {
|
||||
&mut *self.head
|
||||
&mut self.head
|
||||
}
|
||||
|
||||
/// Returns the status code of this response.
|
||||
|
@ -24,7 +24,39 @@ use crate::{
|
||||
h1, ConnectCallback, OnConnectData, Protocol, Request, Response, ServiceConfig,
|
||||
};
|
||||
|
||||
/// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol.
|
||||
/// A [`ServiceFactory`] for HTTP/1.1 and HTTP/2 connections.
|
||||
///
|
||||
/// Use [`build`](Self::build) to begin constructing service. Also see [`HttpServiceBuilder`].
|
||||
///
|
||||
/// # Automatic HTTP Version Selection
|
||||
/// There are two ways to select the HTTP version of an incoming connection:
|
||||
/// - One is to rely on the ALPN information that is provided when using a TLS (HTTPS); both
|
||||
/// versions are supported automatically when using either of the `.rustls()` or `.openssl()`
|
||||
/// finalizing methods.
|
||||
/// - The other is to read the first few bytes of the TCP stream. This is the only viable approach
|
||||
/// for supporting H2C, which allows the HTTP/2 protocol to work over plaintext connections. Use
|
||||
/// the `.tcp_auto_h2c()` finalizing method to enable this behavior.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::convert::Infallible;
|
||||
/// use actix_http::{HttpService, Request, Response, StatusCode};
|
||||
///
|
||||
/// // this service would constructed in an actix_server::Server
|
||||
///
|
||||
/// # actix_rt::System::new().block_on(async {
|
||||
/// HttpService::build()
|
||||
/// // the builder finalizing method, other finalizers would not return an `HttpService`
|
||||
/// .finish(|_req: Request| async move {
|
||||
/// Ok::<_, Infallible>(
|
||||
/// Response::build(StatusCode::OK).body("Hello!")
|
||||
/// )
|
||||
/// })
|
||||
/// // the service finalizing method method
|
||||
/// // you can use `.tcp_auto_h2c()`, `.rustls()`, or `.openssl()` instead of `.tcp()`
|
||||
/// .tcp();
|
||||
/// # })
|
||||
/// ```
|
||||
pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> {
|
||||
srv: S,
|
||||
cfg: ServiceConfig,
|
||||
@ -163,7 +195,9 @@ where
|
||||
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Create simple tcp stream service
|
||||
/// Creates TCP stream service from HTTP service.
|
||||
///
|
||||
/// The resulting service only supports HTTP/1.x.
|
||||
pub fn tcp(
|
||||
self,
|
||||
) -> impl ServiceFactory<
|
||||
@ -179,6 +213,42 @@ where
|
||||
})
|
||||
.and_then(self)
|
||||
}
|
||||
|
||||
/// Creates TCP stream service from HTTP service that automatically selects HTTP/1.x or HTTP/2
|
||||
/// on plaintext connections.
|
||||
#[cfg(feature = "http2")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
||||
pub fn tcp_auto_h2c(
|
||||
self,
|
||||
) -> impl ServiceFactory<
|
||||
TcpStream,
|
||||
Config = (),
|
||||
Response = (),
|
||||
Error = DispatchError,
|
||||
InitError = (),
|
||||
> {
|
||||
fn_service(move |io: TcpStream| async move {
|
||||
// subset of HTTP/2 preface defined by RFC 9113 §3.4
|
||||
// this subset was chosen to maximize likelihood that peeking only once will allow us to
|
||||
// reliably determine version or else it should fallback to h1 and fail quickly if data
|
||||
// on the wire is junk
|
||||
const H2_PREFACE: &[u8] = b"PRI * HTTP/2";
|
||||
|
||||
let mut buf = [0; 12];
|
||||
|
||||
io.peek(&mut buf).await?;
|
||||
|
||||
let proto = if buf == H2_PREFACE {
|
||||
Protocol::Http2
|
||||
} else {
|
||||
Protocol::Http1
|
||||
};
|
||||
|
||||
let peer_addr = io.peer_addr().ok();
|
||||
Ok((io, proto, peer_addr))
|
||||
})
|
||||
.and_then(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration options used when accepting TLS connection.
|
||||
|
@ -1,7 +1,7 @@
|
||||
use actix_codec::{Decoder, Encoder};
|
||||
use bitflags::bitflags;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use bytestring::ByteString;
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
use tracing::error;
|
||||
|
||||
use super::{
|
||||
|
@ -76,7 +76,9 @@ mod inner {
|
||||
use pin_project_lite::pin_project;
|
||||
use tracing::debug;
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed};
|
||||
use actix_codec::Framed;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
use crate::{body::BoxBody, Response};
|
||||
|
||||
|
@ -313,7 +313,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_parse_frame_no_mask() {
|
||||
let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]);
|
||||
buf.extend(&[1u8]);
|
||||
buf.extend([1u8]);
|
||||
|
||||
assert!(Parser::parse(&mut buf, true, 1024).is_err());
|
||||
|
||||
@ -326,7 +326,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_parse_frame_max_size() {
|
||||
let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0010u8][..]);
|
||||
buf.extend(&[1u8, 1u8]);
|
||||
buf.extend([1u8, 1u8]);
|
||||
|
||||
assert!(Parser::parse(&mut buf, true, 1).is_err());
|
||||
|
||||
@ -340,9 +340,9 @@ mod tests {
|
||||
fn test_parse_frame_max_size_recoverability() {
|
||||
let mut buf = BytesMut::new();
|
||||
// The first text frame with length == 2, payload doesn't matter.
|
||||
buf.extend(&[0b0000_0001u8, 0b0000_0010u8, 0b0000_0000u8, 0b0000_0000u8]);
|
||||
buf.extend([0b0000_0001u8, 0b0000_0010u8, 0b0000_0000u8, 0b0000_0000u8]);
|
||||
// Next binary frame with length == 2 and payload == `[0x1111_1111u8, 0x1111_1111u8]`.
|
||||
buf.extend(&[0b0000_0010u8, 0b0000_0010u8, 0b1111_1111u8, 0b1111_1111u8]);
|
||||
buf.extend([0b0000_0010u8, 0b0000_0010u8, 0b1111_1111u8, 0b1111_1111u8]);
|
||||
|
||||
assert_eq!(buf.len(), 8);
|
||||
assert!(matches!(
|
||||
|
@ -3,6 +3,7 @@ use std::{
|
||||
fmt,
|
||||
};
|
||||
|
||||
use base64::prelude::*;
|
||||
use tracing::error;
|
||||
|
||||
/// Operation codes defined in [RFC 6455 §11.8].
|
||||
@ -244,7 +245,7 @@ pub fn hash_key(key: &[u8]) -> [u8; 28] {
|
||||
};
|
||||
|
||||
let mut hash_b64 = [0; 28];
|
||||
let n = base64::encode_config_slice(&hash, base64::STANDARD, &mut hash_b64);
|
||||
let n = BASE64_STANDARD.encode_slice(hash, &mut hash_b64).unwrap();
|
||||
assert_eq!(n, 28);
|
||||
|
||||
hash_b64
|
||||
|
@ -1,4 +1,5 @@
|
||||
#![cfg(feature = "openssl")]
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
extern crate tls_openssl as openssl;
|
||||
|
||||
@ -16,7 +17,7 @@ use actix_utils::future::{err, ok, ready};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use derive_more::{Display, Error};
|
||||
use futures_core::Stream;
|
||||
use futures_util::stream::{once, StreamExt as _};
|
||||
use futures_util::{stream::once, StreamExt as _};
|
||||
use openssl::{
|
||||
pkey::PKey,
|
||||
ssl::{SslAcceptor, SslMethod},
|
||||
|
@ -1,4 +1,5 @@
|
||||
#![cfg(feature = "rustls")]
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
extern crate tls_rustls as rustls;
|
||||
|
||||
@ -41,7 +42,7 @@ where
|
||||
let body = stream.as_mut();
|
||||
|
||||
match ready!(body.poll_next(cx)) {
|
||||
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
|
||||
Some(Ok(bytes)) => buf.extend_from_slice(&bytes),
|
||||
None => return Poll::Ready(Ok(())),
|
||||
Some(Err(err)) => return Poll::Ready(Err(err)),
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
io::{Read, Write},
|
||||
@ -7,18 +9,15 @@ use std::{
|
||||
|
||||
use actix_http::{
|
||||
body::{self, BodyStream, BoxBody, SizedStream},
|
||||
header, Error, HttpService, KeepAlive, Request, Response, StatusCode,
|
||||
header, Error, HttpService, KeepAlive, Request, Response, StatusCode, Version,
|
||||
};
|
||||
use actix_http_test::test_server;
|
||||
use actix_rt::time::sleep;
|
||||
use actix_rt::{net::TcpStream, time::sleep};
|
||||
use actix_service::fn_service;
|
||||
use actix_utils::future::{err, ok, ready};
|
||||
use bytes::Bytes;
|
||||
use derive_more::{Display, Error};
|
||||
use futures_util::{
|
||||
stream::{once, StreamExt as _},
|
||||
FutureExt as _,
|
||||
};
|
||||
use futures_util::{stream::once, FutureExt as _, StreamExt as _};
|
||||
use regex::Regex;
|
||||
|
||||
#[actix_rt::test]
|
||||
@ -858,3 +857,44 @@ async fn not_modified_spec_h1() {
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn h2c_auto() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.keep_alive(KeepAlive::Disabled)
|
||||
.finish(|req: Request| {
|
||||
let body = match req.version() {
|
||||
Version::HTTP_11 => "h1",
|
||||
Version::HTTP_2 => "h2",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
ok::<_, Infallible>(Response::ok().set_body(body))
|
||||
})
|
||||
.tcp_auto_h2c()
|
||||
})
|
||||
.await;
|
||||
|
||||
let req = srv.get("/");
|
||||
assert_eq!(req.get_version(), &Version::HTTP_11);
|
||||
let mut res = req.send().await.unwrap();
|
||||
assert!(res.status().is_success());
|
||||
assert_eq!(res.body().await.unwrap(), &b"h1"[..]);
|
||||
|
||||
// awc doesn't support forcing the version to http/2 so use h2 manually
|
||||
|
||||
let tcp = TcpStream::connect(srv.addr()).await.unwrap();
|
||||
let (h2, connection) = h2::client::handshake(tcp).await.unwrap();
|
||||
tokio::spawn(async move { connection.await.unwrap() });
|
||||
let mut h2 = h2.ready().await.unwrap();
|
||||
|
||||
let request = ::http::Request::new(());
|
||||
let (response, _) = h2.send_request(request, true).unwrap();
|
||||
let (head, mut body) = response.await.unwrap().into_parts();
|
||||
let body = body.data().await.unwrap().unwrap();
|
||||
|
||||
assert!(head.status.is_success());
|
||||
assert_eq!(body, &b"h2"[..]);
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use std::{
|
||||
cell::Cell,
|
||||
convert::Infallible,
|
||||
|
@ -1,7 +1,13 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
|
||||
|
||||
## 0.5.0 - 2023-01-21
|
||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||
- `Field::content_type()` now returns `Option<&mime::Mime>` [#2885]
|
||||
|
||||
[#2885]: https://github.com/actix/actix-web/pull/2885
|
||||
|
||||
|
||||
## 0.4.0 - 2022-02-25
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-multipart"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Multipart form support for Actix Web"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
@ -19,16 +19,16 @@ actix-web = { version = "4", default-features = false }
|
||||
|
||||
bytes = "1"
|
||||
derive_more = "0.99.5"
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||
httparse = "1.3"
|
||||
local-waker = "0.1"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
twoway = "0.2"
|
||||
memchr = "2.5"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-http = "3"
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
tokio = { version = "1.8.4", features = ["sync"] }
|
||||
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||
tokio = { version = "1.18.4", features = ["sync"] }
|
||||
tokio-stream = "0.1"
|
||||
|
@ -3,11 +3,11 @@
|
||||
> Multipart form support for Actix Web.
|
||||
|
||||
[](https://crates.io/crates/actix-multipart)
|
||||
[](https://docs.rs/actix-multipart/0.4.0)
|
||||
[](https://docs.rs/actix-multipart/0.5.0)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-multipart/0.4.0)
|
||||
[](https://deps.rs/crate/actix-multipart/0.5.0)
|
||||
[](https://crates.io/crates/actix-multipart)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
@ -14,7 +14,7 @@ use crate::server::Multipart;
|
||||
/// ```
|
||||
/// use actix_web::{web, HttpResponse, Error};
|
||||
/// use actix_multipart::Multipart;
|
||||
/// use futures_util::stream::StreamExt as _;
|
||||
/// use futures_util::StreamExt as _;
|
||||
///
|
||||
/// async fn index(mut payload: Multipart) -> Result<HttpResponse, Error> {
|
||||
/// // iterate over multipart stream
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![allow(clippy::borrow_interior_mutable_const)]
|
||||
#![allow(clippy::borrow_interior_mutable_const, clippy::uninlined_format_args)]
|
||||
|
||||
mod error;
|
||||
mod extractor;
|
||||
|
@ -289,10 +289,8 @@ impl InnerMultipart {
|
||||
match self.state {
|
||||
// read until first boundary
|
||||
InnerState::FirstBoundary => {
|
||||
match InnerMultipart::skip_until_boundary(
|
||||
&mut *payload,
|
||||
&self.boundary,
|
||||
)? {
|
||||
match InnerMultipart::skip_until_boundary(&mut payload, &self.boundary)?
|
||||
{
|
||||
Some(eof) => {
|
||||
if eof {
|
||||
self.state = InnerState::Eof;
|
||||
@ -306,7 +304,7 @@ impl InnerMultipart {
|
||||
}
|
||||
// read boundary
|
||||
InnerState::Boundary => {
|
||||
match InnerMultipart::read_boundary(&mut *payload, &self.boundary)? {
|
||||
match InnerMultipart::read_boundary(&mut payload, &self.boundary)? {
|
||||
None => return Poll::Pending,
|
||||
Some(eof) => {
|
||||
if eof {
|
||||
@ -323,7 +321,7 @@ impl InnerMultipart {
|
||||
|
||||
// read field headers for next field
|
||||
if self.state == InnerState::Headers {
|
||||
if let Some(headers) = InnerMultipart::read_headers(&mut *payload)? {
|
||||
if let Some(headers) = InnerMultipart::read_headers(&mut payload)? {
|
||||
self.state = InnerState::Boundary;
|
||||
headers
|
||||
} else {
|
||||
@ -361,18 +359,19 @@ impl InnerMultipart {
|
||||
return Poll::Ready(Some(Err(MultipartError::NoContentDisposition)));
|
||||
};
|
||||
|
||||
let ct: mime::Mime = headers
|
||||
let ct: Option<mime::Mime> = headers
|
||||
.get(&header::CONTENT_TYPE)
|
||||
.and_then(|ct| ct.to_str().ok())
|
||||
.and_then(|ct| ct.parse().ok())
|
||||
.unwrap_or(mime::APPLICATION_OCTET_STREAM);
|
||||
.and_then(|ct| ct.parse().ok());
|
||||
|
||||
self.state = InnerState::Boundary;
|
||||
|
||||
// nested multipart stream is not supported
|
||||
if ct.type_() == mime::MULTIPART {
|
||||
if let Some(mime) = &ct {
|
||||
if mime.type_() == mime::MULTIPART {
|
||||
return Poll::Ready(Some(Err(MultipartError::Nested)));
|
||||
}
|
||||
}
|
||||
|
||||
let field =
|
||||
InnerField::new_in_rc(self.payload.clone(), self.boundary.clone(), &headers)?;
|
||||
@ -399,7 +398,7 @@ impl Drop for InnerMultipart {
|
||||
|
||||
/// A single field in a multipart stream
|
||||
pub struct Field {
|
||||
ct: mime::Mime,
|
||||
ct: Option<mime::Mime>,
|
||||
cd: ContentDisposition,
|
||||
headers: HeaderMap,
|
||||
inner: Rc<RefCell<InnerField>>,
|
||||
@ -410,7 +409,7 @@ impl Field {
|
||||
fn new(
|
||||
safety: Safety,
|
||||
headers: HeaderMap,
|
||||
ct: mime::Mime,
|
||||
ct: Option<mime::Mime>,
|
||||
cd: ContentDisposition,
|
||||
inner: Rc<RefCell<InnerField>>,
|
||||
) -> Self {
|
||||
@ -428,9 +427,13 @@ impl Field {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Returns a reference to the field's content (mime) type.
|
||||
pub fn content_type(&self) -> &mime::Mime {
|
||||
&self.ct
|
||||
/// Returns a reference to the field's content (mime) type, if it is supplied by the client.
|
||||
///
|
||||
/// According to [RFC 7578](https://www.rfc-editor.org/rfc/rfc7578#section-4.4), if it is not
|
||||
/// present, it should default to "text/plain". Note it is the responsibility of the client to
|
||||
/// provide the appropriate content type, there is no attempt to validate this by the server.
|
||||
pub fn content_type(&self) -> Option<&mime::Mime> {
|
||||
self.ct.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the field's Content-Disposition.
|
||||
@ -482,7 +485,11 @@ impl Stream for Field {
|
||||
|
||||
impl fmt::Debug for Field {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "\nField: {}", self.ct)?;
|
||||
if let Some(ct) = &self.ct {
|
||||
writeln!(f, "\nField: {}", ct)?;
|
||||
} else {
|
||||
writeln!(f, "\nField:")?;
|
||||
}
|
||||
writeln!(f, " boundary: {}", self.inner.borrow().boundary)?;
|
||||
writeln!(f, " headers:")?;
|
||||
for (key, val) in self.headers.iter() {
|
||||
@ -599,7 +606,7 @@ impl InnerField {
|
||||
}
|
||||
|
||||
loop {
|
||||
return if let Some(idx) = twoway::find_bytes(&payload.buf[pos..], b"\r") {
|
||||
return if let Some(idx) = memchr::memmem::find(&payload.buf[pos..], b"\r") {
|
||||
let cur = pos + idx;
|
||||
|
||||
// check if we have enough data for boundary detection
|
||||
@ -643,9 +650,9 @@ impl InnerField {
|
||||
let result = if let Some(mut payload) = self.payload.as_ref().unwrap().get_mut(s) {
|
||||
if !self.eof {
|
||||
let res = if let Some(ref mut len) = self.length {
|
||||
InnerField::read_len(&mut *payload, len)
|
||||
InnerField::read_len(&mut payload, len)
|
||||
} else {
|
||||
InnerField::read_stream(&mut *payload, &self.boundary)
|
||||
InnerField::read_stream(&mut payload, &self.boundary)
|
||||
};
|
||||
|
||||
match res {
|
||||
@ -820,7 +827,7 @@ impl PayloadBuffer {
|
||||
|
||||
/// Read until specified ending
|
||||
fn read_until(&mut self, line: &[u8]) -> Result<Option<Bytes>, MultipartError> {
|
||||
let res = twoway::find_bytes(&self.buf, line)
|
||||
let res = memchr::memmem::find(&self.buf, line)
|
||||
.map(|idx| self.buf.split_to(idx + line.len()).freeze());
|
||||
|
||||
if res.is_none() && self.eof {
|
||||
@ -861,7 +868,7 @@ mod tests {
|
||||
use actix_web::test::TestRequest;
|
||||
use actix_web::FromRequest;
|
||||
use bytes::Bytes;
|
||||
use futures_util::{future::lazy, StreamExt};
|
||||
use futures_util::{future::lazy, StreamExt as _};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
@ -1024,8 +1031,8 @@ mod tests {
|
||||
assert_eq!(cd.disposition, DispositionType::FormData);
|
||||
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
|
||||
|
||||
assert_eq!(field.content_type().type_(), mime::TEXT);
|
||||
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
||||
assert_eq!(field.content_type().unwrap().type_(), mime::TEXT);
|
||||
assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN);
|
||||
|
||||
match field.next().await.unwrap() {
|
||||
Ok(chunk) => assert_eq!(chunk, "test"),
|
||||
@ -1041,8 +1048,8 @@ mod tests {
|
||||
|
||||
match multipart.next().await.unwrap() {
|
||||
Ok(mut field) => {
|
||||
assert_eq!(field.content_type().type_(), mime::TEXT);
|
||||
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
||||
assert_eq!(field.content_type().unwrap().type_(), mime::TEXT);
|
||||
assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN);
|
||||
|
||||
match field.next().await {
|
||||
Some(Ok(chunk)) => assert_eq!(chunk, "data"),
|
||||
@ -1086,8 +1093,8 @@ mod tests {
|
||||
assert_eq!(cd.disposition, DispositionType::FormData);
|
||||
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
|
||||
|
||||
assert_eq!(field.content_type().type_(), mime::TEXT);
|
||||
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
||||
assert_eq!(field.content_type().unwrap().type_(), mime::TEXT);
|
||||
assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN);
|
||||
|
||||
assert_eq!(get_whole_field(&mut field).await, "test");
|
||||
}
|
||||
@ -1096,8 +1103,8 @@ mod tests {
|
||||
|
||||
match multipart.next().await {
|
||||
Some(Ok(mut field)) => {
|
||||
assert_eq!(field.content_type().type_(), mime::TEXT);
|
||||
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
||||
assert_eq!(field.content_type().unwrap().type_(), mime::TEXT);
|
||||
assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN);
|
||||
|
||||
assert_eq!(get_whole_field(&mut field).await, "data");
|
||||
}
|
||||
|
@ -1,8 +1,14 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
|
||||
|
||||
## 0.5.1 - 2022-09-19
|
||||
- Correct typo in error string for `i32` deserialization. [#2876]
|
||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||
|
||||
[#2876]: https://github.com/actix/actix-web/pull/2876
|
||||
|
||||
|
||||
## 0.5.0 - 2022-02-22
|
||||
### Added
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-router"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",
|
||||
@ -27,7 +27,7 @@ serde = "1"
|
||||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
criterion = { version = "0.4", features = ["html_reports"] }
|
||||
http = "0.2.5"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
percent-encoding = "2.1"
|
||||
|
@ -1,3 +1,5 @@
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
@ -293,7 +293,7 @@ impl<'de> Deserializer<'de> for Value<'de> {
|
||||
parse_value!(deserialize_bool, visit_bool, "bool");
|
||||
parse_value!(deserialize_i8, visit_i8, "i8");
|
||||
parse_value!(deserialize_i16, visit_i16, "i16");
|
||||
parse_value!(deserialize_i32, visit_i32, "i16");
|
||||
parse_value!(deserialize_i32, visit_i32, "i32");
|
||||
parse_value!(deserialize_i64, visit_i64, "i64");
|
||||
parse_value!(deserialize_u8, visit_u8, "u8");
|
||||
parse_value!(deserialize_u16, visit_u16, "u16");
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
||||
|
@ -242,6 +242,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[allow(clippy::needless_borrow)]
|
||||
#[test]
|
||||
fn deref_impls() {
|
||||
let mut foo = Path::new("/foo");
|
||||
|
@ -1503,31 +1503,31 @@ mod tests {
|
||||
fn build_path_list() {
|
||||
let mut s = String::new();
|
||||
let resource = ResourceDef::new("/user/{item1}/test");
|
||||
assert!(resource.resource_path_from_iter(&mut s, &mut (&["user1"]).iter()));
|
||||
assert!(resource.resource_path_from_iter(&mut s, &mut ["user1"].iter()));
|
||||
assert_eq!(s, "/user/user1/test");
|
||||
|
||||
let mut s = String::new();
|
||||
let resource = ResourceDef::new("/user/{item1}/{item2}/test");
|
||||
assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter()));
|
||||
assert!(resource.resource_path_from_iter(&mut s, &mut ["item", "item2"].iter()));
|
||||
assert_eq!(s, "/user/item/item2/test");
|
||||
|
||||
let mut s = String::new();
|
||||
let resource = ResourceDef::new("/user/{item1}/{item2}");
|
||||
assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter()));
|
||||
assert!(resource.resource_path_from_iter(&mut s, &mut ["item", "item2"].iter()));
|
||||
assert_eq!(s, "/user/item/item2");
|
||||
|
||||
let mut s = String::new();
|
||||
let resource = ResourceDef::new("/user/{item1}/{item2}/");
|
||||
assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter()));
|
||||
assert!(resource.resource_path_from_iter(&mut s, &mut ["item", "item2"].iter()));
|
||||
assert_eq!(s, "/user/item/item2/");
|
||||
|
||||
let mut s = String::new();
|
||||
assert!(!resource.resource_path_from_iter(&mut s, &mut (&["item"]).iter()));
|
||||
assert!(!resource.resource_path_from_iter(&mut s, &mut ["item"].iter()));
|
||||
|
||||
let mut s = String::new();
|
||||
assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter()));
|
||||
assert!(resource.resource_path_from_iter(&mut s, &mut ["item", "item2"].iter()));
|
||||
assert_eq!(s, "/user/item/item2/");
|
||||
assert!(!resource.resource_path_from_iter(&mut s, &mut (&["item"]).iter()));
|
||||
assert!(!resource.resource_path_from_iter(&mut s, &mut ["item"].iter()));
|
||||
|
||||
let mut s = String::new();
|
||||
assert!(resource.resource_path_from_iter(&mut s, &mut vec!["item", "item2"].iter()));
|
||||
@ -1604,10 +1604,10 @@ mod tests {
|
||||
let resource = ResourceDef::new("/user/{item1}*");
|
||||
|
||||
let mut s = String::new();
|
||||
assert!(!resource.resource_path_from_iter(&mut s, &mut (&[""; 0]).iter()));
|
||||
assert!(!resource.resource_path_from_iter(&mut s, &mut [""; 0].iter()));
|
||||
|
||||
let mut s = String::new();
|
||||
assert!(resource.resource_path_from_iter(&mut s, &mut (&["user1"]).iter()));
|
||||
assert!(resource.resource_path_from_iter(&mut s, &mut ["user1"].iter()));
|
||||
assert_eq!(s, "/user/user1");
|
||||
|
||||
let mut s = String::new();
|
||||
|
@ -37,12 +37,12 @@ actix-utils = "3"
|
||||
actix-web = { version = "4", default-features = false, features = ["cookies"] }
|
||||
awc = { version = "3", 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 = [] }
|
||||
futures-core = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||
futures-util = { version = "0.3.17", default-features = false, features = [] }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_urlencoded = "0.7"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0", optional = true }
|
||||
tokio = { version = "1.8.4", features = ["sync"] }
|
||||
tokio = { version = "1.18.4", features = ["sync"] }
|
||||
|
@ -321,6 +321,7 @@ where
|
||||
// all thread managed resources should be dropped at this point
|
||||
});
|
||||
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = thread_stop_tx.send(());
|
||||
});
|
||||
|
||||
@ -567,6 +568,7 @@ impl Drop for TestServer {
|
||||
// without needing to await anything
|
||||
|
||||
// signal server to stop
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = self.server.stop(true);
|
||||
|
||||
// signal system to stop
|
||||
|
@ -1,6 +1,9 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
|
||||
|
||||
## 4.2.0 - 2023-01-21
|
||||
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-web-actors"
|
||||
version = "4.1.0"
|
||||
version = "4.2.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix actors support for Actix Web"
|
||||
keywords = ["actix", "http", "web", "framework", "async"]
|
||||
@ -21,9 +21,10 @@ actix-web = { version = "4", default-features = false }
|
||||
|
||||
bytes = "1"
|
||||
bytestring = "1"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-core = { version = "0.3.17", default-features = false }
|
||||
pin-project-lite = "0.2"
|
||||
tokio = { version = "1.13.1", features = ["sync"] }
|
||||
tokio = { version = "1.18.4", features = ["sync"] }
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
@ -34,4 +35,4 @@ actix-web = { version = "4", features = ["macros"] }
|
||||
mime = "0.3"
|
||||
|
||||
env_logger = "0.9"
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.17", default-features = false }
|
||||
|
@ -3,11 +3,11 @@
|
||||
> Actix actors support for Actix Web.
|
||||
|
||||
[](https://crates.io/crates/actix-web-actors)
|
||||
[](https://docs.rs/actix-web-actors/4.1.0)
|
||||
[](https://docs.rs/actix-web-actors/4.2.0)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-web-actors/4.1.0)
|
||||
[](https://deps.rs/crate/actix-web-actors/4.2.0)
|
||||
[](https://crates.io/crates/actix-web-actors)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
@ -57,6 +57,7 @@
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
mod context;
|
||||
pub mod ws;
|
||||
|
@ -74,7 +74,6 @@ use actix::{
|
||||
Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message as ActixMessage,
|
||||
SpawnHandle,
|
||||
};
|
||||
use actix_codec::{Decoder as _, Encoder as _};
|
||||
use actix_http::ws::{hash_key, Codec};
|
||||
pub use actix_http::ws::{
|
||||
CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError,
|
||||
@ -92,6 +91,7 @@ use bytestring::ByteString;
|
||||
use futures_core::Stream;
|
||||
use pin_project_lite::pin_project;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::codec::{Decoder as _, Encoder as _};
|
||||
|
||||
/// Builder for Websocket session response.
|
||||
///
|
||||
|
@ -3,7 +3,7 @@ use actix_http::ws::Codec;
|
||||
use actix_web::{web, App, HttpRequest};
|
||||
use actix_web_actors::ws;
|
||||
use bytes::Bytes;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use futures_util::{SinkExt as _, StreamExt as _};
|
||||
|
||||
struct Ws;
|
||||
|
||||
|
@ -15,7 +15,7 @@ edition = "2018"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
actix-router = "0.5.0"
|
||||
actix-router = "0.5"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "1", features = ["full", "extra-traits"] }
|
||||
@ -27,6 +27,6 @@ actix-test = "0.1"
|
||||
actix-utils = "3"
|
||||
actix-web = "4"
|
||||
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||
trybuild = "1"
|
||||
rustversion = "1"
|
||||
|
@ -155,7 +155,7 @@ impl Args {
|
||||
if !methods.insert(method) {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&nv.lit,
|
||||
&format!(
|
||||
format!(
|
||||
"HTTP method defined more than once: `{}`",
|
||||
lit.value()
|
||||
),
|
||||
|
@ -3,6 +3,34 @@
|
||||
## Unreleased - 2022-xx-xx
|
||||
|
||||
|
||||
## 4.3.0 - 2023-01-21
|
||||
### Added
|
||||
- Add `ContentDisposition::attachment()` constructor. [#2867]
|
||||
- Add `ErrorHandlers::default_handler()` (as well as `default_handler_{server, client}()`) to make registering handlers for groups of response statuses easier. [#2784]
|
||||
- Add `Logger::custom_response_replace()`. [#2631]
|
||||
- Add rudimentary redirection service at `web::redirect()` / `web::Redirect`. [#1961]
|
||||
- Add `guard::Acceptable` for matching against `Accept` header MIME types. [#2265]
|
||||
- Add fallible versions of `test` helpers: `try_call_service()`, `try_call_and_read_body_json()`, `try_read_body()`, and `try_read_body_json()`. [#2961]
|
||||
|
||||
### Fixed
|
||||
- Add `Allow` header to `Resource`'s default responses when no routes are matched. [#2949]
|
||||
|
||||
[#1961]: https://github.com/actix/actix-web/pull/1961
|
||||
[#2265]: https://github.com/actix/actix-web/pull/2265
|
||||
[#2631]: https://github.com/actix/actix-web/pull/2631
|
||||
[#2784]: https://github.com/actix/actix-web/pull/2784
|
||||
[#2867]: https://github.com/actix/actix-web/pull/2867
|
||||
[#2949]: https://github.com/actix/actix-web/pull/2949
|
||||
[#2961]: https://github.com/actix/actix-web/pull/2961
|
||||
|
||||
|
||||
## 4.2.1 - 2022-09-12
|
||||
### Fixed
|
||||
- Bump minimum version of `actix-http` dependency to fix compatibility issue. [#2871]
|
||||
|
||||
[#2871]: https://github.com/actix/actix-web/pull/2871
|
||||
|
||||
|
||||
## 4.2.0 - 2022-09-11
|
||||
### Added
|
||||
- Add `#[routes]` macro to support multiple paths for one handler. [#2718]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-web"
|
||||
version = "4.2.0"
|
||||
version = "4.3.0"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
@ -38,10 +38,7 @@ compress-gzip = ["actix-http/compress-gzip", "__compress"]
|
||||
compress-zstd = ["actix-http/compress-zstd", "__compress"]
|
||||
|
||||
# Routing and runtime proc macros
|
||||
macros = [
|
||||
"actix-macros",
|
||||
"actix-web-codegen",
|
||||
]
|
||||
macros = ["actix-macros", "actix-web-codegen"]
|
||||
|
||||
# Cookies support
|
||||
cookies = ["cookie"]
|
||||
@ -71,7 +68,7 @@ actix-service = "2"
|
||||
actix-utils = "3"
|
||||
actix-tls = { version = "3", default-features = false, optional = true }
|
||||
|
||||
actix-http = { version = "3", features = ["http2", "ws"] }
|
||||
actix-http = { version = "3.3", features = ["http2", "ws"] }
|
||||
actix-router = "0.5"
|
||||
actix-web-codegen = { version = "4.1", optional = true }
|
||||
|
||||
@ -80,10 +77,10 @@ bytes = "1"
|
||||
bytestring = "1"
|
||||
cfg-if = "1"
|
||||
cookie = { version = "0.16", features = ["percent-encode"], optional = true }
|
||||
derive_more = "0.99.5"
|
||||
derive_more = "0.99.8"
|
||||
encoding_rs = "0.8"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
futures-core = { version = "0.3.17", default-features = false }
|
||||
futures-util = { version = "0.3.17", default-features = false }
|
||||
http = "0.2.8"
|
||||
itoa = "1"
|
||||
language-tags = "0.3"
|
||||
@ -107,10 +104,10 @@ awc = { version = "3", features = ["openssl"] }
|
||||
|
||||
brotli = "3.3.3"
|
||||
const-str = "0.4"
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
criterion = { version = "0.4", features = ["html_reports"] }
|
||||
env_logger = "0.9"
|
||||
flate2 = "1.0.13"
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
|
||||
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||
rand = "0.8"
|
||||
rcgen = "0.9"
|
||||
rustls-pemfile = "1"
|
||||
@ -118,8 +115,8 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
static_assertions = "1"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
|
||||
zstd = "0.11"
|
||||
tokio = { version = "1.18.4", features = ["rt-multi-thread", "macros"] }
|
||||
zstd = "0.12"
|
||||
|
||||
[[test]]
|
||||
name = "test_server"
|
||||
|
@ -6,10 +6,10 @@
|
||||
<p>
|
||||
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web/4.2.0)
|
||||
[](https://docs.rs/actix-web/4.3.0)
|
||||

|
||||

|
||||
[](https://deps.rs/crate/actix-web/4.2.0)
|
||||
[](https://deps.rs/crate/actix-web/4.3.0)
|
||||
<br />
|
||||
[](https://github.com/actix/actix-web/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/actix/actix-web)
|
||||
|
@ -1,3 +1,5 @@
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use actix_web::{web, App, HttpResponse};
|
||||
use awc::Client;
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
@ -1,3 +1,5 @@
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use actix_web::{get, middleware, web, App, HttpRequest, HttpResponse, HttpServer};
|
||||
|
||||
#[get("/resource1/{name}/index.html")]
|
||||
|
@ -1,3 +1,5 @@
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use actix_web::{middleware, rt, web, App, HttpRequest, HttpServer};
|
||||
|
||||
async fn index(req: HttpRequest) -> &'static str {
|
||||
|
@ -4,6 +4,8 @@
|
||||
//! For an example of extracting a client TLS certificate, see:
|
||||
//! <https://github.com/actix/examples/tree/master/https-tls/rustls-client-cert>
|
||||
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use std::{any::Any, io, net::SocketAddr};
|
||||
|
||||
use actix_web::{
|
||||
|
@ -1,3 +1,5 @@
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use actix_web::{get, web, HttpRequest};
|
||||
#[cfg(unix)]
|
||||
use actix_web::{middleware, App, Error, HttpResponse, HttpServer};
|
||||
|
@ -5,7 +5,7 @@ use actix_service::{
|
||||
apply, apply_fn_factory, boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt,
|
||||
Transform,
|
||||
};
|
||||
use futures_util::future::FutureExt as _;
|
||||
use futures_util::FutureExt as _;
|
||||
|
||||
use crate::{
|
||||
app_service::{AppEntry, AppInit, AppRoutingFactory},
|
||||
@ -682,7 +682,7 @@ mod tests {
|
||||
"/test",
|
||||
web::get().to(|req: HttpRequest| {
|
||||
HttpResponse::Ok()
|
||||
.body(req.url_for("youtube", &["12345"]).unwrap().to_string())
|
||||
.body(req.url_for("youtube", ["12345"]).unwrap().to_string())
|
||||
}),
|
||||
),
|
||||
)
|
||||
@ -712,6 +712,7 @@ mod tests {
|
||||
.route("/", web::to(|| async { "hello" }))
|
||||
}
|
||||
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = init_service(my_app());
|
||||
}
|
||||
}
|
||||
|
@ -173,7 +173,7 @@ impl AppInitServiceState {
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn rmap(&self) -> &ResourceMap {
|
||||
&*self.rmap
|
||||
&self.rmap
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
@ -344,7 +344,7 @@ mod tests {
|
||||
"/test",
|
||||
web::get().to(|req: HttpRequest| {
|
||||
HttpResponse::Ok()
|
||||
.body(req.url_for("youtube", &["12345"]).unwrap().to_string())
|
||||
.body(req.url_for("youtube", ["12345"]).unwrap().to_string())
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
99
actix-web/src/guard/acceptable.rs
Normal file
99
actix-web/src/guard/acceptable.rs
Normal file
@ -0,0 +1,99 @@
|
||||
use super::{Guard, GuardContext};
|
||||
use crate::http::header::Accept;
|
||||
|
||||
/// A guard that verifies that an `Accept` header is present and it contains a compatible MIME type.
|
||||
///
|
||||
/// An exception is that matching `*/*` must be explicitly enabled because most browsers send this
|
||||
/// as part of their `Accept` header for almost every request.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{guard::Acceptable, web, HttpResponse};
|
||||
///
|
||||
/// web::resource("/images")
|
||||
/// .guard(Acceptable::new(mime::IMAGE_STAR))
|
||||
/// .default_service(web::to(|| async {
|
||||
/// HttpResponse::Ok().body("only called when images responses are acceptable")
|
||||
/// }));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Acceptable {
|
||||
mime: mime::Mime,
|
||||
|
||||
/// Wether to match `*/*` mime type.
|
||||
///
|
||||
/// Defaults to false because it's not very useful otherwise.
|
||||
match_star_star: bool,
|
||||
}
|
||||
|
||||
impl Acceptable {
|
||||
/// Constructs new `Acceptable` guard with the given `mime` type/pattern.
|
||||
pub fn new(mime: mime::Mime) -> Self {
|
||||
Self {
|
||||
mime,
|
||||
match_star_star: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows `*/*` in the `Accept` header to pass the guard check.
|
||||
pub fn match_star_star(mut self) -> Self {
|
||||
self.match_star_star = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Guard for Acceptable {
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
let accept = match ctx.header::<Accept>() {
|
||||
Some(hdr) => hdr,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let target_type = self.mime.type_();
|
||||
let target_subtype = self.mime.subtype();
|
||||
|
||||
for mime in accept.0.into_iter().map(|q| q.item) {
|
||||
return match (mime.type_(), mime.subtype()) {
|
||||
(typ, subtype) if typ == target_type && subtype == target_subtype => true,
|
||||
(typ, mime::STAR) if typ == target_type => true,
|
||||
(mime::STAR, mime::STAR) if self.match_star_star => true,
|
||||
_ => continue,
|
||||
};
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{http::header, test::TestRequest};
|
||||
|
||||
#[test]
|
||||
fn test_acceptable() {
|
||||
let req = TestRequest::default().to_srv_request();
|
||||
assert!(!Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx()));
|
||||
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::ACCEPT, "application/json"))
|
||||
.to_srv_request();
|
||||
assert!(Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx()));
|
||||
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::ACCEPT, "text/html, application/json"))
|
||||
.to_srv_request();
|
||||
assert!(Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acceptable_star() {
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::ACCEPT, "text/html, */*;q=0.8"))
|
||||
.to_srv_request();
|
||||
|
||||
assert!(Acceptable::new(mime::APPLICATION_JSON)
|
||||
.match_star_star()
|
||||
.check(&req.guard_ctx()));
|
||||
}
|
||||
}
|
@ -56,6 +56,9 @@ use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead
|
||||
|
||||
use crate::{http::header::Header, service::ServiceRequest, HttpMessage as _};
|
||||
|
||||
mod acceptable;
|
||||
pub use self::acceptable::Acceptable;
|
||||
|
||||
/// Provides access to request parts that are useful during routing.
|
||||
#[derive(Debug)]
|
||||
pub struct GuardContext<'a> {
|
||||
@ -193,6 +196,7 @@ impl AnyGuard {
|
||||
}
|
||||
|
||||
impl Guard for AnyGuard {
|
||||
#[inline]
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
for guard in &self.guards {
|
||||
if guard.check(ctx) {
|
||||
@ -244,12 +248,14 @@ impl AllGuard {
|
||||
}
|
||||
|
||||
impl Guard for AllGuard {
|
||||
#[inline]
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
for guard in &self.guards {
|
||||
if !guard.check(ctx) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
@ -268,6 +274,7 @@ impl Guard for AllGuard {
|
||||
pub struct Not<G>(pub G);
|
||||
|
||||
impl<G: Guard> Guard for Not<G> {
|
||||
#[inline]
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
!self.0.check(ctx)
|
||||
}
|
||||
@ -279,11 +286,25 @@ pub fn Method(method: HttpMethod) -> impl Guard {
|
||||
MethodGuard(method)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct RegisteredMethods(pub(crate) Vec<HttpMethod>);
|
||||
|
||||
/// HTTP method guard.
|
||||
struct MethodGuard(HttpMethod);
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MethodGuard(HttpMethod);
|
||||
|
||||
impl Guard for MethodGuard {
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
let registered = ctx.req_data_mut().remove::<RegisteredMethods>();
|
||||
|
||||
if let Some(mut methods) = registered {
|
||||
methods.0.push(self.0.clone());
|
||||
ctx.req_data_mut().insert(methods);
|
||||
} else {
|
||||
ctx.req_data_mut()
|
||||
.insert(RegisteredMethods(vec![self.0.clone()]));
|
||||
}
|
||||
|
||||
ctx.head().method == self.0
|
||||
}
|
||||
}
|
@ -6,8 +6,7 @@ use super::{common_header, QualityItem};
|
||||
use crate::http::header;
|
||||
|
||||
common_header! {
|
||||
/// `Accept` header, defined
|
||||
/// in [RFC 7231 §5.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2)
|
||||
/// `Accept` header, defined in [RFC 7231 §5.3.2].
|
||||
///
|
||||
/// The `Accept` header field can be used by user agents to specify
|
||||
/// response media types that are acceptable. Accept header fields can
|
||||
@ -71,6 +70,8 @@ common_header! {
|
||||
/// ])
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// [RFC 7231 §5.3.2]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
(Accept, header::ACCEPT) => (QualityItem<Mime>)*
|
||||
|
||||
test_parse_and_format {
|
||||
@ -106,8 +107,7 @@ common_header! {
|
||||
test4,
|
||||
vec![b"text/plain; charset=utf-8; q=0.5"],
|
||||
Some(Accept(vec![
|
||||
QualityItem::new(mime::TEXT_PLAIN_UTF_8,
|
||||
q(0.5)),
|
||||
QualityItem::new(mime::TEXT_PLAIN_UTF_8, q(0.5)),
|
||||
])));
|
||||
|
||||
#[test]
|
||||
|
@ -176,7 +176,7 @@ impl str::FromStr for CacheDirective {
|
||||
|
||||
_ => match s.find('=') {
|
||||
Some(idx) if idx + 1 < s.len() => {
|
||||
match (&s[..idx], (&s[idx + 1..]).trim_matches('"')) {
|
||||
match (&s[..idx], s[idx + 1..].trim_matches('"')) {
|
||||
("max-age", secs) => secs.parse().map(MaxAge).map_err(Some),
|
||||
("max-stale", secs) => secs.parse().map(MaxStale).map_err(Some),
|
||||
("min-fresh", secs) => secs.parse().map(MinFresh).map_err(Some),
|
||||
|
@ -79,7 +79,7 @@ impl<'a> From<&'a str> for DispositionType {
|
||||
/// assert!(param.is_filename());
|
||||
/// assert_eq!(param.as_filename().unwrap(), "sample.txt");
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum DispositionParam {
|
||||
/// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from
|
||||
@ -302,7 +302,7 @@ 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).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ContentDisposition {
|
||||
/// The disposition type
|
||||
pub disposition: DispositionType,
|
||||
@ -312,16 +312,36 @@ pub struct ContentDisposition {
|
||||
}
|
||||
|
||||
impl ContentDisposition {
|
||||
/// Constructs a Content-Disposition header suitable for downloads.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::http::header::{ContentDisposition, TryIntoHeaderValue as _};
|
||||
///
|
||||
/// let cd = ContentDisposition::attachment("files.zip");
|
||||
///
|
||||
/// let cd_val = cd.try_into_value().unwrap();
|
||||
/// assert_eq!(cd_val, "attachment; filename=\"files.zip\"");
|
||||
/// ```
|
||||
pub fn attachment(filename: impl Into<String>) -> Self {
|
||||
Self {
|
||||
disposition: DispositionType::Attachment,
|
||||
parameters: vec![DispositionParam::Filename(filename.into())],
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a raw Content-Disposition header value.
|
||||
pub fn from_raw(hv: &header::HeaderValue) -> Result<Self, crate::error::ParseError> {
|
||||
// `header::from_one_raw_str` invokes `hv.to_str` which assumes `hv` contains only visible
|
||||
// ASCII characters. So `hv.as_bytes` is necessary here.
|
||||
let hv = String::from_utf8(hv.as_bytes().to_vec())
|
||||
.map_err(|_| crate::error::ParseError::Header)?;
|
||||
|
||||
let (disp_type, mut left) = split_once_and_trim(hv.as_str().trim(), ';');
|
||||
if disp_type.is_empty() {
|
||||
return Err(crate::error::ParseError::Header);
|
||||
}
|
||||
|
||||
let mut cd = ContentDisposition {
|
||||
disposition: disp_type.into(),
|
||||
parameters: Vec::new(),
|
||||
|
@ -69,6 +69,7 @@
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
@ -86,6 +87,7 @@ mod helpers;
|
||||
pub mod http;
|
||||
mod info;
|
||||
pub mod middleware;
|
||||
mod redirect;
|
||||
mod request;
|
||||
mod request_data;
|
||||
mod resource;
|
||||
|
@ -7,7 +7,7 @@ use std::{
|
||||
};
|
||||
|
||||
use futures_core::{future::LocalBoxFuture, ready};
|
||||
use futures_util::future::FutureExt as _;
|
||||
use futures_util::FutureExt as _;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use crate::{
|
||||
|
@ -30,11 +30,25 @@ pub enum ErrorHandlerResponse<B> {
|
||||
|
||||
type ErrorHandler<B> = dyn Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>>;
|
||||
|
||||
type DefaultHandler<B> = Option<Rc<ErrorHandler<B>>>;
|
||||
|
||||
/// Middleware for registering custom status code based error handlers.
|
||||
///
|
||||
/// Register handlers with the `ErrorHandlers::handler()` method to register a custom error handler
|
||||
/// Register handlers with the [`ErrorHandlers::handler()`] method to register a custom error handler
|
||||
/// for a given status code. Handlers can modify existing responses or create completely new ones.
|
||||
///
|
||||
/// To register a default handler, use the [`ErrorHandlers::default_handler()`] method. This
|
||||
/// handler will be used only if a response has an error status code (400-599) that isn't covered by
|
||||
/// a more specific handler (set with the [`handler()`][ErrorHandlers::handler] method). See examples
|
||||
/// below.
|
||||
///
|
||||
/// To register a default for only client errors (400-499) or only server errors (500-599), use the
|
||||
/// [`ErrorHandlers::default_handler_client()`] and [`ErrorHandlers::default_handler_server()`]
|
||||
/// methods, respectively.
|
||||
///
|
||||
/// Any response with a status code that isn't covered by a specific handler or a default handler
|
||||
/// will pass by unchanged by this middleware.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::http::{header, StatusCode};
|
||||
@ -53,7 +67,70 @@ type ErrorHandler<B> = dyn Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse
|
||||
/// .wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_header))
|
||||
/// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError)));
|
||||
/// ```
|
||||
/// ## Registering default handler
|
||||
/// ```
|
||||
/// # use actix_web::http::{header, StatusCode};
|
||||
/// # use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
|
||||
/// # use actix_web::{dev, web, App, HttpResponse, Result};
|
||||
/// fn add_error_header<B>(mut res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
/// res.response_mut().headers_mut().insert(
|
||||
/// header::CONTENT_TYPE,
|
||||
/// header::HeaderValue::from_static("Error"),
|
||||
/// );
|
||||
/// Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
/// }
|
||||
///
|
||||
/// fn handle_bad_request<B>(mut res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
/// res.response_mut().headers_mut().insert(
|
||||
/// header::CONTENT_TYPE,
|
||||
/// header::HeaderValue::from_static("Bad Request Error"),
|
||||
/// );
|
||||
/// Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
/// }
|
||||
///
|
||||
/// // Bad Request errors will hit `handle_bad_request()`, while all other errors will hit
|
||||
/// // `add_error_header()`. The order in which the methods are called is not meaningful.
|
||||
/// let app = App::new()
|
||||
/// .wrap(
|
||||
/// ErrorHandlers::new()
|
||||
/// .default_handler(add_error_header)
|
||||
/// .handler(StatusCode::BAD_REQUEST, handle_bad_request)
|
||||
/// )
|
||||
/// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError)));
|
||||
/// ```
|
||||
/// Alternatively, you can set default handlers for only client or only server errors:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use actix_web::http::{header, StatusCode};
|
||||
/// # use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
|
||||
/// # use actix_web::{dev, web, App, HttpResponse, Result};
|
||||
/// # fn add_error_header<B>(mut res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
/// # res.response_mut().headers_mut().insert(
|
||||
/// # header::CONTENT_TYPE,
|
||||
/// # header::HeaderValue::from_static("Error"),
|
||||
/// # );
|
||||
/// # Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
/// # }
|
||||
/// # fn handle_bad_request<B>(mut res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
/// # res.response_mut().headers_mut().insert(
|
||||
/// # header::CONTENT_TYPE,
|
||||
/// # header::HeaderValue::from_static("Bad Request Error"),
|
||||
/// # );
|
||||
/// # Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
/// # }
|
||||
/// // Bad request errors will hit `handle_bad_request()`, other client errors will hit
|
||||
/// // `add_error_header()`, and server errors will pass through unchanged
|
||||
/// let app = App::new()
|
||||
/// .wrap(
|
||||
/// ErrorHandlers::new()
|
||||
/// .default_handler_client(add_error_header) // or .default_handler_server
|
||||
/// .handler(StatusCode::BAD_REQUEST, handle_bad_request)
|
||||
/// )
|
||||
/// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError)));
|
||||
/// ```
|
||||
pub struct ErrorHandlers<B> {
|
||||
default_client: DefaultHandler<B>,
|
||||
default_server: DefaultHandler<B>,
|
||||
handlers: Handlers<B>,
|
||||
}
|
||||
|
||||
@ -62,6 +139,8 @@ type Handlers<B> = Rc<AHashMap<StatusCode, Box<ErrorHandler<B>>>>;
|
||||
impl<B> Default for ErrorHandlers<B> {
|
||||
fn default() -> Self {
|
||||
ErrorHandlers {
|
||||
default_client: Default::default(),
|
||||
default_server: Default::default(),
|
||||
handlers: Default::default(),
|
||||
}
|
||||
}
|
||||
@ -83,6 +162,66 @@ impl<B> ErrorHandlers<B> {
|
||||
.insert(status, Box::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Register a default error handler.
|
||||
///
|
||||
/// Any request with a status code that hasn't been given a specific other handler (by calling
|
||||
/// [`.handler()`][ErrorHandlers::handler]) will fall back on this.
|
||||
///
|
||||
/// Note that this will overwrite any default handlers previously set by calling
|
||||
/// [`.default_handler_client()`][ErrorHandlers::default_handler_client] or
|
||||
/// [`.default_handler_server()`][ErrorHandlers::default_handler_server], but not any set by
|
||||
/// calling [`.handler()`][ErrorHandlers::handler].
|
||||
pub fn default_handler<F>(self, handler: F) -> Self
|
||||
where
|
||||
F: Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> + 'static,
|
||||
{
|
||||
let handler = Rc::new(handler);
|
||||
Self {
|
||||
default_server: Some(handler.clone()),
|
||||
default_client: Some(handler),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a handler on which to fall back for client error status codes (400-499).
|
||||
pub fn default_handler_client<F>(self, handler: F) -> Self
|
||||
where
|
||||
F: Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> + 'static,
|
||||
{
|
||||
Self {
|
||||
default_client: Some(Rc::new(handler)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a handler on which to fall back for server error status codes (500-599).
|
||||
pub fn default_handler_server<F>(self, handler: F) -> Self
|
||||
where
|
||||
F: Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> + 'static,
|
||||
{
|
||||
Self {
|
||||
default_server: Some(Rc::new(handler)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects the most appropriate handler for the given status code.
|
||||
///
|
||||
/// If the `handlers` map has an entry for that status code, that handler is returned.
|
||||
/// Otherwise, fall back on the appropriate default handler.
|
||||
fn get_handler<'a>(
|
||||
status: &StatusCode,
|
||||
default_client: Option<&'a ErrorHandler<B>>,
|
||||
default_server: Option<&'a ErrorHandler<B>>,
|
||||
handlers: &'a Handlers<B>,
|
||||
) -> Option<&'a ErrorHandler<B>> {
|
||||
handlers
|
||||
.get(status)
|
||||
.map(|h| h.as_ref())
|
||||
.or_else(|| status.is_client_error().then(|| default_client).flatten())
|
||||
.or_else(|| status.is_server_error().then(|| default_server).flatten())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, B> Transform<S, ServiceRequest> for ErrorHandlers<B>
|
||||
@ -99,13 +238,24 @@ where
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
let handlers = self.handlers.clone();
|
||||
Box::pin(async move { Ok(ErrorHandlersMiddleware { service, handlers }) })
|
||||
let default_client = self.default_client.clone();
|
||||
let default_server = self.default_server.clone();
|
||||
Box::pin(async move {
|
||||
Ok(ErrorHandlersMiddleware {
|
||||
service,
|
||||
default_client,
|
||||
default_server,
|
||||
handlers,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct ErrorHandlersMiddleware<S, B> {
|
||||
service: S,
|
||||
default_client: DefaultHandler<B>,
|
||||
default_server: DefaultHandler<B>,
|
||||
handlers: Handlers<B>,
|
||||
}
|
||||
|
||||
@ -123,8 +273,15 @@ where
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
let handlers = self.handlers.clone();
|
||||
let default_client = self.default_client.clone();
|
||||
let default_server = self.default_server.clone();
|
||||
let fut = self.service.call(req);
|
||||
ErrorHandlersFuture::ServiceFuture { fut, handlers }
|
||||
ErrorHandlersFuture::ServiceFuture {
|
||||
fut,
|
||||
default_client,
|
||||
default_server,
|
||||
handlers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,6 +294,8 @@ pin_project! {
|
||||
ServiceFuture {
|
||||
#[pin]
|
||||
fut: Fut,
|
||||
default_client: DefaultHandler<B>,
|
||||
default_server: DefaultHandler<B>,
|
||||
handlers: Handlers<B>,
|
||||
},
|
||||
ErrorHandlerFuture {
|
||||
@ -153,10 +312,22 @@ where
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.as_mut().project() {
|
||||
ErrorHandlersProj::ServiceFuture { fut, handlers } => {
|
||||
ErrorHandlersProj::ServiceFuture {
|
||||
fut,
|
||||
default_client,
|
||||
default_server,
|
||||
handlers,
|
||||
} => {
|
||||
let res = ready!(fut.poll(cx))?;
|
||||
let status = res.status();
|
||||
|
||||
match handlers.get(&res.status()) {
|
||||
let handler = ErrorHandlers::get_handler(
|
||||
&status,
|
||||
default_client.as_mut().map(|f| Rc::as_ref(f)),
|
||||
default_server.as_mut().map(|f| Rc::as_ref(f)),
|
||||
handlers,
|
||||
);
|
||||
match handler {
|
||||
Some(handler) => match handler(res)? {
|
||||
ErrorHandlerResponse::Response(res) => Poll::Ready(Ok(res)),
|
||||
ErrorHandlerResponse::Future(fut) => {
|
||||
@ -166,7 +337,6 @@ where
|
||||
self.poll(cx)
|
||||
}
|
||||
},
|
||||
|
||||
None => Poll::Ready(Ok(res.map_into_left_body())),
|
||||
}
|
||||
}
|
||||
@ -181,7 +351,7 @@ mod tests {
|
||||
use actix_service::IntoService;
|
||||
use actix_utils::future::ok;
|
||||
use bytes::Bytes;
|
||||
use futures_util::future::FutureExt as _;
|
||||
use futures_util::FutureExt as _;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
@ -298,4 +468,117 @@ mod tests {
|
||||
"error in error handler"
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn default_error_handler() {
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn error_handler<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
res.response_mut()
|
||||
.headers_mut()
|
||||
.insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
|
||||
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
}
|
||||
|
||||
let make_mw = |status| async move {
|
||||
ErrorHandlers::new()
|
||||
.default_handler(error_handler)
|
||||
.new_transform(test::status_service(status).into_service())
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
let mw_server = make_mw(StatusCode::INTERNAL_SERVER_ERROR).await;
|
||||
let mw_client = make_mw(StatusCode::BAD_REQUEST).await;
|
||||
|
||||
let resp =
|
||||
test::call_service(&mw_client, TestRequest::default().to_srv_request()).await;
|
||||
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
|
||||
|
||||
let resp =
|
||||
test::call_service(&mw_server, TestRequest::default().to_srv_request()).await;
|
||||
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn default_handlers_separate_client_server() {
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn error_handler_client<B>(
|
||||
mut res: ServiceResponse<B>,
|
||||
) -> Result<ErrorHandlerResponse<B>> {
|
||||
res.response_mut()
|
||||
.headers_mut()
|
||||
.insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
|
||||
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn error_handler_server<B>(
|
||||
mut res: ServiceResponse<B>,
|
||||
) -> Result<ErrorHandlerResponse<B>> {
|
||||
res.response_mut()
|
||||
.headers_mut()
|
||||
.insert(CONTENT_TYPE, HeaderValue::from_static("0002"));
|
||||
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
}
|
||||
|
||||
let make_mw = |status| async move {
|
||||
ErrorHandlers::new()
|
||||
.default_handler_server(error_handler_server)
|
||||
.default_handler_client(error_handler_client)
|
||||
.new_transform(test::status_service(status).into_service())
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
let mw_server = make_mw(StatusCode::INTERNAL_SERVER_ERROR).await;
|
||||
let mw_client = make_mw(StatusCode::BAD_REQUEST).await;
|
||||
|
||||
let resp =
|
||||
test::call_service(&mw_client, TestRequest::default().to_srv_request()).await;
|
||||
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
|
||||
|
||||
let resp =
|
||||
test::call_service(&mw_server, TestRequest::default().to_srv_request()).await;
|
||||
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn default_handlers_specialization() {
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn error_handler_client<B>(
|
||||
mut res: ServiceResponse<B>,
|
||||
) -> Result<ErrorHandlerResponse<B>> {
|
||||
res.response_mut()
|
||||
.headers_mut()
|
||||
.insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
|
||||
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn error_handler_specific<B>(
|
||||
mut res: ServiceResponse<B>,
|
||||
) -> Result<ErrorHandlerResponse<B>> {
|
||||
res.response_mut()
|
||||
.headers_mut()
|
||||
.insert(CONTENT_TYPE, HeaderValue::from_static("0003"));
|
||||
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
}
|
||||
|
||||
let make_mw = |status| async move {
|
||||
ErrorHandlers::new()
|
||||
.default_handler_client(error_handler_client)
|
||||
.handler(StatusCode::UNPROCESSABLE_ENTITY, error_handler_specific)
|
||||
.new_transform(test::status_service(status).into_service())
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
let mw_client = make_mw(StatusCode::BAD_REQUEST).await;
|
||||
let mw_specific = make_mw(StatusCode::UNPROCESSABLE_ENTITY).await;
|
||||
|
||||
let resp =
|
||||
test::call_service(&mw_client, TestRequest::default().to_srv_request()).await;
|
||||
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
|
||||
|
||||
let resp =
|
||||
test::call_service(&mw_specific, TestRequest::default().to_srv_request()).await;
|
||||
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0003");
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ use crate::{
|
||||
body::{BodySize, MessageBody},
|
||||
http::header::HeaderName,
|
||||
service::{ServiceRequest, ServiceResponse},
|
||||
Error, HttpResponse, Result,
|
||||
Error, Result,
|
||||
};
|
||||
|
||||
/// Middleware for logging request and response summaries to the terminal.
|
||||
@ -73,6 +73,7 @@ use crate::{
|
||||
/// `%{FOO}o` | `response.headers["FOO"]`
|
||||
/// `%{FOO}e` | `env_var["FOO"]`
|
||||
/// `%{FOO}xi` | [Custom request replacement](Logger::custom_request_replace) labelled "FOO"
|
||||
/// `%{FOO}xo` | [Custom response replacement](Logger::custom_response_replace) labelled "FOO"
|
||||
///
|
||||
/// # Security
|
||||
/// **\*** "Real IP" remote address is calculated using
|
||||
@ -179,6 +180,55 @@ impl Logger {
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Register a function that receives a `ServiceResponse` and returns a string for use in the
|
||||
/// log line.
|
||||
///
|
||||
/// The label passed as the first argument should match a replacement substring in
|
||||
/// the logger format like `%{label}xo`.
|
||||
///
|
||||
/// It is convention to print "-" to indicate no output instead of an empty string.
|
||||
///
|
||||
/// The replacement function does not have access to the response body.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use actix_web::{dev::ServiceResponse, middleware::Logger};
|
||||
/// fn log_if_error(res: &ServiceResponse) -> String {
|
||||
/// if res.status().as_u16() >= 400 {
|
||||
/// "ERROR".to_string()
|
||||
/// } else {
|
||||
/// "-".to_string()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// Logger::new("example %{ERROR_STATUS}xo")
|
||||
/// .custom_response_replace("ERROR_STATUS", |res| log_if_error(res) );
|
||||
/// ```
|
||||
pub fn custom_response_replace(
|
||||
mut self,
|
||||
label: &str,
|
||||
f: impl Fn(&ServiceResponse) -> String + 'static,
|
||||
) -> Self {
|
||||
let inner = Rc::get_mut(&mut self.0).unwrap();
|
||||
|
||||
let ft = inner.format.0.iter_mut().find(
|
||||
|ft| matches!(ft, FormatText::CustomResponse(unit_label, _) if label == unit_label),
|
||||
);
|
||||
|
||||
if let Some(FormatText::CustomResponse(_, res_fn)) = ft {
|
||||
*res_fn = Some(CustomResponseFn {
|
||||
inner_fn: Rc::new(f),
|
||||
});
|
||||
} else {
|
||||
debug!(
|
||||
"Attempted to register custom response logging function for non-existent label: {}",
|
||||
label
|
||||
);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Logger {
|
||||
@ -210,10 +260,16 @@ where
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
for unit in &self.0.format.0 {
|
||||
// missing request replacement function diagnostic
|
||||
if let FormatText::CustomRequest(label, None) = unit {
|
||||
warn!(
|
||||
"No custom request replacement function was registered for label \"{}\".",
|
||||
"No custom request replacement function was registered for label: {}",
|
||||
label
|
||||
);
|
||||
}
|
||||
|
||||
if let FormatText::CustomResponse(label, None) = unit {
|
||||
warn!(
|
||||
"No custom response replacement function was registered for label: {}",
|
||||
label
|
||||
);
|
||||
}
|
||||
@ -308,12 +364,26 @@ where
|
||||
debug!("Error in response: {:?}", error);
|
||||
}
|
||||
|
||||
if let Some(ref mut format) = this.format {
|
||||
let res = if let Some(ref mut format) = this.format {
|
||||
// to avoid polluting all the Logger types with the body parameter we swap the body
|
||||
// out temporarily since it's not usable in custom response functions anyway
|
||||
|
||||
let (req, res) = res.into_parts();
|
||||
let (res, body) = res.into_parts();
|
||||
|
||||
let temp_res = ServiceResponse::new(req, res.map_into_boxed_body());
|
||||
|
||||
for unit in &mut format.0 {
|
||||
unit.render_response(res.response());
|
||||
}
|
||||
unit.render_response(&temp_res);
|
||||
}
|
||||
|
||||
// re-construct original service response
|
||||
let (req, res) = temp_res.into_parts();
|
||||
ServiceResponse::new(req, res.set_body(body))
|
||||
} else {
|
||||
res
|
||||
};
|
||||
|
||||
let time = *this.time;
|
||||
let format = this.format.take();
|
||||
let log_target = this.log_target.clone();
|
||||
@ -399,7 +469,7 @@ impl Format {
|
||||
/// Returns `None` if the format string syntax is incorrect.
|
||||
pub fn new(s: &str) -> Format {
|
||||
log::trace!("Access log format: {}", s);
|
||||
let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|xi)|[%atPrUsbTD]?)").unwrap();
|
||||
let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|x[io])|[%atPrUsbTD]?)").unwrap();
|
||||
|
||||
let mut idx = 0;
|
||||
let mut results = Vec::new();
|
||||
@ -417,7 +487,7 @@ impl Format {
|
||||
if key.as_str() == "r" {
|
||||
FormatText::RealIpRemoteAddr
|
||||
} else {
|
||||
unreachable!()
|
||||
unreachable!("regex and code mismatch")
|
||||
}
|
||||
}
|
||||
"i" => {
|
||||
@ -428,6 +498,7 @@ impl Format {
|
||||
}
|
||||
"e" => FormatText::EnvironHeader(key.as_str().to_owned()),
|
||||
"xi" => FormatText::CustomRequest(key.as_str().to_owned(), None),
|
||||
"xo" => FormatText::CustomResponse(key.as_str().to_owned(), None),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
} else {
|
||||
@ -475,6 +546,7 @@ enum FormatText {
|
||||
ResponseHeader(HeaderName),
|
||||
EnvironHeader(String),
|
||||
CustomRequest(String, Option<CustomRequestFn>),
|
||||
CustomResponse(String, Option<CustomResponseFn>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -494,6 +566,23 @@ impl fmt::Debug for CustomRequestFn {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CustomResponseFn {
|
||||
inner_fn: Rc<dyn Fn(&ServiceResponse) -> String>,
|
||||
}
|
||||
|
||||
impl CustomResponseFn {
|
||||
fn call(&self, res: &ServiceResponse) -> String {
|
||||
(self.inner_fn)(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for CustomResponseFn {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("custom_response_fn")
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatText {
|
||||
fn render(
|
||||
&self,
|
||||
@ -526,11 +615,12 @@ impl FormatText {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_response<B>(&mut self, res: &HttpResponse<B>) {
|
||||
fn render_response(&mut self, res: &ServiceResponse) {
|
||||
match self {
|
||||
FormatText::ResponseStatus => {
|
||||
*self = FormatText::Str(format!("{}", res.status().as_u16()))
|
||||
}
|
||||
|
||||
FormatText::ResponseHeader(ref name) => {
|
||||
let s = if let Some(val) = res.headers().get(name) {
|
||||
if let Ok(s) = val.to_str() {
|
||||
@ -543,6 +633,16 @@ impl FormatText {
|
||||
};
|
||||
*self = FormatText::Str(s.to_string())
|
||||
}
|
||||
|
||||
FormatText::CustomResponse(_, res_fn) => {
|
||||
let text = match res_fn {
|
||||
Some(res_fn) => FormatText::Str(res_fn.call(res)),
|
||||
None => FormatText::Str("-".to_owned()),
|
||||
};
|
||||
|
||||
*self = text;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@ -627,8 +727,11 @@ mod tests {
|
||||
use actix_utils::future::ok;
|
||||
|
||||
use super::*;
|
||||
use crate::http::{header, StatusCode};
|
||||
use crate::test::{self, TestRequest};
|
||||
use crate::{
|
||||
http::{header, StatusCode},
|
||||
test::{self, TestRequest},
|
||||
HttpResponse,
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_logger() {
|
||||
@ -691,9 +794,10 @@ mod tests {
|
||||
unit.render_request(now, &req);
|
||||
}
|
||||
|
||||
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let res = ServiceResponse::new(req, HttpResponse::Ok().finish());
|
||||
for unit in &mut format.0 {
|
||||
unit.render_response(&resp);
|
||||
unit.render_response(&res);
|
||||
}
|
||||
|
||||
let entry_time = OffsetDateTime::now_utc();
|
||||
@ -723,9 +827,10 @@ mod tests {
|
||||
unit.render_request(now, &req);
|
||||
}
|
||||
|
||||
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let res = ServiceResponse::new(req, HttpResponse::Ok().force_close().finish());
|
||||
for unit in &mut format.0 {
|
||||
unit.render_response(&resp);
|
||||
unit.render_response(&res);
|
||||
}
|
||||
|
||||
let render = |fmt: &mut fmt::Formatter<'_>| {
|
||||
@ -755,9 +860,10 @@ mod tests {
|
||||
unit.render_request(now, &req);
|
||||
}
|
||||
|
||||
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let res = ServiceResponse::new(req, HttpResponse::Ok().force_close().finish());
|
||||
for unit in &mut format.0 {
|
||||
unit.render_response(&resp);
|
||||
unit.render_response(&res);
|
||||
}
|
||||
|
||||
let entry_time = OffsetDateTime::now_utc();
|
||||
@ -784,9 +890,10 @@ mod tests {
|
||||
unit.render_request(now, &req);
|
||||
}
|
||||
|
||||
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let res = ServiceResponse::new(req, HttpResponse::Ok().force_close().finish());
|
||||
for unit in &mut format.0 {
|
||||
unit.render_response(&resp);
|
||||
unit.render_response(&res);
|
||||
}
|
||||
|
||||
let render = |fmt: &mut fmt::Formatter<'_>| {
|
||||
@ -815,9 +922,10 @@ mod tests {
|
||||
unit.render_request(now, &req);
|
||||
}
|
||||
|
||||
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let res = ServiceResponse::new(req, HttpResponse::Ok().finish());
|
||||
for unit in &mut format.0 {
|
||||
unit.render_response(&resp);
|
||||
unit.render_response(&res);
|
||||
}
|
||||
|
||||
let entry_time = OffsetDateTime::now_utc();
|
||||
@ -832,7 +940,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_custom_closure_log() {
|
||||
async fn test_custom_closure_req_log() {
|
||||
let mut logger = Logger::new("test %{CUSTOM}xi")
|
||||
.custom_request_replace("CUSTOM", |_req: &ServiceRequest| -> String {
|
||||
String::from("custom_log")
|
||||
@ -857,6 +965,38 @@ mod tests {
|
||||
assert_eq!(log_output, "custom_log");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_custom_closure_response_log() {
|
||||
let mut logger = Logger::new("test %{CUSTOM}xo").custom_response_replace(
|
||||
"CUSTOM",
|
||||
|res: &ServiceResponse| -> String {
|
||||
if res.status().as_u16() == 200 {
|
||||
String::from("custom_log")
|
||||
} else {
|
||||
String::from("-")
|
||||
}
|
||||
},
|
||||
);
|
||||
let mut unit = Rc::get_mut(&mut logger.0).unwrap().format.0[1].clone();
|
||||
|
||||
let label = match &unit {
|
||||
FormatText::CustomResponse(label, _) => label,
|
||||
ft => panic!("expected CustomResponse, found {:?}", ft),
|
||||
};
|
||||
|
||||
assert_eq!(label, "CUSTOM");
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp_ok = ServiceResponse::new(req, HttpResponse::Ok().finish());
|
||||
let now = OffsetDateTime::now_utc();
|
||||
unit.render_response(&resp_ok);
|
||||
|
||||
let render = |fmt: &mut fmt::Formatter<'_>| unit.render(fmt, 1024, now);
|
||||
|
||||
let log_output = FormatDisplay(&render).to_string();
|
||||
assert_eq!(log_output, "custom_log");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_closure_logger_in_middleware() {
|
||||
let captured = "custom log replacement";
|
||||
|
238
actix-web/src/redirect.rs
Normal file
238
actix-web/src/redirect.rs
Normal file
@ -0,0 +1,238 @@
|
||||
//! See [`Redirect`] for service/responder documentation.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use actix_utils::future::ready;
|
||||
|
||||
use crate::{
|
||||
dev::{fn_service, AppService, HttpServiceFactory, ResourceDef, ServiceRequest},
|
||||
http::{header::LOCATION, StatusCode},
|
||||
HttpRequest, HttpResponse, Responder,
|
||||
};
|
||||
|
||||
/// An HTTP service for redirecting one path to another path or URL.
|
||||
///
|
||||
/// By default, the "307 Temporary Redirect" status is used when responding. See [this MDN
|
||||
/// article][mdn-redirects] on why 307 is preferred over 302.
|
||||
///
|
||||
/// # Examples
|
||||
/// As service:
|
||||
/// ```
|
||||
/// use actix_web::{web, App};
|
||||
///
|
||||
/// App::new()
|
||||
/// // redirect "/duck" to DuckDuckGo
|
||||
/// .service(web::redirect("/duck", "https://duck.com"))
|
||||
/// .service(
|
||||
/// // redirect "/api/old" to "/api/new"
|
||||
/// web::scope("/api").service(web::redirect("/old", "/new"))
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// As responder:
|
||||
/// ```
|
||||
/// use actix_web::{web::Redirect, Responder};
|
||||
///
|
||||
/// async fn handler() -> impl Responder {
|
||||
/// // sends a permanent (308) redirect to duck.com
|
||||
/// Redirect::to("https://duck.com").permanent()
|
||||
/// }
|
||||
/// # actix_web::web::to(handler);
|
||||
/// ```
|
||||
///
|
||||
/// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Redirect {
|
||||
from: Cow<'static, str>,
|
||||
to: Cow<'static, str>,
|
||||
status_code: StatusCode,
|
||||
}
|
||||
|
||||
impl Redirect {
|
||||
/// Construct a new `Redirect` service that matches a path.
|
||||
///
|
||||
/// This service will match exact paths equal to `from` within the current scope. I.e., when
|
||||
/// registered on the root `App`, it will match exact, whole paths. But when registered on a
|
||||
/// `Scope`, it will match paths under that scope, ignoring the defined scope prefix, just like
|
||||
/// a normal `Resource` or `Route`.
|
||||
///
|
||||
/// The `to` argument can be path or URL; whatever is provided shall be used verbatim when
|
||||
/// setting the redirect location. This means that relative paths can be used to navigate
|
||||
/// relatively to matched paths.
|
||||
///
|
||||
/// Prefer [`Redirect::to()`](Self::to) when using `Redirect` as a responder since `from` has
|
||||
/// no meaning in that context.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use actix_web::{web::Redirect, App};
|
||||
/// App::new()
|
||||
/// // redirects "/oh/hi/mark" to "/oh/bye/johnny"
|
||||
/// .service(Redirect::new("/oh/hi/mark", "../../bye/johnny"));
|
||||
/// ```
|
||||
pub fn new(from: impl Into<Cow<'static, str>>, to: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self {
|
||||
from: from.into(),
|
||||
to: to.into(),
|
||||
status_code: StatusCode::TEMPORARY_REDIRECT,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a new `Redirect` to use as a responder.
|
||||
///
|
||||
/// Only receives the `to` argument since responders do not need to do route matching.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{web::Redirect, Responder};
|
||||
///
|
||||
/// async fn admin_page() -> impl Responder {
|
||||
/// // sends a temporary 307 redirect to the login path
|
||||
/// Redirect::to("/login")
|
||||
/// }
|
||||
/// # actix_web::web::to(admin_page);
|
||||
/// ```
|
||||
pub fn to(to: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self {
|
||||
from: "/".into(),
|
||||
to: to.into(),
|
||||
status_code: StatusCode::TEMPORARY_REDIRECT,
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the "308 Permanent Redirect" status when responding.
|
||||
///
|
||||
/// See [this MDN article][mdn-redirects] on why 308 is preferred over 301.
|
||||
///
|
||||
/// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#permanent_redirections
|
||||
pub fn permanent(self) -> Self {
|
||||
self.using_status_code(StatusCode::PERMANENT_REDIRECT)
|
||||
}
|
||||
|
||||
/// Use the "307 Temporary Redirect" status when responding.
|
||||
///
|
||||
/// See [this MDN article][mdn-redirects] on why 307 is preferred over 302.
|
||||
///
|
||||
/// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections
|
||||
pub fn temporary(self) -> Self {
|
||||
self.using_status_code(StatusCode::TEMPORARY_REDIRECT)
|
||||
}
|
||||
|
||||
/// Use the "303 See Other" status when responding.
|
||||
///
|
||||
/// This status code is semantically correct as the response to a successful login, for example.
|
||||
pub fn see_other(self) -> Self {
|
||||
self.using_status_code(StatusCode::SEE_OTHER)
|
||||
}
|
||||
|
||||
/// Allows the use of custom status codes for less common redirect types.
|
||||
///
|
||||
/// In most cases, the default status ("308 Permanent Redirect") or using the `temporary`
|
||||
/// method, which uses the "307 Temporary Redirect" status have more consistent behavior than
|
||||
/// 301 and 302 codes, respectively.
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_web::{http::StatusCode, web::Redirect};
|
||||
/// // redirects would use "301 Moved Permanently" status code
|
||||
/// Redirect::new("/old", "/new")
|
||||
/// .using_status_code(StatusCode::MOVED_PERMANENTLY);
|
||||
///
|
||||
/// // redirects would use "302 Found" status code
|
||||
/// Redirect::new("/old", "/new")
|
||||
/// .using_status_code(StatusCode::FOUND);
|
||||
/// ```
|
||||
pub fn using_status_code(mut self, status: StatusCode) -> Self {
|
||||
self.status_code = status;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpServiceFactory for Redirect {
|
||||
fn register(self, config: &mut AppService) {
|
||||
let redirect = self.clone();
|
||||
let rdef = ResourceDef::new(self.from.into_owned());
|
||||
let redirect_factory = fn_service(move |mut req: ServiceRequest| {
|
||||
let res = redirect.clone().respond_to(req.parts_mut().0);
|
||||
ready(Ok(req.into_response(res.map_into_boxed_body())))
|
||||
});
|
||||
|
||||
config.register_service(rdef, None, redirect_factory, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Responder for Redirect {
|
||||
type Body = ();
|
||||
|
||||
fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||
let mut res = HttpResponse::with_body(self.status_code, ());
|
||||
|
||||
if let Ok(hdr_val) = self.to.parse() {
|
||||
res.headers_mut().insert(LOCATION, hdr_val);
|
||||
} else {
|
||||
log::error!(
|
||||
"redirect target location can not be converted to header value: {:?}",
|
||||
self.to
|
||||
);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{dev::Service, http::StatusCode, test, App};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn absolute_redirects() {
|
||||
let redirector = Redirect::new("/one", "/two").permanent();
|
||||
|
||||
let svc = test::init_service(App::new().service(redirector)).await;
|
||||
|
||||
let req = test::TestRequest::default().uri("/one").to_request();
|
||||
let res = svc.call(req).await.unwrap();
|
||||
assert_eq!(res.status(), StatusCode::from_u16(308).unwrap());
|
||||
let hdr = res.headers().get(&LOCATION).unwrap();
|
||||
assert_eq!(hdr.to_str().unwrap(), "/two");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn relative_redirects() {
|
||||
let redirector = Redirect::new("/one", "two").permanent();
|
||||
|
||||
let svc = test::init_service(App::new().service(redirector)).await;
|
||||
|
||||
let req = test::TestRequest::default().uri("/one").to_request();
|
||||
let res = svc.call(req).await.unwrap();
|
||||
assert_eq!(res.status(), StatusCode::from_u16(308).unwrap());
|
||||
let hdr = res.headers().get(&LOCATION).unwrap();
|
||||
assert_eq!(hdr.to_str().unwrap(), "two");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn temporary_redirects() {
|
||||
let external_service = Redirect::new("/external", "https://duck.com");
|
||||
|
||||
let svc = test::init_service(App::new().service(external_service)).await;
|
||||
|
||||
let req = test::TestRequest::default().uri("/external").to_request();
|
||||
let res = svc.call(req).await.unwrap();
|
||||
assert_eq!(res.status(), StatusCode::from_u16(307).unwrap());
|
||||
let hdr = res.headers().get(&LOCATION).unwrap();
|
||||
assert_eq!(hdr.to_str().unwrap(), "https://duck.com");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn as_responder() {
|
||||
let responder = Redirect::to("https://duck.com");
|
||||
|
||||
let req = test::TestRequest::default().to_http_request();
|
||||
let res = responder.respond_to(&req);
|
||||
|
||||
assert_eq!(res.status(), StatusCode::from_u16(307).unwrap());
|
||||
let hdr = res.headers().get(&LOCATION).unwrap();
|
||||
assert_eq!(hdr.to_str().unwrap(), "https://duck.com");
|
||||
}
|
||||
}
|
@ -219,7 +219,7 @@ impl HttpRequest {
|
||||
/// for urls that do not contain variable parts.
|
||||
pub fn url_for_static(&self, name: &str) -> Result<url::Url, UrlGenerationError> {
|
||||
const NO_PARAMS: [&str; 0] = [];
|
||||
self.url_for(name, &NO_PARAMS)
|
||||
self.url_for(name, NO_PARAMS)
|
||||
}
|
||||
|
||||
/// Get a reference to a `ResourceMap` of current application.
|
||||
@ -306,7 +306,7 @@ impl HttpRequest {
|
||||
|
||||
#[inline]
|
||||
fn app_state(&self) -> &AppInitServiceState {
|
||||
&*self.inner.app_state
|
||||
&self.inner.app_state
|
||||
}
|
||||
|
||||
/// Load request cookies.
|
||||
@ -583,14 +583,14 @@ mod tests {
|
||||
.to_http_request();
|
||||
|
||||
assert_eq!(
|
||||
req.url_for("unknown", &["test"]),
|
||||
req.url_for("unknown", ["test"]),
|
||||
Err(UrlGenerationError::ResourceNotFound)
|
||||
);
|
||||
assert_eq!(
|
||||
req.url_for("index", &["test"]),
|
||||
req.url_for("index", ["test"]),
|
||||
Err(UrlGenerationError::NotEnoughElements)
|
||||
);
|
||||
let url = req.url_for("index", &["test", "html"]);
|
||||
let url = req.url_for("index", ["test", "html"]);
|
||||
assert_eq!(
|
||||
url.ok().unwrap().as_str(),
|
||||
"http://www.rust-lang.org/user/test.html"
|
||||
@ -646,7 +646,7 @@ mod tests {
|
||||
rmap.add(&mut rdef, None);
|
||||
|
||||
let req = TestRequest::default().rmap(rmap).to_http_request();
|
||||
let url = req.url_for("youtube", &["oHg5SJYRHA0"]);
|
||||
let url = req.url_for("youtube", ["oHg5SJYRHA0"]);
|
||||
assert_eq!(
|
||||
url.ok().unwrap().as_str(),
|
||||
"https://youtube.com/watch/oHg5SJYRHA0"
|
||||
|
@ -13,8 +13,9 @@ use crate::{
|
||||
body::MessageBody,
|
||||
data::Data,
|
||||
dev::{ensure_leading_slash, AppService, ResourceDef},
|
||||
guard::Guard,
|
||||
guard::{self, Guard},
|
||||
handler::Handler,
|
||||
http::header,
|
||||
route::{Route, RouteService},
|
||||
service::{
|
||||
BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest,
|
||||
@ -40,8 +41,11 @@ use crate::{
|
||||
/// .route(web::get().to(|| HttpResponse::Ok())));
|
||||
/// ```
|
||||
///
|
||||
/// If no matching route could be found, *405* response code get returned. Default behavior could be
|
||||
/// overridden with `default_resource()` method.
|
||||
/// If no matching route is found, [a 405 response is returned with an appropriate Allow header][RFC
|
||||
/// 9110 §15.5.6]. This default behavior can be overridden using
|
||||
/// [`default_service()`](Self::default_service).
|
||||
///
|
||||
/// [RFC 9110 §15.5.6]: https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.6
|
||||
pub struct Resource<T = ResourceEndpoint> {
|
||||
endpoint: T,
|
||||
rdef: Patterns,
|
||||
@ -66,7 +70,19 @@ impl Resource {
|
||||
guards: Vec::new(),
|
||||
app_data: None,
|
||||
default: boxed::factory(fn_service(|req: ServiceRequest| async {
|
||||
use crate::HttpMessage as _;
|
||||
|
||||
let allowed = req.extensions().get::<guard::RegisteredMethods>().cloned();
|
||||
|
||||
if let Some(methods) = allowed {
|
||||
Ok(req.into_response(
|
||||
HttpResponse::MethodNotAllowed()
|
||||
.insert_header(header::Allow(methods.0))
|
||||
.finish(),
|
||||
))
|
||||
} else {
|
||||
Ok(req.into_response(HttpResponse::MethodNotAllowed()))
|
||||
}
|
||||
})),
|
||||
}
|
||||
}
|
||||
@ -309,13 +325,28 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Default service to be used if no matching route could be found.
|
||||
/// Sets the default service to be used if no matching route is found.
|
||||
///
|
||||
/// You can use a [`Route`] as default service.
|
||||
/// Unlike [`Scope`]s, a `Resource` does _not_ inherit its parent's default service. You can
|
||||
/// use a [`Route`] as default service.
|
||||
///
|
||||
/// If a default service is not registered, an empty `405 Method Not Allowed` response will be
|
||||
/// sent to the client instead. Unlike [`Scope`](crate::Scope)s, a [`Resource`] does **not**
|
||||
/// inherit its parent's default service.
|
||||
/// If a custom default service is not registered, an empty `405 Method Not Allowed` response
|
||||
/// with an appropriate Allow header will be sent instead.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{App, HttpResponse, web};
|
||||
///
|
||||
/// let resource = web::resource("/test")
|
||||
/// .route(web::get().to(HttpResponse::Ok))
|
||||
/// .default_service(web::to(|| {
|
||||
/// HttpResponse::BadRequest()
|
||||
/// }));
|
||||
///
|
||||
/// App::new().service(resource);
|
||||
/// ```
|
||||
///
|
||||
/// [`Scope`]: crate::Scope
|
||||
pub fn default_service<F, U>(mut self, f: F) -> Self
|
||||
where
|
||||
F: IntoServiceFactory<U, ServiceRequest>,
|
||||
@ -606,7 +637,11 @@ mod tests {
|
||||
async fn test_default_resource() {
|
||||
let srv = init_service(
|
||||
App::new()
|
||||
.service(web::resource("/test").route(web::get().to(HttpResponse::Ok)))
|
||||
.service(
|
||||
web::resource("/test")
|
||||
.route(web::get().to(HttpResponse::Ok))
|
||||
.route(web::delete().to(HttpResponse::Ok)),
|
||||
)
|
||||
.default_service(|r: ServiceRequest| {
|
||||
ok(r.into_response(HttpResponse::BadRequest()))
|
||||
}),
|
||||
@ -621,6 +656,10 @@ mod tests {
|
||||
.to_request();
|
||||
let resp = call_service(&srv, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::ALLOW).unwrap().as_bytes(),
|
||||
b"GET, DELETE"
|
||||
);
|
||||
|
||||
let srv = init_service(
|
||||
App::new().service(
|
||||
|
@ -457,7 +457,7 @@ mod tests {
|
||||
assert_eq!(ct, HeaderValue::from_static("application/json"));
|
||||
assert_body_eq!(res, br#"["v1","v2","v3"]"#);
|
||||
|
||||
let res = HttpResponse::Ok().json(&["v1", "v2", "v3"]);
|
||||
let res = HttpResponse::Ok().json(["v1", "v2", "v3"]);
|
||||
let ct = res.headers().get(CONTENT_TYPE).unwrap();
|
||||
assert_eq!(ct, HeaderValue::from_static("application/json"));
|
||||
assert_body_eq!(res, br#"["v1","v2","v3"]"#);
|
||||
|
@ -449,12 +449,12 @@ mod tests {
|
||||
let req = req.to_http_request();
|
||||
|
||||
let url = rmap
|
||||
.url_for(&req, "post", &["u123", "foobar"])
|
||||
.url_for(&req, "post", ["u123", "foobar"])
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert_eq!(url, "http://localhost:8888/user/u123/post/foobar");
|
||||
|
||||
assert!(rmap.url_for(&req, "missing", &["u123"]).is_err());
|
||||
assert!(rmap.url_for(&req, "missing", ["u123"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -490,7 +490,7 @@ mod tests {
|
||||
assert_eq!(url.path(), OUTPUT);
|
||||
|
||||
assert!(rmap.url_for(&req, "external.2", INPUT).is_err());
|
||||
assert!(rmap.url_for(&req, "external.2", &[""]).is_err());
|
||||
assert!(rmap.url_for(&req, "external.2", [""]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -524,7 +524,7 @@ mod tests {
|
||||
let req = req.to_http_request();
|
||||
|
||||
assert_eq!(
|
||||
rmap.url_for(&req, "duck", &["abcd"]).unwrap().to_string(),
|
||||
rmap.url_for(&req, "duck", ["abcd"]).unwrap().to_string(),
|
||||
"https://duck.com/abcd"
|
||||
);
|
||||
}
|
||||
@ -552,9 +552,9 @@ mod tests {
|
||||
|
||||
let req = crate::test::TestRequest::default().to_http_request();
|
||||
|
||||
let url = rmap.url_for(&req, "nested", &[""; 0]).unwrap().to_string();
|
||||
let url = rmap.url_for(&req, "nested", [""; 0]).unwrap().to_string();
|
||||
assert_eq!(url, "http://localhost:8080/bar/nested");
|
||||
|
||||
assert!(rmap.url_for(&req, "missing", &["u123"]).is_err());
|
||||
assert!(rmap.url_for(&req, "missing", ["u123"]).is_err());
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@
|
||||
//! ```
|
||||
//!
|
||||
//! # Running Actix Web Using `#[tokio::main]`
|
||||
//! If you need to run something alongside Actix Web that uses Tokio's work stealing functionality,
|
||||
//! If you need to run something that uses Tokio's work stealing functionality alongside Actix Web,
|
||||
//! you can run Actix Web under `#[tokio::main]`. The [`Server`](crate::dev::Server) object returned
|
||||
//! from [`HttpServer::run`](crate::HttpServer::run) can also be [`spawn`]ed, if preferred.
|
||||
//!
|
||||
|
@ -1133,7 +1133,7 @@ mod tests {
|
||||
"/",
|
||||
web::get().to(|req: HttpRequest| {
|
||||
HttpResponse::Ok()
|
||||
.body(req.url_for("youtube", &["xxxxxx"]).unwrap().to_string())
|
||||
.body(req.url_for("youtube", ["xxxxxx"]).unwrap().to_string())
|
||||
}),
|
||||
);
|
||||
}));
|
||||
@ -1152,8 +1152,7 @@ mod tests {
|
||||
let srv = init_service(App::new().service(web::scope("/a").service(
|
||||
web::scope("/b").service(web::resource("/c/{stuff}").name("c").route(
|
||||
web::get().to(|req: HttpRequest| {
|
||||
HttpResponse::Ok()
|
||||
.body(format!("{}", req.url_for("c", &["12345"]).unwrap()))
|
||||
HttpResponse::Ok().body(format!("{}", req.url_for("c", ["12345"]).unwrap()))
|
||||
}),
|
||||
)),
|
||||
)))
|
||||
|
@ -327,9 +327,7 @@ impl ServiceRequest {
|
||||
.push(extensions);
|
||||
}
|
||||
|
||||
/// Creates a context object for use with a [guard](crate::guard).
|
||||
///
|
||||
/// Useful if you are implementing
|
||||
/// Creates a context object for use with a routing [guard](crate::guard).
|
||||
#[inline]
|
||||
pub fn guard_ctx(&self) -> GuardContext<'_> {
|
||||
GuardContext { req: self }
|
||||
|
@ -10,12 +10,16 @@
|
||||
//! # Calling Test Service
|
||||
//! - [`TestRequest`]
|
||||
//! - [`call_service`]
|
||||
//! - [`try_call_service`]
|
||||
//! - [`call_and_read_body`]
|
||||
//! - [`call_and_read_body_json`]
|
||||
//! - [`try_call_and_read_body_json`]
|
||||
//!
|
||||
//! # Reading Response Payloads
|
||||
//! - [`read_body`]
|
||||
//! - [`try_read_body`]
|
||||
//! - [`read_body_json`]
|
||||
//! - [`try_read_body_json`]
|
||||
|
||||
// TODO: more docs on generally how testing works with these parts
|
||||
|
||||
@ -31,7 +35,8 @@ pub use self::test_services::{default_service, ok_service, simple_service, statu
|
||||
#[allow(deprecated)]
|
||||
pub use self::test_utils::{
|
||||
call_and_read_body, call_and_read_body_json, call_service, init_service, read_body,
|
||||
read_body_json, read_response, read_response_json,
|
||||
read_body_json, read_response, read_response_json, try_call_and_read_body_json,
|
||||
try_call_service, try_read_body, try_read_body_json,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user