Skip to content
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

feat: add server endpoint fallback handler #9755

Merged
merged 20 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

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

feat: add catch-all method handler `all`
25 changes: 25 additions & 0 deletions documentation/docs/20-core-concepts/10-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,31 @@ export async function POST({ request }) {

> In general, [form actions](form-actions) are a better way to submit data from the browser to the server.

> If a `GET` handler is exported, a `HEAD` request will return the `content-length` of the `GET` handler's response body.

### Catch-all method

Exporting the `all` handler will match any unhandled request methods, including methods like `MOVE` which have no dedicated export from `+server.js`.

```js
// @errors: 7031
/// file: src/routes/api/add/+server.js
import { json, text } from '@sveltejs/kit';

export async function POST({ request }) {
const { a, b } = await request.json();
return json(a + b);
}

// This handler will respond to PUT, PATCH, DELETE, etc.
/** @type {import('./$types').RequestHandler} */
export async function all({ request }) {
return text(`I caught your ${request.method} request!`);
eltigerchino marked this conversation as resolved.
Show resolved Hide resolved
}
```

> The `all` handler will also override the implicit `HEAD` handler that is added when a `GET` handler is exported.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm. I'm not sure that's how we'd want this to behave?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's more consistent that way - all is a catch-all, no exceptions. Rich agrees: #9164 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also not sure about the name all 😆 I agree with Rich that if you call it all you might want it to handle HEAD, but I'm not sure we should call it all. Seems like @dominikg also agrees it should not handle HEAD: #9164 (comment)


### Content negotiation

`+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:
Expand Down
4 changes: 3 additions & 1 deletion packages/kit/src/core/postbuild/analyse.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ async function analyse({ manifest_path, env }) {
/** @type {Array<'GET' | 'POST'>} */
const page_methods = [];

/** @type {import('types').HttpMethod[]} */
/** @type {(import('types').HttpMethod | '*')[]} */
const api_methods = [];

/** @type {import('types').PrerenderOption | undefined} */
Expand All @@ -96,6 +96,8 @@ async function analyse({ manifest_path, env }) {
Object.values(mod).forEach((/** @type {import('types').HttpMethod} */ method) => {
if (mod[method] && ENDPOINT_METHODS.has(method)) {
api_methods.push(method);
} else if (mod.all) {
api_methods.push('*');
}
});

Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1059,15 +1059,15 @@ export interface ResolveOptions {
export interface RouteDefinition<Config = any> {
id: string;
api: {
methods: HttpMethod[];
methods: (HttpMethod | '*')[];
};
page: {
methods: Extract<HttpMethod, 'GET' | 'POST'>[];
};
pattern: RegExp;
prerender: PrerenderOption;
segments: RouteSegment[];
methods: HttpMethod[];
methods: (HttpMethod | '*')[];
config: Config;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/server/endpoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { method_not_allowed } from './utils.js';
export async function render_endpoint(event, mod, state) {
const method = /** @type {import('types').HttpMethod} */ (event.request.method);

let handler = mod[method];
let handler = mod[method] || mod.all;

if (!handler && method === 'HEAD') {
handler = mod.GET;
eltigerchino marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
5 changes: 3 additions & 2 deletions packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,12 @@ export interface ServerErrorNode {
export interface ServerMetadataRoute {
config: any;
api: {
methods: HttpMethod[];
methods: (HttpMethod | '*')[];
};
page: {
methods: Array<'GET' | 'POST'>;
};
methods: HttpMethod[];
methods: (HttpMethod | '*')[];
prerender: PrerenderOption | undefined;
entries: Array<string> | undefined;
}
Expand Down Expand Up @@ -367,6 +367,7 @@ export type SSREndpoint = Partial<Record<HttpMethod, RequestHandler>> & {
trailingSlash?: TrailingSlash;
config?: any;
entries?: PrerenderEntryGenerator;
all?: RequestHandler;
};

export interface SSRRoute {
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 @@ -79,6 +79,7 @@ const valid_server_exports = new Set([
'DELETE',
'OPTIONS',
'HEAD',
'all',
'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, HEAD, prerender, trailingSlash, config, entries, or anything with a '_' prefix)");
}, "Invalid export 'answer' (valid exports are GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD, all, 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,7 @@
export function GET() {
return new Response('ok');
}

export function all() {
return new Response('catch-all');
}
18 changes: 18 additions & 0 deletions packages/kit/test/apps/basics/test/server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,24 @@ test.describe('Endpoints', () => {
expect(await endpoint_response.text()).toBe('');
expect(endpoint_response.headers()['x-sveltekit-head-endpoint']).toBe('true');
});

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

let response = await request.fetch(url, {
method: 'GET'
});

expect(response.status()).toBe(200);
expect(await response.text()).toBe('ok');
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved

response = await request.fetch(url, {
method: 'OPTIONS'
});

expect(response.status()).toBe(200);
expect(await response.text()).toBe('catch-all');
eltigerchino marked this conversation as resolved.
Show resolved Hide resolved
});
});

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