diff --git a/src/presets/_nitro/runtime/nitro-prerenderer.ts b/src/presets/_nitro/runtime/nitro-prerenderer.ts index d49a222bd3..42763407d4 100644 --- a/src/presets/_nitro/runtime/nitro-prerenderer.ts +++ b/src/presets/_nitro/runtime/nitro-prerenderer.ts @@ -1,9 +1,28 @@ import "#nitro-internal-pollyfills"; +import consola from "consola"; +import { getRequestHeader, getRequestURL, H3Error, isEvent } from "h3"; import { useNitroApp } from "nitropack/runtime"; import { trapUnhandledNodeErrors } from "nitropack/runtime/internal"; const nitroApp = useNitroApp(); +nitroApp.hooks.hook("error", (error, context) => { + if ( + isEvent(context.event) && + !(error as H3Error).unhandled && + (error as H3Error).statusCode >= 500 && + getRequestHeader(context.event, "x-nitro-prerender") + ) { + const url = getRequestURL(context.event).href; + consola.error( + `[prerender error]`, + `[${context.event.method}]`, + `[${url}]`, + error + ); + } +}); + export const localFetch = nitroApp.localFetch; export const closePrerenderer = () => nitroApp.hooks.callHook("close"); diff --git a/test/fixture/nitro.config.ts b/test/fixture/nitro.config.ts index dcb2e233b8..564b390fa3 100644 --- a/test/fixture/nitro.config.ts +++ b/test/fixture/nitro.config.ts @@ -85,6 +85,7 @@ export default defineNitroConfig({ routeRules: { "/api/param/prerender4": { prerender: true }, "/api/param/prerender2": { prerender: false }, + "/prerender-error": { prerender: true }, "/rules/headers": { headers: { "cache-control": "s-maxage=60" } }, "/rules/cors": { cors: true, diff --git a/test/fixture/routes/prerender-error.ts b/test/fixture/routes/prerender-error.ts new file mode 100644 index 0000000000..d0265429b3 --- /dev/null +++ b/test/fixture/routes/prerender-error.ts @@ -0,0 +1,3 @@ +export default defineEventHandler(() => { + return new Error("Prerender error test"); +}); diff --git a/test/presets/vercel.test.ts b/test/presets/vercel.test.ts index e9ac00f04c..bf664c82c5 100644 --- a/test/presets/vercel.test.ts +++ b/test/presets/vercel.test.ts @@ -181,6 +181,10 @@ describe("nitro:preset:vercel", async () => { "dest": "/raw", "src": "/raw", }, + { + "dest": "/prerender-error", + "src": "/prerender-error", + }, { "dest": "/prerender-custom.html", "src": "/prerender-custom.html", @@ -562,6 +566,7 @@ describe("nitro:preset:vercel", async () => { "functions/modules.func (symlink)", "functions/node-compat.func (symlink)", "functions/prerender-custom.html.func (symlink)", + "functions/prerender-error.func (symlink)", "functions/prerender.func (symlink)", "functions/raw.func (symlink)", "functions/replace.func (symlink)", diff --git a/test/tests.ts b/test/tests.ts index dc1f8be75f..ae2fa4bc28 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -19,7 +19,9 @@ import { type FetchOptions, fetch } from "ofetch"; import { join, resolve } from "pathe"; import { isWindows, nodeMajorVersion } from "std-env"; import { joinURL } from "ufo"; -import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import consola from "consola"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import type { MockInstance } from "vitest"; export interface Context { preset: string; @@ -36,6 +38,7 @@ export interface Context { env: Record; lambdaV1?: boolean; // [key: string]: unknown; + consolaError: MockInstance; } // https://github.com/nitrojs/nitro/pull/1240 @@ -109,6 +112,7 @@ export async function setupTest( redirect: "manual", ...(opts as any), }), + consolaError: vi.spyOn(consola, "error").mockImplementation(() => {}), }; // Set environment variables for process compatible presets @@ -157,6 +161,7 @@ export async function setupTest( } afterAll(async () => { + ctx.consolaError.mockRestore(); if (ctx.server) { await ctx.server.close(); } @@ -459,6 +464,12 @@ export function testNitro( expect(data).toBe("prerender4"); expect(headers["content-type"]).toBe("text/plain; charset=utf-16"); }); + + it("show details for 5xx handled errors", async () => { + expect(ctx.consolaError.mock.calls.flat().join(" ")).toContain( + "Prerender error test" + ); + }); } it("shows 404 for /build/non-file", async () => {