Skip to content

Commit

Permalink
feat: add HEAD handler (#9753)
Browse files Browse the repository at this point in the history
closes #9468

Allows exporting the HEAD handler
  • Loading branch information
eltigerchino committed Jul 4, 2023
1 parent 67a0d86 commit c1ad5b2
Show file tree
Hide file tree
Showing 13 changed files with 68 additions and 27 deletions.
5 changes: 5 additions & 0 deletions .changeset/famous-stingrays-change.md
@@ -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
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
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
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
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
@@ -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
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
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
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
Empty file.
@@ -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
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

0 comments on commit c1ad5b2

Please sign in to comment.