fetch: SharedArrayBuffer-backed view as request body is silently coerced to a string (lossy data corruption)
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 7.7k
- Forks
- 880
- Avg merge
- 2d 16h
- Merged PRs (30d)
- 68
Description
Bug Description
When a fetch/Request body is an ArrayBufferView backed by a SharedArrayBuffer, undici does not treat it as a BufferSource. Instead the value falls through the BodyInit union conversion to string coercion, so the request body becomes String(view) — silent, lossy data corruption with no error thrown.
Two flavors, depending on the view type:
- a plain
Uint8Arrayover a SAB stringifies via%TypedArray%.prototype.toString→ the body is literally"255,216,255,219"; - a Node.js
Bufferover a SAB stringifies viaBuffer.prototype.toString('utf8')→ the body is a lossy UTF-8 decode/re-encode of the bytes (every byte ≥ 0x80 becomesef bf bd, JPEG magicff d8 ff dbarrives asef bf bd ef bf bd …, payload inflated ~1.1–1.7×).
Per WebIDL, BufferSource without [AllowShared] must reject views backed by a SharedArrayBuffer with a TypeError (see whatwg/webidl#353). #4495 already tracked this non-compliance and described the then-current behavior as "views on SharedArrayBuffers are accepted, and result in the source being copied into a new SharedArrayBuffer internally" — i.e. bytes were at least preserved. Current behavior is worse than that description: the view is coerced to a string, so the bytes are destroyed.
Silent corruption rather than a loud TypeError makes this very hard to diagnose in production (see "real-world impact" below).
Reproducible By
// node >= 20, no dependencies; same result with npm undici@8.9.0 (import { Request } from 'undici')
const bytes = [255, 216, 255, 219]; // JPEG magic
const sabView = new Uint8Array(new SharedArrayBuffer(4));
sabView.set(bytes);
const req1 = new Request('http://example.com/', { method: 'POST', body: sabView });
console.log(Buffer.from(await req1.arrayBuffer()).toString());
// "255,216,255,219" <-- String(view), 15 bytes, data destroyed
const sabBuf = Buffer.from(new SharedArrayBuffer(4));
Buffer.from(bytes).copy(sabBuf);
const req2 = new Request('http://example.com/', { method: 'POST', body: sabBuf });
console.log(Buffer.from(await req2.arrayBuffer()).toString('hex'));
// "efbfbdefbfbdefbfbdefbfbd" <-- lossy UTF-8 roundtrip of ff d8 ff db
// control: same bytes in a normal ArrayBuffer-backed view are sent verbatim
const ok = new Request('http://example.com/', { method: 'POST', body: new Uint8Array(bytes) });
console.log(Buffer.from(await ok.arrayBuffer()).toString('hex')); // "ffd8ffdb"
Expected Behavior
A TypeError at Request construction / fetch() call time (spec behavior for a BufferSource not annotated [AllowShared]), or at minimum byte-preserving acceptance — anything but silent string coercion.
Logs & Screenshots
Observed identically with:
- undici 7.22.0 bundled in Node.js v25.8.1 (darwin-arm64)
- undici bundled in Node.js v24.18.0 (linux-x64, Vercel)
- undici 8.9.0 from npm
Environment
macOS 15.1 arm64 / Node v25.8.1, and Amazon Linux (Vercel) x64 / Node v24.18.0.
Real-world impact
sharp 0.35's wasm32 runtime returns toBuffer() results as zero-copy Buffer views into the wasm heap, which is a SharedArrayBuffer (threaded Emscripten build). Since sharp 0.35 the wasm runtime installs on every platform and is a silent last-resort fallback when the native binary fails to load — which currently happens on popular stacks (Next.js 16 Turbopack on Vercel, lovell/sharp#4567). The combination means fetch(url, { method: 'POST', body: await sharp(...).toBuffer() }) silently uploads UTF-8-mangled garbage. We hit this in production: ~3,300 corrupted image uploads (supabase-js storage) over a day, zero errors raised anywhere, and no local reproduction because dev machines load the native runtime. Filed on the sharp side as https://github.com/lovell/sharp/issues/4576.
Additional context
The Buffer-over-SAB case is the nastier one: the output is 100% valid UTF-8 that still looks like binary at a glance, and the size inflation depends on payload entropy, so checksum/length monitoring is the only way to catch it.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the fetch/Request body-conversion path and run the no-dependency reproduction in the issue on Node.js or undici. Compare SharedArrayBuffer-backed Uint8Array and Buffer bodies with the normal ArrayBuffer control, then verify the result matches WebIDL behavior by rejecting the views or preserving their bytes instead of coercing them to strings.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100