| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { workUnitAsyncStorage } from '../app-render/work-unit-async-storage.external' |
|
|
| const MODE: |
| | 'enabled' |
| | 'debug' |
| | 'silent' |
| | 'true' |
| | 'false' |
| | '1' |
| | '0' |
| | '' |
| | string |
| | undefined = process.env.NEXT_UNHANDLED_REJECTION_FILTER |
|
|
| let ENABLE_UHR_FILTER = true |
| let UHR_FILTER_LOG_LEVEL: 'debug' | 'warn' | 'silent' = 'warn' |
|
|
| switch (MODE) { |
| case 'silent': |
| UHR_FILTER_LOG_LEVEL = 'silent' |
| break |
| case 'debug': |
| UHR_FILTER_LOG_LEVEL = 'debug' |
| break |
| case 'false': |
| case 'disabled': |
| case '0': |
| ENABLE_UHR_FILTER = false |
| break |
| case '': |
| case undefined: |
| case 'enabled': |
| case 'true': |
| case '1': |
| break |
| default: |
| if (typeof MODE === 'string') { |
| console.error( |
| `NEXT_UNHANDLED_REJECTION_FILTER has an unrecognized value: ${JSON.stringify(MODE)}. Use "enabled", "disabled", "silent", or "debug", or omit the environment variable altogether` |
| ) |
| } |
| } |
|
|
| let debug: typeof console.debug | undefined |
| let debugWithTrace: typeof console.debug | undefined |
| let warn: typeof console.warn | undefined |
| let warnWithTrace: typeof console.warn | undefined |
|
|
| switch (UHR_FILTER_LOG_LEVEL) { |
| case 'debug': |
| debug = (message: string) => |
| console.log('[Next.js Unhandled Rejection Filter]: ' + message) |
| debugWithTrace = (message: string) => { |
| console.log(new DebugWithStack(message)) |
| } |
| |
| case 'warn': |
| warn = (message: string) => { |
| console.warn('[Next.js Unhandled Rejection Filter]: ' + message) |
| } |
| warnWithTrace = (message: string) => { |
| console.warn(new WarnWithStack(message)) |
| } |
| break |
| case 'silent': |
| default: |
| } |
|
|
| class DebugWithStack extends Error { |
| constructor(message: string) { |
| super(message) |
| this.name = '[Next.js Unhandled Rejection Filter]' |
| } |
| } |
|
|
| class WarnWithStack extends Error { |
| constructor(message: string) { |
| super(message) |
| this.name = '[Next.js Unhandled Rejection Filter]' |
| } |
| } |
|
|
| let didWarnUninstalled = false |
| const warnUninstalledOnce = warn |
| ? function warnUninstalledOnce(...args: any[]) { |
| if (!didWarnUninstalled) { |
| didWarnUninstalled = true |
| warn(...args) |
| } |
| } |
| : undefined |
|
|
| type ListenerMetadata = { |
| listener: NodeJS.UnhandledRejectionListener |
| once: boolean |
| } |
|
|
| let filterInstalled = false |
|
|
| |
| let underlyingListeners: Array<NodeJS.UnhandledRejectionListener> = [] |
| |
| |
| let listenerMetadata: Array<ListenerMetadata> = [] |
|
|
| |
| let originalProcessAddListener: typeof process.addListener |
| let originalProcessRemoveListener: typeof process.removeListener |
| let originalProcessOn: typeof process.on |
| let originalProcessOff: typeof process.off |
| let originalProcessPrependListener: typeof process.prependListener |
| let originalProcessOnce: typeof process.once |
| let originalProcessPrependOnceListener: typeof process.prependOnceListener |
| let originalProcessRemoveAllListeners: typeof process.removeAllListeners |
| let originalProcessListeners: typeof process.listeners |
|
|
| type UnderlyingMethod = |
| | typeof originalProcessAddListener |
| | typeof originalProcessRemoveListener |
| | typeof originalProcessOn |
| | typeof originalProcessOff |
| | typeof originalProcessPrependListener |
| | typeof originalProcessOnce |
| | typeof originalProcessPrependOnceListener |
| | typeof originalProcessRemoveAllListeners |
| | typeof originalProcessListeners |
|
|
| |
| |
| let bypassPatch = false |
|
|
| |
| |
| |
| |
| function patchWithoutReentrancy<T extends UnderlyingMethod>( |
| original: T, |
| patchedImpl: T |
| ): T { |
| |
| const patched = { |
| [original.name]: function (...args: Parameters<T>) { |
| if (bypassPatch) { |
| return Reflect.apply(original, process, args) |
| } |
|
|
| const previousBypassPatch = bypassPatch |
| bypassPatch = true |
| try { |
| return Reflect.apply(patchedImpl, process, args) |
| } finally { |
| bypassPatch = previousBypassPatch |
| } |
| } as any, |
| }[original.name] |
|
|
| |
| Object.defineProperty(patched, 'toString', { |
| value: original.toString.bind(original), |
| writable: true, |
| configurable: true, |
| }) |
|
|
| return patched |
| } |
|
|
| const MACGUFFIN_EVENT = 'Next.UnhandledRejectionFilter.MacguffinEvent' |
|
|
| |
| |
| |
| |
| |
| |
| function installUnhandledRejectionFilter(): void { |
| if (filterInstalled) { |
| warnWithTrace?.( |
| 'Unexpected subsequent filter installation. This is a bug in Next.js' |
| ) |
| return |
| } |
|
|
| debug?.('Installing Filter') |
|
|
| |
| underlyingListeners = Array.from(process.listeners('unhandledRejection')) |
| |
| listenerMetadata = underlyingListeners.map((l) => ({ |
| listener: l, |
| once: false, |
| })) |
|
|
| |
| process.removeAllListeners('unhandledRejection') |
|
|
| |
| process.addListener('unhandledRejection', filteringUnhandledRejectionHandler) |
|
|
| |
| originalProcessAddListener = process.addListener |
| originalProcessRemoveListener = process.removeListener |
| originalProcessOn = process.on |
| originalProcessOff = process.off |
| originalProcessPrependListener = process.prependListener |
| originalProcessOnce = process.once |
| originalProcessPrependOnceListener = process.prependOnceListener |
| originalProcessRemoveAllListeners = process.removeAllListeners |
| originalProcessListeners = process.listeners |
|
|
| process.addListener = patchWithoutReentrancy( |
| originalProcessAddListener, |
| function (event: string | symbol, listener: (...args: any[]) => void) { |
| if (event === 'unhandledRejection') { |
| debugWithTrace?.( |
| `Appending 'unhandledRejection' listener with name \`${listener.name}\`.` |
| ) |
| |
| try { |
| originalProcessAddListener.call( |
| process, |
| MACGUFFIN_EVENT as any, |
| listener |
| ) |
| } finally { |
| |
| originalProcessRemoveAllListeners.call(process, MACGUFFIN_EVENT) |
| } |
| |
| underlyingListeners.push(listener as NodeJS.UnhandledRejectionListener) |
| listenerMetadata.push({ listener, once: false }) |
| return process |
| } |
| |
| return originalProcessAddListener.call(process, event as any, listener) |
| } as typeof process.addListener |
| ) |
|
|
| |
| process.removeListener = patchWithoutReentrancy( |
| originalProcessRemoveListener, |
| function (event: string | symbol, listener: (...args: any[]) => void) { |
| if (event === 'unhandledRejection') { |
| |
| if (listener === filteringUnhandledRejectionHandler) { |
| warnUninstalledOnce?.( |
| `Uninstalling filter because \`process.removeListener('unhandledRejection', listener)\` was called with the filter listener. Uninstalling this filter is not recommended and will cause you to observe 'unhandledRejection' events related to intentionally aborted prerenders. |
| |
| You can silence warnings related to this behavior by running Next.js with \`NEXT_UNHANDLED_REJECTION_FILTER=silent\` environment variable. |
| |
| You can debug event listener operations by running Next.js with \`NEXT_UNHANDLED_REJECTION_FILTER=debug\` environment variable.` |
| ) |
| uninstallUnhandledRejectionFilter() |
| return process |
| } |
|
|
| debugWithTrace?.( |
| `Removing 'unhandledRejection' listener with name \`${listener.name}\`.` |
| ) |
| |
| originalProcessRemoveListener.call( |
| process, |
| MACGUFFIN_EVENT as any, |
| listener |
| ) |
| const index = underlyingListeners.lastIndexOf(listener) |
| if (index > -1) { |
| debug?.(`listener found index ${index} and removed.`) |
| underlyingListeners.splice(index, 1) |
| listenerMetadata.splice(index, 1) |
| } else { |
| debug?.(`listener not found.`) |
| } |
| return process |
| } |
| |
| return originalProcessRemoveListener.call(process, event, listener) |
| } as typeof process.removeListener |
| ) |
|
|
| |
| if (originalProcessOn === originalProcessAddListener) { |
| process.on = process.addListener |
| } else { |
| process.on = patchWithoutReentrancy(originalProcessOn, function ( |
| event: string | symbol, |
| listener: (...args: any[]) => void |
| ) { |
| if (event === 'unhandledRejection') { |
| debugWithTrace?.( |
| `Appending 'unhandledRejection' listener with name \`${listener.name}\`.` |
| ) |
| |
| try { |
| originalProcessOn.call(process, MACGUFFIN_EVENT as any, listener) |
| } finally { |
| |
| originalProcessRemoveAllListeners.call(process, MACGUFFIN_EVENT) |
| } |
| |
| underlyingListeners.push(listener as NodeJS.UnhandledRejectionListener) |
| listenerMetadata.push({ listener, once: false }) |
| return process |
| } |
| |
| return originalProcessOn.call(process, event, listener) |
| } as typeof process.on) |
| } |
|
|
| |
| if (originalProcessOff === originalProcessRemoveListener) { |
| process.off = process.removeListener |
| } else { |
| process.off = patchWithoutReentrancy(originalProcessOff, function ( |
| event: string | symbol, |
| listener: (...args: any[]) => void |
| ) { |
| if (event === 'unhandledRejection') { |
| |
| if (listener === filteringUnhandledRejectionHandler) { |
| warnUninstalledOnce?.( |
| `Uninstalling filter because \`process.off('unhandledRejection', listener)\` was called with the filter listener. Uninstalling this filter is not recommended and will cause you to observe 'unhandledRejection' events related to intentionally aborted prerenders. |
| |
| You can silence warnings related to this behavior by running Next.js with \`NEXT_UNHANDLED_REJECTION_FILTER=silent\` environment variable. |
| |
| You can debug event listener operations by running Next.js with \`NEXT_UNHANDLED_REJECTION_FILTER=debug\` environment variable.` |
| ) |
| uninstallUnhandledRejectionFilter() |
| return process |
| } |
|
|
| debugWithTrace?.( |
| `Removing 'unhandledRejection' listener with name \`${listener.name}\`.` |
| ) |
| |
| originalProcessOff.call(process, MACGUFFIN_EVENT as any, listener) |
| const index = underlyingListeners.lastIndexOf(listener) |
| if (index > -1) { |
| debug?.(`listener found index ${index} and removed.`) |
| underlyingListeners.splice(index, 1) |
| listenerMetadata.splice(index, 1) |
| } else { |
| debug?.(`listener not found.`) |
| } |
| return process |
| } |
| |
| return originalProcessOff.call(process, event, listener) |
| } as typeof process.off) |
| } |
|
|
| |
| process.prependListener = patchWithoutReentrancy( |
| originalProcessPrependListener, |
| function (event: string | symbol, listener: (...args: any[]) => void) { |
| if (event === 'unhandledRejection') { |
| debugWithTrace?.( |
| `(Prepending) Inserting 'unhandledRejection' listener with name \`${listener.name}\` immediately following the Next.js 'unhandledRejection' filter listener.` |
| ) |
| |
| try { |
| originalProcessPrependListener.call( |
| process, |
| MACGUFFIN_EVENT as any, |
| listener |
| ) |
| } finally { |
| |
| originalProcessRemoveAllListeners.call(process, MACGUFFIN_EVENT) |
| } |
|
|
| |
| underlyingListeners.unshift( |
| listener as NodeJS.UnhandledRejectionListener |
| ) |
| listenerMetadata.unshift({ listener, once: false }) |
| return process |
| } |
| |
| return originalProcessPrependListener.call( |
| process, |
| event as any, |
| listener |
| ) |
| } as typeof process.prependListener |
| ) |
|
|
| |
| process.once = patchWithoutReentrancy(originalProcessOnce, function ( |
| event: string | symbol, |
| listener: (...args: any[]) => void |
| ) { |
| if (event === 'unhandledRejection') { |
| debugWithTrace?.( |
| `Appending 'unhandledRejection' once-listener with name \`${listener.name}\`.` |
| ) |
| |
| try { |
| originalProcessOnce.call(process, MACGUFFIN_EVENT as any, listener) |
| } finally { |
| |
| originalProcessRemoveAllListeners.call(process, MACGUFFIN_EVENT) |
| } |
| underlyingListeners.push(listener as NodeJS.UnhandledRejectionListener) |
| listenerMetadata.push({ |
| listener: listener as NodeJS.UnhandledRejectionListener, |
| once: true, |
| }) |
| return process |
| } |
| |
| return originalProcessOnce.call(process, event, listener) |
| } as typeof process.once) |
|
|
| |
| process.prependOnceListener = patchWithoutReentrancy( |
| originalProcessPrependOnceListener, |
| function (event: string | symbol, listener: (...args: any[]) => void) { |
| if (event === 'unhandledRejection') { |
| debugWithTrace?.( |
| `(Prepending) Inserting 'unhandledRejection' once-listener with name \`${listener.name}\` immediately following the Next.js 'unhandledRejection' filter listener.` |
| ) |
| |
| try { |
| originalProcessPrependOnceListener.call( |
| process, |
| MACGUFFIN_EVENT as any, |
| listener |
| ) |
| } finally { |
| |
| originalProcessRemoveAllListeners.call(process, MACGUFFIN_EVENT) |
| } |
|
|
| |
| underlyingListeners.unshift( |
| listener as NodeJS.UnhandledRejectionListener |
| ) |
| listenerMetadata.unshift({ |
| listener: listener as NodeJS.UnhandledRejectionListener, |
| once: true, |
| }) |
| return process |
| } |
| |
| return originalProcessPrependOnceListener.call( |
| process, |
| event as any, |
| listener |
| ) |
| } as typeof process.prependOnceListener |
| ) |
|
|
| |
| process.removeAllListeners = patchWithoutReentrancy( |
| originalProcessRemoveAllListeners, |
| function (event?: string | symbol) { |
| if (event === 'unhandledRejection') { |
| |
| |
| |
| |
| |
|
|
| |
|
|
| |
| |
| debugWithTrace?.( |
| `Removing all 'unhandledRejection' listeners except for the Next.js filter.` |
| ) |
|
|
| underlyingListeners.length = 0 |
| listenerMetadata.length = 0 |
| return process |
| } |
|
|
| |
| if (event !== undefined) { |
| return originalProcessRemoveAllListeners.call(process, event) |
| } |
|
|
| |
| warnUninstalledOnce?.( |
| `Uninstalling filter because \`process.removeAllListeners()\` was called. Uninstalling this filter is not recommended and will cause you to observe 'unhandledRejection' events related to intentionally aborted prerenders. |
| |
| You can silence warnings related to this behavior by running Next.js with \`NEXT_UNHANDLED_REJECTION_FILTER=silent\` environment variable. |
| |
| You can debug event listener operations by running Next.js with \`NEXT_UNHANDLED_REJECTION_FILTER=debug\` environment variable.` |
| ) |
| uninstallUnhandledRejectionFilter() |
| return originalProcessRemoveAllListeners.call(process) |
| } as typeof process.removeAllListeners |
| ) |
|
|
| |
| process.listeners = patchWithoutReentrancy( |
| originalProcessListeners, |
| function (event: string | symbol) { |
| if (event === 'unhandledRejection') { |
| debugWithTrace?.(`Retrieving all 'unhandledRejection' listeners.`) |
| return [filteringUnhandledRejectionHandler, ...underlyingListeners] |
| } |
| return originalProcessListeners.call(process, event as any) |
| } as typeof process.listeners |
| ) |
|
|
| filterInstalled = true |
| } |
|
|
| |
| |
| |
| |
| |
| function uninstallUnhandledRejectionFilter(): void { |
| if (!filterInstalled) { |
| warnWithTrace?.( |
| 'Unexpected subsequent filter uninstallation. This is a bug in Next.js' |
| ) |
| return |
| } |
|
|
| debug?.('Uninstalling Filter') |
|
|
| |
| process.on = originalProcessOn |
| process.addListener = originalProcessAddListener |
| process.once = originalProcessOnce |
| process.prependListener = originalProcessPrependListener |
| process.prependOnceListener = originalProcessPrependOnceListener |
| process.removeListener = originalProcessRemoveListener |
| process.off = originalProcessOff |
| process.removeAllListeners = originalProcessRemoveAllListeners |
| process.listeners = originalProcessListeners |
|
|
| |
| process.removeListener( |
| 'unhandledRejection', |
| filteringUnhandledRejectionHandler |
| ) |
|
|
| |
| for (const meta of listenerMetadata) { |
| if (meta.once) { |
| process.once('unhandledRejection', meta.listener) |
| } else { |
| process.addListener('unhandledRejection', meta.listener) |
| } |
| } |
|
|
| |
| filterInstalled = false |
| underlyingListeners.length = 0 |
| listenerMetadata.length = 0 |
| } |
|
|
| |
| |
| |
| function filteringUnhandledRejectionHandler( |
| reason: any, |
| promise: Promise<any> |
| ): void { |
| const capturedListenerMetadata = Array.from(listenerMetadata) |
|
|
| const workUnitStore = workUnitAsyncStorage.getStore() |
|
|
| if (workUnitStore) { |
| switch (workUnitStore.type) { |
| case 'prerender': |
| case 'prerender-client': |
| case 'prerender-runtime': { |
| const signal = workUnitStore.renderSignal |
| if (signal.aborted) { |
| |
| |
| return |
| } |
| break |
| } |
| case 'prerender-ppr': |
| case 'prerender-legacy': |
| case 'request': |
| case 'cache': |
| case 'private-cache': |
| case 'unstable-cache': |
| break |
| default: |
| workUnitStore satisfies never |
| } |
| } |
|
|
| |
| if (capturedListenerMetadata.length === 0) { |
| |
| |
| |
| |
| |
| |
| console.error('Unhandled Rejection:', reason) |
| } else { |
| try { |
| for (const meta of capturedListenerMetadata) { |
| if (meta.once) { |
| |
| const index = listenerMetadata.indexOf(meta) |
| if (index !== -1) { |
| underlyingListeners.splice(index, 1) |
| listenerMetadata.splice(index, 1) |
| } |
| } |
| const listener = meta.listener |
| listener(reason, promise) |
| } |
| } catch (error) { |
| |
| setImmediate(() => { |
| throw error |
| }) |
| } |
| } |
| } |
|
|
| |
| if (ENABLE_UHR_FILTER) { |
| installUnhandledRejectionFilter() |
| } |
|
|