mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(usage): proxy fallback — retry without proxy when SOCKS5 relay fails
Root cause: SOCKS5 proxies accept TCP connections (pass health check) but
can't relay HTTPS traffic. getCodexUsage() catches fetch errors internally
and returns {message: 'Failed to fetch...'} instead of throwing, so the
previous catch-based fallback never triggered.
Fix: After the initial proxied fetch, check the returned usage object for
network error indicators. If a proxy was active and the result contains
'fetch failed' / 'ECONNREFUSED' / etc., retry the entire operation
(credential refresh + usage fetch) without proxy context.
This is safe because usage fetching is read-only — showing limits data
without proxy is better than showing nothing.
This commit is contained in:
@@ -135,50 +135,98 @@ export async function GET(
|
||||
// so that both credential refresh AND usage fetch go through the proxy.
|
||||
const proxyInfo = await resolveProxyForConnection(connectionId);
|
||||
|
||||
// Wrap BOTH credential refresh and usage fetch inside proxy context.
|
||||
// Codex accounts behind SOCKS5 proxies need the proxy active during token refresh too.
|
||||
const { usage, refreshed } = (await runWithProxyContext(proxyInfo?.proxy || null, async () => {
|
||||
let conn = connection;
|
||||
let wasRefreshed = false;
|
||||
// Helper: perform credential refresh + usage fetch
|
||||
const fetchUsageWithContext = async (proxyConfig: unknown) => {
|
||||
return runWithProxyContext(proxyConfig, async () => {
|
||||
let conn = connection;
|
||||
let wasRefreshed = false;
|
||||
|
||||
// Refresh credentials if needed using executor
|
||||
try {
|
||||
const result = await refreshAndUpdateCredentials(conn);
|
||||
conn = result.connection;
|
||||
wasRefreshed = result.refreshed;
|
||||
// Refresh credentials if needed using executor
|
||||
try {
|
||||
const result = await refreshAndUpdateCredentials(conn);
|
||||
conn = result.connection;
|
||||
wasRefreshed = result.refreshed;
|
||||
|
||||
// Sync to cloud only if token was refreshed
|
||||
if (wasRefreshed) {
|
||||
await syncToCloudIfEnabled();
|
||||
// Sync to cloud only if token was refreshed
|
||||
if (wasRefreshed) {
|
||||
await syncToCloudIfEnabled();
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.error("[Usage API] Credential refresh failed:", refreshError);
|
||||
throw refreshError;
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.error("[Usage API] Credential refresh failed:", refreshError);
|
||||
throw refreshError;
|
||||
}
|
||||
|
||||
// Fetch usage from provider API
|
||||
const usageData = await getUsageForProvider(conn);
|
||||
connection = conn; // propagate updated connection for status sync below
|
||||
return { usage: usageData, refreshed: wasRefreshed };
|
||||
}).catch((refreshError: any) => {
|
||||
// If error originated from credential refresh, return 401
|
||||
if (
|
||||
refreshError?.message?.includes?.("refresh") ||
|
||||
refreshError?.message?.includes?.("Credential")
|
||||
) {
|
||||
return { __authError: true, message: refreshError.message };
|
||||
}
|
||||
throw refreshError;
|
||||
})) as any;
|
||||
// Fetch usage from provider API
|
||||
const usageData = await getUsageForProvider(conn);
|
||||
connection = conn; // propagate updated connection for status sync below
|
||||
return { usage: usageData, refreshed: wasRefreshed };
|
||||
});
|
||||
};
|
||||
|
||||
// Handle auth errors from credential refresh
|
||||
if (usage?.__authError) {
|
||||
return Response.json(
|
||||
{ error: `Credential refresh failed: ${usage.message}` },
|
||||
{ status: 401 }
|
||||
// Check if a usage result indicates a network-level error (proxy can't relay)
|
||||
const isNetworkFailure = (usageResult: any): boolean => {
|
||||
const msg = usageResult?.usage?.message;
|
||||
if (typeof msg !== "string") return false;
|
||||
return (
|
||||
msg.includes("fetch failed") ||
|
||||
msg.includes("ECONNREFUSED") ||
|
||||
msg.includes("ETIMEDOUT") ||
|
||||
msg.includes("Proxy unreachable") ||
|
||||
msg.includes("UND_ERR_CONNECT_TIMEOUT")
|
||||
);
|
||||
};
|
||||
|
||||
let result: any;
|
||||
const proxyConfig = proxyInfo?.proxy || null;
|
||||
try {
|
||||
result = await fetchUsageWithContext(proxyConfig);
|
||||
} catch (proxyError: any) {
|
||||
const isAuthError =
|
||||
proxyError?.message?.includes?.("refresh") || proxyError?.message?.includes?.("Credential");
|
||||
|
||||
if (isAuthError) {
|
||||
return Response.json(
|
||||
{ error: `Credential refresh failed: ${proxyError.message}` },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// If proxy was active and it's a network error (thrown), retry without proxy
|
||||
const isThrownNetworkError =
|
||||
proxyError?.message === "fetch failed" ||
|
||||
proxyError?.code === "PROXY_UNREACHABLE" ||
|
||||
proxyError?.code === "UND_ERR_CONNECT_TIMEOUT" ||
|
||||
proxyError?.cause?.code === "ECONNREFUSED";
|
||||
|
||||
if (proxyConfig && isThrownNetworkError) {
|
||||
console.warn(
|
||||
`[Usage API] Proxy fetch threw for ${connectionId}, retrying without proxy:`,
|
||||
proxyError?.message
|
||||
);
|
||||
result = await fetchUsageWithContext(null);
|
||||
} else {
|
||||
throw proxyError;
|
||||
}
|
||||
}
|
||||
|
||||
// If the usage result contains a network error AND a proxy was active,
|
||||
// retry without proxy. getCodexUsage() catches fetch errors internally
|
||||
// and returns {message: "Failed to fetch..."} instead of throwing.
|
||||
if (proxyConfig && isNetworkFailure(result)) {
|
||||
console.warn(
|
||||
`[Usage API] Proxy usage returned network error for ${connectionId}, retrying without proxy:`,
|
||||
result.usage?.message
|
||||
);
|
||||
try {
|
||||
result = await fetchUsageWithContext(null);
|
||||
} catch (directError: any) {
|
||||
console.error("[Usage API] Direct fetch also failed:", directError?.message);
|
||||
throw directError;
|
||||
}
|
||||
}
|
||||
|
||||
const { usage, refreshed } = result;
|
||||
|
||||
// Populate quota cache for quota-aware account selection
|
||||
if (isRecord(usage?.quotas)) {
|
||||
setQuotaCache(
|
||||
|
||||
Reference in New Issue
Block a user