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
- Catch at the transfer level and offer restart/resume of the whole transfer (single frames cannot be retried)
- Verify relay health before retrying (is the local/remote relay process alive)
- Raise WebSocket idle timeouts on intermediaries for long transfers
- 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
- Design transfers as restartable units; retry the whole transfer, not single frames
- Keep the relay process alive for the transfer's full duration
- Raise proxy/LB idle timeouts for long WebSocket sessions
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
- Connection failures: ECONNREFUSED, ECONNRESET, and friends — why connections get refused, reset, or dropped.
Related errors
- Gateway URL must use ws:// or wss://
- Relay returned an invalid port list: ${banner}
- Relay rejected the connection: ${response}
- Relay could not open the room: ${confirmation}
- Sender did not confirm the croc PAKE handshake
AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15).
Data as JSON: /api/errors/12e9db8ad69d2fed.
Report an issue: GitHub.