From 38e14a867b50dac05229906cc223d57fee5f5cc1 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Wed, 18 Feb 2026 17:14:54 -0800 Subject: [PATCH] fix: cancel volume server requests on client disconnect during S3 downloads (#8373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: cancel volume server requests on client disconnect during S3 downloads - Use http.NewRequestWithContext in ReadUrlAsStream so in-flight volume server requests are properly aborted when the client disconnects and the request context is canceled - Distinguish context-canceled errors (client disconnect, expected) from real server errors in streamFromVolumeServers; log at V(3) instead of ERROR to reduce noise from client-side disconnects (e.g. Nginx upstream timeout, browser cancel, curl --max-time) Fixes: streamFromVolumeServers: streamFn failed...context canceled" * fixup: separate Canceled/DeadlineExceeded log severity in streamFromVolumeServers - context.Canceled → V(3) Infof "client disconnected" (expected, no noise) - context.DeadlineExceeded → Warningf "server-side deadline exceeded" (unexpected, needs attention) - all other errors → Errorf (unchanged)" --- weed/s3api/s3api_object_handlers.go | 11 ++++++++++- weed/util/http/http_global_client_util.go | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/weed/s3api/s3api_object_handlers.go b/weed/s3api/s3api_object_handlers.go index 6a209485f..2a401ac29 100644 --- a/weed/s3api/s3api_object_handlers.go +++ b/weed/s3api/s3api_object_handlers.go @@ -1093,7 +1093,16 @@ func (s3a *S3ApiServer) streamFromVolumeServers(w http.ResponseWriter, r *http.R BucketTrafficSent(cw.written, r) } if err != nil { - glog.Errorf("streamFromVolumeServers: streamFn failed after writing %d bytes: %v", cw.written, err) + switch { + case errors.Is(err, context.Canceled): + // Client disconnected mid-stream (e.g. Nginx upstream timeout, browser cancel) - expected + glog.V(3).Infof("streamFromVolumeServers: client disconnected after writing %d bytes: %v", cw.written, err) + case errors.Is(err, context.DeadlineExceeded): + // Server-side deadline exceeded - unexpected, warrants operator attention + glog.Warningf("streamFromVolumeServers: server-side deadline exceeded after writing %d bytes: %v", cw.written, err) + default: + glog.Errorf("streamFromVolumeServers: streamFn failed after writing %d bytes: %v", cw.written, err) + } // Streaming error after WriteHeader was called - response already partially written return newStreamErrorWithResponse(err) } diff --git a/weed/util/http/http_global_client_util.go b/weed/util/http/http_global_client_util.go index a374c8a2b..612d538ec 100644 --- a/weed/util/http/http_global_client_util.go +++ b/weed/util/http/http_global_client_util.go @@ -311,11 +311,11 @@ func ReadUrlAsStream(ctx context.Context, fileUrl, jwt string, cipherKey []byte, return readEncryptedUrl(ctx, fileUrl, jwt, cipherKey, isContentGzipped, isFullChunk, offset, size, fn) } - req, err := http.NewRequest(http.MethodGet, fileUrl, nil) - maybeAddAuth(req, jwt) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileUrl, nil) if err != nil { return false, err } + maybeAddAuth(req, jwt) if isFullChunk { req.Header.Add("Accept-Encoding", "gzip")