mirror of https://github.com/fafhrd91/actix-web
Compare commits
15 Commits
55f64844e8
...
dd2eef608f
Author | SHA1 | Date |
---|---|---|
|
dd2eef608f | |
|
98d7d0b46b | |
|
4966a54e05 | |
|
00b0f8f700 | |
|
3c2907da41 | |
|
5041cd1c65 | |
|
d3c46537b3 | |
|
5ad04e6de7 | |
|
6f4bdb31f9 | |
|
4e56ae0594 | |
|
9f2b207137 | |
|
b47530d66a | |
|
55c38a625f | |
|
934f68ebf6 | |
|
ffa95b7f60 |
|
@ -9,4 +9,5 @@ words:
|
|||
- rustls
|
||||
- rustup
|
||||
- serde
|
||||
- uring
|
||||
- zstd
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://docs.rs/actix-files/0.6.6)
|
||||
[](https://docs.rs/actix-files/0.6.7)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.6.6)
|
||||
[](https://deps.rs/crate/actix-files/0.6.7)
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- Properly wake Payload receivers when feeding errors or EOF
|
||||
|
||||
## 3.11.1
|
||||
|
||||
- Prevent more hangs after client disconnects.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(|| {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) => ¶ms[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());
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue