Skip to content

fix: serve V1 ListObjects via the backend's V2 API (Hetzner rejects V1)#92

Merged
ServerSideHannes merged 1 commit into
mainfrom
fix/list-objects-v1-via-v2-backend
Jun 30, 2026
Merged

fix: serve V1 ListObjects via the backend's V2 API (Hetzner rejects V1)#92
ServerSideHannes merged 1 commit into
mainfrom
fix/list-objects-v1-via-v2-backend

Conversation

@ServerSideHannes

Copy link
Copy Markdown
Owner

Problem

Hetzner Object Storage only implements ListObjectsV2 — it rejects the legacy V1 ListObjects with HTTP 400 BadRequest. The proxy forwarded V1 client requests to the backend as V1, so every V1 client broke:

  • scylla-manager agent (bundled rclone 1.51.0, which lists with V1 for provider: Minio) → all Scylla backups failed at the location-check / list step:
    GET /…-scylladb-backups-v3?delimiter=%2F&encoding-type=url&max-keys=1000&prefix=… → 400
    operation list: error in ListJSON: BadRequest 400
    
  • barman-cloud-backup-delete (V1 ListObjects) → CNPG retention enforcement failed even after the V2 continuation-token fix landed:
    Barman cloud backup delete exception: An error occurred (BadRequest) when calling the ListObjects operation
    
    Postgres base backups completed, but expired backups were never pruned.

HEAD/PUT and all V2 (list-type=2) clients (botocore, aws-sdk-go, aws-sdk-java) were unaffected.

Fix

handle_list_objects_v1 now serves the client's V1 request by calling the backend's list_objects_v2 instead of list_objects (V1). V1 marker pagination is stateless (marker == last key returned), which maps exactly onto V2 StartAfter, so the translation is lossless for the recursive listings these clients use:

  • client V1 marker → backend V2 StartAfter
  • NextMarker synthesized from the largest raw backend key/prefix when truncated (raw, not the internal-key-filtered list, so an all-internal page still advances)
  • keys still URL-encoded under encoding-type=url (V1 markers are keys — unlike the opaque V2 continuation token in fix: don't URL-encode V2 continuation tokens under encoding-type=url #91)

ponytail note in-code: a single delimiter-level prefix with >max_keys distinct sub-prefixes can re-emit one boundary prefix across pages (harmless/idempotent for read-only catalog walks); upgrade path is a redis-backed marker→continuation-token map.

Test

tests/unit/test_list_objects_v1_via_v2.py: proves the handler calls the backend V2 API (the fake's V1 method asserts if hit), maps markerStartAfter, filters internal keys, and synthesizes NextMarker from the largest raw key when truncated. Full unit suite green (542 passed); ruff check + format clean.

Hetzner Object Storage (and other modern S3 backends) only implement
ListObjectsV2 and reject the legacy V1 ListObjects with HTTP 400. That
broke every V1 client routed through the proxy:
  - scylla-manager agent (bundled rclone 1.51.0 lists with V1) -> all
    Scylla backups failed at the location-check / list step
  - barman-cloud-backup-delete (V1 ListObjects) -> CNPG retention
    enforcement failed (BadRequest on ListObjects), so Postgres backups
    completed but old ones were never pruned

handle_list_objects_v1 now calls the backend's list_objects_v2 instead of
list_objects (V1). V1 marker pagination is stateless (marker == last key
returned), which maps exactly onto V2 StartAfter, so the translation is
lossless for the recursive listings these clients use. NextMarker is
synthesized from the largest raw backend key when truncated; keys are
still URL-encoded under encoding-type=url (V1 markers are keys, unlike the
opaque V2 continuation token).
@ServerSideHannes ServerSideHannes merged commit cb04a8b into main Jun 30, 2026
4 checks passed
@ServerSideHannes ServerSideHannes deleted the fix/list-objects-v1-via-v2-backend branch June 30, 2026 09:52
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.

1 participant