From 221a5c9e2539e3f29ac7fc946713b85e4d21145a Mon Sep 17 00:00:00 2001 From: Ben McClelland Date: Thu, 28 May 2026 14:56:37 -0700 Subject: [PATCH] fix: connection early termination resulting in internal error When the connection terminates before all bytes read, we were getting an io.ErrUnexpectedEOF that was not being handled as a standard io.EOF resulting in an internal error being raised. Translate io.ErrUnexpectedEOF to io.EOF so that we return the normal errors for unexpected content. Add a log message so that its clear the error is due to the connection being terminated before all data sent and not the fault of the gateway. --- s3api/utils/csum-reader.go | 8 ++++++++ s3api/utils/signed-chunk-reader.go | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/s3api/utils/csum-reader.go b/s3api/utils/csum-reader.go index ec1964d5a..966e45f36 100644 --- a/s3api/utils/csum-reader.go +++ b/s3api/utils/csum-reader.go @@ -30,6 +30,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/cespare/xxhash/v2" + "github.com/versity/versitygw/debuglogger" "github.com/versity/versitygw/s3err" "github.com/zeebo/xxh3" ) @@ -127,6 +128,13 @@ func NewHashReader(r io.Reader, expectedSum string, ht HashType) (*HashReader, e // Read allows *HashReader to be used as an io.Reader func (hr *HashReader) Read(p []byte) (int, error) { n, readerr := hr.r.Read(p) + // Treat ErrUnexpectedEOF as EOF so a truncated body triggers checksum + // validation (which will fail on partial data) rather than leaking a + // raw Go error as an internal server error. + if readerr == io.ErrUnexpectedEOF { + debuglogger.Infof("client connection terminated early") + readerr = io.EOF + } _, err := hr.hash.Write(p[:n]) if err != nil { return n, err diff --git a/s3api/utils/signed-chunk-reader.go b/s3api/utils/signed-chunk-reader.go index 0782d7193..4e6479a21 100644 --- a/s3api/utils/signed-chunk-reader.go +++ b/s3api/utils/signed-chunk-reader.go @@ -124,6 +124,14 @@ func NewSignedChunkReader(r io.Reader, authdata AuthData, canonicalString, secre // Read satisfies the io.Reader for this type func (cr *ChunkReader) Read(p []byte) (int, error) { n, err := cr.r.Read(p) + // Treat ErrUnexpectedEOF as EOF so a connection that closes before + // all Content-Length bytes arrive follows the normal EOF path and + // returns a proper S3 error (e.g. ErrContentLengthMismatch or + // SignatureDoesNotMatch) instead of leaking an internal Go error. + if err == io.ErrUnexpectedEOF { + debuglogger.Infof("client connection terminated early") + err = io.EOF + } if err != nil && err != io.EOF { return 0, err }