Skip to content

fix: add UserMetadataStripped to list results - #2256

Merged
harshavardhana merged 6 commits into
minio:masterfrom
allanrogerr:issue-2054-list-usermetadata-decoded
Jul 29, 2026
Merged

harshavardhana merged 6 commits into
minio:masterfrom
allanrogerr:issue-2054-list-usermetadata-decoded

Conversation

@allanrogerr

@allanrogerr allanrogerr commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adds UserMetadataStripped StringMap to ObjectInfo and Version, populated when listing with ListObjectsOptions{WithMetadata: true} (V2 and versioned listings). It carries the keyed form StatObject/GetObject return in UserMetadata: X-Amz-Meta-* keys with the prefix stripped, system entries (content-type, expires, …) dropped, values passed through verbatim — list responses carry the stored values, so the SDK applies no decoding. The existing UserMetadata in list results is unchanged — it keeps the exact values stored on the object, per maintainer direction in #2054 ("a separate field would be needed").

  • New helper stripUserMetadata (utils.go); keys are canonicalized before the prefix match so server casing does not matter.
  • Populated in listObjectsV2Query and listObjectVersionsQuery; copied into the ObjectInfo emitted by versioned listings.
  • Stale doc comments on both UserMetadata fields corrected (they claimed keys were stripped; list results carry raw keys).
  • Regression test TestListObjectsUserMetadataStripped asserts, for both list paths, that UserMetadata stays byte-identical raw AND UserMetadataStripped equals the stripped map — including that an RFC 2047-looking value is passed through verbatim, NOT MIME-decoded.

Review updates (2026-07-22), per @harshavardhana's review:

  • X-Amz-Meta- lifted to const amzMetaPrefix (fc9556e).
  • MIME (RFC 2047) decoding of values removed — list XML carries the stored values verbatim; decoding is the application's responsibility since the SDK never encodes these values (4895775).
  • Field renamed UserMetadataDecoded → UserMetadataStripped to match the decode removal (0fcdf85). The field is new in this PR (zero occurrences in upstream master), so the rename is not a breaking change.
  • Branch merged with upstream master to resolve an ObjectInfo conflict with the new Headers field (89bf1d2).

Fixes #2054

Motivation and Context

