{"record":{"id":"6a4058e8a4c375c1","repo":"can1357/oh-my-pi","slug":"gitlab-oauth-refresh-failed-response-status","errorCode":null,"errorMessage":"GitLab OAuth refresh failed: ${response.status} ${await response.text()}","messagePattern":"GitLab OAuth refresh failed: (.+?) (.+?)","errorType":"http","errorClass":"AIError.OAuthError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/registry/oauth/gitlab-duo.ts","lineNumber":206,"sourceCode":"\tconst clientId = resolveClientId();\n\tconst options = resolveCallbackOptions();\n\tconst flow = new GitLabDuoOAuthFlow(callbacks, pkce, clientId, options);\n\treturn flow.login();\n}\n\nexport async function refreshGitLabDuoToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\tconst response = await fetch(`${GITLAB_COM_URL}/oauth/token`, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: resolveClientId(),\n\t\t\tgrant_type: \"refresh_token\",\n\t\t\trefresh_token: credentials.refresh,\n\t\t}).toString(),\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new AIError.OAuthError(`GitLab OAuth refresh failed: ${response.status} ${await response.text()}`, {\n\t\t\tkind: \"token-refresh\",\n\t\t\tprovider: \"gitlab-duo\",\n\t\t\tstatus: response.status,\n\t\t});\n\t}\n\n\tclearGitLabDuoDirectAccessCache();\n\treturn mapTokenResponse(\n\t\t(await response.json()) as {\n\t\t\taccess_token?: string;\n\t\t\trefresh_token?: string;\n\t\t\texpires_in?: number;\n\t\t\tcreated_at?: number;\n\t\t},\n\t);\n}\n","sourceCodeStart":188,"sourceCodeEnd":223,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/registry/oauth/gitlab-duo.ts#L188-L223","documentation":"refreshGitLabDuoToken exchanges the stored refresh token for a new access token at https://gitlab.com/oauth/token (grant_type=refresh_token) for the GitLab Duo provider. On any non-2xx response this OAuthError is thrown, embedding the HTTP status and GitLab's error body. The stored refresh token is no longer usable.","triggerScenarios":"Refresh POST returns !ok: expired/revoked refresh_token (invalid_grant), revoked application or user session, password change invalidating tokens, GITLAB_CLIENT_ID set to a value different from the one the token was issued to (invalid_client), or GitLab 5xx outage.","commonSituations":"Tokens idle past GitLab's refresh-token lifetime; user revoked the app in GitLab settings; GITLAB_CLIENT_ID changed after the original login so the client id no longer matches; automatic background refresh hitting a transient GitLab incident.","solutions":["Re-run loginGitLabDuo to get a fresh access/refresh token pair and update stored credentials.","If you changed GITLAB_CLIENT_ID, re-login so the refresh token is issued to the matching client id.","Check GitLab Profile > Applications and Active Sessions; re-authorize if revoked.","For 5xx statuses, retry after a delay — likely a transient GitLab outage.","Fall back to a Personal Access Token via GITLAB_TOKEN if OAuth cannot be completed."],"exampleFix":"// before: refresh assumes it never fails\ntokens = await refreshGitLabDuoToken(tokens);\n\n// after: detect invalid_grant and force re-login\ntry {\n  tokens = await refreshGitLabDuoToken(tokens);\n} catch (err) {\n  if (err.kind === \"token-refresh\" && /invalid_grant|expired/.test(err.message)) {\n    tokens = await loginGitLabDuo(callbacks);\n  } else {\n    throw err;\n  }\n}","handlingStrategy":"try-catch","validationCode":"if (!tokens?.refresh) {\n  throw new Error(\"no refresh token stored — run loginGitLabDuo first\");\n}","typeGuard":"function isRefreshable(c: unknown): c is { refresh: string } {\n  return typeof c === \"object\" && c !== null && typeof (c as { refresh?: unknown }).refresh === \"string\" && (c as { refresh: string }).refresh.length > 0;\n}","tryCatchPattern":"try {\n  tokens = await refreshGitLabDuoToken(tokens);\n} catch (err) {\n  const msg = String(err?.message ?? \"\");\n  if (err?.status === 400 && /invalid_grant|revoked|expired/i.test(msg)) {\n    tokens = await loginGitLabDuo(callbacks); // refresh token dead\n  } else if ((err?.status ?? 0) >= 500) {\n    await Bun.sleep(3000);\n    tokens = await refreshGitLabDuoToken(tokens);\n  } else {\n    throw err;\n  }\n}","preventionTips":["Do not change GITLAB_CLIENT_ID after login — refresh tokens are bound to the client id they were issued to.","Treat 400 invalid_grant as terminal: re-authenticate instead of retrying.","Persist refreshed tokens immediately so a crash doesn't lose the rotated refresh token.","Use refresh proactively before expiry (tokens are stored with a 5-minute safety margin)."],"tags":["oauth","gitlab","token-refresh","network"],"backgroundTag":"oauth-refresh-token-revoked","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}