{"record":{"id":"af56988f17331c47","repo":"can1357/oh-my-pi","slug":"gitlab-oauth-token-response-missing-required-field","errorCode":null,"errorMessage":"GitLab OAuth token response missing required fields","messagePattern":"GitLab OAuth token response missing required fields","errorType":"validation","errorClass":"AIError.OAuthError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/registry/oauth/gitlab-duo.ts","lineNumber":100,"sourceCode":"\n\tconst port = parsed.port ? Number.parseInt(parsed.port, 10) : parsed.protocol === \"https:\" ? 443 : 80;\n\n\treturn {\n\t\tpreferredPort: isLoopback ? port : 0,\n\t\tcallbackPath: parsed.pathname || DEFAULT_CALLBACK_PATH,\n\t\tcallbackHostname: isLoopback ? parsed.hostname : DEFAULT_CALLBACK_HOSTNAME,\n\t\tredirectUri: raw,\n\t};\n}\n\nfunction mapTokenResponse(payload: {\n\taccess_token?: string;\n\trefresh_token?: string;\n\texpires_in?: number;\n\tcreated_at?: number;\n}): OAuthCredentials {\n\tif (!payload.access_token || !payload.refresh_token || typeof payload.expires_in !== \"number\") {\n\t\tthrow new AIError.OAuthError(\"GitLab OAuth token response missing required fields\", {\n\t\t\tkind: \"validation\",\n\t\t\tprovider: \"gitlab-duo\",\n\t\t});\n\t}\n\n\tconst createdAtMs =\n\t\ttypeof payload.created_at === \"number\" && Number.isFinite(payload.created_at)\n\t\t\t? payload.created_at * 1000\n\t\t\t: Date.now();\n\n\treturn {\n\t\taccess: payload.access_token,\n\t\trefresh: payload.refresh_token,\n\t\texpires: createdAtMs + payload.expires_in * 1000 - 5 * 60 * 1000,\n\t};\n}\n\nclass GitLabDuoOAuthFlow extends OAuthCallbackFlow {","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/registry/oauth/gitlab-duo.ts#L82-L118","documentation":"mapTokenResponse validates the JSON body GitLab returned from the OAuth token endpoint. If access_token or refresh_token is missing/empty, or expires_in is not a number, this validation OAuthError is thrown. GitLab technically succeeded (HTTP 2xx) but the payload does not match the OAuth token-response contract the library requires.","triggerScenarios":"Called by exchangeToken (after the authorization-code exchange) or refreshGitLabDuoToken (after a refresh grant) when GitLab returns 200 with a body lacking access_token, refresh_token, or a numeric expires_in — e.g. an HTML page behind a proxy, an error JSON with 200, or a truncated response.","commonSituations":"Corporate proxies/interception returning HTML with 200; GitLab instance (self-hosted redirect misconfig) returning unexpected shapes; response.json() succeeding on an error envelope; changed GitLab API behavior or scope restrictions silently dropping fields.","solutions":["Inspect the actual response body (log it before parsing) to see what GitLab returned instead of the expected token fields.","Re-run the GitLab Duo OAuth login to get a clean token exchange.","Check for proxy/VPN/SSL-interception that could rewrite the token endpoint response; bypass it or trust its CA.","If you are on a self-hosted GitLab, verify the instance's OAuth token endpoint behaves per GitLab's documented API."],"exampleFix":"// before: blind cast, cryptic failure\nconst payload = (await response.json()) as TokenResponse;\n\n// after: validate before handing to the flow\nconst payload = await response.json();\nif (typeof payload.access_token !== \"string\" || typeof payload.expires_in !== \"number\") {\n  console.error(\"unexpected token response:\", payload); // inspect, then re-login\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function isGitLabTokenPayload(p: unknown): p is { access_token: string; refresh_token: string; expires_in: number; created_at?: number } {\n  if (typeof p !== \"object\" || p === null) return false;\n  const o = p as Record<string, unknown>;\n  return typeof o.access_token === \"string\" && o.access_token.length > 0\n    && typeof o.refresh_token === \"string\" && o.refresh_token.length > 0\n    && typeof o.expires_in === \"number\";\n}","tryCatchPattern":"try {\n  tokens = await loginGitLabDuo(callbacks);\n} catch (err) {\n  if (err?.kind === \"validation\" && String(err.message).includes(\"missing required fields\")) {\n    // GitLab returned 200 with an unexpected body — check proxy/interception, then retry login\n    await inspectAndReportTokenEndpointResponse();\n    tokens = await loginGitLabDuo(callbacks);\n  } else {\n    throw err;\n  }\n}","preventionTips":["Log raw token-endpoint bodies (redacted) when validation fails to spot proxy/HTML interference.","Bypass SSL-intercepting corporate proxies for gitlab.com or trust their CA.","Re-login rather than reusing credentials after a malformed token response.","Keep the library updated for GitLab API shape changes."],"tags":["oauth","gitlab","validation","token-response"],"backgroundTag":"malformed-token-response","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}