From f96e477034f0ea8095516eb12c006ca03e6c6bd7 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Sat, 27 Jun 2026 16:42:49 -0700 Subject: [PATCH] fix: ListObjects should return NoSuchBucket for prefix on missing bucket ListObjects on a non-existent bucket returned an empty result instead of NoSuchBucket whenever the request hit one of listPath's storage-bypassing shortcuts - most visibly a slash-only prefix such as Prefix="/". AWS S3 returns NoSuchBucket regardless of the prefix/marker/max-keys supplied. This regressed in 80ca12008 ("remove checkBucketExist check entirely to avoid fan-out calls"), which dropped the bucket existence check from checkListObjsArgs. The normal listing path still surfaces a missing bucket via the per-set listing (errVolumeNotFound), but the early-return shortcuts in listPath (slash-only prefix, zero limit, marker outside the prefix) return io.EOF before any set is consulted, masking the missing bucket as an empty listing. Verify bucket existence (via the existing GetBucketInfo, matching the pattern already used by ListMultipartUploads) only on those shortcut paths, so the hot listing path keeps avoiding the fan-out. Fixes #32 Co-Authored-By: Claude Opus 4.8 (1M context) --- cmd/metacache-server-pool.go | 21 ++++++++++++++++++--- cmd/object_api_suite_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/cmd/metacache-server-pool.go b/cmd/metacache-server-pool.go index bcdfed8f10ff4..8e23f811a7e30 100644 --- a/cmd/metacache-server-pool.go +++ b/cmd/metacache-server-pool.go @@ -71,13 +71,13 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) ( if o.Marker != "" && o.Prefix != "" { // Marker not common with prefix is not implemented. Send an empty response if !HasPrefix(o.Marker, o.Prefix) { - return entries, io.EOF + return entries, z.listPathShortcutEOF(ctx, o.Bucket) } } // With max keys of zero we have reached eof, return right here. if o.Limit == 0 { - return entries, io.EOF + return entries, z.listPathShortcutEOF(ctx, o.Bucket) } // For delimiter and prefix as '/' we do not list anything at all @@ -85,7 +85,7 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) ( // as '/' we don't have any entries, since all the keys are // of form 'keyName/...' if strings.HasPrefix(o.Prefix, SlashSeparator) { - return entries, io.EOF + return entries, z.listPathShortcutEOF(ctx, o.Bucket) } // If delimiter is slashSeparator we must return directories of @@ -254,6 +254,21 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) ( return entries, nil } +// listPathShortcutEOF is used by the listPath shortcuts that return an empty +// result without ever consulting the storage layer (a slash-only prefix, a +// zero limit, or a marker outside the prefix). Because they bypass the per-set +// listing - which is what surfaces a missing bucket as errVolumeNotFound - they +// must verify bucket existence explicitly, otherwise a listing on a +// non-existent bucket is masked as an empty result. AWS S3 returns NoSuchBucket +// regardless of the prefix/marker/max-keys supplied. +// See https://github.com/pgsty/minio/issues/32 +func (z *erasureServerPools) listPathShortcutEOF(ctx context.Context, bucket string) error { + if _, err := z.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil { + return err + } + return io.EOF +} + // listMerged will list across all sets and return a merged results stream. // The result channel is closed when no more results are expected. func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions, results chan<- metaCacheEntry) error { diff --git a/cmd/object_api_suite_test.go b/cmd/object_api_suite_test.go index 3377ad208d2f5..9d9bf96814232 100644 --- a/cmd/object_api_suite_test.go +++ b/cmd/object_api_suite_test.go @@ -849,6 +849,41 @@ func testListObjectsTestsForNonExistentBucket(obj ObjectLayer, instanceType stri } } +// Wrapper for calling testListObjectsWithPrefixForNonExistentBucket for both Erasure and FS. +func TestListObjectsWithPrefixForNonExistentBucket(t *testing.T) { + ExecObjectLayerTest(t, testListObjectsWithPrefixForNonExistentBucket) +} + +// Tests validate that ListObjects on a non-existent bucket fails with +// BucketNotFound even when a prefix (or other listing shortcut) is supplied. +// AWS S3 returns NoSuchBucket regardless of the prefix/marker/max-keys values. +// Regression: https://github.com/pgsty/minio/issues/32 +func testListObjectsWithPrefixForNonExistentBucket(obj ObjectLayer, instanceType string, t TestErrHandler) { + testCases := []struct { + prefix string + marker string + maxKeys int + }{ + {prefix: "/", maxKeys: 1000}, // slash-only prefix shortcut (the issue reproduction) + {prefix: "foo/bar", maxKeys: 1000}, // regular prefix + {prefix: "obj", maxKeys: 1}, // max-keys==1 shortcut + {prefix: "obj", maxKeys: 0}, // zero-limit shortcut + {prefix: "a", marker: "b", maxKeys: 1000}, // marker outside the prefix shortcut + } + for i, tc := range testCases { + result, err := obj.ListObjects(context.Background(), "bucket", tc.prefix, tc.marker, "", tc.maxKeys) + if err == nil { + t.Fatalf("%s: test %d (prefix %q): Expected error but found nil.", instanceType, i+1, tc.prefix) + } + if len(result.Objects) != 0 { + t.Fatalf("%s: test %d (prefix %q): Expected 0 objects, but found %d", instanceType, i+1, tc.prefix, len(result.Objects)) + } + if err.Error() != "Bucket not found: bucket" { + t.Errorf("%s: test %d (prefix %q): Expected error msg `%s`, but found `%s`", instanceType, i+1, tc.prefix, "Bucket not found: bucket", err.Error()) + } + } +} + // Wrapper for calling testNonExistentObjectInBucket for both Erasure and FS. func TestNonExistentObjectInBucket(t *testing.T) { ExecObjectLayerTest(t, testNonExistentObjectInBucket)