Skip to content

Commit cef3621

Browse files
authored
ref(sveltekit): Improve SvelteKit 2.0 404 server error handling (#9901)
Improve our logic to filter out "Not Found" errors in our server-side `handleError` hook wrapper: - We now use SvelteKit 2.0 - native [error properties](https://kit.svelte.dev/docs/migrating-to-sveltekit-2#improved-error-handling) (`status` ~and `message`~) to check for "Not Found" errors - Adjusted types for type safety and backwards compatibility with SvelteKit 1.x where the ~two properties~ property don't exist. - Updated Sveltekit to 2.0 in our dev dependency to work with latest types.
1 parent 31c769c commit cef3621

File tree

6 files changed

+458
-97
lines changed

6 files changed

+458
-97
lines changed

packages/sveltekit/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@
4242
"sorcery": "0.11.0"
4343
},
4444
"devDependencies": {
45-
"@sveltejs/kit": "^1.11.0",
45+
"@sveltejs/kit": "^2.0.2",
4646
"rollup": "^3.20.2",
47-
"svelte": "^3.44.0",
48-
"vite": "4.0.5"
47+
"svelte": "^4.2.8",
48+
"vite": "^5.0.10"
4949
},
5050
"scripts": {
5151
"build": "run-p build:transpile build:types",

packages/sveltekit/src/client/handleError.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ function defaultErrorHandler({ error }: Parameters<HandleClientError>[0]): Retur
1111
});
1212
}
1313

