fix(web): raise connection-test timeout for cold upstreams
A cold/idle self-hosted Honcho can take ~5s on its first request (DB pool, tunnel wake); the hardcoded 5s budget aborted just before the response and reported a live instance as 'Connection timed out'. Extract CONNECTION_TIMEOUT_MS (15s), make checkConnection's timeout injectable, and cover the budget behavior.
This commit is contained in:
@@ -7,6 +7,13 @@ const STORE_KEY = "openconcho:instances";
|
|||||||
|
|
||||||
export const HONCHO_CLOUD_URL = "https://api.honcho.dev";
|
export const HONCHO_CLOUD_URL = "https://api.honcho.dev";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connection-test timeout. Generous because a cold/idle self-hosted Honcho (DB
|
||||||
|
* pool spin-up, tunnel wake) can take several seconds on its first request — a
|
||||||
|
* tight 5s budget reported live-and-reachable instances as "Connection timed out".
|
||||||
|
*/
|
||||||
|
export const CONNECTION_TIMEOUT_MS = 15_000;
|
||||||
|
|
||||||
function normalizeBaseUrl(url: string): string {
|
function normalizeBaseUrl(url: string): string {
|
||||||
return url.trim().replace(/\/+$/, "").toLowerCase();
|
return url.trim().replace(/\/+$/, "").toLowerCase();
|
||||||
}
|
}
|
||||||
@@ -162,6 +169,7 @@ export type HealthStatus = "ok" | "auth-required" | "unreachable" | "checking";
|
|||||||
export async function checkConnection(
|
export async function checkConnection(
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
token?: string,
|
token?: string,
|
||||||
|
timeoutMs: number = CONNECTION_TIMEOUT_MS,
|
||||||
): Promise<{ status: HealthStatus; message: string }> {
|
): Promise<{ status: HealthStatus; message: string }> {
|
||||||
try {
|
try {
|
||||||
const { baseUrl: base, headers, fetch } = dispatchFor({ baseUrl, token });
|
const { baseUrl: base, headers, fetch } = dispatchFor({ baseUrl, token });
|
||||||
@@ -169,7 +177,7 @@ export async function checkConnection(
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify({}),
|
body: JSON.stringify({}),
|
||||||
signal: AbortSignal.timeout(5000),
|
signal: AbortSignal.timeout(timeoutMs),
|
||||||
});
|
});
|
||||||
|
|
||||||
const reject = res.headers.get(PROXY_REJECT_HEADER);
|
const reject = res.headers.get(PROXY_REJECT_HEADER);
|
||||||
|
|||||||
@@ -45,3 +45,30 @@ describe("checkConnection — web proxy mode", () => {
|
|||||||
expect(res.message).toMatch(/allowlist/i);
|
expect(res.message).toMatch(/allowlist/i);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("checkConnection — timeout budget", () => {
|
||||||
|
// A fetch that resolves after `ms`, but rejects early if the abort signal fires —
|
||||||
|
// mirrors how a real slow upstream interacts with AbortSignal.timeout.
|
||||||
|
function delayedFetch(ms: number) {
|
||||||
|
return (_url: string, init: { signal?: AbortSignal }) =>
|
||||||
|
new Promise<Response>((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => resolve(new Response("{}", { status: 200 })), ms);
|
||||||
|
init.signal?.addEventListener("abort", () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(new DOMException("The operation timed out", "TimeoutError"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it("reports unreachable when the upstream is slower than the timeout budget", async () => {
|
||||||
|
httpFetchMock.mockImplementation(delayedFetch(80));
|
||||||
|
const res = await checkConnection("https://slow.example.net", undefined, 20);
|
||||||
|
expect(res.status).toBe("unreachable");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("succeeds when a slow upstream responds within the (cold-start) budget", async () => {
|
||||||
|
httpFetchMock.mockImplementation(delayedFetch(20));
|
||||||
|
const res = await checkConnection("https://slow.example.net", undefined, 200);
|
||||||
|
expect(res.status).toBe("ok");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user