Merge pull request #60 from diegosouzapw/fix/token-refresh-connection-tests

fix(token-refresh): detect Qwen invalid_request as unrecoverable & switch broken test endpoints to checkExpiry
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-02-17 07:08:50 -03:00
committed by GitHub
2 changed files with 33 additions and 21 deletions

View File

@@ -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");
}
/**

View File

@@ -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<string, any> = {
testStatus: result.valid ? "active" : "error",
lastError: result.valid ? null : result.error,
lastErrorAt: result.valid ? null : now,