Compare commits

..

33 Commits

Author SHA1 Message Date
Guillermo Céspedes Tabárez 2f64cdb60a fix Cargo.lock 2025-05-12 02:57:39 -03:00
Guillermo Céspedes Tabárez dcad1d9b89 Merge branch 'master' into introspection 2025-05-12 02:37:52 -03:00
Guillermo Céspedes Tabárez d501102610 feat(introspection): rename feature from `resources-introspection` to `experimental-introspection`
- Refactored introspection logic.
- Enhanced route introspection to register HTTP methods and guard names.
- Added example for testing the experimental introspection feature.
2025-05-12 02:29:11 -03:00
dependabot[bot] 072fdc182d
build(deps): bump tempfile from 3.19.1 to 3.20.0 (#3646)
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.19.1 to 3.20.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.19.1...v3.20.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-version: 3.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 01:12:31 +00:00
dependabot[bot] 347c7204a6
build(deps): bump trybuild from 1.0.104 to 1.0.105 (#3647)
Bumps [trybuild](https://github.com/dtolnay/trybuild) from 1.0.104 to 1.0.105.
- [Release notes](https://github.com/dtolnay/trybuild/releases)
- [Commits](https://github.com/dtolnay/trybuild/compare/1.0.104...1.0.105)

---
updated-dependencies:
- dependency-name: trybuild
  dependency-version: 1.0.105
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 01:11:19 +00:00
Rob Ede 573090e01f
chore: update lockfile 2025-05-10 06:48:31 +01:00
Rob Ede 69dda5c943
ci: fix msrv job 2025-05-10 06:23:33 +01:00
Rob Ede 1b4b61d839
chore(awc): prepare release 3.7.0 2025-05-10 06:19:29 +01:00
Rob Ede 2c55d659dd
chore(actix-web): prepare release 4.11.0 2025-05-10 06:19:10 +01:00
Rob Ede 276f5d5bd4
chore(actix-http): prepare release 3.11.0 2025-05-10 06:18:25 +01:00
Rob Ede 5f3c02813a
chore: narrow tokio dep to account for RUSTSEC-2025-0023
closes #3640
2025-05-10 06:09:51 +01:00
Degubi 3d3b31e16a
fix: svg files should be compressed (#3486)
* Fix svg files not being compressed

* docs: update changelog

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-05-10 03:21:36 +00:00
Joel Wurtz 3147aaccc7
feat: do not use host header on http2 for guard (#3525)
* feat(guard): do not use host header on http2 for guard

* docs: update changelog

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-05-10 02:42:00 +00:00
Rob Ede 079400a72b
build: add clippy-msrv recipe 2025-05-10 03:21:59 +01:00
Björn Wärmedal a49f055561
build(deps): update url requirement from 2.1 to 2.5.4 (#3527)
Co-authored-by: Björn Wärmedal <bjorn.warmedal@lumera.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-05-10 02:00:20 +00:00
Rob Ede 55268b6898
fix: improve logger header values printing 2025-05-10 02:56:41 +01:00
Rob Ede 89b5b04653
docs: update docs about peer_addr when bound to a UDS socket 2025-05-10 02:23:22 +01:00
silverpill e42cffc28d
Fix HttpRequest::peer_addr documentation 2025-05-10 02:20:13 +01:00
Rob Ede 8765b04476
refactor: simplify on_connect 2025-05-10 02:19:56 +01:00
Rob Ede 40179e2aa6
docs: fix ConnectionInfo::realip_remote_addr documentation 2025-05-10 01:21:34 +01:00
JackSpagnoli 9bbb5414d1
Implements log_level for Logger middleware (#3605)
* implements log level for Logger

* docs: update changelog

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-05-09 23:51:47 +00:00
Alexander 65f254d1b2
Re-export mime types for easier access #3603 (#3624)
* Re-export mime types for easier access #3603

* docs: update changelog

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-05-09 23:40:21 +00:00
Sebastian Ziebell 25511dfb38
Fix order of template arguments in Handler documentation example (#3566)
Fix order of template arguments in doc
2025-05-09 23:31:59 +00:00
Rob Ede 6e902d1d5c
feat: add HttpServer::shutdown_signal (#3644) 2025-05-10 00:16:21 +01:00
dependabot[bot] f1b7cfb253
build(deps): bump tokio from 1.44.2 to 1.45.0 (#3643)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.44.2 to 1.45.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.44.2...tokio-1.45.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-09 20:43:47 +00:00
dependabot[bot] e983276a78
build(deps): bump taiki-e/install-action from 2.50.7 to 2.50.10 (#3641)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.50.7 to 2.50.10.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.50.7...v2.50.10)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.50.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-09 20:40:46 +00:00
dependabot[bot] bbe0134523
build(deps): bump brotli from 7.0.0 to 8.0.0 (#3627)
* build(deps): bump brotli from 7.0.0 to 8.0.0

Bumps [brotli](https://github.com/dropbox/rust-brotli) from 7.0.0 to 8.0.0.
- [Release notes](https://github.com/dropbox/rust-brotli/releases)
- [Commits](https://github.com/dropbox/rust-brotli/commits)

---
updated-dependencies:
- dependency-name: brotli
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* docs: update changelog

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-05-09 20:05:56 +00:00
dependabot[bot] 2dd165dc0b
build(deps): bump divan from 0.1.18 to 0.1.21 (#3621)
Bumps [divan](https://github.com/nvzqz/divan) from 0.1.18 to 0.1.21.
- [Changelog](https://github.com/nvzqz/divan/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nvzqz/divan/compare/v0.1.18...v0.1.21)

---
updated-dependencies:
- dependency-name: divan
  dependency-version: 0.1.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-09 20:05:19 +00:00
dependabot[bot] 9829f87cd1
build(deps): bump taiki-e/install-action from 2.49.50 to 2.50.7 (#3636)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.49.50 to 2.50.7.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.49.50...v2.50.7)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.50.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-09 19:32:24 +00:00
dependabot[bot] 6892896a62
build(deps): bump actions-rust-lang/setup-rust-toolchain from 1.11.0 to 1.12.0 (#3632)
build(deps): bump actions-rust-lang/setup-rust-toolchain

Bumps [actions-rust-lang/setup-rust-toolchain](https://github.com/actions-rust-lang/setup-rust-toolchain) from 1.11.0 to 1.12.0.
- [Release notes](https://github.com/actions-rust-lang/setup-rust-toolchain/releases)
- [Changelog](https://github.com/actions-rust-lang/setup-rust-toolchain/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions-rust-lang/setup-rust-toolchain/compare/v1.11.0...v1.12.0)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-version: 1.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-09 19:32:17 +00:00
dependabot[bot] bdb7b4da2d
build(deps): bump tokio-util from 0.7.14 to 0.7.15 (#3631)
Bumps [tokio-util](https://github.com/tokio-rs/tokio) from 0.7.14 to 0.7.15.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-util-0.7.14...tokio-util-0.7.15)

---
updated-dependencies:
- dependency-name: tokio-util
  dependency-version: 0.7.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-09 19:32:05 +00:00
Rob Ede 7eea3d3657
chore: address clippy lints 2025-05-09 20:21:02 +01:00
Rob Ede ad73cdc823
build: rework justfile toolchain 2025-05-09 20:15:43 +01:00
51 changed files with 1154 additions and 1139 deletions

View File

@ -1,7 +1,8 @@
disallowed-names = [ disallowed-names = [
"..",
"e", # no single letter error bindings "e", # no single letter error bindings
] ]
disallowed-methods = [ disallowed-methods = [
"std::cell::RefCell::default()", { path = "std::cell::RefCell::default()", reason = "prefer explicit inner type default" },
"std::rc::Rc::default()", { path = "std::rc::Rc::default()", reason = "prefer explicit inner type default" },
] ]

View File

@ -1,3 +1,12 @@
version: "0.2" version: "0.2"
words: words:
- actix - actix
- addrs
- bytestring
- httparse
- msrv
- realip
- rustls
- rustup
- serde
- zstd

View File

@ -1,10 +1,11 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
- package-ecosystem: github-actions - package-ecosystem: github-actions
directory: / directory: /
schedule: schedule:
interval: weekly interval: weekly
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
versioning-strategy: lockfile-only

View File

@ -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.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.12.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.49.50 uses: taiki-e/install-action@v2.50.10
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.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
- name: Install just, cargo-hack - name: Install just, cargo-hack
uses: taiki-e/install-action@v2.49.50 uses: taiki-e/install-action@v2.50.10
with: with:
tool: just,cargo-hack tool: just,cargo-hack

View File

@ -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.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.12.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.49.50 uses: taiki-e/install-action@v2.50.10
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.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.12.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.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
with: with:
toolchain: nightly toolchain: nightly
- name: Install just - name: Install just
uses: taiki-e/install-action@v2.49.50 uses: taiki-e/install-action@v2.50.10
with: with:
tool: just tool: just

View File

@ -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.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.12.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.49.50 uses: taiki-e/install-action@v2.50.10
with: with:
tool: just,cargo-llvm-cov,cargo-nextest tool: just,cargo-llvm-cov,cargo-nextest

View File

@ -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.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.12.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.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.12.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.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.12.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.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.12.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.49.50 uses: taiki-e/install-action@v2.50.10
with: with:
tool: just tool: just

38
.taplo.toml Normal file
View File

@ -0,0 +1,38 @@
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

454
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,7 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.6" version = "0.6.6"
authors = [ authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
"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"
@ -14,13 +11,7 @@ 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 = [ allowed_external_types = ["actix_http::*", "actix_service::*", "actix_web::*", "http::*", "mime::*"]
"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"]

View File

@ -37,25 +37,25 @@ default = []
openssl = ["tls-openssl", "awc/openssl"] openssl = ["tls-openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-service = "2"
actix-codec = "0.5" actix-codec = "0.5"
actix-tls = "3"
actix-utils = "3"
actix-rt = "2.2" actix-rt = "2.2"
actix-server = "2" actix-server = "2"
actix-service = "2"
actix-tls = "3"
actix-utils = "3"
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"
slab = "0.4"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
slab = "0.4"
socket2 = "0.5"
tls-openssl = { version = "0.10.55", package = "openssl", optional = true } tls-openssl = { version = "0.10.55", package = "openssl", optional = true }
tokio = { version = "1.24.2", features = ["sync"] } tokio = { version = "1.38.2", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-http = "3" actix-http = "3"

View File

@ -2,6 +2,10 @@
## Unreleased ## Unreleased
## 3.11.0
- Update `brotli` dependency to `8`.
## 3.10.0 ## 3.10.0
### Added ### Added

View File

@ -1,10 +1,7 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.10.0" version = "3.11.0"
authors = [ authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
"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"
@ -62,12 +59,7 @@ default = []
http2 = ["dep:h2"] http2 = ["dep:h2"]
# WebSocket protocol implementation # WebSocket protocol implementation
ws = [ ws = ["dep:local-channel", "dep:base64", "dep:rand", "dep:sha1"]
"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"]
@ -101,10 +93,10 @@ __compress = []
__tls = [] __tls = []
[dependencies] [dependencies]
actix-service = "2"
actix-codec = "0.5" actix-codec = "0.5"
actix-utils = "3"
actix-rt = { version = "2.2", default-features = false } actix-rt = { version = "2.2", default-features = false }
actix-service = "2"
actix-utils = "3"
bitflags = "2" bitflags = "2"
bytes = "1" bytes = "1"
@ -122,7 +114,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.24.2", features = [] } tokio = { version = "1.38.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"] }
@ -130,8 +122,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
local-channel = { version = "0.1", optional = true }
base64 = { version = "0.22", optional = true } base64 = { version = "0.22", optional = true }
local-channel = { version = "0.1", 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 }
@ -139,7 +131,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 = "7", optional = true } brotli = { version = "8", 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 }
@ -158,14 +150,14 @@ memchr = "2.4"
once_cell = "1.21" once_cell = "1.21"
rcgen = "0.13" rcgen = "0.13"
regex = "1.3" regex = "1.3"
rustversion = "1"
rustls-pemfile = "2" rustls-pemfile = "2"
rustversion = "1"
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.24.2", features = ["net", "rt", "macros"] } tokio = { version = "1.38.2", features = ["net", "rt", "macros"] }
[lints] [lints]
workspace = true workspace = true

View File

@ -5,11 +5,11 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.10.0)](https://docs.rs/actix-http/3.10.0) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.11.0)](https://docs.rs/actix-http/3.11.0)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-http/3.10.0/status.svg)](https://deps.rs/crate/actix-http/3.10.0) [![dependency status](https://deps.rs/crate/actix-http/3.11.0/status.svg)](https://deps.rs/crate/actix-http/3.11.0)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -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::new(io::ErrorKind::Other, "abc")); yield Err(io::Error::other("abc"));
}))) })))
}) })
.tcp() .tcp()

View File

@ -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::new(io::ErrorKind::Other, "")) }); let err_stream = stream::once(async { Err(io::Error::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());

View File

@ -100,10 +100,7 @@ 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::new( PayloadError::Io(io::Error::other("Blocking task was cancelled unexpectedly"))
io::ErrorKind::Other,
"Blocking task was cancelled unexpectedly",
))
})??; })??;
*this.decoder = Some(decoder); *this.decoder = Some(decoder);

View File

@ -183,8 +183,7 @@ 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::new( EncoderError::Io(io::Error::other(
io::ErrorKind::Other,
"Blocking task was cancelled unexpectedly", "Blocking task was cancelled unexpectedly",
)) ))
})? })?

View File

@ -415,7 +415,7 @@ mod tests {
#[test] #[test]
fn test_as_response() { fn test_as_response() {
let orig = io::Error::new(io::ErrorKind::Other, "other"); let orig = io::Error::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::new(io::ErrorKind::Other, "other"); let orig = io::Error::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::new(io::ErrorKind::Other, "other"); let orig = io::Error::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::new(io::ErrorKind::Other, "ParseError").into(); let err: PayloadError = io::Error::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::new(io::ErrorKind::Other, "other") => ParseError::Io(..)); from_and_cause!(io::Error::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);

View File

@ -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::new(io::ErrorKind::Other, "unsupported version")), _ => return Err(io::Error::other("Unsupported version")),
} }
) )
.map_err(|err| io::Error::new(io::ErrorKind::Other, err)) .map_err(io::Error::other)
} }
} }
@ -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(|err| io::Error::new(io::ErrorKind::Other, err))?; .map_err(io::Error::other)?;
buf.reserve(msg.len() + 2); buf.reserve(msg.len() + 2);
buf.extend_from_slice(msg); buf.extend_from_slice(msg);

