Skip to content

feat: add HEAD handler #9753

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/famous-stingrays-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: add `HEAD` server method
6 changes: 3 additions & 3 deletions documentation/docs/20-core-concepts/10-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ Like `+layout.js`, `+layout.server.js` can export [page options](page-options)

## +server

As well as pages, you can define routes with a `+server.js` file (sometimes referred to as an 'API route' or an 'endpoint'), which gives you full control over the response. Your `+server.js` file exports functions corresponding to HTTP verbs like `GET`, `POST`, `PATCH`, `PUT`, `DELETE`, and `OPTIONS` that take a `RequestEvent` argument and return a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) object.
As well as pages, you can define routes with a `+server.js` file (sometimes referred to as an 'API route' or an 'endpoint'), which gives you full control over the response. Your `+server.js` file exports functions corresponding to HTTP verbs like `GET`, `POST`, `PATCH`, `PUT`, `DELETE`, `OPTIONS`, and `HEAD` that take a `RequestEvent` argument and return a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) object.

For example we could create an `/api/random-number` route with a `GET` handler:

Expand Down Expand Up @@ -283,7 +283,7 @@ If an error is thrown (either `throw error(...)` or an unexpected error), the re

### Receiving data

By exporting `POST`/`PUT`/`PATCH`/`DELETE`/`OPTIONS` handlers, `+server.js` files can be used to create a complete API:
By exporting `POST`/`PUT`/`PATCH`/`DELETE`/`OPTIONS`/`HEAD` handlers, `+server.js` files can be used to create a complete API:

