mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-12 01:20:45 +02: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
131 lines
4.4 KiB
Go
131 lines
4.4 KiB
Go
package s3api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/aws/smithy-go"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func requireAPIErrorCode(t *testing.T, err error, expected string) {
|
|
t.Helper()
|
|
require.Error(t, err)
|
|
var apiErr smithy.APIError
|
|
require.True(t, errors.As(err, &apiErr), "expected a smithy.APIError, got %T: %v", err, err)
|
|
assert.Equal(t, expected, apiErr.ErrorCode())
|
|
}
|
|
|
|
// TestConditionalReadsOfMissingObject verifies that a missing key stays a missing key
|
|
// under If-Match and If-Unmodified-Since instead of surfacing as 412.
|
|
// reproduces issue #10984
|
|
func TestConditionalReadsOfMissingObject(t *testing.T) {
|
|
client := getS3Client(t)
|
|
bucketName := getNewBucketName()
|
|
|
|
createBucket(t, client, bucketName)
|
|
defer deleteBucket(t, client, bucketName)
|
|
|
|
existing := putObject(t, client, bucketName, "etag-source", "content")
|
|
require.NotNil(t, existing.ETag)
|
|
future := aws.Time(time.Now().Add(24 * time.Hour))
|
|
missing := aws.String("conditional-missing")
|
|
|
|
t.Run("HeadObject If-Match", func(t *testing.T) {
|
|
_, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
|
Bucket: aws.String(bucketName), Key: missing, IfMatch: existing.ETag,
|
|
})
|
|
requireAPIErrorCode(t, err, "NotFound")
|
|
})
|
|
|
|
t.Run("HeadObject If-Unmodified-Since", func(t *testing.T) {
|
|
_, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
|
Bucket: aws.String(bucketName), Key: missing, IfUnmodifiedSince: future,
|
|
})
|
|
requireAPIErrorCode(t, err, "NotFound")
|
|
})
|
|
|
|
t.Run("GetObject If-Match", func(t *testing.T) {
|
|
_, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
|
|
Bucket: aws.String(bucketName), Key: missing, IfMatch: existing.ETag,
|
|
})
|
|
requireAPIErrorCode(t, err, "NoSuchKey")
|
|
})
|
|
|
|
t.Run("GetObject If-Unmodified-Since", func(t *testing.T) {
|
|
_, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
|
|
Bucket: aws.String(bucketName), Key: missing, IfUnmodifiedSince: future,
|
|
})
|
|
requireAPIErrorCode(t, err, "NoSuchKey")
|
|
})
|
|
|
|
t.Run("GetObject stale If-Match on a live object stays 412", func(t *testing.T) {
|
|
_, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
|
|
Bucket: aws.String(bucketName), Key: aws.String("etag-source"),
|
|
IfMatch: aws.String(`"0000000000000000000000000000dead"`),
|
|
})
|
|
requireAPIErrorCode(t, err, "PreconditionFailed")
|
|
})
|
|
}
|
|
|
|
// TestConditionalReadsOfNamedVersion verifies that a conditional GET or HEAD of an
|
|
// explicit versionId is evaluated against that version rather than the latest one,
|
|
// including when the latest version is a delete marker.
|
|
func TestConditionalReadsOfNamedVersion(t *testing.T) {
|
|
client := getS3Client(t)
|
|
bucketName := getNewBucketName()
|
|
|
|
createBucket(t, client, bucketName)
|
|
defer deleteBucket(t, client, bucketName)
|
|
enableVersioning(t, client, bucketName)
|
|
|
|
key := "conditional-read-version"
|
|
v1 := putObject(t, client, bucketName, key, "content-v1")
|
|
require.NotNil(t, v1.ETag)
|
|
require.NotNil(t, v1.VersionId)
|
|
v2 := putObject(t, client, bucketName, key, "content-v2")
|
|
require.NotNil(t, v2.ETag)
|
|
require.NotEqual(t, *v1.ETag, *v2.ETag)
|
|
|
|
t.Run("If-Match matches the named version, not the latest", func(t *testing.T) {
|
|
_, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
|
|
Bucket: aws.String(bucketName), Key: aws.String(key),
|
|
VersionId: v1.VersionId, IfMatch: v1.ETag,
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("If-Match against the latest ETag fails on the named version", func(t *testing.T) {
|
|
_, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
|
|
Bucket: aws.String(bucketName), Key: aws.String(key),
|
|
VersionId: v1.VersionId, IfMatch: v2.ETag,
|
|
})
|
|
requireAPIErrorCode(t, err, "PreconditionFailed")
|
|
})
|
|
|
|
_, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
|
Bucket: aws.String(bucketName), Key: aws.String(key),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("named version survives a delete marker on the latest", func(t *testing.T) {
|
|
_, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
|
Bucket: aws.String(bucketName), Key: aws.String(key),
|
|
VersionId: v1.VersionId, IfMatch: v1.ETag,
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("delete marker latest is a missing object", func(t *testing.T) {
|
|
_, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
|
|
Bucket: aws.String(bucketName), Key: aws.String(key), IfMatch: v1.ETag,
|
|
})
|
|
requireAPIErrorCode(t, err, "NoSuchKey")
|
|
})
|
|
}
|