schollz/croc · error · Error
Expected recipient to close the file, got ${closed.t}
Error message
Expected recipient to close the file, got ${closed.t} What it means
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.
Source
Thrown at web/src/protocol/client.ts:513
key,
(fileBytes) => {
totalTransferred = beforeFile + fileBytes;
callbacks.onProgress?.({
fileIndex,
fileCount: files.length,
fileName: prepared.name,
fileBytes,
fileSize: prepared.size,
totalBytes: totalTransferred,
totalSize,
});
},
signal,
);
const closed = await receiveControl(control, key);
if (closed.t === "error") throw new Error(closed.m || "Recipient cancelled");
if (closed.t !== "close-sender") {
throw new Error(`Expected recipient to close the file, got ${closed.t}`);
}
await sendControl(control, { t: "close-recipient" }, key);
callbacks.onFileComplete?.(prepared.name);
}
} catch (error) {
await reportPeerError(control, key, error);
throw error;
} finally {
closeAll(control, data);
}
}
type ActiveReceive = {
file: OfferedFile;
sink: ReceiveSink;
received: Set<number>;
bytes: number;
queue: Promise<void>;View on GitHub (pinned to e25f1bdc04)
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
Defensive patterns
Strategy: try-catch
Type guard
function isUnexpectedCloseMessage(e: unknown): boolean {
return e instanceof Error && e.message.startsWith("Expected recipient to close the file");
} Try / catch
catch (e) {
if (e instanceof Error && e.message.startsWith("Expected recipient to close the file")) {
// log e.message — it names the stray message type; abort, never continue the loop
}
throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Expected sender to close the file, got ${close.t}
- Unexpected peer message: ${message.t}
- Recipient requested an unknown file
- Received file data before it was requested
- Sender did not provide file metadata
AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15).
Data as JSON: /api/errors/1dcd53c10d7f996c.
Report an issue: GitHub.