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
- Recreate the OAuth handler (NewHandler) after it has been closed; a closed receiver cannot be reused.
- Ensure Close is not called while an authorize flow is still in flight.
- 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
- Don't close the OAuth handler while an authorize flow is pending.
- Treat handlers as single-use: create a new one after Close.
- Synchronize shutdown with in-flight authorization attempts.
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
- interactive OAuth authorization required
- failed to start OAuth callback listener: all candidate ports
- mcp '%s' does not use OAuth authentication
- mcp '%s' already has an authentication in progress
- oauth_client_id: %w
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/f3ae0e45c014a241.
Report an issue: GitHub.