ruvnet/ruflo · error · Error
Token exchange failed: ${response.status}
Error message
Token exchange failed: ${response.status} What it means
The token-exchange POST to config.tokenEndpoint returned a non-2xx status; the thrown message carries only the HTTP code while the response body is logged server-side. Statuses map to standard OAuth failures: 400 invalid/expired code or redirect_uri mismatch, 401 client authentication failure, 5xx provider trouble.
Source
Thrown at v3/@claude-flow/mcp/src/oauth.ts:199
params.set('client_secret', this.config.clientSecret);
}
if (pending.codeVerifier) {
params.set('code_verifier', pending.codeVerifier);
}
const response = await fetch(this.config.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params.toString(),
});
if (!response.ok) {
const error = await response.text();
this.logger.error('Token exchange failed', { status: response.status, error });
throw new Error(`Token exchange failed: ${response.status}`);
}
const data = (await response.json()) as TokenResponse;
const tokens = this.parseTokenResponse(data);
await this.tokenStorage.save('default', tokens);
this.logger.info('Token exchange successful');
this.emit('tokens:received', { expiresIn: tokens.expiresIn });
return tokens;
}
/**
* Refresh access token using refresh token
*/
async refreshTokens(storageKey: string = 'default'): Promise<OAuthTokens> {
const existing = await this.tokenStorage.load(storageKey);
if (!existing?.refreshToken) {View on GitHub (pinned to fa13ee4ad6)
Solutions
- Read the logged response body — providers return error codes (invalid_grant vs invalid_client) that pinpoint the cause
- Never retry a used code: restart the flow with a fresh createAuthorizationRequest
- Verify clientId, clientSecret, and redirectUri match the app registration byte-for-byte (scheme, host, path, no trailing slash)
- If the provider requires PKCE, ensure the codeVerifier returned by createAuthorizationRequest is included in the exchange
Example fix
// before
await oauth.exchangeCode(code, state); // 400: code already redeemed by an earlier retry
// after
try {
const tokens = await oauth.exchangeCode(code, state);
} catch (e) {
if (e instanceof Error && e.message.includes('Token exchange failed: 400')) {
const authReq = await oauth.createAuthorizationRequest(scopes);
return res.redirect(authReq.url); // fresh flow instead of replaying the code
}
throw e;
} Defensive patterns
Strategy: try-catch
Try / catch
try {
const tokens = await oauth.exchangeCode(code, state);
} catch (e) {
const m = /Token exchange failed: (\d+)/.exec(String((e as Error).message));
if (m) {
const status = Number(m[1]);
if (status === 400 || status === 401) return restartAuthorizationFlow(); // config/grant problem — no retry
if (status >= 500) return retryWithBackoff(); // provider-side, transient
}
throw e;
} Prevention
- Treat authorization codes as single-use: exchange immediately upon callback
- Keep redirect_uri, clientId and clientSecret in one config source mirrored from the app registration
- Log the provider's response body (not just the status) when exchanges fail
- Handle 5xx with backoff but 4xx with a fresh authorization flow
When it happens
Trigger: exchangeCode with an authorization code that expired or was already redeemed, a redirect_uri differing from the authorization request, wrong clientId/clientSecret (or wrong client-auth method), or a provider outage returning 5xx.
Common situations: Retrying or replaying an exchange after the code was already used (codes are one-shot); redirect URI registered in the OAuth app differing in scheme/trailing slash; a rotated secret not updated; PKCE required but the codeVerifier not sent.
Related errors
- Failed to fetch ${baseURL}/models: ${response.status} ${resp
- Failed to fetch ${baseURL}/models: ${response.status} ${resp
- Failed to fetch base servers: ${response.statusText}
- Pinata upload failed (HTTP ${res.status}): ${JSON.stringify(
- Pinata list failed (HTTP ${res.status}): ${JSON.stringify(re
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/915828567332484a.
Report an issue: GitHub.