Compare commits

...

15 Commits

Author SHA1 Message Date
Manuel Garcia de la Vega Hoyo dd2eef608f
Merge 5ad04e6de7 into 98d7d0b46b 2025-08-29 22:48:11 +01:00
Rob Ede 98d7d0b46b
chore(actix-files): prepare release 0.6.7 2025-08-29 22:31:48 +01:00
Rob Ede 4966a54e05
refactor(files): rename read_mode_threshold fn 2025-08-29 22:30:47 +01:00
Andrew Scott 00b0f8f700
feat(actix-files): opt-in filesize threshold for faster synchronous reads (#3706)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-08-29 21:52:34 +01:00
励志买套上海苏河湾大平层 3c2907da41
docs(middleware): complete middleware author's guide (#3680)
Add comprehensive documentation for middleware development in Actix Web, including:
- Detailed explanation of middleware concepts and execution flow
- Clear description of middleware traits and their responsibilities
- Guidelines for body type handling
- Best practices for middleware development
- Error handling recommendations
- Usage scenarios and anti-patterns

Co-authored-by: chenjjiaa <chenjjiaaa@gmail.com>
2025-08-29 20:12:05 +00:00
George Pollard 5041cd1c65
Make 'ws' feature of actix-http optional in actix-web (#3734)
* Make 'ws' feature of actix-http optional

* Update CHANGES.md

* Update actix-web-actors

* Update CHANGES.md

* nits

* nits

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-08-29 02:50:05 +00:00
Thales d3c46537b3
fix(http): Wake Payload when feeding error or EOF (#3749)
* fix(http): Add failing tests to demonstrate the payload problem

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>

* fix(http): Wake Payload when feeding error or eof

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>

---------

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>
2025-08-29 02:47:03 +00:00
Manuel Garcia de la Vega Hoyo 5ad04e6de7
Merge branch 'master' into feature/mgdlvh/capture_match_info 2025-05-24 13:35:01 +02:00
manuelgdlvh 6f4bdb31f9 feat: Add code formatting 2025-05-16 22:27:19 +02:00
manuelgdlvh 4e56ae0594 feat: Add minor refactos and changelog 2025-05-16 21:46:38 +02:00
manuelgdlvh 9f2b207137 feat: Add fix to visibility warning and apply code format 2025-05-14 21:34:50 +02:00
manuelgdlvh b47530d66a feat: Add tests for max path conflicts modification 2025-05-14 21:22:09 +02:00
manuelgdlvh 55c38a625f feat: Move resolve path to Resource trait as default implementation 2025-05-14 20:53:22 +02:00
manuelgdlvh 934f68ebf6 feat: Add router bench for guard check failures 2025-05-14 20:41:45 +02:00
manuelgdlvh ffa95b7f60 feat: Add improvements in resource path resolve 2025-05-14 20:26:28 +02:00
27 changed files with 553 additions and 180 deletions

View File

@ -9,4 +9,5 @@ words:
- rustls
- rustup
- serde
- uring
- zstd

31
Cargo.lock generated
View File

@ -44,7 +44,7 @@ dependencies = [
[[package]]
name = "actix-files"
version = "0.6.6"
version = "0.6.7"
dependencies = [
"actix-http",
"actix-rt",
@ -891,18 +891,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.45"
version = "4.5.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318"
checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.44"
version = "4.5.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8"
checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41"
dependencies = [
"anstyle",
"clap_lex",
@ -1482,7 +1482,7 @@ dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
"wasi 0.14.3+wasi-0.2.4",
]
[[package]]
@ -2305,9 +2305,9 @@ dependencies = [
[[package]]
name = "potential_utf"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
dependencies = [
"zerovec",
]
@ -3461,11 +3461,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
version = "0.14.3+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95"
dependencies = [
"wit-bindgen-rt",
"wit-bindgen",
]
[[package]]
@ -3873,13 +3873,10 @@ dependencies = [
]
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
name = "wit-bindgen"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.3",
]
checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814"
[[package]]
name = "writeable"

View File

@ -2,6 +2,9 @@
## Unreleased
## 0.6.7
- Add `{Files, NamedFile}::read_mode_threshold()` methods to allow faster synchronous reads of small files.
- Minimum supported Rust version (MSRV) is now 1.75.
## 0.6.6

View File

@ -1,6 +1,6 @@
[package]
name = "actix-files"
version = "0.6.6"
version = "0.6.7"
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
description = "Static file serving for Actix Web"
keywords = ["actix", "http", "async", "futures"]

View File

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

View File

@ -14,6 +14,12 @@ use pin_project_lite::pin_project;
use super::named::File;
#[derive(Debug, Clone, Copy)]
pub(crate) enum ReadMode {
Sync,
Async,
}
pin_project! {
/// Adapter to read a `std::file::File` in chunks.
#[doc(hidden)]
@ -24,6 +30,7 @@ pin_project! {
state: ChunkedReadFileState<Fut>,
counter: u64,
callback: F,
read_mode: ReadMode,
}
}
@ -57,6 +64,7 @@ pub(crate) fn new_chunked_read(
size: u64,
offset: u64,
file: File,
read_mode_threshold: u64,
) -> impl Stream<Item = Result<Bytes, Error>> {
ChunkedReadFile {
size,
@ -69,31 +77,50 @@ pub(crate) fn new_chunked_read(
},
counter: 0,
callback: chunked_read_file_callback,
read_mode: if size < read_mode_threshold {
ReadMode::Sync
} else {
ReadMode::Async
},
}
}
#[cfg(not(feature = "experimental-io-uring"))]
async fn chunked_read_file_callback(
fn chunked_read_file_callback_sync(
mut file: File,
offset: u64,
max_bytes: usize,
) -> Result<(File, Bytes), Error> {
) -> Result<(File, Bytes), io::Error> {
use io::{Read as _, Seek as _};
let res = actix_web::web::block(move || {
let mut buf = Vec::with_capacity(max_bytes);
let mut buf = Vec::with_capacity(max_bytes);
file.seek(io::SeekFrom::Start(offset))?;
file.seek(io::SeekFrom::Start(offset))?;
let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
if n_bytes == 0 {
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
} else {
Ok((file, Bytes::from(buf)))
if n_bytes == 0 {
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
} else {
Ok((file, Bytes::from(buf)))
}
}
#[cfg(not(feature = "experimental-io-uring"))]
#[inline]
async fn chunked_read_file_callback(
file: File,
offset: u64,
max_bytes: usize,
read_mode: ReadMode,
) -> Result<(File, Bytes), Error> {
let res = match read_mode {
ReadMode::Sync => chunked_read_file_callback_sync(file, offset, max_bytes)?,
ReadMode::Async => {
actix_web::web::block(move || chunked_read_file_callback_sync(file, offset, max_bytes))
.await??
}
})
.await??;
};
Ok(res)
}
@ -171,7 +198,7 @@ where
#[cfg(not(feature = "experimental-io-uring"))]
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
where
F: Fn(File, u64, usize) -> Fut,
F: Fn(File, u64, usize, ReadMode) -> Fut,
Fut: Future<Output = Result<(File, Bytes), Error>>,
{
type Item = Result<Bytes, Error>;
@ -193,7 +220,7 @@ where
.take()
.expect("ChunkedReadFile polled after completion");
let fut = (this.callback)(file, offset, max_bytes);
let fut = (this.callback)(file, offset, max_bytes, *this.read_mode);
this.state
.project_replace(ChunkedReadFileState::Future { fut });

View File

@ -49,6 +49,7 @@ pub struct Files {
use_guards: Option<Rc<dyn Guard>>,
guards: Vec<Rc<dyn Guard>>,
hidden_files: bool,
read_mode_threshold: u64,
}
impl fmt::Debug for Files {
@ -73,6 +74,7 @@ impl Clone for Files {
use_guards: self.use_guards.clone(),
guards: self.guards.clone(),
hidden_files: self.hidden_files,
read_mode_threshold: self.read_mode_threshold,
}
}
}
@ -119,6 +121,7 @@ impl Files {
use_guards: None,
guards: Vec::new(),
hidden_files: false,
read_mode_threshold: 0,
}
}
@ -204,6 +207,23 @@ impl Files {
self
}
/// Sets the size threshold that determines file read mode (sync/async).
///
/// When a file is smaller than the threshold (bytes), the reader will switch from synchronous
/// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large
/// files.
///
/// Tweaking this value according to your expected usage may lead to signifiant performance
/// gains (or losses in other handlers, if `size` is too high).
///
/// When the `experimental-io-uring` crate feature is enabled, file reads are always async.
///
/// Default is 0, meaning all files are read asynchronously.
pub fn read_mode_threshold(mut self, size: u64) -> Self {
self.read_mode_threshold = size;
self
}
/// Specifies whether to use ETag or not.
///
/// Default is true.
@ -367,6 +387,7 @@ impl ServiceFactory<ServiceRequest> for Files {
file_flags: self.file_flags,
guards: self.use_guards.clone(),
hidden_files: self.hidden_files,
size_threshold: self.read_mode_threshold,
};
if let Some(ref default) = *self.default.borrow() {

View File

@ -80,6 +80,7 @@ pub struct NamedFile {
pub(crate) content_type: Mime,
pub(crate) content_disposition: ContentDisposition,
pub(crate) encoding: Option<ContentEncoding>,
pub(crate) read_mode_threshold: u64,
}
#[cfg(not(feature = "experimental-io-uring"))]
@ -200,6 +201,7 @@ impl NamedFile {
encoding,
status_code: StatusCode::OK,
flags: Flags::default(),
read_mode_threshold: 0,
})
}
@ -353,6 +355,23 @@ impl NamedFile {
self
}
/// Sets the size threshold that determines file read mode (sync/async).
///
/// When a file is smaller than the threshold (bytes), the reader will switch from synchronous
/// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large
/// files.
///
/// Tweaking this value according to your expected usage may lead to signifiant performance
/// gains (or losses in other handlers, if `size` is too high).
///
/// When the `experimental-io-uring` crate feature is enabled, file reads are always async.
///
/// Default is 0, meaning all files are read asynchronously.
pub fn read_mode_threshold(mut self, size: u64) -> Self {
self.read_mode_threshold = size;
self
}
/// Specifies whether to return `ETag` header in response.
///
/// Default is true.
@ -440,7 +459,8 @@ impl NamedFile {
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
}
let reader = chunked::new_chunked_read(self.md.len(), 0, self.file);
let reader =
chunked::new_chunked_read(self.md.len(), 0, self.file, self.read_mode_threshold);
return res.streaming(reader);
}
@ -577,7 +597,7 @@ impl NamedFile {
.map_into_boxed_body();
}
let reader = chunked::new_chunked_read(length, offset, self.file);
let reader = chunked::new_chunked_read(length, offset, self.file, self.read_mode_threshold);
if offset != 0 || length != self.md.len() {
res.status(StatusCode::PARTIAL_CONTENT);

View File

@ -39,6 +39,7 @@ pub struct FilesServiceInner {
pub(crate) file_flags: named::Flags,
pub(crate) guards: Option<Rc<dyn Guard>>,
pub(crate) hidden_files: bool,
pub(crate) size_threshold: u64,
}
impl fmt::Debug for FilesServiceInner {
@ -70,7 +71,9 @@ impl FilesService {
named_file.flags = self.file_flags;
let (req, _) = req.into_parts();
let res = named_file.into_response(&req);
let res = named_file
.read_mode_threshold(self.size_threshold)
.into_response(&req);
ServiceResponse::new(req, res)
}
@ -169,17 +172,7 @@ impl Service<ServiceRequest> for FilesService {
}
} else {
match NamedFile::open_async(&path).await {
Ok(mut named_file) => {
if let Some(ref mime_override) = this.mime_override {
let new_disposition = mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition;
}
named_file.flags = this.file_flags;
let (req, _) = req.into_parts();
let res = named_file.into_response(&req);
Ok(ServiceResponse::new(req, res))
}
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
Err(err) => this.handle_err(err, req).await,
}
}

View File

@ -2,6 +2,8 @@
## Unreleased
- Properly wake Payload receivers when feeding errors or EOF
## 3.11.1
- Prevent more hangs after client disconnects.

View File

@ -156,7 +156,7 @@ serde_json = "1.0"
static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.55" }
tls-rustls_023 = { package = "rustls", version = "0.23" }
tokio = { version = "1.38.2", features = ["net", "rt", "macros"] }
tokio = { version = "1.38.2", features = ["net", "rt", "macros", "sync"] }
[lints]
workspace = true

View File

@ -200,11 +200,13 @@ impl Inner {
#[inline]
fn set_error(&mut self, err: PayloadError) {
self.err = Some(err);
self.wake();
}
#[inline]
fn feed_eof(&mut self) {
self.eof = true;
self.wake();
}
#[inline]
@ -253,8 +255,13 @@ impl Inner {
#[cfg(test)]
mod tests {
use std::{task::Poll, time::Duration};
use actix_rt::time::timeout;
use actix_utils::future::poll_fn;
use futures_util::{FutureExt, StreamExt};
use static_assertions::{assert_impl_all, assert_not_impl_any};
use tokio::sync::oneshot;
use super::*;
@ -263,6 +270,67 @@ mod tests {
assert_impl_all!(Inner: Unpin, Send, Sync);
const WAKE_TIMEOUT: Duration = Duration::from_secs(2);
fn prepare_waking_test(
mut payload: Payload,
expected: Option<Result<(), ()>>,
) -> (oneshot::Receiver<()>, actix_rt::task::JoinHandle<()>) {
let (tx, rx) = oneshot::channel();
let handle = actix_rt::spawn(async move {
// Make sure to poll once to set the waker
poll_fn(|cx| {
assert!(payload.poll_next_unpin(cx).is_pending());
Poll::Ready(())
})
.await;
tx.send(()).unwrap();
// actix-rt is single-threaded, so this won't race with `rx.await`
let mut pend_once = false;
poll_fn(|_| {
if pend_once {
Poll::Ready(())
} else {
// Return pending without storing wakers, we already did on the previous
// `poll_fn`, now this task will only continue if the `sender` wakes us
pend_once = true;
Poll::Pending
}
})
.await;
let got = payload.next().now_or_never().unwrap();
match expected {
Some(Ok(_)) => assert!(got.unwrap().is_ok()),
Some(Err(_)) => assert!(got.unwrap().is_err()),
None => assert!(got.is_none()),
}
});
(rx, handle)
}
#[actix_rt::test]
async fn wake_on_error() {
let (mut sender, payload) = Payload::create(false);
let (rx, handle) = prepare_waking_test(payload, Some(Err(())));
rx.await.unwrap();
sender.set_error(PayloadError::Incomplete(None));
timeout(WAKE_TIMEOUT, handle).await.unwrap().unwrap();
}
#[actix_rt::test]
async fn wake_on_eof() {
let (mut sender, payload) = Payload::create(false);
let (rx, handle) = prepare_waking_test(payload, None);
rx.await.unwrap();
sender.feed_eof();
timeout(WAKE_TIMEOUT, handle).await.unwrap().unwrap();
}
#[actix_rt::test]
async fn test_unread_data() {
let (_, mut payload) = Payload::create(false);

View File

@ -2,6 +2,14 @@
## Unreleased
### Added
- Add conflict path detection and handling to enhance routing performance.
### Changed
- Refactor capture_match_info_fn by splitting it into three distinct functions: capture_match_info(), resolve_path_if_match(), and resolve().
## 0.5.3
- Add `unicode` crate feature (on-by-default) to switch between `regex` and `regex-lite` as a trade-off between full unicode support and binary size.

View File

@ -180,6 +180,15 @@ fn compare_routers(c: &mut Criterion) {
});
});
group.bench_function("actix_guard_failures", |b| {
b.iter(|| {
for route in call() {
let mut path = actix_router::Path::new(route);
black_box(actix.recognize_fn(&mut path, |_, _| false));
}
});
});
let regex_set = regex::RegexSet::new(register!(regex)).unwrap();
group.bench_function("regex", |b| {
b.iter(|| {

View File

@ -662,13 +662,13 @@ mod tests {
let rdef = ResourceDef::new("/{key}");
let mut path = Path::new("/%25");
rdef.capture_match_info(&mut path);
rdef.resolve_path_if_match(&mut path);
let de = PathDeserializer::new(&path);
let segment: String = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(segment, "%");
let mut path = Path::new("/%2F");
rdef.capture_match_info(&mut path);
rdef.resolve_path_if_match(&mut path);
let de = PathDeserializer::new(&path);
let segment: String = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(segment, "/")
@ -679,7 +679,7 @@ mod tests {
let rdef = ResourceDef::new("/{key}/{value}");
let mut path = Path::new("/%30%25/%30%2F");
rdef.capture_match_info(&mut path);
rdef.resolve_path_if_match(&mut path);
let de = PathDeserializer::new(&path);
let segment: (String, String) = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(segment.0, "0%");
@ -697,7 +697,7 @@ mod tests {
let rdef = ResourceDef::new("/{key}/{value}");
let mut path = Path::new("/%25/%2F");
rdef.capture_match_info(&mut path);
rdef.resolve_path_if_match(&mut path);
let de = PathDeserializer::new(&path);
let vals: Vals = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(vals.key, "%");
@ -714,7 +714,7 @@ mod tests {
let rdef = ResourceDef::new("/{val}");
let mut path = Path::new("/X");
rdef.capture_match_info(&mut path);
rdef.resolve_path_if_match(&mut path);
let de = PathDeserializer::new(&path);
let params: Params<'_> = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(params.val, "X");
@ -723,7 +723,7 @@ mod tests {
assert_eq!(params, "X");
let mut path = Path::new("/%2F");
rdef.capture_match_info(&mut path);
rdef.resolve_path_if_match(&mut path);
let de = PathDeserializer::new(&path);
assert!(<Params<'_> as serde::Deserialize>::deserialize(de).is_err());
let de = PathDeserializer::new(&path);

View File

@ -1,14 +1,15 @@
use std::{
borrow::Cow,
mem,
ops::{DerefMut, Index},
};
use serde::{de, Deserialize};
use crate::{de::PathDeserializer, Resource, ResourcePath};
use crate::{de::PathDeserializer, resource::ResourceMatchInfo, Resource, ResourcePath};
#[derive(Debug, Clone)]
pub(crate) enum PathItem {
pub enum PathItem {
Static(Cow<'static, str>),
Segment(u16, u16),
}
@ -106,6 +107,27 @@ impl<T: ResourcePath> Path<T> {
self.skip += n;
}
/// Post-processes the path to resolve dynamic segments, if any, and determines the character offset to skip.
pub fn resolve(&mut self, match_info: ResourceMatchInfo<'_>) {
match match_info {
ResourceMatchInfo::Static { matched_len } => {
self.resource_path().skip(matched_len);
}
ResourceMatchInfo::Dynamic {
matched_len,
matched_vars,
mut segments,
} => {
for i in 0..matched_vars.len() {
self.resource_path()
.add(matched_vars[i], mem::take(&mut segments[i]));
}
self.resource_path().skip(matched_len);
}
}
}
pub(crate) fn add(&mut self, name: impl Into<Cow<'static, str>>, value: PathItem) {
match value {
PathItem::Static(seg) => self.segments.push((name.into(), PathItem::Static(seg))),
@ -260,4 +282,49 @@ mod tests {
let foo = RefCell::new(foo);
let _ = foo.borrow_mut().resource_path();
}
#[test]
fn test_dynamic_path_resolve() {
let mut path = Path::new("/foo/{var1}/{var2}");
assert_eq!(0, path.segments.len());
assert_eq!(0, path.skip);
let mut segments = <[PathItem; 16]>::default();
segments[0] = PathItem::Static(Cow::Borrowed("foo"));
segments[1] = PathItem::Segment(2, 5);
let match_info = ResourceMatchInfo::Dynamic {
matched_len: 3,
matched_vars: &["var1", "var2"],
segments,
};
path.resolve(match_info);
assert_eq!(2, path.segments.len());
assert_eq!(3, path.skip);
let (name, value) = path.segments.get(0).unwrap();
assert_eq!(name.as_ref(), "var1");
assert!(matches!(value, PathItem::Static(Cow::Borrowed("foo"))));
let (name, value) = path.segments.get(1).unwrap();
assert_eq!(name.as_ref(), "var2");
assert!(matches!(value, PathItem::Segment(2, 5)));
}
#[test]
fn test_static_path_resolve() {
let mut path = Path::new("/foo");
assert_eq!(0, path.segments.len());
assert_eq!(0, path.skip);
let match_info = ResourceMatchInfo::Static { matched_len: 2 };
path.resolve(match_info);
assert_eq!(0, path.segments.len());
assert_eq!(2, path.skip);
}
}

View File

@ -2,7 +2,6 @@ use std::{
borrow::{Borrow, Cow},
collections::HashMap,
hash::{BuildHasher, Hash, Hasher},
mem,
};
use tracing::error;
@ -10,7 +9,7 @@ use tracing::error;
use crate::{
path::PathItem,
regex_set::{escape, Regex, RegexSet},
IntoPatterns, Patterns, Resource, ResourcePath,
IntoPatterns, Patterns, Resource,
};
const MAX_DYNAMIC_SEGMENTS: usize = 16;
@ -80,8 +79,7 @@ const REGEX_FLAGS: &str = "(?s-m)";
/// `/rust-is-hard`.
///
/// For information on capturing segment values from paths or other custom resource types,
/// see [`capture_match_info`][Self::capture_match_info]
/// and [`capture_match_info_fn`][Self::capture_match_info_fn].
/// see [`capture_match_info`][Self::capture_match_info].
///
/// A resource definition can contain at most 16 dynamic segments.
///
@ -96,7 +94,7 @@ const REGEX_FLAGS: &str = "(?s-m)";
/// assert!(!resource.is_match("/user/"));
///
/// let mut path = Path::new("/user/123");
/// resource.capture_match_info(&mut path);
/// resource.resolve_path_if_match(&mut path);
/// assert_eq!(path.get("id").unwrap(), "123");
/// ```
///
@ -171,7 +169,7 @@ const REGEX_FLAGS: &str = "(?s-m)";
/// assert!(resource.is_match("/blob/HEAD/README.md"));
///
/// let mut path = Path::new("/blob/main/LICENSE");
/// resource.capture_match_info(&mut path);
/// resource.resolve_path_if_match(&mut path);
/// assert_eq!(path.get("tail").unwrap(), "main/LICENSE");
/// ```
///
@ -249,6 +247,18 @@ enum PatternType {
DynamicSet(RegexSet, Vec<(Regex, Vec<&'static str>)>),
}
/// Holds metadata and parameters used during path resolution.
pub enum ResourceMatchInfo<'a> {
Static {
matched_len: u16,
},
Dynamic {
matched_len: u16,
matched_vars: &'a [&'static str],
segments: [PathItem; MAX_DYNAMIC_SEGMENTS],
},
}
impl ResourceDef {
/// Constructs a new resource definition from patterns.
///
@ -623,18 +633,24 @@ impl ResourceDef {
///
/// let resource = ResourceDef::prefix("/user/{id}");
/// let mut path = Path::new("/user/123/stars");
/// assert!(resource.capture_match_info(&mut path));
/// assert!(resource.resolve_path_if_match(&mut path));
/// assert_eq!(path.get("id").unwrap(), "123");
/// assert_eq!(path.unprocessed(), "/stars");
///
/// let resource = ResourceDef::new("/blob/{path}*");
/// let mut path = Path::new("/blob/HEAD/Cargo.toml");
/// assert!(resource.capture_match_info(&mut path));
/// assert!(resource.resolve_path_if_match(&mut path));
/// assert_eq!(path.get("path").unwrap(), "HEAD/Cargo.toml");
/// assert_eq!(path.unprocessed(), "");
/// ```
pub fn capture_match_info<R: Resource>(&self, resource: &mut R) -> bool {
self.capture_match_info_fn(resource, |_| true)
pub fn resolve_path_if_match<R: Resource>(&self, resource: &mut R) -> bool {
match self.capture_match_info(resource) {
None => false,
Some(match_info) => {
resource.resource_path().resolve(match_info);
true
}
}
}
/// Collects dynamic segment values into `resource` after matching paths and executing
@ -644,21 +660,22 @@ impl ResourceDef {
/// This is useful if you want to conditionally match on some non-path related aspect of the
/// resource type.
///
/// Returns `true` if resource path matches this resource definition _and_ satisfies the
/// given check function.
///
/// Returns `ResourceMatchInfo` if the given resource path matches this resource definition,
/// containing the information required to perform path resolution.
/// # Examples
/// ```
/// use actix_router::{Path, ResourceDef};
/// use actix_router::{Path, Resource, ResourceDef};
///
/// fn try_match(resource: &ResourceDef, path: &mut Path<&str>) -> bool {
/// let admin_allowed = std::env::var("ADMIN_ALLOWED").is_ok();
///
/// resource.capture_match_info_fn(
/// path,
/// // when env var is not set, reject when path contains "admin"
/// |path| !(!admin_allowed && path.as_str().contains("admin")),
/// )
/// let match_info = resource.capture_match_info(path);
/// match match_info{
/// None => {false}
/// Some(match_info) => {
/// path.resource_path().resolve(match_info);
/// true
/// }
/// }
/// }
///
/// let resource = ResourceDef::prefix("/user/{id}");
@ -669,85 +686,72 @@ impl ResourceDef {
/// assert_eq!(path.get("id").unwrap(), "james");
/// assert_eq!(path.unprocessed(), "/stars");
///
/// // path matches but fails check function; no segments are collected
/// let mut path = Path::new("/user/admin/stars");
/// assert!(!try_match(&resource, &mut path));
/// assert_eq!(path.unprocessed(), "/user/admin/stars");
/// ```
pub fn capture_match_info_fn<R, F>(&self, resource: &mut R, check_fn: F) -> bool
pub fn capture_match_info<R>(&self, resource: &mut R) -> Option<ResourceMatchInfo<'_>>
where
R: Resource,
F: FnOnce(&R) -> bool,
{
let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default();
let path = resource.resource_path();
let path_str = path.unprocessed();
let (matched_len, matched_vars) = match &self.pat_type {
match &self.pat_type {
PatternType::Static(pattern) => match self.static_match(pattern, path_str) {
Some(len) => (len, None),
None => return false,
Some(len) => Some(ResourceMatchInfo::Static {
matched_len: len as u16,
}),
None => return None,
},
PatternType::Dynamic(re, names) => {
let captures = match re.captures(path.unprocessed()) {
let captures = match re.captures(path_str) {
Some(captures) => captures,
_ => return false,
_ => return None,
};
let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default();
for (no, name) in names.iter().enumerate() {
if let Some(m) = captures.name(name) {
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
} else {
error!("Dynamic path match but not all segments found: {}", name);
return false;
return None;
}
}
(captures[1].len(), Some(names))
Some(ResourceMatchInfo::Dynamic {
matched_len: captures[1].len() as u16,
matched_vars: names,
segments,
})
}
PatternType::DynamicSet(re, params) => {
let path = path.unprocessed();
let (pattern, names) = match re.first_match_idx(path) {
let (pattern, names) = match re.first_match_idx(path_str) {
Some(idx) => &params[idx],
_ => return false,
_ => return None,
};
let captures = match pattern.captures(path.path()) {
let captures = match pattern.captures(path_str) {
Some(captures) => captures,
_ => return false,
_ => return None,
};
let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default();
for (no, name) in names.iter().enumerate() {
if let Some(m) = captures.name(name) {
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
} else {
error!("Dynamic path match but not all segments found: {}", name);
return false;
return None;
}
}
(captures[1].len(), Some(names))
}
};
if !check_fn(resource) {
return false;
}
// Modify `path` to skip matched part and store matched segments
let path = resource.resource_path();
if let Some(vars) = matched_vars {
for i in 0..vars.len() {
path.add(vars[i], mem::take(&mut segments[i]));
Some(ResourceMatchInfo::Dynamic {
matched_len: captures[1].len() as u16,
matched_vars: names,
segments,
})
}
}
path.skip(matched_len as u16);
true
}
/// Assembles resource path using a closure that maps variable segment names to values.
@ -1171,7 +1175,7 @@ mod tests {
assert!(!re.is_match("/name~"));
let mut path = Path::new("/name");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.unprocessed(), "");
assert_eq!(re.find_match("/name"), Some(5));
@ -1189,7 +1193,7 @@ mod tests {
assert!(!re.is_match("/user/profile/profile"));
let mut path = Path::new("/user/profile");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.unprocessed(), "");
}
@ -1202,12 +1206,12 @@ mod tests {
assert!(!re.is_match("/user/2345/sdg"));
let mut path = Path::new("/user/profile");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "profile");
assert_eq!(path.unprocessed(), "");
let mut path = Path::new("/user/1245125");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "1245125");
assert_eq!(path.unprocessed(), "");
@ -1217,7 +1221,7 @@ mod tests {
assert!(!re.is_match("/resource"));
let mut path = Path::new("/v151/resource/adage32");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("version").unwrap(), "151");
assert_eq!(path.get("id").unwrap(), "adage32");
assert_eq!(path.unprocessed(), "");
@ -1229,7 +1233,7 @@ mod tests {
assert!(!re.is_match("/XXXXXX"));
let mut path = Path::new("/012345");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "012345");
assert_eq!(path.unprocessed(), "");
}
@ -1249,12 +1253,12 @@ mod tests {
assert!(!re.is_match("/user/2345/sdg"));
let mut path = Path::new("/user/profile");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "profile");
assert_eq!(path.unprocessed(), "");
let mut path = Path::new("/user/1245125");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "1245125");
assert_eq!(path.unprocessed(), "");
@ -1263,7 +1267,7 @@ mod tests {
assert!(!re.is_match("/resource"));
let mut path = Path::new("/v151/resource/adage32");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("version").unwrap(), "151");
assert_eq!(path.get("id").unwrap(), "adage32");
@ -1277,7 +1281,7 @@ mod tests {
assert!(!re.is_match("/static/a"));
let mut path = Path::new("/012345");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "012345");
let re = ResourceDef::new([
@ -1314,7 +1318,7 @@ mod tests {
assert_eq!(re.find_match("/12345"), None);
let mut path = Path::new("/151/res");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "151");
assert_eq!(path.unprocessed(), "/res");
}
@ -1324,19 +1328,19 @@ mod tests {
let re = ResourceDef::new("/user/-{id}*");
let mut path = Path::new("/user/-profile");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "profile");
let mut path = Path::new("/user/-2345");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "2345");
let mut path = Path::new("/user/-2345/");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "2345/");
let mut path = Path::new("/user/-2345/sdg");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "2345/sdg");
}
@ -1364,7 +1368,7 @@ mod tests {
let re = ResourceDef::new("/user/{id}/{tail}*");
assert!(!re.is_match("/user/2345"));
let mut path = Path::new("/user/2345/sdg");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "2345");
assert_eq!(path.get("tail").unwrap(), "sdg");
assert_eq!(path.unprocessed(), "");
@ -1379,7 +1383,7 @@ mod tests {
let re = ResourceDef::new("/a{x}b/test/a{y}b");
let mut path = Path::new("/a\nb/test/a\nb");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("x").unwrap(), "\n");
assert_eq!(path.get("y").unwrap(), "\n");
@ -1388,12 +1392,12 @@ mod tests {
let re = ResourceDef::new("/user/{id}*");
let mut path = Path::new("/user/a\nb/a\nb");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "a\nb/a\nb");
let re = ResourceDef::new("/user/{id:.*}");
let mut path = Path::new("/user/a\nb/a\nb");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "a\nb/a\nb");
}
@ -1403,16 +1407,16 @@ mod tests {
let re = ResourceDef::new("/user/{id}/test");
let mut path = Path::new("/user/2345/test");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "2345");
let mut path = Path::new("/user/qwe%25/test");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "qwe%25");
let uri = http::Uri::try_from("/user/qwe%25/test").unwrap();
let mut path = Path::new(uri);
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.get("id").unwrap(), "qwe%25");
}
@ -1429,11 +1433,11 @@ mod tests {
assert!(!re.is_match("/name~"));
let mut path = Path::new("/name");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.unprocessed(), "");
let mut path = Path::new("/name/test");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.unprocessed(), "/test");
assert_eq!(re.find_match("/name"), Some(5));
@ -1449,10 +1453,10 @@ mod tests {
assert!(!re.is_match("/name"));
let mut path = Path::new("/name/gs");
assert!(!re.capture_match_info(&mut path));
assert!(!re.resolve_path_if_match(&mut path));
let mut path = Path::new("/name//gs");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(path.unprocessed(), "/gs");
let re = ResourceDef::root_prefix("name/");
@ -1462,7 +1466,7 @@ mod tests {
assert!(!re.is_match("/name"));
let mut path = Path::new("/name/gs");
assert!(!re.capture_match_info(&mut path));
assert!(!re.resolve_path_if_match(&mut path));
}
#[test]
@ -1481,13 +1485,13 @@ mod tests {
assert_eq!(re.find_match(""), None);
let mut path = Path::new("/test2/");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(&path["name"], "test2");
assert_eq!(&path[0], "test2");
assert_eq!(path.unprocessed(), "/");
let mut path = Path::new("/test2/subpath1/subpath2/index.html");
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
assert_eq!(&path["name"], "test2");
assert_eq!(&path[0], "test2");
assert_eq!(path.unprocessed(), "/subpath1/subpath2/index.html");
@ -1543,7 +1547,7 @@ mod tests {
assert!(resource.resource_path_from_iter(
&mut s,
#[allow(clippy::useless_vec)]
&mut vec!["item", "item2"].iter()
&mut vec!["item", "item2"].iter(),
));
assert_eq!(s, "/user/item/item2/");
}
@ -1561,22 +1565,22 @@ mod tests {
let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]);
let mut path = Path::new("/user/123");
assert!(resource.capture_match_info(&mut path));
assert!(resource.resolve_path_if_match(&mut path));
assert!(path.get("id").is_some());
let mut path = Path::new("/profile/123");
assert!(resource.capture_match_info(&mut path));
assert!(resource.resolve_path_if_match(&mut path));
assert!(path.get("id").is_some());
let resource = ResourceDef::new(["/user/{id}", "/profile/{uid}"]);
let mut path = Path::new("/user/123");
assert!(resource.capture_match_info(&mut path));
assert!(resource.resolve_path_if_match(&mut path));
assert!(path.get("id").is_some());
assert!(path.get("uid").is_none());
let mut path = Path::new("/profile/123");
assert!(resource.capture_match_info(&mut path));
assert!(resource.resolve_path_if_match(&mut path));
assert!(path.get("id").is_none());
assert!(path.get("uid").is_some());
}

View File

@ -13,12 +13,16 @@ pub struct ResourceId(pub u16);
/// not required.
pub struct Router<T, U = ()> {
routes: Vec<(ResourceDef, T, U)>,
max_path_conflicts: u16,
}
impl<T, U> Router<T, U> {
/// Constructs new `RouterBuilder` with empty route list.
pub fn build() -> RouterBuilder<T, U> {
RouterBuilder { routes: Vec::new() }
RouterBuilder {
routes: Vec::new(),
path_conflicts: vec![],
}
}
/// Finds the value in the router that matches a given [routing resource](Resource).
@ -46,14 +50,24 @@ impl<T, U> Router<T, U> {
/// the `check` closure is executed, passing the resource and each route's context data. If the
/// closure returns true then the match result is stored into `resource` and a reference to
/// the matched _value_ is returned.
pub fn recognize_fn<R, F>(&self, resource: &mut R, mut check: F) -> Option<(&T, ResourceId)>
pub fn recognize_fn<R, F>(&self, resource: &mut R, mut check_fn: F) -> Option<(&T, ResourceId)>
where
R: Resource,
F: FnMut(&R, &U) -> bool,
{
let mut next_resource_match_count = 1;
for (rdef, val, ctx) in self.routes.iter() {
if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) {
return Some((val, ResourceId(rdef.id())));
match rdef.capture_match_info(resource) {
None => {}
Some(match_info) => {
if check_fn(resource, ctx) {
resource.resource_path().resolve(match_info);
return Some((val, ResourceId(rdef.id())));
} else if next_resource_match_count == self.max_path_conflicts {
return None;
}
next_resource_match_count += 1;
}
}
}
@ -65,15 +79,25 @@ impl<T, U> Router<T, U> {
pub fn recognize_mut_fn<R, F>(
&mut self,
resource: &mut R,
mut check: F,
mut check_fn: F,
) -> Option<(&mut T, ResourceId)>
where
R: Resource,
F: FnMut(&R, &U) -> bool,
{
let mut matches = 0;
for (rdef, val, ctx) in self.routes.iter_mut() {
if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) {
return Some((val, ResourceId(rdef.id())));
match rdef.capture_match_info(resource) {
None => {}
Some(match_info) => {
matches += 1;
if check_fn(resource, ctx) {
resource.resource_path().resolve(match_info);
return Some((val, ResourceId(rdef.id())));
} else if matches == self.max_path_conflicts {
return None;
}
}
}
}
@ -84,6 +108,7 @@ impl<T, U> Router<T, U> {
/// Builder for an ordered [routing](Router) list.
pub struct RouterBuilder<T, U = ()> {
routes: Vec<(ResourceDef, T, U)>,
path_conflicts: Vec<(usize, u16)>,
}
impl<T, U> RouterBuilder<T, U> {
@ -96,7 +121,18 @@ impl<T, U> RouterBuilder<T, U> {
val: T,
ctx: U,
) -> (&mut ResourceDef, &mut T, &mut U) {
if let Some((_, path_conflicts)) = self
.path_conflicts
.iter_mut()
.find(|(route_idx, _)| rdef.eq(&self.routes.get(*route_idx).unwrap().0))
{
*path_conflicts += 1;
} else {
self.path_conflicts.push((self.routes.len(), 1));
}
self.routes.push((rdef, val, ctx));
#[allow(clippy::map_identity)] // map is used to distribute &mut-ness to tuple elements
self.routes
.last_mut()
@ -106,8 +142,15 @@ impl<T, U> RouterBuilder<T, U> {
/// Finish configuration and create router instance.
pub fn finish(self) -> Router<T, U> {
let max_path_conflicts = self
.path_conflicts
.iter()
.map(|(_, path_conflicts)| *path_conflicts)
.max()
.unwrap_or(1);
Router {
routes: self.routes,
max_path_conflicts,
}
}
}
@ -280,4 +323,42 @@ mod tests {
assert_eq!(*h, 11);
assert_eq!(&path["val"], "ttt");
}
#[test]
fn test_max_path_conflicts() {
let mut router = Router::<usize>::build();
router.path("/test", 10).0.set_id(0);
router.path("/test/{val}", 11).0.set_id(1);
let router = router.finish();
assert_eq!(1, router.max_path_conflicts);
let mut router = Router::<usize>::build();
router.path("/test", 10).0.set_id(0);
router.path("/test", 11).0.set_id(1);
router.path("/test2", 11).0.set_id(1);
router.path("/test2", 11).0.set_id(1);
router.path("/test2", 11).0.set_id(1);
let router = router.finish();
assert_eq!(3, router.max_path_conflicts);
let failures_until_fn_builder = |mut num_failures: u16| {
move |_: &Path<&str>, _: &()| {
if num_failures == 0 {
return true;
}
num_failures -= 1;
false
}
};
assert!(router
.recognize_fn(&mut Path::new("/test2"), failures_until_fn_builder(3))
.is_none());
assert!(router
.recognize_fn(&mut Path::new("/test2"), failures_until_fn_builder(2))
.is_some());
}
}

View File

@ -75,7 +75,7 @@ mod tests {
let re = ResourceDef::new(pattern);
let uri = Uri::try_from(url.as_ref()).unwrap();
let mut path = Path::new(Url::new(uri));
assert!(re.capture_match_info(&mut path));
assert!(re.resolve_path_if_match(&mut path));
path
}

View File

@ -24,7 +24,7 @@ allowed_external_types = [
actix = { version = ">=0.12, <0.14", default-features = false }
actix-codec = "0.5"
actix-http = "3"
actix-web = { version = "4", default-features = false }
actix-web = { version = "4", default-features = false, features = ["ws"] }
bytes = "1"
bytestring = "1"

View File

@ -4,6 +4,7 @@
- `actix_web::response::builder::HttpResponseBuilder::streaming()` now sets `Content-Type` to `application/octet-stream` if `Content-Type` does not exist.
- `actix_web::response::builder::HttpResponseBuilder::streaming()` now calls `actix_web::response::builder::HttpResponseBuilder::no_chunking()` if `Content-Length` is set by user.
- Add `ws` crate feature (on-by-default) which forwards to `actix-http` and guards some of its `ResponseError` impls.
## 4.11.0

View File

@ -67,6 +67,7 @@ default = [
"http2",
"unicode",
"compat",
"ws",
]
# Brotli algorithm content-encoding support
@ -85,9 +86,12 @@ cookies = ["dep:cookie"]
# Secure & signed cookies
secure-cookies = ["cookies", "cookie/secure"]
# HTTP/2 support (including h2c).
# HTTP/2 support (including h2c)
http2 = ["actix-http/http2"]
# WebSocket support
ws = ["actix-http/ws"]
# TLS via OpenSSL
openssl = ["__tls", "http2", "actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
@ -131,7 +135,7 @@ actix-service = "2"
actix-tls = { version = "3.4", default-features = false, optional = true }
actix-utils = "3"
actix-http = { version = "3.11", features = ["ws"] }
actix-http = "3.11"
actix-router = { version = "0.5.3", default-features = false, features = ["http"] }
actix-web-codegen = { version = "4.3", optional = true, default-features = false }

View File

@ -306,16 +306,16 @@ impl Service<ServiceRequest> for AppRouting {
actix_service::always_ready!();
fn call(&self, mut req: ServiceRequest) -> Self::Future {
let res = self.router.recognize_fn(&mut req, |req, guards| {
let guards_check_fn = |req: &ServiceRequest, guards: &Vec<Box<dyn Guard>>| {
let guard_ctx = req.guard_ctx();
guards.iter().all(|guard| guard.check(&guard_ctx))
});
};
let res = self.router.recognize_fn(&mut req, guards_check_fn);
if let Some((srv, _info)) = res {
srv.call(req)
} else {
self.default.call(req)
return srv.call(req);
}
self.default.call(req)
}
}

View File

@ -7,7 +7,6 @@ use std::{
io::{self, Write as _},
};
use actix_http::Response;
use bytes::BytesMut;
use crate::{
@ -126,20 +125,24 @@ impl ResponseError for actix_http::error::PayloadError {
}
}
impl ResponseError for actix_http::ws::ProtocolError {}
impl ResponseError for actix_http::error::ContentTypeError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
#[cfg(feature = "ws")]
impl ResponseError for actix_http::ws::HandshakeError {
fn error_response(&self) -> HttpResponse<BoxBody> {
Response::from(self).map_into_boxed_body().into()
actix_http::Response::from(self)
.map_into_boxed_body()
.into()
}
}
#[cfg(feature = "ws")]
impl ResponseError for actix_http::ws::ProtocolError {}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -2,16 +2,80 @@
## What Is A Middleware?
Middleware in Actix Web is a powerful mechanism that allows you to add additional behavior to request/response processing. It enables you to:
- Pre-process incoming requests (e.g., path normalization, authentication)
- Post-process outgoing responses (e.g., logging, compression)
- Modify application state through ServiceRequest
- Access external services (e.g., sessions, caching)
Middleware is registered for each App, Scope, or Resource and executed in the reverse order of registration. This means the last registered middleware is the first to process the request.
## Middleware Traits
Actix Web's middleware system is built on two main traits:
1. `Transform<S, Req>`: The builder trait that creates the actual Service. It's responsible for:
- Creating new middleware instances
- Assembling the middleware chain
- Handling initialization errors
2. `Service<Req>`: The trait that represents the actual middleware functionality. It:
- Processes requests and responses
- Can modify both request and response
- Can short-circuit request processing
- Must be implemented for the middleware to work
## Understanding Body Types
When working with middleware, it's important to understand body types:
- Middleware can work with different body types for requests and responses
- The `MessageBody` trait is used to handle different body types
- You can use `EitherBody` when you need to handle multiple body types
- Be careful with body consumption - once a body is consumed, it cannot be read again
## Best Practices
1. Keep middleware focused and single-purpose
2. Handle errors appropriately and propagate them correctly
3. Be mindful of performance impact
4. Use appropriate body types and handle them correctly
5. Consider middleware ordering carefully
6. Document your middleware's behavior and requirements
7. Test your middleware thoroughly
## Error Propagation
Proper error handling is crucial in middleware:
1. Always propagate errors from the inner service
2. Use appropriate error types
3. Handle initialization errors
4. Consider using custom error types for specific middleware errors
5. Document error conditions and handling
## When To (Not) Use Middleware
Use middleware when you need to:
- Add cross-cutting concerns
- Modify requests/responses globally
- Add authentication/authorization
- Add logging or monitoring
- Handle compression or caching
Avoid middleware when:
- The functionality is specific to a single route
- The operation is better handled by a service
- The overhead would be too high
- The functionality can be implemented more simply
## Author's References
- `EitherBody` + when is middleware appropriate: https://discord.com/channels/771444961383153695/952016890723729428
- Actix Web Documentation: https://docs.rs/actix-web
- Service Trait Documentation: https://docs.rs/actix-service
- MessageBody Trait Documentation: https://docs.rs/actix-web/latest/actix_web/body/trait.MessageBody.html

View File

@ -510,16 +510,16 @@ impl Service<ServiceRequest> for ScopeService {
actix_service::always_ready!();
fn call(&self, mut req: ServiceRequest) -> Self::Future {
let res = self.router.recognize_fn(&mut req, |req, guards| {
let guards_check_fn = |req: &ServiceRequest, guards: &Vec<Box<dyn Guard>>| {
let guard_ctx = req.guard_ctx();
guards.iter().all(|guard| guard.check(&guard_ctx))
});
};
let res = self.router.recognize_fn(&mut req, guards_check_fn);
if let Some((srv, _info)) = res {
srv.call(req)
} else {
self.default.call(req)
return srv.call(req);
}
self.default.call(req)
}
}

View File

@ -176,7 +176,7 @@ mod tests {
let resource = ResourceDef::new("/{value}/");
let mut req = TestRequest::with_uri("/32/").to_srv_request();
resource.capture_match_info(req.match_info_mut());
resource.resolve_path_if_match(req.match_info_mut());
let (req, mut pl) = req.into_parts();
assert_eq!(*Path::<i8>::from_request(&req, &mut pl).await.unwrap(), 32);
@ -189,7 +189,7 @@ mod tests {
let resource = ResourceDef::new("/{key}/{value}/");
let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request();
resource.capture_match_info(req.match_info_mut());
resource.resolve_path_if_match(req.match_info_mut());
let (req, mut pl) = req.into_parts();
let (Path(res),) = <(Path<(String, String)>,)>::from_request(&req, &mut pl)
@ -215,7 +215,7 @@ mod tests {
let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request();
let resource = ResourceDef::new("/{key}/{value}/");
resource.capture_match_info(req.match_info_mut());
resource.resolve_path_if_match(req.match_info_mut());
let (req, mut pl) = req.into_parts();
let mut s = Path::<MyStruct>::from_request(&req, &mut pl).await.unwrap();
@ -238,7 +238,7 @@ mod tests {
let mut req = TestRequest::with_uri("/name/32/").to_srv_request();
let resource = ResourceDef::new("/{key}/{value}/");
resource.capture_match_info(req.match_info_mut());
resource.resolve_path_if_match(req.match_info_mut());
let (req, mut pl) = req.into_parts();
let s = Path::<Test2>::from_request(&req, &mut pl).await.unwrap();
@ -262,7 +262,7 @@ mod tests {
async fn paths_decoded() {
let resource = ResourceDef::new("/{key}/{value}");
let mut req = TestRequest::with_uri("/na%2Bme/us%2Fer%254%32").to_srv_request();
resource.capture_match_info(req.match_info_mut());
resource.resolve_path_if_match(req.match_info_mut());
let (req, mut pl) = req.into_parts();
let path_items = Path::<MyStruct>::from_request(&req, &mut pl).await.unwrap();