Compare commits

...

16 Commits

Author SHA1 Message Date
Keith Cirkel 0afc188666
Merge 98e0cc4049 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
Rob Ede 98e0cc4049
Merge branch 'master' into add-uri-typed-headers 2025-05-10 21:17:56 +01:00
Rob Ede cfaa5b24c7
Merge branch 'master' into add-uri-typed-headers 2024-10-01 05:19:11 +01:00
Keith Cirkel 66873d16b5
Merge branch 'master' into add-uri-typed-headers 2024-09-17 09:55:44 +01:00
Keith Cirkel 4f8819d277
use parens to appease linter 2024-08-21 00:18:09 +01:00
Keith Cirkel f96a21f1fa
make Uri in http::header private 2024-08-19 14:13:00 +01:00
Keith Cirkel b66866d2d1
update references and text to RFC 9110 2024-08-19 14:13:00 +01:00
Keith Cirkel bbb7258e7b
add changelog entry for TryIntoHeaderValue Uri 2024-08-19 14:12:58 +01:00
Keith Cirkel 3ff861eb29
add referer typed header 2024-08-19 14:12:39 +01:00
Keith Cirkel f5d340878c
add location typed header 2024-08-19 14:12:39 +01:00
Keith Cirkel 342242a0e7
add content-location typed header 2024-08-19 14:12:38 +01:00
Keith Cirkel 9b6a93d72c
TryIntoHeaderValue for Uri 2024-08-19 14:12:09 +01:00
16 changed files with 304 additions and 49 deletions

View File

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

31
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
//! [`TryIntoHeaderValue`] trait and implementations.
use bytes::Bytes;
use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue};
use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue, Uri};
use mime::Mime;
/// An interface for types that can be converted into a [`HeaderValue`].
@ -129,3 +129,12 @@ impl TryIntoHeaderValue for Mime {
HeaderValue::from_str(self.as_ref())
}
}
impl TryIntoHeaderValue for Uri {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_str(&self.to_string())
}
}

View File

@ -36,6 +36,10 @@
- On Windows, an error is now returned from `HttpServer::bind()` (or TLS variants) when binding to a socket that's already in use.
- Update `brotli` dependency to `7`.
- Minimum supported Rust version (MSRV) is now 1.75.
- Add `TryIntoHeaderValue` for `Uri` type.
- Add `http::header::ContentLocation` typed header.
- Add `http::header::Location` typed header.
- Add `http::header::Referer` typed header.
## 4.9.0

View File

@ -0,0 +1,36 @@
use super::{Uri, CONTENT_LOCATION};
crate::http::header::common_header! {
/// `Content-Location` header, defined
/// in [RFC 9110 §8.7](https://datatracker.ietf.org/doc/html/rfc9110#section-8.7)
///
/// The "Content-Location" header field references a URI that can be used
/// as an identifier for a specific resource corresponding to the
/// representation in this message's content.
///
/// # ABNF
/// ```plain
/// Content-Location = absolute-URI / partial-URI
/// ```
///
/// # Example Values
/// * `http://www.example.org/hypertext/Overview.html`
///
/// # Examples
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_http::Uri;
/// use actix_web::http::header::ContentLocation;
///
/// let mut builder = HttpResponse::Created();
/// builder.insert_header(
/// ContentLocation("http://www.example.org".parse::<Uri>().unwrap())
/// );
/// ```
(ContentLocation, CONTENT_LOCATION) => [Uri]
test_parse_and_format {
crate::http::header::common_header_test!(test1, [b"http://www.example.org/hypertext/Overview.html"]);
}
}

View File

@ -0,0 +1,37 @@
use super::{Uri, LOCATION};
crate::http::header::common_header! {
/// `Location` header, defined
/// in [RFC 9110 §10.2.2](https://datatracker.ietf.org/doc/html/rfc9110#section-10.2.2)
///
/// The "Location" header field is used in some responses to refer to a
/// specific resource in relation to the response. The type of relationship
/// is defined by the combination of request method and status code
/// semantics.
///
/// # ABNF
/// ```plain
/// Location = URI-reference
/// ```
///
/// # Example Values
/// * `http://www.example.org/hypertext/Overview.html`
///
/// # Examples
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_http::Uri;
/// use actix_web::http::header::Location;
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// Location("http://www.example.org".parse::<Uri>().unwrap())
/// );
/// ```
(Location, LOCATION) => [Uri]
test_parse_and_format {
crate::http::header::common_header_test!(test1, [b"http://www.example.org/hypertext/Overview.html"]);
}
}

View File

@ -14,6 +14,7 @@ use std::fmt;
// - the few typed headers from actix-http
// - header parsing utils
pub use actix_http::header::*;
use actix_http::Uri;
use bytes::{Bytes, BytesMut};
mod accept;
@ -25,6 +26,7 @@ mod cache_control;
mod content_disposition;
mod content_language;
mod content_length;
mod content_location;
mod content_range;
mod content_type;
mod date;
@ -38,9 +40,11 @@ mod if_none_match;
mod if_range;
mod if_unmodified_since;
mod last_modified;
mod location;
mod macros;
mod preference;
mod range;
mod referer;
#[cfg(test)]
pub(crate) use self::macros::common_header_test;
@ -55,6 +59,7 @@ pub use self::{
content_disposition::{ContentDisposition, DispositionParam, DispositionType},
content_language::ContentLanguage,
content_length::ContentLength,
content_location::ContentLocation,
content_range::{ContentRange, ContentRangeSpec},
content_type::ContentType,
date::Date,
@ -68,8 +73,10 @@ pub use self::{
if_range::IfRange,
if_unmodified_since::IfUnmodifiedSince,
last_modified::LastModified,
location::Location,
preference::Preference,
range::{ByteRangeSpec, Range},
referer::Referer,
};
/// Format writer ([`fmt::Write`]) for a [`BytesMut`].

View File

@ -0,0 +1,36 @@
use super::{Uri, REFERER};
crate::http::header::common_header! {
/// `Referer` header, defined
/// in [RFC 9110 §10.1.3](https://datatracker.ietf.org/doc/html/rfc9110#section-10.1.3)
///
/// The "Referer" (sic) header field allows the user agent to specify a URI
/// reference for the resource from which the target URI was obtained (i.e.,
/// the "referrer", though the field name is misspelled).
///
/// # ABNF
/// ```plain
/// Referer = absolute-URI / partial-URI
/// ```
///
/// # Example Values
/// * `http://www.example.org/hypertext/Overview.html`
///
/// # Examples
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_http::Uri;
/// use actix_web::http::header::Referer;
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// Referer("http://www.example.org".parse::<Uri>().unwrap())
/// );
/// ```
(Referer, REFERER) => [Uri]
test_parse_and_format {
crate::http::header::common_header_test!(test1, [b"http://www.example.org/hypertext/Overview.html"]);
}
}

View File

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