{"record":{"id":"ca57d5cb048fbb74","repo":"can1357/oh-my-pi","slug":"token-exchange-failed-response-status-errort","errorCode":null,"errorMessage":"Token exchange failed: ${response.status} ${errorText}","messagePattern":"Token exchange failed: (.+?) (.+?)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/mcp/oauth-flow.ts","lineNumber":516,"sourceCode":"\t\t\tparams.set(\"resource\", this.#resource);\n\t\t}\n\t\tconst clientSecret = this.config.clientSecret ?? this.#registeredClientSecret;\n\t\tif (clientSecret) {\n\t\t\tparams.set(\"client_secret\", clientSecret);\n\t\t}\n\n\t\tconst response = await this.#fetch(this.config.tokenUrl, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t},\n\t\t\tbody: params.toString(),\n\t\t\tsignal: this.ctrl.signal,\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst errorText = await response.text();\n\t\t\tthrow new Error(`Token exchange failed: ${response.status} ${errorText}`);\n\t\t}\n\n\t\tconst data = (await response.json()) as {\n\t\t\taccess_token?: string;\n\t\t\trefresh_token?: string;\n\t\t\texpires_in?: number;\n\t\t\ttoken_type?: string;\n\t\t\terror?: string;\n\t\t\terror_description?: string;\n\t\t};\n\n\t\t// Some providers (e.g. the Slack Web API) signal failure with HTTP 200 and\n\t\t// an `{ ok: false, error }` body. Accepting such a response would store an\n\t\t// empty access token and only surface `invalid_token` on a later request.\n\t\tif (typeof data.access_token !== \"string\" || data.access_token.length === 0) {\n\t\t\tconst providerError = data.error_description ?? data.error;\n\t\t\tthrow new Error(`Token exchange returned no access token${providerError ? `: ${providerError}` : \"\"}`);\n\t\t}","sourceCodeStart":498,"sourceCodeEnd":534,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/mcp/oauth-flow.ts#L498-L534","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: reusing a consumed code from a failed first exchange attempt\nawait client.exchangeToken(authorizationCode); // second attempt fails: invalid_grant\n// after: restart the flow to mint a new code when exchange fails with invalid_grant\ntry {\n  await client.exchangeToken(authorizationCode);\n} catch (e) {\n  if (/invalid_grant/.test(String(e))) authorizationCode = await startAuthorizationFlow();\n}","handlingStrategy":"retry","validationCode":"const health = await fetch(tokenUrl, { method: 'HEAD' }).catch(() => null);\nif (!health || !health.ok && health.status >= 500) {\n  throw new Error('token endpoint unavailable; retry later');\n}","typeGuard":null,"tryCatchPattern":"try {\n  creds = await client.exchangeToken(code);\n} catch (e) {\n  const m = e.message.match(/Token exchange failed: (\\d+) (.*)/);\n  if (m) {\n    const [status, body] = [Number(m[1]), m[2]];\n    if (status >= 500 || status === 429) await Bun.sleep(1000 * 2 ** attempt++); // retry with backoff\n    else if (/invalid_grant/.test(body)) await restartAuthorizationFlow(); // code expired/consumed\n    else if (/invalid_client/.test(body)) throw new Error('Check clientId/clientSecret configuration');\n  } else throw e;\n}","preventionTips":["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"],"tags":["oauth","network","http","token-exchange"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}