From 012850e6ddaa0bac1df63119952466d30d991913 Mon Sep 17 00:00:00 2001 From: Dominik G Date: Mon, 14 Feb 2022 22:58:06 +0100 Subject: [PATCH] [fix] default handlers for head (#3903) * fix: respond to HEAD requests in endpoints * chore: cleanup * fix: handle head requests when loading shadow data * add changeset * refactor: normalize request method without replace * fix: use instead of empty string for HEAD response body * Apply suggestions from code review Co-authored-by: Rich Harris * code style tweaks * remove more type annotations * more code style * Apply suggestions from code review Co-authored-by: Rich Harris Co-authored-by: Rich Harris --- .changeset/chatty-impalas-smash.md | 5 ++ packages/kit/src/runtime/server/endpoint.js | 11 +++- .../kit/src/runtime/server/page/load_node.js | 13 +++-- packages/kit/src/runtime/server/utils.js | 8 +++ packages/kit/test/apps/basics/test/test.js | 58 +++++++++++++++++++ 5 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 .changeset/chatty-impalas-smash.md diff --git a/.changeset/chatty-impalas-smash.md b/.changeset/chatty-impalas-smash.md new file mode 100644 index 000000000000..f20be82f214f --- /dev/null +++ b/.changeset/chatty-impalas-smash.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +handle HEAD requests in endpoints diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js index 843684d9bc9e..584418ca9b1e 100644 --- a/packages/kit/src/runtime/server/endpoint.js +++ b/packages/kit/src/runtime/server/endpoint.js @@ -1,6 +1,6 @@ import { to_headers } from '../../utils/http.js'; import { hash } from '../hash.js'; -import { is_pojo } from './utils.js'; +import { is_pojo, normalize_request_method } from './utils.js'; /** @param {string} body */ function error(body) { @@ -40,9 +40,14 @@ export function is_text(content_type) { * @returns {Promise} */ export async function render_endpoint(event, mod) { + const method = normalize_request_method(event); + /** @type {import('types/endpoint').RequestHandler} */ - const handler = mod[event.request.method.toLowerCase().replace('delete', 'del')]; // 'delete' is a reserved word + let handler = mod[method]; + if (!handler && method === 'head') { + handler = mod.get; + } if (!handler) { return; } @@ -92,7 +97,7 @@ export async function render_endpoint(event, mod) { } } - return new Response(normalized_body, { + return new Response(method !== 'head' ? normalized_body : undefined, { status, headers }); diff --git a/packages/kit/src/runtime/server/page/load_node.js b/packages/kit/src/runtime/server/page/load_node.js index cbcf18330c2d..36f78ca05223 100644 --- a/packages/kit/src/runtime/server/page/load_node.js +++ b/packages/kit/src/runtime/server/page/load_node.js @@ -4,7 +4,7 @@ import { s } from '../../../utils/misc.js'; import { escape_json_in_html } from '../../../utils/escape.js'; import { is_root_relative, resolve } from '../../../utils/url.js'; import { create_prerendering_url_proxy } from './utils.js'; -import { is_pojo, lowercase_keys } from '../utils.js'; +import { is_pojo, lowercase_keys, normalize_request_method } from '../utils.js'; import { coalesce_to_error } from '../../../utils/error.js'; /** @@ -385,8 +385,8 @@ async function load_shadow_data(route, event, options, prerender) { throw new Error('Cannot prerender pages that have shadow endpoints with mutative methods'); } - const method = event.request.method.toLowerCase().replace('delete', 'del'); - const handler = mod[method]; + const method = normalize_request_method(event); + const handler = method === 'head' ? mod.head || mod.get : mod[method]; if (!handler) { return { @@ -402,7 +402,7 @@ async function load_shadow_data(route, event, options, prerender) { body: {} }; - if (method !== 'get') { + if (method !== 'get' && method !== 'head') { const result = await handler(event); if (result.fallthrough) return result; @@ -426,8 +426,9 @@ async function load_shadow_data(route, event, options, prerender) { data.body = body; } - if (mod.get) { - const result = await mod.get.call(null, event); + const get = (method === 'head' && mod.head) || mod.get; + if (get) { + const result = await get(event); if (result.fallthrough) return result; diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index 345c076928e2..cd9543f9f209 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -49,3 +49,11 @@ export function is_pojo(body) { return true; } +/** + * @param {import('types/hooks').RequestEvent} event + * @returns string + */ +export function normalize_request_method(event) { + const method = event.request.method.toLowerCase(); + return method === 'delete' ? 'del' : method; // 'delete' is a reserved word +} diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 743ee24b0f00..f2a740b4aedf 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -431,6 +431,38 @@ test.describe.parallel('Shadowed pages', () => { expect(await response.json()).toEqual({ answer: 42 }); }); + + test('responds to HEAD requests from endpoint', async ({ request }) => { + const url = '/shadowed/simple'; + + const opts = { + headers: { + accept: 'application/json' + } + }; + + const responses = { + head: await request.head(url, opts), + get: await request.get(url, opts) + }; + + const headers = { + head: responses.head.headers(), + get: responses.get.headers() + }; + + expect(responses.head.status()).toBe(200); + expect(responses.get.status()).toBe(200); + expect(await responses.head.text()).toBe(''); + expect(await responses.get.json()).toEqual({ answer: 42 }); + + ['date', 'transfer-encoding'].forEach((name) => { + delete headers.head[name]; + delete headers.get[name]; + }); + + expect(headers.head).toEqual(headers.get); + }); }); test.describe.parallel('Endpoints', () => { @@ -459,6 +491,32 @@ test.describe.parallel('Endpoints', () => { expect(response.headers()['set-cookie']).toBeDefined(); }); + test('HEAD with matching headers but without body', async ({ request }) => { + const url = '/endpoint-output/body'; + + const responses = { + head: await request.head(url), + get: await request.get(url) + }; + + const headers = { + head: responses.head.headers(), + get: responses.get.headers() + }; + + expect(responses.head.status()).toBe(200); + expect(responses.get.status()).toBe(200); + expect(await responses.head.text()).toBe(''); + expect(await responses.get.text()).toBe('{}'); + + ['date', 'transfer-encoding'].forEach((name) => { + delete headers.head[name]; + delete headers.get[name]; + }); + + expect(headers.head).toEqual(headers.get); + }); + test('200 status by default', async ({ request }) => { const response = await request.get('/endpoint-output/body'); expect(/** @type {import('@playwright/test').APIResponse} */ (response).status()).toBe(200);