{"record":{"id":"669cc50dbf7e6d26","repo":"can1357/oh-my-pi","slug":"gitlab-duo-workflow-direct-access-did-not-return-c","errorCode":null,"errorMessage":"GitLab Duo Workflow direct_access did not return credentials","messagePattern":"GitLab Duo Workflow direct_access did not return credentials","errorType":"exception","errorClass":"AIError.ProviderResponseError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/providers/gitlab-duo-workflow.ts","lineNumber":1674,"sourceCode":"\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,\n\t\t...(serviceEndpoint && payload.duo_workflow_service?.base_url\n\t\t\t? { baseUrl: normalizeGitLabDuoWorkflowServiceBaseUrl(payload.duo_workflow_service.base_url) }\n\t\t\t: {}),\n\t\theaders: serviceEndpoint ? (payload.duo_workflow_service?.headers ?? {}) : {},\n\t\tserviceEndpoint,\n\t};\n}\n\nasync function createGitLabDuoWorkflow(\n\tfetchImpl: FetchImpl,","sourceCodeStart":1656,"sourceCodeEnd":1692,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/providers/gitlab-duo-workflow.ts#L1656-L1692","documentation":"Thrown when the direct_access endpoint returns HTTP 200 but the JSON payload contains no usable workflow token — extractGitLabWorkflowToken finds nothing in payload.gitlab_rails, payload.duo_workflow_service, etc. Without credentials the client cannot talk to the Duo Workflow service, so it fails with a ProviderResponseError (kind: empty-body).","triggerScenarios":"direct_access responds ok but the body lacks any recognized token fields — Duo Workflow service not provisioned for the namespace, GitLab version returning a different response shape, or an empty object returned by a proxy.","commonSituations":"GitLab project/group without Duo Workflow activated; older self-managed GitLab whose direct_access response schema differs from what the client expects; intermediary (proxy/gateway) swallowing the body.","solutions":["Confirm Duo Workflow is enabled for your GitLab instance and project/namespace","Compare your GitLab version's direct_access response shape with what the client expects; upgrade pi-ai or GitLab as needed","Check for a proxy stripping the response body and bypass it","Call direct_access manually with curl to inspect the raw JSON payload"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Confirm Duo Workflow is provisioned before calling direct_access:\nconst res = await fetch(`${gitlabBaseUrl}/api/v4/profile`, { headers: { authorization: `Bearer ${token}` } });\nconst profile = await res.json();\nif (!profile) throw new Error(\"GitLab token has no profile access — Duo Workflow likely unprovisioned\");","typeGuard":"function hasWorkflowToken(p: unknown): p is GitLabDirectAccessResponse & { token: string } {\n  return typeof p === \"object\" && p !== null && extractGitLabWorkflowToken(p as GitLabDirectAccessResponse) !== undefined;\n}","tryCatchPattern":"try {\n  await requestDirectAccess(...);\n} catch (err) {\n  if (err instanceof AIError.ProviderResponseError && err.message.includes(\"did not return credentials\")) {\n    throw new Error(\"Duo Workflow credentials missing — enable Duo Workflow for this namespace/project or check GitLab version\");\n  }\n  throw err;\n}","preventionTips":["Enable Duo Workflow on your GitLab project/namespace before using the provider","Match GitLab server version to what the pi-ai client expects","Bypass stripping proxies for the direct_access route","Verify with curl that direct_access returns token fields before wiring up the client"],"tags":["gitlab","empty-response","credentials","provider-response"],"backgroundTag":"empty-response-body","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}