{"record":{"id":"eaa47b68561a6b7e","repo":"wavetermdev/waveterm","slug":"stream-file-protocol-error-first-pk-fileinfo-is-e","errorCode":null,"errorMessage":"stream file protocol error, first pk fileinfo is empty","messagePattern":"stream file protocol error, first pk fileinfo is empty","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/remote/fileshare/fsutil/fsutil.go","lineNumber":91,"sourceCode":"\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn fmt.Errorf(\"context cancelled: %v\", context.Cause(ctx))\n\t\tcase respUnion, ok := <-readCh:\n\t\t\tif !ok {\n\t\t\t\tdrain = false\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif respUnion.Error != nil {\n\t\t\t\treturn respUnion.Error\n\t\t\t}\n\t\t\tresp := respUnion.Response\n\t\t\tif firstPk {\n\t\t\t\tfirstPk = false\n\t\t\t\t// first packet has the fileinfo\n\t\t\t\tif resp.Info == nil {\n\t\t\t\t\treturn fmt.Errorf(\"stream file protocol error, first pk fileinfo is empty\")\n\t\t\t\t}\n\t\t\t\tfileData = &resp\n\t\t\t\tif fileData.Info.IsDir {\n\t\t\t\t\tisDir = true\n\t\t\t\t}\n\t\t\t\tfileInfoCallback(*fileData.Info)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif isDir {\n\t\t\t\tif len(resp.Entries) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif resp.Data64 != \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"stream file protocol error, directory entry has data\")\n\t\t\t\t}\n\t\t\t\tif err := dirCallback(resp.Entries); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/pkg/remote/fileshare/fsutil/fsutil.go#L73-L109","documentation":"The fileshare stream protocol requires the first packet from the remote to carry the FileInfo (resp.Info). ReadFileStream enforces this; if the first packet arrives with a nil Info it means the remote violated the protocol, so the stream is aborted with this error.","triggerScenarios":"ReadStreamToFileData streaming a path where the remote responder sends a first packet without file info — the remote file vanished between stat and stream, or the remote side (older/mismatched wsh) does not populate Info.","commonSituations":"File deleted or renamed on remote right after the read started; version mismatch between client and remote wsh fileshare implementations; race with another writer replacing the file.","solutions":["Retry the read — a deletion race usually succeeds on a second attempt","Verify the file still exists on the remote before streaming","Upgrade wsh on both client and remote to matching versions","Check remote-side logs for responder errors at stream start"],"exampleFix":"// before\ndata, err := fsutil.ReadStreamToFileData(ctx, ctl, path) // protocol error\n// after\nif exists, _ := remoteFileExists(ctx, ctl, path); !exists {\n    return fmt.Errorf(\"remote file %q gone, skipping\", path)\n}\ndata, err := fsutil.ReadStreamToFileData(ctx, ctl, path)","handlingStrategy":"retry","validationCode":"if ok, _ := remoteExists(ctx, ctl, path); !ok {\n    return fmt.Errorf(\"remote path %q no longer exists\", path)\n}","typeGuard":"func hasFileInfo(resp *fileshare.StreamResponse) bool { return resp != nil && resp.Info != nil }","tryCatchPattern":"data, err := fsutil.ReadStreamToFileData(ctx, ctl, path)\nif err != nil && strings.Contains(err.Error(), \"first pk fileinfo is empty\") {\n    time.Sleep(250 * time.Millisecond) // deletion race: retry once\n    data, err = fsutil.ReadStreamToFileData(ctx, ctl, path)\n}","preventionTips":["Match wsh versions on client and remote","Avoid deleting/moving files while they are being streamed","Check existence immediately before streaming in racy workflows"],"tags":["go","protocol","stream"],"backgroundTag":"stream-protocol-violation","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}