github/github-mcp-server · error

exchanging authorization code: %w

Error message

exchanging authorization code: %w

What it means

The authorization-code-for-token exchange (oc.Exchange with the PKCE verifier) failed at the GitHub token endpoint. The %w wrap keeps the oauth2 error, which embeds the HTTP status and response body. Distinct causes map to distinct statuses: 400 bad_verification_code (code expired, already used, or verifier mismatch), 401 bad client credentials, 403 redirect/callback restriction, network failure.

Source

Thrown at internal/oauth/flow.go:105

		// Inside a container the callback binds all interfaces so the published
		// port is reachable, which also exposes it to the container network.
		// Publishing to loopback only (e.g. -p 127.0.0.1:%d:%d) keeps the
		// authorization code off the network.
		m.logger.Warn(fmt.Sprintf("OAuth callback is listening on all container interfaces; publish it to loopback only (e.g. -p 127.0.0.1:%d:%d) so the authorization code is not exposed on your network", m.config.CallbackPort, m.config.CallbackPort))
	}
	cs := newCallbackServer(listener, state)

	oc := m.oauth2Config(cs.redirect)
	authURL := oc.AuthCodeURL(state, oauth2.S256ChallengeOption(verifier))

	run := func(ctx context.Context) (*oauth2.Token, error) {
		code, err := cs.wait(ctx)
		if err != nil {
			return nil, err
		}
		tok, err := oc.Exchange(ctx, code, oauth2.VerifierOption(verifier))
		if err != nil {
			return nil, fmt.Errorf("exchanging authorization code: %w", err)
		}
		return tok, nil
	}

	browserErr := m.openURL(authURL)
	switch {
	case browserErr == nil:
		m.logger.Info("opened browser for GitHub authorization")
		return &flowPlan{run: run}, nil
	case errors.Is(browserErr, errNoDisplay) && m.config.CallbackPort == 0:
		// Headless host with a random callback port: every PKCE channel ends in a
		// redirect to this machine's localhost, which a browser on another machine
		// (e.g. a remote SSH client) cannot reach — so even URL elicitation would
		// dead-end. Device flow is the only channel reachable from elsewhere, so
		// prefer it when the app supports it; otherwise fall through to the manual
		// authorization URL below for a same-machine browser.
		plan, deviceErr := m.beginDevice(prompter)
		if deviceErr == nil {

View on GitHub (pinned to 0ea1f775a7)

Solutions

  1. Restart the login flow and complete authorization promptly — expired/used codes are the top cause
  2. Align the app's registered callback URL with http://localhost:{--oauth-callback-port}/callback
  3. Read the embedded status: 400 = code/verifier problem, 401 = client_id/secret wrong, network text = connectivity/proxy
  4. Avoid refreshing the callback page; complete the flow in one pass
Defensive patterns

Strategy: retry

Try / catch

if err != nil && strings.Contains(err.Error(), "exchanging authorization code") {
    if strings.Contains(err.Error(), "bad_verification_code") {
        // expired/used code — restart the flow, complete promptly in one tab
    }
}

Prevention

When it happens

Trigger: oc.Exchange at internal/oauth/flow.go:103 fails when: the user dwells too long before the callback so the 10-minute code expires; the code was consumed by a retry (browser re-GET of the redirect); the S256 verifier does not match because state came from one flow and code from another; GitHub's callback URL for the app differs from http://localhost:PORT/callback; the token endpoint is unreachable (network error).

Common situations: Browser reloads the callback URL after success, burning the code twice; clock-heavy flows on slow devices; the OAuth app's callback field still points at an old port; corporate proxies blocking POSTs to github.com/login/oauth/access_token; mixing up dev/prod app credentials.

Related errors


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