denoland/deno · error · TypeError

Socket has no handle - cannot upgrade

Error message

Socket has no handle - cannot upgrade

What it means

Also on the node:http upgrade path: after confirming the socket is alive, Deno reads nodeSocket._handle (the libuv stream handle) to take over the raw TCP stream via takeStream(). If _handle is null/absent - typically because the handle was already closed/detached - there is nothing to take and the upgrade is refused.

Source

Thrown at ext/http/02_websocket.ts:162

    const nodeSocket = options.socket;

    // Build the 101 response from r.headerList so the headers stay in
    // sync with the header-list built above (protocol negotiation, etc.).
    let responseHead = "HTTP/1.1 101 Switching Protocols\r\n";
    for (let i = 0; i < r.headerList.length; i++) {
      const { 0: name, 1: value } = r.headerList[i];
      responseHead += `${name}: ${value}\r\n`;
    }
    responseHead += "\r\n";

    if (nodeSocket.destroyed) {
      throw new TypeError(
        "Socket is already destroyed - cannot upgrade to WebSocket",
      );
    }
    const handle = nodeSocket._handle;
    if (!handle) {
      throw new TypeError("Socket has no handle - cannot upgrade");
    }
    if (typeof handle.takeStream !== "function") {
      throw new TypeError(
        "Socket is not a TCP socket - only TCP connections can be upgraded to WebSocket",
      );
    }

    // Extra bytes that were already buffered (e.g., from the upgrade
    // request body that arrived with the headers)
    const extraBytes = options.head || new Uint8Array(0);

    // Defer setup so the caller can attach event handlers (onopen,
    // onmessage, etc.) before events fire.
    (async () => {
      try {
        // Wait for the 101 response to fully flush before taking the
        // stream. A fire-and-forget write could leave data in the
        // internal_write_queue that would be orphaned once we detach

View on GitHub (pinned to 89f33cbef2)

Solutions

  1. Only pass genuine, live node sockets from the 'upgrade' event; guard with socket?._handle before calling.
  2. Ensure nothing destroyed/closed the socket earlier in the pipeline (destroyed check alone is not enough - handle can be gone).
  3. In tests, use real sockets (createServer + client connect) rather than mocks.

Example fix

// before
server.on("upgrade", (req, socket, head) => {
  const { response } = Deno.upgradeWebSocket(req, { socket, head }); // _handle already null
});

// after
server.on("upgrade", (req, socket, head) => {
  if (socket.destroyed || !socket._handle) return socket.destroy();
  const { socket: ws } = Deno.upgradeWebSocket(req, { socket, head });
  ws.on("open", () => console.log("ws open"));
});
Defensive patterns

Strategy: validation

Validate before calling

server.on("upgrade", (req, socket, head) => {
  if (socket.destroyed || !(socket as any)._handle) return socket.destroy();
  Deno.upgradeWebSocket(req, { socket, head });
});

Type guard

function hasNodeHandle(s: unknown): boolean { const sock = s as { _handle?: unknown } | null; return !!sock && !!sock._handle; }

Try / catch

try { Deno.upgradeWebSocket(req, { socket, head }); } catch (e) { if (e instanceof TypeError && e.message.includes("no handle")) { socket.destroy(); return; } throw e; }

Prevention

When it happens

Trigger: Passing a socket whose libuv handle was already closed (net.Socket after an error where Node nulls _handle); passing a mock/socket-like object in tests that lacks _handle; passing a socket obtained from a different runtime or an already-consumed socket from a previous upgrade attempt.

Common situations: Unit tests substituting fake sockets for node net.Socket; error paths where socket.destroy() ran (destroy clears the handle) before the guard-visible destroyed flag; interop layers constructing sockets manually; racing a second upgrade on the same socket after the first detached the handle.

Related errors


AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16). Data as JSON: /api/errors/3008da4caf609962. Report an issue: GitHub.