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
- Inspect the body — GitLab returns {error: "invalid_grant"|"invalid_client", error_description} which names the cause.
- Confirm baseUrl points at the instance root and tokenUrlPath resolves to /oauth/token.
- Ensure client_secret is set when the GitLab app is configured as confidential.
- 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
- Double-check baseUrl for self-hosted instances (instance root, correct token path).
- Set client_secret whenever the GitLab application is confidential.
- Restart the flow instead of retrying an already-consumed authorization code.
- Log the resolved token URL so misconfigurations are immediately visible.
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
- `ClinePass token exchange failed: ${error}`
- `Token exchange failed: ${error}`
- `Token exchange failed: ${error}`
- HTTP ${response.status}: ${errorMsg}
- Failed to reset Codex limit
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/ea7c9da2d5aa0b45.
Report an issue: GitHub.