diff --git a/src/instrumentation-node.ts b/src/instrumentation-node.ts index b3f885c93f..31fe0b4099 100755 --- a/src/instrumentation-node.ts +++ b/src/instrumentation-node.ts @@ -7,6 +7,7 @@ */ import { markServerReady, markServerStarting } from "@/lib/serverLifecycle"; +import { normalizeBootError } from "@/lib/instrumentationBootError"; function getRandomBytes(byteLength: number): Uint8Array { const bytes = new Uint8Array(byteLength); @@ -37,23 +38,13 @@ export function renameProcessTitle(currentTitle: string): string { return `omniroute${currentTitle.slice("next-server".length)}`; } -/** - * Normalize any thrown/rejected value into a real `Error` instance. - * - * Next.js's own `registerInstrumentation()` wrapper (see - * `node_modules/next/dist/server/lib/router-utils/instrumentation-globals.external.js`) - * unconditionally does `err.message = \`...${err.message}\`` on whatever our - * `register()` export rejects with, assuming it is always an `Error`. If a raw - * non-Error primitive bubbles up instead (e.g. sql.js's WASM adapter throws the - * bare string `"Database closed"` — see `./lib/db/adapters/sqljsAdapter.ts`), - * that assignment throws `TypeError: Cannot create property 'message' on - * string '...'` in strict mode, masking the original error and crashing the - * whole server on every boot (#6560). Normalizing before it leaves our code - * guarantees Next always receives something `.message`-assignable. - */ -export function normalizeBootError(err: unknown): Error { - return err instanceof Error ? err : new Error(String(err)); -} +// `normalizeBootError` now lives in `@/lib/instrumentationBootError` (imported +// above) — shared, dependency-free, and reused by `src/instrumentation.ts`'s +// outermost boot boundary (#10171) so both boot-failure logging sites agree +// on the same normalization instead of maintaining two copies of the same +// one-liner. Re-exported here so existing callers/tests importing it from +// this module keep working unchanged. +export { normalizeBootError }; // Matches sql.js's raw `throw "Database closed"` (and similarly-worded // variants) thrown when a query runs against an already-closed WASM handle — diff --git a/src/instrumentation.ts b/src/instrumentation.ts index f0e6d41ce6..59270c7a2d 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -8,6 +8,8 @@ * @see https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation */ +import { normalizeBootError } from "@/lib/instrumentationBootError"; + /** * `registerNodejsFn` is only for tests to inject a fake without module-mocking * (`node:test` does not support `mock.module` reliably here) — mirrors the @@ -34,7 +36,12 @@ export async function register(registerNodejsFn?: () => Promise) { // up while every DB-touching route 500s forever with a permanently // empty app.log. This guarantees stdout/app.log is never silently // empty on a failed boot, regardless of platform or which step threw. - const normalizedError = err instanceof Error ? err : new Error(String(err)); + // Reuses the same normalizeBootError() helper as ensureDbReadyForBoot + // (./instrumentation-node) — imported from a dependency-free module so + // this file, which also loads under the Edge runtime, never has to pull + // in Node-only code (or risk a second failing dynamic import of + // ./instrumentation-node itself) just to normalize the caught value. + const normalizedError = normalizeBootError(err); const message = normalizedError.message; console.error("[STARTUP] Fatal: instrumentation hook failed during boot:", message); throw normalizedError; diff --git a/src/lib/instrumentationBootError.ts b/src/lib/instrumentationBootError.ts new file mode 100644 index 0000000000..9cbee2733e --- /dev/null +++ b/src/lib/instrumentationBootError.ts @@ -0,0 +1,26 @@ +/** + * Normalize any thrown/rejected value into a real `Error` instance. + * + * Next.js's own `registerInstrumentation()` wrapper (see + * `node_modules/next/dist/server/lib/router-utils/instrumentation-globals.external.js`) + * unconditionally does `err.message = \`...${err.message}\`` on whatever our + * `register()` export (`src/instrumentation.ts`) rejects with, assuming it is + * always an `Error`. If a raw non-Error primitive bubbles up instead (e.g. + * sql.js's WASM adapter throws the bare string `"Database closed"` — see + * `./db/adapters/sqljsAdapter.ts`), that assignment throws `TypeError: Cannot + * create property 'message' on string '...'` in strict mode, masking the + * original error and crashing the whole server on every boot (#6560). + * Normalizing before it leaves our code guarantees Next always receives + * something `.message`-assignable. + * + * Deliberately dependency-free (no imports) so both `src/instrumentation.ts` + * (the shared Edge+Node instrumentation entry point) and + * `src/instrumentation-node.ts` (the Node-only boot sequence) can import it + * statically without pulling Node-specific modules (fs/path/os/better-sqlite3 + * etc.) into the Edge runtime bundle, and without relying on a dynamic + * `import("./instrumentation-node")` that could itself be the thing that + * failed to load (#10171). + */ +export function normalizeBootError(err: unknown): Error { + return err instanceof Error ? err : new Error(String(err)); +}