Skip to content

Commit e1a5a2d

Browse files
matthewpsarah11918
andauthoredDec 14, 2023
Handle unhandledrejections in the dev server (#9424)
* Handle unhandledrejections in the dev server * Adding changeset * Update .changeset/curvy-lobsters-crash.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Use AsyncLocalStorage * Return errorWithMetadata * Send the error to the browser --------- Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
1 parent 8c9fe00 commit e1a5a2d

File tree

5 files changed

+82
-26
lines changed

5 files changed

+82
-26
lines changed
 

‎.changeset/curvy-lobsters-crash.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Prevents dev server from crashing on unhandled rejections, and adds a helpful error message

‎packages/astro/src/core/errors/errors-data.ts

+9
Original file line numberDiff line numberDiff line change
@@ -1292,3 +1292,12 @@ export const CantRenderPage = {
12921292

12931293
// Generic catch-all - Only use this in extreme cases, like if there was a cosmic ray bit flip
12941294
export const UnknownError = { name: 'UnknownError', title: 'Unknown Error.' } satisfies ErrorData;
1295+
1296+
export const UnhandledRejection = {
1297+
name: 'UnhandledRejection',
1298+
title: 'Unhandled rejection',
1299+
message: (stack: string) => {
1300+
return `Astro detected an unhandled rejection. Here's the stack trace:\n${stack}`;
1301+
},
1302+
hint: 'Make sure your promises all have an `await` or a `.catch()` handler.'
1303+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { ModuleLoader } from '../core/module-loader/index.js'
2+
import type { AstroConfig } from '../@types/astro.js';
3+
import type DevPipeline from './devPipeline.js';
4+
5+
import { collectErrorMetadata } from '../core/errors/dev/index.js';
6+
import { createSafeError } from '../core/errors/index.js';
7+
import { formatErrorMessage } from '../core/messages.js';
8+
import { eventError, telemetry } from '../events/index.js';
9+
10+
export function recordServerError(loader: ModuleLoader, config: AstroConfig, pipeline: DevPipeline, _err: unknown) {
11+
const err = createSafeError(_err);
12+
13+
// This could be a runtime error from Vite's SSR module, so try to fix it here
14+
try {
15+
loader.fixStacktrace(err);
16+
} catch {}
17+
18+
// This is our last line of defense regarding errors where we still might have some information about the request
19+
// Our error should already be complete, but let's try to add a bit more through some guesswork
20+
const errorWithMetadata = collectErrorMetadata(err, config.root);
21+
22+
telemetry.record(eventError({ cmd: 'dev', err: errorWithMetadata, isFatal: false }));
23+
24+
pipeline.logger.error(
25+
null,
26+
formatErrorMessage(errorWithMetadata, pipeline.logger.level() === 'debug')
27+
);
28+
29+
return {
30+
error: err,
31+
errorWithMetadata
32+
};
33+
}

‎packages/astro/src/vite-plugin-astro-server/plugin.ts

+32-7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import { baseMiddleware } from './base.js';
1010
import { createController } from './controller.js';
1111
import DevPipeline from './devPipeline.js';
1212
import { handleRequest } from './request.js';
13+
import { AstroError, AstroErrorData } from '../core/errors/index.js';
14+
import { getViteErrorPayload } from '../core/errors/dev/index.js';
15+
import { AsyncLocalStorage } from 'node:async_hooks';
16+
import { IncomingMessage } from 'node:http';
17+
import { setRouteError } from './server-state.js';
18+
import { recordServerError } from './error.js';
1319

1420
export interface AstroPluginOptions {
1521
settings: AstroSettings;
@@ -30,6 +36,7 @@ export default function createVitePluginAstroServer({
3036
const pipeline = new DevPipeline({ logger, manifest, settings, loader });
3137
let manifestData: ManifestData = createRouteManifest({ settings, fsMod }, logger);
3238
const controller = createController({ loader });
39+
const localStorage = new AsyncLocalStorage();
3340

3441
/** rebuild the route cache + manifest, as needed. */
3542
function rebuildManifest(needsManifestRebuild: boolean) {
@@ -43,6 +50,22 @@ export default function createVitePluginAstroServer({
4350
viteServer.watcher.on('unlink', rebuildManifest.bind(null, true));
4451
viteServer.watcher.on('change', rebuildManifest.bind(null, false));
4552

53+
function handleUnhandledRejection(rejection: any) {
54+
const error = new AstroError({
55+
...AstroErrorData.UnhandledRejection,
56+
message: AstroErrorData.UnhandledRejection.message(rejection?.stack || rejection)
57+
});
58+
const store = localStorage.getStore();
59+
if(store instanceof IncomingMessage) {
60+
const request = store;
61+
setRouteError(controller.state, request.url!, error);
62+
}
63+
const { errorWithMetadata } = recordServerError(loader, settings.config, pipeline, error);
64+
setTimeout(async () => loader.webSocketSend(await getViteErrorPayload(errorWithMetadata)), 200)
65+
}
66+
67+
process.on('unhandledRejection', handleUnhandledRejection);
68+
4669
return () => {
4770
// Push this middleware to the front of the stack so that it can intercept responses.
4871
// fix(#6067): always inject this to ensure zombie base handling is killed after restarts
@@ -57,13 +80,15 @@ export default function createVitePluginAstroServer({
5780
response.end();
5881
return;
5982
}
60-
handleRequest({
61-
pipeline,
62-
manifestData,
63-
controller,
64-
incomingRequest: request,
65-
incomingResponse: response,
66-
manifest,
83+
localStorage.run(request, () => {
84+
handleRequest({
85+
pipeline,
86+
manifestData,
87+
controller,
88+
incomingRequest: request,
89+
incomingResponse: response,
90+
manifest,
91+
});
6792
});
6893
});
6994
};

‎packages/astro/src/vite-plugin-astro-server/request.ts

+3-19
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { runWithErrorHandling } from './controller.js';
1111
import type DevPipeline from './devPipeline.js';
1212
import { handle500Response } from './response.js';
1313
import { handleRoute, matchRoute } from './route.js';
14+
import { recordServerError } from './error.js';
1415

1516
type HandleRequest = {
1617
pipeline: DevPipeline;
@@ -89,26 +90,9 @@ export async function handleRequest({
8990
});
9091
},
9192
onError(_err) {
92-
const err = createSafeError(_err);
93-
94-
// This could be a runtime error from Vite's SSR module, so try to fix it here
95-
try {
96-
moduleLoader.fixStacktrace(err);
97-
} catch {}
98-
99-
// This is our last line of defense regarding errors where we still might have some information about the request
100-
// Our error should already be complete, but let's try to add a bit more through some guesswork
101-
const errorWithMetadata = collectErrorMetadata(err, config.root);
102-
103-
telemetry.record(eventError({ cmd: 'dev', err: errorWithMetadata, isFatal: false }));
104-
105-
pipeline.logger.error(
106-
null,
107-
formatErrorMessage(errorWithMetadata, pipeline.logger.level() === 'debug')
108-
);
93+
const { error, errorWithMetadata } = recordServerError(moduleLoader, config, pipeline, _err);
10994
handle500Response(moduleLoader, incomingResponse, errorWithMetadata);
110-
111-
return err;
95+
return error;
11296
},
11397
});
11498
}

0 commit comments

Comments
 (0)
Please sign in to comment.