From aba5c11eab888d702c7bf49ccca0de11a1c2a676 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Thu, 30 Jul 2026 11:44:38 +0100 Subject: [PATCH] shade: fix uploads failing with EOF when completing multipart uploads The multipart upload complete endpoint returns 200 with an empty body, but rclone tried to decode that body as JSON, failed with EOF and retried until the retries ran out, so every upload failed even though the server had actually completed it. Fixed by not attempting to decode the response body. This was a regression introduced in a4972de505afc1cb shade: retry server errors instead of failing the transfer which started treating the JSON decode error as fatal where previously it was accidentally ignored. --- backend/shade/upload.go | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/backend/shade/upload.go b/backend/shade/upload.go index 89f62bac2..e86cecc02 100644 --- a/backend/shade/upload.go +++ b/backend/shade/upload.go @@ -17,7 +17,6 @@ import ( "github.com/rclone/rclone/backend/shade/api" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/chunksize" - "github.com/rclone/rclone/fs/fserrors" "github.com/rclone/rclone/lib/multipart" "github.com/rclone/rclone/lib/rest" ) @@ -255,30 +254,20 @@ func (s *shadeChunkWriter) Close(ctx context.Context) error { return err } + // The complete response has an empty body so don't attempt to decode it completeOpts := rest.Opts{ - Method: "POST", - Path: fmt.Sprintf("/%s/upload/multipart/complete?token=%s", s.f.drive, url.QueryEscape(s.initToken)), - RootURL: s.f.endpoint, + Method: "POST", + Path: fmt.Sprintf("/%s/upload/multipart/complete?token=%s", s.f.drive, url.QueryEscape(s.initToken)), + RootURL: s.f.endpoint, + NoResponse: true, ExtraHeaders: map[string]string{ "Authorization": "Bearer " + token, }, } - var response http.Response - err = s.f.pacer.Call(func() (bool, error) { - res, err := s.f.srv.CallJSON(ctx, &completeOpts, completeBody, &response) - - if err != nil { - return shouldRetry(ctx, res, err) - } - - if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusCreated { - body, _ := io.ReadAll(res.Body) - return fserrors.ShouldRetryHTTP(res, retryErrorCodes), fmt.Errorf("complete multipart failed with status %d: %s", res.StatusCode, string(body)) - } - - return false, nil + res, err := s.f.srv.CallJSON(ctx, &completeOpts, completeBody, nil) + return shouldRetry(ctx, res, err) }) if err != nil {