diff --git a/.changeset/great-guests-visit.md b/.changeset/great-guests-visit.md new file mode 100644 index 000000000000..3f8001aaffe6 --- /dev/null +++ b/.changeset/great-guests-visit.md @@ -0,0 +1,7 @@ +--- +'@sveltejs/adapter-cloudflare-workers': patch +'@sveltejs/adapter-netlify': patch +'@sveltejs/kit': patch +--- + +Update and consolidate checks for binary body types diff --git a/packages/adapter-cloudflare-workers/files/entry.js b/packages/adapter-cloudflare-workers/files/entry.js index 34e8834639b2..5b5dde625d0d 100644 --- a/packages/adapter-cloudflare-workers/files/entry.js +++ b/packages/adapter-cloudflare-workers/files/entry.js @@ -1,6 +1,7 @@ // TODO hardcoding the relative location makes this brittle import { init, render } from '../output/server/app.js'; // eslint-disable-line import/no-unresolved import { getAssetFromKV, NotFoundError } from '@cloudflare/kv-asset-handler'; // eslint-disable-line import/no-unresolved +import { isContentTypeBinary } from '@sveltejs/kit/adapter-utils'; // eslint-disable-line import/no-unresolved init(); @@ -57,7 +58,7 @@ async function handle(event) { /** @param {Request} request */ async function read(request) { const type = request.headers.get('content-type') || ''; - if (type.includes('application/octet-stream')) { + if (isContentTypeBinary(type)) { return new Uint8Array(await request.arrayBuffer()); } diff --git a/packages/adapter-netlify/files/entry.js b/packages/adapter-netlify/files/entry.js index 57356d123d3a..cd920d418b3a 100644 --- a/packages/adapter-netlify/files/entry.js +++ b/packages/adapter-netlify/files/entry.js @@ -1,5 +1,6 @@ // TODO hardcoding the relative location makes this brittle import { init, render } from '../output/server/app.js'; // eslint-disable-line import/no-unresolved +import { isContentTypeBinary } from '@sveltejs/kit/adapter-utils'; // eslint-disable-line import/no-unresolved init(); @@ -8,8 +9,9 @@ export async function handler(event) { const query = new URLSearchParams(rawQuery); + const type = headers['content-type']; const rawBody = - headers['content-type'] === 'application/octet-stream' + type && isContentTypeBinary(type) ? new TextEncoder('base64').encode(body) : isBase64Encoded ? Buffer.from(body, 'base64').toString() diff --git a/packages/kit/package.json b/packages/kit/package.json index c9b4af2b3182..dc15460bae37 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -74,6 +74,9 @@ "./install-fetch": { "import": "./dist/install-fetch.js" }, + "./adapter-utils": { + "import": "./dist/adapter-utils.js" + }, "./types": "./types/index.d.ts" }, "types": "types/index.d.ts", diff --git a/packages/kit/rollup.config.js b/packages/kit/rollup.config.js index 2abdbec70ff5..a24a103f267c 100644 --- a/packages/kit/rollup.config.js +++ b/packages/kit/rollup.config.js @@ -48,7 +48,8 @@ export default [ cli: 'src/cli.js', ssr: 'src/runtime/server/index.js', node: 'src/core/node/index.js', - 'install-fetch': 'src/install-fetch.js' + 'install-fetch': 'src/install-fetch.js', + 'adapter-utils': 'src/core/adapter-utils.js' }, output: { dir: 'dist', diff --git a/packages/kit/src/core/adapter-utils.js b/packages/kit/src/core/adapter-utils.js new file mode 100644 index 000000000000..192a962ff57c --- /dev/null +++ b/packages/kit/src/core/adapter-utils.js @@ -0,0 +1,16 @@ +/** + * Decides how the body should be parsed based on its mime type. + * + * This is intended to be used with both requests and responses, to have a consistent body parsing across adapters. + * + * @param {string} content_type The `content-type` header of a request/response. + * @returns {boolean} + */ +export function isContentTypeBinary(content_type) { + return ( + content_type.startsWith('image/') || + content_type.startsWith('audio/') || + content_type.startsWith('video/') || + content_type.startsWith('application/octet-stream') + ); +} diff --git a/packages/kit/src/core/node/index.js b/packages/kit/src/core/node/index.js index 7f8e0a57012d..451268a6cba7 100644 --- a/packages/kit/src/core/node/index.js +++ b/packages/kit/src/core/node/index.js @@ -1,3 +1,5 @@ +import { isContentTypeBinary } from '../adapter-utils.js'; + /** * @param {import('http').IncomingMessage} req * @returns {Promise} @@ -48,7 +50,7 @@ export function getRawBody(req) { req.on('end', () => { const [type] = h['content-type'].split(/;\s*/); - if (type === 'application/octet-stream') { + if (isContentTypeBinary(type)) { return fulfil(data); } diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js index c3349929da08..94c232a47bca 100644 --- a/packages/kit/src/runtime/server/endpoint.js +++ b/packages/kit/src/runtime/server/endpoint.js @@ -1,3 +1,4 @@ +import { isContentTypeBinary } from '../../core/adapter-utils.js'; import { lowercase_keys } from './utils.js'; /** @param {string} body */ @@ -37,24 +38,28 @@ export default async function render_route(request, route) { headers = lowercase_keys(headers); const type = headers['content-type']; + const is_type_binary = type && isContentTypeBinary(type); + // validation - if (type === 'application/octet-stream' && !(body instanceof Uint8Array)) { + if (is_type_binary && !(body instanceof Uint8Array)) { return error( - `${preface}: body must be an instance of Uint8Array if content type is application/octet-stream` + `${preface}: body must be an instance of Uint8Array if content type is image/*, audio/*, video/* or application/octet-stream` ); } - if (body instanceof Uint8Array && type !== 'application/octet-stream') { + if (body instanceof Uint8Array && !is_type_binary) { return error( - `${preface}: Uint8Array body must be accompanied by content-type: application/octet-stream header` + `${preface}: Uint8Array body must have content-type header of image/*, audio/*, video/* or application/octet-stream` ); } /** @type {import('types/hooks').StrictBody} */ let normalized_body; + // ensure the body is an object if ( (typeof body === 'object' || typeof body === 'undefined') && + !(body instanceof Uint8Array) && (!type || type.startsWith('application/json')) ) { headers = { ...headers, 'content-type': 'application/json; charset=utf-8' };