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

  1. Check the wrapped accept error: 'too many open files' means an fd leak — raise the limit and fix the leak
  2. If it follows a suspend/resume or container migration, simply restarting the OAuth flow recovers
  3. Ensure only cs.close()/server.Shutdown is used to stop the callback server — never close the listener directly
  4. 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

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


AI-assisted analysis of github/github-mcp-server@0ea1f775a7 (2026-08-15). Data as JSON: /api/errors/9ebc0d8237a8b64e. Report an issue: GitHub.