Files
seaweedfs/weed/server/filer_grpc_server_test.go
T
Eliah RusinandClaude Opus 5.5 164c3db606 s3: return 403, not 500, when an over-quota bucket refuses a write (#11552)
* s3: return 403, not 500, when an over-quota bucket refuses a write

Filer AssignVolume flattened ErrReadOnly into the free-text
AssignVolumeResponse.Error string, so S3 PutObject / PutObjectPart via
UploadReaderInChunks could not match it with errors.Is and fell through
to 500 InternalError: retryable, and it hides the quota.

Add FilerError READ_ONLY and AssignVolumeResponse.error_code, set it
alongside the unchanged error text, and rebuild the sentinel with
filer_pb.AssignVolumeResponseError. weed_server.ErrReadOnly now aliases
filer_pb.ErrReadOnly so errors.Is matches on both sides, and
mapChunkedUploadErrorToS3Error maps it to ErrAccessDenied. There is no
"read only" substring matching, so a volume server's "volume N is read
only" stays retryable.

Carrying the verdict as a response code rather than a gRPC status keeps
clients from treating it as a transport failure: the S3 gateway does not
fail over across filers and the Java client does not retry it.

Wrap per-chunk copy errors with %w so CopyObject keeps the sentinel, and
map UploadPartCopy chunk errors through mapCopyErrorToS3Error instead of
always returning 500.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci: re-run integration tests (PyPI download timeout)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 22:21:03 +08:00

97 lines
2.8 KiB
Go

package weed_server
import (
"context"
"errors"
"strings"
"testing"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
)
func newAssignTestServer(fc *filer.FilerConf, diskType string) *FilerServer {
return &FilerServer{
option: &FilerOption{
DiskType: diskType,
},
filer: &filer.Filer{
DirBucketsPath: "/buckets",
FilerConf: fc,
MaxFilenameLength: 255,
},
}
}
func TestResolveAssignStorageOptionUsesBucketRuleBeforeFilerDiskDefault(t *testing.T) {
fc := filer.NewFilerConf()
if err := fc.SetLocationConf(&filer_pb.FilerConf_PathConf{
LocationPrefix: "/buckets/zot",
DiskType: "disk",
}); err != nil {
t.Fatalf("set location conf: %v", err)
}
fs := newAssignTestServer(fc, "hdd")
so, err := fs.resolveAssignStorageOption(context.Background(), &filer_pb.AssignVolumeRequest{
Path: "/buckets/zot/.uploads/upload-id/0001_part.part",
})
if err != nil {
t.Fatalf("resolve assign storage option: %v", err)
}
if got, want := so.Collection, "zot"; got != want {
t.Fatalf("collection = %q, want %q", got, want)
}
if got, want := so.DiskType, "disk"; got != want {
t.Fatalf("disk type = %q, want %q", got, want)
}
}
func TestResolveAssignStorageOptionFallsBackToFilerDiskDefault(t *testing.T) {
fs := newAssignTestServer(filer.NewFilerConf(), "hdd")
so, err := fs.resolveAssignStorageOption(context.Background(), &filer_pb.AssignVolumeRequest{
Path: "/tmp/unmatched/file.bin",
})
if err != nil {
t.Fatalf("resolve assign storage option: %v", err)
}
if got, want := so.DiskType, "hdd"; got != want {
t.Fatalf("disk type = %q, want %q", got, want)
}
}
// A read-only path is a verdict, not a transport failure: it must come back as a
// successful RPC (so clients neither retry nor fail over) carrying READ_ONLY.
func TestAssignVolumeReadOnlyReturnsErrorCode(t *testing.T) {
fc := filer.NewFilerConf()
if err := fc.SetLocationConf(&filer_pb.FilerConf_PathConf{
LocationPrefix: "/buckets/overquota",
ReadOnly: true,
}); err != nil {
t.Fatalf("set location conf: %v", err)
}
fs := newAssignTestServer(fc, "")
resp, err := fs.AssignVolume(context.Background(), &filer_pb.AssignVolumeRequest{
Path: "/buckets/overquota/x",
})
if err != nil {
t.Fatalf("AssignVolume err = %v, want a response", err)
}
if resp.ErrorCode != filer_pb.FilerError_READ_ONLY {
t.Fatalf("AssignVolume error_code = %v, want %v", resp.ErrorCode, filer_pb.FilerError_READ_ONLY)
}
// Clients that predate error_code still get the old message.
if want := "assign volume: read only: /buckets/overquota"; !strings.HasPrefix(resp.Error, want) {
t.Fatalf("AssignVolume error = %q, want prefix %q", resp.Error, want)
}
if err := filer_pb.AssignVolumeResponseError(resp); !errors.Is(err, ErrReadOnly) {
t.Fatalf("AssignVolumeResponseError = %v, want ErrReadOnly", err)
}
}