diff --git a/.changeset/shaggy-mails-eat.md b/.changeset/shaggy-mails-eat.md new file mode 100644 index 000000000000..1402f5b5ff02 --- /dev/null +++ b/.changeset/shaggy-mails-eat.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +fix: use the UTF-8 byte length when reading inline assets diff --git a/packages/kit/src/runtime/app/server/index.js b/packages/kit/src/runtime/app/server/index.js index dbf2ad325432..8169635c6b18 100644 --- a/packages/kit/src/runtime/app/server/index.js +++ b/packages/kit/src/runtime/app/server/index.js @@ -1,6 +1,6 @@ import { read_implementation, manifest } from '../../server/internal.js'; import { assets } from '#app/paths'; -import { base64_decode } from '../../utils.js'; +import { base64_decode, text_encoder } from '../../utils.js'; import * as e from '../../../messages/server-errors.js'; /** @@ -42,11 +42,11 @@ export function read(asset) { }); } - const decoded = decodeURIComponent(data); + const decoded = text_encoder.encode(decodeURIComponent(data)); return new Response(decoded, { headers: { - 'Content-Length': decoded.length.toString(), + 'Content-Length': decoded.byteLength.toString(), 'Content-Type': type } }); diff --git a/packages/kit/src/runtime/app/server/index.spec.js b/packages/kit/src/runtime/app/server/index.spec.js new file mode 100644 index 000000000000..d97c9687ad3f --- /dev/null +++ b/packages/kit/src/runtime/app/server/index.spec.js @@ -0,0 +1,33 @@ +import { afterAll, expect, test, vi } from 'vitest'; + +vi.stubGlobal('__SVELTEKIT_DEV__', false); +vi.stubGlobal('__SVELTEKIT_APP_VERSION__', 'test'); +vi.stubGlobal('__SVELTEKIT_TRACK__', vi.fn()); +afterAll(() => vi.unstubAllGlobals()); + +const { read } = await import('./index.js'); +const { set_read_implementation } = await import('../../server/internal.js'); +set_read_implementation(() => new ReadableStream()); + +test.each(['', 'hello', 'café', '中文', '\u{1f680}'])( + 'read uses the byte length of an inline asset: %j', + async (title) => { + const text = `${title}`; + const response = read(`data:image/svg+xml,${encodeURIComponent(text)}`); + const body = await response.arrayBuffer(); + + expect(new TextDecoder().decode(body)).toBe(text); + expect(response.headers.get('content-type')).toBe('image/svg+xml'); + expect(response.headers.get('content-length')).toBe(body.byteLength.toString()); + } +); + +test.each(['data:text/plain,café', 'data:text/plain;base64,Y2Fmw6k='])( + 'read preserves unescaped and base64 inline assets: %s', + async (asset) => { + const response = read(asset); + expect(await response.text()).toBe('café'); + expect(response.headers.get('content-type')).toBe('text/plain'); + expect(response.headers.get('content-length')).toBe('5'); + } +);