Compare commits

...

6 Commits

Author SHA1 Message Date
Andrew Scott 1e38ce07f1
Merge f7778df597 into 9fb6c13a1a 2025-08-26 10:11:10 +02:00
Rob Ede 9fb6c13a1a
ci: fix msrv job 2025-08-26 08:26:49 +01:00
Rob Ede 05cfef7f4b
ci: fix msrv job 2025-08-26 08:18:34 +01:00
Rob Ede 8f3eb32a32
chore: fix justfile for msrv 2025-08-26 08:00:19 +01:00
Rob Ede ddd16ec9db
chore(actix-http): prepare release 3.11.1 2025-08-26 07:28:27 +01:00
imgurbot12 f7778df597
feat(actix-files): opt-in filesize threshold for faster synchronous reads 2025-07-31 20:36:46 -07:00
13 changed files with 336 additions and 212 deletions

View File

@ -3,6 +3,6 @@ disallowed-names = [
"e", # no single letter error bindings
]
disallowed-methods = [
{ path = "std::cell::RefCell::default()", reason = "prefer explicit inner type default" },
{ path = "std::rc::Rc::default()", reason = "prefer explicit inner type default" },
{ path = "std::cell::RefCell::default()", reason = "prefer explicit inner type default (remove allow-invalid when rust-lang/rust-clippy/#8581 is fixed)", allow-invalid = true },
{ path = "std::rc::Rc::default()", reason = "prefer explicit inner type default (remove allow-invalid when rust-lang/rust-clippy/#8581 is fixed)", allow-invalid = true },
]

422
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@
## Unreleased
- Opt-In filesize threshold for faster synchronus reads that allow for 20x better performance.
- Minimum supported Rust version (MSRV) is now 1.75.
## 0.6.6

View File

@ -24,6 +24,7 @@ pin_project! {
state: ChunkedReadFileState<Fut>,
counter: u64,
callback: F,
read_sync: bool,
}
}
@ -57,6 +58,7 @@ pub(crate) fn new_chunked_read(
size: u64,
offset: u64,
file: File,
size_threshold: u64,
) -> impl Stream<Item = Result<Bytes, Error>> {
ChunkedReadFile {
size,
@ -69,31 +71,45 @@ pub(crate) fn new_chunked_read(
},
counter: 0,
callback: chunked_read_file_callback,
read_sync: size < size_threshold,
}
}
#[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)))
}
})
.await??;
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_sync: bool,
) -> Result<(File, Bytes), Error> {
let res = if read_sync {
chunked_read_file_callback_sync(file, offset, max_bytes)?
} else {
actix_web::web::block(move || chunked_read_file_callback_sync(file, offset, max_bytes))
.await??
};
Ok(res)
}
@ -171,7 +187,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, bool) -> Fut,
Fut: Future<Output = Result<(File, Bytes), Error>>,
{
type Item = Result<Bytes, Error>;
@ -193,7 +209,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_sync);
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,
size_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,
size_threshold: self.size_threshold,
}
}
}
@ -119,6 +121,7 @@ impl Files {
use_guards: None,
guards: Vec::new(),
hidden_files: false,
size_threshold: 0,
}
}
@ -204,6 +207,18 @@ impl Files {
self
}
/// Sets the async file-size threshold.
///
/// When a file is larger than the threshold, the reader
/// will switch from faster blocking file-reads to slower async reads
/// to avoid blocking the main-thread when processing large files.
///
/// Default is 0, meaning all files are read asyncly.
pub fn set_size_threshold(mut self, size: u64) -> Self {
self.size_threshold = size;
self
}
/// Specifies whether to use ETag or not.
///
/// Default is true.
@ -367,6 +382,7 @@ impl ServiceFactory<ServiceRequest> for Files {
file_flags: self.file_flags,
guards: self.use_guards.clone(),
hidden_files: self.hidden_files,
size_threshold: self.size_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) size_threshold: u64,
}
#[cfg(not(feature = "experimental-io-uring"))]
@ -200,6 +201,7 @@ impl NamedFile {
encoding,
status_code: StatusCode::OK,
flags: Flags::default(),
size_threshold: 0,
})
}
@ -353,6 +355,18 @@ impl NamedFile {
self
}
/// Sets the async file-size threshold.
///
/// When a file is larger than the threshold, the reader
/// will switch from faster blocking file-reads to slower async reads
/// to avoid blocking the main-thread when processing large files.
///
/// Default is 0, meaning all files are read asyncly.
pub fn set_size_threshold(mut self, size: u64) -> Self {
self.size_threshold = size;
self
}
/// Specifies whether to return `ETag` header in response.
///
/// Default is true.
@ -440,7 +454,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.size_threshold);
return res.streaming(reader);
}
@ -577,7 +592,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.size_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
.set_size_threshold(self.size_threshold)
.into_response(&req);
ServiceResponse::new(req, res)
}
@ -169,17 +172,7 @@ impl Service<ServiceRequest> for FilesService {
}
} else {
match NamedFile::open_async(&path).await {
Ok(mut named_file) => {
if let Some(ref mime_override) = this.mime_override {
let new_disposition = mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition;
}
named_file.flags = this.file_flags;
let (req, _) = req.into_parts();
let res = named_file.into_response(&req);
Ok(ServiceResponse::new(req, res))
}
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
Err(err) => this.handle_err(err, req).await,
}
}