```svelte
/// file: src/routes/add/+page.svelte
Expand Down Expand Up @@ -330,7 +330,7 @@ export async function POST({ request }) {
`+server.js` files can be placed in the same directory as `+page` files, allowing the same route to be either a page or an API endpoint. To determine which, SvelteKit applies the following rules:

- `PUT`/`PATCH`/`DELETE`/`OPTIONS` requests are always handled by `+server.js` since they do not apply to pages
- `GET`/`POST` requests are treated as page requests if the `accept` header prioritises `text/html` (in other words, it's a browser page request), else they are handled by `+server.js`.
- `GET`/`POST`/`HEAD` requests are treated as page requests if the `accept` header prioritises `text/html` (in other words, it's a browser page request), else they are handled by `+server.js`.
- Responses to `GET` requests will inlcude a `Vary: Accept` header, so that proxies and browsers cache HTML and JSON responses separately.

## $types
Expand Down
12 changes: 12 additions & 0 deletions packages/kit/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,15 @@
export const SVELTE_KIT_ASSETS = '/_svelte_kit_assets';

export const GENERATED_COMMENT = '// this file is generated — do not edit it\n';

export const ENDPOINT_METHODS = new Set([
'GET',
'POST',
'PUT',
'PATCH',
'DELETE',
'OPTIONS',
'HEAD'
]);

export const PAGE_METHODS = new Set(['GET', 'POST', 'HEAD']);
12 changes: 6 additions & 6 deletions packages/kit/src/core/postbuild/analyse.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { forked } from '../../utils/fork.js';
import { should_polyfill } from '../../utils/platform.js';
import { installPolyfills } from '../../exports/node/polyfills.js';
import { resolvePath } from '../../exports/index.js';
import { ENDPOINT_METHODS } from '../../constants.js';
import { filter_private_env, filter_public_env } from '../../utils/env.js';

export default forked(import.meta.url, analyse);
Expand Down Expand Up @@ -92,12 +93,11 @@ async function analyse({ manifest_path, env }) {
prerender = mod.prerender;
}

if (mod.GET) api_methods.push('GET');
if (mod.POST) api_methods.push('POST');
if (mod.PUT) api_methods.push('PUT');
if (mod.PATCH) api_methods.push('PATCH');
if (mod.DELETE) api_methods.push('DELETE');
if (mod.OPTIONS) api_methods.push('OPTIONS');
Object.values(mod).forEach((/** @type {import('types').HttpMethod} */ method) => {
if (mod[method] && ENDPOINT_METHODS.has(method)) {
api_methods.push(method);
}
});

config = mod.config;
entries = mod.entries;
Expand Down
11 changes: 0 additions & 11 deletions packages/kit/src/exports/vite/build/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,3 @@ export function resolve_symlinks(manifest, file) {
export function assets_base(config) {
return (config.paths.assets || config.paths.base || '.') + '/';
}

const method_names = new Set(['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH', 'OPTIONS']);

// If we'd written this in TypeScript, it could be easy...
/**
* @param {string} str
* @returns {str is import('types').HttpMethod}
*/
export function is_http_method(str) {
return method_names.has(str);
}
5 changes: 3 additions & 2 deletions packages/kit/src/runtime/server/endpoint.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ENDPOINT_METHODS, PAGE_METHODS } from '../../constants.js';
import { negotiate } from '../../utils/http.js';
import { Redirect } from '../control.js';
import { method_not_allowed } from './utils.js';
Expand Down Expand Up @@ -79,8 +80,8 @@ export async function render_endpoint(event, mod, state) {
export function is_endpoint_request(event) {
const { method, headers } = event.request;

if (method === 'PUT' || method === 'PATCH' || method === 'DELETE' || method === 'OPTIONS') {
// These methods exist exclusively for endpoints
// These methods exist exclusively for endpoints
if (ENDPOINT_METHODS.has(method) && !PAGE_METHODS.has(method)) {
return true;
}

Expand Down
5 changes: 2 additions & 3 deletions packages/kit/src/runtime/server/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { coalesce_to_error } from '../../utils/error.js';
import { negotiate } from '../../utils/http.js';
import { HttpError } from '../control.js';
import { fix_stack_trace } from '../shared-server.js';
import { ENDPOINT_METHODS } from '../../constants.js';

/** @param {any} body */
export function is_pojo(body) {
Expand Down Expand Up @@ -34,9 +35,7 @@ export function method_not_allowed(mod, method) {

/** @param {Partial<Record<import('types').HttpMethod, any>>} mod */
export function allowed_methods(mod) {
const allowed = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'].filter(
(method) => method in mod
);
const allowed = Array.from(ENDPOINT_METHODS).filter((method) => method in mod);

if ('GET' in mod || 'HEAD' in mod) allowed.push('HEAD');

Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/utils/exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const valid_server_exports = new Set([
'PUT',
'DELETE',
'OPTIONS',
'HEAD',
'prerender',
'trailingSlash',
'config',
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/utils/exports.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ test('validates +server.js', () => {
validate_server_exports({
answer: 42
});
}, "Invalid export 'answer' (valid exports are GET, POST, PATCH, PUT, DELETE, OPTIONS, prerender, trailingSlash, config, entries, or anything with a '_' prefix)");
}, "Invalid export 'answer' (valid exports are GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD, prerender, trailingSlash, config, entries, or anything with a '_' prefix)");

check_error(() => {
validate_server_exports({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('@sveltejs/kit').RequestHandler} */
export function HEAD() {
return new Response('', {
headers: {
'x-sveltekit-head-endpoint': 'true'
}
});
}
28 changes: 27 additions & 1 deletion packages/kit/test/apps/basics/test/server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ test.describe('Endpoints', () => {
});

test('OPTIONS handler', async ({ request }) => {
const url = '/endpoint-output/options';
const url = '/endpoint-output';

const response = await request.fetch(url, {
method: 'OPTIONS'
Expand All @@ -199,6 +199,32 @@ test.describe('Endpoints', () => {
expect(response.status()).toBe(200);
expect(await response.text()).toBe('ok');
});

test('HEAD handler', async ({ request }) => {
const url = '/endpoint-output/head-handler';

const page_response = await request.fetch(url, {
method: 'HEAD',
headers: {
accept: 'text/html'
}
});

expect(page_response.status()).toBe(200);
expect(await page_response.text()).toBe('');
expect(page_response.headers()['x-sveltekit-page']).toBe('true');

const endpoint_response = await request.fetch(url, {
method: 'HEAD',
headers: {
accept: 'application/json'
}
});

expect(endpoint_response.status()).toBe(200);
expect(await endpoint_response.text()).toBe('');
expect(endpoint_response.headers()['x-sveltekit-head-endpoint']).toBe('true');
});
});

test.describe('Errors', () => {
Expand Down