View File

@ -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.24.2", features = ["sync", "io-util"] } tokio = { version = "1.38.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-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
futures-test = "0.3" futures-test = "0.3"
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
multer = "3" multer = "3"
tokio = { version = "1.24.2", features = ["sync"] } tokio = { version = "1.38.2", features = ["sync"] }
tokio-stream = "0.1" tokio-stream = "0.1"
[lints] [lints]

View File

@ -13,10 +13,7 @@ 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 = [ allowed_external_types = ["http::*", "serde::*"]
"http::*",
"serde::*",
]
[features] [features]
default = ["http", "unicode"] default = ["http", "unicode"]
@ -35,8 +32,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"
serde = { version = "1", features = ["derive"] }
percent-encoding = "2.1" percent-encoding = "2.1"
serde = { version = "1", features = ["derive"] }
[lints] [lints]
workspace = true workspace = true

View File

@ -1,10 +1,7 @@
[package] [package]
name = "actix-test" name = "actix-test"
version = "0.1.5" version = "0.1.5"
authors = [ authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
"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"
@ -72,7 +69,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.24.2", features = ["sync"] } tokio = { version = "1.38.2", features = ["sync"] }
[lints] [lints]
workspace = true workspace = true

View File

@ -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.24.2", features = ["sync"] } tokio = { version = "1.38.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"
awc = { version = "3", default-features = false }
actix-web = { version = "4", features = ["macros"] } actix-web = { version = "4", features = ["macros"] }
awc = { version = "3", default-features = false }
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"] }

View File

@ -776,10 +776,7 @@ 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::new( return Poll::Ready(Some(Err(ProtocolError::Io(io::Error::other(err)))));
io::ErrorKind::Other,
format!("{err}"),
)))));
} }
} }
} }
@ -795,11 +792,10 @@ where
} }
Some(frm) => { Some(frm) => {
let msg = match frm { let msg = match frm {
Frame::Text(data) => { Frame::Text(data) => Message::Text(
Message::Text(ByteString::try_from(data).map_err(|err| { ByteString::try_from(data)
ProtocolError::Io(io::Error::new(io::ErrorKind::Other, err)) .map_err(|err| ProtocolError::Io(io::Error::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),

View File

@ -2,10 +2,7 @@
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 = [ authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
"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
@ -33,8 +30,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"] }
trybuild = "1"
rustversion-msrv = "0.100" rustversion-msrv = "0.100"
trybuild = "1"
[lints] [lints]
workspace = true workspace = true

View File

@ -2,7 +2,18 @@
## Unreleased ## Unreleased
- Add `resources-introspection` feature for retrieving configured route paths and HTTP methods. - Add `experimental-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

View File

@ -1,17 +1,14 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.10.2" version = "4.11.0"
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 = [ authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
"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"
@ -121,26 +118,24 @@ __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 = ["compat-routing-macros-force-pub"]
"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.
resources-introspection = [] experimental-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" actix-server = "2.6"
actix-service = "2" actix-service = "2"
actix-utils = "3"
actix-tls = { version = "3.4", default-features = false, optional = true } actix-tls = { version = "3.4", default-features = false, optional = true }
actix-utils = "3"
actix-http = { version = "3.10", features = ["ws"] } actix-http = { version = "3.11", 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 }
@ -153,8 +148,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 }
itoa = "1"
impl-more = "0.1.4" impl-more = "0.1.4"
itoa = "1"
language-tags = "0.3" language-tags = "0.3"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
@ -166,18 +161,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"] }
url = "2.1" tracing = "0.1.30"
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 = "7" brotli = "8"
const-str = "0.5" const-str = "0.5" # TODO(MSRV 1.77): update to 0.6
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"
@ -190,7 +185,8 @@ 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.24.2", features = ["rt-multi-thread", "macros"] } tokio = { version = "1.38.2", features = ["rt-multi-thread", "macros"] }
tokio-util = "0.7"
zstd = "0.13" zstd = "0.13"
[lints] [lints]

View File

@ -8,10 +8,10 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.10.2)](https://docs.rs/actix-web/4.10.2) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.11.0)](https://docs.rs/actix-web/4.11.0)
![MSRV](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![MSRV](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/4.10.2/status.svg)](https://deps.rs/crate/actix-web/4.10.2) [![Dependency Status](https://deps.rs/crate/actix-web/4.11.0/status.svg)](https://deps.rs/crate/actix-web/4.11.0)
<br /> <br />
[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) [![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/actix/actix-web/graph/badge.svg?token=dSwOnp9QCv)](https://codecov.io/gh/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/graph/badge.svg?token=dSwOnp9QCv)](https://codecov.io/gh/actix/actix-web)

View File

@ -0,0 +1,206 @@
// 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)
}
}),
),
);
}

View File

@ -82,9 +82,6 @@ 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,
@ -92,24 +89,35 @@ 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")]
{ {
let http_methods: Vec<String> = use std::borrow::Borrow;
guards.as_ref().map_or_else(Vec::new, |g| { let pat = rdef.pattern().unwrap_or("").to_string();
g.iter() let mut methods = Vec::new();
.flat_map(|g| { let mut guard_names = Vec::new();
crate::guard::HttpMethodsExtractor::extract_http_methods( if let Some(gs) = guards.borrow().as_ref() {
&**g, for g in gs.iter() {
) let name = g.name().to_string();
}) if !guard_names.contains(&name) {
.collect::<Vec<_>>() guard_names.push(name.clone());
}); }
if let Some(details) = g.details() {
rdef_methods for d in details {
.push((rdef.pattern().unwrap_or_default().to_string(), http_methods)); if let crate::guard::GuardDetail::HttpMethods(v) = d {
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<_>>()
@ -126,11 +134,6 @@ 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()));
@ -156,6 +159,11 @@ 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),

View File

@ -1,4 +1,4 @@
use actix_http::{header, uri::Uri, RequestHead}; use actix_http::{header, uri::Uri, RequestHead, Version};
use super::{Guard, GuardContext}; use super::{Guard, GuardContext};
@ -66,6 +66,7 @@ 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())
} }
@ -123,6 +124,38 @@ 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()

View File

@ -397,35 +397,6 @@ 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.")]

View File

@ -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, Arg1, Arg2, Fut> Handler<(Arg1, Arg2)> for Func /// impl<Func, Fut, Arg1, Arg2> 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,

View File

@ -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 [`remote_addr`](Self::remote_addr)) /// - peer address of opened socket (same as [`peer_addr`](Self::peer_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`

View File

@ -1,494 +1,172 @@
use std::{ use std::{
rc::Rc, collections::HashMap,
sync::{OnceLock, RwLock}, sync::{
atomic::{AtomicBool, Ordering},
Mutex, OnceLock,
},
thread, thread,
}; };
use crate::rmap::ResourceMap; use crate::{http::Method, rmap::ResourceMap};
/// Represents an HTTP resource registered for introspection. static REGISTRY: OnceLock<Mutex<IntrospectionNode>> = OnceLock::new();
#[derive(Clone, Debug, PartialEq, Eq)] static DETAIL_REGISTRY: OnceLock<Mutex<HashMap<String, RouteDetail>>> = OnceLock::new();
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);
/// Inserts a resource into the global registry, avoiding duplicates. pub fn initialize_registry() {
pub fn register_resource(resource: ResourceIntrospection) { REGISTRY.get_or_init(|| Mutex::new(IntrospectionNode::new(ResourceType::App, "".into())));
let mut global = RESOURCE_REGISTRY.write().unwrap(); }
if !global.iter().any(|r| r == &resource) {
global.push(resource); pub fn get_registry() -> &'static Mutex<IntrospectionNode> {
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(),
} }
} }
/// Completes (updates) partial routes in the global registry whose path contains `marker`, pub fn display(&self, indent: usize, parent_path: &str) {
/// by applying the specified `prefix`. let full_path = if parent_path.is_empty() {
pub fn complete_partial_routes_with_marker(marker: &str, prefix: &str) { self.pattern.clone()
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!("{}/{}", prefix, resource.path) format!(
"{}/{}",
parent_path.trim_end_matches('/'),
self.pattern.trim_start_matches('/')
)
}; };
let completed = ResourceIntrospection { if !self.methods.is_empty() || !self.guards.is_empty() {
method: resource.method, let methods = if self.methods.is_empty() {
path: full_path, "".to_string()
} else {
format!(" Methods: {:?}", self.methods)
};
let guards = if self.guards.is_empty() {
"".to_string()
} else {
format!(" Guards: {:?}", self.guards)
}; };
// Add to `updated` if it's not already in there. println!("{}{}{}{}", " ".repeat(indent), full_path, methods, guards);
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 {
// Keep this resource as-is. ResourceType::Resource
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();
} }
} }
// Merge updated items back with the remaining ones. build_tree(&mut new_node, child);
remaining.extend(updated); node.children.push(new_node);
*global = remaining; }
}
} }
/// Returns a **copy** of the global registry (safe to call from any thread). fn is_designated_thread() -> bool {
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(|| current_id); DESIGNATED_THREAD.get_or_init(|| {
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. *DESIGNATED_THREAD.get().unwrap() == current_id
// 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 { pub fn register_rmap(rmap: &ResourceMap) {
if !is_designated_thread() {
return; return;
} }
let rmap_vec = rmap.to_vec(); initialize_registry();
let mut root = IntrospectionNode::new(ResourceType::App, "".into());
build_tree(&mut root, rmap);
*get_registry().lock().unwrap() = root;
// If there is no data, nothing to process. // WIP. Display the introspection tree
// Avoid unnecessary work. let reg = get_registry().lock().unwrap();
if rmap_vec.is_empty() && rdef_methods.is_empty() { reg.display(0, "");
}
fn update_unique<T: Clone + PartialEq>(existing: &mut Vec<T>, new_items: &[T]) {
for item in new_items {
if !existing.contains(item) {
existing.push(item.clone());
}
}
}
pub fn register_pattern_detail(pattern: String, methods: Vec<Method>, guards: Vec<String>) {
if !is_designated_thread() {
return; return;
} }
initialize_detail_registry();
// Keep track of the deduced prefix for partial routes. let mut reg = get_detail_registry().lock().unwrap();
let mut deduced_prefix: Option<String> = None; reg.entry(pattern)
.and_modify(|d| {
// 1. Handle "marker" entries (where methods is empty). update_unique(&mut d.methods, &methods);
for (sub_path, http_methods) in &rdef_methods { update_unique(&mut d.guards, &guards);
if http_methods.is_empty() { })
// Find any route that contains sub_path and use it to deduce a prefix. .or_insert(RouteDetail { methods, guards });
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()
});
// 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);
}
// Wait for all spawned tasks to complete.
for rx in completion_receivers {
let _ = rx.await;
}
// After all blocks, we expect the final registry to contain 14 entries.
let registered_resources = get_registered_resources();
assert_eq!(
registered_resources.len(),
14,
"Expected 14 registered resources, found: {:?}",
registered_resources
);
// List of expected resources
let expected_resources = vec![
ResourceIntrospection {
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(),
},
];
for exp in expected_resources {
assert!(
registered_resources.contains(&exp),
"Expected resource not found: {:?}",
exp
);
}
}
} }

