github/github-mcp-server · error
callback server: %w
Error message
callback server: %w
What it means
The callback HTTP server's Serve loop terminated with an error other than http.ErrServerClosed — reported through the same single-slot channel as real OAuth outcomes. Because the listener was successfully bound just before, this is an accept-loop failure: the listener died underneath the server (fd closed, socket torn down) or the accept syscall is persistently failing.
Source
Thrown at internal/oauth/callback.go:72
}
return listener, nil
}
// newCallbackServer starts a callback server on listener that validates state
// and reports the result on a buffered channel. The redirect URI always uses
// localhost so it matches the value registered on the OAuth/GitHub App.
func newCallbackServer(listener net.Listener, expectedState string) *callbackServer {
cs := &callbackServer{
server: &http.Server{ReadHeaderTimeout: 10 * time.Second}, // ReadHeaderTimeout guards against Slowloris.
listener: listener,
redirect: fmt.Sprintf("http://localhost:%d/callback", listener.Addr().(*net.TCPAddr).Port),
results: make(chan callbackResult, 1),
}
cs.server.Handler = cs.handler(expectedState)
go func() {
if err := cs.server.Serve(listener); err != nil && err != http.ErrServerClosed {
cs.report(callbackResult{err: fmt.Errorf("callback server: %w", err)})
}
}()
return cs
}
// handler renders the callback endpoint. It reports the outcome exactly once and
// always shows the user a friendly page.
func (cs *callbackServer) handler(expectedState string) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
if errCode := q.Get("error"); errCode != "" {
msg := errCode
if desc := q.Get("error_description"); desc != "" {
msg = fmt.Sprintf("%s: %s", errCode, desc)
}View on GitHub (pinned to 0ea1f775a7)
Solutions
- Check the wrapped accept error: 'too many open files' means an fd leak — raise the limit and fix the leak
- If it follows a suspend/resume or container migration, simply restarting the OAuth flow recovers
- Ensure only cs.close()/server.Shutdown is used to stop the callback server — never close the listener directly
- Retry the login; the listener is re-bound on each new flow attempt
Defensive patterns
Strategy: try-catch
Try / catch
// treat as flow failure surfaced via the flow's result channel;
// check ulimit -n if the wrapped error is 'too many open files'
if err != nil && strings.Contains(err.Error(), "callback server:") {
_ = ln.Close() // cleanup, then restart the login flow
} Prevention
- Raise RLIMIT_NOFILE and fix fd leaks in long-running processes hosting the callback server
- Only stop the server via Shutdown/ErrServerClosed paths, never by closing the listener
When it happens
Trigger: cs.server.Serve(listener) at internal/oauth/callback.go:66 returns a non-ErrServerClosed error: another goroutine closed the listener's file descriptor, the OS invalidated the socket (VM suspend/resume, network namespace change), or accept hits EMFILE (process fd limit exhausted). The error is delivered to the flow via cs.report, so the user sees the flow abort with 'callback server: ...'.
Common situations: Container live-restore or checkpoint/restore tearing down sockets; ulimit -n exhausted by an fd leak elsewhere in the process; laptop suspend/resume during the OAuth wait; test harnesses closing the listener to simulate shutdown but the close races an in-flight accept.
Related errors
- installation token request failed: %s (reading response: %w)
- starting callback listener on %s: %w
- %w: %w
- requesting device code: %w
- App not connected
AI-assisted analysis of github/github-mcp-server@0ea1f775a7 (2026-08-15).
Data as JSON: /api/errors/9ebc0d8237a8b64e.
Report an issue: GitHub.