mirror of https://github.com/fafhrd91/actix-web
Compare commits
No commits in common. "2f64cdb60af998be3ccf0a3596c0353ee7d4b8a7" and "013a8ec6b6cd2271a44e83a7eb98c24f621a843a" have entirely different histories.
2f64cdb60a
...
013a8ec6b6
|
@ -1,8 +1,7 @@
|
||||||
disallowed-names = [
|
disallowed-names = [
|
||||||
"..",
|
|
||||||
"e", # no single letter error bindings
|
"e", # no single letter error bindings
|
||||||
]
|
]
|
||||||
disallowed-methods = [
|
disallowed-methods = [
|
||||||
{ path = "std::cell::RefCell::default()", reason = "prefer explicit inner type default" },
|
"std::cell::RefCell::default()",
|
||||||
{ path = "std::rc::Rc::default()", reason = "prefer explicit inner type default" },
|
"std::rc::Rc::default()",
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,12 +1,3 @@
|
||||||
version: "0.2"
|
version: "0.2"
|
||||||
words:
|
words:
|
||||||
- actix
|
- actix
|
||||||
- addrs
|
|
||||||
- bytestring
|
|
||||||
- httparse
|
|
||||||
- msrv
|
|
||||||
- realip
|
|
||||||
- rustls
|
|
||||||
- rustup
|
|
||||||
- serde
|
|
||||||
- zstd
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
- package-ecosystem: cargo
|
- package-ecosystem: cargo
|
||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
versioning-strategy: lockfile-only
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
|
|
@ -44,12 +44,12 @@ jobs:
|
||||||
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
|
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Install Rust (${{ matrix.version.name }})
|
- name: Install Rust (${{ matrix.version.name }})
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ matrix.version.version }}
|
toolchain: ${{ matrix.version.version }}
|
||||||
|
|
||||||
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
|
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
|
||||||
uses: taiki-e/install-action@v2.50.10
|
uses: taiki-e/install-action@v2.49.50
|
||||||
with:
|
with:
|
||||||
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
|
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
|
||||||
|
|
||||||
|
@ -80,10 +80,10 @@ jobs:
|
||||||
uses: rui314/setup-mold@v1
|
uses: rui314/setup-mold@v1
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
|
|
||||||
- name: Install just, cargo-hack
|
- name: Install just, cargo-hack
|
||||||
uses: taiki-e/install-action@v2.50.10
|
uses: taiki-e/install-action@v2.49.50
|
||||||
with:
|
with:
|
||||||
tool: just,cargo-hack
|
tool: just,cargo-hack
|
||||||
|
|
||||||
|
|
|
@ -59,12 +59,12 @@ jobs:
|
||||||
uses: rui314/setup-mold@v1
|
uses: rui314/setup-mold@v1
|
||||||
|
|
||||||
- name: Install Rust (${{ matrix.version.name }})
|
- name: Install Rust (${{ matrix.version.name }})
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ matrix.version.version }}
|
toolchain: ${{ matrix.version.version }}
|
||||||
|
|
||||||
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
|
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
|
||||||
uses: taiki-e/install-action@v2.50.10
|
uses: taiki-e/install-action@v2.49.50
|
||||||
with:
|
with:
|
||||||
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
|
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
|
|
||||||
|
@ -108,12 +108,12 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust (nightly)
|
- name: Install Rust (nightly)
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
|
|
||||||
- name: Install just
|
- name: Install just
|
||||||
uses: taiki-e/install-action@v2.50.10
|
uses: taiki-e/install-action@v2.49.50
|
||||||
with:
|
with:
|
||||||
tool: just
|
tool: just
|
||||||
|
|
||||||
|
|
|
@ -18,13 +18,13 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust (nightly)
|
- name: Install Rust (nightly)
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
components: llvm-tools
|
components: llvm-tools
|
||||||
|
|
||||||
- name: Install just, cargo-llvm-cov, cargo-nextest
|
- name: Install just, cargo-llvm-cov, cargo-nextest
|
||||||
uses: taiki-e/install-action@v2.50.10
|
uses: taiki-e/install-action@v2.49.50
|
||||||
with:
|
with:
|
||||||
tool: just,cargo-llvm-cov,cargo-nextest
|
tool: just,cargo-llvm-cov,cargo-nextest
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust (nightly)
|
- name: Install Rust (nightly)
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
|
@ -36,7 +36,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
components: clippy
|
components: clippy
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust (nightly)
|
- name: Install Rust (nightly)
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
components: rust-docs
|
components: rust-docs
|
||||||
|
@ -72,12 +72,12 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }})
|
- name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }})
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
|
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
|
||||||
|
|
||||||
- name: Install just
|
- name: Install just
|
||||||
uses: taiki-e/install-action@v2.50.10
|
uses: taiki-e/install-action@v2.49.50
|
||||||
with:
|
with:
|
||||||
tool: just
|
tool: just
|
||||||
|
|
||||||
|
|
38
.taplo.toml
38
.taplo.toml
|
@ -1,38 +0,0 @@
|
||||||
exclude = ["target/*"]
|
|
||||||
include = ["**/*.toml"]
|
|
||||||
|
|
||||||
[formatting]
|
|
||||||
column_width = 100
|
|
||||||
align_comments = false
|
|
||||||
|
|
||||||
[[rule]]
|
|
||||||
include = ["**/Cargo.toml"]
|
|
||||||
keys = ["features"]
|
|
||||||
formatting.column_width = 105
|
|
||||||
formatting.reorder_keys = false
|
|
||||||
|
|
||||||
[[rule]]
|
|
||||||
include = ["**/Cargo.toml"]
|
|
||||||
keys = [
|
|
||||||
"dependencies",
|
|
||||||
"*-dependencies",
|
|
||||||
"workspace.dependencies",
|
|
||||||
"workspace.*-dependencies",
|
|
||||||
"target.*.dependencies",
|
|
||||||
"target.*.*-dependencies",
|
|
||||||
]
|
|
||||||
formatting.column_width = 120
|
|
||||||
formatting.reorder_keys = true
|
|
||||||
|
|
||||||
[[rule]]
|
|
||||||
include = ["**/Cargo.toml"]
|
|
||||||
keys = [
|
|
||||||
"dependencies.*",
|
|
||||||
"*-dependencies.*",
|
|
||||||
"workspace.dependencies.*",
|
|
||||||
"workspace.*-dependencies.*",
|
|
||||||
"target.*.dependencies",
|
|
||||||
"target.*.*-dependencies",
|
|
||||||
]
|
|
||||||
formatting.column_width = 120
|
|
||||||
formatting.reorder_keys = false
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-files"
|
name = "actix-files"
|
||||||
version = "0.6.6"
|
version = "0.6.6"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
|
authors = [
|
||||||
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
|
]
|
||||||
description = "Static file serving for Actix Web"
|
description = "Static file serving for Actix Web"
|
||||||
keywords = ["actix", "http", "async", "futures"]
|
keywords = ["actix", "http", "async", "futures"]
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
|
@ -11,7 +14,13 @@ license = "MIT OR Apache-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[package.metadata.cargo_check_external_types]
|
[package.metadata.cargo_check_external_types]
|
||||||
allowed_external_types = ["actix_http::*", "actix_service::*", "actix_web::*", "http::*", "mime::*"]
|
allowed_external_types = [
|
||||||
|
"actix_http::*",
|
||||||
|
"actix_service::*",
|
||||||
|
"actix_web::*",
|
||||||
|
"http::*",
|
||||||
|
"mime::*",
|
||||||
|
]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
|
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
|
||||||
|
|
|
@ -37,25 +37,25 @@ default = []
|
||||||
openssl = ["tls-openssl", "awc/openssl"]
|
openssl = ["tls-openssl", "awc/openssl"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-codec = "0.5"
|
|
||||||
actix-rt = "2.2"
|
|
||||||
actix-server = "2"
|
|
||||||
actix-service = "2"
|
actix-service = "2"
|
||||||
|
actix-codec = "0.5"
|
||||||
actix-tls = "3"
|
actix-tls = "3"
|
||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
|
actix-rt = "2.2"
|
||||||
|
actix-server = "2"
|
||||||
awc = { version = "3", default-features = false }
|
awc = { version = "3", default-features = false }
|
||||||
|
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
futures-core = { version = "0.3.17", default-features = false }
|
futures-core = { version = "0.3.17", default-features = false }
|
||||||
http = "0.2.7"
|
http = "0.2.7"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
socket2 = "0.5"
|
||||||
serde = "1"
|
serde = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_urlencoded = "0.7"
|
|
||||||
slab = "0.4"
|
slab = "0.4"
|
||||||
socket2 = "0.5"
|
serde_urlencoded = "0.7"
|
||||||
tls-openssl = { version = "0.10.55", package = "openssl", optional = true }
|
tls-openssl = { version = "0.10.55", package = "openssl", optional = true }
|
||||||
tokio = { version = "1.38.2", features = ["sync"] }
|
tokio = { version = "1.24.2", features = ["sync"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-http = "3"
|
actix-http = "3"
|
||||||
|
|
|
@ -2,10 +2,6 @@
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
## 3.11.0
|
|
||||||
|
|
||||||
- Update `brotli` dependency to `8`.
|
|
||||||
|
|
||||||
## 3.10.0
|
## 3.10.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.11.0"
|
version = "3.10.0"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
|
authors = [
|
||||||
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
|
]
|
||||||
description = "HTTP types and services for the Actix ecosystem"
|
description = "HTTP types and services for the Actix ecosystem"
|
||||||
keywords = ["actix", "http", "framework", "async", "futures"]
|
keywords = ["actix", "http", "framework", "async", "futures"]
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
|
@ -59,7 +62,12 @@ default = []
|
||||||
http2 = ["dep:h2"]
|
http2 = ["dep:h2"]
|
||||||
|
|
||||||
# WebSocket protocol implementation
|
# WebSocket protocol implementation
|
||||||
ws = ["dep:local-channel", "dep:base64", "dep:rand", "dep:sha1"]
|
ws = [
|
||||||
|
"dep:local-channel",
|
||||||
|
"dep:base64",
|
||||||
|
"dep:rand",
|
||||||
|
"dep:sha1",
|
||||||
|
]
|
||||||
|
|
||||||
# TLS via OpenSSL
|
# TLS via OpenSSL
|
||||||
openssl = ["__tls", "actix-tls/accept", "actix-tls/openssl"]
|
openssl = ["__tls", "actix-tls/accept", "actix-tls/openssl"]
|
||||||
|
@ -93,10 +101,10 @@ __compress = []
|
||||||
__tls = []
|
__tls = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-codec = "0.5"
|
|
||||||
actix-rt = { version = "2.2", default-features = false }
|
|
||||||
actix-service = "2"
|
actix-service = "2"
|
||||||
|
actix-codec = "0.5"
|
||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
|
actix-rt = { version = "2.2", default-features = false }
|
||||||
|
|
||||||
bitflags = "2"
|
bitflags = "2"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
|
@ -114,7 +122,7 @@ mime = "0.3.4"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
smallvec = "1.6.1"
|
smallvec = "1.6.1"
|
||||||
tokio = { version = "1.38.2", features = [] }
|
tokio = { version = "1.24.2", features = [] }
|
||||||
tokio-util = { version = "0.7", features = ["io", "codec"] }
|
tokio-util = { version = "0.7", features = ["io", "codec"] }
|
||||||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||||
|
|
||||||
|
@ -122,8 +130,8 @@ tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||||
h2 = { version = "0.3.26", optional = true }
|
h2 = { version = "0.3.26", optional = true }
|
||||||
|
|
||||||
# websockets
|
# websockets
|
||||||
base64 = { version = "0.22", optional = true }
|
|
||||||
local-channel = { version = "0.1", optional = true }
|
local-channel = { version = "0.1", optional = true }
|
||||||
|
base64 = { version = "0.22", optional = true }
|
||||||
rand = { version = "0.9", optional = true }
|
rand = { version = "0.9", optional = true }
|
||||||
sha1 = { version = "0.10", optional = true }
|
sha1 = { version = "0.10", optional = true }
|
||||||
|
|
||||||
|
@ -131,7 +139,7 @@ sha1 = { version = "0.10", optional = true }
|
||||||
actix-tls = { version = "3.4", default-features = false, optional = true }
|
actix-tls = { version = "3.4", default-features = false, optional = true }
|
||||||
|
|
||||||
# compress-*
|
# compress-*
|
||||||
brotli = { version = "8", optional = true }
|
brotli = { version = "7", optional = true }
|
||||||
flate2 = { version = "1.0.13", optional = true }
|
flate2 = { version = "1.0.13", optional = true }
|
||||||
zstd = { version = "0.13", optional = true }
|
zstd = { version = "0.13", optional = true }
|
||||||
|
|
||||||
|
@ -150,14 +158,14 @@ memchr = "2.4"
|
||||||
once_cell = "1.21"
|
once_cell = "1.21"
|
||||||
rcgen = "0.13"
|
rcgen = "0.13"
|
||||||
regex = "1.3"
|
regex = "1.3"
|
||||||
rustls-pemfile = "2"
|
|
||||||
rustversion = "1"
|
rustversion = "1"
|
||||||
|
rustls-pemfile = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
static_assertions = "1"
|
static_assertions = "1"
|
||||||
tls-openssl = { package = "openssl", version = "0.10.55" }
|
tls-openssl = { package = "openssl", version = "0.10.55" }
|
||||||
tls-rustls_023 = { package = "rustls", version = "0.23" }
|
tls-rustls_023 = { package = "rustls", version = "0.23" }
|
||||||
tokio = { version = "1.38.2", features = ["net", "rt", "macros"] }
|
tokio = { version = "1.24.2", features = ["net", "rt", "macros"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
@ -5,11 +5,11 @@
|
||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-http)
|
[](https://crates.io/crates/actix-http)
|
||||||
[](https://docs.rs/actix-http/3.11.0)
|
[](https://docs.rs/actix-http/3.10.0)
|
||||||

|

|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-http/3.11.0)
|
[](https://deps.rs/crate/actix-http/3.10.0)
|
||||||
[](https://crates.io/crates/actix-http)
|
[](https://crates.io/crates/actix-http)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ async fn main() -> io::Result<()> {
|
||||||
|
|
||||||
actix_rt::time::sleep(Duration::from_secs(1)).await;
|
actix_rt::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
|
||||||
yield Err(io::Error::other("abc"));
|
yield Err(io::Error::new(io::ErrorKind::Other, "abc"));
|
||||||
})))
|
})))
|
||||||
})
|
})
|
||||||
.tcp()
|
.tcp()
|
||||||
|
|
|
@ -190,7 +190,7 @@ mod tests {
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn to_body_limit_error() {
|
async fn to_body_limit_error() {
|
||||||
let err_stream = stream::once(async { Err(io::Error::other("")) });
|
let err_stream = stream::once(async { Err(io::Error::new(io::ErrorKind::Other, "")) });
|
||||||
let body = SizedStream::new(8, err_stream);
|
let body = SizedStream::new(8, err_stream);
|
||||||
// not too big, but propagates error from body stream
|
// not too big, but propagates error from body stream
|
||||||
assert!(to_bytes_limited(body, 10).await.unwrap().is_err());
|
assert!(to_bytes_limited(body, 10).await.unwrap().is_err());
|
||||||
|
|
|
@ -100,7 +100,10 @@ where
|
||||||
loop {
|
loop {
|
||||||
if let Some(ref mut fut) = this.fut {
|
if let Some(ref mut fut) = this.fut {
|
||||||
let (chunk, decoder) = ready!(Pin::new(fut).poll(cx)).map_err(|_| {
|
let (chunk, decoder) = ready!(Pin::new(fut).poll(cx)).map_err(|_| {
|
||||||
PayloadError::Io(io::Error::other("Blocking task was cancelled unexpectedly"))
|
PayloadError::Io(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"Blocking task was cancelled unexpectedly",
|
||||||
|
))
|
||||||
})??;
|
})??;
|
||||||
|
|
||||||
*this.decoder = Some(decoder);
|
*this.decoder = Some(decoder);
|
||||||
|
|
|
@ -183,7 +183,8 @@ where
|
||||||
if let Some(ref mut fut) = this.fut {
|
if let Some(ref mut fut) = this.fut {
|
||||||
let mut encoder = ready!(Pin::new(fut).poll(cx))
|
let mut encoder = ready!(Pin::new(fut).poll(cx))
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
EncoderError::Io(io::Error::other(
|
EncoderError::Io(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
"Blocking task was cancelled unexpectedly",
|
"Blocking task was cancelled unexpectedly",
|
||||||
))
|
))
|
||||||
})?
|
})?
|
||||||
|
|
|
@ -415,7 +415,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_as_response() {
|
fn test_as_response() {
|
||||||
let orig = io::Error::other("other");
|
let orig = io::Error::new(io::ErrorKind::Other, "other");
|
||||||
let err: Error = ParseError::Io(orig).into();
|
let err: Error = ParseError::Io(orig).into();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", err),
|
format!("{}", err),
|
||||||
|
@ -425,14 +425,14 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_error_display() {
|
fn test_error_display() {
|
||||||
let orig = io::Error::other("other");
|
let orig = io::Error::new(io::ErrorKind::Other, "other");
|
||||||
let err = Error::new_io().with_cause(orig);
|
let err = Error::new_io().with_cause(orig);
|
||||||
assert_eq!("connection error: other", err.to_string());
|
assert_eq!("connection error: other", err.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_error_http_response() {
|
fn test_error_http_response() {
|
||||||
let orig = io::Error::other("other");
|
let orig = io::Error::new(io::ErrorKind::Other, "other");
|
||||||
let err = Error::new_io().with_cause(orig);
|
let err = Error::new_io().with_cause(orig);
|
||||||
let resp: Response<BoxBody> = err.into();
|
let resp: Response<BoxBody> = err.into();
|
||||||
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
@ -440,7 +440,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_payload_error() {
|
fn test_payload_error() {
|
||||||
let err: PayloadError = io::Error::other("ParseError").into();
|
let err: PayloadError = io::Error::new(io::ErrorKind::Other, "ParseError").into();
|
||||||
assert!(err.to_string().contains("ParseError"));
|
assert!(err.to_string().contains("ParseError"));
|
||||||
|
|
||||||
let err = PayloadError::Incomplete(None);
|
let err = PayloadError::Incomplete(None);
|
||||||
|
@ -475,7 +475,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_from() {
|
fn test_from() {
|
||||||
from_and_cause!(io::Error::other("other") => ParseError::Io(..));
|
from_and_cause!(io::Error::new(io::ErrorKind::Other, "other") => ParseError::Io(..));
|
||||||
from!(httparse::Error::HeaderName => ParseError::Header);
|
from!(httparse::Error::HeaderName => ParseError::Header);
|
||||||
from!(httparse::Error::HeaderName => ParseError::Header);
|
from!(httparse::Error::HeaderName => ParseError::Header);
|
||||||
from!(httparse::Error::HeaderValue => ParseError::Header);
|
from!(httparse::Error::HeaderValue => ParseError::Header);
|
||||||
|
|
|
@ -310,10 +310,10 @@ impl MessageType for RequestHeadType {
|
||||||
Version::HTTP_11 => "HTTP/1.1",
|
Version::HTTP_11 => "HTTP/1.1",
|
||||||
Version::HTTP_2 => "HTTP/2.0",
|
Version::HTTP_2 => "HTTP/2.0",
|
||||||
Version::HTTP_3 => "HTTP/3.0",
|
Version::HTTP_3 => "HTTP/3.0",
|
||||||
_ => return Err(io::Error::other("Unsupported version")),
|
_ => return Err(io::Error::new(io::ErrorKind::Other, "unsupported version")),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.map_err(io::Error::other)
|
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -433,7 +433,7 @@ impl TransferEncoding {
|
||||||
buf.extend_from_slice(b"0\r\n\r\n");
|
buf.extend_from_slice(b"0\r\n\r\n");
|
||||||
} else {
|
} else {
|
||||||
writeln!(helpers::MutWriter(buf), "{:X}\r", msg.len())
|
writeln!(helpers::MutWriter(buf), "{:X}\r", msg.len())
|
||||||
.map_err(io::Error::other)?;
|
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
|
||||||
|
|
||||||
buf.reserve(msg.len() + 2);
|
buf.reserve(msg.len() + 2);
|
||||||
buf.extend_from_slice(msg);
|
buf.extend_from_slice(msg);
|
||||||
|
|
|
@ -55,7 +55,7 @@ serde = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_plain = "1"
|
serde_plain = "1"
|
||||||
tempfile = { version = "3.4", optional = true }
|
tempfile = { version = "3.4", optional = true }
|
||||||
tokio = { version = "1.38.2", features = ["sync", "io-util"] }
|
tokio = { version = "1.24.2", features = ["sync", "io-util"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-http = "3"
|
actix-http = "3"
|
||||||
|
@ -66,10 +66,10 @@ actix-web = "4"
|
||||||
assert_matches = "1"
|
assert_matches = "1"
|
||||||
awc = "3"
|
awc = "3"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
futures-test = "0.3"
|
|
||||||
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||||
|
futures-test = "0.3"
|
||||||
multer = "3"
|
multer = "3"
|
||||||
tokio = { version = "1.38.2", features = ["sync"] }
|
tokio = { version = "1.24.2", features = ["sync"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|
|
@ -13,7 +13,10 @@ license = "MIT OR Apache-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[package.metadata.cargo_check_external_types]
|
[package.metadata.cargo_check_external_types]
|
||||||
allowed_external_types = ["http::*", "serde::*"]
|
allowed_external_types = [
|
||||||
|
"http::*",
|
||||||
|
"serde::*",
|
||||||
|
]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["http", "unicode"]
|
default = ["http", "unicode"]
|
||||||
|
@ -32,8 +35,8 @@ tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { version = "0.5", features = ["html_reports"] }
|
criterion = { version = "0.5", features = ["html_reports"] }
|
||||||
http = "0.2.7"
|
http = "0.2.7"
|
||||||
percent-encoding = "2.1"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
percent-encoding = "2.1"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-test"
|
name = "actix-test"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
|
authors = [
|
||||||
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
|
]
|
||||||
description = "Integration testing tools for Actix Web applications"
|
description = "Integration testing tools for Actix Web applications"
|
||||||
keywords = ["http", "web", "framework", "async", "futures"]
|
keywords = ["http", "web", "framework", "async", "futures"]
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
|
@ -69,7 +72,7 @@ tls-rustls-0_20 = { package = "rustls", version = "0.20", optional = true }
|
||||||
tls-rustls-0_21 = { package = "rustls", version = "0.21", optional = true }
|
tls-rustls-0_21 = { package = "rustls", version = "0.21", optional = true }
|
||||||
tls-rustls-0_22 = { package = "rustls", version = "0.22", optional = true }
|
tls-rustls-0_22 = { package = "rustls", version = "0.22", optional = true }
|
||||||
tls-rustls-0_23 = { package = "rustls", version = "0.23", default-features = false, optional = true }
|
tls-rustls-0_23 = { package = "rustls", version = "0.23", default-features = false, optional = true }
|
||||||
tokio = { version = "1.38.2", features = ["sync"] }
|
tokio = { version = "1.24.2", features = ["sync"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
@ -30,14 +30,14 @@ bytes = "1"
|
||||||
bytestring = "1"
|
bytestring = "1"
|
||||||
futures-core = { version = "0.3.17", default-features = false }
|
futures-core = { version = "0.3.17", default-features = false }
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
tokio = { version = "1.38.2", features = ["sync"] }
|
tokio = { version = "1.24.2", features = ["sync"] }
|
||||||
tokio-util = { version = "0.7", features = ["codec"] }
|
tokio-util = { version = "0.7", features = ["codec"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-test = "0.1"
|
actix-test = "0.1"
|
||||||
actix-web = { version = "4", features = ["macros"] }
|
|
||||||
awc = { version = "3", default-features = false }
|
awc = { version = "3", default-features = false }
|
||||||
|
actix-web = { version = "4", features = ["macros"] }
|
||||||
|
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||||
|
|
|
@ -776,7 +776,10 @@ where
|
||||||
}
|
}
|
||||||
Poll::Pending => break,
|
Poll::Pending => break,
|
||||||
Poll::Ready(Some(Err(err))) => {
|
Poll::Ready(Some(Err(err))) => {
|
||||||
return Poll::Ready(Some(Err(ProtocolError::Io(io::Error::other(err)))));
|
return Poll::Ready(Some(Err(ProtocolError::Io(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
format!("{err}"),
|
||||||
|
)))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -792,10 +795,11 @@ where
|
||||||
}
|
}
|
||||||
Some(frm) => {
|
Some(frm) => {
|
||||||
let msg = match frm {
|
let msg = match frm {
|
||||||
Frame::Text(data) => Message::Text(
|
Frame::Text(data) => {
|
||||||
ByteString::try_from(data)
|
Message::Text(ByteString::try_from(data).map_err(|err| {
|
||||||
.map_err(|err| ProtocolError::Io(io::Error::other(err)))?,
|
ProtocolError::Io(io::Error::new(io::ErrorKind::Other, err))
|
||||||
),
|
})?)
|
||||||
|
}
|
||||||
Frame::Binary(data) => Message::Binary(data),
|
Frame::Binary(data) => Message::Binary(data),
|
||||||
Frame::Ping(s) => Message::Ping(s),
|
Frame::Ping(s) => Message::Ping(s),
|
||||||
Frame::Pong(s) => Message::Pong(s),
|
Frame::Pong(s) => Message::Pong(s),
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
name = "actix-web-codegen"
|
name = "actix-web-codegen"
|
||||||
version = "4.3.0"
|
version = "4.3.0"
|
||||||
description = "Routing and runtime macros for Actix Web"
|
description = "Routing and runtime macros for Actix Web"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
|
authors = [
|
||||||
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
|
]
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
@ -30,8 +33,8 @@ actix-utils = "3"
|
||||||
actix-web = "4"
|
actix-web = "4"
|
||||||
|
|
||||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||||
rustversion-msrv = "0.100"
|
|
||||||
trybuild = "1"
|
trybuild = "1"
|
||||||
|
rustversion-msrv = "0.100"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
@ -2,18 +2,7 @@
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
- Add `experimental-introspection` feature for retrieving configured route paths and HTTP methods.
|
- Add `resources-introspection` feature for retrieving configured route paths and HTTP methods.
|
||||||
|
|
||||||
## 4.11.0
|
|
||||||
|
|
||||||
- Add `Logger::log_level()` method.
|
|
||||||
- Improve handling of non-UTF-8 header values in `Logger` middleware.
|
|
||||||
- Add `HttpServer::shutdown_signal()` method.
|
|
||||||
- Mark `HttpServer` as `#[must_use]`.
|
|
||||||
- Allow SVG images to be compressed by the `Compress` middleware.
|
|
||||||
- Ignore `Host` header in `Host` guard when connection protocol is HTTP/2.
|
|
||||||
- Re-export `mime` dependency.
|
|
||||||
- Update `brotli` dependency to `8`.
|
|
||||||
|
|
||||||
## 4.10.2
|
## 4.10.2
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-web"
|
name = "actix-web"
|
||||||
version = "4.11.0"
|
version = "4.10.2"
|
||||||
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
|
authors = [
|
||||||
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
|
]
|
||||||
keywords = ["actix", "http", "web", "framework", "async"]
|
keywords = ["actix", "http", "web", "framework", "async"]
|
||||||
categories = [
|
categories = [
|
||||||
"network-programming",
|
"network-programming",
|
||||||
"asynchronous",
|
"asynchronous",
|
||||||
"web-programming::http-server",
|
"web-programming::http-server",
|
||||||
"web-programming::websocket",
|
"web-programming::websocket"
|
||||||
]
|
]
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
repository = "https://github.com/actix/actix-web"
|
repository = "https://github.com/actix/actix-web"
|
||||||
|
@ -118,24 +121,26 @@ __tls = []
|
||||||
experimental-io-uring = ["actix-server/io-uring"]
|
experimental-io-uring = ["actix-server/io-uring"]
|
||||||
|
|
||||||
# Feature group which, when disabled, helps migrate code to v5.0.
|
# Feature group which, when disabled, helps migrate code to v5.0.
|
||||||
compat = ["compat-routing-macros-force-pub"]
|
compat = [
|
||||||
|
"compat-routing-macros-force-pub",
|
||||||
|
]
|
||||||
|
|
||||||
# Opt-out forwards-compatibility for handler visibility inheritance fix.
|
# Opt-out forwards-compatibility for handler visibility inheritance fix.
|
||||||
compat-routing-macros-force-pub = ["actix-web-codegen?/compat-routing-macros-force-pub"]
|
compat-routing-macros-force-pub = ["actix-web-codegen?/compat-routing-macros-force-pub"]
|
||||||
|
|
||||||
# Enabling the retrieval of metadata for initialized resources, including path and HTTP method.
|
# Enabling the retrieval of metadata for initialized resources, including path and HTTP method.
|
||||||
experimental-introspection = []
|
resources-introspection = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-codec = "0.5"
|
actix-codec = "0.5"
|
||||||
actix-macros = { version = "0.2.3", optional = true }
|
actix-macros = { version = "0.2.3", optional = true }
|
||||||
actix-rt = { version = "2.6", default-features = false }
|
actix-rt = { version = "2.6", default-features = false }
|
||||||
actix-server = "2.6"
|
actix-server = "2"
|
||||||
actix-service = "2"
|
actix-service = "2"
|
||||||
actix-tls = { version = "3.4", default-features = false, optional = true }
|
|
||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
|
actix-tls = { version = "3.4", default-features = false, optional = true }
|
||||||
|
|
||||||
actix-http = { version = "3.11", features = ["ws"] }
|
actix-http = { version = "3.10", features = ["ws"] }
|
||||||
actix-router = { version = "0.5.3", default-features = false, features = ["http"] }
|
actix-router = { version = "0.5.3", default-features = false, features = ["http"] }
|
||||||
actix-web-codegen = { version = "4.3", optional = true, default-features = false }
|
actix-web-codegen = { version = "4.3", optional = true, default-features = false }
|
||||||
|
|
||||||
|
@ -148,8 +153,8 @@ encoding_rs = "0.8"
|
||||||
foldhash = "0.1"
|
foldhash = "0.1"
|
||||||
futures-core = { version = "0.3.17", default-features = false }
|
futures-core = { version = "0.3.17", default-features = false }
|
||||||
futures-util = { version = "0.3.17", default-features = false }
|
futures-util = { version = "0.3.17", default-features = false }
|
||||||
impl-more = "0.1.4"
|
|
||||||
itoa = "1"
|
itoa = "1"
|
||||||
|
impl-more = "0.1.4"
|
||||||
language-tags = "0.3"
|
language-tags = "0.3"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
|
@ -161,18 +166,18 @@ serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
smallvec = "1.6.1"
|
smallvec = "1.6.1"
|
||||||
|
tracing = "0.1.30"
|
||||||
socket2 = "0.5"
|
socket2 = "0.5"
|
||||||
time = { version = "0.3", default-features = false, features = ["formatting"] }
|
time = { version = "0.3", default-features = false, features = ["formatting"] }
|
||||||
tracing = "0.1.30"
|
url = "2.1"
|
||||||
url = "2.5.4"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-files = "0.6"
|
actix-files = "0.6"
|
||||||
actix-test = { version = "0.1", features = ["openssl", "rustls-0_23"] }
|
actix-test = { version = "0.1", features = ["openssl", "rustls-0_23"] }
|
||||||
awc = { version = "3", features = ["openssl"] }
|
awc = { version = "3", features = ["openssl"] }
|
||||||
|
|
||||||
brotli = "8"
|
brotli = "7"
|
||||||
const-str = "0.5" # TODO(MSRV 1.77): update to 0.6
|
const-str = "0.5"
|
||||||
core_affinity = "0.8"
|
core_affinity = "0.8"
|
||||||
criterion = { version = "0.5", features = ["html_reports"] }
|
criterion = { version = "0.5", features = ["html_reports"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
|
@ -185,8 +190,7 @@ serde = { version = "1", features = ["derive"] }
|
||||||
static_assertions = "1"
|
static_assertions = "1"
|
||||||
tls-openssl = { package = "openssl", version = "0.10.55" }
|
tls-openssl = { package = "openssl", version = "0.10.55" }
|
||||||
tls-rustls = { package = "rustls", version = "0.23" }
|
tls-rustls = { package = "rustls", version = "0.23" }
|
||||||
tokio = { version = "1.38.2", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] }
|
||||||
tokio-util = "0.7"
|
|
||||||
zstd = "0.13"
|
zstd = "0.13"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|
|
@ -8,10 +8,10 @@
|
||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-web)
|
[](https://crates.io/crates/actix-web)
|
||||||
[](https://docs.rs/actix-web/4.11.0)
|
[](https://docs.rs/actix-web/4.10.2)
|
||||||

|

|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-web/4.11.0)
|
[](https://deps.rs/crate/actix-web/4.10.2)
|
||||||
<br />
|
<br />
|
||||||
[](https://github.com/actix/actix-web/actions/workflows/ci.yml)
|
[](https://github.com/actix/actix-web/actions/workflows/ci.yml)
|
||||||
[](https://codecov.io/gh/actix/actix-web)
|
[](https://codecov.io/gh/actix/actix-web)
|
||||||
|
|
|
@ -1,206 +0,0 @@
|
||||||
// NOTE: This is a work-in-progress example being used to test the new implementation
|
|
||||||
// of the experimental introspection feature.
|
|
||||||
// `cargo run --features experimental-introspection --example introspection`
|
|
||||||
|
|
||||||
use actix_web::{dev::Service, guard, web, App, HttpResponse, HttpServer, Responder};
|
|
||||||
use serde::Deserialize;
|
|
||||||
// Custom guard that checks if the Content-Type header is present.
|
|
||||||
struct ContentTypeGuard;
|
|
||||||
|
|
||||||
impl guard::Guard for ContentTypeGuard {
|
|
||||||
fn check(&self, req: &guard::GuardContext<'_>) -> bool {
|
|
||||||
req.head()
|
|
||||||
.headers()
|
|
||||||
.contains_key(actix_web::http::header::CONTENT_TYPE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data structure for endpoints that receive JSON.
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct UserInfo {
|
|
||||||
username: String,
|
|
||||||
age: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_web::main]
|
|
||||||
async fn main() -> std::io::Result<()> {
|
|
||||||
let server = HttpServer::new(|| {
|
|
||||||
let app = App::new()
|
|
||||||
.service(
|
|
||||||
web::scope("/api")
|
|
||||||
.service(
|
|
||||||
web::scope("/v1")
|
|
||||||
// GET /api/v1/item/{id}: returns the item id from the path.
|
|
||||||
.service(get_item)
|
|
||||||
// POST /api/v1/info: accepts JSON and returns user info.
|
|
||||||
.service(post_user_info)
|
|
||||||
// /api/v1/guarded: only accessible if Content-Type header is present.
|
|
||||||
.route(
|
|
||||||
"/guarded",
|
|
||||||
web::route().guard(ContentTypeGuard).to(guarded_handler),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
// API scope /api/v2: additional endpoint.
|
|
||||||
.service(web::scope("/v2").route("/hello", web::get().to(hello_v2))),
|
|
||||||
)
|
|
||||||
// Scope /v1 outside /api: exposes only GET /v1/item/{id}.
|
|
||||||
.service(web::scope("/v1").service(get_item))
|
|
||||||
// Scope /admin: admin endpoints with different HTTP methods.
|
|
||||||
.service(
|
|
||||||
web::scope("/admin")
|
|
||||||
.route("/dashboard", web::get().to(admin_dashboard))
|
|
||||||
// Single route handling multiple methods using separate handlers.
|
|
||||||
.service(
|
|
||||||
web::resource("/settings")
|
|
||||||
.route(web::get().to(get_settings))
|
|
||||||
.route(web::post().to(update_settings)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
// Root resource: supports GET and POST on "/".
|
|
||||||
.service(
|
|
||||||
web::resource("/")
|
|
||||||
.route(web::get().to(root_index))
|
|
||||||
.route(web::post().to(root_index)),
|
|
||||||
)
|
|
||||||
// Additional endpoints configured in a separate function.
|
|
||||||
.configure(extra_endpoints)
|
|
||||||
// Endpoint that rejects GET on /not_guard (allows other methods).
|
|
||||||
.route(
|
|
||||||
"/not_guard",
|
|
||||||
web::route()
|
|
||||||
.guard(guard::Not(guard::Get()))
|
|
||||||
.to(HttpResponse::MethodNotAllowed),
|
|
||||||
)
|
|
||||||
// Endpoint that requires GET, content-type: plain/text header, and/or POST on /all_guard.
|
|
||||||
.route(
|
|
||||||
"/all_guard",
|
|
||||||
web::route()
|
|
||||||
.guard(
|
|
||||||
guard::All(guard::Get())
|
|
||||||
.and(guard::Header("content-type", "plain/text"))
|
|
||||||
.and(guard::Any(guard::Post())),
|
|
||||||
)
|
|
||||||
.to(HttpResponse::MethodNotAllowed),
|
|
||||||
);
|
|
||||||
|
|
||||||
/*#[cfg(feature = "experimental-introspection")]
|
|
||||||
{
|
|
||||||
actix_web::introspection::introspect();
|
|
||||||
}*/
|
|
||||||
// TODO: Enable introspection without the feature flag.
|
|
||||||
app
|
|
||||||
})
|
|
||||||
.workers(5)
|
|
||||||
.bind("127.0.0.1:8080")?;
|
|
||||||
|
|
||||||
server.run().await
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/v1/item/{id} and GET /v1/item/{id}
|
|
||||||
// Returns a message with the provided id.
|
|
||||||
#[actix_web::get("/item/{id:\\d+}")]
|
|
||||||
async fn get_item(path: web::Path<u32>) -> impl Responder {
|
|
||||||
let id = path.into_inner();
|
|
||||||
HttpResponse::Ok().body(format!("Requested item with id: {}", id))
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /api/v1/info
|
|
||||||
// Expects JSON and responds with the received user info.
|
|
||||||
#[actix_web::post("/info")]
|
|
||||||
async fn post_user_info(info: web::Json<UserInfo>) -> impl Responder {
|
|
||||||
HttpResponse::Ok().json(format!(
|
|
||||||
"User {} with age {} received",
|
|
||||||
info.username, info.age
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/v1/guarded
|
|
||||||
// Uses a custom guard that requires the Content-Type header.
|
|
||||||
async fn guarded_handler() -> impl Responder {
|
|
||||||
HttpResponse::Ok().body("Passed the Content-Type guard!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/v2/hello
|
|
||||||
// Simple greeting endpoint.
|
|
||||||
async fn hello_v2() -> impl Responder {
|
|
||||||
HttpResponse::Ok().body("Hello from API v2!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /admin/dashboard
|
|
||||||
// Returns a message for the admin dashboard.
|
|
||||||
async fn admin_dashboard() -> impl Responder {
|
|
||||||
HttpResponse::Ok().body("Welcome to the Admin Dashboard!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /admin/settings
|
|
||||||
// Returns the current admin settings.
|
|
||||||
async fn get_settings() -> impl Responder {
|
|
||||||
HttpResponse::Ok().body("Current settings: ...")
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /admin/settings
|
|
||||||
// Updates the admin settings.
|
|
||||||
async fn update_settings() -> impl Responder {
|
|
||||||
HttpResponse::Ok().body("Settings have been updated!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET and POST on /
|
|
||||||
// Generic root endpoint.
|
|
||||||
async fn root_index() -> impl Responder {
|
|
||||||
HttpResponse::Ok().body("Welcome to the Root Endpoint!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional endpoints configured in a separate function.
|
|
||||||
fn extra_endpoints(cfg: &mut web::ServiceConfig) {
|
|
||||||
cfg.service(
|
|
||||||
web::scope("/extra")
|
|
||||||
// GET /extra/ping: simple ping endpoint.
|
|
||||||
.route(
|
|
||||||
"/ping",
|
|
||||||
web::get().to(|| async { HttpResponse::Ok().body("pong") }),
|
|
||||||
)
|
|
||||||
// /extra/multi: resource that supports GET and POST.
|
|
||||||
.service(
|
|
||||||
web::resource("/multi")
|
|
||||||
.route(
|
|
||||||
web::get().to(|| async {
|
|
||||||
HttpResponse::Ok().body("GET response from /extra/multi")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(web::post().to(|| async {
|
|
||||||
HttpResponse::Ok().body("POST response from /extra/multi")
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
// /extra/{entities_id}/secure: nested scope with GET and POST, prints the received id.
|
|
||||||
.service(
|
|
||||||
web::scope("{entities_id:\\d+}")
|
|
||||||
.service(
|
|
||||||
web::scope("/secure")
|
|
||||||
.route(
|
|
||||||
"",
|
|
||||||
web::get().to(|| async {
|
|
||||||
HttpResponse::Ok().body("GET response from /extra/secure")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"",
|
|
||||||
web::post().to(|| async {
|
|
||||||
HttpResponse::Ok().body("POST response from /extra/secure")
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
// Middleware that prints the id received in the route.
|
|
||||||
.wrap_fn(|req, srv| {
|
|
||||||
println!(
|
|
||||||
"Request to /extra/secure with id: {}",
|
|
||||||
req.match_info().get("entities_id").unwrap()
|
|
||||||
);
|
|
||||||
let fut = srv.call(req);
|
|
||||||
async move {
|
|
||||||
let res = fut.await?;
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -82,6 +82,9 @@ where
|
||||||
|
|
||||||
let (config, services) = config.into_services();
|
let (config, services) = config.into_services();
|
||||||
|
|
||||||
|
#[cfg(feature = "resources-introspection")]
|
||||||
|
let mut rdef_methods: Vec<(String, Vec<String>)> = Vec::new();
|
||||||
|
|
||||||
// complete pipeline creation.
|
// complete pipeline creation.
|
||||||
*self.factory_ref.borrow_mut() = Some(AppRoutingFactory {
|
*self.factory_ref.borrow_mut() = Some(AppRoutingFactory {
|
||||||
default,
|
default,
|
||||||
|
@ -89,35 +92,24 @@ where
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(mut rdef, srv, guards, nested)| {
|
.map(|(mut rdef, srv, guards, nested)| {
|
||||||
rmap.add(&mut rdef, nested);
|
rmap.add(&mut rdef, nested);
|
||||||
#[cfg(feature = "experimental-introspection")]
|
|
||||||
|
#[cfg(feature = "resources-introspection")]
|
||||||
{
|
{
|
||||||
use std::borrow::Borrow;
|
let http_methods: Vec<String> =
|
||||||
let pat = rdef.pattern().unwrap_or("").to_string();
|
guards.as_ref().map_or_else(Vec::new, |g| {
|
||||||
let mut methods = Vec::new();
|
g.iter()
|
||||||
let mut guard_names = Vec::new();
|
.flat_map(|g| {
|
||||||
if let Some(gs) = guards.borrow().as_ref() {
|
crate::guard::HttpMethodsExtractor::extract_http_methods(
|
||||||
for g in gs.iter() {
|
&**g,
|
||||||
let name = g.name().to_string();
|
)
|
||||||
if !guard_names.contains(&name) {
|
})
|
||||||
guard_names.push(name.clone());
|
.collect::<Vec<_>>()
|
||||||
}
|
});
|
||||||
if let Some(details) = g.details() {
|
|
||||||
for d in details {
|
rdef_methods
|
||||||
if let crate::guard::GuardDetail::HttpMethods(v) = d {
|
.push((rdef.pattern().unwrap_or_default().to_string(), http_methods));
|
||||||
for s in v {
|
|
||||||
if let Ok(m) = s.parse() {
|
|
||||||
if !methods.contains(&m) {
|
|
||||||
methods.push(m);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
crate::introspection::register_pattern_detail(pat, methods, guard_names);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(rdef, srv, RefCell::new(guards))
|
(rdef, srv, RefCell::new(guards))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
@ -134,6 +126,11 @@ where
|
||||||
let rmap = Rc::new(rmap);
|
let rmap = Rc::new(rmap);
|
||||||
ResourceMap::finish(&rmap);
|
ResourceMap::finish(&rmap);
|
||||||
|
|
||||||
|
#[cfg(feature = "resources-introspection")]
|
||||||
|
{
|
||||||
|
crate::introspection::process_introspection(Rc::clone(&rmap), rdef_methods);
|
||||||
|
}
|
||||||
|
|
||||||
// construct all async data factory futures
|
// construct all async data factory futures
|
||||||
let factory_futs = join_all(self.async_data_factories.iter().map(|f| f()));
|
let factory_futs = join_all(self.async_data_factories.iter().map(|f| f()));
|
||||||
|
|
||||||
|
@ -159,11 +156,6 @@ where
|
||||||
factory.create(&mut app_data);
|
factory.create(&mut app_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "experimental-introspection")]
|
|
||||||
{
|
|
||||||
crate::introspection::register_rmap(&rmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(AppInitService {
|
Ok(AppInitService {
|
||||||
service,
|
service,
|
||||||
app_data: Rc::new(app_data),
|
app_data: Rc::new(app_data),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use actix_http::{header, uri::Uri, RequestHead, Version};
|
use actix_http::{header, uri::Uri, RequestHead};
|
||||||
|
|
||||||
use super::{Guard, GuardContext};
|
use super::{Guard, GuardContext};
|
||||||
|
|
||||||
|
@ -66,7 +66,6 @@ fn get_host_uri(req: &RequestHead) -> Option<Uri> {
|
||||||
req.headers
|
req.headers
|
||||||
.get(header::HOST)
|
.get(header::HOST)
|
||||||
.and_then(|host_value| host_value.to_str().ok())
|
.and_then(|host_value| host_value.to_str().ok())
|
||||||
.filter(|_| req.version < Version::HTTP_2)
|
|
||||||
.or_else(|| req.uri.host())
|
.or_else(|| req.uri.host())
|
||||||
.and_then(|host| host.parse().ok())
|
.and_then(|host| host.parse().ok())
|
||||||
}
|
}
|
||||||
|
@ -124,38 +123,6 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::test::TestRequest;
|
use crate::test::TestRequest;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn host_not_from_header_if_http2() {
|
|
||||||
let req = TestRequest::default()
|
|
||||||
.uri("www.rust-lang.org")
|
|
||||||
.insert_header((
|
|
||||||
header::HOST,
|
|
||||||
header::HeaderValue::from_static("www.example.com"),
|
|
||||||
))
|
|
||||||
.to_srv_request();
|
|
||||||
|
|
||||||
let host = Host("www.example.com");
|
|
||||||
assert!(host.check(&req.guard_ctx()));
|
|
||||||
|
|
||||||
let host = Host("www.rust-lang.org");
|
|
||||||
assert!(!host.check(&req.guard_ctx()));
|
|
||||||
|
|
||||||
let req = TestRequest::default()
|
|
||||||
.version(actix_http::Version::HTTP_2)
|
|
||||||
.uri("www.rust-lang.org")
|
|
||||||
.insert_header((
|
|
||||||
header::HOST,
|
|
||||||
header::HeaderValue::from_static("www.example.com"),
|
|
||||||
))
|
|
||||||
.to_srv_request();
|
|
||||||
|
|
||||||
let host = Host("www.example.com");
|
|
||||||
assert!(!host.check(&req.guard_ctx()));
|
|
||||||
|
|
||||||
let host = Host("www.rust-lang.org");
|
|
||||||
assert!(host.check(&req.guard_ctx()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn host_from_header() {
|
fn host_from_header() {
|
||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
|
|
|
@ -397,6 +397,35 @@ impl Guard for MethodGuard {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "resources-introspection")]
|
||||||
|
pub trait HttpMethodsExtractor {
|
||||||
|
fn extract_http_methods(&self) -> Vec<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "resources-introspection")]
|
||||||
|
impl HttpMethodsExtractor for dyn Guard {
|
||||||
|
fn extract_http_methods(&self) -> Vec<String> {
|
||||||
|
let methods: Vec<String> = self
|
||||||
|
.details()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|detail| {
|
||||||
|
if let GuardDetail::HttpMethods(methods) = detail {
|
||||||
|
methods.clone()
|
||||||
|
} else {
|
||||||
|
vec!["UNKNOWN".to_string()]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if methods.is_empty() {
|
||||||
|
vec!["UNKNOWN".to_string()]
|
||||||
|
} else {
|
||||||
|
methods
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! method_guard {
|
macro_rules! method_guard {
|
||||||
($method_fn:ident, $method_const:ident) => {
|
($method_fn:ident, $method_const:ident) => {
|
||||||
#[doc = concat!("Creates a guard that matches the `", stringify!($method_const), "` request method.")]
|
#[doc = concat!("Creates a guard that matches the `", stringify!($method_const), "` request method.")]
|
||||||
|
|
|
@ -70,7 +70,7 @@ use crate::{
|
||||||
/// This is the source code for the 2-parameter implementation of `Handler` to help illustrate the
|
/// This is the source code for the 2-parameter implementation of `Handler` to help illustrate the
|
||||||
/// bounds of the handler call after argument extraction:
|
/// bounds of the handler call after argument extraction:
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// impl<Func, Fut, Arg1, Arg2> Handler<(Arg1, Arg2)> for Func
|
/// impl<Func, Arg1, Arg2, Fut> Handler<(Arg1, Arg2)> for Func
|
||||||
/// where
|
/// where
|
||||||
/// Func: Fn(Arg1, Arg2) -> Fut + Clone + 'static,
|
/// Func: Fn(Arg1, Arg2) -> Fut + Clone + 'static,
|
||||||
/// Fut: Future,
|
/// Fut: Future,
|
||||||
|
|
|
@ -158,7 +158,7 @@ impl ConnectionInfo {
|
||||||
/// The address is resolved through the following, in order:
|
/// The address is resolved through the following, in order:
|
||||||
/// - `Forwarded` header
|
/// - `Forwarded` header
|
||||||
/// - `X-Forwarded-For` header
|
/// - `X-Forwarded-For` header
|
||||||
/// - peer address of opened socket (same as [`peer_addr`](Self::peer_addr))
|
/// - peer address of opened socket (same as [`remote_addr`](Self::remote_addr))
|
||||||
///
|
///
|
||||||
/// # Security
|
/// # Security
|
||||||
/// Do not use this function for security purposes unless you can be sure that the `Forwarded`
|
/// Do not use this function for security purposes unless you can be sure that the `Forwarded`
|
||||||
|
|
|
@ -1,172 +1,494 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
rc::Rc,
|
||||||
sync::{
|
sync::{OnceLock, RwLock},
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
Mutex, OnceLock,
|
|
||||||
},
|
|
||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{http::Method, rmap::ResourceMap};
|
use crate::rmap::ResourceMap;
|
||||||
|
|
||||||
static REGISTRY: OnceLock<Mutex<IntrospectionNode>> = OnceLock::new();
|
/// Represents an HTTP resource registered for introspection.
|
||||||
static DETAIL_REGISTRY: OnceLock<Mutex<HashMap<String, RouteDetail>>> = OnceLock::new();
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ResourceIntrospection {
|
||||||
|
/// HTTP method (e.g., "GET").
|
||||||
|
pub method: String,
|
||||||
|
/// Route path (e.g., "/api/v1/test").
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A global registry of listed resources for introspection.
|
||||||
|
/// Only the designated thread can modify it.
|
||||||
|
static RESOURCE_REGISTRY: RwLock<Vec<ResourceIntrospection>> = RwLock::new(Vec::new());
|
||||||
|
|
||||||
|
/// Stores the thread ID of the designated thread (the first to call `process_introspection`).
|
||||||
|
/// Any other thread will immediately return without updating the global registry.
|
||||||
static DESIGNATED_THREAD: OnceLock<thread::ThreadId> = OnceLock::new();
|
static DESIGNATED_THREAD: OnceLock<thread::ThreadId> = OnceLock::new();
|
||||||
static IS_INITIALIZED: AtomicBool = AtomicBool::new(false);
|
|
||||||
|
|
||||||
pub fn initialize_registry() {
|
/// Inserts a resource into the global registry, avoiding duplicates.
|
||||||
REGISTRY.get_or_init(|| Mutex::new(IntrospectionNode::new(ResourceType::App, "".into())));
|
pub fn register_resource(resource: ResourceIntrospection) {
|
||||||
}
|
let mut global = RESOURCE_REGISTRY.write().unwrap();
|
||||||
|
if !global.iter().any(|r| r == &resource) {
|
||||||
pub fn get_registry() -> &'static Mutex<IntrospectionNode> {
|
global.push(resource);
|
||||||
REGISTRY.get().expect("Registry not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn initialize_detail_registry() {
|
|
||||||
DETAIL_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_detail_registry() -> &'static Mutex<HashMap<String, RouteDetail>> {
|
|
||||||
DETAIL_REGISTRY
|
|
||||||
.get()
|
|
||||||
.expect("Detail registry not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct RouteDetail {
|
|
||||||
methods: Vec<Method>,
|
|
||||||
guards: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum ResourceType {
|
|
||||||
App,
|
|
||||||
Scope,
|
|
||||||
Resource,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct IntrospectionNode {
|
|
||||||
pub kind: ResourceType,
|
|
||||||
pub pattern: String,
|
|
||||||
pub methods: Vec<Method>,
|
|
||||||
pub guards: Vec<String>,
|
|
||||||
pub children: Vec<IntrospectionNode>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntrospectionNode {
|
|
||||||
pub fn new(kind: ResourceType, pattern: String) -> Self {
|
|
||||||
IntrospectionNode {
|
|
||||||
kind,
|
|
||||||
pattern,
|
|
||||||
methods: Vec::new(),
|
|
||||||
guards: Vec::new(),
|
|
||||||
children: Vec::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn display(&self, indent: usize, parent_path: &str) {
|
/// Completes (updates) partial routes in the global registry whose path contains `marker`,
|
||||||
let full_path = if parent_path.is_empty() {
|
/// by applying the specified `prefix`.
|
||||||
self.pattern.clone()
|
pub fn complete_partial_routes_with_marker(marker: &str, prefix: &str) {
|
||||||
|
let mut global = RESOURCE_REGISTRY.write().unwrap();
|
||||||
|
|
||||||
|
let mut updated = Vec::new();
|
||||||
|
let mut remaining = Vec::new();
|
||||||
|
|
||||||
|
// Move all items out of the current registry.
|
||||||
|
for resource in global.drain(..) {
|
||||||
|
if resource.path.contains(marker) {
|
||||||
|
// Build the full path by applying the prefix if needed.
|
||||||
|
let full_path = if prefix.is_empty() {
|
||||||
|
resource.path.clone()
|
||||||
|
} else if prefix.ends_with('/') || resource.path.starts_with('/') {
|
||||||
|
format!("{}{}", prefix, resource.path)
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!("{}/{}", prefix, resource.path)
|
||||||
"{}/{}",
|
|
||||||
parent_path.trim_end_matches('/'),
|
|
||||||
self.pattern.trim_start_matches('/')
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if !self.methods.is_empty() || !self.guards.is_empty() {
|
let completed = ResourceIntrospection {
|
||||||
let methods = if self.methods.is_empty() {
|
method: resource.method,
|
||||||
"".to_string()
|
path: full_path,
|
||||||
} else {
|
|
||||||
format!(" Methods: {:?}", self.methods)
|
|
||||||
};
|
|
||||||
let guards = if self.guards.is_empty() {
|
|
||||||
"".to_string()
|
|
||||||
} else {
|
|
||||||
format!(" Guards: {:?}", self.guards)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("{}{}{}{}", " ".repeat(indent), full_path, methods, guards);
|
// Add to `updated` if it's not already in there.
|
||||||
|
if !updated.iter().any(|r| r == &completed) {
|
||||||
|
updated.push(completed);
|
||||||
}
|
}
|
||||||
|
|
||||||
for child in &self.children {
|
|
||||||
child.display(indent, &full_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_tree(node: &mut IntrospectionNode, rmap: &ResourceMap) {
|
|
||||||
initialize_detail_registry();
|
|
||||||
let detail_registry = get_detail_registry();
|
|
||||||
if let Some(ref children) = rmap.nodes {
|
|
||||||
for child_rc in children {
|
|
||||||
let child = child_rc;
|
|
||||||
let pat = child.pattern.pattern().unwrap_or("").to_string();
|
|
||||||
let kind = if child.nodes.is_some() {
|
|
||||||
ResourceType::Scope
|
|
||||||
} else {
|
} else {
|
||||||
ResourceType::Resource
|
// Keep this resource as-is.
|
||||||
};
|
remaining.push(resource);
|
||||||
let mut new_node = IntrospectionNode::new(kind, pat.clone());
|
|
||||||
|
|
||||||
if let ResourceType::Resource = new_node.kind {
|
|
||||||
if let Some(d) = detail_registry.lock().unwrap().get(&pat) {
|
|
||||||
new_node.methods = d.methods.clone();
|
|
||||||
new_node.guards = d.guards.clone();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
build_tree(&mut new_node, child);
|
// Merge updated items back with the remaining ones.
|
||||||
node.children.push(new_node);
|
remaining.extend(updated);
|
||||||
}
|
*global = remaining;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_designated_thread() -> bool {
|
/// Returns a **copy** of the global registry (safe to call from any thread).
|
||||||
|
pub fn get_registered_resources() -> Vec<ResourceIntrospection> {
|
||||||
|
RESOURCE_REGISTRY.read().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes introspection data for routes and methods.
|
||||||
|
/// Only the **first thread** that calls this function (the "designated" one) may update
|
||||||
|
/// the global resource registry. Any other thread will immediately return without updating it.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `rmap`: A resource map convertible to a vector of route strings.
|
||||||
|
/// - `rdef_methods`: A vector of `(sub_path, [methods])`.
|
||||||
|
/// - A tuple with an **empty** methods vector is treated as a "marker" (a partial route)
|
||||||
|
/// for which we try to deduce a prefix by finding `sub_path` in a route, then calling
|
||||||
|
/// `complete_partial_routes_with_marker`.
|
||||||
|
/// - A tuple with one or more methods registers a resource with `register_resource`.
|
||||||
|
pub fn process_introspection(rmap: Rc<ResourceMap>, rdef_methods: Vec<(String, Vec<String>)>) {
|
||||||
|
// Determine the designated thread: if none is set yet, assign the current thread's ID.
|
||||||
|
// This ensures that the first thread to call this function becomes the designated thread.
|
||||||
let current_id = thread::current().id();
|
let current_id = thread::current().id();
|
||||||
DESIGNATED_THREAD.get_or_init(|| {
|
DESIGNATED_THREAD.get_or_init(|| current_id);
|
||||||
IS_INITIALIZED.store(true, Ordering::SeqCst);
|
|
||||||
current_id // Assign the first thread that calls this function
|
// If the current thread is not the designated one, return immediately.
|
||||||
|
// This ensures that only the designated thread updates the global registry,
|
||||||
|
// avoiding any interleaving or inconsistent updates from other threads.
|
||||||
|
if *DESIGNATED_THREAD.get().unwrap() != current_id {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rmap_vec = rmap.to_vec();
|
||||||
|
|
||||||
|
// If there is no data, nothing to process.
|
||||||
|
// Avoid unnecessary work.
|
||||||
|
if rmap_vec.is_empty() && rdef_methods.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep track of the deduced prefix for partial routes.
|
||||||
|
let mut deduced_prefix: Option<String> = None;
|
||||||
|
|
||||||
|
// 1. Handle "marker" entries (where methods is empty).
|
||||||
|
for (sub_path, http_methods) in &rdef_methods {
|
||||||
|
if http_methods.is_empty() {
|
||||||
|
// Find any route that contains sub_path and use it to deduce a prefix.
|
||||||
|
if let Some(route) = rmap_vec.iter().find(|r| r.contains(sub_path)) {
|
||||||
|
if let Some(pos) = route.find(sub_path) {
|
||||||
|
let prefix = &route[..pos];
|
||||||
|
deduced_prefix = Some(prefix.to_string());
|
||||||
|
// Complete partial routes in the global registry using this prefix.
|
||||||
|
complete_partial_routes_with_marker(sub_path, prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Handle endpoint entries (where methods is non-empty).
|
||||||
|
for (sub_path, http_methods) in &rdef_methods {
|
||||||
|
if !http_methods.is_empty() {
|
||||||
|
// Identify candidate routes that end with sub_path (or exactly match "/" if sub_path == "/").
|
||||||
|
let candidates: Vec<&String> = if sub_path == "/" {
|
||||||
|
rmap_vec.iter().filter(|r| r.as_str() == "/").collect()
|
||||||
|
} else {
|
||||||
|
rmap_vec.iter().filter(|r| r.ends_with(sub_path)).collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
// If we found any candidates, pick the best match.
|
||||||
|
if !candidates.is_empty() {
|
||||||
|
let chosen = if let Some(prefix) = &deduced_prefix {
|
||||||
|
if !prefix.is_empty() {
|
||||||
|
candidates
|
||||||
|
.iter()
|
||||||
|
.find(|&&r| r.starts_with(prefix))
|
||||||
|
.cloned()
|
||||||
|
.or_else(|| candidates.iter().min_by_key(|&&r| r.len()).cloned())
|
||||||
|
} else {
|
||||||
|
candidates.iter().min_by_key(|&&r| r.len()).cloned()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
candidates.iter().min_by_key(|&&r| r.len()).cloned()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(full_route) = chosen {
|
||||||
|
// Register the endpoint in the global resource registry.
|
||||||
|
register_resource(ResourceIntrospection {
|
||||||
|
method: http_methods.join(","),
|
||||||
|
path: full_route.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{num::NonZeroUsize, rc::Rc};
|
||||||
|
|
||||||
|
use actix_router::ResourceDef;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::rmap::ResourceMap;
|
||||||
|
|
||||||
|
/// Helper function to create a ResourceMap from a list of route strings.
|
||||||
|
/// It creates a root ResourceMap with an empty prefix and adds each route as a leaf.
|
||||||
|
fn create_resource_map(routes: Vec<&str>) -> Rc<ResourceMap> {
|
||||||
|
// Create a root node with an empty prefix.
|
||||||
|
let mut root = ResourceMap::new(ResourceDef::root_prefix(""));
|
||||||
|
// For each route, create a ResourceDef and add it as a leaf (nested = None).
|
||||||
|
for route in routes {
|
||||||
|
let mut def = ResourceDef::new(route);
|
||||||
|
root.add(&mut def, None);
|
||||||
|
}
|
||||||
|
Rc::new(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to run the full introspection flow.
|
||||||
|
// It processes introspection data for multiple blocks, each with a different set of routes and methods.
|
||||||
|
fn run_full_introspection_flow() {
|
||||||
|
// Block 1:
|
||||||
|
// rmap_vec: ["/item/{id}"]
|
||||||
|
// rdef_methods: []
|
||||||
|
process_introspection(create_resource_map(vec!["/item/{id}"]), vec![]);
|
||||||
|
|
||||||
|
// Block 2:
|
||||||
|
// rmap_vec: ["/info"]
|
||||||
|
// rdef_methods: []
|
||||||
|
process_introspection(create_resource_map(vec!["/info"]), vec![]);
|
||||||
|
|
||||||
|
// Block 3:
|
||||||
|
// rmap_vec: ["/guarded"]
|
||||||
|
// rdef_methods: []
|
||||||
|
process_introspection(create_resource_map(vec!["/guarded"]), vec![]);
|
||||||
|
|
||||||
|
// Block 4:
|
||||||
|
// rmap_vec: ["/v1/item/{id}", "/v1/info", "/v1/guarded"]
|
||||||
|
// rdef_methods: [("/item/{id}", ["GET"]), ("/info", ["POST"]), ("/guarded", ["UNKNOWN"])]
|
||||||
|
process_introspection(
|
||||||
|
create_resource_map(vec!["/v1/item/{id}", "/v1/info", "/v1/guarded"]),
|
||||||
|
vec![
|
||||||
|
("/item/{id}".to_string(), vec!["GET".to_string()]),
|
||||||
|
("/info".to_string(), vec!["POST".to_string()]),
|
||||||
|
("/guarded".to_string(), vec!["UNKNOWN".to_string()]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Block 5:
|
||||||
|
// rmap_vec: ["/hello"]
|
||||||
|
// rdef_methods: []
|
||||||
|
process_introspection(create_resource_map(vec!["/hello"]), vec![]);
|
||||||
|
|
||||||
|
// Block 6:
|
||||||
|
// rmap_vec: ["/v2/hello"]
|
||||||
|
// rdef_methods: [("/hello", ["GET"])]
|
||||||
|
process_introspection(
|
||||||
|
create_resource_map(vec!["/v2/hello"]),
|
||||||
|
vec![("/hello".to_string(), vec!["GET".to_string()])],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Block 7:
|
||||||
|
// rmap_vec: ["/api/v1/item/{id}", "/api/v1/info", "/api/v1/guarded", "/api/v2/hello"]
|
||||||
|
// rdef_methods: [("/v1", []), ("/v2", [])]
|
||||||
|
process_introspection(
|
||||||
|
create_resource_map(vec![
|
||||||
|
"/api/v1/item/{id}",
|
||||||
|
"/api/v1/info",
|
||||||
|
"/api/v1/guarded",
|
||||||
|
"/api/v2/hello",
|
||||||
|
]),
|
||||||
|
vec![("/v1".to_string(), vec![]), ("/v2".to_string(), vec![])],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Block 8:
|
||||||
|
// rmap_vec: ["/dashboard"]
|
||||||
|
// rdef_methods: []
|
||||||
|
process_introspection(create_resource_map(vec!["/dashboard"]), vec![]);
|
||||||
|
|
||||||
|
// Block 9:
|
||||||
|
// rmap_vec: ["/settings"]
|
||||||
|
// rdef_methods: [("/settings", ["GET"]), ("/settings", ["POST"])]
|
||||||
|
process_introspection(
|
||||||
|
create_resource_map(vec!["/settings"]),
|
||||||
|
vec![
|
||||||
|
("/settings".to_string(), vec!["GET".to_string()]),
|
||||||
|
("/settings".to_string(), vec!["POST".to_string()]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Block 10:
|
||||||
|
// rmap_vec: ["/admin/dashboard", "/admin/settings"]
|
||||||
|
// rdef_methods: [("/dashboard", ["GET"]), ("/settings", [])]
|
||||||
|
process_introspection(
|
||||||
|
create_resource_map(vec!["/admin/dashboard", "/admin/settings"]),
|
||||||
|
vec![
|
||||||
|
("/dashboard".to_string(), vec!["GET".to_string()]),
|
||||||
|
("/settings".to_string(), vec![]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Block 11:
|
||||||
|
// rmap_vec: ["/"]
|
||||||
|
// rdef_methods: [("/", ["GET"]), ("/", ["POST"])]
|
||||||
|
process_introspection(
|
||||||
|
create_resource_map(vec!["/"]),
|
||||||
|
vec![
|
||||||
|
("/".to_string(), vec!["GET".to_string()]),
|
||||||
|
("/".to_string(), vec!["POST".to_string()]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Block 12:
|
||||||
|
// rmap_vec: ["/ping"]
|
||||||
|
// rdef_methods: []
|
||||||
|
process_introspection(create_resource_map(vec!["/ping"]), vec![]);
|
||||||
|
|
||||||
|
// Block 13:
|
||||||
|
// rmap_vec: ["/multi"]
|
||||||
|
// rdef_methods: [("/multi", ["GET"]), ("/multi", ["POST"])]
|
||||||
|
process_introspection(
|
||||||
|
create_resource_map(vec!["/multi"]),
|
||||||
|
vec![
|
||||||
|
("/multi".to_string(), vec!["GET".to_string()]),
|
||||||
|
("/multi".to_string(), vec!["POST".to_string()]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Block 14:
|
||||||
|
// rmap_vec: ["/extra/ping", "/extra/multi"]
|
||||||
|
// rdef_methods: [("/ping", ["GET"]), ("/multi", [])]
|
||||||
|
process_introspection(
|
||||||
|
create_resource_map(vec!["/extra/ping", "/extra/multi"]),
|
||||||
|
vec![
|
||||||
|
("/ping".to_string(), vec!["GET".to_string()]),
|
||||||
|
("/multi".to_string(), vec![]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Block 15:
|
||||||
|
// rmap_vec: ["/other_guard"]
|
||||||
|
// rdef_methods: []
|
||||||
|
process_introspection(create_resource_map(vec!["/other_guard"]), vec![]);
|
||||||
|
|
||||||
|
// Block 16:
|
||||||
|
// rmap_vec: ["/all_guard"]
|
||||||
|
// rdef_methods: []
|
||||||
|
process_introspection(create_resource_map(vec!["/all_guard"]), vec![]);
|
||||||
|
|
||||||
|
// Block 17:
|
||||||
|
// rmap_vec: ["/api/v1/item/{id}", "/api/v1/info", "/api/v1/guarded", "/api/v2/hello",
|
||||||
|
// "/admin/dashboard", "/admin/settings", "/", "/extra/ping", "/extra/multi",
|
||||||
|
// "/other_guard", "/all_guard"]
|
||||||
|
// rdef_methods: [("/api", []), ("/admin", []), ("/", []), ("/extra", []),
|
||||||
|
// ("/other_guard", ["UNKNOWN"]), ("/all_guard", ["GET", "UNKNOWN", "POST"])]
|
||||||
|
process_introspection(
|
||||||
|
create_resource_map(vec![
|
||||||
|
"/api/v1/item/{id}",
|
||||||
|
"/api/v1/info",
|
||||||
|
"/api/v1/guarded",
|
||||||
|
"/api/v2/hello",
|
||||||
|
"/admin/dashboard",
|
||||||
|
"/admin/settings",
|
||||||
|
"/",
|
||||||
|
"/extra/ping",
|
||||||
|
"/extra/multi",
|
||||||
|
"/other_guard",
|
||||||
|
"/all_guard",
|
||||||
|
]),
|
||||||
|
vec![
|
||||||
|
("/api".to_string(), vec![]),
|
||||||
|
("/admin".to_string(), vec![]),
|
||||||
|
("/".to_string(), vec![]),
|
||||||
|
("/extra".to_string(), vec![]),
|
||||||
|
("/other_guard".to_string(), vec!["UNKNOWN".to_string()]),
|
||||||
|
(
|
||||||
|
"/all_guard".to_string(),
|
||||||
|
vec!["GET".to_string(), "UNKNOWN".to_string(), "POST".to_string()],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This test spawns multiple tasks that run the full introspection flow concurrently.
|
||||||
|
/// Only the designated task (the first one to call process_introspection) updates the global registry,
|
||||||
|
/// ensuring that the internal order remains consistent. Finally, we verify that get_registered_resources()
|
||||||
|
/// returns the expected set of listed resources.
|
||||||
|
/// Using a dedicated arbiter for each task ensures that the global registry is thread-safe.
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_introspection() {
|
||||||
|
// Number of tasks to spawn.
|
||||||
|
const NUM_TASKS: usize = 4;
|
||||||
|
let mut completion_receivers = Vec::with_capacity(NUM_TASKS);
|
||||||
|
|
||||||
|
// Check that the registry is initially empty.
|
||||||
|
let registered_resources = get_registered_resources();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
registered_resources.len(),
|
||||||
|
0,
|
||||||
|
"Expected 0 registered resources, found: {:?}",
|
||||||
|
registered_resources
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine parallelism and max blocking threads.
|
||||||
|
let parallelism = std::thread::available_parallelism().map_or(2, NonZeroUsize::get);
|
||||||
|
let max_blocking_threads = std::cmp::max(512 / parallelism, 1);
|
||||||
|
|
||||||
|
// Spawn tasks on the arbiter. Each task runs the full introspection flow and then signals completion.
|
||||||
|
for _ in 0..NUM_TASKS {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "linux", feature = "experimental-io-uring"))]
|
||||||
|
let arbiter = {
|
||||||
|
// TODO: pass max blocking thread config when tokio-uring enable configuration
|
||||||
|
// on building runtime.
|
||||||
|
let _ = max_blocking_threads;
|
||||||
|
actix_rt::Arbiter::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(all(target_os = "linux", feature = "experimental-io-uring")))]
|
||||||
|
let arbiter = actix_rt::Arbiter::with_tokio_rt(move || {
|
||||||
|
// Create an Arbiter with a dedicated Tokio runtime.
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.max_blocking_threads(max_blocking_threads)
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
});
|
});
|
||||||
|
|
||||||
*DESIGNATED_THREAD.get().unwrap() == current_id
|
// Spawn the task on the arbiter.
|
||||||
|
arbiter.spawn(async move {
|
||||||
|
run_full_introspection_flow();
|
||||||
|
// Signal that this task has finished.
|
||||||
|
let _ = tx.send(());
|
||||||
|
});
|
||||||
|
completion_receivers.push(rx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_rmap(rmap: &ResourceMap) {
|
// Wait for all spawned tasks to complete.
|
||||||
if !is_designated_thread() {
|
for rx in completion_receivers {
|
||||||
return;
|
let _ = rx.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize_registry();
|
// After all blocks, we expect the final registry to contain 14 entries.
|
||||||
let mut root = IntrospectionNode::new(ResourceType::App, "".into());
|
let registered_resources = get_registered_resources();
|
||||||
build_tree(&mut root, rmap);
|
|
||||||
*get_registry().lock().unwrap() = root;
|
|
||||||
|
|
||||||
// WIP. Display the introspection tree
|
assert_eq!(
|
||||||
let reg = get_registry().lock().unwrap();
|
registered_resources.len(),
|
||||||
reg.display(0, "");
|
14,
|
||||||
}
|
"Expected 14 registered resources, found: {:?}",
|
||||||
|
registered_resources
|
||||||
|
);
|
||||||
|
|
||||||
fn update_unique<T: Clone + PartialEq>(existing: &mut Vec<T>, new_items: &[T]) {
|
// List of expected resources
|
||||||
for item in new_items {
|
let expected_resources = vec![
|
||||||
if !existing.contains(item) {
|
ResourceIntrospection {
|
||||||
existing.push(item.clone());
|
method: "GET".to_string(),
|
||||||
}
|
path: "/api/v1/item/{id}".to_string(),
|
||||||
}
|
},
|
||||||
}
|
ResourceIntrospection {
|
||||||
|
method: "POST".to_string(),
|
||||||
|
path: "/api/v1/info".to_string(),
|
||||||
|
},
|
||||||
|
ResourceIntrospection {
|
||||||
|
method: "UNKNOWN".to_string(),
|
||||||
|
path: "/api/v1/guarded".to_string(),
|
||||||
|
},
|
||||||
|
ResourceIntrospection {
|
||||||
|
method: "GET".to_string(),
|
||||||
|
path: "/api/v2/hello".to_string(),
|
||||||
|
},
|
||||||
|
ResourceIntrospection {
|
||||||
|
method: "GET".to_string(),
|
||||||
|
path: "/admin/settings".to_string(),
|
||||||
|
},
|
||||||
|
ResourceIntrospection {
|
||||||
|
method: "POST".to_string(),
|
||||||
|
path: "/admin/settings".to_string(),
|
||||||
|
},
|
||||||
|
ResourceIntrospection {
|
||||||
|
method: "GET".to_string(),
|
||||||
|
path: "/admin/dashboard".to_string(),
|
||||||
|
},
|
||||||
|
ResourceIntrospection {
|
||||||
|
method: "GET".to_string(),
|
||||||
|
path: "/extra/multi".to_string(),
|
||||||
|
},
|
||||||
|
ResourceIntrospection {
|
||||||
|
method: "POST".to_string(),
|
||||||
|
path: "/extra/multi".to_string(),
|
||||||
|
},
|
||||||
|
ResourceIntrospection {
|
||||||
|
method: "GET".to_string(),
|
||||||
|
path: "/extra/ping".to_string(),
|
||||||
|
},
|
||||||
|
ResourceIntrospection {
|
||||||
|
method: "GET".to_string(),
|
||||||
|
path: "/".to_string(),
|
||||||
|
},
|
||||||
|
ResourceIntrospection {
|
||||||
|
method: "POST".to_string(),
|
||||||
|
path: "/".to_string(),
|
||||||
|
},
|
||||||
|
ResourceIntrospection {
|
||||||
|
method: "UNKNOWN".to_string(),
|
||||||
|
path: "/other_guard".to_string(),
|
||||||
|
},
|
||||||
|
ResourceIntrospection {
|
||||||
|
method: "GET,UNKNOWN,POST".to_string(),
|
||||||
|
path: "/all_guard".to_string(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
pub fn register_pattern_detail(pattern: String, methods: Vec<Method>, guards: Vec<String>) {
|
for exp in expected_resources {
|
||||||
if !is_designated_thread() {
|
assert!(
|
||||||
return;
|
registered_resources.contains(&exp),
|
||||||
|
"Expected resource not found: {:?}",
|
||||||
|
exp
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
initialize_detail_registry();
|
|
||||||
let mut reg = get_detail_registry().lock().unwrap();
|
|
||||||
reg.entry(pattern)
|
|
||||||
.and_modify(|d| {
|
|
||||||
update_unique(&mut d.methods, &methods);
|
|
||||||
update_unique(&mut d.guards, &guards);
|
|
||||||
})
|
|
||||||
.or_insert(RouteDetail { methods, guards });
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ pub use actix_http::{body, HttpMessage};
|
||||||
#[cfg(feature = "cookies")]
|
#[cfg(feature = "cookies")]
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use cookie;
|
pub use cookie;
|
||||||
pub use mime;
|
|
||||||
mod app;
|
mod app;
|
||||||
mod app_service;
|
mod app_service;
|
||||||
mod config;
|
mod config;
|
||||||
|
@ -108,7 +108,7 @@ mod thin_data;
|
||||||
pub(crate) mod types;
|
pub(crate) mod types;
|
||||||
pub mod web;
|
pub mod web;
|
||||||
|
|
||||||
#[cfg(feature = "experimental-introspection")]
|
#[cfg(feature = "resources-introspection")]
|
||||||
pub mod introspection;
|
pub mod introspection;
|
||||||
|
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
|
|
|
@ -191,10 +191,8 @@ where
|
||||||
None => true,
|
None => true,
|
||||||
Some(hdr) => {
|
Some(hdr) => {
|
||||||
match hdr.to_str().ok().and_then(|hdr| hdr.parse::<Mime>().ok()) {
|
match hdr.to_str().ok().and_then(|hdr| hdr.parse::<Mime>().ok()) {
|
||||||
Some(mime) if mime.type_() == mime::IMAGE => {
|
Some(mime) if mime.type_().as_str() == "image" => false,
|
||||||
matches!(mime.subtype(), mime::SVG)
|
Some(mime) if mime.type_().as_str() == "video" => false,
|
||||||
}
|
|
||||||
Some(mime) if mime.type_() == mime::VIDEO => false,
|
|
||||||
_ => true,
|
_ => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ use actix_service::{Service, Transform};
|
||||||
use actix_utils::future::{ready, Ready};
|
use actix_utils::future::{ready, Ready};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_core::ready;
|
use futures_core::ready;
|
||||||
use log::{debug, warn, Level};
|
use log::{debug, warn};
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
#[cfg(feature = "unicode")]
|
#[cfg(feature = "unicode")]
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
@ -92,7 +92,6 @@ struct Inner {
|
||||||
exclude: HashSet<String>,
|
exclude: HashSet<String>,
|
||||||
exclude_regex: Vec<Regex>,
|
exclude_regex: Vec<Regex>,
|
||||||
log_target: Cow<'static, str>,
|
log_target: Cow<'static, str>,
|
||||||
log_level: Level,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Logger {
|
impl Logger {
|
||||||
|
@ -103,7 +102,6 @@ impl Logger {
|
||||||
exclude: HashSet::new(),
|
exclude: HashSet::new(),
|
||||||
exclude_regex: Vec::new(),
|
exclude_regex: Vec::new(),
|
||||||
log_target: Cow::Borrowed(module_path!()),
|
log_target: Cow::Borrowed(module_path!()),
|
||||||
log_level: Level::Info,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,23 +139,6 @@ impl Logger {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the log level to `level`.
|
|
||||||
///
|
|
||||||
/// By default, the log level is `Level::Info`.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// Using `.log_level(Level::Debug)` would have this effect on request logs:
|
|
||||||
/// ```diff
|
|
||||||
/// - [2015-10-21T07:28:00Z INFO actix_web::middleware::logger] 127.0.0.1 "GET / HTTP/1.1" 200 88 "-" "dmc/1.0" 0.001985
|
|
||||||
/// + [2015-10-21T07:28:00Z DEBUG actix_web::middleware::logger] 127.0.0.1 "GET / HTTP/1.1" 200 88 "-" "dmc/1.0" 0.001985
|
|
||||||
/// ^^^^^^
|
|
||||||
/// ```
|
|
||||||
pub fn log_level(mut self, level: log::Level) -> Self {
|
|
||||||
let inner = Rc::get_mut(&mut self.0).unwrap();
|
|
||||||
inner.log_level = level;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register a function that receives a ServiceRequest and returns a String for use in the
|
/// Register a function that receives a ServiceRequest and returns a String for use in the
|
||||||
/// log line. The label passed as the first argument should match a replacement substring in
|
/// log line. The label passed as the first argument should match a replacement substring in
|
||||||
/// the logger format like `%{label}xi`.
|
/// the logger format like `%{label}xi`.
|
||||||
|
@ -261,7 +242,6 @@ impl Default for Logger {
|
||||||
exclude: HashSet::new(),
|
exclude: HashSet::new(),
|
||||||
exclude_regex: Vec::new(),
|
exclude_regex: Vec::new(),
|
||||||
log_target: Cow::Borrowed(module_path!()),
|
log_target: Cow::Borrowed(module_path!()),
|
||||||
log_level: Level::Info,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -332,7 +312,6 @@ where
|
||||||
format: None,
|
format: None,
|
||||||
time: OffsetDateTime::now_utc(),
|
time: OffsetDateTime::now_utc(),
|
||||||
log_target: Cow::Borrowed(""),
|
log_target: Cow::Borrowed(""),
|
||||||
log_level: self.inner.log_level,
|
|
||||||
_phantom: PhantomData,
|
_phantom: PhantomData,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -348,7 +327,6 @@ where
|
||||||
format: Some(format),
|
format: Some(format),
|
||||||
time: now,
|
time: now,
|
||||||
log_target: self.inner.log_target.clone(),
|
log_target: self.inner.log_target.clone(),
|
||||||
log_level: self.inner.log_level,
|
|
||||||
_phantom: PhantomData,
|
_phantom: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -366,7 +344,6 @@ pin_project! {
|
||||||
time: OffsetDateTime,
|
time: OffsetDateTime,
|
||||||
format: Option<Format>,
|
format: Option<Format>,
|
||||||
log_target: Cow<'static, str>,
|
log_target: Cow<'static, str>,
|
||||||
log_level: Level,
|
|
||||||
_phantom: PhantomData<B>,
|
_phantom: PhantomData<B>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -413,7 +390,6 @@ where
|
||||||
let time = *this.time;
|
let time = *this.time;
|
||||||
let format = this.format.take();
|
let format = this.format.take();
|
||||||
let log_target = this.log_target.clone();
|
let log_target = this.log_target.clone();
|
||||||
let log_level = *this.log_level;
|
|
||||||
|
|
||||||
Poll::Ready(Ok(res.map_body(move |_, body| StreamLog {
|
Poll::Ready(Ok(res.map_body(move |_, body| StreamLog {
|
||||||
body,
|
body,
|
||||||
|
@ -421,7 +397,6 @@ where
|
||||||
format,
|
format,
|
||||||
size: 0,
|
size: 0,
|
||||||
log_target,
|
log_target,
|
||||||
log_level,
|
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -434,7 +409,6 @@ pin_project! {
|
||||||
size: usize,
|
size: usize,
|
||||||
time: OffsetDateTime,
|
time: OffsetDateTime,
|
||||||
log_target: Cow<'static, str>,
|
log_target: Cow<'static, str>,
|
||||||
log_level: Level
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B> PinnedDrop for StreamLog<B> {
|
impl<B> PinnedDrop for StreamLog<B> {
|
||||||
|
@ -447,9 +421,8 @@ pin_project! {
|
||||||
Ok(())
|
Ok(())
|
||||||
};
|
};
|
||||||
|
|
||||||
log::log!(
|
log::info!(
|
||||||
target: this.log_target.as_ref(),
|
target: this.log_target.as_ref(),
|
||||||
this.log_level,
|
|
||||||
"{}", FormatDisplay(&render)
|
"{}", FormatDisplay(&render)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -649,9 +622,9 @@ impl FormatText {
|
||||||
|
|
||||||
FormatText::ResponseHeader(ref name) => {
|
FormatText::ResponseHeader(ref name) => {
|
||||||
let s = if let Some(val) = res.headers().get(name) {
|
let s = if let Some(val) = res.headers().get(name) {
|
||||||
String::from_utf8_lossy(val.as_bytes()).into_owned()
|
val.to_str().unwrap_or("-")
|
||||||
} else {
|
} else {
|
||||||
"-".to_owned()
|
"-"
|
||||||
};
|
};
|
||||||
*self = FormatText::Str(s.to_string())
|
*self = FormatText::Str(s.to_string())
|
||||||
}
|
}
|
||||||
|
@ -693,11 +666,11 @@ impl FormatText {
|
||||||
FormatText::RequestTime => *self = FormatText::Str(now.format(&Rfc3339).unwrap()),
|
FormatText::RequestTime => *self = FormatText::Str(now.format(&Rfc3339).unwrap()),
|
||||||
FormatText::RequestHeader(ref name) => {
|
FormatText::RequestHeader(ref name) => {
|
||||||
let s = if let Some(val) = req.headers().get(name) {
|
let s = if let Some(val) = req.headers().get(name) {
|
||||||
String::from_utf8_lossy(val.as_bytes()).into_owned()
|
val.to_str().unwrap_or("-")
|
||||||
} else {
|
} else {
|
||||||
"-".to_owned()
|
"-"
|
||||||
};
|
};
|
||||||
*self = FormatText::Str(s);
|
*self = FormatText::Str(s.to_string());
|
||||||
}
|
}
|
||||||
FormatText::RemoteAddr => {
|
FormatText::RemoteAddr => {
|
||||||
let s = if let Some(peer) = req.connection_info().peer_addr() {
|
let s = if let Some(peer) = req.connection_info().peer_addr() {
|
||||||
|
|
|
@ -264,10 +264,8 @@ impl HttpRequest {
|
||||||
///
|
///
|
||||||
/// For expanded client connection information, use [`connection_info`] instead.
|
/// For expanded client connection information, use [`connection_info`] instead.
|
||||||
///
|
///
|
||||||
/// Will only return `None` when server is listening on [UDS socket] or when called in unit
|
/// Will only return None when called in unit tests unless [`TestRequest::peer_addr`] is used.
|
||||||
/// tests unless [`TestRequest::peer_addr`] is used.
|
|
||||||
///
|
///
|
||||||
/// [UDS socket]: crate::HttpServer::bind_uds
|
|
||||||
/// [`TestRequest::peer_addr`]: crate::test::TestRequest::peer_addr
|
/// [`TestRequest::peer_addr`]: crate::test::TestRequest::peer_addr
|
||||||
/// [`connection_info`]: Self::connection_info
|
/// [`connection_info`]: Self::connection_info
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
|
@ -417,8 +417,6 @@ where
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
fn register(mut self, config: &mut AppService) {
|
fn register(mut self, config: &mut AppService) {
|
||||||
let routes = std::mem::take(&mut self.routes);
|
|
||||||
|
|
||||||
let guards = if self.guards.is_empty() {
|
let guards = if self.guards.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
@ -435,28 +433,27 @@ where
|
||||||
rdef.set_name(name);
|
rdef.set_name(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "experimental-introspection")]
|
#[cfg(feature = "resources-introspection")]
|
||||||
|
let mut rdef_methods: Vec<(String, Vec<String>)> = Vec::new();
|
||||||
|
#[cfg(feature = "resources-introspection")]
|
||||||
|
let mut rmap = crate::rmap::ResourceMap::new(ResourceDef::prefix(""));
|
||||||
|
|
||||||
|
#[cfg(feature = "resources-introspection")]
|
||||||
{
|
{
|
||||||
let pat = rdef.pattern().unwrap_or("").to_string();
|
rmap.add(&mut rdef, None);
|
||||||
let mut methods = Vec::new();
|
|
||||||
let mut guard_names = Vec::new();
|
self.routes.iter().for_each(|r| {
|
||||||
for route in &routes {
|
r.get_guards().iter().for_each(|g| {
|
||||||
if let Some(m) = route.get_method() {
|
let http_methods: Vec<String> =
|
||||||
if !methods.contains(&m) {
|
crate::guard::HttpMethodsExtractor::extract_http_methods(&**g);
|
||||||
methods.push(m);
|
rdef_methods
|
||||||
}
|
.push((rdef.pattern().unwrap_or_default().to_string(), http_methods));
|
||||||
}
|
});
|
||||||
for name in route.guard_names() {
|
});
|
||||||
if !guard_names.contains(&name) {
|
|
||||||
guard_names.push(name.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
crate::introspection::register_pattern_detail(pat, methods, guard_names);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*self.factory_ref.borrow_mut() = Some(ResourceFactory {
|
*self.factory_ref.borrow_mut() = Some(ResourceFactory {
|
||||||
routes,
|
routes: self.routes,
|
||||||
default: self.default,
|
default: self.default,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -473,6 +470,14 @@ where
|
||||||
async { Ok(fut.await?.map_into_boxed_body()) }
|
async { Ok(fut.await?.map_into_boxed_body()) }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[cfg(feature = "resources-introspection")]
|
||||||
|
{
|
||||||
|
crate::introspection::process_introspection(
|
||||||
|
Rc::clone(&Rc::new(rmap.clone())),
|
||||||
|
rdef_methods,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
config.register_service(rdef, guards, endpoint, None)
|
config.register_service(rdef, guards, endpoint, None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,16 @@ const AVG_PATH_LEN: usize = 24;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ResourceMap {
|
pub struct ResourceMap {
|
||||||
pub(crate) pattern: ResourceDef,
|
pattern: ResourceDef,
|
||||||
|
|
||||||
/// Named resources within the tree or, for external resources, it points to isolated nodes
|
/// Named resources within the tree or, for external resources, it points to isolated nodes
|
||||||
/// outside the tree.
|
/// outside the tree.
|
||||||
named: FoldHashMap<String, Rc<ResourceMap>>,
|
named: FoldHashMap<String, Rc<ResourceMap>>,
|
||||||
|
|
||||||
parent: RefCell<Weak<ResourceMap>>,
|
parent: RefCell<Weak<ResourceMap>>,
|
||||||
|
|
||||||
/// Must be `None` for "edge" nodes.
|
/// Must be `None` for "edge" nodes.
|
||||||
pub(crate) nodes: Option<Vec<Rc<ResourceMap>>>,
|
nodes: Option<Vec<Rc<ResourceMap>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceMap {
|
impl ResourceMap {
|
||||||
|
@ -35,6 +38,42 @@ impl ResourceMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "resources-introspection")]
|
||||||
|
/// Returns a list of all paths in the resource map.
|
||||||
|
pub fn to_vec(&self) -> Vec<String> {
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
self.collect_full_paths(String::new(), &mut paths);
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "resources-introspection")]
|
||||||
|
/// Recursive function that accumulates the full path and adds it only if the node is an endpoint (leaf).
|
||||||
|
fn collect_full_paths(&self, current_path: String, paths: &mut Vec<String>) {
|
||||||
|
// Get the current segment of the pattern
|
||||||
|
if let Some(segment) = self.pattern.pattern() {
|
||||||
|
let mut new_path = current_path;
|
||||||
|
// Add the '/' separator if necessary
|
||||||
|
if !segment.is_empty() {
|
||||||
|
if !new_path.ends_with('/') && !new_path.is_empty() && !segment.starts_with('/') {
|
||||||
|
new_path.push('/');
|
||||||
|
}
|
||||||
|
new_path.push_str(segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this node is an endpoint (has no children), add the full path
|
||||||
|
if self.nodes.is_none() {
|
||||||
|
paths.push(new_path.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it has children, iterate over each one
|
||||||
|
if let Some(children) = &self.nodes {
|
||||||
|
for child in children {
|
||||||
|
child.collect_full_paths(new_path.clone(), paths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Format resource map as tree structure (unfinished).
|
/// Format resource map as tree structure (unfinished).
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn tree(&self) -> String {
|
pub(crate) fn tree(&self) -> String {
|
||||||
|
|
|
@ -65,10 +65,10 @@ impl Route {
|
||||||
pub(crate) fn take_guards(&mut self) -> Vec<Box<dyn Guard>> {
|
pub(crate) fn take_guards(&mut self) -> Vec<Box<dyn Guard>> {
|
||||||
mem::take(Rc::get_mut(&mut self.guards).unwrap())
|
mem::take(Rc::get_mut(&mut self.guards).unwrap())
|
||||||
}
|
}
|
||||||
/// Get the names of all guards applied to this route.
|
|
||||||
#[cfg(feature = "experimental-introspection")]
|
#[cfg(feature = "resources-introspection")]
|
||||||
pub fn guard_names(&self) -> Vec<String> {
|
pub(crate) fn get_guards(&self) -> &Vec<Box<dyn Guard>> {
|
||||||
self.guards.iter().map(|g| g.name()).collect()
|
&self.guards
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,23 +145,6 @@ impl Route {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "experimental-introspection")]
|
|
||||||
/// Get the first HTTP method guard applied to this route (if any).
|
|
||||||
/// WIP.
|
|
||||||
pub(crate) fn get_method(&self) -> Option<Method> {
|
|
||||||
self.guards.iter().find_map(|g| {
|
|
||||||
g.details().and_then(|details| {
|
|
||||||
details.into_iter().find_map(|d| {
|
|
||||||
if let crate::guard::GuardDetail::HttpMethods(mut m) = d {
|
|
||||||
m.pop().and_then(|s| s.parse().ok())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add guard to the route.
|
/// Add guard to the route.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
|
@ -181,10 +164,6 @@ impl Route {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn guards(&self) -> &Vec<Box<dyn Guard>> {
|
|
||||||
&self.guards
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set handler function, use request extractors for parameters.
|
/// Set handler function, use request extractors for parameters.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
|
|
|
@ -395,6 +395,9 @@ where
|
||||||
rmap.add(&mut rdef, None);
|
rmap.add(&mut rdef, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "resources-introspection")]
|
||||||
|
let mut rdef_methods: Vec<(String, Vec<String>)> = Vec::new();
|
||||||
|
|
||||||
// complete scope pipeline creation
|
// complete scope pipeline creation
|
||||||
*self.factory_ref.borrow_mut() = Some(ScopeFactory {
|
*self.factory_ref.borrow_mut() = Some(ScopeFactory {
|
||||||
default,
|
default,
|
||||||
|
@ -405,38 +408,21 @@ where
|
||||||
.map(|(mut rdef, srv, guards, nested)| {
|
.map(|(mut rdef, srv, guards, nested)| {
|
||||||
rmap.add(&mut rdef, nested);
|
rmap.add(&mut rdef, nested);
|
||||||
|
|
||||||
#[cfg(feature = "experimental-introspection")]
|
#[cfg(feature = "resources-introspection")]
|
||||||
{
|
{
|
||||||
use std::borrow::Borrow;
|
let http_methods: Vec<String> =
|
||||||
|
guards.as_ref().map_or_else(Vec::new, |g| {
|
||||||
// Get the pattern stored in ResourceMap
|
g.iter()
|
||||||
let pat = rdef.pattern().unwrap_or("").to_string();
|
.flat_map(|g| {
|
||||||
let guard_list: &[Box<dyn Guard>] =
|
crate::guard::HttpMethodsExtractor::extract_http_methods(
|
||||||
guards.borrow().as_ref().map_or(&[], |v| &v[..]);
|
&**g,
|
||||||
|
)
|
||||||
// Extract HTTP methods from guards
|
|
||||||
let methods = guard_list
|
|
||||||
.iter()
|
|
||||||
.flat_map(|g| g.details().unwrap_or_default())
|
|
||||||
.flat_map(|d| {
|
|
||||||
if let crate::guard::GuardDetail::HttpMethods(v) = d {
|
|
||||||
v.into_iter()
|
|
||||||
.filter_map(|s| s.parse().ok())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
|
||||||
// Extract guard names
|
rdef_methods
|
||||||
let guard_names = guard_list
|
.push((rdef.pattern().unwrap_or_default().to_string(), http_methods));
|
||||||
.iter()
|
|
||||||
.map(|g| g.name().to_string())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// Register route details for introspection
|
|
||||||
crate::introspection::register_pattern_detail(pat, methods, guard_names);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(rdef, srv, RefCell::new(guards))
|
(rdef, srv, RefCell::new(guards))
|
||||||
|
@ -466,6 +452,14 @@ where
|
||||||
async { Ok(fut.await?.map_into_boxed_body()) }
|
async { Ok(fut.await?.map_into_boxed_body()) }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[cfg(feature = "resources-introspection")]
|
||||||
|
{
|
||||||
|
crate::introspection::process_introspection(
|
||||||
|
Rc::clone(&Rc::new(rmap.clone())),
|
||||||
|
rdef_methods,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// register final service
|
// register final service
|
||||||
config.register_service(
|
config.register_service(
|
||||||
ResourceDef::root_prefix(&self.rdef),
|
ResourceDef::root_prefix(&self.rdef),
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
cmp, fmt,
|
cmp, fmt, io,
|
||||||
future::Future,
|
|
||||||
io,
|
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
net,
|
net,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
|
@ -66,7 +64,6 @@ struct Config {
|
||||||
/// .await
|
/// .await
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
|
||||||
pub struct HttpServer<F, I, S, B>
|
pub struct HttpServer<F, I, S, B>
|
||||||
where
|
where
|
||||||
F: Fn() -> I + Send + Clone + 'static,
|
F: Fn() -> I + Send + Clone + 'static,
|
||||||
|
@ -275,12 +272,19 @@ where
|
||||||
/// - `actix_web::rt::net::TcpStream` when no encryption is used.
|
/// - `actix_web::rt::net::TcpStream` when no encryption is used.
|
||||||
///
|
///
|
||||||
/// See the `on_connect` example for additional details.
|
/// See the `on_connect` example for additional details.
|
||||||
pub fn on_connect<CB>(mut self, f: CB) -> HttpServer<F, I, S, B>
|
pub fn on_connect<CB>(self, f: CB) -> HttpServer<F, I, S, B>
|
||||||
where
|
where
|
||||||
CB: Fn(&dyn Any, &mut Extensions) + Send + Sync + 'static,
|
CB: Fn(&dyn Any, &mut Extensions) + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
self.on_connect_fn = Some(Arc::new(f));
|
HttpServer {
|
||||||
self
|
factory: self.factory,
|
||||||
|
config: self.config,
|
||||||
|
backlog: self.backlog,
|
||||||
|
sockets: self.sockets,
|
||||||
|
builder: self.builder,
|
||||||
|
on_connect_fn: Some(Arc::new(f)),
|
||||||
|
_phantom: PhantomData,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets server host name.
|
/// Sets server host name.
|
||||||
|
@ -308,37 +312,6 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Specify shutdown signal from a future.
|
|
||||||
///
|
|
||||||
/// Using this method will prevent OS signal handlers being set up.
|
|
||||||
///
|
|
||||||
/// Typically, a `CancellationToken` will be used, but any future _can_ be.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// use actix_web::{App, HttpServer};
|
|
||||||
/// use tokio_util::sync::CancellationToken;
|
|
||||||
///
|
|
||||||
/// # #[actix_web::main]
|
|
||||||
/// # async fn main() -> std::io::Result<()> {
|
|
||||||
/// let stop_signal = CancellationToken::new();
|
|
||||||
///
|
|
||||||
/// HttpServer::new(move || App::new())
|
|
||||||
/// .shutdown_signal(stop_signal.cancelled_owned())
|
|
||||||
/// .bind(("127.0.0.1", 8080))?
|
|
||||||
/// .run()
|
|
||||||
/// .await
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn shutdown_signal<Fut>(mut self, shutdown_signal: Fut) -> Self
|
|
||||||
where
|
|
||||||
Fut: Future<Output = ()> + Send + 'static,
|
|
||||||
{
|
|
||||||
self.builder = self.builder.shutdown_signal(shutdown_signal);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets timeout for graceful worker shutdown of workers.
|
/// Sets timeout for graceful worker shutdown of workers.
|
||||||
///
|
///
|
||||||
/// After receiving a stop signal, workers have this much time to finish serving requests.
|
/// After receiving a stop signal, workers have this much time to finish serving requests.
|
||||||
|
@ -909,7 +882,6 @@ where
|
||||||
let factory = self.factory.clone();
|
let factory = self.factory.clone();
|
||||||
let cfg = Arc::clone(&self.config);
|
let cfg = Arc::clone(&self.config);
|
||||||
let addr = lst.local_addr().unwrap();
|
let addr = lst.local_addr().unwrap();
|
||||||
|
|
||||||
self.sockets.push(Socket {
|
self.sockets.push(Socket {
|
||||||
addr,
|
addr,
|
||||||
scheme: "https",
|
scheme: "https",
|
||||||
|
@ -1014,7 +986,6 @@ where
|
||||||
let factory = self.factory.clone();
|
let factory = self.factory.clone();
|
||||||
let socket_addr =
|
let socket_addr =
|
||||||
net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080);
|
net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080);
|
||||||
|
|
||||||
self.sockets.push(Socket {
|
self.sockets.push(Socket {
|
||||||
scheme: "http",
|
scheme: "http",
|
||||||
addr: socket_addr,
|
addr: socket_addr,
|
||||||
|
@ -1102,7 +1073,10 @@ fn bind_addrs(addrs: impl net::ToSocketAddrs, backlog: u32) -> io::Result<Vec<ne
|
||||||
} else if let Some(err) = err.take() {
|
} else if let Some(err) = err.take() {
|
||||||
Err(err)
|
Err(err)
|
||||||
} else {
|
} else {
|
||||||
Err(io::Error::other("Could not bind to address"))
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"Can not bind to address.",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,6 @@
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
## 3.7.0
|
|
||||||
|
|
||||||
- Update `brotli` dependency to `8`.
|
|
||||||
|
|
||||||
## 3.6.0
|
## 3.6.0
|
||||||
|
|
||||||
- Prevent panics on connection pool drop when Tokio runtime is shutdown early.
|
- Prevent panics on connection pool drop when Tokio runtime is shutdown early.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "awc"
|
name = "awc"
|
||||||
version = "3.7.0"
|
version = "3.6.0"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Async HTTP and WebSocket client library"
|
description = "Async HTTP and WebSocket client library"
|
||||||
keywords = ["actix", "http", "framework", "async", "web"]
|
keywords = ["actix", "http", "framework", "async", "web"]
|
||||||
|
@ -97,9 +97,9 @@ dangerous-h2c = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-codec = "0.5"
|
actix-codec = "0.5"
|
||||||
|
actix-service = "2"
|
||||||
actix-http = { version = "3.10", features = ["http2", "ws"] }
|
actix-http = { version = "3.10", features = ["http2", "ws"] }
|
||||||
actix-rt = { version = "2.1", default-features = false }
|
actix-rt = { version = "2.1", default-features = false }
|
||||||
actix-service = "2"
|
|
||||||
actix-tls = { version = "3.4", features = ["connect", "uri"] }
|
actix-tls = { version = "3.4", features = ["connect", "uri"] }
|
||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ rand = "0.9"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
tokio = { version = "1.38.2", features = ["sync"] }
|
tokio = { version = "1.24.2", features = ["sync"] }
|
||||||
|
|
||||||
cookie = { version = "0.16", features = ["percent-encode"], optional = true }
|
cookie = { version = "0.16", features = ["percent-encode"], optional = true }
|
||||||
|
|
||||||
|
@ -141,15 +141,15 @@ actix-tls = { version = "3.4", features = ["openssl", "rustls-0_23"] }
|
||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
actix-web = { version = "4", features = ["openssl"] }
|
actix-web = { version = "4", features = ["openssl"] }
|
||||||
|
|
||||||
brotli = "8"
|
brotli = "7"
|
||||||
const-str = "0.5" # TODO(MSRV 1.77): update to 0.6
|
const-str = "0.5"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
flate2 = "1.0.13"
|
flate2 = "1.0.13"
|
||||||
futures-util = { version = "0.3.17", default-features = false }
|
futures-util = { version = "0.3.17", default-features = false }
|
||||||
static_assertions = "1.1"
|
static_assertions = "1.1"
|
||||||
rcgen = "0.13"
|
rcgen = "0.13"
|
||||||
rustls-pemfile = "2"
|
rustls-pemfile = "2"
|
||||||
tokio = { version = "1.38.2", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] }
|
||||||
zstd = "0.13"
|
zstd = "0.13"
|
||||||
tls-rustls-0_23 = { package = "rustls", version = "0.23" } # add rustls 0.23 with default features to make aws_lc_rs work in tests
|
tls-rustls-0_23 = { package = "rustls", version = "0.23" } # add rustls 0.23 with default features to make aws_lc_rs work in tests
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
[](https://crates.io/crates/awc)
|
[](https://crates.io/crates/awc)
|
||||||
[](https://docs.rs/awc/3.7.0)
|
[](https://docs.rs/awc/3.6.0)
|
||||||

|

|
||||||
[](https://deps.rs/crate/awc/3.7.0)
|
[](https://deps.rs/crate/awc/3.6.0)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
|
|
|
@ -179,8 +179,9 @@ where
|
||||||
.acquire_owned()
|
.acquire_owned()
|
||||||
.await
|
.await
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
ConnectError::Io(io::Error::other(
|
ConnectError::Io(io::Error::new(
|
||||||
"Failed to acquire semaphore on client connection pool",
|
io::ErrorKind::Other,
|
||||||
|
"failed to acquire semaphore on client connection pool",
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
42
justfile
42
justfile
|
@ -1,8 +1,6 @@
|
||||||
_list:
|
_list:
|
||||||
@just --list
|
@just --list
|
||||||
|
|
||||||
toolchain := ""
|
|
||||||
|
|
||||||
# Format workspace.
|
# Format workspace.
|
||||||
fmt:
|
fmt:
|
||||||
just --unstable --fmt
|
just --unstable --fmt
|
||||||
|
@ -12,11 +10,11 @@ fmt:
|
||||||
# Downgrade dependencies necessary to run MSRV checks/tests.
|
# Downgrade dependencies necessary to run MSRV checks/tests.
|
||||||
[private]
|
[private]
|
||||||
downgrade-for-msrv:
|
downgrade-for-msrv:
|
||||||
cargo {{ toolchain }} update -p=divan --precise=0.1.15 # next ver: 1.80.0
|
cargo update -p=clap --precise=4.4.18 # next ver: 1.74.0
|
||||||
cargo {{ toolchain }} update -p=half --precise=2.4.1 # next ver: 1.81.0
|
cargo update -p=divan --precise=0.1.15 # next ver: 1.80.0
|
||||||
cargo {{ toolchain }} update -p=idna_adapter --precise=1.2.0 # next ver: 1.82.0
|
cargo update -p=litemap --precise=0.7.4 # next ver: 1.81.0
|
||||||
cargo {{ toolchain }} update -p=litemap --precise=0.7.4 # next ver: 1.81.0
|
cargo update -p=zerofrom --precise=0.1.5 # next ver: 1.81.0
|
||||||
cargo {{ toolchain }} update -p=zerofrom --precise=0.1.5 # next ver: 1.81.0
|
cargo update -p=half --precise=2.4.1 # next ver: 1.81.0
|
||||||
|
|
||||||
msrv := ```
|
msrv := ```
|
||||||
cargo metadata --format-version=1 \
|
cargo metadata --format-version=1 \
|
||||||
|
@ -41,50 +39,42 @@ check-min:
|
||||||
check-default:
|
check-default:
|
||||||
cargo hack --workspace check
|
cargo hack --workspace check
|
||||||
|
|
||||||
# Check workspace.
|
# Run Clippy over workspace.
|
||||||
check: && clippy
|
check toolchain="": && (clippy toolchain)
|
||||||
fd --hidden --type=file --extension=md --extension=yml --exec-batch npx -y prettier --check
|
|
||||||
|
|
||||||
# Run Clippy over workspace.
|
# Run Clippy over workspace.
|
||||||
clippy:
|
clippy toolchain="":
|
||||||
cargo {{ toolchain }} clippy --workspace --all-targets {{ all_crate_features }}
|
cargo {{ toolchain }} clippy --workspace --all-targets {{ all_crate_features }}
|
||||||
|
|
||||||
# Run Clippy over workspace using MSRV.
|
# Test workspace using MSRV.
|
||||||
clippy-msrv:
|
test-msrv: downgrade-for-msrv (test msrv_rustup)
|
||||||
@just toolchain={{ msrv_rustup }} downgrade-for-msrv
|
|
||||||
@just toolchain={{ msrv_rustup }} clippy
|
|
||||||
|
|
||||||
# Test workspace code.
|
# Test workspace code.
|
||||||
test:
|
test toolchain="":
|
||||||
cargo {{ toolchain }} test --lib --tests -p=actix-web-codegen --all-features
|
cargo {{ toolchain }} test --lib --tests -p=actix-web-codegen --all-features
|
||||||
cargo {{ toolchain }} test --lib --tests -p=actix-multipart-derive --all-features
|
cargo {{ toolchain }} test --lib --tests -p=actix-multipart-derive --all-features
|
||||||
cargo {{ toolchain }} nextest run --no-tests=warn -p=actix-router --no-default-features
|
cargo {{ toolchain }} nextest run --no-tests=warn -p=actix-router --no-default-features
|
||||||
cargo {{ toolchain }} nextest run --no-tests=warn --workspace --exclude=actix-web-codegen --exclude=actix-multipart-derive {{ all_crate_features }} --filter-expr="not test(test_reading_deflate_encoding_large_random_rustls)"
|
cargo {{ toolchain }} nextest run --no-tests=warn --workspace --exclude=actix-web-codegen --exclude=actix-multipart-derive {{ all_crate_features }} --filter-expr="not test(test_reading_deflate_encoding_large_random_rustls)"
|
||||||
|
|
||||||
# Test workspace using MSRV.
|
|
||||||
test-msrv:
|
|
||||||
@just toolchain={{ msrv_rustup }} downgrade-for-msrv
|
|
||||||
@just toolchain={{ msrv_rustup }} test
|
|
||||||
|
|
||||||
# Test workspace docs.
|
# Test workspace docs.
|
||||||
test-docs: && doc
|
test-docs toolchain="": && doc
|
||||||
cargo {{ toolchain }} test --doc --workspace {{ all_crate_features }} --no-fail-fast -- --nocapture
|
cargo {{ toolchain }} test --doc --workspace {{ all_crate_features }} --no-fail-fast -- --nocapture
|
||||||
|
|
||||||
# Test workspace.
|
# Test workspace.
|
||||||
test-all: test test-docs
|
test-all toolchain="": (test toolchain) (test-docs toolchain)
|
||||||
|
|
||||||
# Test workspace and collect coverage info.
|
# Test workspace and collect coverage info.
|
||||||
[private]
|
[private]
|
||||||
test-coverage:
|
test-coverage toolchain="":
|
||||||
cargo {{ toolchain }} llvm-cov nextest --no-tests=warn --no-report {{ all_crate_features }}
|
cargo {{ toolchain }} llvm-cov nextest --no-tests=warn --no-report {{ all_crate_features }}
|
||||||
cargo {{ toolchain }} llvm-cov --doc --no-report {{ all_crate_features }}
|
cargo {{ toolchain }} llvm-cov --doc --no-report {{ all_crate_features }}
|
||||||
|
|
||||||
# Test workspace and generate Codecov report.
|
# Test workspace and generate Codecov report.
|
||||||
test-coverage-codecov: test-coverage
|
test-coverage-codecov toolchain="": (test-coverage toolchain)
|
||||||
cargo {{ toolchain }} llvm-cov report --doctests --codecov --output-path=codecov.json
|
cargo {{ toolchain }} llvm-cov report --doctests --codecov --output-path=codecov.json
|
||||||
|
|
||||||
# Test workspace and generate LCOV report.
|
# Test workspace and generate LCOV report.
|
||||||
test-coverage-lcov: test-coverage
|
test-coverage-lcov toolchain="": (test-coverage toolchain)
|
||||||
cargo {{ toolchain }} llvm-cov report --doctests --lcov --output-path=lcov.info
|
cargo {{ toolchain }} llvm-cov report --doctests --lcov --output-path=lcov.info
|
||||||
|
|
||||||
# Document crates in workspace.
|
# Document crates in workspace.
|
||||||
|
|
Loading…
Reference in New Issue