View File

@ -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 = "resources-introspection")] #[cfg(feature = "experimental-introspection")]
pub mod introspection; pub mod introspection;
#[doc(inline)] #[doc(inline)]

View File

@ -191,8 +191,10 @@ 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_().as_str() == "image" => false, Some(mime) if mime.type_() == mime::IMAGE => {
Some(mime) if mime.type_().as_str() == "video" => false, matches!(mime.subtype(), mime::SVG)
}
Some(mime) if mime.type_() == mime::VIDEO => false,
_ => true, _ => true,
} }
} }

View File

@ -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}; use log::{debug, warn, Level};
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,6 +92,7 @@ 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 {
@ -102,6 +103,7 @@ 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,
})) }))
} }
@ -139,6 +141,23 @@ 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`.
@ -242,6 +261,7 @@ 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,
})) }))
} }
} }
@ -312,6 +332,7 @@ 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 {
@ -327,6 +348,7 @@ 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,
} }
} }
@ -344,6 +366,7 @@ 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>,
} }
} }
@ -390,6 +413,7 @@ 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,
@ -397,6 +421,7 @@ where
format, format,
size: 0, size: 0,
log_target, log_target,
log_level,
}))) })))
} }
} }
@ -409,6 +434,7 @@ 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> {
@ -421,8 +447,9 @@ pin_project! {
Ok(()) Ok(())
}; };
log::info!( log::log!(
target: this.log_target.as_ref(), target: this.log_target.as_ref(),
this.log_level,
"{}", FormatDisplay(&render) "{}", FormatDisplay(&render)
); );
} }
@ -622,9 +649,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) {
val.to_str().unwrap_or("-") String::from_utf8_lossy(val.as_bytes()).into_owned()
} else { } else {
"-" "-".to_owned()
}; };
*self = FormatText::Str(s.to_string()) *self = FormatText::Str(s.to_string())
} }
@ -666,11 +693,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) {
val.to_str().unwrap_or("-") String::from_utf8_lossy(val.as_bytes()).into_owned()
} else { } else {
"-" "-".to_owned()
}; };
*self = FormatText::Str(s.to_string()); *self = FormatText::Str(s);
} }
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() {

View File

@ -264,8 +264,10 @@ 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 called in unit tests unless [`TestRequest::peer_addr`] is used. /// Will only return `None` when server is listening on [UDS socket] or when called in unit
/// 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]

View File

@ -417,6 +417,8 @@ 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 {
@ -433,27 +435,28 @@ where
rdef.set_name(name); rdef.set_name(name);
} }
#[cfg(feature = "resources-introspection")] #[cfg(feature = "experimental-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")]
{ {
rmap.add(&mut rdef, None); let pat = rdef.pattern().unwrap_or("").to_string();
let mut methods = Vec::new();
self.routes.iter().for_each(|r| { let mut guard_names = Vec::new();
r.get_guards().iter().for_each(|g| { for route in &routes {
let http_methods: Vec<String> = if let Some(m) = route.get_method() {
crate::guard::HttpMethodsExtractor::extract_http_methods(&**g); if !methods.contains(&m) {
rdef_methods methods.push(m);
.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: self.routes, routes,
default: self.default, default: self.default,
}); });
@ -470,14 +473,6 @@ 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)
} }
} }

View File

@ -15,16 +15,13 @@ const AVG_PATH_LEN: usize = 24;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ResourceMap { pub struct ResourceMap {
pattern: ResourceDef, pub(crate) 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.
nodes: Option<Vec<Rc<ResourceMap>>>, pub(crate) nodes: Option<Vec<Rc<ResourceMap>>>,
} }
impl ResourceMap { impl ResourceMap {
@ -38,42 +35,6 @@ 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 {

View File

@ -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 = "resources-introspection")] #[cfg(feature = "experimental-introspection")]
pub(crate) fn get_guards(&self) -> &Vec<Box<dyn Guard>> { pub fn guard_names(&self) -> Vec<String> {
&self.guards self.guards.iter().map(|g| g.name()).collect()
} }
} }
@ -145,6 +145,23 @@ 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
@ -164,6 +181,10 @@ 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

View File

@ -395,9 +395,6 @@ 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,
@ -408,21 +405,38 @@ 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 = "resources-introspection")] #[cfg(feature = "experimental-introspection")]
{ {
let http_methods: Vec<String> = use std::borrow::Borrow;
guards.as_ref().map_or_else(Vec::new, |g| {
g.iter()
.flat_map(|g| {
crate::guard::HttpMethodsExtractor::extract_http_methods(
&**g,
)
})
.collect::<Vec<_>>()
});
rdef_methods // Get the pattern stored in ResourceMap
.push((rdef.pattern().unwrap_or_default().to_string(), http_methods)); let pat = rdef.pattern().unwrap_or("").to_string();
let guard_list: &[Box<dyn Guard>] =
guards.borrow().as_ref().map_or(&[], |v| &v[..]);
// 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<_>>();
// Extract guard names
let guard_names = guard_list
.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))
@ -452,14 +466,6 @@ 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),

View File

@ -1,6 +1,8 @@
use std::{ use std::{
any::Any, any::Any,
cmp, fmt, io, cmp, fmt,
future::Future,
io,
marker::PhantomData, marker::PhantomData,
net, net,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
@ -64,6 +66,7 @@ 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,
@ -272,19 +275,12 @@ 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>(self, f: CB) -> HttpServer<F, I, S, B> pub fn on_connect<CB>(mut 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,
{ {
HttpServer { self.on_connect_fn = Some(Arc::new(f));
factory: self.factory, self
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.
@ -312,6 +308,37 @@ 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.
@ -882,6 +909,7 @@ 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",
@ -986,6 +1014,7 @@ 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,
@ -1073,10 +1102,7 @@ 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::new( Err(io::Error::other("Could not bind to address"))
io::ErrorKind::Other,
"Can not bind to address.",
))
} }
} }

View File

@ -2,6 +2,10 @@
## 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.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "awc" name = "awc"
version = "3.6.0" version = "3.7.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.24.2", features = ["sync"] } tokio = { version = "1.38.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 = "7" brotli = "8"
const-str = "0.5" const-str = "0.5" # TODO(MSRV 1.77): update to 0.6
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.24.2", features = ["rt-multi-thread", "macros"] } tokio = { version = "1.38.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

View File

@ -5,9 +5,9 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc)
[![Documentation](https://docs.rs/awc/badge.svg?version=3.6.0)](https://docs.rs/awc/3.6.0) [![Documentation](https://docs.rs/awc/badge.svg?version=3.7.0)](https://docs.rs/awc/3.7.0)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc)
[![Dependency Status](https://deps.rs/crate/awc/3.6.0/status.svg)](https://deps.rs/crate/awc/3.6.0) [![Dependency Status](https://deps.rs/crate/awc/3.7.0/status.svg)](https://deps.rs/crate/awc/3.7.0)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->

View File

@ -179,9 +179,8 @@ where
.acquire_owned() .acquire_owned()
.await .await
.map_err(|_| { .map_err(|_| {
ConnectError::Io(io::Error::new( ConnectError::Io(io::Error::other(
io::ErrorKind::Other, "Failed to acquire semaphore on client connection pool",
"failed to acquire semaphore on client connection pool",
)) ))
})?; })?;

View File

@ -1,6 +1,8 @@
_list: _list:
@just --list @just --list
toolchain := ""
# Format workspace. # Format workspace.
fmt: fmt:
just --unstable --fmt just --unstable --fmt
@ -10,11 +12,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 update -p=clap --precise=4.4.18 # next ver: 1.74.0 cargo {{ toolchain }} update -p=divan --precise=0.1.15 # next ver: 1.80.0
cargo update -p=divan --precise=0.1.15 # next ver: 1.80.0 cargo {{ toolchain }} update -p=half --precise=2.4.1 # next ver: 1.81.0
cargo update -p=litemap --precise=0.7.4 # next ver: 1.81.0 cargo {{ toolchain }} update -p=idna_adapter --precise=1.2.0 # next ver: 1.82.0
cargo update -p=zerofrom --precise=0.1.5 # next ver: 1.81.0 cargo {{ toolchain }} update -p=litemap --precise=0.7.4 # next ver: 1.81.0
cargo update -p=half --precise=2.4.1 # next ver: 1.81.0 cargo {{ toolchain }} update -p=zerofrom --precise=0.1.5 # next ver: 1.81.0
msrv := ``` msrv := ```
cargo metadata --format-version=1 \ cargo metadata --format-version=1 \
@ -39,42 +41,50 @@ check-min:
check-default: check-default:
cargo hack --workspace check cargo hack --workspace check
# Run Clippy over workspace. # Check workspace.
check toolchain="": && (clippy toolchain) check: && clippy
fd --hidden --type=file --extension=md --extension=yml --exec-batch npx -y prettier --check
# Run Clippy over workspace. # Run Clippy over workspace.
clippy toolchain="": clippy:
cargo {{ toolchain }} clippy --workspace --all-targets {{ all_crate_features }} cargo {{ toolchain }} clippy --workspace --all-targets {{ all_crate_features }}
# Test workspace using MSRV. # Run Clippy over workspace using MSRV.
test-msrv: downgrade-for-msrv (test msrv_rustup) clippy-msrv:
@just toolchain={{ msrv_rustup }} downgrade-for-msrv
@just toolchain={{ msrv_rustup }} clippy
# Test workspace code. # Test workspace code.
test toolchain="": test:
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 toolchain="": && doc test-docs: && 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 toolchain="": (test toolchain) (test-docs toolchain) test-all: test test-docs
# Test workspace and collect coverage info. # Test workspace and collect coverage info.
[private] [private]
test-coverage toolchain="": test-coverage:
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 toolchain="": (test-coverage toolchain) test-coverage-codecov: test-coverage
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 toolchain="": (test-coverage toolchain) test-coverage-lcov: test-coverage
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.