Skip to content

Fix ListBucket in s3bolt backend to honor page.Marker and page.MaxKeys#114

Open
Aliexe-code wants to merge 2 commits intojohannesboyne:masterfrom
Aliexe-code:fix/issue-66-s3bolt-marker
Open

Fix ListBucket in s3bolt backend to honor page.Marker and page.MaxKeys#114
Aliexe-code wants to merge 2 commits intojohannesboyne:masterfrom
Aliexe-code:fix/issue-66-s3bolt-marker

Conversation

@Aliexe-code
Copy link

Fixes #66

The s3bolt backend now properly supports pagination with the marker parameter and maxKeys limit, matching the behavior of the s3mem backend.

Changes:

  • Remove ErrInternalPageNotImplemented error for non-empty pages
  • Implement page.Marker support using c.Seek() to jump to the marker
  • Implement page.MaxKeys support with proper IsTruncated and NextMarker
  • Add comprehensive tests for marker-based pagination

@Aliexe-code Aliexe-code force-pushed the fix/issue-66-s3bolt-marker branch from 7f04192 to c022794 Compare February 7, 2026 21:47
Fixes johannesboyne#66

The s3bolt backend now properly supports pagination with the marker parameter and maxKeys limit, matching the behavior of the s3mem backend.

Changes:
- Remove ErrInternalPageNotImplemented error for non-empty pages
- Implement page.Marker support using c.Seek() to jump to the marker
- Implement page.MaxKeys support with proper IsTruncated and NextMarker
- Add comprehensive tests for marker-based pagination
- Refactor tests with helper functions to reduce code duplication
@Aliexe-code Aliexe-code force-pushed the fix/issue-66-s3bolt-marker branch from c022794 to 9169291 Compare February 7, 2026 21:57
@codecov
Copy link

codecov bot commented Feb 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 51.70%. Comparing base (ebf3e50) to head (48b337d).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #114      +/-   ##
==========================================
+ Coverage   49.35%   51.70%   +2.34%     
==========================================
  Files          47       47              
  Lines        5155     4000    -1155     
==========================================
- Hits         2544     2068     -476     
+ Misses       2299     1615     -684     
- Partials      312      317       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Owner

@johannesboyne johannesboyne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Aliexe-code - thanks a lot for the contribution! Just some minor remarks - see comments.

For testing, these tests could be added

package s3bolt

import (
    "os"
    "strings"
    "testing"

    "github.com/johannesboyne/gofakes3"
)

func mkBackend(t *testing.T, objs []string) (*Backend, func()) {
    t.Helper()
    f, err := os.CreateTemp("", "pr114-*.db")
    if err != nil { t.Fatal(err) }
    _ = f.Close()
    b, err := NewFile(f.Name())
    if err != nil { t.Fatal(err) }
    if err := b.CreateBucket("b"); err != nil { t.Fatal(err) }
    for _, o := range objs {
        if _, err := b.PutObject("b", o, nil, strings.NewReader(o), int64(len(o)), nil); err != nil { t.Fatal(err) }
    }
    return b, func(){ _ = os.Remove(f.Name()) }
}

func TestRepro_NextMarkerEmptyWithDelimiter(t *testing.T) {
    b, done := mkBackend(t, []string{"a/1", "a/2", "a/3", "b/1"})
    defer done()
    p := &gofakes3.Prefix{HasDelimiter: true, Delimiter: "/"}
    out, err := b.ListBucket("b", p, gofakes3.ListBucketPage{MaxKeys:1})
    if err != nil { t.Fatal(err) }
    if !out.IsTruncated { t.Fatalf("expected truncated") }
    if out.NextMarker == "" { t.Fatalf("expected non-empty next marker, got empty") }
}

func TestRepro_TruncatedShouldRespectPrefixFilter(t *testing.T) {
    b, done := mkBackend(t, []string{"x/1", "x/2", "y/1"})
    defer done()
    p := &gofakes3.Prefix{HasPrefix:true, Prefix:"x/"}
    out, err := b.ListBucket("b", p, gofakes3.ListBucketPage{MaxKeys:2})
    if err != nil { t.Fatal(err) }
    if out.IsTruncated { t.Fatalf("expected not truncated when no more matching keys") }
}

func TestRepro_DuplicateCommonPrefixCountsTowardMaxKeys(t *testing.T) {
    b, done := mkBackend(t, []string{"a/1", "a/2", "a/3", "b/1"})
    defer done()
    p := &gofakes3.Prefix{HasDelimiter:true, Delimiter:"/"}
    out, err := b.ListBucket("b", p, gofakes3.ListBucketPage{MaxKeys:2})
    if err != nil { t.Fatal(err) }
    if len(out.CommonPrefixes) != 2 {
        t.Fatalf("expected 2 common prefixes (a/, b/), got %d", len(out.CommonPrefixes))
    }
}

Comment on lines 186 to 188
cnt++
if page.MaxKeys > 0 && cnt >= page.MaxKeys {
nextK, _ := c.Next()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsTruncated is computed by checking whether any later key exists (c.Next()), not whether any later key matches the requested prefix/delimiter filter. So, I assume paginated calls can return IsTruncated=true even when there are no more matching results.

LastModified: gofakes3.NewContentTime(b.LastModified.UTC()),
}
objects.Add(item)
lastKey = key
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a page is truncated on CommonPrefixes entries, NextMarker can be empty because lastKey is only set for object contents, not for common-prefix matches, so clients cannot reliably request the next page (especially for delimiter-based listing).

lastKey = key
}

cnt++
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cnt increments for every matching key, including duplicates that collapse into the same CommonPrefix. This violates each rolled-up prefix counts once behavior and causes early truncation.

- Fix cnt increment to only count unique items (not duplicate CommonPrefix entries)
- Fix IsTruncated logic to properly check prefix/delimiter filter for remaining keys
- Fix NextMarker for CommonPrefixes by setting lastKey to MatchedPart
- Add reproduction tests for delimiter pagination edge cases

Fixes review comments from johannesboyne on PR johannesboyne#114
@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 8, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ListBucket in s3bolt backend does not honor page.Marker value

2 participants