{"record":{"id":"5d786e0ac35494a2","repo":"Eugeny/tabby","slug":"cannot-remove-remote-port-forward-before-auth","errorCode":null,"errorMessage":"Cannot remove remote port forward before auth","messagePattern":"Cannot remove remote port forward before auth","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tabby-ssh/src/session/ssh.ts","lineNumber":839,"sourceCode":"                await this.ssh.forwardTCPPort(fw.host, fw.port)\n            } catch (err) {\n                // eslint-disable-next-line @typescript-eslint/no-base-to-string\n                this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`)\n                return\n            }\n            this.emitServiceMessage(colors.bgGreen.black(' <- ') + ` Forwarded ${fw}`)\n            this.forwardedPorts.push(fw)\n        }\n    }\n\n    async removePortForward (fw: ForwardedPort): Promise<void> {\n        if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {\n            fw.stopLocalListener()\n            this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)\n        }\n        if (fw.type === PortForwardType.Remote) {\n            if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {\n                throw new Error('Cannot remove remote port forward before auth')\n            }\n            this.ssh.stopForwardingTCPPort(fw.host, fw.port)\n            this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)\n        }\n        this.emitServiceMessage(`Stopped forwarding ${fw}`)\n    }\n\n    async destroy (): Promise<void> {\n        this.logger.info('Destroying')\n        this.willDestroy.next()\n        this.willDestroy.complete()\n        this.serviceMessage.complete()\n        this.ssh.disconnect()\n    }\n\n    async openShellChannel (options: { x11: boolean }): Promise<russh.Channel> {\n        if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {\n            throw new Error('Cannot open shell channel before auth')","sourceCodeStart":821,"sourceCodeEnd":857,"githubUrl":"https://github.com/Eugeny/tabby/blob/14e2d60b9b6dee84a53c37f05eefeb803787de04/tabby-ssh/src/session/ssh.ts#L821-L857","documentation":"Thrown by removePortForward() when removing a Remote-type forward while this.ssh is not an AuthenticatedSSHClient. Removing a remote forward calls stopForwardingTCPPort on the authenticated client, which is unavailable before auth or after disconnect.","triggerScenarios":"removePortForward(fw) called with fw.type === PortForwardType.Remote when this.ssh is a plain SSHClient (not yet authenticated) or after destroy()/disconnect(). Typically a teardown path that runs after the transport already dropped.","commonSituations":"UI 'stop forwarding' action invoked right as the connection drops; cleanup handler firing during session teardown after auth was lost; the forward was never actually established (forwardedPorts entry from a previous session) and the user tries to remove it on a fresh, unauthenticated session.","solutions":["Guard removal with the instance check and skip/defer if not authenticated.","Track whether the remote forward was actually established (it is pushed to forwardedPorts only on success) and only call stopForwardingTCPPort for those.","Call removePortForward during an active authenticated session, not from a disconnect/destroy handler.","Wrap the call in try/catch during teardown so a lost transport does not abort cleanup."],"exampleFix":"// before\nawait session.removePortForward(remoteFw) // throws post-disconnect\n// after\nif (session.ssh instanceof russh.AuthenticatedSSHClient) {\n    await session.removePortForward(remoteFw)\n} else {\n    session.forwardedPorts = session.forwardedPorts.filter(x => x !== remoteFw)\n}","handlingStrategy":"type-guard","validationCode":"import * as russh from 'russh'\nif (fw.type === PortForwardType.Remote &&\n    !(session.ssh instanceof russh.AuthenticatedSSHClient)) {\n    // just drop it from the local list; transport is gone\n    session.forwardedPorts = session.forwardedPorts.filter(x => x !== fw)\n    return\n}","typeGuard":"import * as russh from 'russh'\nfunction isAuthed (s: unknown): s is russh.AuthenticatedSSHClient {\n    return s instanceof russh.AuthenticatedSSHClient\n}","tryCatchPattern":"try { await session.removePortForward(fw) } catch (e) {\n    if (/before auth/.test(e.message)) { /* transport gone; drop locally */ }\n    else throw e\n}","preventionTips":["Only remove remote forwards you successfully added (tracked in forwardedPorts).","Do not call removePortForward from destroy()/disconnect paths; guard with the type check.","Wrap teardown calls in try/catch so cleanup is resilient."],"tags":["ssh","port-forwarding","russh","teardown"],"backgroundTag":null,"analyzedSha":"14e2d60b9b6dee84a53c37f05eefeb803787de04","analyzedAt":"2026-08-12T11:46:48.773Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}