32 lines
1.6 KiB
TypeScript
32 lines
1.6 KiB
TypeScript
// Process-level safety net for the production Bun runtime.
|
|
//
|
|
// @hono/node-server (pulled in via srvx) runs a request-body "drain" timer:
|
|
// when a response is sent without the request body being fully read, a
|
|
// setTimeout(forceClose) eventually calls incoming.socket.destroySoon().
|
|
// Under the Bun build that socket is a unenv polyfill whose destroySoon()
|
|
// calls this.destroy() — and unenv's getDuplex() loses the non-enumerable
|
|
// `destroy` method (Object.assign skips it), so the call throws
|
|
// `TypeError: this.destroy is not a function`. The throw originates in a
|
|
// detached timer with no try/catch, so it surfaces as an uncaughtException
|
|
// and Bun's default is to exit(1) — taking the whole server down and putting
|
|
// it into a deploy-restart crash loop. /mcp triggers it on every
|
|
// unauthenticated POST (it 401s before reading the body), so an MCP client's
|
|
// connect attempts alone keep killing the origin.
|
|
//
|
|
// This guard keeps the process alive when a detached timer throws. It is a
|
|
// containment net, not the cure — the real fix is to stop bundling unenv's
|
|
// node stubs under Bun so a native socket (with a working destroy) is used.
|
|
//
|
|
// Production runtime only: during the build-time prerender pass and in dev we
|
|
// want errors to surface (fail the build / show the crash) rather than be
|
|
// swallowed.
|
|
export default defineNitroPlugin(() => {
|
|
if (import.meta.dev || import.meta.prerender) return
|
|
|
|
process.on('uncaughtException', (err) => {
|
|
console.error('[process-guard] uncaughtException (kept alive):', err)
|
|
})
|
|
process.on('unhandledRejection', (reason) => {
|
|
console.error('[process-guard] unhandledRejection (kept alive):', reason)
|
|
})
|
|
})
|