From e1a5a2d36ac3637f5c94a27b69128a121541bae8 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 14 Dec 2023 08:39:06 -0500 Subject: [PATCH] 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 * Use AsyncLocalStorage * Return errorWithMetadata * Send the error to the browser --------- Co-authored-by: Sarah Rainsberger --- .changeset/curvy-lobsters-crash.md | 5 +++ packages/astro/src/core/errors/errors-data.ts | 9 +++++ .../src/vite-plugin-astro-server/error.ts | 33 ++++++++++++++++ .../src/vite-plugin-astro-server/plugin.ts | 39 +++++++++++++++---- .../src/vite-plugin-astro-server/request.ts | 22 ++--------- 5 files changed, 82 insertions(+), 26 deletions(-) create mode 100644 .changeset/curvy-lobsters-crash.md create mode 100644 packages/astro/src/vite-plugin-astro-server/error.ts diff --git a/.changeset/curvy-lobsters-crash.md b/.changeset/curvy-lobsters-crash.md new file mode 100644 index 000000000000..f3ccdb436040 --- /dev/null +++ b/.changeset/curvy-lobsters-crash.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Prevents dev server from crashing on unhandled rejections, and adds a helpful error message diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 5c30495d649b..8e8fb3911efa 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -1292,3 +1292,12 @@ export const CantRenderPage = { // Generic catch-all - Only use this in extreme cases, like if there was a cosmic ray bit flip export const UnknownError = { name: 'UnknownError', title: 'Unknown Error.' } satisfies ErrorData; + +export const UnhandledRejection = { + name: 'UnhandledRejection', + title: 'Unhandled rejection', + message: (stack: string) => { + return `Astro detected an unhandled rejection. Here's the stack trace:\n${stack}`; + }, + hint: 'Make sure your promises all have an `await` or a `.catch()` handler.' +} diff --git a/packages/astro/src/vite-plugin-astro-server/error.ts b/packages/astro/src/vite-plugin-astro-server/error.ts new file mode 100644 index 000000000000..d29647f98f88 --- /dev/null +++ b/packages/astro/src/vite-plugin-astro-server/error.ts @@ -0,0 +1,33 @@ +import type { ModuleLoader } from '../core/module-loader/index.js' +import type { AstroConfig } from '../@types/astro.js'; +import type DevPipeline from './devPipeline.js'; + +import { collectErrorMetadata } from '../core/errors/dev/index.js'; +import { createSafeError } from '../core/errors/index.js'; +import { formatErrorMessage } from '../core/messages.js'; +import { eventError, telemetry } from '../events/index.js'; + +export function recordServerError(loader: ModuleLoader, config: AstroConfig, pipeline: DevPipeline, _err: unknown) { + const err = createSafeError(_err); + + // This could be a runtime error from Vite's SSR module, so try to fix it here + try { + loader.fixStacktrace(err); + } catch {} + + // This is our last line of defense regarding errors where we still might have some information about the request + // Our error should already be complete, but let's try to add a bit more through some guesswork + const errorWithMetadata = collectErrorMetadata(err, config.root); + + telemetry.record(eventError({ cmd: 'dev', err: errorWithMetadata, isFatal: false })); + + pipeline.logger.error( + null, + formatErrorMessage(errorWithMetadata, pipeline.logger.level() === 'debug') + ); + + return { + error: err, + errorWithMetadata + }; +} diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index f0df0e3ebfc3..6969184f21d7 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -10,6 +10,12 @@ import { baseMiddleware } from './base.js'; import { createController } from './controller.js'; import DevPipeline from './devPipeline.js'; import { handleRequest } from './request.js'; +import { AstroError, AstroErrorData } from '../core/errors/index.js'; +import { getViteErrorPayload } from '../core/errors/dev/index.js'; +import { AsyncLocalStorage } from 'node:async_hooks'; +import { IncomingMessage } from 'node:http'; +import { setRouteError } from './server-state.js'; +import { recordServerError } from './error.js'; export interface AstroPluginOptions { settings: AstroSettings; @@ -30,6 +36,7 @@ export default function createVitePluginAstroServer({ const pipeline = new DevPipeline({ logger, manifest, settings, loader }); let manifestData: ManifestData = createRouteManifest({ settings, fsMod }, logger); const controller = createController({ loader }); + const localStorage = new AsyncLocalStorage(); /** rebuild the route cache + manifest, as needed. */ function rebuildManifest(needsManifestRebuild: boolean) { @@ -43,6 +50,22 @@ export default function createVitePluginAstroServer({ viteServer.watcher.on('unlink', rebuildManifest.bind(null, true)); viteServer.watcher.on('change', rebuildManifest.bind(null, false)); + function handleUnhandledRejection(rejection: any) { + const error = new AstroError({ + ...AstroErrorData.UnhandledRejection, + message: AstroErrorData.UnhandledRejection.message(rejection?.stack || rejection) + }); + const store = localStorage.getStore(); + if(store instanceof IncomingMessage) { + const request = store; + setRouteError(controller.state, request.url!, error); + } + const { errorWithMetadata } = recordServerError(loader, settings.config, pipeline, error); + setTimeout(async () => loader.webSocketSend(await getViteErrorPayload(errorWithMetadata)), 200) + } + + process.on('unhandledRejection', handleUnhandledRejection); + return () => { // Push this middleware to the front of the stack so that it can intercept responses. // fix(#6067): always inject this to ensure zombie base handling is killed after restarts @@ -57,13 +80,15 @@ export default function createVitePluginAstroServer({ response.end(); return; } - handleRequest({ - pipeline, - manifestData, - controller, - incomingRequest: request, - incomingResponse: response, - manifest, + localStorage.run(request, () => { + handleRequest({ + pipeline, + manifestData, + controller, + incomingRequest: request, + incomingResponse: response, + manifest, + }); }); }); }; diff --git a/packages/astro/src/vite-plugin-astro-server/request.ts b/packages/astro/src/vite-plugin-astro-server/request.ts index aa9124fb4146..f0c6b3de0c50 100644 --- a/packages/astro/src/vite-plugin-astro-server/request.ts +++ b/packages/astro/src/vite-plugin-astro-server/request.ts @@ -11,6 +11,7 @@ import { runWithErrorHandling } from './controller.js'; import type DevPipeline from './devPipeline.js'; import { handle500Response } from './response.js'; import { handleRoute, matchRoute } from './route.js'; +import { recordServerError } from './error.js'; type HandleRequest = { pipeline: DevPipeline; @@ -89,26 +90,9 @@ export async function handleRequest({ }); }, onError(_err) { - const err = createSafeError(_err); - - // This could be a runtime error from Vite's SSR module, so try to fix it here - try { - moduleLoader.fixStacktrace(err); - } catch {} - - // This is our last line of defense regarding errors where we still might have some information about the request - // Our error should already be complete, but let's try to add a bit more through some guesswork - const errorWithMetadata = collectErrorMetadata(err, config.root); - - telemetry.record(eventError({ cmd: 'dev', err: errorWithMetadata, isFatal: false })); - - pipeline.logger.error( - null, - formatErrorMessage(errorWithMetadata, pipeline.logger.level() === 'debug') - ); + const { error, errorWithMetadata } = recordServerError(moduleLoader, config, pipeline, _err); handle500Response(moduleLoader, incomingResponse, errorWithMetadata); - - return err; + return error; }, }); }