{"record":{"id":"12e9db8ad69d2fed","repo":"schollz/croc","slug":"relay-connection-closed-while-sending","errorCode":null,"errorMessage":"Relay connection closed while sending","messagePattern":"Relay connection closed while sending","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"web/src/protocol/transport.ts","lineNumber":90,"sourceCode":"        socket.removeEventListener(\"open\", onOpen);\n        socket.removeEventListener(\"error\", onError);\n        signal?.removeEventListener(\"abort\", onAbort);\n      };\n      socket.addEventListener(\"open\", onOpen, { once: true });\n      socket.addEventListener(\"error\", onError, { once: true });\n      signal?.addEventListener(\"abort\", onAbort, { once: true });\n    });\n  }\n\n  async send(payload: Uint8Array) {\n    if (this.socket.readyState !== WebSocket.OPEN) {\n      throw this.failure ?? new Error(\"Relay connection is not open\");\n    }\n    while (this.socket.bufferedAmount > 4 * 1024 * 1024) {\n      await new Promise((resolve) => window.setTimeout(resolve, 10));\n      if (this.failure) throw this.failure;\n      if (this.socket.readyState !== WebSocket.OPEN) {\n        throw new Error(\"Relay connection closed while sending\");\n      }\n    }\n    this.socket.send(encodeFrame(payload));\n  }\n\n  async receive(skipPings = true): Promise<Uint8Array> {\n    for (;;) {\n      const message = await this.next();\n      if (skipPings && message.byteLength === 1 && message[0] === 1) continue;\n      return message;\n    }\n  }\n\n  close() {\n    if (\n      this.socket.readyState === WebSocket.OPEN ||\n      this.socket.readyState === WebSocket.CONNECTING\n    ) {","sourceCodeStart":72,"sourceCodeEnd":108,"githubUrl":"https://github.com/schollz/croc/blob/e25f1bdc04f07f094d50b0a1bf67e2563944b57a/web/src/protocol/transport.ts#L72-L108","documentation":"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.","triggerScenarios":"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.","commonSituations":"Large transfers over unstable mobile links; local relay process exiting mid-transfer; proxy/LB idle timeouts closing slow WebSocket sessions.","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"],"exampleFix":"// before\nawait socket.send(chunk); // raw throw mid-stream\n\n// after\ntry {\n  await socket.send(chunk);\n} catch (e) {\n  if (e instanceof Error && e.message === 'Relay connection closed while sending')\n    throw new TransferInterruptedError('connection lost mid-upload; restart the transfer');\n  throw e;\n}","handlingStrategy":"retry","validationCode":"function socketHealthy(s: WebSocket): boolean {\n  return s.readyState === WebSocket.OPEN;\n}","typeGuard":null,"tryCatchPattern":"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; }","preventionTips":["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"],"tags":["websocket","backpressure","network","relay"],"backgroundTag":null,"analyzedSha":"e25f1bdc04f07f094d50b0a1bf67e2563944b57a","analyzedAt":"2026-08-15T12:53:39.096Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}