{"record":{"id":"8d18f5fda32632e1","repo":"decolua/9router","slug":"cline-token-exchange-failed-error","errorCode":null,"errorMessage":"`Cline token exchange failed: ${error}`","messagePattern":"`Cline token exchange failed: (.+?)`","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/providers/cline.js","lineNumber":40,"sourceCode":"      if (lastBrace === -1) throw new Error(\"No JSON found in decoded code\");\n      const tokenData = JSON.parse(decoded.substring(0, lastBrace + 1));\n      return {\n        access_token: tokenData.accessToken,\n        refresh_token: tokenData.refreshToken,\n        email: tokenData.email,\n        firstName: tokenData.firstName,\n        lastName: tokenData.lastName,\n        expires_at: tokenData.expiresAt,\n      };\n    } catch (e) {\n      const response = await fetch(config.tokenExchangeUrl, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\", Accept: \"application/json\" },\n        body: JSON.stringify({ grant_type: \"authorization_code\", code, client_type: \"extension\", redirect_uri: redirectUri }),\n      });\n      if (!response.ok) {\n        const error = await response.text();\n        throw new Error(`Cline token exchange failed: ${error}`);\n      }\n      const data = await response.json();\n      return {\n        access_token: data.data?.accessToken || data.accessToken,\n        refresh_token: data.data?.refreshToken || data.refreshToken,\n        email: data.data?.userInfo?.email || \"\",\n        expires_at: data.data?.expiresAt || data.expiresAt,\n      };\n    }\n  },\n  mapTokens: (tokens) => ({\n    accessToken: tokens.access_token,\n    refreshToken: tokens.refresh_token,\n    expiresIn: tokens.expires_at\n      ? Math.floor((new Date(tokens.expires_at).getTime() - Date.now()) / 1000)\n      : 3600,\n    email: tokens.email,\n    providerSpecificData: { firstName: tokens.firstName, lastName: tokens.lastName },","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/providers/cline.js#L22-L58","documentation":"When the base64-encoded-JSON shortcut in cline.js throws (see error 197), the code falls back to POSTing the authorization code to Cline's tokenExchangeUrl. If that HTTP fallback returns non-ok, this error is thrown with the raw response body. This is the terminal failure of the Cline token exchange — the code could not be converted into tokens either locally or via the API.","triggerScenarios":"Fallback exchange with: an expired/replayed authorization code, a redirect_uri not matching the one in buildAuthUrl, Cline API rejecting client_type 'extension' or returning 4xx/5xx, or the code param being garbage (neither base64 JSON nor a valid code).","commonSituations":"Cline service outage or API contract change, user refreshing the callback (code reuse), wrong callback_url/redirect_uri in CLINE_CONFIG, network proxy returning error pages, or expired codes from slow manual flows.","solutions":["Read the appended body for the upstream error (invalid_grant etc.) and fix that cause.","invalid_grant: restart the OAuth flow — codes are single-use and short-lived.","Verify redirect_uri passed to exchangeToken equals the callback_url/redirect_uri used in buildAuthUrl.","Check Cline service status / update tokenExchangeUrl if the API moved.","Retry after a 5xx with a fresh authorization flow."],"exampleFix":"// before: code reused from a previous completed flow\nawait exchangeToken(config, usedCode, redirectUri); // 400 invalid_grant\n// after: always exchange a freshly delivered code exactly once\nconst tokens = await exchangeToken(config, freshCallbackCode, redirectUri);","handlingStrategy":"try-catch","validationCode":"const code = new URL(callbackUrl).searchParams.get('code');\nif (!code || code.length < 8) throw new Error('invalid or missing code from Cline callback — skip exchange');\nif (usedClineCodes.has(code)) throw new Error('Cline code already exchanged');","typeGuard":"function isPlausibleClineCode(v) {\n  return typeof v === 'string' && v.trim().length >= 8 && !/\\s/.test(v);\n}","tryCatchPattern":"try {\n  const tokens = await cline.exchangeToken(config, code, redirectUri);\n  // use tokens\n} catch (err) {\n  if (String(err.message).startsWith('Cline token exchange failed:')) {\n    const body = err.message.slice('Cline token exchange failed:'.length);\n    if (/invalid_grant|expired/i.test(body)) startFreshClineLogin();\n    else if (/5\\d\\d|unavailable/i.test(body)) scheduleRetryWithBackoff();\n    else reportToUser(body); // 4xx config/contract issue\n  } else throw err;\n}","preventionTips":["Consume each Cline authorization code exactly once; block callback refreshes.","Keep CLINE_CONFIG.callbackUrl/redirect_uri identical across buildAuthUrl and exchangeToken.","Watch Cline's API status; 5xx bodies here are usually upstream outages.","Update tokenExchangeUrl promptly when Cline changes its extension API.","Exchange promptly after the callback to avoid code expiry."],"tags":["oauth","cline","token-exchange","http-4xx"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}