Skip to content

Commit

Permalink
breaking: turn redirect and error into commands. Export helpers f…
Browse files Browse the repository at this point in the history
…or 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>
  • Loading branch information
4 people authored Dec 11, 2023
1 parent d18f11e commit 85793ab
Show file tree
Hide file tree
Showing 65 changed files with 206 additions and 113 deletions.
5 changes: 5 additions & 0 deletions .changeset/clean-cars-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': major
---

breaking: turn `error` and `redirect` into commands
8 changes: 4 additions & 4 deletions documentation/docs/20-core-concepts/10-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function load({ params }) {
};
}

throw error(404, 'Not found');
error(404, 'Not found');
}
```

Expand Down Expand Up @@ -104,7 +104,7 @@ export async function load({ params }) {
return post;
}

throw error(404, 'Not found');
error(404, 'Not found');
}
```

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

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

const random = min + Math.random() * d;
Expand All @@ -277,7 +277,7 @@ The first argument to `Response` can be a [`ReadableStream`](https://developer.m
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).
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).
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).
> 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.
Expand Down
8 changes: 4 additions & 4 deletions documentation/docs/20-core-concepts/20-load.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,11 +395,11 @@ import { error } from '@sveltejs/kit';
/** @type {import('./$types').LayoutServerLoad} */
export function load({ locals }) {
if (!locals.user) {
throw error(401, 'not logged in');
error(401, 'not logged in');
}

if (!locals.user.isAdmin) {
throw error(403, 'not an admin');
error(403, 'not an admin');
}
}
```
Expand Down Expand Up @@ -428,12 +428,12 @@ import { redirect } from '@sveltejs/kit';
/** @type {import('./$types').LayoutServerLoad} */
export function load({ locals }) {
if (!locals.user) {
throw redirect(307, '/login');
redirect(307, '/login');
}
}
```

> Don't use `throw redirect()` from within a try-catch block, as the redirect will immediately trigger the catch statement.
> Don't use `redirect()` inside a `try {...}` block, as the redirect will immediately trigger the catch statement.
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).

