fix mcp crash loop from node-mock-http mock socket missing destroy()
This commit is contained in:
parent
0ccc8e9e55
commit
9772f18750
2 changed files with 60 additions and 0 deletions
32
server/plugins/00-process-guards.ts
Normal file
32
server/plugins/00-process-guards.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// 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)
|
||||
})
|
||||
})
|
||||
28
server/plugins/01-patch-mock-socket.ts
Normal file
28
server/plugins/01-patch-mock-socket.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { IncomingMessage } from 'node-mock-http'
|
||||
|
||||
// Backfill the missing `destroy()` on node-mock-http's mock Socket.
|
||||
//
|
||||
// The bun preset serves via Bun.serve and fakes Node's HTTP API with
|
||||
// node-mock-http. Its mock Socket exposes destroySoon() — which calls
|
||||
// this.destroy() — but is missing destroy() itself: an
|
||||
// Object.assign-over-non-enumerable-methods bug drops it from the prototype.
|
||||
// The MCP SDK bridges requests through @hono/node-server, whose request-body
|
||||
// drain calls socket.destroySoon() ~500ms after each request, so the missing
|
||||
// destroy() throws `TypeError: this.destroy is not a function` and — coming
|
||||
// from a detached timer — takes the whole process down (deploy-restart crash
|
||||
// loop). 00-process-guards.ts is the backstop; this removes the crash at the
|
||||
// source so it never fires.
|
||||
//
|
||||
// node-mock-http is already at its latest (1.0.4) and the MCP SDK pins
|
||||
// @hono/node-server to 1.x (whose newest still calls destroySoon() unguarded),
|
||||
// so there's no upgrade that fixes this — hence the runtime prototype patch.
|
||||
// Purely additive (only fills in a missing method), so it's safe in every mode.
|
||||
export default defineNitroPlugin(() => {
|
||||
const socketProto = Object.getPrototypeOf(new IncomingMessage().socket)
|
||||
if (socketProto && typeof socketProto.destroy !== 'function') {
|
||||
socketProto.destroy = function (this: { destroyed: boolean }) {
|
||||
this.destroyed = true
|
||||
return this
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Reference in a new issue