View File

@ -2,8 +2,11 @@
## Unreleased
- Update `TestRequest::set_payload` to generate "Content-Length" header
- Malformed websocket frames are now gracefully rejected.
## 3.11.1
- Prevent more hangs after client disconnects.
- More malformed WebSocket frames are now gracefully rejected.
- Using `TestRequest::set_payload()` now sets a Content-Length header.
## 3.11.0

View File

@ -1,6 +1,6 @@
[package]
name = "actix-http"
version = "3.11.0"
version = "3.11.1"
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
description = "HTTP types and services for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"]

View File

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

View File

@ -13,6 +13,7 @@ macro_rules! register {
register!(finish => "(.*)", "(.*)", "(.*)", "(.*)")
}};
(finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{
#[expect(clippy::useless_concat)]
let arr = [
concat!("/authorizations"),
concat!("/authorizations/", $p1),

View File

@ -3,7 +3,6 @@
- The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
- Cookie handling has been offloaded to the `cookie` crate:
- `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs.
- Some types now require lifetime parameters.

View File

@ -13,6 +13,8 @@ fmt:
[private]
downgrade-for-msrv:
cargo {{ toolchain }} update -p=divan --precise=0.1.15 # next ver: 1.80.0
cargo {{ toolchain }} update -p=rayon --precise=1.10.0 # next ver: 1.80.0
cargo {{ toolchain }} update -p=rayon-core --precise=1.12.1 # next ver: 1.80.0
cargo {{ toolchain }} update -p=half --precise=2.4.1 # next ver: 1.81.0
cargo {{ toolchain }} update -p=idna_adapter --precise=1.2.0 # next ver: 1.82.0
cargo {{ toolchain }} update -p=litemap --precise=0.7.4 # next ver: 1.81.0
@ -50,8 +52,7 @@ clippy:
cargo {{ toolchain }} clippy --workspace --all-targets {{ all_crate_features }}
# Run Clippy over workspace using MSRV.
clippy-msrv:
@just toolchain={{ msrv_rustup }} downgrade-for-msrv
clippy-msrv: downgrade-for-msrv
@just toolchain={{ msrv_rustup }} clippy
# Test workspace code.
@ -62,8 +63,7 @@ test:
cargo {{ toolchain }} nextest run --no-tests=warn --workspace --exclude=actix-web-codegen --exclude=actix-multipart-derive {{ all_crate_features }} --filter-expr="not test(test_reading_deflate_encoding_large_random_rustls)"
# Test workspace using MSRV.
test-msrv:
@just toolchain={{ msrv_rustup }} downgrade-for-msrv
test-msrv: downgrade-for-msrv
@just toolchain={{ msrv_rustup }} test
# Test workspace docs.