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
- Read the status and body in the error: 400 invalid_grant means restart the authorization flow; 401 invalid_client means fix clientId/clientSecret
- Verify redirect_uri exactly matches what is registered at the provider
- Re-run the full OAuth authorization flow to get a fresh authorization code (codes are single-use and short-lived)
- Check provider status/availability for 5xx responses and retry with backoff
- 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
- Never reuse authorization codes; each is single-use and expires in minutes
- Ensure redirect_uri in the exchange matches the authorization request and provider registration exactly
- Keep clientId/clientSecret in sync with provider-side rotations
- Retry only transient statuses (429/5xx) with exponential backoff
- Monitor clock sync (NTP) to avoid premature code expiry
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
- GitLab OAuth token exchange failed: ${response.status} ${awa
- V2 remote compaction failed (${response.status} ${response.s
- sso-role
- Failed to initiate device authorization: ${initiateResponse.
- GitLab Duo Workflow OAuth refresh failed: ${response.status}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/ca57d5cb048fbb74.
Report an issue: GitHub.