{"record":{"id":"08f9611b939caac2","repo":"decolua/9router","slug":"clinepass-token-exchange-failed-error","errorCode":null,"errorMessage":"`ClinePass token exchange failed: ${error}`","messagePattern":"`ClinePass token exchange failed: (.+?)`","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/providers/clinepass.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.tokenUrl, {\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(`ClinePass 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/clinepass.js#L22-L58","documentation":"ClinePass OAuth token exchange: the POST to the ClinePass token endpoint with grant_type=authorization_code returned a non-2xx status. The provider reads the raw response body as text and rethrows it inside this Error, so the message contains whatever the server said (HTML error page, JSON error, rate-limit text, etc.). It means the authorization code could not be traded for access/refresh tokens.","triggerScenarios":"Calling exchangeCode (the authorization-code callback handler in src/lib/oauth/providers/clinepass.js) when the upstream POST /token endpoint responds !response.ok — e.g. 400 invalid_grant (code already used or expired), 401 bad client credentials, 429 rate limit, or 5xx outage. The body text is interpolated verbatim into the message.","commonSituations":"User takes too long between authorize and callback so the code expires; the same code is replayed after a retry/refresh of the callback page; ClinePass service outage returning HTML error pages; clock skew or wrong redirect_uri in config making the server reject the grant.","solutions":["Log the interpolated error body — it names the exact OAuth error (invalid_grant, invalid_client, etc.) returned by ClinePass.","Restart the OAuth flow to obtain a fresh authorization code; codes are single-use and short-lived.","Verify the redirect_uri sent in the exchange exactly matches the one used in the authorize step.","Check ClinePass service status / network connectivity if the body is an HTML 5xx page."],"exampleFix":"// before\nconst error = await response.text();\nthrow new Error(`ClinePass token exchange failed: ${error}`);\n// after\nconst error = await response.text();\nlet detail = error;\ntry { detail = JSON.parse(error).error || error; } catch {}\nthrow new Error(`ClinePass token exchange failed (${response.status}): ${detail}`);","handlingStrategy":"try-catch","validationCode":"// Before starting the flow, sanity-check config\nif (!config?.tokenUrl || !redirectUri) throw new Error(\"ClinePass oauth config incomplete\");\n// Ensure a fresh, non-empty authorization code\nif (!code) throw new Error(\"No authorization code to exchange\");","typeGuard":"function isOAuthErrorResponse(body) {\n  return typeof body === \"object\" && body !== null &&\n    (typeof body.error === \"string\" || typeof body.message === \"string\");\n}","tryCatchPattern":"try {\n  const tokens = await provider.exchangeCode(code, redirectUri);\n  saveTokens(tokens);\n} catch (err) {\n  if (String(err.message).includes(\"ClinePass token exchange failed\")) {\n    logger.error(\"ClinePass exchange rejected:\", err.message);\n    // invalid_grant => restart flow; do not retry with same code\n    await restartOAuthFlow();\n  } else throw err;\n}","preventionTips":["Never reuse an authorization code — always restart the flow on exchange failure.","Log response.status alongside the body for faster diagnosis.","Keep redirect_uri identical between authorize and exchange steps.","Add a short timeout/AbortController so hung token requests fail fast."],"tags":["oauth","token-exchange","http-error","network"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}