Skip to content

fix(client): preserve Buffer replies in the client-side cache - #3539

Open
suryakant-source wants to merge 4 commits into
redis:masterfrom
suryakant-source:patch-1
Open

suryakant-source wants to merge 4 commits into
redis:masterfrom
suryakant-source:patch-1

Conversation

@suryakant-source

@suryakant-source suryakant-source commented Oct 24, 2026 •

Copy link
Copy Markdown

Summary

Every read served through the client-side cache passes through structuredClone, which clones a Buffer as a plain Uint8Array. With a BLOB_STRING -> Buffer type mapping (the setup the client-side caching docs recommend for binary replies), cached commands return Uint8Array instead of Buffer: Buffer.isBuffer() is false, toString() gives comma-separated byte values, and the static types disagree with the runtime value. This happens on the very first read too, because the reply is cloned before being stored.

The clone and the original reply have identical structure, so the fix walks them in tandem and turns the cloned values back into Buffers exactly where the original reply had them. The memoized walk keeps shared references (and cycles) intact, and structuredClone semantics are unchanged for everything else (arrays, maps, sets, plain objects, errors, dates).

Fixes #3536.

Test plan

  • 3 new server-free tests in cache.spec.ts: Buffer preserved on miss and hit with independent copies; Buffers nested in arrays, maps, sets and plain objects; structuredClone semantics kept for the rest of the reply.
  • Verified the new tests fail without the fix and pass with it; the existing server-free cache test still passes; tsc --build and eslint are clean.
  • The server-backed cache tests need a local Redis and were not run here.

Note

Medium Risk
Touches every cached reply return path in the client, but the change is a targeted clone fix with broad unit coverage and no auth or persistence impact.

Overview
Client-side cache reads no longer turn Buffer replies into plain Uint8Array values. handleCache now returns replies through cloneReply instead of bare structuredClone, so BLOB_STRING -> Buffer type mapping stays correct on cache misses and hits.

cloneReply still deep-clones with structuredClone, then reapplyReplyBuffers walks the original and clone together to restore Buffer instances in arrays, maps, sets, and plain objects while keeping shared references and cycles. Non-plain types such as VerbatimString keep structuredClone behavior without unsafe property writes.

New server-free Reply cloning tests cover top-level and nested buffers, mutation isolation, verbatim strings, and unchanged clone semantics elsewhere. Fixes #3536.

Reviewed by Cursor Bugbot for commit dd4d28e. Bugbot is set up for automated code reviews on this repo. Configure here.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T04:49:50.368957Z dd4d28e New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cc5e8f7d7f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/client/lib/client/cache.ts Outdated
Comment on lines +511 to +513
if (original instanceof Buffer) {
const view = clone as Uint8Array;
return Buffer.from(view.buffer, view.byteOffset, view.length);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Memoize reconstructed Buffer objects

When the same Buffer instance appears in multiple positions in a reply, this branch runs before the seen lookup and creates a distinct Buffer wrapper on every visit. The wrappers share an ArrayBuffer, but a === b becomes false; if the shared buffer is both a Map key and referenced elsewhere, lookups through that reference can consequently fail. structuredClone normally preserves this alias identity, so the reconstructed buffer needs to be stored in the memo as well.

Useful? React with 👍 / 👎.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit cc5e8f7. Configure here.

Comment thread packages/client/lib/client/cache.ts

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dd4d28e512

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +530 to +531
for (let i = 0; i < original.length; i++) {
clonedArray[i] = reapplyReplyBuffers(clonedArray[i], original[i], seen);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve holes while walking reply arrays

When a cacheable reply contains a sparse array—concretely, JSON.GET is cacheable and its public JSON reviver creates a hole when it returns undefined for an array element—this loop visits the absent index and assigns undefined into the clone. structuredClone preserves the hole, so enabling client-side caching changes checks such as 1 in result from false to true on both the fill and hit paths; only assign indices that are actually present on the original array.

Useful? React with 👍 / 👎.

Comment on lines +560 to +564
if (!isPlainObject(original)) {
// `String` objects (`VerbatimString`), errors, dates and custom mapped classes keep
// their `structuredClone` result: their indexed/own properties are not writable
// (a `String` object throws on assignment) and cannot hold buffers in practice
return clone;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reapply buffers in custom transformed objects

When a cacheable transform returns a class instance with a Buffer own property—for example, JSON.GET's exposed reviver can return a wrapper class—this branch returns the raw structured clone, where that property is still a Uint8Array. Unlike the String object this guard was added for, structuredClone turns an ordinary custom class into a plain object with writable fields, so the blanket exclusion leaves the binary-reply bug unfixed for supported arbitrary transforms; walk those cloned own fields or restrict the exemption to clone-special built-ins.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
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.

Client-side cache returns Uint8Array instead of Buffer when the client maps strings to Buffer

1 participant