can1357/oh-my-pi · error · Error

Token exchange failed: ${response.status} ${errorText}

Error message

Token exchange failed: ${response.status} ${errorText}

What it means

exchangeToken posts the authorization-code (or refresh) grant to the provider's token endpoint and expects a 2xx response. On any non-ok HTTP status the response body is read as error text and thrown as "Token exchange failed: <status> <body>", surfacing the provider's own OAuth error (e.g. invalid_grant, invalid_client) along with the status code.

Source

Thrown at packages/coding-agent/src/mcp/oauth-flow.ts:516

			params.set("resource", this.#resource);
		}
		const clientSecret = this.config.clientSecret ?? this.#registeredClientSecret;
		if (clientSecret) {
			params.set("client_secret", clientSecret);
		}

		const response = await this.#fetch(this.config.tokenUrl, {
			method: "POST",
			headers: {
				"Content-Type": "application/x-www-form-urlencoded",
			},
			body: params.toString(),
			signal: this.ctrl.signal,
		});

		if (!response.ok) {
			const errorText = await response.text();
			throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
		}

		const data = (await response.json()) as {
			access_token?: string;
			refresh_token?: string;
			expires_in?: number;
			token_type?: string;
			error?: string;
			error_description?: string;
		};

		// Some providers (e.g. the Slack Web API) signal failure with HTTP 200 and
		// an `{ ok: false, error }` body. Accepting such a response would store an
		// empty access token and only surface `invalid_token` on a later request.
		if (typeof data.access_token !== "string" || data.access_token.length === 0) {
			const providerError = data.error_description ?? data.error;
			throw new Error(`Token exchange returned no access token${providerError ? `: ${providerError}` : ""}`);
		}

View on GitHub (pinned to 9690622007)

Solutions

  1. Read the status and body in the error: 400 invalid_grant means restart the authorization flow; 401 invalid_client means fix clientId/clientSecret
  2. Verify redirect_uri exactly matches what is registered at the provider
  3. Re-run the full OAuth authorization flow to get a fresh authorization code (codes are single-use and short-lived)
  4. Check provider status/availability for 5xx responses and retry with backoff
  5. Check machine clock sync if codes expire prematurely

Example fix

// before: reusing a consumed code from a failed first exchange attempt
await client.exchangeToken(authorizationCode); // second attempt fails: invalid_grant
// after: restart the flow to mint a new code when exchange fails with invalid_grant
try {
  await client.exchangeToken(authorizationCode);
} catch (e) {
  if (/invalid_grant/.test(String(e))) authorizationCode = await startAuthorizationFlow();
}
Defensive patterns

Strategy: retry

Validate before calling

const health = await fetch(tokenUrl, { method: 'HEAD' }).catch(() => null);
if (!health || !health.ok && health.status >= 500) {
  throw new Error('token endpoint unavailable; retry later');
}

Try / catch

try {
  creds = await client.exchangeToken(code);
} catch (e) {
  const m = e.message.match(/Token exchange failed: (\d+) (.*)/);
  if (m) {
    const [status, body] = [Number(m[1]), m[2]];
    if (status >= 500 || status === 429) await Bun.sleep(1000 * 2 ** attempt++); // retry with backoff
    else if (/invalid_grant/.test(body)) await restartAuthorizationFlow(); // code expired/consumed
    else if (/invalid_client/.test(body)) throw new Error('Check clientId/clientSecret configuration');
  } else throw e;
}

Prevention

When it happens

Trigger: Any token-endpoint HTTP failure during the MCP OAuth flow: expired/already-used authorization code, wrong client credentials (invalid_client), mismatched redirect_uri, expired/revoked refresh token (invalid_grant), provider outage (5xx), or a rejected resource indicator.

Common situations: Replaying an authorization code after a retry; clock skew causing code expiry; client secret rotated server-side; redirect URI not exactly registered at the provider; rate-limited or temporarily down token endpoint.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/ca57d5cb048fbb74. Report an issue: GitHub.