Summary
A Next.js App Router route handler (export const runtime = 'nodejs') that returns binary data via new Response(buffer) where buffer is a Node Buffer (e.g. the return value of sharp(...).toBuffer()) delivers corrupted bytes when deployed on Vercel: the body appears to be decoded as UTF-8 text and re-encoded, so every byte sequence that is not valid UTF-8 is replaced with EF BF BD (U+FFFD). In our case a 31,054-byte WebP image arrived as 56,700 bytes and the browser failed with EncodingError.
The identical build served locally with next start returns the body byte-perfect, which is why this looks like a Vercel function egress issue rather than a Next.js one.
Environment
- Next.js 16.2.10 (App Router, Turbopack build)
- Route handler with
export const runtime = 'nodejs' - Vercel Node.js functions, Node 24
- Observed 2026-07-28, regions fra1 / iad1
Minimal reproduction
// app/api/repro/route.ts
export const runtime = 'nodejs'
export async function GET() {
// 16 bytes, several of them invalid as UTF-8
const bytes = Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
0xff, 0xd8, 0xff, 0xe0, 0xc3, 0x28, 0x80, 0xbf,
])
return new Response(bytes, {
headers: { 'Content-Type': 'application/octet-stream' },
})
}
Check with:
curl -s https://<deployment>/api/repro | od -A x -t x1
- Local
next start: exactly the 16 constructed bytes. - On Vercel: high bytes replaced by
ef bf bd, body longer than 16 bytes.
Expected
Byte-identical delivery of the response body regardless of whether the BodyInit is a Buffer or a plain Uint8Array — Buffer is a Uint8Array subclass and a valid BodyInit per the Fetch spec.
Workaround that fixes it
Copying the Buffer into a plain Uint8Array and setting an explicit Content-Length:
headers.set('Content-Length', String(buf.byteLength))
return new Response(new Uint8Array(buf), { headers, status: 200 })
Both changes were applied together, so I cannot yet say whether the fresh copy or the explicit Content-Length is the effective part.
Open question
Are ReadableStream bodies whose chunks are Node Buffers (e.g. Readable.toWeb(nodeStream)) affected as well? We now defensively copy each stream chunk into a plain Uint8Array, but could not verify the streaming path on Vercel yet.
Happy to provide deployment IDs / timestamps privately if that helps debugging.