{"record":{"id":"8574368cdd4ee692","repo":"can1357/oh-my-pi","slug":"gitlab-duo-workflow-oauth-refresh-failed-respon","errorCode":null,"errorMessage":"GitLab Duo Workflow OAuth refresh failed: ${response.status} ${await response.text()}","messagePattern":"GitLab Duo Workflow OAuth refresh failed: (.+?) (.+?)","errorType":"http","errorClass":"AIError.OAuthError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/registry/oauth/gitlab-duo-workflow.ts","lineNumber":128,"sourceCode":"}\n\nexport async function refreshGitLabDuoWorkflowToken(\n\tcredentials: OAuthCredentials,\n\tfetchImpl: FetchImpl = fetch,\n): Promise<OAuthCredentials> {\n\tconst response = await fetchImpl(`${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: GITLAB_DUO_WORKFLOW_OAUTH_CLIENT_ID,\n\t\t\tredirect_uri: GITLAB_DUO_WORKFLOW_OAUTH_REDIRECT_URI,\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(\n\t\t\t`GitLab Duo Workflow OAuth refresh failed: ${response.status} ${await response.text()}`,\n\t\t\t{\n\t\t\t\tkind: \"token-refresh\",\n\t\t\t\tprovider: \"gitlab-duo-workflow\",\n\t\t\t\tstatus: response.status,\n\t\t\t},\n\t\t);\n\t}\n\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}","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/registry/oauth/gitlab-duo-workflow.ts#L110-L146","documentation":"refreshGitLabDuoWorkflowToken POSTs a refresh_token grant to https://gitlab.com/oauth/token using GitLab's VS Code Workflow client id and the vscode:// redirect URI. When GitLab responds with a non-2xx status, this OAuthError is thrown with the HTTP status and the raw response body embedded in the message. It means the stored refresh token could no longer be exchanged for a new access token.","triggerScenarios":"The refresh request to /oauth/token returns !response.ok: expired or revoked refresh_token, token revoked by a GitLab password change or account session revocation, malformed credentials.refresh (empty/missing), or GitLab returning 4xx/5xx (invalid_client, invalid_grant, server errors).","commonSituations":"Long-lived sessions where the refresh token expired (GitLab refresh tokens can expire after inactivity); user revoked the app in GitLab Profile > Applications; admin disabled Duo Workflow; stored credentials were truncated by a config tool; transient GitLab 5xx outages during automatic token refresh.","solutions":["Re-run the GitLab Duo Workflow OAuth login (loginGitLabDuoWorkflow) to obtain fresh access/refresh tokens and replace the stored credentials.","Check GitLab Profile > Applications / Active Sessions and re-authorize if the application or token was revoked.","Verify the stored credentials object actually contains a non-empty refresh token; re-login if it is empty or malformed.","If the status is 5xx, retry after a short delay — it may be a transient GitLab outage.","As a fallback, authenticate via a GitLab Personal Access Token (GITLAB_TOKEN) if the provider supports it."],"exampleFix":"// before: assuming the stored refresh token is forever valid\nawait refreshGitLabDuoWorkflowToken(credentials);\n\n// after: catch the OAuthError and fall back to interactive re-login\ntry {\n  credentials = await refreshGitLabDuoWorkflowToken(credentials);\n} catch (err) {\n  if (err.kind === \"token-refresh\" && err.status === 401) {\n    credentials = await loginGitLabDuoWorkflow(callbacks); // re-authenticate\n  } else {\n    throw err;\n  }\n}","handlingStrategy":"try-catch","validationCode":"if (!credentials?.refresh || typeof credentials.refresh !== \"string\") {\n  throw new Error(\"cannot refresh: missing refresh token; re-run loginGitLabDuoWorkflow\");\n}","typeGuard":"function hasRefreshToken(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  credentials = await refreshGitLabDuoWorkflowToken(credentials);\n} catch (err) {\n  const status = err?.status;\n  if (status === 401 || status === 400) {\n    credentials = await loginGitLabDuoWorkflow(callbacks); // token dead, re-auth\n  } else if (status >= 500) {\n    await Bun.sleep(2000); // transient GitLab outage, retry once\n    credentials = await refreshGitLabDuoWorkflowToken(credentials);\n  } else {\n    throw err;\n  }\n}","preventionTips":["Persist refresh tokens durably and re-run login proactively when refresh fails with 400/401.","Never hand-edit or truncate stored credential files.","Watch GitLab status/incidents before blaming stored tokens on 5xx refresh failures.","Fall back to GITLAB_TOKEN (PAT) where OAuth re-login is impractical."],"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"}