Expand Down
2 changes: 1 addition & 1 deletion documentation/docs/20-core-concepts/30-form-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export const actions = {
cookies.set('sessionid', await db.createSession(user));

+ if (url.searchParams.has('redirectTo')) {
+ throw redirect(303, url.searchParams.get('redirectTo'));
+ redirect(303, url.searchParams.get('redirectTo'));
+ }

return { success: true };
Expand Down
2 changes: 1 addition & 1 deletion documentation/docs/30-advanced/10-advanced-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import { error } from '@sveltejs/kit';

/** @type {import('./$types').PageLoad} */
export function load(event) {
throw error(404, 'Not Found');
error(404, 'Not Found');
}
```

Expand Down
8 changes: 4 additions & 4 deletions documentation/docs/30-advanced/25-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export async function load({ params }) {
const post = await db.getPost(params.slug);

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

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

```diff
-throw error(404, { message: 'Not found' });
+throw error(404, 'Not found');
-error(404, { message: 'Not found' });
+error(404, 'Not found');
```

## Unexpected errors
Expand Down
82 changes: 69 additions & 13 deletions packages/kit/src/exports/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,103 @@ import { get_route_segments } from '../utils/routing.js';
export { VERSION } from '../version.js';

/**
* @template {number} TNumber
* @template {any[]} [TArray=[]]
* @typedef {TNumber extends TArray['length'] ? TArray[number] : LessThan<TNumber, [...TArray, TArray['length']]>} LessThan
*/

/**
* @template {number} TStart
* @template {number} TEnd
* @typedef {Exclude<TEnd | LessThan<TEnd>, LessThan<TStart>>} NumericRange
*/

// we have to repeat the JSDoc because the display for function overloads is broken
// see https://github.com/microsoft/TypeScript/issues/55056

/**
* Throws an error with a HTTP status code and an optional message.
* When called during request handling, this will cause SvelteKit to
* return an error response without invoking `handleError`.
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
* @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.
* @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.
* @overload
* @param {number} status
* @param {NumericRange<400, 599>} status
* @param {App.Error} body
* @return {HttpError}
* @return {never}
* @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling.
* @throws {Error} If the provided status is invalid (not between 400 and 599).
*/

/**
* Throws an error with a HTTP status code and an optional message.
* When called during request handling, this will cause SvelteKit to
* return an error response without invoking `handleError`.
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
* @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.
* @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.
* @overload
* @param {number} status
* @param {NumericRange<400, 599>} status
* @param {{ message: string } extends App.Error ? App.Error | string | undefined : never} [body]
* @return {HttpError}
* @return {never}
* @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling.
* @throws {Error} If the provided status is invalid (not between 400 and 599).
*/

/**
* Creates an `HttpError` object with an HTTP status code and an optional message.
* This object, if thrown during request handling, will cause SvelteKit to
* Throws an error with a HTTP status code and an optional message.
* When called during request handling, this will cause SvelteKit to
* return an error response without invoking `handleError`.
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
* @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.
* @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.
* @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.
* @return {never}
* @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling.
* @throws {Error} If the provided status is invalid (not between 400 and 599).
*/
export function error(status, body) {
if ((!BROWSER || DEV) && (isNaN(status) || status < 400 || status > 599)) {
throw new Error(`HTTP error status codes must be between 400 and 599 — ${status} is invalid`);
}

return new HttpError(status, body);
throw new HttpError(status, body);
}

/**
* Create a `Redirect` object. If thrown during request handling, SvelteKit will return a redirect response.
* Checks whether this is an error thrown by {@link error}.
* @template {number} T
* @param {unknown} e
* @param {T} [status] The status to filter for.
* @return {e is (HttpError & { status: T extends undefined ? never : T })}
*/
export function isHttpError(e, status) {
if (!(e instanceof HttpError)) return false;
return !status || e.status === status;
}

/**
* Redirect a request. When called during request handling, SvelteKit will return a redirect response.
* Make sure you're not catching the thrown redirect, which would prevent SvelteKit from handling it.
* @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.
* @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.
* @param {string | URL} location The location to redirect to.
* @throws {Redirect} This error instructs SvelteKit to redirect to the specified location.
* @throws {Error} If the provided status is invalid.
* @return {never}
*/
export function redirect(status, location) {
if ((!BROWSER || DEV) && (isNaN(status) || status < 300 || status > 308)) {
throw new Error('Invalid status code');
}

return new Redirect(status, location.toString());
throw new Redirect(status, location.toString());
}

/**
* Checks whether this is a redirect thrown by {@link redirect}.
* @param {unknown} e The object to check.
* @return {e is Redirect}
*/
export function isRedirect(e) {
return e instanceof Redirect;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/exports/node/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function get_raw_body(req, body_size_limit) {
if (!length) {
length = body_size_limit;
} else if (length > body_size_limit) {
throw error(
error(
413,
`Received content-length of ${length}, but only accept up to ${body_size_limit} bytes.`
);
Expand Down
15 changes: 8 additions & 7 deletions packages/kit/src/runtime/server/page/actions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as devalue from 'devalue';
import { error, json } from '../../../exports/index.js';
import { json } from '../../../exports/index.js';
import { normalize_error } from '../../../utils/error.js';
import { is_form_content_type, negotiate } from '../../../utils/http.js';
import { HttpError, Redirect, ActionFailure } from '../../control.js';
Expand All @@ -25,7 +25,10 @@ export async function handle_action_json_request(event, options, server) {

if (!actions) {
// TODO should this be a different error altogether?
const no_actions_error = error(405, 'POST method not allowed. No actions exist for this page');
const no_actions_error = new HttpError(
405,
'POST method not allowed. No actions exist for this page'
);
return action_json(
{
type: 'error',
Expand Down Expand Up @@ -139,7 +142,7 @@ export async function handle_action_request(event, server) {
});
return {
type: 'error',
error: error(405, 'POST method not allowed. No actions exist for this page')
error: new HttpError(405, 'POST method not allowed. No actions exist for this page')
};
}

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

if (data instanceof HttpError) {
throw new Error(
'Cannot `return error(...)` — use `throw error(...)` or `return fail(...)` instead'
);
throw new Error('Cannot `return error(...)` — use `error(...)` or `return fail(...)` instead');
}
}

Expand Down
9 changes: 6 additions & 3 deletions packages/kit/src/runtime/server/respond.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { exec } from '../../utils/routing.js';
import { redirect_json_response, render_data } from './data/index.js';
import { add_cookies_to_headers, get_cookies } from './cookie.js';
import { create_fetch } from './fetch.js';
import { Redirect } from '../control.js';
import { HttpError, Redirect } from '../control.js';
import {
validate_layout_exports,
validate_layout_server_exports,
Expand All @@ -27,7 +27,7 @@ import {
validate_server_exports
} from '../../utils/exports.js';
import { get_option } from '../../utils/options.js';
import { error, json, text } from '../../exports/index.js';
import { json, text } from '../../exports/index.js';
import { action_json_redirect, is_action_json_request } from './page/actions.js';
import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM } from '../shared.js';

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

if (forbidden) {
const csrf_error = error(403, `Cross-site ${request.method} form submissions are forbidden`);
const csrf_error = new HttpError(
403,
`Cross-site ${request.method} form submissions are forbidden`
);
if (request.headers.get('accept') === 'application/json') {
return json(csrf_error.body, { status: csrf_error.status });
}
Expand Down
8 changes: 4 additions & 4 deletions packages/kit/test/apps/basics/src/hooks.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const handle = sequence(
if (event.url.pathname === '/errors/error-in-handle') {
throw new Error('Error in handle');
} else if (event.url.pathname === '/errors/expected-error-in-handle') {
throw error(500, 'Expected error in handle');
error(500, 'Expected error in handle');
}

const response = await resolve(event, {
Expand All @@ -108,10 +108,10 @@ export const handle = sequence(
async ({ event, resolve }) => {
if (event.url.pathname.includes('/redirect/in-handle')) {
if (event.url.search === '?throw') {
throw redirect(307, event.url.origin + '/redirect/c');
redirect(307, event.url.origin + '/redirect/c');
} else if (event.url.search.includes('cookies')) {
event.cookies.delete(COOKIE_NAME, { path: '/cookies' });
throw redirect(307, event.url.origin + '/cookies');
redirect(307, event.url.origin + '/cookies');
} else {
return new Response(undefined, { status: 307, headers: { location: '/redirect/c' } });
}
Expand All @@ -128,7 +128,7 @@ export const handle = sequence(
},
async ({ event, resolve }) => {
if (event.url.pathname === '/actions/redirect-in-handle' && event.request.method === 'POST') {
throw redirect(303, '/actions/enhance');
redirect(303, '/actions/enhance');
}

return resolve(event);
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/test/apps/basics/src/routes/+layout.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ export async function load({ cookies, locals, fetch }) {
if (should_fail) {
cookies.delete('fail-type', { path: '/' });
if (should_fail === 'expected') {
throw error(401, 'Not allowed');
error(401, 'Not allowed');
} else if (should_fail === 'unexpected') {
throw new Error('Failed to load');
} else {
throw redirect(307, '/load');
redirect(307, '/load');
}
}
// Do NOT make this load function depend on something which would cause it to rerun
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const actions = {
};
},
error: () => {
throw error(400, 'error');
error(400, 'error');
},
echo: async ({ request }) => {
const data = await request.formData();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import { error } from '@sveltejs/kit';

export const actions = {
default: async () => {
throw error(502, 'something went wrong');
error(502, 'something went wrong');
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ export function load() {
/** @type {import('./$types').Actions} */
export const actions = {
default: async () => {
throw redirect(303, '/actions/enhance');
redirect(303, '/actions/enhance');
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import { COOKIE_NAME } from '../shared';
/** @type {import('@sveltejs/kit').RequestHandler} */
export const GET = (event) => {
event.cookies.delete(COOKIE_NAME, { path: '/cookies' });
throw redirect(303, '/cookies');
redirect(303, '/cookies');
};
Loading

0 comments on commit 85793ab

Please sign in to comment.