From 6c5ef0fadab25ff8857d67a6e0d0614565c744d5 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Sat, 7 Feb 2026 16:46:28 +0900 Subject: [PATCH] fix behavior change for audio file --- actix-files/src/named.rs | 11 ++++ actix-files/src/service.rs | 103 ++++++++++++++++++---------------- actix-files/tests/encoding.rs | 20 ++++++- 3 files changed, 84 insertions(+), 50 deletions(-) diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 51a1ce164..77ae53d1c 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -714,3 +714,14 @@ impl HttpServiceFactory for NamedFile { ) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn audio_files_use_inline_content_disposition() { + let (_ct, cd) = get_content_type_and_disposition(Path::new("sound.mp3")).unwrap(); + assert_eq!(cd.disposition, DispositionType::Inline); + } +} diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index 94eb7c517..5e91eaaa7 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -197,9 +197,9 @@ impl Service for FilesService { if let Some((named_file, encoding)) = find_compressed(&req, &named_path).await { - return Ok(this.serve_named_file_with_encoding( - req, named_file, encoding, - )); + return Ok( + this.serve_named_file_with_encoding(req, named_file, encoding) + ); } } // fallback to the uncompressed version @@ -246,7 +246,7 @@ async fn find_compressed( original_path: &Path, ) -> Option<(NamedFile, header::ContentEncoding)> { use actix_web::HttpMessage; - use header::{ContentEncoding, Encoding, Preference}; + use header::{AcceptEncoding, ContentEncoding, Encoding}; // Retrieve the content type and content disposition based on the original filename. If we // can't get these successfully, don't even try to find a compressed file. @@ -256,52 +256,59 @@ async fn find_compressed( Err(_) => return None, }; - let accept_encoding = req.get_header::(); - let ranked_encodings = accept_encoding - .map(|encodings| encodings.ranked()) - .unwrap_or_else(Vec::new); + let accept_encoding = req.get_header::()?; - let ranked_encodings_iter = ranked_encodings - .into_iter() - .filter_map(|e| match e { - Preference::Any | Preference::Specific(Encoding::Unknown(_)) => None, - Preference::Specific(Encoding::Known(encoding)) => Some(encoding), - }) - .filter_map(|encoding| { - if !SUPPORTED_PRECOMPRESSION_ENCODINGS.contains(&encoding) { - return None; + let mut supported = SUPPORTED_PRECOMPRESSION_ENCODINGS + .iter() + .copied() + .map(Encoding::Known) + .collect::>(); + + // Only move the original content-type/disposition into the chosen compressed file once. + let mut content_type = Some(content_type); + let mut content_disposition = Some(content_disposition); + + loop { + // Select next acceptable encoding (honouring q=0 rejections) from remaining supported set. + let chosen = accept_encoding.negotiate(supported.iter())?; + + let encoding = match chosen { + Encoding::Known(enc) => enc, + // No supported encoding should ever be unknown here. + Encoding::Unknown(_) => return None, + }; + + // Identity indicates there is no acceptable pre-compressed representation. + if encoding == ContentEncoding::Identity { + return None; + } + + let extension = match encoding { + ContentEncoding::Brotli => ".br", + ContentEncoding::Gzip => ".gz", + ContentEncoding::Zstd => ".zst", + ContentEncoding::Identity => unreachable!(), + // Only variants in SUPPORTED_PRECOMPRESSION_ENCODINGS can occur here. + _ => unreachable!(), + }; + + let mut compressed_path = original_path.to_owned(); + let filename = match compressed_path.file_name().and_then(|name| name.to_str()) { + Some(filename) => filename.to_owned(), + None => return None, + }; + compressed_path.set_file_name(filename + extension); + + match NamedFile::open_async(&compressed_path).await { + Ok(mut named_file) => { + named_file.content_type = content_type.take().unwrap(); + named_file.content_disposition = content_disposition.take().unwrap(); + return Some((named_file, encoding)); + } + // Ignore errors while searching disk for a suitable encoding. + Err(_) => { + supported.retain(|enc| enc != &chosen); } - let extension = match encoding { - ContentEncoding::Brotli => Some(".br"), - ContentEncoding::Gzip => Some(".gz"), - ContentEncoding::Zstd => Some(".zst"), - ContentEncoding::Identity => None, - // Only variants in SUPPORTED_PRECOMPRESSION_ENCODINGS can occur here - _ => unreachable!(), - }; - let path = match extension { - Some(extension) => { - let mut compressed_path = original_path.to_owned(); - let filename = compressed_path - .file_name() - .unwrap() - .to_str() - .unwrap() - .to_string(); - compressed_path.set_file_name(filename + extension); - compressed_path - } - None => original_path.to_owned(), - }; - Some((path, encoding)) - }); - for (path, encoding) in ranked_encodings_iter { - // Ignore errors while searching disk for a suitable encoding - if let Ok(mut named_file) = NamedFile::open_async(&path).await { - named_file.content_type = content_type; - named_file.content_disposition = content_disposition; - return Some((named_file, encoding)); } } - None } diff --git a/actix-files/tests/encoding.rs b/actix-files/tests/encoding.rs index 8b765e296..80ad1c49e 100644 --- a/actix-files/tests/encoding.rs +++ b/actix-files/tests/encoding.rs @@ -41,8 +41,7 @@ async fn test_compression_encodings() { use actix_web::body::MessageBody; let srv = - test::init_service(App::new().service(Files::new("/", "./tests").try_compressed())) - .await; + test::init_service(App::new().service(Files::new("/", "./tests").try_compressed())).await; // Select the requested encoding when present let mut req = TestRequest::with_uri("/utf8.txt").to_request(); @@ -96,6 +95,23 @@ async fn test_compression_encodings() { Some(&HeaderValue::from_static("text/plain; charset=utf-8")), ); assert_eq!(res.headers().get(header::CONTENT_ENCODING), None,); + assert_eq!(res.into_body().size(), actix_web::body::BodySize::Sized(44),); + + // Do not select an encoding explicitly refused via q=0 + let mut req = TestRequest::with_uri("/utf8.txt").to_request(); + req.headers_mut().insert( + header::ACCEPT_ENCODING, + header::HeaderValue::from_static("zstd;q=1, gzip;q=0"), + ); + 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), None,); + assert_eq!(res.into_body().size(), actix_web::body::BodySize::Sized(44),); // Can still request a compressed file directly let req = TestRequest::with_uri("/utf8.txt.gz").to_request();