Skip to content

listen connects but never prints "Connected": readiness line is dropped when output is piped or --color off #376

Description

@leggetter

Symptom

hookdeck listen connects, forwards events, and never says it connected.

The documented readiness line — Connected. Waiting for events..., promised in README § Output modes for both compact and quiet — is not printed when stdout/stderr are pipes, or when --color off is passed. Nothing else is printed either, so a caller waiting on the documented signal has no way to tell a healthy tunnel from a broken one and eventually reports a connection timeout.

Reported from the field: a wrapper ran

hookdeck listen 28765 <source> <connection> --path /… --output compact --color off --log-level info

waited 45s for the readiness line, saw nothing, and filed it as a websocket failure. The websocket was fine the whole time.

Cause

SimpleRenderer.OnConnected puts its entire body — the readiness line and the active-filters notice — behind if r.spinner != nil (pkg/listen/proxy/renderer_simple.go:47).

r.spinner is set from ansi.StartNewSpinner, which returns nil when !isTerminal(w) || !shouldUseColors(w) (pkg/ansi/ansi.go:134). So the spinner is nil, and the readiness line is dropped, whenever:

  • --color off is passed (ansi.DisableColors, pkg/config/config.go:174) — even on a full TTY; or
  • the writer is not a terminal.

The writer is log.StandardLogger().Out, i.e. stderr. Nothing in the CLI calls SetOutput, so whether a line reaches stdout is decided by whether stderr is a TTY. A harness that gives stdout a PTY but pipes stderr loses the line too.

OnDisconnected has the same shape: Connection lost, reconnecting... goes to stderr via the spinner fallback while stdout receives only a bare newline.

Related: #333 downgrades --output interactive to compact when stdout is not a terminal, so non-interactive callers are steered onto exactly the renderer whose readiness line a non-interactive environment suppresses.

Affected

All 2.x through v2.5.0, and release/v3.0.0pkg/ansi/ansi.go and pkg/listen/proxy/renderer_simple.go are byte-identical between v2.5.0 and that branch.

Reproduction

Built from the v2.5.0 tag, run against a local stand-in for api.hookdeck.com / ws.hookdeck.com, with a real PTY via script.

With --color off, on a PTY — server side is healthy throughout:

[mock] API POST /2025-07-01/cli-sessions   body: {"webhook_ids":["web_test"]}
[mock] WS upgrade  websocket-id=cses_test  x-webhook-ids=web_test
[mock] WS handshake completed (101)
[mock] WS ping received -> pong        (x14, for minutes)

Terminal side, for the whole run:

Listening on

repro-source
│  Requests to → https://hkdk.events/src_test
└─ Forwards to → http://localhost:28765/v1/tradingview-alert (cli-repro-source)

Getting ready...

Same command without --color off: ⣾ Getting ready... Connected. Waiting for events...

No --color flag at all, stdout and stderr piped: readiness line absent from stdout; Getting ready... on stderr. The flag is not required to hit this — any CI job, tee, or agent harness reproduces it.

Telling the three outcomes apart (for triage)

Useful when someone reports "listen never connects":

What actually happened What you see
Connected; readiness line suppressed (this bug) banner, then nothing, process still running past ~30s
Websocket unreachable spinner ~20s, then Could not connect. Terminating after 10 failed attempts to establish a connection., exit 1
Server rejecting the session (close 4001) repeated Connection lost, reconnecting... on stderr, bare blank lines on stdout

Only the first is silent-but-alive. If a report says the process was still running quietly after 30–45 seconds, the tunnel was up.

Fix

Print connection state unconditionally, and put it on stdout next to the banner and the event log, so one stream carries the whole state machine. The spinner stays exactly as it is when there is a terminal to draw on.

Also worth doing, but out of scope for a patch: the reason a dial failed is only ever logged at debug, so the Could not connect message above never says why (no "connection refused", no status code).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions