Skip to content

Commit

Permalink
[fix] default handlers for head (#3903)
Browse files Browse the repository at this point in the history
* 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 <hello@rich-harris.dev>

* code style tweaks

* remove more type annotations

* more code style

* Apply suggestions from code review

Co-authored-by: Rich Harris <hello@rich-harris.dev>

Co-authored-by: Rich Harris <hello@rich-harris.dev>
  • Loading branch information
dominikg and Rich-Harris committed Feb 14, 2022
1 parent d520d40 commit 012850e
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/chatty-impalas-smash.md
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

handle HEAD requests in endpoints
11 changes: 8 additions & 3 deletions 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) {
Expand Down Expand Up @@ -40,9 +40,14 @@ export function is_text(content_type) {
* @returns {Promise<Response | undefined>}
*/
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;
}
Expand Down Expand Up @@ -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
});
Expand Down
13 changes: 7 additions & 6 deletions packages/kit/src/runtime/server/page/load_node.js
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand All @@ -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;

Expand Down
8 changes: 8 additions & 0 deletions packages/kit/src/runtime/server/utils.js
Expand Up @@ -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
}
58 changes: 58 additions & 0 deletions packages/kit/test/apps/basics/test/test.js
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 012850e

Please sign in to comment.