{"record":{"id":"1dcd53c10d7f996c","repo":"schollz/croc","slug":"expected-recipient-to-close-the-file-got-closed","errorCode":null,"errorMessage":"Expected recipient to close the file, got ${closed.t}","messagePattern":"Expected recipient to close the file, got (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/src/protocol/client.ts","lineNumber":513,"sourceCode":"        key,\n        (fileBytes) => {\n          totalTransferred = beforeFile + fileBytes;\n          callbacks.onProgress?.({\n            fileIndex,\n            fileCount: files.length,\n            fileName: prepared.name,\n            fileBytes,\n            fileSize: prepared.size,\n            totalBytes: totalTransferred,\n            totalSize,\n          });\n        },\n        signal,\n      );\n      const closed = await receiveControl(control, key);\n      if (closed.t === \"error\") throw new Error(closed.m || \"Recipient cancelled\");\n      if (closed.t !== \"close-sender\") {\n        throw new Error(`Expected recipient to close the file, got ${closed.t}`);\n      }\n      await sendControl(control, { t: \"close-recipient\" }, key);\n      callbacks.onFileComplete?.(prepared.name);\n    }\n  } catch (error) {\n    await reportPeerError(control, key, error);\n    throw error;\n  } finally {\n    closeAll(control, data);\n  }\n}\n\ntype ActiveReceive = {\n  file: OfferedFile;\n  sink: ReceiveSink;\n  received: Set<number>;\n  bytes: number;\n  queue: Promise<void>;","sourceCodeStart":495,"sourceCodeEnd":531,"githubUrl":"https://github.com/schollz/croc/blob/e25f1bdc04f07f094d50b0a1bf67e2563944b57a/web/src/protocol/client.ts#L495-L531","documentation":"Thrown by the sender when, after sending all chunks of a file, the next control message is neither 'error' nor 'close-sender'. The recipient protocol requires it to acknowledge each completed file with close-sender; anything else (e.g. 'recipientready', 'finished', 'pake') means the recipient state machine is out of step with the sender's, so the sender aborts instead of desynchronizing further.","triggerScenarios":"Recipient skips the close-sender handshake (buggy or older implementation); recipient sends the next recipientready before acknowledging the current file; a relay or proxy injects/reorders control frames; recipient crashes and another peer reuses the room, sending an unexpected control message.","commonSituations":"Interoperating with a modified croc CLI or non-standard client that omits the per-file close handshake; version drift where the close choreography changed; testing with a mock recipient that only implements the happy path; room collisions from reusing a short code phrase.","solutions":["Log the unexpected message type (already embedded in the message) and compare against the expected choreography: recipientready -> data -> close-sender -> close-recipient","Ensure both peers run compatible versions of this web client / croc protocol; upgrade both sides","Use a fresh, unique code phrase per transfer to avoid a third peer joining the room","If writing a mock recipient, implement the full close-sender/close-recipient exchange per file"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isUnexpectedCloseMessage(e: unknown): boolean {\n  return e instanceof Error && e.message.startsWith(\"Expected recipient to close the file\");\n}","tryCatchPattern":"catch (e) {\n  if (e instanceof Error && e.message.startsWith(\"Expected recipient to close the file\")) {\n    // log e.message — it names the stray message type; abort, never continue the loop\n  }\n  throw e;\n}","preventionTips":["When implementing a recipient, always follow the per-file sequence: recipientready -> receive -> close-sender -> await close-recipient","Use fresh code phrases per transfer to prevent foreign peers from injecting control frames","Pin both peers to the same client version so the close choreography cannot drift"],"tags":["protocol","state-machine","peer-mismatch"],"backgroundTag":null,"analyzedSha":"e25f1bdc04f07f094d50b0a1bf67e2563944b57a","analyzedAt":"2026-08-15T12:53:39.096Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}