Skip to content

fix: interactive mode swallows the anyhow error chain on connection failure, hiding the failing hop #238

Description

@inureyes

Problem / Background

Interactive-mode connection failures print only the outermost anyhow context, hiding the root cause.

Motivating incident: a user ran bssh -J <jumphost> user@192.168.1.22 against a macOS host that had just rebooted. bssh printed only:

✗ Failed to connect to inureyes@192.168.1.22: Failed to establish jump host connection to 192.168.1.22
Error: Failed to connect to any nodes

The actual failing step (the direct-tcpip channel open from the jump host to the destination, refused because the destination's sshd was not yet accepting connections) was invisible. The same command via OpenSSH surfaced its progress step by step, so the user misattributed the cause. With the full context chain displayed, the root cause would have been obvious on the first run.

Technical Analysis

  • src/commands/interactive/execution.rs:64 and src/commands/interactive/execution.rs:133 print connection errors with eprintln!("✗ Failed to connect to {}: {}", node.to_string().red(), e). anyhow's {} Display shows only the outermost context message.
  • The jump path wraps the real error in multiple nested contexts that are all swallowed:
    • src/commands/interactive/connection.rs:296-301: "Failed to establish jump host connection to {host}:{port}" (this is the only context the user ever sees)
    • src/jump/chain.rs:272-277: "Failed to connect to first jump host: ..."
    • src/jump/chain.rs:304-311: "Failed to connect to jump host ... (hop N)"
    • src/jump/chain.rs:329-333: "Failed to connect to destination ... through jump host chain"
    • src/jump/chain/tunnel.rs adds the innermost causes: timeout opening tunnel (lines 62-69), "Failed to open direct-tcpip channel to destination ..." (lines 206-210), SSH handshake failures (lines 240-250), and authentication failures (lines 256-262).
  • Depending on which hop fails, the user-visible message is identical, so first-jump auth failures, destination TCP refusals, and destination auth failures are indistinguishable.
  • Inconsistency inside the codebase: the parallel exec path already formats errors with the alternate form format!("{e:#}") at src/executor/parallel.rs:517, which renders the full chain. Interactive mode is the outlier.

Proposed Solution

  1. In both sites in src/commands/interactive/execution.rs, render the full chain, either with {e:#} (single line, "outer: mid: inner") or by iterating e.chain() into an indented "Caused by:" list for readability. Prefer extracting a small helper (for example format_connection_error(&anyhow::Error) -> String) so both the PTY path and the multiplex path share it and it can be unit tested.
  2. Audit other user-facing eprintln!/println! sites in src/commands/ that print anyhow errors with {} and switch them to the same helper where a wrapped chain exists.
  3. Keep tracing verbosity (-vv) as is; this change is about the default non-verbose UX.

Acceptance Criteria

  • A first-jump failure, an intermediate-hop failure, a destination channel-open failure, and a destination auth failure each produce visibly different terminal output containing the innermost cause.
  • Both interactive execution sites (lines 64 and 133) use the shared formatting helper; the helper has a unit test asserting that all context layers of a nested anyhow error appear in its output.
  • Behavior of exec mode (src/executor/parallel.rs:517) is unchanged.
  • The fix is integrated into the actual interactive code path (not a standalone helper left unused).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions