Skip to content

[miniflare] Fix KV bulk getWithMetadata dropping metadata for falsy values#14762

Open
gianghungtien wants to merge 2 commits into
cloudflare:mainfrom
gianghungtien:fix/kv-bulk-get-falsy-value-metadata
Open

[miniflare] Fix KV bulk getWithMetadata dropping metadata for falsy values#14762
gianghungtien wants to merge 2 commits into
cloudflare:mainfrom
gianghungtien:fix/kv-bulk-get-falsy-value-metadata

Conversation

@gianghungtien

@gianghungtien gianghungtien commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Fixes #14594.

The KV bulk-get handler (@POST("/bulk/get")) decides whether to wrap each entry as { value, metadata } based on the truthiness of the decoded value rather than on whether the key exists:

if (val && withMetadata) {
	return [{ value: val, metadata: obj?.metadata ?? null }, size];
}
return [val, size];

So a present key holding a falsy value — an empty string, or 0, false and null when read with type: "json" — falls through to the bare-value branch, loses its metadata, and returns a shape the runtime binding only expects for missing keys:

await env.KV.put("k", "", { metadata: { a: 1 } });
await env.KV.getWithMetadata(["k"]);
// expected: Map { "k" => { value: "", metadata: { a: 1 } } }
// actual:   Map { "k" => "" }   ← metadata lost, wrong shape

Empty-string values are the most common real-world trigger.

This contradicts the contract the existing tests in packages/miniflare/test/plugins/kv/index.spec.ts already assert — every present key yields { value, metadata } and only missing keys yield null. The gap was simply that every existing case used a truthy value.

The change

Gate the wrapper on entry presence instead of value truthiness. storage.get() returns null only for a missing key, and a present KeyValueEntry always carries a value: ReadableStream<Uint8Array>, so obj !== null is the correct — and unambiguous — presence check. Missing keys still return a bare null.

The two preceding obj?.value truthiness checks are switched to the same explicit presence check. Those are behaviourally equivalent (a present entry's value stream is never null), but reading presence three different ways in one function is what allowed the bug to hide in the first place.

Scope / compatibility

  • Only the withMetadata path changes shape, and only for present-but-falsy values — which were previously returning the documented-wrong shape.
  • The non-metadata bulk path is unchanged: falsy values were, and still are, returned as bare values. The other in-repo consumer of this endpoint (local-explorer's bulkGetKVValues) uses that path, so it is unaffected.
  • Single-key GET /:key is untouched.

Testing

Four tests added to packages/miniflare/test/plugins/kv/index.spec.ts:

  • metadata is preserved for an empty-string value
  • metadata is preserved for falsy JSON values (0, false, null)
  • a present falsy key keeps its { value, metadata } wrapper while a missing key in the same request still returns a bare null
  • falsy values are still returned correctly on the non-metadata path (guards the unchanged branch)

The first three fail on main and pass with this change. Full miniflare suite: 898 passed. Two unrelated failures (hyperdrive/proxy.spec.ts TLS modes, and the TZ=UTC subprocess test in index.spec.ts) reproduce identically on an unmodified main on this Windows machine and are environmental.

oxlint --deny-warnings --type-aware, tsc, and oxfmt are all clean.


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: this restores the already-documented getWithMetadata behaviour in local dev; no public API or documented behaviour changes.

A picture of a cute animal (not mandatory, but encouraged)

🦫


Open in Devin Review

…alues

The bulk-get handler in `processKeyValue` decided whether to wrap an entry
as `{ value, metadata }` based on the truthiness of the decoded value rather
than on whether the key existed. Present keys holding a falsy value - an
empty string, or `0`, `false` and `null` when read with `type: "json"` -
were returned as bare values, losing their metadata and diverging from the
shape the runtime binding expects for present keys.

Gate the wrapper on entry presence instead. `storage.get()` returns `null`
only for missing keys, and a present entry always carries a `value`
ReadableStream, so `obj !== null` is the correct presence check. Missing
keys continue to return a bare `null`.

Also switch the two preceding `obj?.value` truthiness checks to the same
explicit presence check. These are equivalent - a present entry's `value`
is always a non-null stream - but make the invariant clear at each site.
@changeset-bot

changeset-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b34f696

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 7 packages
Name Type
miniflare Patch
@cloudflare/deploy-helpers Patch
@cloudflare/pages-shared Patch
@cloudflare/runtime-types Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-pool-workers Patch
wrangler Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@workers-devprod
workers-devprod requested review from a team and NuroDev and removed request for a team July 20, 2026 02:55
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/workers-kv
  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/shaggy-geese-flow.md: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/kv/namespace.worker.ts: [@cloudflare/workers-kv @cloudflare/wrangler]
  • packages/miniflare/test/plugins/kv/index.spec.ts: [@cloudflare/workers-kv @cloudflare/wrangler]

@devin-ai-integration devin-ai-integration Bot 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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

Comment thread packages/miniflare/test/plugins/kv/index.spec.ts Outdated
Addresses review feedback. The annotations were copied from the surrounding
bulk get tests; dropping them and letting inference do the work type-checks
cleanly and reads better.

Annotating the expected Maps with the real result type isn't an option here:
`KVNamespaceGetWithMetadataResult` declares a `cacheStatus` field that the
bulk endpoint doesn't return, so the literals would have to carry a property
the assertion would then fail on.
@pkg-pr-new

pkg-pr-new Bot commented Jul 20, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@14762

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@14762

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@14762

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@14762

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@14762

miniflare

npm i https://pkg.pr.new/miniflare@14762

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@14762

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@14762

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@14762

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@14762

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@14762

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@14762

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@14762

wrangler

npm i https://pkg.pr.new/wrangler@14762

commit: b34f696

@gianghungtien

Copy link
Copy Markdown
Contributor Author

@NuroDev this should be ready for another look when you have a moment — the any assertions are gone as of b34f696, with a note on the thread about why annotating the expected Map with the real result type isn't an option (KVNamespaceGetWithMetadataResult declares a cacheStatus field the bulk endpoint doesn't return, so the literals would carry a property the assertion then fails on). Inference handles it cleanly instead.

Separately, this is also blocked on Tests (Windows, packages-and-tools) — could someone re-run that job? I only have read access, so I can't trigger it myself.

I believe it's unrelated to this PR:

  • The failure is in @cloudflare/workflows-shared: tests/engine.test.ts > Engine > lifecycle methods > should pause after multiple concurrent in-flight step.dos finish, failing after ~10.9s with its retry exhausted — a timing-sensitive concurrency test.
  • That package has no miniflare dependency at all; its only pool dep is the catalog-pinned published @cloudflare/vitest-pool-workers 0.13.3. The KV bulk-get change here isn't in its code path.
  • Linux and macOS packages-and-tools both passed — Windows only.
  • The same job is failing the same way on [vitest-pool-workers] Treat webSocket* as optional Durable Object handlers #14763, which touches vitest-pool-workers rather than miniflare. Two unrelated changes tripping the same test in the same third package points at flakiness rather than either PR.

For completeness: other currently-open PRs are passing this job, so this doesn't look like broad breakage — just an intermittent Windows failure that has caught both of mine. If a re-run reproduces it I'll investigate properly rather than keep calling it flaky.

The miniflare KV suite itself is green (54/54), and I re-confirmed the three new tests still fail without the fix after removing the anys, so the regression coverage is intact.

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

Labels

None yet

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

miniflare KV bulk getWithMetadata drops metadata for present-but-falsy values (0, false, empty string)

3 participants