decolua/9router · error · Error
`ClinePass token exchange failed: ${error}`
Error message
`ClinePass token exchange failed: ${error}` What it means
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.
Source
Thrown at src/lib/oauth/providers/clinepass.js:40
if (lastBrace === -1) throw new Error("No JSON found in decoded code");
const tokenData = JSON.parse(decoded.substring(0, lastBrace + 1));
return {
access_token: tokenData.accessToken,
refresh_token: tokenData.refreshToken,
email: tokenData.email,
firstName: tokenData.firstName,
lastName: tokenData.lastName,
expires_at: tokenData.expiresAt,
};
} catch (e) {
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ grant_type: "authorization_code", code, client_type: "extension", redirect_uri: redirectUri }),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`ClinePass token exchange failed: ${error}`);
}
const data = await response.json();
return {
access_token: data.data?.accessToken || data.accessToken,
refresh_token: data.data?.refreshToken || data.refreshToken,
email: data.data?.userInfo?.email || "",
expires_at: data.data?.expiresAt || data.expiresAt,
};
}
},
mapTokens: (tokens) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_at
? Math.floor((new Date(tokens.expires_at).getTime() - Date.now()) / 1000)
: 3600,
email: tokens.email,
providerSpecificData: { firstName: tokens.firstName, lastName: tokens.lastName },View on GitHub (pinned to 90b52e06ff)
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.
Example fix
// before
const error = await response.text();
throw new Error(`ClinePass token exchange failed: ${error}`);
// after
const error = await response.text();
let detail = error;
try { detail = JSON.parse(error).error || error; } catch {}
throw new Error(`ClinePass token exchange failed (${response.status}): ${detail}`); Defensive patterns
Strategy: try-catch
Validate before calling
// Before starting the flow, sanity-check config
if (!config?.tokenUrl || !redirectUri) throw new Error("ClinePass oauth config incomplete");
// Ensure a fresh, non-empty authorization code
if (!code) throw new Error("No authorization code to exchange"); Type guard
function isOAuthErrorResponse(body) {
return typeof body === "object" && body !== null &&
(typeof body.error === "string" || typeof body.message === "string");
} Try / catch
try {
const tokens = await provider.exchangeCode(code, redirectUri);
saveTokens(tokens);
} catch (err) {
if (String(err.message).includes("ClinePass token exchange failed")) {
logger.error("ClinePass exchange rejected:", err.message);
// invalid_grant => restart flow; do not retry with same code
await restartOAuthFlow();
} else throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- `CodeBuddy state request failed: ${await response.text()}`
- `CodeBuddy Intl state request failed: ${await response.text(
- `Token exchange failed: ${error}`
- `Token exchange failed: ${error}`
- `GitLab token exchange failed: ${await response.text()}`
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/08f9611b939caac2.
Report an issue: GitHub.