Skip to content

Commit 85793ab

Browse files
elliott-with-the-longest-name-on-githubbenmccannteemingcRich-Harris
authored
breaking: turn redirect and error into commands. Export helpers for identifying them when caught (#11165)
* feat: Do the things * changeset * better * fix: types * feat: Replace `throw redirect` with `redirect` * feat: Replace `throw error` with `error` * fix: Revert changes to migrations * fix: Internals * Update .changeset/clean-cars-kiss.md Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * fix: lint * fix jsdoc overload function description * oops accidentally overwrote these while experimenting * feat: Do the things * changeset * better * fix: types * feat: Replace `throw redirect` with `redirect` * feat: Replace `throw error` with `error` * fix: Revert changes to migrations * fix: Internals * fix: lint * Update .changeset/clean-cars-kiss.md Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * feat: Document errors thrown by error and redirect, add fancy numeric constraints * fix: meh * fix: type * fix overloaded error method jsdocs * tweak docs * tweak implementation * update types --------- Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> Co-authored-by: Tee Ming <chewteeming01@gmail.com> Co-authored-by: Rich Harris <rich.harris@vercel.com>
1 parent d18f11e commit 85793ab

File tree

65 files changed

+206
-113
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+206
-113
lines changed

.changeset/clean-cars-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': major
3+
---
4+
5+
breaking: turn `error` and `redirect` into commands

documentation/docs/20-core-concepts/10-routing.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function load({ params }) {
6161
};
6262
}
6363

64-
throw error(404, 'Not found');
64+
error(404, 'Not found');
6565
}
6666
```
6767

@@ -104,7 +104,7 @@ export async function load({ params }) {
104104
return post;
105105
}
106106

107-
throw error(404, 'Not found');
107+
error(404, 'Not found');
108108
}
109109
```
110110

