{"record":{"id":"3e8c1b35c97ccc5a","repo":"Eugeny/tabby","slug":"file-handle-is-closed","errorCode":null,"errorMessage":"File handle is closed","messagePattern":"File handle is closed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tabby-ssh/src/session/sftp.ts","lineNumber":34,"sourceCode":"}\n\nexport class SFTPFileHandle {\n    position = 0\n\n    constructor (\n        private inner: russh.SFTPFile|null,\n    ) { }\n\n    async read (): Promise<Uint8Array> {\n        if (!this.inner) {\n            return Promise.resolve(new Uint8Array(0))\n        }\n        return this.inner.read(256 * 1024)\n    }\n\n    async write (chunk: Uint8Array): Promise<void> {\n        if (!this.inner) {\n            throw new Error('File handle is closed')\n        }\n        await this.inner.writeAll(chunk)\n    }\n\n    async close (): Promise<void> {\n        await this.inner?.shutdown()\n        this.inner = null\n    }\n}\n\nexport class SFTPSession {\n    get closed$ (): Observable<void> { return this.closed }\n    private closed = new Subject<void>()\n    private logger: Logger\n\n    constructor (private sftp: russh.SFTP, injector: Injector) {\n        this.logger = injector.get(LogService).create('sftp')\n        sftp.closed$.subscribe(() => {","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/Eugeny/tabby/blob/14e2d60b9b6dee84a53c37f05eefeb803787de04/tabby-ssh/src/session/sftp.ts#L16-L52","documentation":"Thrown by the SFTPFileAdapter's `write` method when `this.inner` (the underlying `russh.SFTPFile` handle) is null - i.e. after `close()` has already been called on this adapter. `close()` shuts down the inner handle and nulls it; any subsequent write is rejected because there is no live file handle to write to.","triggerScenarios":"Calling `write(chunk)` after `close()` on the same SFTPFileAdapter; a race where close completes before a queued write runs; reuse of a file object whose stream was already ended.","commonSituations":"Stream pipeline where `close` is invoked by completion/cancel logic but a buffered write still drains; error handler that closes the file then a finally block attempts to flush; double-close followed by a write.","solutions":["Track lifecycle: do not enqueue writes after close; use a `closed` flag and skip or buffer-and-drain before closing.","Order operations so all writes await before calling `close()` (await each `write` in the producer before ending the stream).","Guard writes with a null check that returns early/throws a typed 'closed' error the caller can distinguish from real I/O errors.","Use a single owner for the file handle to prevent concurrent close/write races."],"exampleFix":"// before\nasync write (chunk: Uint8Array): Promise<void> {\n    if (!this.inner) throw new Error('File handle is closed')\n    await this.inner.writeAll(chunk)\n}\n\n// caller - serialize close after writes drain\nfor (const chunk of chunks) await file.write(chunk)\nawait file.close()  // only close after all writes settled","handlingStrategy":"validation","validationCode":"function isFileHandleOpen (adapter: { inner: unknown }): boolean {\n    return adapter.inner !== null\n}\n\nif (!isFileHandleOpen(fileAdapter)) {\n    throw new Error('File handle is closed; open a new one before writing')\n}","typeGuard":"function isFileHandleOpen<T> (a: { inner: T | null }): a is { inner: T } {\n    return a.inner !== null\n}","tryCatchPattern":"try {\n    await file.write(chunk)\n} catch (e) {\n    if (e instanceof Error && e.message === 'File handle is closed') {\n        // reopen or skip; do not silently lose data\n        throw e\n    }\n    throw e\n}","preventionTips":["Await all writes before calling close() to drain the producer fully.","Use a single owner for the file handle to avoid concurrent close/write.","Track a `closed` flag and skip enqueuing writes after close.","Order finally blocks so close runs only after writes settle."],"tags":["sftp","ssh","file-handle","lifecycle","race-condition"],"backgroundTag":null,"analyzedSha":"14e2d60b9b6dee84a53c37f05eefeb803787de04","analyzedAt":"2026-08-12T11:46:48.773Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}