From c1ad5b2f84762a34adb327a0e309792603b405b3 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Tue, 4 Jul 2023 21:50:44 +0800 Subject: [PATCH] feat: add `HEAD` handler (#9753) closes #9468 Allows exporting the HEAD handler --- .changeset/famous-stingrays-change.md | 5 ++++ .../docs/20-core-concepts/10-routing.md | 6 ++-- packages/kit/src/constants.js | 12 ++++++++ packages/kit/src/core/postbuild/analyse.js | 12 ++++---- packages/kit/src/exports/vite/build/utils.js | 11 -------- packages/kit/src/runtime/server/endpoint.js | 5 ++-- packages/kit/src/runtime/server/utils.js | 5 ++-- packages/kit/src/utils/exports.js | 1 + packages/kit/src/utils/exports.spec.js | 2 +- .../endpoint-output/{options => }/+server.js | 0 .../endpoint-output/head-handler/+page.svelte | 0 .../endpoint-output/head-handler/+server.js | 8 ++++++ .../kit/test/apps/basics/test/server.test.js | 28 ++++++++++++++++++- 13 files changed, 68 insertions(+), 27 deletions(-) create mode 100644 .changeset/famous-stingrays-change.md rename packages/kit/test/apps/basics/src/routes/endpoint-output/{options => }/+server.js (100%) create mode 100644 packages/kit/test/apps/basics/src/routes/endpoint-output/head-handler/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/endpoint-output/head-handler/+server.js diff --git a/.changeset/famous-stingrays-change.md b/.changeset/famous-stingrays-change.md new file mode 100644 index 000000000000..39b7ad106ab3 --- /dev/null +++ b/.changeset/famous-stingrays-change.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: add `HEAD` server method diff --git a/documentation/docs/20-core-concepts/10-routing.md b/documentation/docs/20-core-concepts/10-routing.md index 5cb48bd52e4a..00716e7a3647 100644 --- a/documentation/docs/20-core-concepts/10-routing.md +++ b/documentation/docs/20-core-concepts/10-routing.md @@ -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: @@ -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 @@ -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 diff --git a/packages/kit/src/constants.js b/packages/kit/src/constants.js index 60c17e2beca2..4360335d4820 100644 --- a/packages/kit/src/constants.js +++ b/packages/kit/src/constants.js @@ -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']); diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index 689b017294f8..a78924d72221 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -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); @@ -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; diff --git a/packages/kit/src/exports/vite/build/utils.js b/packages/kit/src/exports/vite/build/utils.js index 82b69e5998cd..7670a827d330 100644 --- a/packages/kit/src/exports/vite/build/utils.js +++ b/packages/kit/src/exports/vite/build/utils.js @@ -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); -} diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js index 3bb5f15a2863..86763581c435 100644 --- a/packages/kit/src/runtime/server/endpoint.js +++ b/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'; @@ -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; } diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index 25b22720fb4e..ec5831b7e4de 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -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) { @@ -34,9 +35,7 @@ export function method_not_allowed(mod, method) { /** @param {Partial>} 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'); diff --git a/packages/kit/src/utils/exports.js b/packages/kit/src/utils/exports.js index 52ce04b6ea54..8ec6ec0cb6d6 100644 --- a/packages/kit/src/utils/exports.js +++ b/packages/kit/src/utils/exports.js @@ -78,6 +78,7 @@ const valid_server_exports = new Set([ 'PUT', 'DELETE', 'OPTIONS', + 'HEAD', 'prerender', 'trailingSlash', 'config', diff --git a/packages/kit/src/utils/exports.spec.js b/packages/kit/src/utils/exports.spec.js index 79b803f83512..6c27e5d40b8c 100644 --- a/packages/kit/src/utils/exports.spec.js +++ b/packages/kit/src/utils/exports.spec.js @@ -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({ diff --git a/packages/kit/test/apps/basics/src/routes/endpoint-output/options/+server.js b/packages/kit/test/apps/basics/src/routes/endpoint-output/+server.js similarity index 100% rename from packages/kit/test/apps/basics/src/routes/endpoint-output/options/+server.js rename to packages/kit/test/apps/basics/src/routes/endpoint-output/+server.js diff --git a/packages/kit/test/apps/basics/src/routes/endpoint-output/head-handler/+page.svelte b/packages/kit/test/apps/basics/src/routes/endpoint-output/head-handler/+page.svelte new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/kit/test/apps/basics/src/routes/endpoint-output/head-handler/+server.js b/packages/kit/test/apps/basics/src/routes/endpoint-output/head-handler/+server.js new file mode 100644 index 000000000000..f3166fe93cd8 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/endpoint-output/head-handler/+server.js @@ -0,0 +1,8 @@ +/** @type {import('@sveltejs/kit').RequestHandler} */ +export function HEAD() { + return new Response('', { + headers: { + 'x-sveltekit-head-endpoint': 'true' + } + }); +} diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js index bec943f70486..87d0a5fe4071 100644 --- a/packages/kit/test/apps/basics/test/server.test.js +++ b/packages/kit/test/apps/basics/test/server.test.js @@ -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' @@ -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', () => {