Spaces:
Sleeping
Sleeping
| // Captures the original Error out-of-band so server.ts can recover the stack | |
| // when h3 has already swallowed the throw into a generic 500 Response. | |
| let lastCapturedError: { error: unknown; at: number } | undefined; | |
| const TTL_MS = 5_000; | |
| function record(error: unknown) { | |
| lastCapturedError = { error, at: Date.now() }; | |
| } | |
| // h3's HTTPError serializes to {"status":500,"unhandled":true,"message":"HTTPError"} β | |
| // no stack, no cause β so a plain console.error(error) reaches the log pipeline with | |
| // the failure detail stripped. Expand Error-like args into a string that keeps the | |
| // message, stack, and the full cause chain. | |
| const CAUSE_DEPTH_LIMIT = 5; | |
| const DESCRIPTION_LENGTH_LIMIT = 8_000; | |
| export function describeError(error: unknown): string { | |
| const parts: string[] = []; | |
| let current: unknown = error; | |
| for (let depth = 0; depth < CAUSE_DEPTH_LIMIT && current != null; depth++) { | |
| if (!(current instanceof Error)) { | |
| parts.push(typeof current === "string" ? current : safeStringify(current)); | |
| break; | |
| } | |
| const label = depth === 0 ? "" : "caused by: "; | |
| const status = describeStatus(current); | |
| parts.push(`${label}${current.stack ?? `${current.name}: ${current.message}`}${status}`); | |
| current = current.cause; | |
| } | |
| return parts.join("\n").slice(0, DESCRIPTION_LENGTH_LIMIT); | |
| } | |
| function describeStatus(error: Error): string { | |
| const { status, statusCode } = error as { status?: unknown; statusCode?: unknown }; | |
| const value = status ?? statusCode; | |
| return typeof value === "number" ? ` (status ${value})` : ""; | |
| } | |
| function safeStringify(value: unknown): string { | |
| try { | |
| return JSON.stringify(value) ?? String(value); | |
| } catch { | |
| return String(value); | |
| } | |
| } | |
| function isErrorLike(value: unknown): value is Error { | |
| return value instanceof Error; | |
| } | |
| // Wrap console.error so errors logged by any layer β including h3's internal | |
| // unhandled-error logging, which this file cannot hook directly β are both | |
| // recorded for consumeLastCapturedError and expanded before serialization. | |
| const originalConsoleError = console.error.bind(console); | |
| console.error = (...args: unknown[]) => { | |
| const expanded = args.map((arg) => { | |
| if (!isErrorLike(arg)) return arg; | |
| record(arg); | |
| return describeError(arg); | |
| }); | |
| originalConsoleError(...expanded); | |
| }; | |
| if (typeof globalThis.addEventListener === "function") { | |
| globalThis.addEventListener("error", (event) => record((event as ErrorEvent).error ?? event)); | |
| globalThis.addEventListener("unhandledrejection", (event) => | |
| record((event as PromiseRejectionEvent).reason), | |
| ); | |
| } | |
| export function consumeLastCapturedError(): unknown { | |
| if (!lastCapturedError) return undefined; | |
| if (Date.now() - lastCapturedError.at > TTL_MS) { | |
| lastCapturedError = undefined; | |
| return undefined; | |
| } | |
| const { error } = lastCapturedError; | |
| lastCapturedError = undefined; | |
| return error; | |
| } | |