Skip to content

Commit dac3d4e

Browse files
committed
fix: prevent stack overflow in transport close with re-entrancy guard
When 15-25+ transports close simultaneously, close() -> onclose() triggers the Protocol layer to call close() again recursively, causing a stack overflow. The process stays alive but becomes unresponsive. Three problems in the original close(): 1. No re-entrancy guard: onclose() can call back into close() 2. Iterating _streamMapping while cleanup callbacks delete from it 3. No error isolation: one failing cleanup prevents others The fix: - Adds _closing boolean guard to prevent recursive calls - Snapshots stream values into an array and clears the map before iterating, avoiding mutation-during-iteration - Wraps each cleanup() in try/catch so one failure doesn't block remaining stream cleanups - Calls onclose() only after all cleanup is complete Note: two prior PRs (#1700, #1705) attempted similar fixes but were closed. This implementation combines the best elements of both: the re-entrancy guard from #1700 and the snapshot-before-clear pattern from #1705, with the addition of error isolation. Closes #1699
1 parent cce3ac7 commit dac3d4e

File tree

1 file changed

+24
-5
lines changed

1 file changed

+24
-5
lines changed

packages/server/src/server/streamableHttp.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -899,15 +899,34 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
899899
return undefined;
900900
}
901901

902+
private _closing = false;
903+
902904
async close(): Promise<void> {
903-
// Close all SSE connections
904-
for (const { cleanup } of this._streamMapping.values()) {
905-
cleanup();
905+
// Guard against re-entrant calls. When onclose() triggers the
906+
// Protocol layer to call close() again, this prevents infinite
907+
// recursion that causes a stack overflow with many transports.
908+
if (this._closing) {
909+
return;
906910
}
907-
this._streamMapping.clear();
911+
this._closing = true;
908912

909-
// Clear any pending responses
913+
// Snapshot and clear before iterating to avoid issues with
914+
// cleanup callbacks that modify the map during iteration.
915+
const streams = Array.from(this._streamMapping.values());
916+
this._streamMapping.clear();
910917
this._requestResponseMap.clear();
918+
919+
// Close all SSE connections with error isolation so one
920+
// failing cleanup doesn't prevent others from running.
921+
for (const { cleanup } of streams) {
922+
try {
923+
cleanup();
924+
} catch {
925+
// Individual stream cleanup failures should not
926+
// prevent other streams from being cleaned up.
927+
}
928+
}
929+
911930
this.onclose?.();
912931
}
913932

0 commit comments

Comments
 (0)