fix(file-preview): copy arrayBuffer source to avoid detached-ArrayBuffer crash
`readSourceAsArrayBuffer` returned `source.buffer` directly for arrayBuffer
sources. Downstream consumers (pdf.js via `getDocument({ data })`) transfer
the buffer to a Web Worker, which detaches the original ArrayBuffer. A second
read on the same source — React StrictMode double-invoke, file re-selection,
source reuse across previews — then crashed with:
TypeError: Cannot perform Construct on a detached ArrayBuffer
The `file` and `blob` branches already returned fresh ArrayBuffers per call
(`File.arrayBuffer()` / `Blob.arrayBuffer()`); the arrayBuffer branch now
matches that behavior via `source.buffer.slice(0)`.
Tests cover both the copy semantics and the post-detach re-read path
(simulates worker transfer via `MessageChannel.postMessage(..., [buf])`).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
4aeff5b706
commit
14a429fbcf
@@ -43,13 +43,44 @@ describe("readSourceAsArrayBuffer", () => {
|
||||
expect(new TextDecoder().decode(buf)).toBe("ho");
|
||||
});
|
||||
|
||||
it("returns the underlying ArrayBuffer verbatim", async () => {
|
||||
it("returns a copy, not the underlying ArrayBuffer reference", async () => {
|
||||
const original = new TextEncoder().encode("abc").buffer as ArrayBuffer;
|
||||
const buf = await readSourceAsArrayBuffer({
|
||||
kind: "arrayBuffer",
|
||||
buffer: original,
|
||||
});
|
||||
expect(buf).toBe(original); // same reference, no copy
|
||||
expect(buf).not.toBe(original); // distinct reference — defensive copy
|
||||
expect(new TextDecoder().decode(buf)).toBe("abc");
|
||||
// The original is still usable (not detached) after the read.
|
||||
expect(original.byteLength).toBe(3);
|
||||
});
|
||||
|
||||
it("keeps the source buffer usable after the returned copy is detached", async () => {
|
||||
// Reproduces the PdfPreview crash: pdf.js's `getDocument({ data })`
|
||||
// transfers the underlying ArrayBuffer to a Web Worker, detaching it.
|
||||
// Before the fix, readSourceAsArrayBuffer returned `source.buffer`
|
||||
// directly, so after the first read the source buffer was detached and
|
||||
// a second read (React StrictMode double-invoke, file re-selection,
|
||||
// source reuse) crashed with "Cannot perform Construct on a detached
|
||||
// ArrayBuffer". The fix returns a copy, so only the copy gets detached.
|
||||
const source = buffer("payload");
|
||||
const original = (source as { buffer: ArrayBuffer }).buffer;
|
||||
|
||||
const first = await readSourceAsArrayBuffer(source);
|
||||
expect(new TextDecoder().decode(first)).toBe("payload");
|
||||
|
||||
// Detach the *returned* buffer (mimics worker transfer of `data`).
|
||||
const port = new MessageChannel();
|
||||
port.port1.postMessage(first, [first]);
|
||||
port.port1.close();
|
||||
port.port2.close();
|
||||
expect(first.byteLength).toBe(0); // returned copy is detached
|
||||
|
||||
// The source's own buffer must still be intact — second read works.
|
||||
expect(original.byteLength).toBe("payload".length);
|
||||
const second = await readSourceAsArrayBuffer(source);
|
||||
expect(new TextDecoder().decode(second)).toBe("payload");
|
||||
expect(second).not.toBe(first); // a fresh copy each call
|
||||
});
|
||||
|
||||
describe("url", () => {
|
||||
|
||||
@@ -25,7 +25,15 @@ export async function readSourceAsArrayBuffer(
|
||||
if (options.signal?.aborted) {
|
||||
throw new DOMException("The operation was aborted.", "AbortError");
|
||||
}
|
||||
return source.buffer;
|
||||
// Return a copy, not the underlying reference. Downstream consumers
|
||||
// (e.g. pdf.js via `getDocument({ data })`) transfer the buffer to a
|
||||
// Web Worker, which detaches the original ArrayBuffer. If we returned
|
||||
// `source.buffer` directly, a second read on the same source (React
|
||||
// StrictMode double-invoke, file re-selection, source reuse across
|
||||
// previews) would hit `Cannot perform Construct on a detached ArrayBuffer`.
|
||||
// The `file` and `blob` branches already return fresh buffers per call;
|
||||
// this matches their behavior.
|
||||
return source.buffer.slice(0);
|
||||
|
||||
case "url": {
|
||||
const response = await fetch(source.url, {
|
||||
|
||||
Reference in New Issue
Block a user