14+
// TODO: add backwards-compatible type for kit 1.x (soon)
15+
type HandleClientErrorInput = Parameters<HandleClientError>[0];
16+
1417
/**
1518
* Wrapper for the SvelteKit error handler that sends the error to Sentry.
1619
*
1720
* @param handleError The original SvelteKit error handler.
1821
*/
1922
export function handleErrorWithSentry(handleError: HandleClientError = defaultErrorHandler): HandleClientError {
20-
return (input: { error: unknown; event: NavigationEvent }): ReturnType<HandleClientError> => {
23+
return (input: HandleClientErrorInput): ReturnType<HandleClientError> => {
2124
captureException(input.error, {
2225
mechanism: {
2326
type: 'sveltekit',

packages/sveltekit/src/server/handleError.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { captureException } from '@sentry/node';
2-
import type { HandleServerError, RequestEvent } from '@sveltejs/kit';
2+
import type { HandleServerError } from '@sveltejs/kit';
33

44
import { flushIfServerless } from './utils';
55

@@ -11,14 +11,28 @@ function defaultErrorHandler({ error }: Parameters<HandleServerError>[0]): Retur
1111
console.error(error && error.stack);
1212
}
1313

14+
type HandleServerErrorInput = Parameters<HandleServerError>[0];
15+
16+
/**
17+
* Backwards-compatible HandleServerError Input type for SvelteKit 1.x and 2.x
18+
* `message` and `status` were added in 2.x.
19+
* For backwards-compatibility, we make them optional
20+
*
21+
* @see https://kit.svelte.dev/docs/migrating-to-sveltekit-2#improved-error-handling
22+
*/
23+
type SafeHandleServerErrorInput = Omit<HandleServerErrorInput, 'status' | 'message'> &
24+
Partial<Pick<HandleServerErrorInput, 'status' | 'message'>>;
25+
1426
/**
1527
* Wrapper for the SvelteKit error handler that sends the error to Sentry.
1628
*
1729
* @param handleError The original SvelteKit error handler.
1830
*/
1931
export function handleErrorWithSentry(handleError: HandleServerError = defaultErrorHandler): HandleServerError {
20-
return async (input: { error: unknown; event: RequestEvent }): Promise<void | App.Error> => {
32+
return async (input: SafeHandleServerErrorInput): Promise<void | App.Error> => {
2133
if (isNotFoundError(input)) {
34+
// We're extra cautious with SafeHandleServerErrorInput - this type is not compatible with HandleServerErrorInput
35+
// @ts-expect-error - we're still passing the same object, just with a different (backwards-compatible) type
2236
return handleError(input);
2337
}
2438

@@ -31,19 +45,26 @@ export function handleErrorWithSentry(handleError: HandleServerError = defaultEr
3145

3246
await flushIfServerless();
3347

48+
// We're extra cautious with SafeHandleServerErrorInput - this type is not compatible with HandleServerErrorInput
49+
// @ts-expect-error - we're still passing the same object, just with a different (backwards-compatible) type
3450
return handleError(input);
3551
};
3652
}
3753

3854
/**
3955
* When a page request fails because the page is not found, SvelteKit throws a "Not found" error.
40-
* In the error handler here, we can't access the response yet (which we do in the load instrumentation),
41-
* so we have to check if the error is a "Not found" error by checking if the route id is missing and
42-
* by checking the error message on top of the raw stack trace.
4356
*/
44-
function isNotFoundError(input: { error: unknown; event: RequestEvent }): boolean {
45-
const { error, event } = input;
57+
function isNotFoundError(input: SafeHandleServerErrorInput): boolean {
58+
const { error, event, status } = input;
59+
60+
// SvelteKit 2.0 offers a reliable way to check for a Not Found error:
61+
if (status === 404) {
62+
return true;
63+
}
4664

65+
// SvelteKit 1.x doesn't offer a reliable way to check for a Not Found error.
66+
// So we check the route id (shouldn't exist) and the raw stack trace
67+
// We can delete all of this below whenever we drop Kit 1.x support
4768
const hasNoRouteId = !event.route || !event.route.id;
4869

4970
const rawStack: string =

packages/sveltekit/test/common/utils.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe('isHttpError', () => {
3434
expect(isHttpError(httpErrorObject)).toBe(true);
3535
});
3636

37-
it.each([new Error(), redirect(301, '/users/id'), 'string error', { status: 404 }, { body: 'Not found' }])(
37+
it.each([new Error(), { status: 301, message: '/users/id' }, 'string error', { status: 404 }, { body: 'Not found' }])(
3838
'returns `false` for other thrown objects (%s)',
3939
httpErrorObject => {
4040
expect(isHttpError(httpErrorObject)).toBe(false);

packages/sveltekit/test/server/handleError.test.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('handleError', () => {
2626
consoleErrorSpy.mockClear();
2727
});
2828

29-
it('doesn\'t capture "Not found" errors for incorrect navigations', async () => {
29+
it('doesn\'t capture "Not found" errors for incorrect navigations [Kit 1.x]', async () => {
3030
const wrappedHandleError = handleErrorWithSentry();
3131
const mockError = new Error('Not found: /asdf/123');
3232
const mockEvent = {
@@ -35,18 +35,39 @@ describe('handleError', () => {
3535
// ...
3636
} as RequestEvent;
3737

38+
// @ts-expect-error - purposefully omitting status and message to cover SvelteKit 1.x compatibility
3839
const returnVal = await wrappedHandleError({ error: mockError, event: mockEvent });
3940

4041
expect(returnVal).not.toBeDefined();
4142
expect(mockCaptureException).toHaveBeenCalledTimes(0);
4243
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
4344
});
4445

46+
it('doesn\'t capture "Not found" errors for incorrect navigations [Kit 2.x]', async () => {
47+
const wrappedHandleError = handleErrorWithSentry();
48+
49+
const returnVal = await wrappedHandleError({
50+
error: new Error('404 /asdf/123'),
51+
event: requestEvent,
52+
status: 404,
53+
message: 'Not Found',
54+
});
55+
56+
expect(returnVal).not.toBeDefined();
57+
expect(mockCaptureException).toHaveBeenCalledTimes(0);
58+
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
59+
});
60+
4561
describe('calls captureException', () => {
4662
it('invokes the default handler if no handleError func is provided', async () => {
4763
const wrappedHandleError = handleErrorWithSentry();
4864
const mockError = new Error('test');
49-
const returnVal = await wrappedHandleError({ error: mockError, event: requestEvent });
65+
const returnVal = await wrappedHandleError({
66+
error: mockError,
67+
event: requestEvent,
68+
status: 500,
69+
message: 'Internal Error',
70+
});
5071

5172
expect(returnVal).not.toBeDefined();
5273
expect(mockCaptureException).toHaveBeenCalledTimes(1);
@@ -58,7 +79,12 @@ describe('handleError', () => {
5879
it('invokes the user-provided error handler', async () => {
5980
const wrappedHandleError = handleErrorWithSentry(handleError);
6081
const mockError = new Error('test');
61-
const returnVal = (await wrappedHandleError({ error: mockError, event: requestEvent })) as any;
82+
const returnVal = (await wrappedHandleError({
83+
error: mockError,
84+
event: requestEvent,
85+
status: 500,
86+
message: 'Internal Error',
87+
})) as any;
6288

6389
expect(returnVal.message).toEqual('Whoops!');
6490
expect(mockCaptureException).toHaveBeenCalledTimes(1);

0 commit comments

Comments
 (0)