From 9610ef8a4e1d83c982ba90cc7213dacb4b1b48cd Mon Sep 17 00:00:00 2001 From: Ben McClelland Date: Wed, 24 Jun 2026 09:14:18 -0700 Subject: [PATCH] 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 --- backend/posix/posix.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 5115205b..bf6e1a8e 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -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], "/") {