mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 15:52:52 +03:00
fix(healthcheck): per-provider proactive-refresh skip list (rescue short-TTL OAuth) (#3159)
Per-provider proactive-refresh skip list (OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS) to rescue short-TTL OAuth. Integrated into release/v3.8.10.
This commit is contained in:
committed by
GitHub
parent
1be9606cc2
commit
42df1703c6
@@ -1347,6 +1347,12 @@ APP_LOG_TO_FILE=true
|
||||
# Disable the OAuth token healthcheck loop during tests (default: true).
|
||||
# OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK=true
|
||||
|
||||
# Exclude specific providers from the PROACTIVE token-refresh sweep (comma-separated,
|
||||
# case-insensitive). Targeted alternative to OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK: keeps
|
||||
# rotating-cascade providers (Codex/OpenAI share one Auth0 family) on the reactive 401
|
||||
# path only, while short-TTL providers like Kimi-coding keep being refreshed proactively.
|
||||
# OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS=codex,openai
|
||||
|
||||
# Silence healthcheck noise in Playwright stdout (default: true).
|
||||
# OMNIROUTE_HIDE_HEALTHCHECK_LOGS=true
|
||||
|
||||
|
||||
@@ -911,6 +911,7 @@ value below unset in production deployments.
|
||||
| `OMNIROUTE_E2E_PASSWORD` | falls back to `INITIAL_PASSWORD` | `scripts/dev/run-next-playwright.mjs` | Admin password injected into the Playwright environment. |
|
||||
| `OMNIROUTE_DISABLE_LOCAL_HEALTHCHECK` | `true` | `scripts/dev/run-next-playwright.mjs` | Disable the local healthcheck poll during Playwright runs. |
|
||||
| `OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK` | `true` | `scripts/dev/run-next-playwright.mjs` | Disable the OAuth token healthcheck loop during tests. |
|
||||
| `OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS` | _(unset)_ | `src/lib/tokenHealthCheck.ts` | Comma-separated providers excluded from the proactive token-refresh sweep (e.g. `codex,openai`). Targeted alternative to fully disabling the healthcheck — short-TTL providers keep refreshing while cascade providers stay reactive-only. |
|
||||
| `OMNIROUTE_HIDE_HEALTHCHECK_LOGS` | `true` | `scripts/dev/run-next-playwright.mjs` | Silence healthcheck noise in Playwright stdout. |
|
||||
| `OMNIROUTE_PLAYWRIGHT_SKIP_BUILD` | `0` | `scripts/dev/run-next-playwright.mjs` | Skip the Next.js production build before Playwright starts (CI optimization). |
|
||||
| `OMNIROUTE_SKIP_UNINSTALL_HOOK` | `0` | `scripts/build/uninstall.mjs` | Skip the OmniRoute uninstall hook (used by CI to keep `node_modules` intact). |
|
||||
|
||||
@@ -109,6 +109,25 @@ function isHealthCheckDisabled(): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Providers excluded from the PROACTIVE refresh sweep, comma-separated and
|
||||
* case-insensitive (e.g. "codex,openai"). A targeted alternative to the blunt
|
||||
* OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK switch: it lets an operator keep the
|
||||
* rotating-token cascade providers (Codex/OpenAI share one Auth0 family) off the
|
||||
* proactive sweep — leaving their refresh to the reactive, serialized 401 path —
|
||||
* WITHOUT also starving short-TTL providers like Kimi-coding, whose tokens expire
|
||||
* while idle when the whole sweep is disabled.
|
||||
*/
|
||||
function getHealthCheckSkipProviders(): Set<string> {
|
||||
const raw = process.env.OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS || "";
|
||||
return new Set(
|
||||
raw
|
||||
.split(",")
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
);
|
||||
}
|
||||
|
||||
// ── Logging helper ───────────────────────────────────────────────────────────
|
||||
let cachedHideLogs: boolean | null = null;
|
||||
let cacheTimestamp = 0;
|
||||
@@ -265,6 +284,13 @@ export async function checkConnection(conn) {
|
||||
const latestConnection = (await getProviderConnectionById(conn.id)) || conn;
|
||||
conn = latestConnection;
|
||||
|
||||
// Per-provider opt-out of proactive refresh (e.g. Codex/OpenAI cascade
|
||||
// providers) — their token stays on the reactive, serialized 401 path while
|
||||
// other providers keep being refreshed proactively.
|
||||
if (getHealthCheckSkipProviders().has(String(conn.provider || "").toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine interval (0 = disabled)
|
||||
const intervalMin = conn.healthCheckInterval ?? DEFAULT_HEALTH_CHECK_INTERVAL_MIN;
|
||||
if (intervalMin <= 0) return;
|
||||
|
||||
@@ -398,3 +398,74 @@ test("checkConnection skips interval refresh when token expiry is known and stil
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("checkConnection skips providers listed in OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS (#kimi-15)", async () => {
|
||||
await resetStorage();
|
||||
|
||||
const providerId = "custom-oauth-skip-list";
|
||||
const refreshRequests: string[] = [];
|
||||
const prevSkip = process.env.OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS;
|
||||
|
||||
await withHttpServer(
|
||||
(req, res) => {
|
||||
let body = "";
|
||||
req.setEncoding("utf8");
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
req.on("end", () => {
|
||||
refreshRequests.push(body);
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
access_token: "should-not-be-fetched",
|
||||
refresh_token: "should-not-be-fetched",
|
||||
expires_in: 3600,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
async (tokenServer) => {
|
||||
await withPatchedProvider(
|
||||
providerId,
|
||||
{
|
||||
tokenUrl: `${tokenServer.url}/token`,
|
||||
clientId: "skip-client-id",
|
||||
clientSecret: "skip-client-secret",
|
||||
},
|
||||
async () => {
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: providerId,
|
||||
authType: "oauth",
|
||||
name: "Skip-list Account",
|
||||
email: "skip@example.com",
|
||||
accessToken: "stale-access-token",
|
||||
refreshToken: "refresh-token-skip",
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
// The connection is due for refresh (no known expiry, never checked).
|
||||
// With the provider listed, the proactive sweep must skip it entirely —
|
||||
// NO refresh request is made.
|
||||
process.env.OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS = `foo, ${providerId} ,bar`;
|
||||
await tokenHealthCheck.checkConnection(connection);
|
||||
assert.equal(
|
||||
refreshRequests.length,
|
||||
0,
|
||||
"listed provider must NOT trigger a proactive refresh"
|
||||
);
|
||||
|
||||
// Control: with the provider no longer listed, the same due connection
|
||||
// IS refreshed — proving the skip (not token freshness) gated it.
|
||||
process.env.OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS = "some-other-provider";
|
||||
const stillStale = await providersDb.getProviderConnectionById((connection as any).id);
|
||||
await tokenHealthCheck.checkConnection(stillStale);
|
||||
assert.equal(refreshRequests.length, 1, "non-listed provider must refresh");
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if (prevSkip === undefined) delete process.env.OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS;
|
||||
else process.env.OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS = prevSkip;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user