charmbracelet/crush · error

failed to subscribe to events: %w

Error message

failed to subscribe to events: %w

What it means

SubscribeEvents wraps an error from c.get when opening the SSE stream GET /workspaces/{id}/events?client_id=... with event-stream headers. Raised before any status check, it means the transport itself failed: the stream could not be opened. The SSE channel is never created in this case and the wrapped cause (dial error, context canceled, TLS failure) is preserved.

Source

Thrown at internal/client/proto.go:124

	defer rsp.Body.Close()
	if err := checkStatus(rsp); err != nil {
		return fmt.Errorf("failed to set current session: %w", err)
	}
	return nil
}

// SubscribeEvents subscribes to server-sent events for a workspace.
func (c *Client) SubscribeEvents(ctx context.Context, id string) (<-chan any, error) {
	events := make(chan any, 100)
	q := url.Values{"client_id": []string{c.clientID}}
	//nolint:bodyclose
	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/events", id), q, http.Header{
		"Accept":        []string{"text/event-stream"},
		"Cache-Control": []string{"no-cache"},
		"Connection":    []string{"keep-alive"},
	})
	if err != nil {
		return nil, fmt.Errorf("failed to subscribe to events: %w", err)
	}

	if err := checkStatus(rsp); err != nil {
		rsp.Body.Close()
		return nil, fmt.Errorf("failed to subscribe to events: %w", err)
	}

	go func() {
		defer rsp.Body.Close()
		defer close(events)

		scr := bufio.NewReader(rsp.Body)
		for {
			line, err := scr.ReadBytes('\n')
			if errors.Is(err, io.EOF) {
				break
			}
			if err != nil {

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Confirm the server is up and /workspaces/{id}/events is reachable (curl with Accept: text/event-stream).
  2. Check errors.Is(err, context.Canceled) to distinguish deliberate cancellation from real failures.
  3. Retry SubscribeEvents with backoff; SSE reconnects are expected to be re-established.
  4. Validate the server URL/port configuration.

Example fix

// before
events, err := client.SubscribeEvents(ctx, id)
if err != nil { return err }
// after: retry stream establishment
var events <-chan any
err := client.SubscribeEventsErr() // wrapper
for i := 0; i < 3; i++ {
    events, err = client.SubscribeEvents(ctx, id)
    if err == nil { break }
    if errors.Is(err, context.Canceled) { return err }
    time.Sleep(500 * time.Millisecond << i)
}
Defensive patterns

Strategy: retry

Validate before calling

if ctx.Err() != nil { return fmt.Errorf("cannot subscribe: context already done") }
if err := serverReachable(serverURL); err != nil { return err }

Try / catch

var events <-chan any
var err error
for attempt := 0; attempt < 5; attempt++ {
    events, err = client.SubscribeEvents(ctx, id)
    if err == nil { break }
    if errors.Is(err, context.Canceled) { return err }
    select {
    case <-time.After(time.Second << attempt):
    case <-ctx.Done():
        return ctx.Err()
    }
}

Prevention

When it happens

Trigger: Calling client.SubscribeEvents(ctx, id) when the server is unreachable, the connection is refused/reset during handshake, or ctx is canceled before the request completes. Called by runNonInteractive and tests like TestSubscribeEventsContextCancelClosesEvents.

Common situations: runNonInteractive starting against a dead server; context canceled during startup (e.g. ctrl-c before the stream opens); wrong port/URL; proxy stripping long-lived connections.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/a2b3494caa3e25c1. Report an issue: GitHub.