From 968c30238c4deba912afe121e7039994e7e70df2 Mon Sep 17 00:00:00 2001 From: Paris Yu Date: Sat, 26 Sep 2026 20:37:20 +0800 Subject: [PATCH] fix: name the checksum algorithm in PutObject/UploadPart mismatch errors hash.ChecksumMismatch now carries the requested algorithm (CRC32, CRC32C, SHA1, SHA256, CRC64NVME), and toAPIError renders it S3-style: The CRC32 you specified did not match the calculated checksum. instead of the generic "The provided 'x-amz-checksum' header does not match what was computed.", for all four mismatch construction sites (streaming reader, trailing-checksum decode, and both Checksum.Matches paths). Mirrors the wording already shipped for CompleteMultipartUpload in #225. Fixes #226 --- cmd/api-errors.go | 8 ++++++++ cmd/object-handlers-chunked-checksum_test.go | 13 +++++++++---- internal/hash/checksum.go | 10 ++++++---- internal/hash/errors.go | 7 +++++-- internal/hash/reader.go | 7 ++++--- 5 files changed, 32 insertions(+), 13 deletions(-) diff --git a/cmd/api-errors.go b/cmd/api-errors.go index c60756d2393ca..7de44a39a4813 100644 --- a/cmd/api-errors.go +++ b/cmd/api-errors.go @@ -2505,6 +2505,14 @@ func toAPIError(ctx context.Context, err error) APIError { case errors.Is(err, errMissingPartChecksum): apiErr.Description = strings.TrimPrefix(err.Error(), errMissingPartChecksum.Error()+": ") } + // PutObject/UploadPart checksum mismatches name the algorithm in the + // message, matching S3 (e.g. "The CRC32 you specified did not match the + // calculated checksum."). Falls through to the generic table description + // when the algorithm is unknown. + var checksumMismatch hash.ChecksumMismatch + if errors.As(err, &checksumMismatch) && checksumMismatch.Algorithm != "" { + apiErr.Description = fmt.Sprintf("The %s you specified did not match the calculated checksum.", checksumMismatch.Algorithm) + } switch apiErr.Code { case "NotImplemented": apiErr = APIError{ diff --git a/cmd/object-handlers-chunked-checksum_test.go b/cmd/object-handlers-chunked-checksum_test.go index abd9f0d1680cf..85d1a611e49bb 100644 --- a/cmd/object-handlers-chunked-checksum_test.go +++ b/cmd/object-handlers-chunked-checksum_test.go @@ -62,12 +62,13 @@ func testAPIPutObjectChunkedChecksum(obj ObjectLayer, instanceType, bucketName s // A well-formed 4-byte value that does not match the content. wrongCRC := base64.StdEncoding.EncodeToString([]byte{0x01, 0x02, 0x03, 0x04}) - apiCode := func(rec *httptest.ResponseRecorder) string { + apiErrOf := func(rec *httptest.ResponseRecorder) APIErrorResponse { var apiErr APIErrorResponse b, _ := io.ReadAll(rec.Body) _ = xml.Unmarshal(b, &apiErr) - return apiErr.Code + return apiErr } + apiCode := func(rec *httptest.ResponseRecorder) string { return apiErrOf(rec).Code } // newChunkedJavaForm builds a non-trailer signed chunked PutObject request that // mirrors the AWS Java SDK v2 wire form: the checksum value sits in the header @@ -131,8 +132,12 @@ func testAPIPutObjectChunkedChecksum(obj ObjectLayer, instanceType, bucketName s if rec.Code != http.StatusBadRequest { t.Fatalf("%s: chunked+wrong CRC32: expected 400, got %d", instanceType, rec.Code) } - if code := apiCode(rec); code != "XAmzContentChecksumMismatch" { - t.Fatalf("%s: chunked+wrong CRC32: want XAmzContentChecksumMismatch, got %q", instanceType, code) + gotErr := apiErrOf(rec) + if gotErr.Code != "XAmzContentChecksumMismatch" { + t.Fatalf("%s: chunked+wrong CRC32: want XAmzContentChecksumMismatch, got %q", instanceType, gotErr.Code) + } + if gotErr.Message != "The CRC32 you specified did not match the calculated checksum." { + t.Fatalf("%s: chunked+wrong CRC32: message = %q, want S3 algorithm-specific wording", instanceType, gotErr.Message) } } } diff --git a/internal/hash/checksum.go b/internal/hash/checksum.go index f8238c506b676..73290ae696033 100644 --- a/internal/hash/checksum.go +++ b/internal/hash/checksum.go @@ -584,15 +584,17 @@ func (c Checksum) Matches(content []byte, parts int) error { sum := hasher.Sum(nil) if c.WantParts > 0 && c.WantParts != parts { return ChecksumMismatch{ - Want: fmt.Sprintf("%s-%d", c.Encoded, c.WantParts), - Got: fmt.Sprintf("%s-%d", base64.StdEncoding.EncodeToString(sum), parts), + Algorithm: c.Type.String(), + Want: fmt.Sprintf("%s-%d", c.Encoded, c.WantParts), + Got: fmt.Sprintf("%s-%d", base64.StdEncoding.EncodeToString(sum), parts), } } if !bytes.Equal(sum, c.Raw) { return ChecksumMismatch{ - Want: c.Encoded, - Got: base64.StdEncoding.EncodeToString(sum), + Algorithm: c.Type.String(), + Want: c.Encoded, + Got: base64.StdEncoding.EncodeToString(sum), } } return nil diff --git a/internal/hash/errors.go b/internal/hash/errors.go index 0d2f764909960..048f92eee03fe 100644 --- a/internal/hash/errors.go +++ b/internal/hash/errors.go @@ -74,8 +74,11 @@ func (e SizeMismatch) Error() string { // ChecksumMismatch - when content checksum does not match with what was sent from client. type ChecksumMismatch struct { - Want string - Got string + // Algorithm carries the requested checksum type (e.g. "CRC32") so API + // error rendering can name it, matching S3's wording. + Algorithm string + Want string + Got string } func (e ChecksumMismatch) Error() string { diff --git a/internal/hash/reader.go b/internal/hash/reader.go index ddae850a07c40..fc289f788d157 100644 --- a/internal/hash/reader.go +++ b/internal/hash/reader.go @@ -302,13 +302,14 @@ func (r *Reader) Read(p []byte) (int, error) { r.contentHash.Encoded = r.trailer.Get(r.contentHash.Type.Key()) r.contentHash.Raw, err = base64.StdEncoding.DecodeString(r.contentHash.Encoded) if err != nil || len(r.contentHash.Raw) == 0 { - return 0, ChecksumMismatch{Got: r.contentHash.Encoded} + return 0, ChecksumMismatch{Algorithm: r.contentHash.Type.String(), Got: r.contentHash.Encoded} } } if sum := r.contentHasher.Sum(nil); !bytes.Equal(r.contentHash.Raw, sum) { err := ChecksumMismatch{ - Want: r.contentHash.Encoded, - Got: base64.StdEncoding.EncodeToString(sum), + Algorithm: r.contentHash.Type.String(), + Want: r.contentHash.Encoded, + Got: base64.StdEncoding.EncodeToString(sum), } return n, err }