Skip to content

Commit

Permalink
[breaking] error handling rework (#6586)
Browse files Browse the repository at this point in the history
* handlError returns App.PageData shape

* client hooks (simple, only error is passed), tests passing

* docs

* create-svelte

* note about message

* ffs

* cleanup my merge fuckup some more

* more fixes

* fix the wrong fix, FUCK THIS MERGE IS KILLING ME

* fix tests

* fixes

* pass sensible event data to handleError

* renames

* that was stupid

* fix doc links

* Update documentation/docs/05-load.md

Co-authored-by: Rich Harris <richard.a.harris@gmail.com>

* Update documentation/docs/07-hooks.md

Co-authored-by: Rich Harris <richard.a.harris@gmail.com>

* Update documentation/docs/07-hooks.md

Co-authored-by: Rich Harris <richard.a.harris@gmail.com>

* Update packages/kit/src/core/config/options.js

Co-authored-by: Rich Harris <richard.a.harris@gmail.com>

* simplify error_to_pojo as far as we can without disrupting existing tests

* rename ClientRequestEvent to NavigationEvent - still not perfect, but closer i think

* pass generic argument

* ok this seems to work

* restructure docs to avoid duplication

* lint

* argh

Co-authored-by: Rich Harris <richard.a.harris@gmail.com>
Co-authored-by: Rich Harris <hello@rich-harris.dev>
  • Loading branch information
3 people authored Sep 7, 2022
1 parent 01a6a22 commit 177a5a9
Show file tree
Hide file tree
Showing 60 changed files with 475 additions and 480 deletions.
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

0 comments on commit 177a5a9

Please sign in to comment.