From f1b8f8312df9206361a5b4f8f8e29150da67a036 Mon Sep 17 00:00:00 2001 From: GatewayJ <835269233@qq.com> Date: Fri, 5 Jun 2026 10:03:07 +0800 Subject: [PATCH] fix(server): normalize empty request content length (#3215) * fix(server): normalize empty S3 request content length * fix(server): normalize empty console content length * fix(server): keep console constants test scoped * test(server): align empty body route expectations --- rustfs/src/server/layer.rs | 157 ++++++++++++++++++++++++++++++++++--- 1 file changed, 148 insertions(+), 9 deletions(-) diff --git a/rustfs/src/server/layer.rs b/rustfs/src/server/layer.rs index ef6085d16..d70809deb 100644 --- a/rustfs/src/server/layer.rs +++ b/rustfs/src/server/layer.rs @@ -284,6 +284,10 @@ fn should_force_zero_content_length_for_empty_body_route(req: &HttpRequest return false; } + if is_empty_body_console_path(req.method(), req.uri()) { + return true; + } + is_empty_body_s3_path(req.method(), req.uri()) } @@ -335,7 +339,11 @@ fn is_heal_status_query(path: &str, query: Option<&str>) -> bool { } fn is_empty_body_s3_path(method: &Method, uri: &http::Uri) -> bool { - *method == Method::DELETE && ConditionalCorsLayer::is_s3_path(uri.path()) + matches!(*method, Method::GET | Method::HEAD | Method::DELETE) && ConditionalCorsLayer::is_s3_path(uri.path()) +} + +fn is_empty_body_console_path(method: &Method, uri: &http::Uri) -> bool { + matches!(*method, Method::GET | Method::HEAD) && is_console_path(uri.path()) } #[derive(Clone)] @@ -1135,6 +1143,7 @@ where #[cfg(test)] mod tests { use super::*; + use crate::server::{FAVICON_PATH, LICENSE, VERSION}; use futures::future::{Ready, ready}; use http::Request; use http_body_util::BodyExt; @@ -1456,14 +1465,8 @@ mod tests { } #[test] - fn non_admin_get_without_content_length_is_not_normalized() { - let paths = [ - "/bucket/object", - "/rustfs/administrator/object", - "/minio/administrator/object", - "/rustfs/adminx/object", - "/minio/adminx/object", - ]; + fn non_s3_non_admin_get_without_content_length_is_not_normalized() { + let paths = ["/rustfs/rpc/read_file_stream", "/health", "/profile/cpu"]; for path in paths { let request = Request::builder().method(Method::GET).uri(path).body(()).expect("request"); @@ -1492,6 +1495,107 @@ mod tests { assert_eq!(headers.get(http::header::CONTENT_LENGTH).unwrap(), "0"); } + #[test] + fn s3_empty_body_get_without_content_length_is_normalized() { + let paths = [ + "/?x-id=ListBuckets", + "/", + "/bucket?list-type=2", + "/bucket/object.txt", + "/rustfs/administrator/object", + "/minio/adminx/object", + ]; + + for path in paths { + let request = Request::builder().method(Method::GET).uri(path).body(()).expect("request"); + + assert!( + should_force_zero_content_length_for_empty_body_route(&request), + "{path} should force Content-Length: 0" + ); + } + } + + #[test] + fn s3_empty_body_head_without_content_length_is_normalized() { + let request = Request::builder() + .method(Method::HEAD) + .uri("/bucket/object.txt") + .body(()) + .expect("request"); + + assert!(should_force_zero_content_length_for_empty_body_route(&request)); + } + + #[test] + fn console_empty_body_get_without_content_length_is_normalized() { + let paths = [ + FAVICON_PATH.to_string(), + format!("{CONSOLE_PREFIX}{LICENSE}"), + format!("{CONSOLE_PREFIX}{VERSION}"), + format!("{CONSOLE_PREFIX}{HEALTH_PREFIX}"), + format!("{CONSOLE_PREFIX}{HEALTH_COMPAT_LIVE_PATH}"), + format!("{CONSOLE_PREFIX}{HEALTH_READY_PATH}"), + format!("{CONSOLE_PREFIX}/index.html"), + format!("{CONSOLE_PREFIX}/assets/app.js"), + ]; + + for path in paths { + let request = Request::builder() + .method(Method::GET) + .uri(path.as_str()) + .body(()) + .expect("request"); + + assert!( + should_force_zero_content_length_for_empty_body_route(&request), + "{path} should force Content-Length: 0" + ); + } + } + + #[test] + fn console_empty_body_head_without_content_length_is_normalized() { + let paths = [ + format!("{CONSOLE_PREFIX}{HEALTH_PREFIX}"), + format!("{CONSOLE_PREFIX}{HEALTH_COMPAT_LIVE_PATH}"), + format!("{CONSOLE_PREFIX}{HEALTH_READY_PATH}"), + format!("{CONSOLE_PREFIX}/index.html"), + ]; + + for path in paths { + let request = Request::builder() + .method(Method::HEAD) + .uri(path.as_str()) + .body(()) + .expect("request"); + + assert!( + should_force_zero_content_length_for_empty_body_route(&request), + "{path} should force Content-Length: 0" + ); + } + } + + #[tokio::test] + async fn empty_body_layer_inserts_zero_content_length_for_s3_and_console_get() { + for path in ["/?x-id=ListBuckets".to_string(), format!("{CONSOLE_PREFIX}/index.html")] { + let capture = HeaderCaptureService::default(); + let headers = capture.headers(); + let mut service = EmptyBodyContentLengthCompatLayer.layer(capture); + let request = Request::builder() + .method(Method::GET) + .uri(path.as_str()) + .body(()) + .expect("request"); + + let _ = service.call(request).await.expect("service call"); + + let headers = headers.lock().expect("captured headers").take().expect("captured headers"); + assert_eq!(headers.get(http::header::CONTENT_LENGTH).unwrap(), "0"); + } + } + #[tokio::test] async fn empty_body_layer_inserts_zero_content_length_for_admin_post() { let capture = HeaderCaptureService::default(); @@ -1693,6 +1797,41 @@ mod tests { assert!(!should_force_zero_content_length_for_empty_body_route(&request)); } + #[test] + fn s3_get_with_transfer_encoding_is_not_normalized() { + let request = Request::builder() + .method(Method::GET) + .uri("/?x-id=ListBuckets") + .header(http::header::TRANSFER_ENCODING, "chunked") + .body(()) + .expect("request"); + + assert!(!should_force_zero_content_length_for_empty_body_route(&request)); + } + + #[test] + fn console_get_with_transfer_encoding_is_not_normalized() { + let request = Request::builder() + .method(Method::GET) + .uri(format!("{CONSOLE_PREFIX}/index.html")) + .header(http::header::TRANSFER_ENCODING, "chunked") + .body(()) + .expect("request"); + + assert!(!should_force_zero_content_length_for_empty_body_route(&request)); + } + + #[test] + fn console_post_without_content_length_is_not_normalized() { + let request = Request::builder() + .method(Method::POST) + .uri(format!("{CONSOLE_PREFIX}/upload")) + .body(()) + .expect("request"); + + assert!(!should_force_zero_content_length_for_empty_body_route(&request)); + } + #[test] fn non_s3_delete_paths_are_not_normalized() { let paths = [