{"record":{"id":"f3ae0e45c014a241","repo":"charmbracelet/crush","slug":"oauth-callback-listener-closed","errorCode":null,"errorMessage":"OAuth callback listener closed","messagePattern":"OAuth callback listener closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/oauth/mcp/handler.go","lineNumber":489,"sourceCode":"\treturn r.flight\n}\n\n// bind starts the listener if one is not already running. The port was\n// resolved and pinned at construction, so this always targets the port the\n// redirect URI points at; if it is busy the error surfaces loudly rather\n// than silently binding a port nobody will redirect to.\nfunc (r *callbackReceiver) bind() error {\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\treturn r.bindLocked()\n}\n\n// bindLocked is bind with r.mu already held. The listener starts accepting\n// before this returns, so a browser opened immediately after cannot beat\n// the server to the port.\nfunc (r *callbackReceiver) bindLocked() error {\n\tif r.closed {\n\t\treturn errors.New(\"OAuth callback listener closed\")\n\t}\n\tif r.server != nil {\n\t\treturn nil\n\t}\n\tmux := http.NewServeMux()\n\tmux.HandleFunc(\"/\", r.handleCallback)\n\tserver := &http.Server{Handler: mux}\n\n\tlc := &net.ListenConfig{}\n\tlistener, err := lc.Listen(context.Background(), \"tcp\", fmt.Sprintf(\"localhost:%d\", r.fixedPort))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to bind OAuth callback port %d: %w\", r.fixedPort, err)\n\t}\n\tr.port = r.fixedPort\n\tgo r.serve(server, listener)\n\tr.server = server\n\treturn nil\n}","sourceCodeStart":471,"sourceCodeEnd":507,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/oauth/mcp/handler.go#L471-L507","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nreceiver.Close()\nreceiver.Begin(ctx) // \"OAuth callback listener closed\"\n// after\nif err := receiver.Begin(ctx); err != nil { return err } // flow first\nreceiver.Close()","handlingStrategy":"validation","validationCode":"// Check receiver state before starting a flow:\n// if handler.Closed() { handler = recreateHandler() }","typeGuard":null,"tryCatchPattern":"if err := receiver.Begin(ctx); err != nil {\n    if strings.Contains(err.Error(), \"listener closed\") {\n        receiver = newReceiver()\n        return receiver.Begin(ctx)\n    }\n    return err\n}","preventionTips":["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."],"tags":["oauth","mcp","use-after-close"],"backgroundTag":"use-after-close","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}