doxa-maps-cdn/server/plugins/00-process-guards.ts

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)
})
})