Merge pull request #229 from yupoet/fix/putobject-checksum-error-message-226

Preserve the checksum rejection status and code while naming the checksum algorithm in PutObject and UploadPart error messages.

Validated on the branch updated to main: all six Go CI jobs and VulnCheck passed. Additional local coverage checked all five algorithms, wrapped errors, trailing checksums, multipart count mismatches and the generic fallback.

The maintainer explicitly accepted this contribution without the original author's DCO trailer as a one-time exception. Original authorship and the original commit are retained; no sign-off has been added on the author's behalf.

Fixes #226

Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-10-15 21:22:45 +08:00
committed by GitHub
5 changed files with 32 additions and 13 deletions
+8
View File
@@ -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{
+9 -4
View File
@@ -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)
}
}
}
+6 -4
View File
@@ -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
+5 -2
View File
@@ -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 {
+4 -3
View File
@@ -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
}