cli / cli/gh-webhook

forward: 1006 reconnect never triggers (wrapped error vs IsCloseError), and exits with a usage dump

Open
#43 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
43
Forks
16
PR merge metrics
No merged PRs in 30d

Description

### Summary

`gh webhook forward` has retry logic for a 1006 abnormal closure, but it never runs. The error is wrapped before it reaches the check, and `websocket.IsCloseError` does not unwrap. The result is that the command exits on the first server-side disconnect — and, because the command sets neither `SilenceUsage` nor `SilenceErrors`, prints its entire help text on the way out.

In my case GitHub closes the forwarding socket roughly every five minutes, so a long-running `forward` dies about twelve times an hour and each death writes ~26 lines of usage output. 73% of my log is cobra help text.

### The cause

`webhook/forward.go`, in `runFwd`:

```go
for i := 0; i < 3; i++ {
err := handleWebsocket(out, url, token, wsURL, activateHook)
if err != nil {
// If the error is a server disconnect (1006), retry connecting
if websocket.IsCloseError(err, websocket.CloseAbnormalClosure) {
time.Sleep(5 * time.Second)
continue
}
...
return err
}
}
```

`handleWebsocket` wraps the read error before returning it:

```go
err := c.ReadJSON(&ev)
if err != nil {
return fmt.Errorf("error receiving json event: %w", err)
}
```

and `gorilla/websocket` v1.5.0's `IsCloseError` is a bare type assertion, not `errors.As`:

```go
func IsCloseError(err error, codes ...int) bool {
if e, ok := err.(*CloseError); ok {
```

So the assertion fails on the wrapped error, the `continue` is unreachable, and `runFwd` returns immediately. The observed error confirms it — this is the wrapped text, not `unable to connect to webhooks server, forwarding stopped`, which is what exhausting the loop would produce:

```
Error: error receiving json event: websocket: close 1006 (abnormal closure): unexpected EOF
```

### Suggested fix

```go
var closeErr *websocket.CloseError
if errors.As(err, &closeErr) && closeErr.Code == websocket.CloseAbnormalClosure {
time.Sleep(5 * time.Second)
continue
}
```

and `SilenceUsage: true` on the command, since a runtime network error is not a usage error.

### One more thing worth considering

Fixing the unwrap alone may not be enough for a long-running forwarder. `i` counts *total* disconnects across the process lifetime and is never reset after a successful session, so with a server that closes on a timer the command would still exit permanently after three closes — for me, about fifteen minutes. Resetting the counter after a session that lasted a meaningful length of time would make `forward` survivable as a long-running process.

### Versions

- `gh` 2.98.0
- `gh-webhook` v0.2.0
- macOS 15 (arm64)

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.