From 97f2a00e74c470e97205cf3bd7d0457c7e304511 Mon Sep 17 00:00:00 2001 From: Ryan <215719061+East-rayyy@users.noreply.github.com> Date: Mon, 23 Mar 2026 06:29:28 +0300 Subject: [PATCH] fix: test Codex connection against actual endpoint (#347) Change Codex test from token-expiry-only check to probing the real Codex API endpoint. Sends a minimal request body that triggers a fast 400 without consuming quota. A 400 confirms auth works; only 401/403 indicates a bad token. Also adds generic acceptStatuses support to the OAuth test framework so other providers can define non-200 success statuses. --- src/app/api/providers/[id]/test/testUtils.js | 30 ++++++++++++++------ 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/app/api/providers/[id]/test/testUtils.js b/src/app/api/providers/[id]/test/testUtils.js index 1c7f19db..08f97ed0 100644 --- a/src/app/api/providers/[id]/test/testUtils.js +++ b/src/app/api/providers/[id]/test/testUtils.js @@ -18,7 +18,18 @@ import { buildClineHeaders } from "@/shared/utils/clineAuth"; // OAuth provider test endpoints const OAUTH_TEST_CONFIG = { claude: { checkExpiry: true, refreshable: true }, - codex: { checkExpiry: true, refreshable: true }, + codex: { + url: "https://chatgpt.com/backend-api/codex/responses", + method: "POST", + authHeader: "Authorization", + authPrefix: "Bearer ", + extraHeaders: { "Content-Type": "application/json", "originator": "codex-cli", "User-Agent": "codex-cli/1.0.18 (macOS; arm64)" }, + // Minimal invalid body — triggers fast 400 without consuming quota + body: JSON.stringify({ model: "gpt-5.3-codex", input: [], stream: false, store: false }), + // 400 (bad request) means auth succeeded; only 401/403 means token is bad + acceptStatuses: [400], + refreshable: true, + }, "gemini-cli": { url: "https://www.googleapis.com/oauth2/v1/userinfo?alt=json", method: "GET", @@ -263,9 +274,12 @@ async function testOAuthConnection(connection, effectiveProxy = null) { const headers = config.noAuth ? { ...config.extraHeaders } : { [config.authHeader]: `${config.authPrefix}${accessToken}`, ...config.extraHeaders }; - const res = await fetchWithConnectionProxy(testUrl, { method: config.method, headers }, effectiveProxy); + const fetchOpts = { method: config.method, headers }; + if (config.body) fetchOpts.body = config.body; + const res = await fetchWithConnectionProxy(testUrl, fetchOpts, effectiveProxy); - if (res.ok) return { valid: true, error: null, refreshed, newTokens }; + const accepted = res.ok || (config.acceptStatuses && config.acceptStatuses.includes(res.status)); + if (accepted) return { valid: true, error: null, refreshed, newTokens }; if (res.status === 401 && config.refreshable && !refreshed && connection.refreshToken) { const tokens = await refreshOAuthToken(connection); @@ -274,11 +288,11 @@ async function testOAuthConnection(connection, effectiveProxy = null) { const retryHeaders = config.noAuth ? { ...config.extraHeaders } : { [config.authHeader]: `${config.authPrefix}${tokens.accessToken}`, ...config.extraHeaders }; - const retryRes = await fetchWithConnectionProxy(retryUrl, { - method: config.method, - headers: retryHeaders, - }, effectiveProxy); - if (retryRes.ok) return { valid: true, error: null, refreshed: true, newTokens: tokens }; + const retryOpts = { method: config.method, headers: retryHeaders }; + if (config.body) retryOpts.body = config.body; + const retryRes = await fetchWithConnectionProxy(retryUrl, retryOpts, effectiveProxy); + const retryAccepted = retryRes.ok || (config.acceptStatuses && config.acceptStatuses.includes(retryRes.status)); + if (retryAccepted) return { valid: true, error: null, refreshed: true, newTokens: tokens }; } return { valid: false, error: "Token invalid or revoked", refreshed: false }; }