{"record":{"id":"915828567332484a","repo":"ruvnet/ruflo","slug":"token-exchange-failed-response-status","errorCode":null,"errorMessage":"Token exchange failed: ${response.status}","messagePattern":"Token exchange failed: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/mcp/src/oauth.ts","lineNumber":199,"sourceCode":"      params.set('client_secret', this.config.clientSecret);\n    }\n\n    if (pending.codeVerifier) {\n      params.set('code_verifier', pending.codeVerifier);\n    }\n\n    const response = await fetch(this.config.tokenEndpoint, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/x-www-form-urlencoded',\n      },\n      body: params.toString(),\n    });\n\n    if (!response.ok) {\n      const error = await response.text();\n      this.logger.error('Token exchange failed', { status: response.status, error });\n      throw new Error(`Token exchange failed: ${response.status}`);\n    }\n\n    const data = (await response.json()) as TokenResponse;\n    const tokens = this.parseTokenResponse(data);\n\n    await this.tokenStorage.save('default', tokens);\n    this.logger.info('Token exchange successful');\n    this.emit('tokens:received', { expiresIn: tokens.expiresIn });\n\n    return tokens;\n  }\n\n  /**\n   * Refresh access token using refresh token\n   */\n  async refreshTokens(storageKey: string = 'default'): Promise<OAuthTokens> {\n    const existing = await this.tokenStorage.load(storageKey);\n    if (!existing?.refreshToken) {","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/mcp/src/oauth.ts#L181-L217","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait oauth.exchangeCode(code, state); // 400: code already redeemed by an earlier retry\n\n// after\ntry {\n  const tokens = await oauth.exchangeCode(code, state);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('Token exchange failed: 400')) {\n    const authReq = await oauth.createAuthorizationRequest(scopes);\n    return res.redirect(authReq.url); // fresh flow instead of replaying the code\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  const tokens = await oauth.exchangeCode(code, state);\n} catch (e) {\n  const m = /Token exchange failed: (\\d+)/.exec(String((e as Error).message));\n  if (m) {\n    const status = Number(m[1]);\n    if (status === 400 || status === 401) return restartAuthorizationFlow(); // config/grant problem — no retry\n    if (status >= 500) return retryWithBackoff(); // provider-side, transient\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["oauth","token-exchange","http","authentication"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}