diff --git a/packages/server/src/server/streamableHttp.ts b/packages/server/src/server/streamableHttp.ts index 31053f35c..f7d12598d 100644 --- a/packages/server/src/server/streamableHttp.ts +++ b/packages/server/src/server/streamableHttp.ts @@ -899,15 +899,34 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { return undefined; } + private _closing = false; + async close(): Promise { - // Close all SSE connections - for (const { cleanup } of this._streamMapping.values()) { - cleanup(); + // Guard against re-entrant calls. When onclose() triggers the + // Protocol layer to call close() again, this prevents infinite + // recursion that causes a stack overflow with many transports. + if (this._closing) { + return; } - this._streamMapping.clear(); + this._closing = true; - // Clear any pending responses + // Snapshot and clear before iterating to avoid issues with + // cleanup callbacks that modify the map during iteration. + const streams = [...this._streamMapping.values()]; + this._streamMapping.clear(); this._requestResponseMap.clear(); + + // Close all SSE connections with error isolation so one + // failing cleanup doesn't prevent others from running. + for (const { cleanup } of streams) { + try { + cleanup(); + } catch { + // Individual stream cleanup failures should not + // prevent other streams from being cleaned up. + } + } + this.onclose?.(); }