Skip to content

Commit 7718d0f

Browse files
dummdidummRich-Harrisbenmccann
authored
breaking: tighten up error handling (#11289)
* breaking: tighten up error handling Introduces a NonFatalError object that is used internally and that is user-detectable in handleError * how did this happen * fix tests * lint * types * adjust wording (is this even a breaking change now?) * adjust * pass status and message to handleError * Apply suggestions from code review Co-authored-by: Rich Harris <richard.a.harris@gmail.com> * lint * Update documentation/docs/30-advanced/20-hooks.md Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * lint * simplify example * tweak docs * Update documentation/docs/30-advanced/20-hooks.md * various tweaks. we can be less duplicative i think * tweak * tweak * handle too large body after streaming has started * cancel stream from the inside if content-length exceeds limit * remove unnecessary try-catch, bump adapter-node/adapter-vercel majors * migration docs * tweak/fix tests * fix more * more --------- Co-authored-by: Rich Harris <rich.harris@vercel.com> Co-authored-by: Rich Harris <richard.a.harris@gmail.com> Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
1 parent 57be35a commit 7718d0f

File tree

31 files changed

+246
-200
lines changed

31 files changed

+246
-200
lines changed

.changeset/fast-dolls-clean.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@sveltejs/adapter-vercel': major
3+
'@sveltejs/adapter-node': major
4+
---
5+
6+
breaking: require SvelteKit 2 peer dependency

.changeset/real-pets-fix.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: tighten up error handling

documentation/docs/30-advanced/20-hooks.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,14 @@ The following can be added to `src/hooks.server.js` _and_ `src/hooks.client.js`:
139139

140140
### handleError
141141

142-
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:
142+
If an [unexpected error](/docs/errors#unexpected-errors) is thrown during loading or rendering, this function will be called with the `error`, `event`, `status` code and `message`. This allows for two things:
143143

144144
- you can log the error
145-
- 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 becomes the value of `$page.error`. It defaults to `{ message: 'Not Found' }` in case of a 404 (you can detect them through `event.route.id` being `null`) and to `{ message: 'Internal Error' }` for everything else. To make this type-safe, you can customize the expected shape by declaring an `App.Error` interface (which must include `message: string`, to guarantee sensible fallback behavior).
145+
- 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 }`, becomes the value of `$page.error`.
146146

147-
The following code shows an example of typing the error shape as `{ message: string; errorId: string }` and returning it accordingly from the `handleError` functions:
147+
For errors thrown from your code (or library code called by your code) the status will be 500 and the message will be "Internal Error". While `error.message` may contain sensitive information that should not be exposed to users, `message` is safe (albeit meaningless to the average user).
148+
149+
To add more information to the `$page.error` object in a type-safe way, you can customize the expected shape by declaring an `App.Error` interface (which must include `message: string`, to guarantee sensible fallback behavior). This allows you to — for example — append a tracking ID for users to quote in correspondence with your technical support staff:
148150

149151
```ts
150152
/// file: src/app.d.ts
@@ -172,15 +174,17 @@ declare module '@sentry/sveltekit' {
172174
// @filename: index.js
173175
// ---cut---
174176
import * as Sentry from '@sentry/sveltekit';
175-
import crypto from 'crypto';
176177

177178
Sentry.init({/*...*/})
178179

179180
/** @type {import('@sveltejs/kit').HandleServerError} */
180-
export async function handleError({ error, event }) {
181+
export async function handleError({ error, event, status, message }) {
181182
const errorId = crypto.randomUUID();
183+
182184
// example integration with https://sentry.io/
183-
Sentry.captureException(error, { extra: { event, errorId } });
185+
Sentry.captureException(error, {
186+
extra: { event, errorId, status }
187+
});
184188

185189
return {
186190
message: 'Whoops!',
@@ -205,10 +209,13 @@ import * as Sentry from '@sentry/sveltekit';
205209
Sentry.init({/*...*/})
206210

207211
/** @type {import('@sveltejs/kit').HandleClientError} */
208-
export async function handleError({ error, event }) {
212+
export async function handleError({ error, event, status, message }) {
209213
const errorId = crypto.randomUUID();
214+
210215
// example integration with https://sentry.io/
211-
Sentry.captureException(error, { extra: { event, errorId } });
216+
Sentry.captureException(error, {
217+
extra: { event, errorId, status }
218+
});
212219

213220
return {
214221
message: 'Whoops!',

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

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -77,36 +77,7 @@ By default, unexpected errors are printed to the console (or, in production, you
7777
{ "message": "Internal Error" }
7878
```
7979

80-
Unexpected errors will go through the [`handleError`](hooks#shared-hooks-handleerror) hook, where you can add your own error handling — for example, sending errors to a reporting service, or returning a custom error object.
81-
82-
```js
83-
/// file: src/hooks.server.js
84-
// @errors: 2322 1360 2571 2339 2353
85-
// @filename: ambient.d.ts
86-
declare module '@sentry/sveltekit' {
87-
export const init: (opts: any) => void;
88-
export const captureException: (error: any, opts: any) => void;
89-
}
90-
91-
// @filename: index.js
92-
// ---cut---
93-
import * as Sentry from '@sentry/sveltekit';
94-
95-
Sentry.init({/*...*/})
96-
97-
/** @type {import('@sveltejs/kit').HandleServerError} */
98-
export function handleError({ error, event }) {
99-
// example integration with https://sentry.io/
100-
Sentry.captureException(error, { extra: { event } });
101-
102-
return {
103-
message: 'Whoops!',
104-
code: error?.code ?? 'UNKNOWN'
105-
};
106-
}
107-
```
108-
109-
> Make sure that `handleError` _never_ throws an error
80+
Unexpected errors will go through the [`handleError`](hooks#shared-hooks-handleerror) hook, where you can add your own error handling — for example, sending errors to a reporting service, or returning a custom error object which becomes `$page.error`.
11081

11182
## Responses
11283

documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ As such, SvelteKit 2 replaces `resolvePath` with a (slightly better named) funct
105105

106106
`svelte-migrate` will do the method replacement for you, though if you later prepend the result with `base`, you need to remove that yourself.
107107

108+
## Improved error handling
109+
110+
Errors are handled inconsistently in SvelteKit 1. Some errors trigger the `handleError` hook but there is no good way to discern their status (for example, the only way to tell a 404 from a 500 is by seeing if `event.route.id` is `null`), while others (such as 405 errors for `POST` requests to pages without actions) don't trigger `handleError` at all, but should. In the latter case, the resulting `$page.error` will deviate from the [`App.Error`](/docs/types#app-error) type, if it is specified.
111+
112+
SvelteKit 2 cleans this up by calling `handleError` hooks with two new properties: `status` and `message`. For errors thrown from your code (or library code called by your code) the status will be `500` and the message will be `Internal Error`. While `error.message` may contain sensitive information that should not be exposed to users, `message` is safe.
113+
108114
## Dynamic environment variables cannot be used during prerendering
109115

110116
The `$env/dynamic/public` and `$env/dynamic/private` modules provide access to _run time_ environment variables, as opposed to the _build time_ environment variables exposed by `$env/static/public` and `$env/static/private`.
@@ -127,6 +133,12 @@ If a form contains an `<input type="file">` but does not have an `enctype="multi
127133

128134
Previously, the generated `tsconfig.json` was trying its best to still produce a somewhat valid config when your `tsconfig.json` included `paths` or `baseUrl`. In SvelteKit 2, the validation is more strict and will warn when you use either `paths` or `baseUrl` in your `tsconfig.json`. These settings are used to generate path aliases and you should use [the `alias` config](configuration#alias) option in your `svelte.config.js` instead, to also create a corresponding alias for the bundler.
129135

136+
## `getRequest` no longer throws errors
137+
138+
The `@sveltejs/kit/node` module exports helper functions for use in Node environments, including `getRequest` which turns a Node [`ClientRequest`](https://nodejs.org/api/http.html#class-httpclientrequest) into a standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object.
139+
140+
In SvelteKit 1, `getRequest` could throw if the `Content-Length` header exceeded the specified size limit. In SvelteKit 2, the error will not be thrown until later, when the request body (if any) is being read. This enables better diagnostics and simpler code.
141+
130142
## `vitePreprocess` is no longer exported from `@sveltejs/kit/vite`
131143

132144
Since `@sveltejs/vite-plugin-svelte` is now a peer dependency, SvelteKit 2 no longer re-exports `vitePreprocess`. You should import it directly from `@svelte/vite-plugin-svelte`.

packages/adapter-node/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,6 @@
5050
"rollup": "^4.8.0"
5151
},
5252
"peerDependencies": {
53-
"@sveltejs/kit": "^1.0.0 || ^2.0.0"
53+
"@sveltejs/kit": "^2.0.0"
5454
}
5555
}

packages/adapter-node/src/handler.js

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,20 +76,11 @@ function serve_prerendered() {
7676

7777
/** @type {import('polka').Middleware} */
7878
const ssr = async (req, res) => {
79-
/** @type {Request | undefined} */
80-
let request;
81-
82-
try {
83-
request = await getRequest({
84-
base: origin || get_origin(req.headers),
85-
request: req,
86-
bodySizeLimit: body_size_limit
87-
});
88-
} catch (err) {
89-
res.statusCode = err.status || 400;
90-
res.end('Invalid request body');
91-
return;
92-
}
79+
const request = await getRequest({
80+
base: origin || get_origin(req.headers),
81+
request: req,
82+
bodySizeLimit: body_size_limit
83+
});
9384

9485
setResponse(
9586
res,

packages/adapter-vercel/files/serverless.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,7 @@ export default async (req, res) => {
3232
}
3333
}
3434

35-
/** @type {Request} */
36-
let request;
37-
38-
try {
39-
request = await getRequest({ base: `https://${req.headers.host}`, request: req });
40-
} catch (err) {
41-
res.statusCode = /** @type {any} */ (err).status || 400;
42-
return res.end('Invalid request body');
43-
}
35+
const request = await getRequest({ base: `https://${req.headers.host}`, request: req });
4436

4537
setResponse(
4638
res,

packages/adapter-vercel/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,6 @@
4242
"vitest": "^1.0.4"
4343
},
4444
"peerDependencies": {
45-
"@sveltejs/kit": "^1.5.0 || ^2.0.0"
45+
"@sveltejs/kit": "^2.0.0"
4646
}
4747
}

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

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as set_cookie_parser from 'set-cookie-parser';
2-
import { error } from '../index.js';
2+
import { SvelteKitError } from '../../runtime/control.js';
33

44
/**
55
* @param {import('http').IncomingMessage} req
@@ -22,19 +22,6 @@ function get_raw_body(req, body_size_limit) {
2222
return null;
2323
}
2424

25-
let length = content_length;
26-
27-
if (body_size_limit) {
28-
if (!length) {
29-
length = body_size_limit;
30-
} else if (length > body_size_limit) {
31-
error(
32-
413,
33-
`Received content-length of ${length}, but only accept up to ${body_size_limit} bytes.`
34-
);
35-
}
36-
}
37-
3825
if (req.destroyed) {
3926
const readable = new ReadableStream();
4027
readable.cancel();
@@ -46,6 +33,17 @@ function get_raw_body(req, body_size_limit) {
4633

4734
return new ReadableStream({
4835
start(controller) {
36+
if (body_size_limit !== undefined && content_length > body_size_limit) {
37+
const error = new SvelteKitError(
38+
413,
39+
'Payload Too Large',
40+
`Content-length of ${content_length} exceeds limit of ${body_size_limit} bytes.`
41+
);
42+
43+
controller.error(error);
44+
return;
45+
}
46+
4947
req.on('error', (error) => {
5048
cancelled = true;
5149
controller.error(error);
@@ -60,16 +58,15 @@ function get_raw_body(req, body_size_limit) {
6058
if (cancelled) return;
6159

6260
size += chunk.length;
63-
if (size > length) {
61+
if (size > content_length) {
6462
cancelled = true;
65-
controller.error(
66-
error(
67-
413,
68-
`request body size exceeded ${
69-
content_length ? "'content-length'" : 'BODY_SIZE_LIMIT'
70-
} of ${length}`
71-
)
72-
);
63+
64+
const constraint = content_length ? 'content-length' : 'BODY_SIZE_LIMIT';
65+
const message = `request body size exceeded ${constraint} of ${content_length}`;
66+
67+
const error = new SvelteKitError(413, 'Payload Too Large', message);
68+
controller.error(error);
69+
7370
return;
7471
}
7572

0 commit comments

Comments
 (0)