From e1ea99ff065b09b20cf1786611b94a042f9c4579 Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Sat, 29 Aug 2026 17:00:18 +0800 Subject: [PATCH] fix(s3): return BadDigest for Content-MD5 mismatch (#6842) --- crates/rio/src/etag_reader.rs | 17 +++++++++++++++-- rustfs/src/error.rs | 15 +++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/crates/rio/src/etag_reader.rs b/crates/rio/src/etag_reader.rs index ba1638069..f4641f042 100644 --- a/crates/rio/src/etag_reader.rs +++ b/crates/rio/src/etag_reader.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::compress_index::{Index, TryGetIndex}; -use crate::{EtagResolvable, HashReaderDetector, HashReaderMut}; +use crate::{BadDigest, EtagResolvable, HashReaderDetector, HashReaderMut}; use md5::{Digest, Md5}; use pin_project_lite::pin_project; use std::pin::Pin; @@ -89,7 +89,13 @@ where && *checksum != etag { error!("Checksum mismatch, expected={:?}, actual={:?}", checksum, etag); - return Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Checksum mismatch"))); + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + BadDigest { + expected_md5: checksum.clone(), + calculated_md5: etag, + }, + ))); } } } @@ -255,6 +261,7 @@ mod tests { async fn test_etag_reader_checksum_mismatch() { let data = b"checksum test data"; let wrong_checksum = "deadbeefdeadbeefdeadbeefdeadbeef".to_string(); + let calculated_md5 = hex_simd::encode_to_string(Md5::digest(data), hex_simd::AsciiCase::Lower); let reader = BufReader::new(&data[..]); let mut etag_reader = EtagReader::new(reader, Some(wrong_checksum.clone())); @@ -262,5 +269,11 @@ mod tests { // Verification failed, should return InvalidData error let err = etag_reader.read_to_end(&mut buf).await.unwrap_err(); assert_eq!(err.kind(), std::io::ErrorKind::InvalidData); + let digest = err + .get_ref() + .and_then(|source| source.downcast_ref::()) + .expect("checksum mismatch should preserve the BadDigest type"); + assert_eq!(digest.expected_md5, wrong_checksum); + assert_eq!(digest.calculated_md5, calculated_md5); } } diff --git a/rustfs/src/error.rs b/rustfs/src/error.rs index cb9bafdd9..687d18f11 100644 --- a/rustfs/src/error.rs +++ b/rustfs/src/error.rs @@ -621,6 +621,21 @@ mod tests { assert_eq!(api_error.message, ApiError::error_code_to_message(&S3ErrorCode::BadDigest)); } + #[test] + fn content_md5_mismatch_io_errors_map_to_bad_digest() { + let bad_digest = || rustfs_rio::BadDigest { + expected_md5: "expected".to_string(), + calculated_md5: "calculated".to_string(), + }; + let api_error = ApiError::from(IoError::new(ErrorKind::InvalidData, bad_digest())); + assert_eq!(api_error.code, S3ErrorCode::BadDigest); + assert_eq!(api_error.message, ApiError::error_code_to_message(&S3ErrorCode::BadDigest)); + + let api_error = ApiError::from(StorageError::Io(IoError::new(ErrorKind::InvalidData, bad_digest()))); + assert_eq!(api_error.code, S3ErrorCode::BadDigest); + assert_eq!(api_error.message, ApiError::error_code_to_message(&S3ErrorCode::BadDigest)); + } + #[test] fn upload_stream_sha256_mismatch_maps_to_bad_digest() { let api_error = ApiError::from(IoError::other(MockUploadStreamError::Sha256Mismatch));