mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
fix(providers): eliminate HTTP self-calls in batch test for VPS compatibility
- Extract testSingleConnection() from [id]/test/route.js as reusable export
- Rewrite test-batch/route.js to call testSingleConnection() directly
instead of fetch(${request.nextUrl.origin}/api/...) which resolves to
localhost behind reverse proxy, causing NETWORK_ERROR on VPS
- Add NEXT_PUBLIC_APP_URL to cloudSyncScheduler.js fallback chain
- POST handler in [id]/test/route.js now delegates to testSingleConnection()
Fixes: all 34 providers failing with NETWORK_ERROR on llms.omniroute.online
This commit is contained in:
@@ -502,89 +502,102 @@ async function testApiKeyConnection(connection) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Core test logic — reusable by test-batch without HTTP self-calls.
|
||||
* @param {string} connectionId
|
||||
* @returns {Promise<object>} Test result (same shape as the JSON response)
|
||||
*/
|
||||
export async function testSingleConnection(connectionId) {
|
||||
const connection = await getProviderConnectionById(connectionId);
|
||||
|
||||
if (!connection) {
|
||||
return { valid: false, error: "Connection not found", diagnosis: null, latencyMs: 0 };
|
||||
}
|
||||
|
||||
let result;
|
||||
const startTime = Date.now();
|
||||
const runtime = await getProviderRuntimeStatus(connection.provider);
|
||||
|
||||
if (runtime?.diagnosis) {
|
||||
result = {
|
||||
valid: false,
|
||||
error: runtime.error,
|
||||
refreshed: false,
|
||||
diagnosis: runtime.diagnosis,
|
||||
};
|
||||
} else if (connection.authType === "apikey") {
|
||||
result = await testApiKeyConnection(connection);
|
||||
} else {
|
||||
result = await testOAuthConnection(connection);
|
||||
}
|
||||
|
||||
const latencyMs = Date.now() - startTime;
|
||||
|
||||
// Build update data
|
||||
const now = new Date().toISOString();
|
||||
const diagnosis =
|
||||
result.diagnosis ||
|
||||
(result.valid
|
||||
? makeDiagnosis("ok", "local", null, null)
|
||||
: classifyFailure({ error: result.error, statusCode: result.statusCode }));
|
||||
|
||||
const updateData = {
|
||||
testStatus: result.valid ? "active" : "error",
|
||||
lastError: result.valid ? null : result.error,
|
||||
lastErrorAt: result.valid ? null : now,
|
||||
lastTested: now,
|
||||
lastErrorType: result.valid ? null : diagnosis.type,
|
||||
lastErrorSource: result.valid ? null : diagnosis.source,
|
||||
errorCode: result.valid ? null : diagnosis.code || result.statusCode || null,
|
||||
rateLimitedUntil: result.valid ? null : connection.rateLimitedUntil || null,
|
||||
};
|
||||
|
||||
if (result.valid) {
|
||||
updateData.backoffLevel = 0;
|
||||
}
|
||||
|
||||
// If token was refreshed, update tokens in DB
|
||||
if (result.refreshed && result.newTokens) {
|
||||
updateData.accessToken = result.newTokens.accessToken;
|
||||
if (result.newTokens.refreshToken) {
|
||||
updateData.refreshToken = result.newTokens.refreshToken;
|
||||
}
|
||||
if (result.newTokens.expiresIn) {
|
||||
updateData.expiresAt = new Date(Date.now() + result.newTokens.expiresIn * 1000).toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
// Update status in db
|
||||
await updateProviderConnection(connectionId, updateData);
|
||||
|
||||
// Sync to cloud if token was refreshed
|
||||
if (result.refreshed) {
|
||||
await syncToCloudIfEnabled();
|
||||
}
|
||||
|
||||
return {
|
||||
valid: result.valid,
|
||||
error: result.error,
|
||||
refreshed: result.refreshed || false,
|
||||
diagnosis,
|
||||
latencyMs,
|
||||
statusCode: result.statusCode || null,
|
||||
runtime: runtime || null,
|
||||
testedAt: now,
|
||||
};
|
||||
}
|
||||
|
||||
// POST /api/providers/[id]/test - Test connection
|
||||
export async function POST(request, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const connection = await getProviderConnectionById(id);
|
||||
const data = await testSingleConnection(id);
|
||||
|
||||
if (!connection) {
|
||||
if (data.error === "Connection not found") {
|
||||
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
let result;
|
||||
const startTime = Date.now();
|
||||
const runtime = await getProviderRuntimeStatus(connection.provider);
|
||||
|
||||
if (runtime?.diagnosis) {
|
||||
result = {
|
||||
valid: false,
|
||||
error: runtime.error,
|
||||
refreshed: false,
|
||||
diagnosis: runtime.diagnosis,
|
||||
};
|
||||
} else if (connection.authType === "apikey") {
|
||||
result = await testApiKeyConnection(connection);
|
||||
} else {
|
||||
result = await testOAuthConnection(connection);
|
||||
}
|
||||
|
||||
const latencyMs = Date.now() - startTime;
|
||||
|
||||
// Build update data
|
||||
const now = new Date().toISOString();
|
||||
const diagnosis =
|
||||
result.diagnosis ||
|
||||
(result.valid
|
||||
? makeDiagnosis("ok", "local", null, null)
|
||||
: classifyFailure({ error: result.error, statusCode: result.statusCode }));
|
||||
|
||||
const updateData = {
|
||||
testStatus: result.valid ? "active" : "error",
|
||||
lastError: result.valid ? null : result.error,
|
||||
lastErrorAt: result.valid ? null : now,
|
||||
lastTested: now,
|
||||
lastErrorType: result.valid ? null : diagnosis.type,
|
||||
lastErrorSource: result.valid ? null : diagnosis.source,
|
||||
errorCode: result.valid ? null : diagnosis.code || result.statusCode || null,
|
||||
rateLimitedUntil: result.valid ? null : connection.rateLimitedUntil || null,
|
||||
};
|
||||
|
||||
if (result.valid) {
|
||||
updateData.backoffLevel = 0;
|
||||
}
|
||||
|
||||
// If token was refreshed, update tokens in DB
|
||||
if (result.refreshed && result.newTokens) {
|
||||
updateData.accessToken = result.newTokens.accessToken;
|
||||
if (result.newTokens.refreshToken) {
|
||||
updateData.refreshToken = result.newTokens.refreshToken;
|
||||
}
|
||||
if (result.newTokens.expiresIn) {
|
||||
updateData.expiresAt = new Date(
|
||||
Date.now() + result.newTokens.expiresIn * 1000
|
||||
).toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
// Update status in db
|
||||
await updateProviderConnection(id, updateData);
|
||||
|
||||
// Sync to cloud if token was refreshed
|
||||
if (result.refreshed) {
|
||||
await syncToCloudIfEnabled();
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
valid: result.valid,
|
||||
error: result.error,
|
||||
refreshed: result.refreshed || false,
|
||||
diagnosis,
|
||||
latencyMs,
|
||||
statusCode: result.statusCode || null,
|
||||
runtime: runtime || null,
|
||||
testedAt: now,
|
||||
});
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.log("Error testing connection:", error);
|
||||
return NextResponse.json({ error: "Test failed" }, { status: 500 });
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
OPENAI_COMPATIBLE_PREFIX,
|
||||
ANTHROPIC_COMPATIBLE_PREFIX,
|
||||
} from "@/shared/constants/providers";
|
||||
import { testSingleConnection } from "../[id]/test/route";
|
||||
|
||||
// Determine auth type group for a provider id
|
||||
function getAuthGroup(providerId) {
|
||||
@@ -73,43 +74,33 @@ export async function POST(request) {
|
||||
});
|
||||
}
|
||||
|
||||
// Test each connection sequentially via internal API call
|
||||
// Test each connection sequentially via direct function call (no HTTP self-call)
|
||||
const results = [];
|
||||
const baseUrl = request.nextUrl.origin;
|
||||
|
||||
for (const conn of connectionsToTest) {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/api/providers/${conn.id}/test`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
signal: AbortSignal.timeout(20000),
|
||||
});
|
||||
const data = await res.json();
|
||||
const latencyMs = data.latencyMs || Date.now() - startTime;
|
||||
|
||||
const data = await testSingleConnection(conn.id);
|
||||
results.push({
|
||||
provider: conn.provider,
|
||||
connectionId: conn.id,
|
||||
connectionName: conn.name || conn.email || conn.provider,
|
||||
authType: conn.authType || getAuthGroup(conn.provider),
|
||||
valid: data.valid,
|
||||
latencyMs,
|
||||
latencyMs: data.latencyMs || 0,
|
||||
error: data.error || null,
|
||||
diagnosis: data.diagnosis || null,
|
||||
statusCode: data.statusCode || null,
|
||||
testedAt: data.testedAt || new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
const latencyMs = Date.now() - startTime;
|
||||
results.push({
|
||||
provider: conn.provider,
|
||||
connectionId: conn.id,
|
||||
connectionName: conn.name || conn.email || conn.provider,
|
||||
authType: conn.authType || getAuthGroup(conn.provider),
|
||||
valid: false,
|
||||
latencyMs,
|
||||
error: error.name === "TimeoutError" ? "Timeout (20s)" : error.message,
|
||||
latencyMs: 0,
|
||||
error: error.message,
|
||||
diagnosis: { type: "network_error", source: "local", code: null, message: error.message },
|
||||
statusCode: null,
|
||||
testedAt: new Date().toISOString(),
|
||||
|
||||
@@ -2,7 +2,10 @@ import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { isCloudEnabled } from "@/lib/localDb";
|
||||
|
||||
const INTERNAL_BASE_URL =
|
||||
process.env.BASE_URL || process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:20128";
|
||||
process.env.BASE_URL ||
|
||||
process.env.NEXT_PUBLIC_BASE_URL ||
|
||||
process.env.NEXT_PUBLIC_APP_URL ||
|
||||
"http://localhost:20128";
|
||||
|
||||
/**
|
||||
* Cloud sync scheduler
|
||||
|
||||
Reference in New Issue
Block a user