From ed1f6e0ea28950ba4c67dd57f203ae09b9c5e045 Mon Sep 17 00:00:00 2001 From: tobiasvdorp Date: Thu, 9 Jul 2026 16:36:01 +0200 Subject: [PATCH] fix nested buffer containing array of numbers instead of base64 --- .../src/helpers/serialization.test.ts | 33 +++++++++++++++++++ .../src/helpers/serialization.ts | 32 ++++++++++++++---- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/packages/cache-handler/src/helpers/serialization.test.ts b/packages/cache-handler/src/helpers/serialization.test.ts index b9cd1b4..fd30ffc 100644 --- a/packages/cache-handler/src/helpers/serialization.test.ts +++ b/packages/cache-handler/src/helpers/serialization.test.ts @@ -65,6 +65,32 @@ describe("serialization helpers", () => { }); }); + describe("nested Buffer (Node toJSON before replacer)", () => { + test("serializes rscData as base64, not int-array JSON", () => { + const entry = { rscData: Buffer.from('1:"test"') }; + const serialized = JSON.stringify(entry, jsonReplacer); + + expect(serialized).toContain('"__serialized_type":"Buffer"'); + expect(serialized).toContain('"data":"MToidGVzdCI="'); + expect(serialized).not.toContain('"type":"Buffer"'); + expect(serialized).not.toMatch(/"data":\[\d+,/); + }); + + test("round-trips rscData nested in an APP_PAGE-like object", () => { + const entry = { + kind: "APP_PAGE", + rscData: Buffer.from("page-rsc-payload"), + html: "", + }; + const result = roundTrip(entry) as typeof entry; + + expect(Buffer.isBuffer(result.rscData)).toBe(true); + expect(result.rscData.toString()).toBe("page-rsc-payload"); + expect(result.kind).toBe("APP_PAGE"); + expect(result.html).toBe(""); + }); + }); + describe("Next.js APP_PAGE shape (Map)", () => { test("round-trips a Map of Buffers (segmentData)", () => { const segmentData = new Map([ @@ -107,6 +133,13 @@ describe("serialization helpers", () => { expect(result).toEqual(userValue); }); + test("does not convert { type: 'Buffer' } with invalid byte values", () => { + const userValue = { type: "Buffer", data: [1, 256, 3.5] }; + const result = roundTrip(userValue); + expect(Buffer.isBuffer(result)).toBe(false); + expect(result).toEqual(userValue); + }); + test("does not convert a fake Map marker with malformed entries", () => { const userValue = { __serialized_type: "Map", entries: "not-an-array" }; const result = roundTrip(userValue); diff --git a/packages/cache-handler/src/helpers/serialization.ts b/packages/cache-handler/src/helpers/serialization.ts index c5b804e..4540c65 100644 --- a/packages/cache-handler/src/helpers/serialization.ts +++ b/packages/cache-handler/src/helpers/serialization.ts @@ -6,6 +6,22 @@ * loses Buffer identity, so custom replacer/reviver functions are needed. */ +type NodeBufferJson = { + type: "Buffer"; + data: number[]; +}; + +function isByte(value: unknown): value is number { + return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 255; +} + +function isNodeBufferJson(value: unknown): value is NodeBufferJson { + if (!value || typeof value !== "object") return false; + + const obj = value as Record; + return obj.type === "Buffer" && Array.isArray(obj.data) && obj.data.every(isByte); +} + /** * Custom JSON replacer that serializes Map and Buffer instances. * - Maps become `{ __serialized_type: "Map", entries: [...] }` @@ -24,6 +40,14 @@ export function jsonReplacer(_key: string, value: unknown): unknown { data: value.toString("base64"), }; } + // Node calls Buffer.toJSON() before the replacer — nested Buffers arrive as + // `{ type: "Buffer", data: number[] }` instead of a Buffer instance. + if (isNodeBufferJson(value)) { + return { + __serialized_type: "Buffer", + data: Buffer.from(value.data).toString("base64"), + }; + } return value; } @@ -51,12 +75,8 @@ export function jsonReviver(_key: string, value: unknown): unknown { // Backward compat: Node's Buffer.toJSON() format. // Guard with number[] check to avoid false-positives on user data // that happens to have { type: "Buffer", data: [...] } shape. - if ( - obj.type === "Buffer" && - Array.isArray(obj.data) && - obj.data.every((n) => typeof n === "number") - ) { - return Buffer.from(obj.data as number[]); + if (isNodeBufferJson(obj)) { + return Buffer.from(obj.data); } } return value;