Eugeny/tabby · error · Error
Cannot remove remote port forward before auth
Error message
Cannot remove remote port forward before auth
What it means
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.
Source
Thrown at tabby-ssh/src/session/ssh.ts:839
await this.ssh.forwardTCPPort(fw.host, fw.port)
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`)
return
}
this.emitServiceMessage(colors.bgGreen.black(' <- ') + ` Forwarded ${fw}`)
this.forwardedPorts.push(fw)
}
}
async removePortForward (fw: ForwardedPort): Promise<void> {
if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
fw.stopLocalListener()
this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
}
if (fw.type === PortForwardType.Remote) {
if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
throw new Error('Cannot remove remote port forward before auth')
}
this.ssh.stopForwardingTCPPort(fw.host, fw.port)
this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
}
this.emitServiceMessage(`Stopped forwarding ${fw}`)
}
async destroy (): Promise<void> {
this.logger.info('Destroying')
this.willDestroy.next()
this.willDestroy.complete()
this.serviceMessage.complete()
this.ssh.disconnect()
}
async openShellChannel (options: { x11: boolean }): Promise<russh.Channel> {
if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
throw new Error('Cannot open shell channel before auth')View on GitHub (pinned to 14e2d60b9b)
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.
Example fix
// before
await session.removePortForward(remoteFw) // throws post-disconnect
// after
if (session.ssh instanceof russh.AuthenticatedSSHClient) {
await session.removePortForward(remoteFw)
} else {
session.forwardedPorts = session.forwardedPorts.filter(x => x !== remoteFw)
} Defensive patterns
Strategy: type-guard
Validate before calling
import * as russh from 'russh'
if (fw.type === PortForwardType.Remote &&
!(session.ssh instanceof russh.AuthenticatedSSHClient)) {
// just drop it from the local list; transport is gone
session.forwardedPorts = session.forwardedPorts.filter(x => x !== fw)
return
} Type guard
import * as russh from 'russh'
function isAuthed (s: unknown): s is russh.AuthenticatedSSHClient {
return s instanceof russh.AuthenticatedSSHClient
} Try / catch
try { await session.removePortForward(fw) } catch (e) {
if (/before auth/.test(e.message)) { /* transport gone; drop locally */ }
else throw e
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Cannot add remote port forward before auth
- Authentication rejected
- Cannot open shell channel before auth
- Download cancelled
- ${profile.options.host}: jump host "${profile.options.jumpHo
AI-assisted analysis of Eugeny/tabby@14e2d60b9b (2026-08-12).
Data as JSON: /api/errors/5d786e0ac35494a2.
Report an issue: GitHub.