decolua/9router · error · Error

`GitLab token exchange failed: ${await response.text()}`

Error message

`GitLab token exchange failed: ${await response.text()}`

What it means

GitLab OAuth token exchange: the POST (application/x-www-form-urlencoded) to <baseUrl><tokenUrlPath> returned a non-2xx status and the body text is thrown. This provider is instance-relative, so baseUrl mistakes are a leading cause. No tokens are returned and the subsequent userInfo fetch never runs.

Source

Thrown at src/lib/oauth/providers/gitlab.js:39

  },
  exchangeToken: async (config, code, redirectUri, codeVerifier, state, meta = {}) => {
    const baseUrl = meta.baseUrl || config.defaultBaseUrl;
    const clientId = meta.clientId || "";
    const clientSecret = meta.clientSecret || "";
    const body = new URLSearchParams({
      client_id: clientId,
      grant_type: "authorization_code",
      code,
      redirect_uri: redirectUri,
      code_verifier: codeVerifier,
    });
    if (clientSecret) body.set("client_secret", clientSecret);
    const response = await fetch(`${baseUrl}${config.tokenUrlPath}`, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
      body: body.toString(),
    });
    if (!response.ok) throw new Error(`GitLab token exchange failed: ${await response.text()}`);
    const tokens = await response.json();
    // Fetch user info
    const userRes = await fetch(`${baseUrl}${config.userInfoUrlPath}`, {
      headers: { Authorization: `Bearer ${tokens.access_token}` },
    });
    const user = userRes.ok ? await userRes.json() : {};
    return { ...tokens, _user: user, _baseUrl: baseUrl, _clientId: clientId };
  },
  mapTokens: (tokens) => ({
    accessToken: tokens.access_token,
    refreshToken: tokens.refresh_token,
    expiresIn: tokens.expires_in,
    scope: tokens.scope,
    providerSpecificData: {
      username: tokens._user?.username || "",
      email: tokens._user?.email || tokens._user?.public_email || "",
      name: tokens._user?.name || "",
      baseUrl: tokens._baseUrl,

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Inspect the body — GitLab returns {error: "invalid_grant"|"invalid_client", error_description} which names the cause.
  2. Confirm baseUrl points at the instance root and tokenUrlPath resolves to /oauth/token.
  3. Ensure client_secret is set when the GitLab app is configured as confidential.
  4. Restart the flow for a fresh code and verify redirect_uri matches the app registration exactly.

Example fix

// before
if (!response.ok) throw new Error(`GitLab token exchange failed: ${await response.text()}`);
// after
if (!response.ok) {
  const t = await response.text();
  throw new Error(`GitLab token exchange failed (HTTP ${response.status} at ${baseUrl}${config.tokenUrlPath}): ${t}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate GitLab target before exchanging
const u = new URL(baseUrl + config.tokenUrlPath);
if (!u.pathname.endsWith("/oauth/token")) throw new Error(`Suspicious GitLab token path: ${u.pathname}`);
if (clientSecretRequired && !clientSecret) throw new Error("Confidential GitLab app requires client_secret");

Type guard

function isGitlabTokenError(body) {
  return typeof body === "object" && body !== null &&
    typeof body.error === "string"; // "invalid_grant" | "invalid_client"
}

Try / catch

try {
  const session = await gitlabProvider.exchangeCode(code);
} catch (err) {
  if (String(err.message).includes("GitLab token exchange failed")) {
    if (/invalid_client/.test(err.message)) console.error("Check GitLab client_id/client_secret");
    else if (/invalid_grant/.test(err.message)) await restartGitlabLogin();
    else console.error("GitLab token endpoint error:", err.message);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling exchangeCode for gitlab when the token endpoint replies !response.ok — 401 invalid_client (bad/missing client_secret), invalid_grant (code expired/replayed), redirect_uri mismatch, wrong baseUrl (self-hosted instance path or /api/v4 confusion), or 5xx.

Common situations: Self-hosted GitLab with wrong baseUrl or token path; client_secret omitted even though the instance requires it; code replayed after callback retry; GitLab.com outage; reverse proxy stripping the POST body.

Related errors


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