fix(oauth): fall back to public Code Suggestions on any GitLab Duo direct_access 403 (#12958) - #13758
Merged
diegosouzapw merged 3 commits intoSep 16, 2026
Conversation
…rect_access 403 (#12958) Root cause: shouldFallbackToPublicCodeSuggestions() only treated a direct_access 403 as recoverable when the body contained GitLab's exact "direct connections are disabled" tenant-config message (#10365/#10499). An entitlement/scope-resolution 403 GitLab returns for an API-only client is a different failure class, so the public-completions fallback was never attempted even though the reporter's same token was accepted by that endpoint. Separately, the connection-test path read res.text() twice for gitlab-duo (once for the fallback decision, once for the error body), so the second read of an already-drained stream silently collapsed to "" and the real upstream error was replaced with a generic "Access denied". Fix: broaden the fallback predicate to any 401/403, remove the isGitLabDirectAccessDisabled() gate on the chat-path executor's hard-403 branch, and reuse the single body read in testOAuthConnection() so a 403 that fails both endpoints now surfaces GitLab's real (sanitized, capped) error text. Regression test: tests/unit/issue-12958-gitlab-duo-403-entitlement-fallback.test.ts (RED on unfixed code: fallback not attempted, body collapses to "Access denied"; GREEN after the fix). Extended tests/unit/executor-gitlab.test.ts with the same entitlement-403 case for the chat-path executor.
…8-gitlab-duo-direct-access-403
…03 (base-red fix #13747)
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
…rect_access 403 (diegosouzapw#12958) (diegosouzapw#13758) Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #12958
Root cause (short)
Two independent bugs in OmniRoute's own GitLab Duo code (not GitLab's server-side
behavior, which we can't fully verify without a live Duo-seat account):
shouldFallbackToPublicCodeSuggestions()only treated adirect_access403 asrecoverable when the body contained GitLab's exact "direct connections are disabled"
tenant-config message ([BUG] GitLab Duo: Token invalid or revoked #10365/fix(providers): fall back to public Code Suggestions endpoint on GitLab Duo direct_access 401 (#10365) #10499). The reporter's 403 is an entitlement/
scope-resolution failure — a different failure class — so the public Code
Suggestions fallback (which the reporter proved works with the same token) was
never attempted, and both the connection-test path and the real chat-path executor
hard-failed instead.
testOAuthConnection),res.text()was read once todecide the fallback and then read again to build the error message. A
fetchresponse body can only be read once, so the second read of an already-drained
stream silently resolved to
"", and the stored/surfaced error collapsed to ageneric
"Access denied"— discarding the real upstream body the operator needs totell an entitlement issue apart from an instance-config issue or a genuinely revoked
token.
Fix
src/lib/oauth/gitlab.ts: broadenedshouldFallbackToPublicCodeSuggestions()tostatus === 401 || status === 403(anydirect_access403, not only the tenant-config message).
isGitLabDirectAccessDisabled()is kept for log/diagnosticlabeling but no longer gates the fallback decision.
open-sse/executors/gitlab.ts(resolveRequestTarget()): removed the!isGitLabDirectAccessDisabled(...)guard around the hard-403 branch, so the realchat-path executor falls back to the public
completionsendpoint for the samebroadened set of 403s instead of hard-failing every non-tenant-config 403. Kept a
diagnostic log line distinguishing the two 403 sub-cases.
src/app/api/providers/[id]/test/route.ts(testOAuthConnection()): captured thesingle
res.text()read for gitlab-duo in an outer-scope variable and reused itfor the generic error-body selection instead of re-reading a drained stream. When a
403 fails both the
direct_accessand the public-fallback probe, the real upstreambody is now surfaced (trimmed of control characters, capped at 300 chars per
docs/security/ERROR_SANITIZATION.md— this is GitLab's own JSON error body, not astack trace, but capped/stripped defensively) instead of a hardcoded
"Access denied".Left the reporter's
namespace_path/rate-limit suggestions as a documented follow-up(not blocking this fix) — GitLab's public Code Suggestions API docs don't document any
accepted request-body field for
direct_access, and their troubleshooting docsdescribe the "multiple Duo namespaces / no default" case as a 422, not a 403, so
whether
namespace_pathchanges server-side entitlement resolution can only beverified against a live gitlab.com Duo-seat account.
Regression test
tests/unit/issue-12958-gitlab-duo-403-entitlement-fallback.test.ts(new):RED on unfixed code:
GREEN after the fix:
Also extended
tests/unit/executor-gitlab.test.tswith an equivalent case for thechat-path executor (
resolveRequestTarget()), confirming the fallback fires for anentitlement-flavored 403 there too — 8/8 pass.
Gates run
npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files>→ clean, 0 new warningsnpm run typecheck:core→ cleannpm run check:open-sse-typecheck→openSseTypecheckErrors=0node scripts/check/check-file-size.mjs→ 1 pre-existing violation onopen-sse/utils/stream.ts, untouched by this PR (base drift, not mine)node scripts/check/check-test-discovery.mjs→ OK, new test file discoverednode scripts/check/check-complexity.mjs→ OK (2825 violations vs baseline 3218)node scripts/check/check-cognitive-complexity.mjs→ OK (1276 violations vs baseline 1437)tests/unit/issue-12958-gitlab-duo-403-entitlement-fallback.test.ts→ 2/2 passtests/unit/executor-gitlab.test.ts→ 8/8 pass (7 pre-existing + 1 new)tests/unit/gitlab-duo-oauth-test-401-fallback.test.ts→ 3/3 pass (unaffected pre-existing 401/403-disabled fallback contract still holds)Existing tests aligned
None weakened or removed.
tests/unit/executor-gitlab.test.tsgot one new test case;the pre-existing 401/403-disabled fallback tests in both files pass unchanged, confirming
the broadened predicate is additive (still covers the original two cases plus the new one).