charmbracelet/crush · error

OAuth callback listener closed

Error message

OAuth callback listener closed

What it means

bindLocked refuses to start the callback receiver's HTTP server because the receiver has already been closed. Once closed, the receiver can never accept an OAuth redirect, so begin/bind fail fast with this error.

Source

Thrown at internal/oauth/mcp/handler.go:489

	return r.flight
}

// bind starts the listener if one is not already running. The port was
// resolved and pinned at construction, so this always targets the port the
// redirect URI points at; if it is busy the error surfaces loudly rather
// than silently binding a port nobody will redirect to.
func (r *callbackReceiver) bind() error {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.bindLocked()
}

// bindLocked is bind with r.mu already held. The listener starts accepting
// before this returns, so a browser opened immediately after cannot beat
// the server to the port.
func (r *callbackReceiver) bindLocked() error {
	if r.closed {
		return errors.New("OAuth callback listener closed")
	}
	if r.server != nil {
		return nil
	}
	mux := http.NewServeMux()
	mux.HandleFunc("/", r.handleCallback)
	server := &http.Server{Handler: mux}

	lc := &net.ListenConfig{}
	listener, err := lc.Listen(context.Background(), "tcp", fmt.Sprintf("localhost:%d", r.fixedPort))
	if err != nil {
		return fmt.Errorf("failed to bind OAuth callback port %d: %w", r.fixedPort, err)
	}
	r.port = r.fixedPort
	go r.serve(server, listener)
	r.server = server
	return nil
}

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Recreate the OAuth handler (NewHandler) after it has been closed; a closed receiver cannot be reused.
  2. Ensure Close is not called while an authorize flow is still in flight.
  3. Serialize handler shutdown with any pending authorization attempts in caller code.

Example fix

// before
receiver.Close()
receiver.Begin(ctx) // "OAuth callback listener closed"
// after
if err := receiver.Begin(ctx); err != nil { return err } // flow first
receiver.Close()
Defensive patterns

Strategy: validation

Validate before calling

// Check receiver state before starting a flow:
// if handler.Closed() { handler = recreateHandler() }

Try / catch

if err := receiver.Begin(ctx); err != nil {
    if strings.Contains(err.Error(), "listener closed") {
        receiver = newReceiver()
        return receiver.Begin(ctx)
    }
    return err
}

Prevention

When it happens

Trigger: Calling begin or bind on a callbackReceiver after close has run — e.g. the handler was shut down, Close was called, or an in-flight authorize raced with shutdown.

Common situations: An OAuth flow is in progress when the MCP handler is torn down (session ending, config reload); a concurrent authorize attempt then finds the receiver closed.

Related errors


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