schollz/croc · error · Error

Expected sender to close the file, got ${close.t}

Error message

Expected sender to close the file, got ${close.t}

What it means

Thrown by the recipient when the sender's reply to 'close-sender' is neither 'error' nor 'close-recipient'. The per-file closing choreography requires the sender to acknowledge with close-recipient; any other message type means the sender's protocol state machine diverged (e.g. it sent the next recipientready itself, a finished message, or stale/duplicated frames arrived), so the recipient aborts before verifying and committing the file.

Source

Thrown at web/src/protocol/client.ts:799

          });
        });
        const request: RemoteFileRequestWire = {
          CurrentFileChunkRanges: [],
          FilesToTransferCurrentNum: fileIndex,
          MachineID: machineID(),
          ReconnectVersion: 0,
        };
        await sendControl(control, {
          t: "recipientready",
          b: textEncoder.encode(JSON.stringify(request)),
        }, key);
        await receivePromise;
        await sink.finalize();
        await sendControl(control, { t: "close-sender" }, key);
        const close = await receiveControl(control, key);
        if (close.t === "error") throw new Error(close.m || "Sender cancelled");
        if (close.t !== "close-recipient") {
          throw new Error(`Expected sender to close the file, got ${close.t}`);
        }
        callbacks.onStatus?.(`Verifying ${file.path}`);
        await verifySink(sink, file.hash);
        await sink.commit();
        totalTransferred = beforeFile + file.size;
        callbacks.onFileComplete?.(file.path);
      } catch (error) {
        await sink.abort();
        throw error;
      }
    }

    await sendControl(control, { t: "finished" }, key);
    const finishedMessage = await receiveControl(control, key);
    if (finishedMessage.t !== "finished") {
      throw new Error(`Expected transfer completion, got ${finishedMessage.t}`);
    }
    callbacks.onStatus?.("Transfer complete");

View on GitHub (pinned to e25f1bdc04)

Solutions

  1. Log the unexpected type (embedded in the message) and compare with the expected choreography: close-sender -> close-recipient
  2. Use unique code phrases per transfer to prevent room collisions
  3. Ensure sender implementations always reply close-recipient after each file's close-sender
  4. Upgrade both peers so the closing sequence matches
Defensive patterns

Strategy: try-catch

Type guard

function isUnexpectedSenderClose(e: unknown): boolean {
  return e instanceof Error && e.message.startsWith("Expected sender to close the file");
}

Try / catch

catch (e) {
  if (e instanceof Error && e.message.startsWith("Expected sender to close the file")) {
    // protocol divergence at file close: abort; the stray type in the message names the cause
  }
  throw e;
}

Prevention

When it happens

Trigger: Sender skips the close-recipient acknowledgement; duplicated or reordered control frames from the relay deliver an old message here; a non-conforming sender implementation replies with a different message; sender crash followed by relay garbage on the control socket.

Common situations: Custom/mock senders that end the file loop without close-recipient; relay duplication bugs; protocol drift between client versions; concurrent transfers colliding on a reused code phrase/room.

Related errors


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