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

  1. Log the interpolated error body — it names the exact OAuth error (invalid_grant, invalid_client, etc.) returned by ClinePass.
  2. Restart the OAuth flow to obtain a fresh authorization code; codes are single-use and short-lived.
  3. Verify the redirect_uri sent in the exchange exactly matches the one used in the authorize step.
  4. 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

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


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/08f9611b939caac2. Report an issue: GitHub.