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:
diegosouzapw
2026-02-16 17:57:30 -03:00
parent bc38805b51
commit f1deb98348
3 changed files with 98 additions and 91 deletions

View File

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

View File

@@ -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(),

View File

@@ -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