Skip to content

Fix socket leak (CLOSE_WAIT) in NetworkTransport when reconnection is disabled - #283

Open
skirrellyjones wants to merge 1 commit into
modelcontextprotocol:mainfrom
skirrellyjones:fix/network-transport-close-wait-leak
Open

Fix socket leak (CLOSE_WAIT) in NetworkTransport when reconnection is disabled#283
skirrellyjones wants to merge 1 commit into
modelcontextprotocol:mainfrom
skirrellyjones:fix/network-transport-close-wait-leak

Conversation

@skirrellyjones

Copy link
Copy Markdown

Fixes #282.

Problem

When reconnectionConfig.enabled == false and the receive loop's
NWConnection terminates for any reason — including the peer closing
gracefully — the loop finishes the message stream but never calls
connection.cancel(). NWConnection.state never transitions to
.cancelled or .failed on its own after a peer-initiated FIN, so
nothing downstream (e.g. code watching connection.state to decide when
to clean up) fires either. The socket is left stuck in CLOSE_WAIT
until the process itself exits.

This hits hardest for servers that intentionally disable reconnection
for their per-client connections (the correct choice — a server
shouldn't try to reconnect to a client). Every client disconnect leaks
one file descriptor permanently.

Fix

Call connection.cancel() in both non-reconnecting "give up" branches
of the receive loop, mirroring what the reconnecting branches already do
a few lines above (they call self.connection.cancel() before
retrying).

Testing

  • swift build succeeds.
  • swift test --filter NetworkTransportTests — all 16 existing tests
    pass, including "Resource Cleanup" and "Disconnect During Receive".
  • Verified against a real server (mattt/iMCP,
    which sets reconnectionConfig: .disabled for its server-side
    connections): before this fix, a burst of ~250 client connect/disconnect
    cycles left 247+ sockets in CLOSE_WAIT within 10 seconds and had
    already crashed the process once from file-descriptor exhaustion
    (SIGABRT, "Too many open files"). After the fix: 0 leaked sockets,
    and the connections that live through the burst work correctly (I
    exercised a live MCP tools/call over one and got a real response
    back).

… disabled

When reconnectionConfig.enabled is false and the receive loop's connection
terminates for any reason -- including the peer closing gracefully -- the
loop finishes the message stream without calling connection.cancel().
NWConnection.state never transitions to .cancelled or .failed on its own
after a peer-initiated FIN, so nothing else cleans up the connection either.
The socket is left stuck in CLOSE_WAIT until the process exits.

The reconnecting branches already call connection.cancel() before
retrying; this adds the same call to the two non-reconnecting "give up"
branches so the underlying socket is always released.

Verified against a server using reconnectionConfig: .disabled for
per-client connections (mattt/iMCP): before this fix, a burst of ~250
client connect/disconnect cycles left 247+ sockets in CLOSE_WAIT within
10 seconds; after the fix, 0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NetworkTransport leaks the underlying socket (CLOSE_WAIT) when reconnection is disabled and the receive loop terminates

1 participant