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] error handling rework #6586

Merged
merged 29 commits into from
Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a039c7d
handlError returns App.PageData shape
dummdidumm Sep 5, 2022
869c0b1
client hooks (simple, only error is passed), tests passing
dummdidumm Sep 6, 2022
79157cc
docs
dummdidumm Sep 6, 2022
a0a8e00
create-svelte
dummdidumm Sep 6, 2022
9ae1232
note about message
dummdidumm Sep 6, 2022
240f9ac
painful merge
dummdidumm Sep 6, 2022
c5f1c14
ffs
dummdidumm Sep 6, 2022
c1da398
cleanup my merge fuckup some more
dummdidumm Sep 6, 2022
b7dc502
more fixes
dummdidumm Sep 6, 2022
6d87f07
fix the wrong fix, FUCK THIS MERGE IS KILLING ME
dummdidumm Sep 6, 2022
34651b5
fix tests
dummdidumm Sep 6, 2022
7416126
fixes
dummdidumm Sep 6, 2022
3b5368a
pass sensible event data to handleError
dummdidumm Sep 6, 2022
50cda05
renames
dummdidumm Sep 6, 2022
39092dc
that was stupid
dummdidumm Sep 6, 2022
4f24fc0
fix doc links
dummdidumm Sep 6, 2022
9568053
Update documentation/docs/05-load.md
dummdidumm Sep 7, 2022
d2a284e
Update documentation/docs/07-hooks.md
dummdidumm Sep 7, 2022
c4c635c
Update documentation/docs/07-hooks.md
dummdidumm Sep 7, 2022
a9522ab
Update packages/kit/src/core/config/options.js
dummdidumm Sep 7, 2022
f5a4ac9
Merge branch 'master' into error-rework
Rich-Harris Sep 7, 2022
ee7c515
simplify error_to_pojo as far as we can without disrupting existing t…
Rich-Harris Sep 7, 2022
0d94008
rename ClientRequestEvent to NavigationEvent - still not perfect, but…
Rich-Harris Sep 7, 2022
fe74aea
pass generic argument
Rich-Harris Sep 7, 2022
7dc9456
ok this seems to work
Rich-Harris Sep 7, 2022
d6d1e36
restructure docs to avoid duplication
Rich-Harris Sep 7, 2022
4826b1b
merge master
Rich-Harris Sep 7, 2022
7429460
lint
Rich-Harris Sep 7, 2022
4687d14
argh
Rich-Harris Sep 7, 2022
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
4 changes: 2 additions & 2 deletions documentation/docs/05-load.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export async function load({ depends }) {
- it can be used to make credentialed requests on the server, as it inherits the `cookie` and `authorization` headers for the page request
- it can make relative requests on the server (ordinarily, `fetch` requires a URL with an origin when used in a server context)
- internal requests (e.g. for `+server.js` routes) go direct to the handler function when running on the server, without the overhead of an HTTP call
- during server-side rendering, the response will be captured and inlined into the rendered HTML. Note that headers will _not_ be serialized, unless explicitly included via [`filterSerializedResponseHeaders`](/docs/hooks#handle)
- during server-side rendering, the response will be captured and inlined into the rendered HTML. Note that headers will _not_ be serialized, unless explicitly included via [`filterSerializedResponseHeaders`](/docs/hooks#hooks-server-js-handle)
- during hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request

> Cookies will only be passed through if the target host is the same as the SvelteKit application or a more specific subdomain of it.
Expand Down Expand Up @@ -308,7 +308,7 @@ export function load({ locals }) {
}
```

If an _unexpected_ error is thrown, SvelteKit will invoke [`handleError`](/docs/hooks#handleerror) and treat it as a 500 Internal Server Error.
If an _unexpected_ error is thrown, SvelteKit will invoke [`handleError`](/docs/hooks#hooks-server-js-handleerror) and treat it as a 500 Internal Error.

> In development, stack traces for unexpected errors are visible as `$page.error.stack`. In production, stack traces are hidden.

Expand Down
96 changes: 52 additions & 44 deletions documentation/docs/07-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@
title: Hooks
---

An optional `src/hooks.js` (or `src/hooks.ts`, or `src/hooks/index.js`) file exports three functions, all optional, that run on the server — `handle`, `handleError` and `handleFetch`.
'Hooks' are app-wide functions you declare that SvelteKit will call in response to specific events, giving you fine-grained control over the framework's behaviour.

> The location of this file can be [configured](/docs/configuration) as `config.kit.files.hooks`
There are two hooks files, both optional:

### handle
- `src/hooks.server.js` — your app's server hooks
- `src/hooks.client.js` — your app's client hooks

> You can configure the location of these files with [`config.kit.files.hooks`](/docs/configuration#files).

### Server hooks

The following hooks can be added to `src/hooks.server.js`:

#### handle

This function runs every time the SvelteKit server receives a [request](/docs/web-standards#fetch-apis-request) — whether that happens while the app is running, or during [prerendering](/docs/page-options#prerender) — and determines the [response](/docs/web-standards#fetch-apis-response). It receives an `event` object representing the request and a function called `resolve`, which renders the route and generates a `Response`. This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing routes programmatically, for example).

Expand Down Expand Up @@ -79,47 +88,9 @@ export async function handle({ event, resolve }) {
}
```

### handleError

If an error is thrown during loading or rendering, this function will be called with the `error` and the `event` that caused it. This allows you to send data to an error tracking service, or to customise the formatting before printing the error to the console.

During development, if an error occurs because of a syntax error in your Svelte code, a `frame` property will be appended highlighting the location of the error.

If unimplemented, SvelteKit will log the error with default formatting.
#### handleFetch

```js
/// file: src/hooks.js
// @filename: ambient.d.ts
const Sentry: any;

// @filename: index.js
// ---cut---
/** @type {import('@sveltejs/kit').HandleError} */
export function handleError({ error, event }) {
// example integration with https://sentry.io/
Sentry.captureException(error, { event });
}
```

> `handleError` is only called for _unexpected_ errors. It is not called for errors created with the [`error`](/docs/modules#sveltejs-kit-error) function imported from `@sveltejs/kit`, as these are _expected_ errors.

### handleFetch

This function allows you to modify (or replace) a `fetch` request that happens inside a `load` function that runs on the server (or during pre-rendering).

For example, you might need to include custom headers that are added by a proxy that sits in front of your app:

```js
// @errors: 2345
/** @type {import('@sveltejs/kit').HandleFetch} */
export async function handleFetch({ event, request, fetch }) {
const name = 'x-geolocation-city';
const value = event.request.headers.get(name);
request.headers.set(name, value);

return fetch(request);
}
```
This function allows you to modify (or replace) a `fetch` request for an external resource that happens inside a `load` function that runs on the server (or during pre-rendering).

Or your `load` function might make a request to a public URL like `https://api.yourapp.com` when the user performs a client-side navigation to the respective page, but during SSR it might make sense to hit the API directly (bypassing whatever proxies and load balancers sit between it and the public internet).

Expand All @@ -138,7 +109,7 @@ export async function handleFetch({ request, fetch }) {
}
```

#### Credentials
**Credentials**

For same-origin requests, SvelteKit's `fetch` implementation will forward `cookie` and `authorization` headers unless the `credentials` option is set to `"omit"`.

Expand All @@ -157,3 +128,40 @@ export async function handleFetch({ event, request, fetch }) {
return fetch(request);
}
```

### Shared hooks

The following can be added to `src/hooks.server.js` _and_ `src/hooks.client.js`:

#### handleError

If an unexpected error is thrown during loading or rendering, this function will be called with the `error` and the `event`. This allows for two things:

- you can log the error
- you can generate a custom representation of the error that is safe to show to users, omitting sensitive details like messages and stack traces. The returned value, which defaults to `{ message: 'Internal Error' }`, becomes the value of `$page.error`. To make this type-safe, you can customize the expected shape by declaring an `App.PageError` interface (which must include `message: string`, to guarantee sensible fallback behavior).

```js
/// file: src/hooks.server.js
// @errors: 2322 2571
// @filename: ambient.d.ts
const Sentry: any;

// @filename: index.js
// ---cut---
/** @type {import('@sveltejs/kit').HandleServerError} */
export function handleError({ error, event }) {
// example integration with https://sentry.io/
Sentry.captureException(error, { event });

return {
message: 'Whoops!',
code: error.code ?? 'UNKNOWN'
};
}
```

> In `src/hooks.client.js`, the type of `handleError` is `HandleClientError` instead of `HandleServerError`, and `event` is a `NavigationEvent` rather than a `RequestEvent`.

This function is not called for _expected_ errors (those thrown with the [`error`](/docs/modules#sveltejs-kit-error) function imported from `@sveltejs/kit`).

During development, if an error occurs because of a syntax error in your Svelte code, a `frame` property will be appended highlighting the location of the error.
9 changes: 6 additions & 3 deletions documentation/docs/15-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ const config = {
},
files: {
assets: 'static',
hooks: 'src/hooks',
hooks: {
client: 'src/hooks.client',
server: 'src/hooks.server'
},
lib: 'src/lib',
params: 'src/params',
routes: 'src/routes',
Expand Down Expand Up @@ -179,7 +182,7 @@ Environment variable configuration:
An object containing zero or more of the following `string` values:

- `assets` — a place to put static files that should have stable URLs and undergo no processing, such as `favicon.ico` or `manifest.json`
- `hooks` — the location of your hooks module (see [Hooks](/docs/hooks))
- `hooks` — the location of your client and server hooks (see [Hooks](/docs/hooks))
- `lib` — your app's internal library, accessible throughout the codebase as `$lib`
- `params` — a directory containing [parameter matchers](/docs/routing#advanced-routing-matching)
- `routes` — the files that define the structure of your app (see [Routing](/docs/routing))
Expand Down Expand Up @@ -296,7 +299,7 @@ Whether to remove, append, or ignore trailing slashes when resolving URLs (note

This option also affects [prerendering](/docs/page-options#prerender). If `trailingSlash` is `always`, a route like `/about` will result in an `about/index.html` file, otherwise it will create `about.html`, mirroring static webserver conventions.

> Ignoring trailing slashes is not recommended — the semantics of relative paths differ between the two cases (`./y` from `/x` is `/y`, but from `/x/` is `/x/y`), and `/x` and `/x/` are treated as separate URLs which is harmful to SEO. If you use this option, ensure that you implement logic for conditionally adding or removing trailing slashes from `request.path` inside your [`handle`](/docs/hooks#handle) function.
> Ignoring trailing slashes is not recommended — the semantics of relative paths differ between the two cases (`./y` from `/x` is `/y`, but from `/x/` is `/x/y`), and `/x` and `/x/` are treated as separate URLs which is harmful to SEO. If you use this option, ensure that you implement logic for conditionally adding or removing trailing slashes from `request.path` inside your [`handle`](/docs/hooks#hooks-server-js-handle) function.

### version

Expand Down
2 changes: 1 addition & 1 deletion documentation/docs/17-seo.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The most important aspect of SEO is to create high-quality content that is widel

#### SSR

While search engines have got better in recent years at indexing content that was rendered with client-side JavaScript, server-side rendered content is indexed more frequently and reliably. SvelteKit employs SSR by default, and while you can disable it in [`handle`](/docs/hooks#handle), you should leave it on unless you have a good reason not to.
While search engines have got better in recent years at indexing content that was rendered with client-side JavaScript, server-side rendered content is indexed more frequently and reliably. SvelteKit employs SSR by default, and while you can disable it in [`handle`](/docs/hooks#hooks-server-js-handle), you should leave it on unless you have a good reason not to.

> SvelteKit's rendering is highly configurable and you can implement [dynamic rendering](https://developers.google.com/search/docs/advanced/javascript/dynamic-rendering) if necessary. It's not generally recommended, since SSR has other benefits beyond SEO.

Expand Down
2 changes: 1 addition & 1 deletion documentation/docs/19-accessibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ By default, SvelteKit's page template sets the default language of the document
<html lang="de">
```

If your content is available in multiple languages, you should set the `lang` attribute based on the language of the current page. You can do this with SvelteKit's [handle hook](/docs/hooks#handle):
If your content is available in multiple languages, you should set the `lang` attribute based on the language of the current page. You can do this with SvelteKit's [handle hook](/docs/hooks#hooks-server-js-handle):

```html
/// file: src/app.html
Expand Down
2 changes: 1 addition & 1 deletion documentation/docs/80-migrating.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ See [the FAQ](/faq#integrations) for detailed information about integrations.

#### HTML minifier

Sapper includes `html-minifier` by default. SvelteKit does not include this, but it can be added as a [hook](/docs/hooks#handle):
Sapper includes `html-minifier` by default. SvelteKit does not include this, but it can be added as a [hook](/docs/hooks#hooks-server-js-handle):

```js
// @filename: ambient.d.ts
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-cloudflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Functions contained in the `/functions` directory at the project's root will _no

The [`_headers` and `_redirects`](config files) files specific to Cloudflare Pages can be used for static asset responses (like images) by putting them into the `/static` folder.

However, they will have no effect on responses dynamically rendered by SvelteKit, which should return custom headers or redirect responses from [endpoints](https://kit.svelte.dev/docs/routing#endpoints) or with the [`handle`](https://kit.svelte.dev/docs/hooks#handle) hook.
However, they will have no effect on responses dynamically rendered by SvelteKit, which should return custom headers or redirect responses from [endpoints](https://kit.svelte.dev/docs/routing#endpoints) or with the [`handle`](https://kit.svelte.dev/docs/hooks#hooks-server-js-handle) hook.

## Changelog

Expand Down
2 changes: 2 additions & 0 deletions packages/create-svelte/templates/default/src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ declare namespace App {

// interface PageData {}

// interface PageError {}

// interface Platform {}
}
1 change: 1 addition & 0 deletions packages/create-svelte/templates/skeleton/src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
declare namespace App {
// interface Locals {}
// interface PageData {}
// interface PageError {}
// interface Platform {}
}
1 change: 1 addition & 0 deletions packages/create-svelte/templates/skeletonlib/src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
declare namespace App {
// interface Locals {}
// interface PageData {}
// interface PageError {}
// interface Platform {}
}
9 changes: 7 additions & 2 deletions packages/kit/src/core/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,13 @@ function process_config(config, { cwd = process.cwd() } = {}) {
// TODO remove for 1.0
if (key === 'template') continue;

// @ts-expect-error this is typescript at its stupidest
validated.kit.files[key] = path.resolve(cwd, validated.kit.files[key]);
if (key === 'hooks') {
validated.kit.files.hooks.client = path.resolve(cwd, validated.kit.files.hooks.client);
validated.kit.files.hooks.server = path.resolve(cwd, validated.kit.files.hooks.server);
} else {
// @ts-expect-error
validated.kit.files[key] = path.resolve(cwd, validated.kit.files[key]);
}
}

if (!fs.existsSync(validated.kit.files.errorTemplate)) {
Expand Down
5 changes: 4 additions & 1 deletion packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ const get_defaults = (prefix = '') => ({
},
files: {
assets: join(prefix, 'static'),
hooks: join(prefix, 'src/hooks'),
hooks: {
client: join(prefix, 'src/hooks.client'),
server: join(prefix, 'src/hooks.server')
},
lib: join(prefix, 'src/lib'),
params: join(prefix, 'src/params'),
routes: join(prefix, 'src/routes'),
Expand Down
14 changes: 13 additions & 1 deletion packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,19 @@ const options = object(

files: object({
assets: string('static'),
hooks: string(join('src', 'hooks')),
hooks: (input, keypath) => {
// TODO remove this for the 1.0 release
if (typeof input === 'string') {
throw new Error(
`${keypath} is an object with { server: string, client: string } now. See the PR for more information: https://github.com/sveltejs/kit/pull/6586`
);
}

return object({
client: string(join('src', 'hooks.client')),
server: string(join('src', 'hooks.server'))
})(input, keypath);
},
lib: string(join('src', 'lib')),
params: string(join('src', 'params')),
routes: string(join('src', 'routes')),
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/core/sync/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export async function create(config) {

const output = path.join(config.kit.outDir, 'generated');

write_client_manifest(manifest_data, output);
write_client_manifest(config, manifest_data, output);
write_root(manifest_data, output);
write_matchers(manifest_data, output);
await write_all_types(config, manifest_data);
Expand Down
14 changes: 13 additions & 1 deletion packages/kit/src/core/sync/write_client_manifest.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { relative } from 'path';
import { posixify, resolve_entry } from '../../utils/filesystem.js';
import { s } from '../../utils/misc.js';
import { trim, write_if_changed } from './utils.js';

/**
* Writes the client manifest to disk. The manifest is used to power the router. It contains the
* list of routes and corresponding Svelte components (i.e. pages and layouts).
* @param {import('types').ValidatedConfig} config
* @param {import('types').ManifestData} manifest_data
* @param {string} output
*/
export function write_client_manifest(manifest_data, output) {
export function write_client_manifest(config, manifest_data, output) {
/**
* Creates a module that exports a `CSRPageNode`
* @param {import('types').PageNode} node
Expand Down Expand Up @@ -78,17 +80,27 @@ export function write_client_manifest(manifest_data, output) {
.join(',\n\t\t')}
}`.replace(/^\t/gm, '');

const hooks_file = resolve_entry(config.kit.files.hooks.client);

// String representation of __GENERATED__/client-manifest.js
write_if_changed(
`${output}/client-manifest.js`,
trim(`
${hooks_file ? `import * as client_hooks from '${posixify(relative(output, hooks_file))}';` : ''}

export { matchers } from './client-matchers.js';

export const nodes = [${nodes}];

export const server_loads = [${[...layouts_with_server_load].join(',')}];

export const dictionary = ${dictionary};

export const hooks = {
handleError: ${
hooks_file ? 'client_hooks.handleError || ' : ''
}(({ error }) => { console.error(error); return { message: 'Internal Error' }; }),
};
`)
);
}
21 changes: 6 additions & 15 deletions packages/kit/src/exports/index.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import { HttpError, Redirect, ValidationError } from '../runtime/control.js';

// For some reason we need to type the params as well here,
// JSdoc doesn't seem to like @type with function overloads
/**
* Creates an `HttpError` object with an HTTP status code and an optional message.
* This object, if thrown during request handling, will cause SvelteKit to
* return an error response without invoking `handleError`
* @type {import('@sveltejs/kit').error}
* @param {number} status
* @param {string | undefined} [message]
* @param {any} message
*/
export function error(status, message) {
return new HttpError(status, message);
}

/**
* Creates a `Redirect` object. If thrown during request handling, SvelteKit will
* return a redirect response.
* @param {number} status
* @param {string} location
*/
/** @type {import('@sveltejs/kit').redirect} */
export function redirect(status, location) {
if (isNaN(status) || status < 300 || status > 399) {
throw new Error('Invalid status code');
Expand All @@ -25,11 +20,7 @@ export function redirect(status, location) {
return new Redirect(status, location);
}

/**
* Generates a JSON `Response` object from the supplied data.
* @param {any} data
* @param {ResponseInit} [init]
*/
/** @type {import('@sveltejs/kit').json} */
export function json(data, init) {
// TODO deprecate this in favour of `Response.json` when it's
// more widely supported
Expand Down
Loading