mirror of https://github.com/fafhrd91/actix-web
Compare commits
2 Commits
86eeea7f92
...
b6f93ba9a0
| Author | SHA1 | Date |
|---|---|---|
|
|
b6f93ba9a0 | |
|
|
1aa74f4234 |
|
|
@ -2,11 +2,13 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- Add support for passing multiple root directories to `Files::new`. [#3402]
|
||||
- Add `Files::try_compressed()` to support serving pre-compressed static files [#2615]
|
||||
- Fix handling of `bytes=0-`
|
||||
- Fix `NamedFile` panic when serving files with pre-UNIX epoch modification times. [#2748]
|
||||
- Fix invalid `Content-Encoding: identity` header in `NamedFile` range responses. [#3191]
|
||||
|
||||
[#3402]: https://github.com/actix/actix-web/issues/3402
|
||||
[#2615]: https://github.com/actix/actix-web/pull/2615
|
||||
[#2748]: https://github.com/actix/actix-web/issues/2748
|
||||
[#3191]: https://github.com/actix/actix-web/issues/3191
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
ffi::{OsStr, OsString},
|
||||
fmt, io,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
|
|
@ -37,7 +39,7 @@ use crate::{
|
|||
/// ```
|
||||
pub struct Files {
|
||||
mount_path: String,
|
||||
directory: PathBuf,
|
||||
directories: Vec<PathBuf>,
|
||||
index: Option<String>,
|
||||
show_index: bool,
|
||||
redirect_to_slash: bool,
|
||||
|
|
@ -63,7 +65,7 @@ impl fmt::Debug for Files {
|
|||
impl Clone for Files {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
directory: self.directory.clone(),
|
||||
directories: self.directories.clone(),
|
||||
index: self.index.clone(),
|
||||
show_index: self.show_index,
|
||||
redirect_to_slash: self.redirect_to_slash,
|
||||
|
|
@ -83,6 +85,131 @@ impl Clone for Files {
|
|||
}
|
||||
}
|
||||
|
||||
/// File serving root directories for [`Files`].
|
||||
///
|
||||
/// This type is used by [`Files::new`] to accept either one root directory or an ordered
|
||||
/// collection of root directories.
|
||||
#[derive(Debug)]
|
||||
pub struct FilesDirs(Vec<PathBuf>);
|
||||
|
||||
impl FilesDirs {
|
||||
fn canonicalize(self) -> Vec<PathBuf> {
|
||||
self.0
|
||||
.into_iter()
|
||||
.map(|orig_dir| match orig_dir.canonicalize() {
|
||||
Ok(canon_dir) => canon_dir,
|
||||
Err(_) => {
|
||||
log::error!("Specified path is not a directory: {:?}", orig_dir);
|
||||
// Preserve original path so requests don't fall back to CWD.
|
||||
orig_dir
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Path> for FilesDirs {
|
||||
fn from(dir: &Path) -> Self {
|
||||
Self(vec![dir.into()])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PathBuf> for FilesDirs {
|
||||
fn from(dir: &PathBuf) -> Self {
|
||||
Self(vec![dir.into()])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PathBuf> for FilesDirs {
|
||||
fn from(dir: PathBuf) -> Self {
|
||||
Self(vec![dir])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for FilesDirs {
|
||||
fn from(dir: &str) -> Self {
|
||||
Self(vec![dir.into()])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&String> for FilesDirs {
|
||||
fn from(dir: &String) -> Self {
|
||||
Self(vec![dir.into()])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for FilesDirs {
|
||||
fn from(dir: String) -> Self {
|
||||
Self(vec![dir.into()])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&OsStr> for FilesDirs {
|
||||
fn from(dir: &OsStr) -> Self {
|
||||
Self(vec![dir.into()])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OsString> for FilesDirs {
|
||||
fn from(dir: OsString) -> Self {
|
||||
Self(vec![dir.into()])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&OsString> for FilesDirs {
|
||||
fn from(dir: &OsString) -> Self {
|
||||
Self(vec![dir.into()])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<Path>> for FilesDirs {
|
||||
fn from(dir: Box<Path>) -> Self {
|
||||
Self(vec![dir.into()])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Cow<'_, Path>> for FilesDirs {
|
||||
fn from(dir: Cow<'_, Path>) -> Self {
|
||||
Self(vec![dir.into()])
|
||||
}
|
||||
}
|
||||
|
||||
impl<P, const N: usize> From<[P; N]> for FilesDirs
|
||||
where
|
||||
P: Into<PathBuf>,
|
||||
{
|
||||
fn from(dirs: [P; N]) -> Self {
|
||||
Self(dirs.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<P, const N: usize> From<&[P; N]> for FilesDirs
|
||||
where
|
||||
P: Clone + Into<PathBuf>,
|
||||
{
|
||||
fn from(dirs: &[P; N]) -> Self {
|
||||
Self(dirs.iter().cloned().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> From<&[P]> for FilesDirs
|
||||
where
|
||||
P: Clone + Into<PathBuf>,
|
||||
{
|
||||
fn from(dirs: &[P]) -> Self {
|
||||
Self(dirs.iter().cloned().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> From<Vec<P>> for FilesDirs
|
||||
where
|
||||
P: Into<PathBuf>,
|
||||
{
|
||||
fn from(dirs: Vec<P>) -> Self {
|
||||
Self(dirs.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl Files {
|
||||
/// Create new `Files` instance for a specified base directory.
|
||||
///
|
||||
|
|
@ -90,34 +217,34 @@ impl Files {
|
|||
/// The first argument (`mount_path`) is the root URL at which the static files are served.
|
||||
/// For example, `/assets` will serve files at `example.com/assets/...`.
|
||||
///
|
||||
/// The second argument (`serve_from`) is the location on disk at which files are loaded.
|
||||
/// This can be a relative path. For example, `./` would serve files from the current
|
||||
/// working directory.
|
||||
/// The second argument (`serve_from`) is the location on disk that files are served from. This
|
||||
/// can be a single path or an ordered collection of paths. Relative paths are resolved from the
|
||||
/// current working directory.
|
||||
///
|
||||
/// When multiple directories are provided, they are checked in order. The first directory that
|
||||
/// can serve the requested path is used.
|
||||
///
|
||||
/// Directory listings are generated from the first matching directory and are not merged across
|
||||
/// roots. When [`Files::index_file()`] is configured, later roots are searched if an earlier
|
||||
/// matching directory does not contain the index file.
|
||||
///
|
||||
/// Empty root collections never match files; requests fall through to the default handler, or
|
||||
/// return `404 Not Found` if none is configured.
|
||||
///
|
||||
/// # Implementation Notes
|
||||
/// If the mount path is set as the root path `/`, services registered after this one will
|
||||
/// be inaccessible. Register more specific handlers and services first.
|
||||
///
|
||||
/// If `serve_from` cannot be canonicalized at startup, an error is logged and the original
|
||||
/// path is preserved. Requests will return `404 Not Found` until the path exists.
|
||||
/// If a `serve_from` path cannot be canonicalized at startup, an error is logged and the
|
||||
/// original path is preserved. Requests will return `404 Not Found` until the path exists.
|
||||
///
|
||||
/// `Files` utilizes the existing Tokio thread-pool for blocking filesystem operations.
|
||||
/// The number of running threads is adjusted over time as needed, up to a maximum of 512 times
|
||||
/// the number of server [workers](actix_web::HttpServer::workers), by default.
|
||||
pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
|
||||
let orig_dir = serve_from.into();
|
||||
let dir = match orig_dir.canonicalize() {
|
||||
Ok(canon_dir) => canon_dir,
|
||||
Err(_) => {
|
||||
log::error!("Specified path is not a directory: {:?}", orig_dir);
|
||||
// Preserve original path so requests don't fall back to CWD.
|
||||
orig_dir
|
||||
}
|
||||
};
|
||||
|
||||
pub fn new<T: Into<FilesDirs>>(mount_path: &str, serve_from: T) -> Files {
|
||||
Files {
|
||||
mount_path: mount_path.trim_end_matches('/').to_owned(),
|
||||
directory: dir,
|
||||
directories: serve_from.into().canonicalize(),
|
||||
index: None,
|
||||
show_index: false,
|
||||
redirect_to_slash: false,
|
||||
|
|
@ -149,6 +276,9 @@ impl Files {
|
|||
/// Redirects to a slash-ended path when browsing a directory.
|
||||
///
|
||||
/// By default never redirect.
|
||||
///
|
||||
/// When multiple root directories are configured, a matching directory in an earlier root can
|
||||
/// trigger a redirect before later roots are checked for a file at the same path.
|
||||
pub fn redirect_to_slash_directory(mut self) -> Self {
|
||||
self.redirect_to_slash = true;
|
||||
self
|
||||
|
|
@ -407,7 +537,7 @@ impl ServiceFactory<ServiceRequest> for Files {
|
|||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
let mut inner = FilesServiceInner {
|
||||
directory: self.directory.clone(),
|
||||
directories: self.directories.clone(),
|
||||
index: self.index.clone(),
|
||||
show_index: self.show_index,
|
||||
redirect_to_slash: self.redirect_to_slash,
|
||||
|
|
|
|||
|
|
@ -37,8 +37,14 @@ mod range;
|
|||
mod service;
|
||||
|
||||
pub use self::{
|
||||
chunked::ChunkedReadFile, directory::Directory, error::UriSegmentError, files::Files,
|
||||
named::NamedFile, path_buf::PathBufWrap, range::HttpRange, service::FilesService,
|
||||
chunked::ChunkedReadFile,
|
||||
directory::Directory,
|
||||
error::UriSegmentError,
|
||||
files::{Files, FilesDirs},
|
||||
named::NamedFile,
|
||||
path_buf::PathBufWrap,
|
||||
range::HttpRange,
|
||||
service::FilesService,
|
||||
};
|
||||
use self::{
|
||||
directory::{directory_listing, DirectoryRenderer},
|
||||
|
|
@ -63,9 +69,11 @@ type PathFilter = dyn Fn(&Path, &RequestHead) -> bool;
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
fmt::Write as _,
|
||||
fs::{self},
|
||||
ops::Add,
|
||||
path::PathBuf,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
|
|
@ -832,6 +840,243 @@ mod tests {
|
|||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_files_accepts_borrowed_os_string_directory() {
|
||||
let dir = OsString::from(".");
|
||||
let service = Files::new("/", &dir).new_service(()).await.unwrap();
|
||||
|
||||
let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
|
||||
let resp = test::call_service(&service, req).await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_files_empty_directories() {
|
||||
let service = Files::new("/", Vec::<PathBuf>::new())
|
||||
.new_service(())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
|
||||
let resp = test::call_service(&service, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let service = Files::new("/", Vec::<PathBuf>::new())
|
||||
.default_handler(|req: ServiceRequest| async {
|
||||
Ok(req.into_response(HttpResponse::Ok().body("default content")))
|
||||
})
|
||||
.new_service(())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
|
||||
let resp = test::call_service(&service, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
test::read_body(resp).await,
|
||||
Bytes::from_static(b"default content")
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_files_multiple_directories() {
|
||||
let first_dir = tempfile::tempdir().unwrap();
|
||||
let second_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
fs::write(first_dir.path().join("shared.txt"), "first").unwrap();
|
||||
fs::write(second_dir.path().join("shared.txt"), "second").unwrap();
|
||||
fs::write(second_dir.path().join("fallback.txt"), "fallback").unwrap();
|
||||
|
||||
let service = Files::new("/", [first_dir.path(), second_dir.path()])
|
||||
.new_service(())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let req = TestRequest::with_uri("/shared.txt").to_srv_request();
|
||||
let resp = test::call_service(&service, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(test::read_body(resp).await, Bytes::from_static(b"first"));
|
||||
|
||||
let req = TestRequest::with_uri("/fallback.txt").to_srv_request();
|
||||
let resp = test::call_service(&service, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_files_multiple_directories_file_as_parent_falls_back() {
|
||||
let first_dir = tempfile::tempdir().unwrap();
|
||||
let second_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
fs::write(first_dir.path().join("assets"), "file").unwrap();
|
||||
fs::create_dir(second_dir.path().join("assets")).unwrap();
|
||||
fs::write(
|
||||
second_dir.path().join("assets").join("fallback.txt"),
|
||||
"fallback",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let service = Files::new("/", [first_dir.path(), second_dir.path()])
|
||||
.new_service(())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let req = TestRequest::with_uri("/assets/fallback.txt").to_srv_request();
|
||||
let resp = test::call_service(&service, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_files_multiple_directories_default_handler() {
|
||||
let first_dir = tempfile::tempdir().unwrap();
|
||||
let second_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
fs::write(second_dir.path().join("fallback.txt"), "fallback").unwrap();
|
||||
|
||||
let service = Files::new("/", vec![first_dir.path(), second_dir.path()])
|
||||
.default_handler(|req: ServiceRequest| async {
|
||||
Ok(req.into_response(HttpResponse::Ok().body("default content")))
|
||||
})
|
||||
.new_service(())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let req = TestRequest::with_uri("/fallback.txt").to_srv_request();
|
||||
let resp = test::call_service(&service, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback"));
|
||||
|
||||
let req = TestRequest::with_uri("/missing.txt").to_srv_request();
|
||||
let resp = test::call_service(&service, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
test::read_body(resp).await,
|
||||
Bytes::from_static(b"default content")
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_files_multiple_directories_index_file() {
|
||||
let first_dir = tempfile::tempdir().unwrap();
|
||||
let second_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
fs::create_dir(first_dir.path().join("nested")).unwrap();
|
||||
fs::create_dir(second_dir.path().join("nested")).unwrap();
|
||||
fs::write(
|
||||
second_dir.path().join("nested").join("index.html"),
|
||||
"second index",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let service = Files::new("/", [first_dir.path(), second_dir.path()])
|
||||
.index_file("index.html")
|
||||
.new_service(())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let req = TestRequest::with_uri("/nested/").to_srv_request();
|
||||
let resp = test::call_service(&service, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
test::read_body(resp).await,
|
||||
Bytes::from_static(b"second index")
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_files_multiple_directories_index_file_as_parent_falls_back() {
|
||||
let first_dir = tempfile::tempdir().unwrap();
|
||||
let second_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
fs::create_dir(first_dir.path().join("nested")).unwrap();
|
||||
fs::write(first_dir.path().join("nested").join("index.html"), "file").unwrap();
|
||||
fs::create_dir(second_dir.path().join("nested")).unwrap();
|
||||
fs::create_dir(second_dir.path().join("nested").join("index.html")).unwrap();
|
||||
fs::write(
|
||||
second_dir
|
||||
.path()
|
||||
.join("nested")
|
||||
.join("index.html")
|
||||
.join("fallback.txt"),
|
||||
"fallback",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let service = Files::new("/", [first_dir.path(), second_dir.path()])
|
||||
.index_file("index.html/fallback.txt")
|
||||
.new_service(())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let req = TestRequest::with_uri("/nested/").to_srv_request();
|
||||
let resp = test::call_service(&service, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_files_index_file_error_falls_back_to_listing() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
fs::write(dir.path().join("listed.txt"), "listed").unwrap();
|
||||
|
||||
let service = Files::new("/", dir.path())
|
||||
.index_file("index.html\0")
|
||||
.show_files_listing()
|
||||
.new_service(())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let req = TestRequest::with_uri("/").to_srv_request();
|
||||
let resp = test::call_service(&service, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let bytes = test::read_body(resp).await;
|
||||
assert!(format!("{bytes:?}").contains("listed.txt"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_files_multiple_directories_show_files_listing() {
|
||||
let first_dir = tempfile::tempdir().unwrap();
|
||||
let second_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
fs::write(first_dir.path().join("listed.txt"), "listed").unwrap();
|
||||
|
||||
let service = Files::new("/", [first_dir.path(), second_dir.path()])
|
||||
.show_files_listing()
|
||||
.new_service(())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let req = TestRequest::with_uri("/").to_srv_request();
|
||||
let resp = test::call_service(&service, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let bytes = test::read_body(resp).await;
|
||||
assert!(format!("{bytes:?}").contains("listed.txt"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_files_multiple_directories_redirect_precedence() {
|
||||
let first_dir = tempfile::tempdir().unwrap();
|
||||
let second_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
fs::create_dir(first_dir.path().join("item")).unwrap();
|
||||
fs::write(second_dir.path().join("item"), "file").unwrap();
|
||||
|
||||
let service = Files::new("/", [first_dir.path(), second_dir.path()])
|
||||
.show_files_listing()
|
||||
.redirect_to_slash_directory()
|
||||
.new_service(())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let req = TestRequest::with_uri("/item").to_srv_request();
|
||||
let resp = test::call_service(&service, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::TEMPORARY_REDIRECT);
|
||||
assert_eq!(resp.headers().get(header::LOCATION).unwrap(), "/item/");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_default_handler_file_missing() {
|
||||
let st = Files::new("/", ".")
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ impl Deref for FilesService {
|
|||
}
|
||||
|
||||
pub struct FilesServiceInner {
|
||||
pub(crate) directory: PathBuf,
|
||||
pub(crate) directories: Vec<PathBuf>,
|
||||
pub(crate) index: Option<String>,
|
||||
pub(crate) show_index: bool,
|
||||
pub(crate) redirect_to_slash: bool,
|
||||
|
|
@ -113,8 +113,8 @@ impl FilesService {
|
|||
self.serve_named_file_with_encoding(req, named_file, header::ContentEncoding::Identity)
|
||||
}
|
||||
|
||||
fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse {
|
||||
let dir = Directory::new(self.directory.clone(), path);
|
||||
fn show_index(&self, req: ServiceRequest, base: PathBuf, path: PathBuf) -> ServiceResponse {
|
||||
let dir = Directory::new(base, path);
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
|
||||
|
|
@ -171,70 +171,124 @@ impl Service<ServiceRequest> for FilesService {
|
|||
}
|
||||
}
|
||||
|
||||
// full file path
|
||||
let path = this.directory.join(&path_on_disk);
|
||||
let mut last_miss = None;
|
||||
let mut first_index_listing = None;
|
||||
let mut found_unrenderable_dir = false;
|
||||
|
||||
// Try serving pre-compressed file even if the uncompressed file doesn't exist yet.
|
||||
// Still handle directories (index/listing) through the normal branch below.
|
||||
if this.try_compressed && !path.is_dir() {
|
||||
if let Some((named_file, encoding)) = find_compressed(&req, &path).await {
|
||||
return Ok(this.serve_named_file_with_encoding(req, named_file, encoding));
|
||||
}
|
||||
}
|
||||
for directory in &this.directories {
|
||||
// full file path
|
||||
let path = directory.join(&path_on_disk);
|
||||
|
||||
if let Err(err) = path.canonicalize() {
|
||||
return this.handle_err(err, req).await;
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
if this.redirect_to_slash
|
||||
&& !req.path().ends_with('/')
|
||||
&& (this.index.is_some() || this.show_index)
|
||||
{
|
||||
let redirect_to = format!("{}/", req.path());
|
||||
|
||||
let response = if this.with_permanent_redirect {
|
||||
HttpResponse::PermanentRedirect()
|
||||
} else {
|
||||
HttpResponse::TemporaryRedirect()
|
||||
// Try serving pre-compressed file even if the uncompressed file doesn't exist yet.
|
||||
// Still handle directories (index/listing) through the normal branch below.
|
||||
if this.try_compressed && !path.is_dir() {
|
||||
if let Some((named_file, encoding)) = find_compressed(&req, &path).await {
|
||||
return Ok(this.serve_named_file_with_encoding(req, named_file, encoding));
|
||||
}
|
||||
.insert_header((header::LOCATION, redirect_to))
|
||||
.finish();
|
||||
|
||||
return Ok(req.into_response(response));
|
||||
}
|
||||
|
||||
match this.index {
|
||||
Some(ref index) => {
|
||||
let named_path = path.join(index);
|
||||
if this.try_compressed {
|
||||
if let Some((named_file, encoding)) =
|
||||
find_compressed(&req, &named_path).await
|
||||
{
|
||||
return Ok(
|
||||
this.serve_named_file_with_encoding(req, named_file, encoding)
|
||||
);
|
||||
if let Err(err) = path.canonicalize() {
|
||||
if matches!(
|
||||
err.kind(),
|
||||
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
|
||||
) {
|
||||
last_miss = Some(err);
|
||||
continue;
|
||||
}
|
||||
|
||||
return this.handle_err(err, req).await;
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
if this.redirect_to_slash
|
||||
&& !req.path().ends_with('/')
|
||||
&& (this.index.is_some() || this.show_index)
|
||||
{
|
||||
let redirect_to = format!("{}/", req.path());
|
||||
|
||||
let response = if this.with_permanent_redirect {
|
||||
HttpResponse::PermanentRedirect()
|
||||
} else {
|
||||
HttpResponse::TemporaryRedirect()
|
||||
}
|
||||
.insert_header((header::LOCATION, redirect_to))
|
||||
.finish();
|
||||
|
||||
return Ok(req.into_response(response));
|
||||
}
|
||||
|
||||
match &this.index {
|
||||
Some(index) => {
|
||||
let named_path = path.join(index);
|
||||
if this.try_compressed {
|
||||
if let Some((named_file, encoding)) =
|
||||
find_compressed(&req, &named_path).await
|
||||
{
|
||||
return Ok(this.serve_named_file_with_encoding(
|
||||
req, named_file, encoding,
|
||||
));
|
||||
}
|
||||
}
|
||||
// fallback to the uncompressed version
|
||||
match NamedFile::open_async(named_path).await {
|
||||
Ok(named_file) => return Ok(this.serve_named_file(req, named_file)),
|
||||
Err(err)
|
||||
if matches!(
|
||||
err.kind(),
|
||||
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
|
||||
) =>
|
||||
{
|
||||
if this.show_index && first_index_listing.is_none() {
|
||||
first_index_listing =
|
||||
Some((directory.to_path_buf(), path.clone()));
|
||||
}
|
||||
last_miss = Some(err);
|
||||
}
|
||||
Err(_) if this.show_index => {
|
||||
if first_index_listing.is_none() {
|
||||
first_index_listing =
|
||||
Some((directory.to_path_buf(), path.clone()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(err) => return this.handle_err(err, req).await,
|
||||
}
|
||||
}
|
||||
// fallback to the uncompressed version
|
||||
match NamedFile::open_async(named_path).await {
|
||||
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
|
||||
Err(_) if this.show_index => Ok(this.show_index(req, path)),
|
||||
Err(err) => this.handle_err(err, req).await,
|
||||
None if this.show_index => {
|
||||
return Ok(this.show_index(req, directory.to_path_buf(), path));
|
||||
}
|
||||
None => found_unrenderable_dir = true,
|
||||
}
|
||||
} else {
|
||||
match NamedFile::open_async(&path).await {
|
||||
Ok(named_file) => return Ok(this.serve_named_file(req, named_file)),
|
||||
Err(err)
|
||||
if matches!(
|
||||
err.kind(),
|
||||
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
|
||||
) =>
|
||||
{
|
||||
last_miss = Some(err);
|
||||
}
|
||||
Err(err) => return this.handle_err(err, req).await,
|
||||
}
|
||||
None if this.show_index => Ok(this.show_index(req, path)),
|
||||
None => Ok(ServiceResponse::from_err(
|
||||
FilesError::IsDirectory,
|
||||
req.into_parts().0,
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
match NamedFile::open_async(&path).await {
|
||||
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
|
||||
Err(err) => this.handle_err(err, req).await,
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((base, path)) = first_index_listing {
|
||||
return Ok(this.show_index(req, base, path));
|
||||
}
|
||||
|
||||
if found_unrenderable_dir {
|
||||
return Ok(ServiceResponse::from_err(
|
||||
FilesError::IsDirectory,
|
||||
req.into_parts().0,
|
||||
));
|
||||
}
|
||||
|
||||
let err = last_miss
|
||||
.unwrap_or_else(|| io::Error::new(io::ErrorKind::NotFound, "No such file"));
|
||||
this.handle_err(err, req).await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,6 +166,44 @@ async fn test_compression_encodings() {
|
|||
assert_eq!(res.headers().get(header::CONTENT_ENCODING), None);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn test_compression_encodings_multiple_directories() {
|
||||
use actix_web::body::MessageBody;
|
||||
|
||||
let first_dir = tempfile::tempdir().unwrap();
|
||||
let second_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let compressed_path = second_dir.path().join("fallback.txt.gz");
|
||||
std::fs::write(&compressed_path, b"compressed").unwrap();
|
||||
let compressed_len = std::fs::metadata(compressed_path).unwrap().len();
|
||||
|
||||
let srv = test::init_service(
|
||||
App::new().service(Files::new("/", [first_dir.path(), second_dir.path()]).try_compressed()),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut req = TestRequest::with_uri("/fallback.txt").to_request();
|
||||
req.headers_mut().insert(
|
||||
header::ACCEPT_ENCODING,
|
||||
header::HeaderValue::from_static("gzip"),
|
||||
);
|
||||
let res = test::call_service(&srv, req).await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
res.headers().get(header::CONTENT_TYPE),
|
||||
Some(&HeaderValue::from_static("text/plain; charset=utf-8")),
|
||||
);
|
||||
assert_eq!(
|
||||
res.headers().get(header::CONTENT_ENCODING),
|
||||
Some(&HeaderValue::from_static("gzip")),
|
||||
);
|
||||
assert_eq!(
|
||||
res.into_body().size(),
|
||||
actix_web::body::BodySize::Sized(compressed_len),
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn partial_range_response_encoding() {
|
||||
let srv = test::init_service(App::new().default_service(web::to(|| async {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
## Unreleased
|
||||
|
||||
- When configured, gracefully close HTTP/1 connections after early responses to unread request bodies. [#3967]
|
||||
- Wake HTTP/1 payload receivers with an incomplete-payload error when the sender is dropped before EOF. [#3100]
|
||||
- Update `foldhash` dependency to `0.2`.
|
||||
|
||||
[#3967]: https://github.com/actix/actix-web/issues/3967
|
||||
[#3100]: https://github.com/actix/actix-web/issues/3100
|
||||
|
||||
## 3.12.1
|
||||
|
||||
|
|
|
|||
|
|
@ -140,11 +140,20 @@ impl PayloadSender {
|
|||
}
|
||||
}
|
||||
|
||||
impl Drop for PayloadSender {
|
||||
fn drop(&mut self) {
|
||||
if let Some(shared) = self.inner.upgrade() {
|
||||
shared.borrow_mut().close_sender();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Inner {
|
||||
len: usize,
|
||||
eof: bool,
|
||||
err: Option<PayloadError>,
|
||||
sender_closed: bool,
|
||||
need_read: bool,
|
||||
items: VecDeque<Bytes>,
|
||||
task: Option<Waker>,
|
||||
|
|
@ -157,6 +166,7 @@ impl Inner {
|
|||
eof,
|
||||
len: 0,
|
||||
err: None,
|
||||
sender_closed: eof,
|
||||
items: VecDeque::new(),
|
||||
need_read: true,
|
||||
task: None,
|
||||
|
|
@ -200,12 +210,21 @@ impl Inner {
|
|||
|
||||
#[inline]
|
||||
fn set_error(&mut self, err: PayloadError) {
|
||||
self.sender_closed = true;
|
||||
self.err = Some(err);
|
||||
self.wake();
|
||||
}
|
||||
|
||||
fn close_sender(&mut self) {
|
||||
if !self.sender_closed {
|
||||
self.sender_closed = true;
|
||||
self.set_error(PayloadError::Incomplete(None));
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn feed_eof(&mut self) {
|
||||
self.sender_closed = true;
|
||||
self.eof = true;
|
||||
self.wake();
|
||||
}
|
||||
|
|
@ -332,6 +351,16 @@ mod tests {
|
|||
timeout(WAKE_TIMEOUT, handle).await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn wake_on_sender_drop() {
|
||||
let (sender, payload) = Payload::create(false);
|
||||
let (rx, handle) = prepare_waking_test(payload, Some(Err(())));
|
||||
|
||||
rx.await.unwrap();
|
||||
drop(sender);
|
||||
timeout(WAKE_TIMEOUT, handle).await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_unread_data() {
|
||||
let (_, mut payload) = Payload::create(false);
|
||||
|
|
|
|||
|
|
@ -1013,7 +1013,7 @@ mod tests {
|
|||
#[actix_rt::test]
|
||||
async fn test_multipart_payload_consumption() {
|
||||
// with sample payload and HttpRequest with no headers
|
||||
let (_, inner_payload) = h1::Payload::create(false);
|
||||
let (_sender, inner_payload) = h1::Payload::create(false);
|
||||
let mut payload = actix_web::dev::Payload::from(inner_payload);
|
||||
let req = TestRequest::default().to_http_request();
|
||||
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn basic() {
|
||||
let (_, payload) = h1::Payload::create(false);
|
||||
let (_sender, payload) = h1::Payload::create(false);
|
||||
let mut payload = PayloadBuffer::new_with_limit(payload, DEFAULT_BUFFER_LIMIT);
|
||||
|
||||
assert_eq!(payload.buf.len(), 0);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
- Panic when calling `Route::to()` or `Route::service()` after `Route::wrap()` to prevent silently dropping route middleware. [#3944]
|
||||
- Fix `HttpRequest::{match_pattern,match_name}` reporting path-only matches when route guards disambiguate overlapping resources. [#3346]
|
||||
- Fix `Readlines` handling of lines split across payload chunks so combined line limits are enforced and complete lines are yielded.
|
||||
- Fix app data being retained after graceful shutdown with in-flight slow request bodies. [#3100]
|
||||
- Update `foldhash` dependency to `0.2`.
|
||||
- Update `rand` dependency to `0.10`.
|
||||
- Add `HttpServer::h1_write_buffer_size()`.
|
||||
|
|
@ -14,6 +15,7 @@
|
|||
[#3944]: https://github.com/actix/actix-web/pull/3944
|
||||
[#3346]: https://github.com/actix/actix-web/issues/3346
|
||||
[#3542]: https://github.com/actix/actix-web/issues/3542
|
||||
[#3100]: https://github.com/actix/actix-web/issues/3100
|
||||
|
||||
## 4.13.0
|
||||
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ where
|
|||
T: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
self.app_state.pool().clear();
|
||||
self.app_state.pool().disable();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
cell::{Cell, Ref, RefCell, RefMut},
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
hash::{BuildHasher, Hash},
|
||||
|
|
@ -669,6 +669,7 @@ impl fmt::Debug for HttpRequest {
|
|||
/// The pool's default capacity is 128 items.
|
||||
pub(crate) struct HttpRequestPool {
|
||||
inner: RefCell<Vec<Rc<HttpRequestInner>>>,
|
||||
enabled: Cell<bool>,
|
||||
cap: usize,
|
||||
}
|
||||
|
||||
|
|
@ -682,6 +683,7 @@ impl HttpRequestPool {
|
|||
pub(crate) fn with_capacity(cap: usize) -> Self {
|
||||
HttpRequestPool {
|
||||
inner: RefCell::new(Vec::with_capacity(cap)),
|
||||
enabled: Cell::new(true),
|
||||
cap,
|
||||
}
|
||||
}
|
||||
|
|
@ -698,7 +700,7 @@ impl HttpRequestPool {
|
|||
/// Check if the pool still has capacity for request storage.
|
||||
#[inline]
|
||||
pub(crate) fn is_available(&self) -> bool {
|
||||
self.inner.borrow_mut().len() < self.cap
|
||||
self.enabled.get() && self.inner.borrow().len() < self.cap
|
||||
}
|
||||
|
||||
/// Push a request to pool.
|
||||
|
|
@ -707,15 +709,16 @@ impl HttpRequestPool {
|
|||
self.inner.borrow_mut().push(req);
|
||||
}
|
||||
|
||||
/// Clears all allocated HttpRequest objects.
|
||||
pub(crate) fn clear(&self) {
|
||||
self.inner.borrow_mut().clear()
|
||||
/// Prevents future requests from being returned to the pool and clears existing entries.
|
||||
pub(crate) fn disable(&self) {
|
||||
self.enabled.set(false);
|
||||
self.inner.borrow_mut().clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use bytes::Bytes;
|
||||
|
||||
|
|
@ -993,6 +996,41 @@ mod tests {
|
|||
assert_eq!(resp.headers().get("pool_cap").unwrap(), "128");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_request_dropped_after_service_does_not_reenter_pool() {
|
||||
struct State {
|
||||
_data: Arc<String>,
|
||||
}
|
||||
|
||||
let (weak_data, app_data) = {
|
||||
let data = Arc::new("data".to_owned());
|
||||
(Arc::downgrade(&data), web::Data::new(State { _data: data }))
|
||||
};
|
||||
|
||||
let held_req = Rc::new(RefCell::new(None));
|
||||
|
||||
{
|
||||
let held_req = Rc::clone(&held_req);
|
||||
let srv = init_service(App::new().app_data(app_data).service(web::resource("/").to(
|
||||
move |req: HttpRequest| {
|
||||
*held_req.borrow_mut() = Some(req.clone());
|
||||
HttpResponse::Ok()
|
||||
},
|
||||
)))
|
||||
.await;
|
||||
|
||||
let resp = call_service(&srv, TestRequest::default().to_request()).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
drop(resp);
|
||||
drop(srv);
|
||||
}
|
||||
|
||||
assert!(weak_data.upgrade().is_some());
|
||||
drop(held_req.borrow_mut().take());
|
||||
assert!(weak_data.upgrade().is_none());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_data() {
|
||||
let srv = init_service(App::new().app_data(10usize).service(web::resource("/").to(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
#[cfg(feature = "openssl")]
|
||||
extern crate tls_openssl as openssl;
|
||||
|
||||
use std::{sync::mpsc, thread, time::Duration};
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
sync::{mpsc, Arc},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
|
||||
use actix_web::{rt::time::sleep, web, App, HttpRequest, HttpResponse, HttpServer};
|
||||
use bytes::Bytes;
|
||||
use futures_util::stream;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_start() {
|
||||
|
|
@ -74,6 +81,74 @@ async fn test_start() {
|
|||
srv.stop(false).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_app_data_dropped_after_graceful_shutdown_with_slow_request() {
|
||||
struct State {
|
||||
_data: Arc<String>,
|
||||
}
|
||||
|
||||
async fn echo(_body: web::Json<String>) -> HttpResponse {
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
||||
|
||||
let (weak_data, app_data) = {
|
||||
let data = Arc::new("data".to_owned());
|
||||
(Arc::downgrade(&data), web::Data::new(State { _data: data }))
|
||||
};
|
||||
|
||||
let server = HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(app_data.clone())
|
||||
.service(web::resource("/echo").route(web::post().to(echo)))
|
||||
})
|
||||
.workers(1)
|
||||
.shutdown_timeout(1)
|
||||
.bind(("127.0.0.1", 0))
|
||||
.unwrap();
|
||||
|
||||
let addr = server.addrs()[0];
|
||||
let server = server.run();
|
||||
let server_handle = server.handle();
|
||||
|
||||
let send_request = async move {
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
let slow_body = stream::unfold(0, |idx| async move {
|
||||
if idx < 8 {
|
||||
sleep(Duration::from_millis(200)).await;
|
||||
Some((Ok::<_, Infallible>(Bytes::from_static(b" ")), idx + 1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let client = awc::Client::default();
|
||||
let _ = client
|
||||
.post(format!("http://{addr}/echo"))
|
||||
.insert_header(("content-type", "application/json"))
|
||||
.send_stream(slow_body)
|
||||
.await;
|
||||
};
|
||||
|
||||
let graceful_stop = async move {
|
||||
sleep(Duration::from_millis(300)).await;
|
||||
server_handle.stop(true).await;
|
||||
};
|
||||
|
||||
let (server_res, (), ()) = tokio::join!(server, send_request, graceful_stop);
|
||||
server_res.unwrap();
|
||||
|
||||
for _ in 0..20 {
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
if weak_data.upgrade().is_none() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
panic!("app data still referenced after graceful shutdown");
|
||||
}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
fn ssl_acceptor() -> openssl::ssl::SslAcceptorBuilder {
|
||||
use openssl::{
|
||||
|
|
|
|||
Loading…
Reference in New Issue