From 6bff90edf78847ae84e72dc875c5191b11d5067e Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Tue, 17 Feb 2026 02:52:50 -0300 Subject: [PATCH] fix(token-refresh): detect Qwen invalid_request as unrecoverable error and switch broken test endpoints to checkExpiry - refreshQwenToken now detects 'invalid_request' and returns sentinel error - isUnrecoverableRefreshError expanded to match invalid_request - Switched Qwen, IFlow, Cline test configs to checkExpiry mode (endpoints were returning 404/400/stale-auth) - Fixed TypeScript errors in testSingleConnection (union type casts, Record) --- open-sse/services/tokenRefresh.js | 20 +++++++++++++- src/app/api/providers/[id]/test/route.ts | 34 ++++++++++-------------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/open-sse/services/tokenRefresh.js b/open-sse/services/tokenRefresh.js index 298c6cb203..6ac7ec4062 100644 --- a/open-sse/services/tokenRefresh.js +++ b/open-sse/services/tokenRefresh.js @@ -312,6 +312,23 @@ export async function refreshQwenToken(refreshToken, log) { }; } else { const errorText = await response.text().catch(() => ""); + + // Detect unrecoverable invalid_request (expired/revoked refresh token or bad client_id) + let errorCode = null; + try { + const parsed = JSON.parse(errorText); + errorCode = parsed?.error; + } catch { + // not JSON, ignore + } + + if (errorCode === "invalid_request") { + log?.error?.("TOKEN_REFRESH", "Qwen refresh token is invalid or expired. Re-authentication required.", { + status: response.status, + }); + return { error: "invalid_request" }; + } + log?.warn?.("TOKEN_REFRESH", `Error with Qwen endpoint`, { status: response.status, error: errorText, @@ -703,7 +720,8 @@ export function supportsTokenRefresh(provider) { * Callers should stop retrying and request re-authentication. */ export function isUnrecoverableRefreshError(result) { - return result && typeof result === "object" && result.error === "refresh_token_reused"; + return result && typeof result === "object" && + (result.error === "refresh_token_reused" || result.error === "invalid_request"); } /** diff --git a/src/app/api/providers/[id]/test/route.ts b/src/app/api/providers/[id]/test/route.ts index bd4270eb1c..70cd072a10 100644 --- a/src/app/api/providers/[id]/test/route.ts +++ b/src/app/api/providers/[id]/test/route.ts @@ -44,17 +44,15 @@ const OAUTH_TEST_CONFIG = { extraHeaders: { "User-Agent": "OmniRoute", Accept: "application/vnd.github+json" }, }, iflow: { - url: "https://iflow.cn/api/oauth/getUserInfo", - method: "GET", - authHeader: "Authorization", - authPrefix: "Bearer ", + // iFlow's getUserInfo endpoint returns 400 without a specific format. + // Use checkExpiry instead — actual connectivity is validated via real requests. + checkExpiry: true, refreshable: true, }, qwen: { - url: "https://portal.qwen.ai/v1/models", - method: "GET", - authHeader: "Authorization", - authPrefix: "Bearer ", + // portal.qwen.ai/v1/models returns 404 — endpoint no longer exists. + // Use checkExpiry instead — actual connectivity is validated via real requests. + checkExpiry: true, refreshable: true, }, cursor: { @@ -70,15 +68,11 @@ const OAUTH_TEST_CONFIG = { checkExpiry: true, }, cline: { - url: "https://api.cline.bot/api/v1/models", - method: "GET", - authHeader: "Authorization", - authPrefix: "Bearer ", + // Cline's /api/v1/models endpoint frequently returns stale auth errors even + // with fresh tokens. Use checkExpiry instead — actual connectivity is validated + // via real requests. + checkExpiry: true, refreshable: true, - extraHeaders: { - "HTTP-Referer": "https://cline.bot", - "X-Title": "Cline", - }, }, kiro: { checkExpiry: true, @@ -520,12 +514,12 @@ export async function testSingleConnection(connectionId) { const startTime = Date.now(); const runtime = await getProviderRuntimeStatus(connection.provider); - if (runtime?.diagnosis) { + if ((runtime as any)?.diagnosis) { result = { valid: false, - error: runtime.error, + error: (runtime as any).error, refreshed: false, - diagnosis: runtime.diagnosis, + diagnosis: (runtime as any).diagnosis, }; } else if (connection.authType === "apikey") { result = await testApiKeyConnection(connection); @@ -543,7 +537,7 @@ export async function testSingleConnection(connectionId) { ? makeDiagnosis("ok", "local", null, null) : classifyFailure({ error: result.error, statusCode: result.statusCode })); - const updateData = { + const updateData: Record = { testStatus: result.valid ? "active" : "error", lastError: result.valid ? null : result.error, lastErrorAt: result.valid ? null : now,