diff --git a/packages/app/src/cli/services/dev/extension/server/middlewares.test.ts b/packages/app/src/cli/services/dev/extension/server/middlewares.test.ts index a0863aef7c..9fc0ff5883 100644 --- a/packages/app/src/cli/services/dev/extension/server/middlewares.test.ts +++ b/packages/app/src/cli/services/dev/extension/server/middlewares.test.ts @@ -133,7 +133,7 @@ describe('fileServerMiddleware()', async () => { }) expect(event.node.res.setHeader).toHaveBeenCalledWith('Content-Type', 'text/html') - expect(result).toBe('') + expect(String(result)).toBe('') }) }) @@ -166,7 +166,25 @@ describe('fileServerMiddleware()', async () => { }) expect(event.node.res.setHeader).toHaveBeenCalledWith('Content-Type', contentType) - expect(result).toBe(fileContent) + expect(String(result)).toBe(fileContent) + }) + }) + + test('serves binary files as a Buffer without UTF-8 corruption', async () => { + await inTemporaryDirectory(async (tmpDir: string) => { + // Bytes that are invalid as UTF-8 input (0x89, 0xFF, 0xFE) — if the + // middleware decoded these as UTF-8 they'd collapse to U+FFFD and the + // image would be corrupt. Includes the real PNG magic header. + const pngBytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0xfe, 0x00, 0x42]) + await mkdir(joinPath(tmpDir, 'img')) + await writeFile(joinPath(tmpDir, 'img', 'logo.png'), pngBytes) + + const event = getMockEvent() + const result = await fileServerMiddleware(event, {filePath: joinPath(tmpDir, 'img', 'logo.png')}) + + expect(event.node.res.setHeader).toHaveBeenCalledWith('Content-Type', 'image/png') + expect(Buffer.isBuffer(result)).toBe(true) + expect(Buffer.compare(result as Buffer, pngBytes)).toBe(0) }) }) @@ -183,7 +201,7 @@ describe('fileServerMiddleware()', async () => { }) expect(event.node.res.setHeader).toHaveBeenCalledWith('Content-Type', 'text/plain') - expect(result).toBe('Content for bar.foo') + expect(String(result)).toBe('Content for bar.foo') }) }) }) @@ -275,7 +293,7 @@ describe('getExtensionAssetMiddleware()', () => { const result = await getExtensionAssetMiddleware(options)(event) expect(event.node.res.setHeader).toHaveBeenCalledWith('Content-Type', 'text/javascript') - expect(result).toBe('compiled bundle content') + expect(String(result)).toBe('compiled bundle content') }) }) diff --git a/packages/app/src/cli/services/dev/extension/server/middlewares.ts b/packages/app/src/cli/services/dev/extension/server/middlewares.ts index a205ab4a18..3ba750872a 100644 --- a/packages/app/src/cli/services/dev/extension/server/middlewares.ts +++ b/packages/app/src/cli/services/dev/extension/server/middlewares.ts @@ -41,7 +41,11 @@ export async function fileServerMiddleware(event: H3Event, options: {filePath: s return sendError(event, {statusCode: 404, statusMessage: `Not Found: ${filePath}`}) } - const fileContent = await readFile(filePath) + // Pass `{}` to opt out of cli-kit's `{encoding: 'utf8'}` default — binary + // files (png, jpeg, pdf, wasm, …) must come back as a Buffer or their bytes + // get mangled into U+FFFD replacement chars by the UTF-8 decode. h3 sends + // Buffers as-is and the browser decodes per Content-Type for text types. + const fileContent = await readFile(filePath, {}) const extensionToContent = { '.ico': 'image/x-icon', '.html': 'text/html',