@@ -264,7 +264,7 @@ export function GET({ url }) {
264264
const d = max - min;
265265

266266
if (isNaN(d) || d < 0) {
267-
throw error(400, 'min and max must be numbers, and min must be less than max');
267+
error(400, 'min and max must be numbers, and min must be less than max');
268268
}
269269

270270
const random = min + Math.random() * d;
@@ -277,7 +277,7 @@ The first argument to `Response` can be a [`ReadableStream`](https://developer.m
277277
278278
You can use the [`error`](modules#sveltejs-kit-error), [`redirect`](modules#sveltejs-kit-redirect) and [`json`](modules#sveltejs-kit-json) methods from `@sveltejs/kit` for convenience (but you don't have to).
279279
280-
If an error is thrown (either `throw error(...)` or an unexpected error), the response will be a JSON representation of the error or a fallback error page — which can be customised via `src/error.html` — depending on the `Accept` header. The [`+error.svelte`](#error) component will _not_ be rendered in this case. You can read more about error handling [here](errors).
280+
If an error is thrown (either `error(...)` or an unexpected error), the response will be a JSON representation of the error or a fallback error page — which can be customised via `src/error.html` — depending on the `Accept` header. The [`+error.svelte`](#error) component will _not_ be rendered in this case. You can read more about error handling [here](errors).
281281
282282
> When creating an `OPTIONS` handler, note that Vite will inject `Access-Control-Allow-Origin` and `Access-Control-Allow-Methods` headers — these will not be present in production unless you add them.
283283

documentation/docs/20-core-concepts/20-load.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -395,11 +395,11 @@ import { error } from '@sveltejs/kit';
395395
/** @type {import('./$types').LayoutServerLoad} */
396396
export function load({ locals }) {
397397
if (!locals.user) {
398-
throw error(401, 'not logged in');
398+
error(401, 'not logged in');
399399
}
400400

401401
if (!locals.user.isAdmin) {
402-
throw error(403, 'not an admin');
402+
error(403, 'not an admin');
403403
}
404404
}
405405
```
@@ -428,12 +428,12 @@ import { redirect } from '@sveltejs/kit';
428428
/** @type {import('./$types').LayoutServerLoad} */
429429
export function load({ locals }) {
430430
if (!locals.user) {
431-
throw redirect(307, '/login');
431+
redirect(307, '/login');
432432
}
433433
}
434434
```
435435

436-
> Don't use `throw redirect()` from within a try-catch block, as the redirect will immediately trigger the catch statement.
436+
> Don't use `redirect()` inside a `try {...}` block, as the redirect will immediately trigger the catch statement.
437437
438438
In the browser, you can also navigate programmatically outside of a `load` function using [`goto`](modules#$app-navigation-goto) from [`$app.navigation`](modules#$app-navigation).
439439

documentation/docs/20-core-concepts/30-form-actions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ export const actions = {
234234
cookies.set('sessionid', await db.createSession(user));
235235

236236
+ if (url.searchParams.has('redirectTo')) {
237-
+ throw redirect(303, url.searchParams.get('redirectTo'));
237+
+ redirect(303, url.searchParams.get('redirectTo'));
238238
+ }
239239

240240
return { success: true };

documentation/docs/30-advanced/10-advanced-routing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ import { error } from '@sveltejs/kit';
5757

5858
/** @type {import('./$types').PageLoad} */
5959
export function load(event) {
60-
throw error(404, 'Not Found');
60+
error(404, 'Not Found');
6161
}
6262
```
6363

documentation/docs/30-advanced/25-errors.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export async function load({ params }) {
3131
const post = await db.getPost(params.slug);
3232

3333
if (!post) {
34-
throw error(404, {
34+
error(404, {
3535
message: 'Not found'
3636
});
3737
}
@@ -54,7 +54,7 @@ This tells SvelteKit to set the response status code to 404 and render an [`+err
5454
You can add extra properties to the error object if needed...
5555

5656
```diff
57-
throw error(404, {
57+
error(404, {
5858
message: 'Not found',
5959
+ code: 'NOT_FOUND'
6060
});
@@ -63,8 +63,8 @@ throw error(404, {
6363
...otherwise, for convenience, you can pass a string as the second argument:
6464

6565
```diff
66-
-throw error(404, { message: 'Not found' });
67-
+throw error(404, 'Not found');
66+
-error(404, { message: 'Not found' });
67+
+error(404, 'Not found');
6868
```
6969

7070
## Unexpected errors

packages/kit/src/exports/index.js

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,103 @@ import { get_route_segments } from '../utils/routing.js';
55
export { VERSION } from '../version.js';
66

77
/**
8+
* @template {number} TNumber
9+
* @template {any[]} [TArray=[]]
10+
* @typedef {TNumber extends TArray['length'] ? TArray[number] : LessThan<TNumber, [...TArray, TArray['length']]>} LessThan
11+
*/
12+
13+
/**
14+
* @template {number} TStart
15+
* @template {number} TEnd
16+
* @typedef {Exclude<TEnd | LessThan<TEnd>, LessThan<TStart>>} NumericRange
17+
*/
18+
19+
// we have to repeat the JSDoc because the display for function overloads is broken
20+
// see https://github.com/microsoft/TypeScript/issues/55056
21+
22+
/**
23+
* Throws an error with a HTTP status code and an optional message.
24+
* When called during request handling, this will cause SvelteKit to
25+
* return an error response without invoking `handleError`.
26+
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
27+
* @param {NumericRange<400, 599>} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
28+
* @param {App.Error} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
829
* @overload
9-
* @param {number} status
30+
* @param {NumericRange<400, 599>} status
1031
* @param {App.Error} body
11-
* @return {HttpError}
32+
* @return {never}
33+
* @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling.
34+
* @throws {Error} If the provided status is invalid (not between 400 and 599).
1235
*/
13-
1436
/**
37+
* Throws an error with a HTTP status code and an optional message.
38+
* When called during request handling, this will cause SvelteKit to
39+
* return an error response without invoking `handleError`.
40+
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
41+
* @param {NumericRange<400, 599>} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
42+
* @param {{ message: string } extends App.Error ? App.Error | string | undefined : never} [body] An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
1543
* @overload
16-
* @param {number} status
44+
* @param {NumericRange<400, 599>} status
1745
* @param {{ message: string } extends App.Error ? App.Error | string | undefined : never} [body]
18-
* @return {HttpError}
46+
* @return {never}
47+
* @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling.
48+
* @throws {Error} If the provided status is invalid (not between 400 and 599).
1949
*/
20-
2150
/**
22-
* Creates an `HttpError` object with an HTTP status code and an optional message.
23-
* This object, if thrown during request handling, will cause SvelteKit to
51+
* Throws an error with a HTTP status code and an optional message.
52+
* When called during request handling, this will cause SvelteKit to
2453
* return an error response without invoking `handleError`.
2554
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
26-
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
55+
* @param {NumericRange<400, 599>} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
2756
* @param {{ message: string } extends App.Error ? App.Error | string | undefined : never} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
57+
* @return {never}
58+
* @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling.
59+
* @throws {Error} If the provided status is invalid (not between 400 and 599).
2860
*/
2961
export function error(status, body) {
3062
if ((!BROWSER || DEV) && (isNaN(status) || status < 400 || status > 599)) {
3163
throw new Error(`HTTP error status codes must be between 400 and 599 — ${status} is invalid`);
3264
}
3365

34-
return new HttpError(status, body);
66+
throw new HttpError(status, body);
3567
}
3668

3769
/**
38-
* Create a `Redirect` object. If thrown during request handling, SvelteKit will return a redirect response.
70+
* Checks whether this is an error thrown by {@link error}.
71+
* @template {number} T
72+
* @param {unknown} e
73+
* @param {T} [status] The status to filter for.
74+
* @return {e is (HttpError & { status: T extends undefined ? never : T })}
75+
*/
76+
export function isHttpError(e, status) {
77+
if (!(e instanceof HttpError)) return false;
78+
return !status || e.status === status;
79+
}
80+
81+
/**
82+
* Redirect a request. When called during request handling, SvelteKit will return a redirect response.
3983
* Make sure you're not catching the thrown redirect, which would prevent SvelteKit from handling it.
40-
* @param {300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). Must be in the range 300-308.
84+
* @param {NumericRange<300, 308>} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). Must be in the range 300-308.
4185
* @param {string | URL} location The location to redirect to.
86+
* @throws {Redirect} This error instructs SvelteKit to redirect to the specified location.
87+
* @throws {Error} If the provided status is invalid.
88+
* @return {never}
4289
*/
4390
export function redirect(status, location) {
4491
if ((!BROWSER || DEV) && (isNaN(status) || status < 300 || status > 308)) {
4592
throw new Error('Invalid status code');
4693
}
4794

48-
return new Redirect(status, location.toString());
95+
throw new Redirect(status, location.toString());
96+
}
97+
98+
/**
99+
* Checks whether this is a redirect thrown by {@link redirect}.
100+
* @param {unknown} e The object to check.
101+
* @return {e is Redirect}
102+
*/
103+
export function isRedirect(e) {
104+
return e instanceof Redirect;
49105
}
50106

51107
/**

packages/kit/src/exports/node/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function get_raw_body(req, body_size_limit) {
2828
if (!length) {
2929
length = body_size_limit;
3030
} else if (length > body_size_limit) {
31-
throw error(
31+
error(
3232
413,
3333
`Received content-length of ${length}, but only accept up to ${body_size_limit} bytes.`
3434
);

packages/kit/src/runtime/server/page/actions.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as devalue from 'devalue';
2-
import { error, json } from '../../../exports/index.js';
2+
import { json } from '../../../exports/index.js';
33
import { normalize_error } from '../../../utils/error.js';
44
import { is_form_content_type, negotiate } from '../../../utils/http.js';
55
import { HttpError, Redirect, ActionFailure } from '../../control.js';
@@ -25,7 +25,10 @@ export async function handle_action_json_request(event, options, server) {
2525

2626
if (!actions) {
2727
// TODO should this be a different error altogether?
28-
const no_actions_error = error(405, 'POST method not allowed. No actions exist for this page');
28+
const no_actions_error = new HttpError(
29+
405,
30+
'POST method not allowed. No actions exist for this page'
31+
);
2932
return action_json(
3033
{
3134
type: 'error',
@@ -139,7 +142,7 @@ export async function handle_action_request(event, server) {
139142
});
140143
return {
141144
type: 'error',
142-
error: error(405, 'POST method not allowed. No actions exist for this page')
145+
error: new HttpError(405, 'POST method not allowed. No actions exist for this page')
143146
};
144147
}
145148

@@ -231,13 +234,11 @@ async function call_action(event, actions) {
231234
/** @param {any} data */
232235
function validate_action_return(data) {
233236
if (data instanceof Redirect) {
234-
throw new Error('Cannot `return redirect(...)` — use `throw redirect(...)` instead');
237+
throw new Error('Cannot `return redirect(...)` — use `redirect(...)` instead');
235238
}
236239

237240
if (data instanceof HttpError) {
238-
throw new Error(
239-
'Cannot `return error(...)` — use `throw error(...)` or `return fail(...)` instead'
240-
);
241+
throw new Error('Cannot `return error(...)` — use `error(...)` or `return fail(...)` instead');
241242
}
242243
}
243244

packages/kit/src/runtime/server/respond.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { exec } from '../../utils/routing.js';
1818
import { redirect_json_response, render_data } from './data/index.js';
1919
import { add_cookies_to_headers, get_cookies } from './cookie.js';
2020
import { create_fetch } from './fetch.js';
21-
import { Redirect } from '../control.js';
21+
import { HttpError, Redirect } from '../control.js';
2222
import {
2323
validate_layout_exports,
2424
validate_layout_server_exports,
@@ -27,7 +27,7 @@ import {
2727
validate_server_exports
2828
} from '../../utils/exports.js';
2929
import { get_option } from '../../utils/options.js';
30-
import { error, json, text } from '../../exports/index.js';
30+
import { json, text } from '../../exports/index.js';
3131
import { action_json_redirect, is_action_json_request } from './page/actions.js';
3232
import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM } from '../shared.js';
3333

@@ -67,7 +67,10 @@ export async function respond(request, options, manifest, state) {
6767
request.headers.get('origin') !== url.origin;
6868

6969
if (forbidden) {
70-
const csrf_error = error(403, `Cross-site ${request.method} form submissions are forbidden`);
70+
const csrf_error = new HttpError(
71+
403,
72+
`Cross-site ${request.method} form submissions are forbidden`
73+
);
7174
if (request.headers.get('accept') === 'application/json') {
7275
return json(csrf_error.body, { status: csrf_error.status });
7376
}

0 commit comments

Comments
 (0)