mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-22 06:42:19 +03:00
fix(oauth): fall back to public Code Suggestions on any GitLab Duo direct_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.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- **fix(providers):** GitLab Duo Retest and chat requests now fall back to the public Code Suggestions endpoint for ANY `direct_access` 403 (not only the "direct connections are disabled" tenant-config message), and surface the real upstream error body instead of a generic "Access denied" when both endpoints reject the token (#12958) — thanks @Rahulsharma0810
|
||||
@@ -599,12 +599,17 @@ export class GitlabExecutor extends BaseExecutor {
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status === 403 && !isGitLabDirectAccessDisabled(response.status, bodyText)) {
|
||||
return {
|
||||
target: null,
|
||||
credentials,
|
||||
errorResponse: toOpenAIError(403, "GitLab Duo direct access scope is unavailable"),
|
||||
};
|
||||
// #12958: any direct_access 403 (not only GitLab's exact "direct connections
|
||||
// are disabled" tenant-config message) is recoverable via the public
|
||||
// completions fallback — mirrors the 401 branch above and the connection-test
|
||||
// path's shouldFallbackToPublicCodeSuggestions() contract.
|
||||
if (response.status === 403 && input.log) {
|
||||
input.log.warn(
|
||||
"GITLAB-DUO",
|
||||
isGitLabDirectAccessDisabled(response.status, bodyText)
|
||||
? "direct_access exchange rejected (403, direct connections disabled); falling back to public completions endpoint"
|
||||
: `direct_access exchange rejected (403); falling back to public completions endpoint. Body: ${bodyText.slice(0, 500)}`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -239,6 +239,16 @@ function isTokenExpired(connection: any) {
|
||||
return expiresAt <= Date.now() + buffer;
|
||||
}
|
||||
|
||||
// #12958: GitLab's own `direct_access` 403 JSON body (e.g. `{"error":"insufficient_scope"}`)
|
||||
// is safe operator-facing diagnostic text — it is not a stack trace and does not echo the
|
||||
// token — but is capped and stripped of control characters defensively before it reaches
|
||||
// the stored/surfaced error message, per docs/security/ERROR_SANITIZATION.md.
|
||||
function sanitizeUpstreamBodyText(bodyText: string): string {
|
||||
const collapsed = bodyText.replace(/[\r\n\t | ||||