schollz/croc · error

Relay connection closed while sending

Error message

Relay connection closed while sending

What it means

Thrown by CrocSocket.send() while backpressuring: the socket's bufferedAmount exceeded 4 MiB, send() entered its 10 ms wait loop, and during the wait the socket left the OPEN state without this.failure having been recorded. It marks a connection that died mid-bulk-send; a stored failure error is thrown first when present.

Source

Thrown at web/src/protocol/transport.ts:90

        socket.removeEventListener("open", onOpen);
        socket.removeEventListener("error", onError);
        signal?.removeEventListener("abort", onAbort);
      };
      socket.addEventListener("open", onOpen, { once: true });
      socket.addEventListener("error", onError, { once: true });
      signal?.addEventListener("abort", onAbort, { once: true });
    });
  }

  async send(payload: Uint8Array) {
    if (this.socket.readyState !== WebSocket.OPEN) {
      throw this.failure ?? new Error("Relay connection is not open");
    }
    while (this.socket.bufferedAmount > 4 * 1024 * 1024) {
      await new Promise((resolve) => window.setTimeout(resolve, 10));
      if (this.failure) throw this.failure;
      if (this.socket.readyState !== WebSocket.OPEN) {
        throw new Error("Relay connection closed while sending");
      }
    }
    this.socket.send(encodeFrame(payload));
  }

  async receive(skipPings = true): Promise<Uint8Array> {
    for (;;) {
      const message = await this.next();
      if (skipPings && message.byteLength === 1 && message[0] === 1) continue;
      return message;
    }
  }

  close() {
    if (
      this.socket.readyState === WebSocket.OPEN ||
      this.socket.readyState === WebSocket.CONNECTING
    ) {

View on GitHub (pinned to e25f1bdc04)

Solutions

  1. Catch at the transfer level and offer restart/resume of the whole transfer (single frames cannot be retried)
  2. Verify relay health before retrying (is the local/remote relay process alive)
  3. Raise WebSocket idle timeouts on intermediaries for long transfers
  4. Surface AbortSignal cancellation separately from socket death

Example fix

// before
await socket.send(chunk); // raw throw mid-stream

// after
try {
  await socket.send(chunk);
} catch (e) {
  if (e instanceof Error && e.message === 'Relay connection closed while sending')
    throw new TransferInterruptedError('connection lost mid-upload; restart the transfer');
  throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

function socketHealthy(s: WebSocket): boolean {
  return s.readyState === WebSocket.OPEN;
}

Try / catch

try { await socket.send(chunk); } catch (e) { if (e instanceof Error && /closed while sending|is not open/.test(e.message)) throw new TransferInterruptedError(e.message); throw e; }

Prevention

When it happens

Trigger: Sending faster than the relay drains (bufferedAmount above 4 MiB) while the WebSocket closes: network drop, relay restart, peer/proxy closing the connection, all before the error handler populates this.failure.

Common situations: Large transfers over unstable mobile links; local relay process exiting mid-transfer; proxy/LB idle timeouts closing slow WebSocket sessions.

Understand the failure class

Related errors


AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15). Data as JSON: /api/errors/12e9db8ad69d2fed. Report an issue: GitHub.