{"record":{"id":"9814a13dbf2aa560","repo":"schollz/croc","slug":"expected-transfer-completion-got-finishedmessag","errorCode":null,"errorMessage":"Expected transfer completion, got ${finishedMessage.t}","messagePattern":"Expected transfer completion, got (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/src/protocol/client.ts","lineNumber":815,"sourceCode":"        if (close.t === \"error\") throw new Error(close.m || \"Sender cancelled\");\n        if (close.t !== \"close-recipient\") {\n          throw new Error(`Expected sender to close the file, got ${close.t}`);\n        }\n        callbacks.onStatus?.(`Verifying ${file.path}`);\n        await verifySink(sink, file.hash);\n        await sink.commit();\n        totalTransferred = beforeFile + file.size;\n        callbacks.onFileComplete?.(file.path);\n      } catch (error) {\n        await sink.abort();\n        throw error;\n      }\n    }\n\n    await sendControl(control, { t: \"finished\" }, key);\n    const finishedMessage = await receiveControl(control, key);\n    if (finishedMessage.t !== \"finished\") {\n      throw new Error(`Expected transfer completion, got ${finishedMessage.t}`);\n    }\n    callbacks.onStatus?.(\"Transfer complete\");\n  } catch (error) {\n    await reportPeerError(control, key, error);\n    throw error;\n  } finally {\n    receiver?.stop();\n    closeAll(control, data);\n  }\n}\n","sourceCodeStart":797,"sourceCodeEnd":826,"githubUrl":"https://github.com/schollz/croc/blob/e25f1bdc04f07f094d50b0a1bf67e2563944b57a/web/src/protocol/client.ts#L797-L826","documentation":"Thrown by the recipient after sending its final 'finished' message when the sender does not echo 'finished' back. This final exchange confirms both sides consider the whole transfer complete; any other message (or a stale frame) means the sender's overall state does not match — it may still be in a file loop, have reported an error earlier, or the control channel delivered unexpected data. The transfer is aborted after all files were received and committed.","triggerScenarios":"Sender sends an extra control message (e.g. another recipientready or error) instead of finished; relay delivers a duplicated stale frame after the last file; sender implementation omits the final finished echo; desynchronized message counters between peers.","commonSituations":"Version-skewed sender with a different termination sequence; mock senders that skip the final handshake; relay frame duplication at connection teardown; a race where the sender's error report from the last file arrives after the recipient already moved on.","solutions":["Log the received type to identify the stray message and its origin in the sender's flow","Verify the sender's loop sends finished immediately upon receiving the recipient's finished (line 474-477 of the sender path)","Upgrade both peers so termination choreography matches","Retry with a fresh code phrase; room collisions can inject foreign frames at completion time"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isUnexpectedCompletion(e: unknown): boolean {\n  return e instanceof Error && e.message.startsWith(\"Expected transfer completion\");\n}","tryCatchPattern":"catch (e) {\n  if (e instanceof Error && e.message.startsWith(\"Expected transfer completion\")) {\n    // files were committed but the final handshake failed: treat data as delivered, log the stray type\n  }\n  throw e;\n}","preventionTips":["If implementing a sender, always echo finished immediately upon receiving finished","Note that this error fires AFTER all files were verified and committed — your data is safe; report it as a protocol anomaly, not data loss","Use fresh code phrases per transfer to avoid foreign frames at completion time"],"tags":["protocol","state-machine","termination"],"backgroundTag":null,"analyzedSha":"e25f1bdc04f07f094d50b0a1bf67e2563944b57a","analyzedAt":"2026-08-15T12:53:39.096Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}