ListObjects(WithMetadata: true) and StatObject return the same user metadata in incompatible shapes, forcing callers to probe both key forms (see #2054). Changing UserMetadata in place would break existing consumers, so the stripped form is exposed as a separate additive field.

How to test this PR?

Server test matrix (summary): the same live reproduction of #2054 was run against both server lines, each built from source at master on 2026-07-17. Output was identical on both servers, for both the failing (master) client and the fixed (PR) client. (This A/B predates the 2026-07-22 rename — the field was then UserMetadataDecoded and included a decode step; the observed values are unchanged by its removal because list XML already carries the stored values verbatim, as the tables show.)

Server Build License Result
MinIO AIStor master miniohq/aistor @ 85ddefa, built from source valid AIStor license (startup log: License: MinIO Enterprise License) ✅ identical to community — raw UserMetadata unchanged, stripped field matches StatObject
MinIO community master minio/minio @ 7aac2a2, built from source AGPLv3 ✅ same

Client-side A/B against each of those servers, running the issue's reproduction program (object stored with Hello: World and a Unicode value that forces RFC 2047 on the PUT wire) — the only variable is the client library:

client @ master (c30a92d) — failing client @ this PR — fixed
StatObject UserMetadata map[Hello:World Unicode:smörgåsbord] map[Hello:World Unicode:smörgåsbord]
ListObjects V2 UserMetadata map[X-Amz-Meta-Hello:World X-Amz-Meta-Unicode:smörgåsbord content-type:application/octet-stream] same (unchanged)
ListObjects V2 stripped field (field does not exist) map[Hello:World Unicode:smörgåsbord]
ListObjects versions UserMetadata map[X-Amz-Meta-Hello:World X-Amz-Meta-Unicode:smörgåsbord content-type:application/octet-stream] same (unchanged)
ListObjects versions stripped field (field does not exist) map[Hello:World Unicode:smörgåsbord]

Testing performed, in full:

  • Live-server A/B above on AIStor master (licensed) and community master: raw UserMetadata byte-identical before/after on both; stripped field correct on both list paths on both servers.
  • go test -run TestListObjectsUserMetadataStripped -count=1 . — PASS at head 0b12391 (canned-XML httptest covering V2 + versions paths, raw-unchanged + stripped-map + verbatim-RFC-2047 assertions).
  • go test -short -count=1 . — package suite PASS at merge commit 89bf1d2 (post-merge with upstream master).
  • go test -race -count=1 -short ./... — full module suite PASS (12 packages ok, pre-review head 72ba76d).
  • Scale benchmark (1000-object list page, 2 meta keys + content-type each, pre-review head with the since-removed decode step): XML unmarshal alone 72.84ms/76,119 allocs vs unmarshal+strip 73.76ms/78,118 allocs (≈1.3% of local parse cost, unobservable against the network round trip); removing the decode only lowers this.
  • make lint (golangci-lint) 0 issues, go vet clean, gofumpt -d clean (pre-review head; go vet + gofumpt re-run clean on every review-round commit).

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Optimization (provides speedup with no functional changes)
  • Cleanup/Maintenance (no functional changes)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • Fixes a regression (If yes, please add commit-id or PR # here)
  • Unit tests added/updated
  • Internal documentation updated (godoc on the affected fields)

Summary by CodeRabbit

  • New Features
    • Added a new userMetadataStripped field to object and version listing results, returning prefix-stripped user metadata (values preserved) in JSON and omitting it from XML.
  • Bug Fixes
    • Improved consistency of user-metadata handling across standard listings and version listings, including correct normalization of metadata header key casing.
  • Tests
    • Added regression coverage for both raw and prefix-stripped metadata in listing and version-listing scenarios.
  • Documentation
    • Clarified how user metadata is represented in the public metadata fields.

@allanrogerr allanrogerr self-assigned this Jul 17, 2026
@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Object listings and version listings now expose UserMetadataStripped, containing canonicalized, prefix-stripped metadata while preserving raw metadata. Regression tests cover regular and versioned listing responses.

Changes

User metadata listing

Layer / File(s) Summary
Metadata contract and normalization
api-datatypes.go, api-s3-datatypes.go, utils.go
Adds UserMetadataStripped and centralizes canonical prefix stripping while preserving stored values.
List response integration
api-list.go
Populates stripped metadata for List Objects V2 and List Object Versions results.
Listing regression coverage
api-list_test.go
Validates raw and stripped metadata for regular and versioned listings.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ListQuery
  participant stripUserMetadata
  participant ObjectInfo
  Client->>ListQuery: request metadata-enabled listing
  ListQuery->>stripUserMetadata: transform raw UserMetadata
  stripUserMetadata->>ObjectInfo: set UserMetadataStripped
  ObjectInfo-->>Client: return raw and stripped metadata
Loading

Possibly related PRs

Suggested reviewers: jiuker, klauspost

Poem

I’m a rabbit with metadata bright,
Prefixes hop out of sight.
Raw values stay true,
Versions join too,
Listings now look just right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR adds a stripped metadata field, but it does not decode values or remove system entries as required by #2054. Decode RFC 2047 values, omit system entries from list metadata, and keep raw UserMetadata unchanged while adding the decoded field.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The changes stay focused on user-metadata handling and accompanying tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: adding a stripped user-metadata field to list results.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

ListObjects with WithMetadata returns UserMetadata exactly as stored on
the object (X-Amz-Meta-* prefixed keys plus system entries), while
StatObject returns the decoded form. Add a separate UserMetadataDecoded
field to ObjectInfo and Version, populated on the V2 and versions list
paths, without changing UserMetadata.

Fixes minio#2054
@allanrogerr
allanrogerr force-pushed the issue-2054-list-usermetadata-decoded branch from 06d1701 to 72ba76d Compare July 17, 2026 23:18
@allanrogerr
allanrogerr marked this pull request as ready for review July 20, 2026 23:41

@klauspost klauspost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice!

Comment thread utils.go Outdated
Comment thread utils.go Outdated
Replaces repeated "X-Amz-Meta-" string literals in utils.go with a
single named constant, as requested in PR minio#2256 review.
List responses carry the stored metadata values verbatim, unlike
HEAD/GET response headers which the server MIME-encodes. Decoding
here could corrupt a stored value that literally begins with "=?",
and decoding is the application's responsibility since the SDK
never encodes these values itself. UserMetadataDecoded now only
strips the X-Amz-Meta- prefix from keys.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api-list_test.go (1)

92-106: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that the client requests metadata.

The fixture returns <UserMetadata> unconditionally, so this test would still pass if WithMetadata: true stopped adding metadata=true to the request. Check the query parameter in the handler before returning metadata.

Proposed fix
  ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  	w.Header().Set("Content-Type", "application/xml")
+  	if r.URL.Query().Get("metadata") != "true" {
+  		http.Error(w, "metadata query parameter missing", http.StatusBadRequest)
+  		return
+  	}
  	if _, versioned := r.URL.Query()["versions"]; versioned {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api-list_test.go` around lines 92 - 106, The httptest server handler
currently returns user metadata regardless of the request, so it does not verify
the metadata query parameter. Update the handler in the test server setup to
inspect the request query and only include userMetadataXML when metadata=true is
present, while preserving the existing versioned and unversioned response
branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@api-list_test.go`:
- Around line 92-106: The httptest server handler currently returns user
metadata regardless of the request, so it does not verify the metadata query
parameter. Update the handler in the test server setup to inspect the request
query and only include userMetadataXML when metadata=true is present, while
preserving the existing versioned and unversioned response branches.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d259e99a-96f5-4363-bd61-a116a1da5239

📥 Commits

Reviewing files that changed from the base of the PR and between 72ba76d and 4895775.

📒 Files selected for processing (4)
  • api-datatypes.go
  • api-list_test.go
  • api-s3-datatypes.go
  • utils.go

The field no longer decodes values; it only strips the X-Amz-Meta-
prefix from keys. The field is new in this PR, so the rename is not
a breaking change.
…sermetadata-decoded

# Conflicts:
#	api-datatypes.go
Merge redundant lines so each remaining line states a contract the
code cannot show.
@allanrogerr allanrogerr changed the title fix: add UserMetadataDecoded to list results fix: add UserMetadataStripped to list results Jul 22, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api-datatypes.go (1)

187-195: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the Headers MIME-decoding contract.

ToObjectInfo assigns Headers: h directly, so Headers contains the raw response headers. RFC 2047 values in this field are not MIME-decoded; remove the “MIME-decoded, matching Metadata” language so the public contract does not mislead callers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api-datatypes.go` around lines 187 - 195, The Headers documentation in
ObjectInfo incorrectly promises MIME-decoded RFC 2047 values. Update the Headers
comment to describe the field as raw response headers without claiming MIME
decoding or parity with Metadata, while preserving the existing scope, aliasing,
and read-only guidance.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@api-datatypes.go`:
- Around line 187-195: The Headers documentation in ObjectInfo incorrectly
promises MIME-decoded RFC 2047 values. Update the Headers comment to describe
the field as raw response headers without claiming MIME decoding or parity with
Metadata, while preserving the existing scope, aliasing, and read-only guidance.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 24e22d7d-a360-4a0e-8695-3aabb5ed2165

📥 Commits

Reviewing files that changed from the base of the PR and between 4895775 and 89bf1d2.

📒 Files selected for processing (5)
  • api-datatypes.go
  • api-list.go
  • api-list_test.go
  • api-s3-datatypes.go
  • utils.go

@allanrogerr

Copy link
Copy Markdown
Contributor Author

PTAL @rraulinio

@allanrogerr

Copy link
Copy Markdown
Contributor Author

PR looks good to merge @harshavardhana - let me know

@harshavardhana
harshavardhana merged commit 3910698 into minio:master Jul 29, 2026
9 checks passed
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.

ListObjects doesn't decode UserMetadata correctly

5 participants