{"record":{"id":"63a49ad5ca82db9b","repo":"can1357/oh-my-pi","slug":"gitlab-duo-workflow-direct-access-failed-with-http","errorCode":null,"errorMessage":"GitLab Duo Workflow direct_access failed with HTTP ${response.status}: ${message} (or, when the body carries no message: GitLab Duo Workflow direct_access failed with HTTP ${response.status})","messagePattern":"GitLab Duo Workflow direct_access failed with HTTP (.+?): (.+?) \\(or, when the body carries no message: GitLab Duo Workflow direct_access failed with HTTP (.+?)\\)","errorType":"http","errorClass":"AIError.GitLabDuoWorkflowApiError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/providers/gitlab-duo-workflow.ts","lineNumber":1664,"sourceCode":"\t\t},\n\t\tbody: JSON.stringify(buildGitLabDuoWorkflowDirectAccessBody(rootNamespaceId, projectId, workflowDefinition)),\n\t\tsignal: gitLabDuoWorkflowRestSignal(signal),\n\t});\n\ttraceGitLabDuoWorkflow(\"direct_access.response\", {\n\t\tstatus: response.status,\n\t\tok: response.ok,\n\t\trootNamespaceId,\n\t\thasProjectId: Boolean(projectId),\n\t});\n\tif (!response.ok) {\n\t\tconst message = await readGitLabDuoWorkflowResponseErrorMessage(response);\n\t\t// Always embed the HTTP status, even when the body carries a message: the\n\t\t// streaming auth-retry/rotation path (`extractStatusFromAssistantError` ->\n\t\t// `extractHttpStatusFromError`) refreshes/rotates broker credentials only\n\t\t// when the assistant error exposes `errorStatus` or the message embeds an\n\t\t// `HTTP <status>` token. A 401 `{\"message\":\"Unauthorized\"}` or a 429 quota\n\t\t// body would otherwise surface as a hard failure with no recoverable status.\n\t\tthrow new AIError.GitLabDuoWorkflowApiError(\n\t\t\tmessage\n\t\t\t\t? `GitLab Duo Workflow direct_access failed with HTTP ${response.status}: ${message}`\n\t\t\t\t: `GitLab Duo Workflow direct_access failed with HTTP ${response.status}`,\n\t\t\tresponse.status,\n\t\t);\n\t}\n\tconst payload = (await response.json()) as GitLabDirectAccessResponse;\n\tconst token = extractGitLabWorkflowToken(payload);\n\tif (!token) {\n\t\tthrow new AIError.ProviderResponseError(\"GitLab Duo Workflow direct_access did not return credentials\", {\n\t\t\tprovider: \"gitlab-duo-agent\",\n\t\t\tkind: \"empty-body\",\n\t\t});\n\t}\n\ttraceGitLabDuoWorkflow(\"direct_access.token\", { hasToken: true });\n\tconst serviceEndpoint = !payload.gitlab_rails?.token && Boolean(payload.duo_workflow_service?.base_url);\n\treturn {\n\t\ttoken,","sourceCodeStart":1646,"sourceCodeEnd":1682,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/providers/gitlab-duo-workflow.ts#L1646-L1682","documentation":"Thrown when the GitLab Duo Workflow direct_access endpoint returns a non-2xx status. If the response body carries a JSON message it is appended; otherwise the status alone is embedded. Per the source comment, the message always embeds 'HTTP <status>' so the streaming auth-retry path (extractStatusFromAssistantError → extractHttpStatusFromError) can detect the status and refresh/rotate broker credentials on 401/429 instead of failing hard.","triggerScenarios":"POST to the Duo Workflow direct_access credentials endpoint returns 401 (expired/invalid GitLab token), 403 (insufficient Duo Workflow entitlement), 429 (rate limited), or 5xx.","commonSituations":"GitLab personal access token expired or lacking api scope; GitLab tier without Duo Workflow enabled; GitLab.com rate limiting; GitLab instance version mismatch with the direct_access route.","solutions":["Check the HTTP status in the message: 401/403 → re-issue the GitLab token with api scope and Duo Workflow entitlement","The auth-retry path only fires when the status is embedded — if you catch this error, inspect errorStatus and retry with refreshed credentials","For 429, back off and retry after the rate-limit window","For 5xx, check GitLab service status and retry later"],"exampleFix":"// before: stale token\napiKey: process.env.OLD_GITLAB_TOKEN\n// after: freshly minted token with api scope + Duo Workflow enabled\napiKey: process.env.GITLAB_TOKEN","handlingStrategy":"try-catch","validationCode":"// Preflight entitlement/token check:\nconst res = await fetch(`${gitlabBaseUrl}/api/v4/user`, { headers: { authorization: `Bearer ${token}` } });\nif (res.status === 401) throw new Error(\"GitLab token invalid — re-issue with api scope\");","typeGuard":"function isGitlabDuoApiError(err: unknown): err is InstanceType<typeof AIError.GitLabDuoWorkflowApiError> {\n  return err instanceof AIError.GitLabDuoWorkflowApiError && typeof err.status === \"number\";\n}","tryCatchPattern":"try {\n  await runGitLabDuoWorkflow(model, context, options, state);\n} catch (err) {\n  if (err instanceof AIError.GitLabDuoWorkflowApiError) {\n    // message embeds `HTTP <status>` so extractHttpStatusFromError works:\n    const status = err.status;\n    if (status === 401) await refreshBrokerCredentials(); // then retry\n    else if (status === 429) await Bun.sleep(rateLimitBackoff);\n    else throw err;\n  } else throw err;\n}","preventionTips":["Keep the GitLab PAT fresh and scoped with api access","Confirm Duo Workflow entitlement on your GitLab plan before integration","Respect the embedded HTTP status: the message is designed to be machine-parsed for retry/rotation","Handle 429 with real backoff rather than tight retries"],"tags":["gitlab","http","auth","entitlement"],"backgroundTag":"http-auth-error","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}