Skip to content

fix(server): close socket fd leaks on connection errors#5

Open
missuo wants to merge 1 commit intoMercuryWorkshop:devfrom
missuo:fix/socket-fd-leak
Open

fix(server): close socket fd leaks on connection errors#5
missuo wants to merge 1 commit intoMercuryWorkshop:devfrom
missuo:fix/socket-fd-leak

Conversation

@missuo
Copy link

@missuo missuo commented Mar 22, 2026

Summary

This PR fixes multiple socket file descriptor leaks that occur when connections are abnormally terminated.

Background

We discovered this issue while running Asspp (the web version), which uses @mercuryworkshop/wisp-js to proxy browser-to-Apple-server TCP connections via WebSocket on the /wisp/ path. Every Apple API request from the browser goes through the Wisp proxy.

On a VPS with 111GB disk, the disk usage would reach 100% every few days. Investigation using lsof +L1 revealed that the node dist/index.js process was holding file descriptors to 216.8GB of deleted-but-unreleased files — invisible to du but occupying real disk space reported by df. These were accumulated socket fds from connections that were never properly closed.

After auditing the Asspp codebase, we confirmed its own file operations (chunked downloads, sinf injection, etc.) all have proper cleanup. The leak source was traced to @mercuryworkshop/wisp-js.

Root Causes

1. NodeTCPSocket.close() uses socket.end() instead of socket.destroy()src/server/net.mjs

This is the primary cause of the fd leak. socket.end() only half-closes the connection (sends FIN) and waits for the remote side to close as well. If the remote never responds or the connection is in a broken state, the fd is held indefinitely. In a high-throughput proxy scenario, these accumulate rapidly.

Fix: Changed to socket.destroy() for immediate fd release.

2. WSProxyConnection has no cross-directional cleanup — src/server/wsproxy.mjs

When tcp_to_ws() errors, only the WebSocket is closed — the TCP socket leaks. When ws_to_tcp() errors, only the TCP socket is closed — the WebSocket leaks. There is no centralized cleanup.

Fix: Added a cleanup() method with a closed flag to ensure both the TCP socket and WebSocket are always released together, regardless of which direction fails.

3. UDP socket error handler doesn't call socket.close()src/server/net.mjs

The error handler sets this.socket = null without calling close() on the dgram socket, leaking the underlying OS socket resource.

Fix: Added this.socket.close() before nulling the reference.

4. TCP connect() error handler doesn't reject the promise — src/server/net.mjs

The "error" event handler only logs a warning but never calls reject(). If a connection error occurs, the promise can hang indefinitely, keeping the socket fd alive.

Fix: Added reject(error) when !this.connected.

5. AsyncWebSocket.connect() missing onerror handler — src/websocket.mjs

No onerror handler means WebSocket connection errors don't reject the promise, which can hang forever.

Fix: Added onerror handler that rejects when not yet connected.

6. create_connection catch block doesn't call cleanup()src/server/http.mjs

If ServerConnection.setup() throws after starting the ping setInterval, the catch block only calls ws.close() — the interval timer and any partially-created streams are never cleaned up.

Fix: Track the ServerConnection instance and call cleanup() in the catch block.

Diagnostic Method

# Compare df vs du to detect deleted-but-unreleased files
df -h /
du -sh /

# Find which processes hold deleted files and how much space
lsof +L1 | awk '$7+0>1000000{a[$1" (pid:"$2")"]+=($7)} END{for(k in a) printf "%s\t%.1fG\n", k, a[k]/1073741824}' | sort -t$'\t' -k2 -nr

Changed Files

  • src/server/net.mjsdestroy() instead of end(), TCP error rejects promise, UDP error closes socket
  • src/server/wsproxy.mjs — centralized cleanup() method for bidirectional resource release
  • src/websocket.mjsonerror handler in AsyncWebSocket.connect()
  • src/server/http.mjs — call cleanup() on ServerConnection in catch block

Use socket.destroy() instead of socket.end() to immediately release
file descriptors. Add proper error handling and cross-directional
cleanup in WSProxyConnection, TCP/UDP sockets, and AsyncWebSocket
to prevent resource leaks on abnormal disconnections.
@ading2210 ading2210 changed the base branch from master to dev March 23, 2026 02:28
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.

1 participant