fix: prevent connection errors in space/quotas error paths

When uploads hit ENOSPC or EDQUOT, the server was returning the
correct S3 error but could close the connection while unread
request bytes were still in flight, which caused TCP resets and
surfaced as broken pipe/connection reset errors in SDKs instead
of a clean Insufficient Storage response. This change drains the
remaining upload body before returning the error so the response
can be delivered and the connection can close gracefully,
preserving correct client-visible behavior under disk-full and
quota-exceeded conditions.

Fixes #2209
This commit is contained in:
Ben McClelland
2026-06-24 09:18:25 -07:00
parent bf8b18839f
commit 9610ef8a4e
+19
View File
@@ -3024,9 +3024,11 @@ func (p *Posix) UploadPartWithPostFunc(ctx context.Context, input *s3.UploadPart
bucket, partPath, length, acct, doFalloc, p.forceNoTmpFile)
if err != nil {
if errors.Is(err, syscall.EDQUOT) {
drainBody(r)
return nil, s3err.GetAPIError(s3err.ErrQuotaExceeded)
}
if errors.Is(err, syscall.ENOSPC) {
drainBody(r)
return nil, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice)
}
return nil, fmt.Errorf("open temp file: %w", err)
@@ -3144,9 +3146,11 @@ func (p *Posix) UploadPartWithPostFunc(ctx context.Context, input *s3.UploadPart
_, err = io.Copy(f, tr)
if err != nil {
if errors.Is(err, syscall.EDQUOT) {
drainBody(tr)
return nil, s3err.GetAPIError(s3err.ErrQuotaExceeded)
}
if errors.Is(err, syscall.ENOSPC) {
drainBody(tr)
return nil, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice)
}
// Return the error itself, if it implements the s3err.S3Error interface
@@ -3793,9 +3797,11 @@ func (p *Posix) PutObjectWithPostFunc(ctx context.Context, po s3response.PutObje
*po.Bucket, *po.Key, contentLength, acct, doFalloc, p.forceNoTmpFile)
if err != nil {
if errors.Is(err, syscall.EDQUOT) {
drainBody(po.Body)
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrQuotaExceeded)
}
if errors.Is(err, syscall.ENOSPC) {
drainBody(po.Body)
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice)
}
return s3response.PutObjectOutput{}, fmt.Errorf("open temp file: %w", err)
@@ -3820,9 +3826,11 @@ func (p *Posix) PutObjectWithPostFunc(ctx context.Context, po s3response.PutObje
_, err = io.Copy(f, rdr)
if err != nil {
if errors.Is(err, syscall.EDQUOT) {
drainBody(rdr)
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrQuotaExceeded)
}
if errors.Is(err, syscall.ENOSPC) {
drainBody(rdr)
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice)
}
// Return the error itself, if it implements the s3err.S3Error interface
@@ -6764,6 +6772,17 @@ func getString(str *string) string {
return *str
}
// drainBody consumes and discards all remaining bytes from r.
// It is called after a server error is detected mid-stream so that
// the client can read the error response before the write-side of the
// connection is shut down.
func drainBody(r io.Reader) {
if r == nil {
return
}
_, _ = io.Copy(io.Discard, r)
}
func joinPathWithTrailer(paths ...string) string {
joined := filepath.Join(paths...)
if strings.HasSuffix(paths[len(paths)-1], "/") {