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

breaking: turn redirect and error into commands. Export helpers for identifying them when caught #11165

Merged
merged 38 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
cfeade1
feat: Do the things
elliott-with-the-longest-name-on-github Dec 1, 2023
0f06fd1
changeset
elliott-with-the-longest-name-on-github Dec 1, 2023
20be7b5
better
elliott-with-the-longest-name-on-github Dec 1, 2023
3f5ae76
fix: types
elliott-with-the-longest-name-on-github Dec 1, 2023
d1920a2
feat: Replace `throw redirect` with `redirect`
elliott-with-the-longest-name-on-github Dec 1, 2023
1efdc9e
feat: Replace `throw error` with `error`
elliott-with-the-longest-name-on-github Dec 1, 2023
e12d34d
fix: Revert changes to migrations
elliott-with-the-longest-name-on-github Dec 2, 2023
017a494
fix: Internals
elliott-with-the-longest-name-on-github Dec 2, 2023
b07797f
Update .changeset/clean-cars-kiss.md
elliott-with-the-longest-name-on-github Dec 2, 2023
764e9e3
fix: lint
elliott-with-the-longest-name-on-github Dec 2, 2023
3df53ab
Merge branch 'elliott/kit-command-error-improvements' of github.com:s…
elliott-with-the-longest-name-on-github Dec 2, 2023
77ab3bb
Merge branch 'version-2' into elliott/kit-command-error-improvements
benmccann Dec 3, 2023
1588f50
Merge branch 'version-2' into elliott/kit-command-error-improvements
benmccann Dec 3, 2023
daf4d7a
fix jsdoc overload function description
eltigerchino Dec 4, 2023
0be2b5a
oops accidentally overwrote these while experimenting
eltigerchino Dec 4, 2023
3e6be33
Merge branch 'version-2' into elliott/kit-command-error-improvements
benmccann Dec 5, 2023
53ac369
Merge branch 'version-2' into elliott/kit-command-error-improvements
benmccann Dec 5, 2023
efffd3d
feat: Do the things
elliott-with-the-longest-name-on-github Dec 1, 2023
8e2a89e
changeset
elliott-with-the-longest-name-on-github Dec 1, 2023
ed210ba
better
elliott-with-the-longest-name-on-github Dec 1, 2023
e7d5295
fix: types
elliott-with-the-longest-name-on-github Dec 1, 2023
15dcd3e
feat: Replace `throw redirect` with `redirect`
elliott-with-the-longest-name-on-github Dec 1, 2023
4d1eb74
feat: Replace `throw error` with `error`
elliott-with-the-longest-name-on-github Dec 1, 2023
e534718
fix: Revert changes to migrations
elliott-with-the-longest-name-on-github Dec 2, 2023
862c8b0
fix: Internals
elliott-with-the-longest-name-on-github Dec 2, 2023
d0f5413
fix: lint
elliott-with-the-longest-name-on-github Dec 2, 2023
97b1828
Update .changeset/clean-cars-kiss.md
elliott-with-the-longest-name-on-github Dec 2, 2023
aa1c0a1
feat: Document errors thrown by error and redirect, add fancy numeric…
elliott-with-the-longest-name-on-github Dec 6, 2023
cf32083
Merge branch 'elliott/kit-command-error-improvements' of github.com:s…
elliott-with-the-longest-name-on-github Dec 6, 2023
e1bbc16
fix: meh
elliott-with-the-longest-name-on-github Dec 6, 2023
ea0747c
fix: type
elliott-with-the-longest-name-on-github Dec 6, 2023
b2b1b02
fix overloaded error method jsdocs
eltigerchino Dec 6, 2023
78763e3
merge version-2
Rich-Harris Dec 10, 2023
c93158e
tweak docs
Rich-Harris Dec 10, 2023
f84b2d4
tweak implementation
Rich-Harris Dec 10, 2023
dcf3c95
Merge branch 'version-2' into elliott/kit-command-error-improvements
Rich-Harris Dec 10, 2023
fcc4570
Merge branch 'version-2' into elliott/kit-command-error-improvements
Rich-Harris Dec 11, 2023
171d25c
update types
Rich-Harris Dec 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}.
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved
* @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}.
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved
* @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
Loading