mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-01 13:47:51 +00:00
* s3: keep a missing object a 404 under If-Match and If-Unmodified-Since GET and HEAD resolved the target before evaluating the conditional headers, and a missing target failed If-Match and If-Unmodified-Since outright, so absence surfaced as 412 PreconditionFailed. AWS reports the missing object instead: 404 for HeadObject, NoSuchKey for GetObject, and 412 only when a live object fails the condition. Clients cannot tell absence from a stale precondition without an extra racy HEAD, so OpenDAL disabled its four conditional stat/read capabilities against SeaweedFS. A precondition now only fails against an object that exists; a missing one -- including a latest version that is a delete marker -- returns NoSuchKey. Claude-Session: https://claude.ai/code/session_01X4kEbuwxd9DFsTnSXjfjgv * s3: evaluate a conditional read against the version the request names GET and HEAD resolved the latest version before evaluating the conditional headers, so a request carrying versionId had its If-Match compared against a different version than the one it was asking for: a live version whose ETag the client held failed once a newer version -- or a delete marker -- became the latest. resolveObjectEntry now resolves the named version on a versioned bucket, the way DELETE already does. A named version that resolves to nothing is left to the handler, which alone knows whether the bucket is versioned and so whether it owes NoSuchVersion. Claude-Session: https://claude.ai/code/session_01X4kEbuwxd9DFsTnSXjfjgv
61 lines
2.4 KiB
Go
61 lines
2.4 KiB
Go
package s3api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
)
|
|
|
|
// A precondition can only fail against an object that exists. GET/HEAD of a missing
|
|
// key must stay a missing-key answer even when If-Match or If-Unmodified-Since is sent.
|
|
func TestValidateConditionalHeadersForReadsMissingObject(t *testing.T) {
|
|
s3a := &S3ApiServer{}
|
|
|
|
existing := &filer_pb.Entry{
|
|
Attributes: &filer_pb.FuseAttributes{Mtime: time.Now().Unix()},
|
|
Extended: map[string][]byte{s3_constants.ExtETagKey: []byte("d41d8cd98f00b204e9800998ecf8427e")},
|
|
}
|
|
deleteMarker := &filer_pb.Entry{
|
|
Attributes: &filer_pb.FuseAttributes{Mtime: time.Now().Unix()},
|
|
Extended: map[string][]byte{s3_constants.ExtDeleteMarkerKey: []byte("true")},
|
|
}
|
|
future := time.Now().Add(24 * time.Hour).UTC().Format(http.TimeFormat)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
header string
|
|
value string
|
|
entry *filer_pb.Entry
|
|
want s3err.ErrorCode
|
|
}{
|
|
{"if-match on missing object", s3_constants.IfMatch, "0000", nil, s3err.ErrNoSuchKey},
|
|
{"if-match star on missing object", s3_constants.IfMatch, "*", nil, s3err.ErrNoSuchKey},
|
|
{"if-unmodified-since on missing object", s3_constants.IfUnmodifiedSince, future, nil, s3err.ErrNoSuchKey},
|
|
{"if-match on delete marker", s3_constants.IfMatch, "0000", deleteMarker, s3err.ErrNoSuchKey},
|
|
{"if-none-match on missing object", s3_constants.IfNoneMatch, "*", nil, s3err.ErrNone},
|
|
{"if-modified-since on missing object", s3_constants.IfModifiedSince, future, nil, s3err.ErrNone},
|
|
{"if-match mismatch on existing object", s3_constants.IfMatch, "0000", existing, s3err.ErrPreconditionFailed},
|
|
{"if-match hit on existing object", s3_constants.IfMatch, "d41d8cd98f00b204e9800998ecf8427e", existing, s3err.ErrNone},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := httptest.NewRequest(http.MethodGet, "/bucket/object", nil)
|
|
r.Header.Set(tc.header, tc.value)
|
|
headers, errCode := parseConditionalHeaders(r)
|
|
if errCode != s3err.ErrNone {
|
|
t.Fatalf("parseConditionalHeaders: %v", errCode)
|
|
}
|
|
result := s3a.validateConditionalHeadersForReads(r, headers, tc.entry, "bucket", "object")
|
|
if result.ErrorCode != tc.want {
|
|
t.Errorf("got %v, want %v", result.ErrorCode, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|