diff --git a/.changeset/fluffy-cheetahs-remain.md b/.changeset/fluffy-cheetahs-remain.md new file mode 100644 index 000000000000..bf4424eb6222 --- /dev/null +++ b/.changeset/fluffy-cheetahs-remain.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +handle undefined body on endpoint output diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js index 0467f418413b..c3349929da08 100644 --- a/packages/kit/src/runtime/server/endpoint.js +++ b/packages/kit/src/runtime/server/endpoint.js @@ -25,12 +25,11 @@ export default async function render_route(request, route) { const params = route.params(match); const response = await handler({ ...request, params }); + const preface = `Invalid response from route ${request.path}`; if (response) { if (typeof response !== 'object') { - return error( - `Invalid response from route ${request.path}: expected an object, got ${typeof response}` - ); + return error(`${preface}: expected an object, got ${typeof response}`); } let { status = 200, body, headers = {} } = response; @@ -41,13 +40,13 @@ export default async function render_route(request, route) { // validation if (type === 'application/octet-stream' && !(body instanceof Uint8Array)) { return error( - `Invalid response from route ${request.path}: 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 application/octet-stream` ); } if (body instanceof Uint8Array && type !== 'application/octet-stream') { return error( - `Invalid response from route ${request.path}: Uint8Array body must be accompanied by content-type: application/octet-stream header` + `${preface}: Uint8Array body must be accompanied by content-type: application/octet-stream header` ); } @@ -55,11 +54,11 @@ export default async function render_route(request, route) { let normalized_body; if ( - typeof body === 'object' && - (!type || type === 'application/json' || type === 'application/json; charset=utf-8') + (typeof body === 'object' || typeof body === 'undefined') && + (!type || type.startsWith('application/json')) ) { headers = { ...headers, 'content-type': 'application/json; charset=utf-8' }; - normalized_body = JSON.stringify(body); + normalized_body = JSON.stringify(body || {}); } else { normalized_body = /** @type {import('types/hooks').StrictBody} */ (body); } diff --git a/packages/kit/test/apps/basics/src/routes/endpoint-output/_tests.js b/packages/kit/test/apps/basics/src/routes/endpoint-output/_tests.js new file mode 100644 index 000000000000..a65e96fe43b8 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/endpoint-output/_tests.js @@ -0,0 +1,39 @@ +import * as assert from 'uvu/assert'; + +/** @type {import('test').TestMaker} */ +export default function (test) { + test('not ok on void endpoint', null, async ({ fetch }) => { + const res = await fetch('/endpoint-output/empty', { method: 'DELETE' }); + assert.equal(res.ok, false); + }); + test('200 status on empty endpoint', null, async ({ fetch }) => { + const res = await fetch('/endpoint-output/empty'); + assert.equal(res.status, 200); + assert.equal(await res.json(), {}); + }); + + test('set-cookie without body', null, async ({ fetch }) => { + const res = await fetch('/endpoint-output/headers'); + assert.equal(res.status, 200); + assert.equal(res.headers.has('set-cookie'), true); + }); + + test('200 status by default', null, async ({ fetch }) => { + const res = await fetch('/endpoint-output/body'); + assert.equal(res.status, 200); + assert.equal(await res.text(), '{}'); + }); + + test('does not throw on blob method', null, async ({ fetch }) => { + const res = await fetch('/endpoint-output/empty'); + assert.type(await res.blob(), 'object'); + }); + test('does not throw on arrayBuffer method', null, async ({ fetch }) => { + const res = await fetch('/endpoint-output/empty'); + assert.type(await res.arrayBuffer(), 'object'); + }); + test('does not throw on buffer method', null, async ({ fetch }) => { + const res = await fetch('/endpoint-output/empty'); + assert.type(await res.buffer(), 'object'); + }); +} diff --git a/packages/kit/test/apps/basics/src/routes/endpoint-output/body.js b/packages/kit/test/apps/basics/src/routes/endpoint-output/body.js new file mode 100644 index 000000000000..6cafc71b6f6f --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/endpoint-output/body.js @@ -0,0 +1,4 @@ +/** @type {import('@sveltejs/kit').RequestHandler} */ +export function get() { + return { body: {} }; +} diff --git a/packages/kit/test/apps/basics/src/routes/endpoint-output/empty.js b/packages/kit/test/apps/basics/src/routes/endpoint-output/empty.js new file mode 100644 index 000000000000..78ba9f6fb252 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/endpoint-output/empty.js @@ -0,0 +1,7 @@ +/** @type {import('@sveltejs/kit').RequestHandler} */ +export function get() { + return {}; +} + +/** @type {import('@sveltejs/kit').RequestHandler} */ +export function del() {} diff --git a/packages/kit/test/apps/basics/src/routes/endpoint-output/headers.js b/packages/kit/test/apps/basics/src/routes/endpoint-output/headers.js new file mode 100644 index 000000000000..481f0f882e4d --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/endpoint-output/headers.js @@ -0,0 +1,8 @@ +/** @type {import('@sveltejs/kit').RequestHandler} */ +export function get() { + return { + headers: { + 'Set-Cookie': 'foo=bar' + } + }; +}