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 - rustls
- rustup - rustup
- serde - serde
- uring
- zstd - zstd

31
Cargo.lock generated
View File

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

View File

@ -2,6 +2,9 @@
## Unreleased ## 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. - Minimum supported Rust version (MSRV) is now 1.75.
## 0.6.6 ## 0.6.6

View File

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

View File

@ -3,11 +3,11 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![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) ![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-files.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
<br /> <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) [![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) [![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; use super::named::File;
#[derive(Debug, Clone, Copy)]
pub(crate) enum ReadMode {
Sync,
Async,
}
pin_project! { pin_project! {
/// Adapter to read a `std::file::File` in chunks. /// Adapter to read a `std::file::File` in chunks.
#[doc(hidden)] #[doc(hidden)]
@ -24,6 +30,7 @@ pin_project! {
state: ChunkedReadFileState<Fut>, state: ChunkedReadFileState<Fut>,
counter: u64, counter: u64,
callback: F, callback: F,
read_mode: ReadMode,
} }
} }
@ -57,6 +64,7 @@ pub(crate) fn new_chunked_read(
size: u64, size: u64,
offset: u64, offset: u64,
file: File, file: File,
read_mode_threshold: u64,
) -> impl Stream<Item = Result<Bytes, Error>> { ) -> impl Stream<Item = Result<Bytes, Error>> {
ChunkedReadFile { ChunkedReadFile {
size, size,
@ -69,31 +77,50 @@ pub(crate) fn new_chunked_read(
}, },
counter: 0, counter: 0,
callback: chunked_read_file_callback, callback: chunked_read_file_callback,
read_mode: if size < read_mode_threshold {
ReadMode::Sync
} else {
ReadMode::Async
},
} }
} }
#[cfg(not(feature = "experimental-io-uring"))] #[cfg(not(feature = "experimental-io-uring"))]
async fn chunked_read_file_callback( fn chunked_read_file_callback_sync(
mut file: File, mut file: File,
offset: u64, offset: u64,
max_bytes: usize, max_bytes: usize,
) -> Result<(File, Bytes), Error> { ) -> Result<(File, Bytes), io::Error> {
use io::{Read as _, Seek as _}; 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 { if n_bytes == 0 {
Err(io::Error::from(io::ErrorKind::UnexpectedEof)) Err(io::Error::from(io::ErrorKind::UnexpectedEof))
} else { } else {
Ok((file, Bytes::from(buf))) 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) Ok(res)
} }
@ -171,7 +198,7 @@ where
#[cfg(not(feature = "experimental-io-uring"))] #[cfg(not(feature = "experimental-io-uring"))]
impl<F, Fut> Stream for ChunkedReadFile<F, Fut> impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
where where
F: Fn(File, u64, usize) -> Fut, F: Fn(File, u64, usize, ReadMode) -> Fut,
Fut: Future<Output = Result<(File, Bytes), Error>>, Fut: Future<Output = Result<(File, Bytes), Error>>,
{ {
type Item = Result<Bytes, Error>; type Item = Result<Bytes, Error>;
@ -193,7 +220,7 @@ where
.take() .take()
.expect("ChunkedReadFile polled after completion"); .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 this.state
.project_replace(ChunkedReadFileState::Future { fut }); .project_replace(ChunkedReadFileState::Future { fut });

View File

@ -49,6 +49,7 @@ pub struct Files {
use_guards: Option<Rc<dyn Guard>>, use_guards: Option<Rc<dyn Guard>>,
guards: Vec<Rc<dyn Guard>>, guards: Vec<Rc<dyn Guard>>,
hidden_files: bool, hidden_files: bool,
read_mode_threshold: u64,
} }
impl fmt::Debug for Files { impl fmt::Debug for Files {
@ -73,6 +74,7 @@ impl Clone for Files {
use_guards: self.use_guards.clone(), use_guards: self.use_guards.clone(),
guards: self.guards.clone(), guards: self.guards.clone(),
hidden_files: self.hidden_files, hidden_files: self.hidden_files,
read_mode_threshold: self.read_mode_threshold,
} }
} }
} }
@ -119,6 +121,7 @@ impl Files {
use_guards: None, use_guards: None,
guards: Vec::new(), guards: Vec::new(),
hidden_files: false, hidden_files: false,
read_mode_threshold: 0,
} }
} }
@ -204,6 +207,23 @@ impl Files {
self 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. /// Specifies whether to use ETag or not.
/// ///
/// Default is true. /// Default is true.
@ -367,6 +387,7 @@ impl ServiceFactory<ServiceRequest> for Files {
file_flags: self.file_flags, file_flags: self.file_flags,
guards: self.use_guards.clone(), guards: self.use_guards.clone(),
hidden_files: self.hidden_files, hidden_files: self.hidden_files,
size_threshold: self.read_mode_threshold,
}; };
if let Some(ref default) = *self.default.borrow() { 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_type: Mime,
pub(crate) content_disposition: ContentDisposition, pub(crate) content_disposition: ContentDisposition,
pub(crate) encoding: Option<ContentEncoding>, pub(crate) encoding: Option<ContentEncoding>,
pub(crate) read_mode_threshold: u64,
} }
#[cfg(not(feature = "experimental-io-uring"))] #[cfg(not(feature = "experimental-io-uring"))]
@ -200,6 +201,7 @@ impl NamedFile {
encoding, encoding,
status_code: StatusCode::OK, status_code: StatusCode::OK,
flags: Flags::default(), flags: Flags::default(),
read_mode_threshold: 0,
}) })
} }
@ -353,6 +355,23 @@ impl NamedFile {
self 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. /// Specifies whether to return `ETag` header in response.
/// ///
/// Default is true. /// Default is true.
@ -440,7 +459,8 @@ impl NamedFile {
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str())); 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); return res.streaming(reader);
} }
@ -577,7 +597,7 @@ impl NamedFile {
.map_into_boxed_body(); .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() { if offset != 0 || length != self.md.len() {
res.status(StatusCode::PARTIAL_CONTENT); res.status(StatusCode::PARTIAL_CONTENT);

View File

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

View File

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

View File

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

View File

@ -200,11 +200,13 @@ impl Inner {
#[inline] #[inline]
fn set_error(&mut self, err: PayloadError) { fn set_error(&mut self, err: PayloadError) {
self.err = Some(err); self.err = Some(err);
self.wake();
} }
#[inline] #[inline]
fn feed_eof(&mut self) { fn feed_eof(&mut self) {
self.eof = true; self.eof = true;
self.wake();
} }
#[inline] #[inline]
@ -253,8 +255,13 @@ impl Inner {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{task::Poll, time::Duration};
use actix_rt::time::timeout;
use actix_utils::future::poll_fn; use actix_utils::future::poll_fn;
use futures_util::{FutureExt, StreamExt};
use static_assertions::{assert_impl_all, assert_not_impl_any}; use static_assertions::{assert_impl_all, assert_not_impl_any};
use tokio::sync::oneshot;
use super::*; use super::*;
@ -263,6 +270,67 @@ mod tests {
assert_impl_all!(Inner: Unpin, Send, Sync); 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] #[actix_rt::test]
async fn test_unread_data() { async fn test_unread_data() {
let (_, mut payload) = Payload::create(false); let (_, mut payload) = Payload::create(false);

View File

@ -2,6 +2,14 @@
## Unreleased ## 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 ## 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. - 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(); let regex_set = regex::RegexSet::new(register!(regex)).unwrap();
group.bench_function("regex", |b| { group.bench_function("regex", |b| {
b.iter(|| { b.iter(|| {

View File

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

View File

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

View File

@ -13,12 +13,16 @@ pub struct ResourceId(pub u16);
/// not required. /// not required.
pub struct Router<T, U = ()> { pub struct Router<T, U = ()> {
routes: Vec<(ResourceDef, T, U)>, routes: Vec<(ResourceDef, T, U)>,
max_path_conflicts: u16,
} }
impl<T, U> Router<T, U> { impl<T, U> Router<T, U> {
/// Constructs new `RouterBuilder` with empty route list. /// Constructs new `RouterBuilder` with empty route list.
pub fn build() -> RouterBuilder<T, U> { 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). /// 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 /// 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 /// closure returns true then the match result is stored into `resource` and a reference to
/// the matched _value_ is returned. /// 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 where
R: Resource, R: Resource,
F: FnMut(&R, &U) -> bool, F: FnMut(&R, &U) -> bool,
{ {
let mut next_resource_match_count = 1;
for (rdef, val, ctx) in self.routes.iter() { for (rdef, val, ctx) in self.routes.iter() {
if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) { match rdef.capture_match_info(resource) {
return Some((val, ResourceId(rdef.id()))); 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>( pub fn recognize_mut_fn<R, F>(
&mut self, &mut self,
resource: &mut R, resource: &mut R,
mut check: F, mut check_fn: F,
) -> Option<(&mut T, ResourceId)> ) -> Option<(&mut T, ResourceId)>
where where
R: Resource, R: Resource,
F: FnMut(&R, &U) -> bool, F: FnMut(&R, &U) -> bool,
{ {
let mut matches = 0;
for (rdef, val, ctx) in self.routes.iter_mut() { for (rdef, val, ctx) in self.routes.iter_mut() {
if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) { match rdef.capture_match_info(resource) {
return Some((val, ResourceId(rdef.id()))); 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. /// Builder for an ordered [routing](Router) list.
pub struct RouterBuilder<T, U = ()> { pub struct RouterBuilder<T, U = ()> {
routes: Vec<(ResourceDef, T, U)>, routes: Vec<(ResourceDef, T, U)>,
path_conflicts: Vec<(usize, u16)>,
} }
impl<T, U> RouterBuilder<T, U> { impl<T, U> RouterBuilder<T, U> {
@ -96,7 +121,18 @@ impl<T, U> RouterBuilder<T, U> {
val: T, val: T,
ctx: U, ctx: U,
) -> (&mut ResourceDef, &mut T, &mut 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)); self.routes.push((rdef, val, ctx));
#[allow(clippy::map_identity)] // map is used to distribute &mut-ness to tuple elements #[allow(clippy::map_identity)] // map is used to distribute &mut-ness to tuple elements
self.routes self.routes
.last_mut() .last_mut()
@ -106,8 +142,15 @@ impl<T, U> RouterBuilder<T, U> {
/// Finish configuration and create router instance. /// Finish configuration and create router instance.
pub fn finish(self) -> Router<T, U> { 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 { Router {
routes: self.routes, routes: self.routes,
max_path_conflicts,
} }
} }
} }
@ -280,4 +323,42 @@ mod tests {
assert_eq!(*h, 11); assert_eq!(*h, 11);
assert_eq!(&path["val"], "ttt"); 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 re = ResourceDef::new(pattern);
let uri = Uri::try_from(url.as_ref()).unwrap(); let uri = Uri::try_from(url.as_ref()).unwrap();
let mut path = Path::new(Url::new(uri)); let mut path = Path::new(Url::new(uri));
assert!(re.capture_match_info(&mut path)); assert!(re.resolve_path_if_match(&mut path));
path path
} }

View File

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

View File

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

View File

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

View File

@ -7,7 +7,6 @@ use std::{
io::{self, Write as _}, io::{self, Write as _},
}; };
use actix_http::Response;
use bytes::BytesMut; use bytes::BytesMut;
use crate::{ 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 { impl ResponseError for actix_http::error::ContentTypeError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST StatusCode::BAD_REQUEST
} }
} }
#[cfg(feature = "ws")]
impl ResponseError for actix_http::ws::HandshakeError { impl ResponseError for actix_http::ws::HandshakeError {
fn error_response(&self) -> HttpResponse<BoxBody> { 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -2,16 +2,80 @@
## What Is A Middleware? ## 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 ## 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 ## 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 ## 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 ## 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 ## 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 ## Author's References
- `EitherBody` + when is middleware appropriate: https://discord.com/channels/771444961383153695/952016890723729428 - `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!(); actix_service::always_ready!();
fn call(&self, mut req: ServiceRequest) -> Self::Future { 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(); let guard_ctx = req.guard_ctx();
guards.iter().all(|guard| guard.check(&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 { if let Some((srv, _info)) = res {
srv.call(req) return srv.call(req);
} else {
self.default.call(req)
} }
self.default.call(req)
} }
} }

View File

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