Files
seaweedfs/weed/s3api/s3api_object_range_test.go
T
Jay HemnaniandChris Lu a3e97ec709 s3: apply Range to HeadObject (#11856)
* s3: apply Range to HeadObject

* s3: apply Range to a HEAD of a stored directory key

Directory-key entries answer through serveDirectoryContent before
HeadObjectHandler's range branch, so a ranged HEAD returned 200 and the
full length.

* s3: keep the missing-directory check for plain directories only

A stored directory-key marker is an object, so a range on its empty body
answers 416 like on any other zero-byte object.

* s3: let partNumber keep its priority over Range on a directory key HEAD

* s3: serve ranged GETs of a stored directory key

The same branch now applies the validated range to the inline content on
GET, so a directory-key object answers 206 with the selected bytes like
any other object.

---------

Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
2026-10-21 13:37:00 -07:00

73 lines
2.4 KiB
Go

package s3api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
)
func TestParseAndValidateRangeRejectsMalformedNumericOffsets(t *testing.T) {
s3a := &S3ApiServer{}
entry := &filer_pb.Entry{}
tests := []struct {
name string
rangeHeader string
}{
{"invalid start", "bytes=abc-5"},
{"invalid end", "bytes=5-abc"},
{"invalid open ended start", "bytes=abc-"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/bucket/object", nil)
req.Header.Set("Range", tt.rangeHeader)
rec := httptest.NewRecorder()
_, _, _, streamErr := s3a.parseAndValidateRange(rec, req, entry, 100, "bucket", "object")
if streamErr == nil {
t.Fatalf("parseAndValidateRange(%q) error = nil, want invalid range error", tt.rangeHeader)
}
if rec.Code != http.StatusRequestedRangeNotSatisfiable {
t.Fatalf("parseAndValidateRange(%q) status = %d, want %d", tt.rangeHeader, rec.Code, http.StatusRequestedRangeNotSatisfiable)
}
if got := rec.Header().Get("Content-Range"); got != "bytes */100" {
t.Fatalf("parseAndValidateRange(%q) Content-Range = %q, want %q", tt.rangeHeader, got, "bytes */100")
}
})
}
}
func TestHeadObjectRange(t *testing.T) {
s3a, f := newAnonymousReadTestServer(t)
f.entries["/buckets/b/o"] = anonymousReadEntry(anonymousReadGrant("READ"))
for _, tc := range []struct {
rangeHeader, contentRange, contentLength string
code int
}{
{"", "", "11", http.StatusOK},
{"bytes=1-4", "bytes 1-4/11", "4", http.StatusPartialContent},
{"bytes=-3", "bytes 8-10/11", "3", http.StatusPartialContent},
{"bytes=5-", "bytes 5-10/11", "6", http.StatusPartialContent},
{"bytes=20-", "bytes */11", "", http.StatusRequestedRangeNotSatisfiable},
} {
headers := map[string]string{}
if tc.rangeHeader != "" {
headers["Range"] = tc.rangeHeader
}
rr := serveAnonymousRead(s3a, http.MethodHead, "o", "", headers)
if rr.Code != tc.code {
t.Errorf("Range %q: status %d, want %d", tc.rangeHeader, rr.Code, tc.code)
}
if got := rr.Header().Get("Content-Range"); got != tc.contentRange {
t.Errorf("Range %q: Content-Range %q, want %q", tc.rangeHeader, got, tc.contentRange)
}
if got := rr.Header().Get("Content-Length"); tc.contentLength != "" && got != tc.contentLength {
t.Errorf("Range %q: Content-Length %q, want %q", tc.rangeHeader, got, tc.contentLength)
}
}
}