You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Also reproduced on Node v24.18.0 linux-x64 (Vercel).
Does this problem relate to file caching?
Adding sharp.cache(false) does not fix this problem.
Does this problem relate to images appearing to have been rotated by 90 degrees?
Using rotate() or keepExif() does not fix this problem.
What are the steps to reproduce?
When the wasm32 runtime is in use, toBuffer() resolves with a Buffer that is a zero-copy view into the wasm heap — and because @img/sharp-wasm32 is a threaded Emscripten build, that heap is a SharedArrayBuffer:
buf.buffer instanceof SharedArrayBuffer // true (byteOffset ~5 MB into a 16 MB backing)
SharedArrayBuffer-backed views are rejected or mishandled by many web APIs (BufferSource without [AllowShared], see nodejs/undici#4495). The concrete disaster case is fetch: Node.js/undici silently coerces such a view to a string, so the request body becomes the lossy UTF-8 decode/re-encode of the image — JPEG magic ff d8 ff db is sent as ef bf bd ef bf bd …, inflated, 100% valid UTF-8, no error thrown (reported on the undici side as nodejs/undici#5596). The native runtimes return a normal ArrayBuffer-backed Buffer and are unaffected.
This matters much more in 0.35 than before, because the wasm32 runtime is no longer opt-in:
@img/sharp-wasm32 0.35.x has no cpu/os constraints, so it installs on every platform (0.34.x had cpu: ["wasm32"] and was skipped unless requested).
dist/sharp.mjs falls back to require('@img/sharp-wasm32/sharp.node') as a silent last resort — no warning is emitted when the native binary fails to load.
So a production deployment that used the native linux-x64 runtime on sharp 0.34 can upgrade to 0.35 and silently switch to wasm32 — and from that moment every fetch-uploaded toBuffer() result is corrupted, with no error anywhere and no local repro (dev machines load the native runtime). This is how we found it: ~3,300 corrupted image uploads (Next.js 16 on Vercel → supabase-js storage, which passes the Buffer as a raw fetch body) over ~a day in our print-shop MIS.
Steps:
mkdir repro && cd repro && npm init -ynpm install --cpu=wasm32 sharp # or: npm install sharp, then simulate native-load failure by renaming node_modules/@img/sharp-<platform>* — the silent fallback pathnode repro.mjs
What is the expected behaviour?
toBuffer() resolves with a Buffer backed by a normal, non-shared ArrayBuffer on every runtime, so the result is safe to hand to fetch, Blob, streams, etc. On the wasm32 path that presumably means one copy out of the wasm heap before resolving. Independently (or at least): emit a warning when the loader falls back to the wasm32 runtime, so a silent native→wasm degradation in production is visible.
Please provide a minimal, standalone code sample, without other dependencies, that demonstrates this problem
// repro.mjs — run once with the native runtime (clean) and once with wasm32 (corrupt)importsharpfrom'sharp';import{createHash}from'node:crypto';importhttpfrom'node:http';constsha=(b)=>createHash('sha256').update(b).digest('hex').slice(0,16);constserver=http.createServer((req,res)=>{constchunks=[];req.on('data',(c)=>chunks.push(c));req.on('end',()=>{constbody=Buffer.concat(chunks);res.end(JSON.stringify({len: body.length,sha: sha(body),head: body.subarray(0,4).toString('hex')}));});});awaitnewPromise((r)=>server.listen(0,'127.0.0.1',r));constbuf=awaitsharp({create: {width: 300,height: 200,channels: 3,background: {r: 200,g: 30,b: 30}},}).jpeg().toBuffer();console.log('SharedArrayBuffer-backed:',buf.buffer.constructor.name==='SharedArrayBuffer');console.log('sent ',{len: buf.length,sha: sha(buf),head: buf.subarray(0,4).toString('hex')});constres=awaitfetch(`http://127.0.0.1:${server.address().port}/`,{method: 'POST',body: buf});console.log('received',awaitres.json());server.close();
Output with the wasm32 runtime (Node v25.8.1; identical corruption signature observed on Node v24.18.0 linux-x64 on Vercel):
SharedArrayBuffer-backed: true
sent { len: 640, sha: '68000ffc9d46ed3a', head: 'ffd8ffdb' }
received { len: 686, sha: '73a15ff7aa14bfba', head: 'efbfbdef' }
The received bytes are exactly Buffer.from(buf.toString('utf8'), 'utf8') — a lossy UTF-8 roundtrip.
Output with the native runtime (same script, same machine):
SharedArrayBuffer-backed: false
sent { len: 640, sha: '68000ffc9d46ed3a', head: 'ffd8ffdb' }
received { len: 640, sha: '68000ffc9d46ed3a', head: 'ffd8ffdb' }
Additional context
Workaround for consumers: copy before upload — Buffer.from(buf) / wrap in a Blob — or ensure the native runtime actually loads.
Possible bug
Is this a possible bug in a feature of sharp, unrelated to installation?
npm install sharpcompletes without error.node -e "import 'sharp'"completes without error.Are you using the latest version of sharp?
sharpas reported bynpm view sharp dist-tags.latest(0.35.3).What is the output of running
npx envinfo --binaries --system --npmPackages=sharp --npmGlobalPackages=sharp?Also reproduced on Node v24.18.0 linux-x64 (Vercel).
Does this problem relate to file caching?
sharp.cache(false)does not fix this problem.Does this problem relate to images appearing to have been rotated by 90 degrees?
rotate()orkeepExif()does not fix this problem.What are the steps to reproduce?
When the wasm32 runtime is in use,
toBuffer()resolves with aBufferthat is a zero-copy view into the wasm heap — and because@img/sharp-wasm32is a threaded Emscripten build, that heap is aSharedArrayBuffer:SharedArrayBuffer-backed views are rejected or mishandled by many web APIs (
BufferSourcewithout[AllowShared], see nodejs/undici#4495). The concrete disaster case isfetch: Node.js/undici silently coerces such a view to a string, so the request body becomes the lossy UTF-8 decode/re-encode of the image — JPEG magicff d8 ff dbis sent asef bf bd ef bf bd …, inflated, 100% valid UTF-8, no error thrown (reported on the undici side as nodejs/undici#5596). The native runtimes return a normalArrayBuffer-backedBufferand are unaffected.This matters much more in 0.35 than before, because the wasm32 runtime is no longer opt-in:
@img/sharp-wasm320.35.x has nocpu/osconstraints, so it installs on every platform (0.34.x hadcpu: ["wasm32"]and was skipped unless requested).dist/sharp.mjsfalls back torequire('@img/sharp-wasm32/sharp.node')as a silent last resort — no warning is emitted when the native binary fails to load.libvips-cpp.sofrom the traced bundle (Turbopack: sharp 0.35.3 fails to load on Vercel (Next.js 16 Server Actions) with ERR_DLOPEN_FAILED: libvips-cpp.so.8.18.3 #4567).So a production deployment that used the native linux-x64 runtime on sharp 0.34 can upgrade to 0.35 and silently switch to wasm32 — and from that moment every
fetch-uploadedtoBuffer()result is corrupted, with no error anywhere and no local repro (dev machines load the native runtime). This is how we found it: ~3,300 corrupted image uploads (Next.js 16 on Vercel → supabase-js storage, which passes the Buffer as a raw fetch body) over ~a day in our print-shop MIS.Steps:
What is the expected behaviour?
toBuffer()resolves with aBufferbacked by a normal, non-sharedArrayBufferon every runtime, so the result is safe to hand tofetch,Blob, streams, etc. On the wasm32 path that presumably means one copy out of the wasm heap before resolving. Independently (or at least): emit a warning when the loader falls back to the wasm32 runtime, so a silent native→wasm degradation in production is visible.Please provide a minimal, standalone code sample, without other dependencies, that demonstrates this problem
Output with the wasm32 runtime (Node v25.8.1; identical corruption signature observed on Node v24.18.0 linux-x64 on Vercel):
The received bytes are exactly
Buffer.from(buf.toString('utf8'), 'utf8')— a lossy UTF-8 roundtrip.Output with the native runtime (same script, same machine):
Additional context
Buffer.from(buf)/ wrap in aBlob— or ensure the native runtime actually loads.TypeError) is reported separately: fetch: SharedArrayBuffer-backed view as request body is silently coerced to a string (lossy data corruption) nodejs/undici#5596. Even if undici starts throwing, sharp resolving SAB-backed Buffers would turn silent corruption into loud errors rather than correct behavior, so a copy on the wasm path (plus a fallback warning) still seems warranted.