From 3357c14e91bdcf75e4ef059ea53396398f8bac69 Mon Sep 17 00:00:00 2001 From: ys-ll <114486081+ys-ll@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:18:31 +0800 Subject: [PATCH] fix: support zero-byte uploads in FilePut Reading the body into a zero-length buffer returned io.EOF on an already-exhausted reader, so empty files and directory markers always failed. Use io.ReadFull and send an explicit Content-Length: 0 for empty bodies instead of falling back to chunked encoding. Also fix an error variable mix-up in the NewRequest error check. --- object.go | 14 ++++++++++--- object_zerobody_test.go | 46 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 object_zerobody_test.go diff --git a/object.go b/object.go index 3275e10..2a7e2d5 100644 --- a/object.go +++ b/object.go @@ -157,16 +157,24 @@ func (s3 *S3) FilePut(u UploadInput) (PutResponse, error) { } content := make([]byte, fSize) - _, err = u.Body.Read(content) + // io.ReadFull succeeds for a zero-length read (empty body) and reports + // ErrUnexpectedEOF if the body is shorter than its size, unlike a bare + // Read which returns io.EOF on an already-exhausted reader. + _, err = io.ReadFull(u.Body, content) if err != nil { return PutResponse{}, err } u.Body.Seek(0, 0) - req, er := http.NewRequest(http.MethodPut, s3.getURL(u.Bucket, u.ObjectKey), u.Body) - if er != nil { + req, err := http.NewRequest(http.MethodPut, s3.getURL(u.Bucket, u.ObjectKey), u.Body) + if err != nil { return PutResponse{}, err } + // A non-nil empty body makes net/http fall back to chunked encoding and + // omit Content-Length; nil body sends a clean "Content-Length: 0". + if fSize == 0 { + req.Body = nil + } if u.ContentType == "" { u.ContentType = "application/octet-stream" diff --git a/object_zerobody_test.go b/object_zerobody_test.go new file mode 100644 index 0000000..cbc7ece --- /dev/null +++ b/object_zerobody_test.go @@ -0,0 +1,46 @@ +package simples3 + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "testing" +) + +// A zero-byte upload (empty file, directory marker) must succeed: FilePut +// used to fail with io.EOF because it read the body into a zero-length +// buffer before sending the request. +func TestS3_FilePut_EmptyBody(t *testing.T) { + var gotBody []byte + var gotContentLength string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotContentLength = r.Header.Get("Content-Length") + gotBody, _ = io.ReadAll(r.Body) + w.Header().Set("ETag", `"d41d8cd98f00b204e9800998ecf8427e"`) + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + s3 := New("us-east-1", "test-access", "test-secret") + s3.SetEndpoint(srv.URL) + + resp, err := s3.FilePut(UploadInput{ + Bucket: "test-bucket", + ObjectKey: "empty-dir/", + ContentType: "application/x-directory", + Body: bytes.NewReader(nil), + }) + if err != nil { + t.Fatalf("S3.FilePut() with empty body returned error: %v", err) + } + if resp.ETag == "" { + t.Errorf("S3.FilePut() returned empty response: %+v", resp) + } + if gotContentLength != "0" { + t.Errorf("Content-Length = %q, want %q", gotContentLength, "0") + } + if len(gotBody) != 0 { + t.Errorf("request body = %q, want empty", gotBody) + } +}