Files
openchamber/packages/electron/electron-host-probe.mjs
Jordi Ibáñez 70e04fc15f fix: avoid false offline results in Electron instance probes (#3415)
* fix: avoid false offline results in Electron instance probes

Use Chromium networking with one abortable deadline per direct-host attempt. Retry unreachable selector probes once while preserving identity, auth and compatibility checks, and clean up response bodies on every exit.

Add regression coverage and document the native probe invariants.

* fix: declare Electron probe's zod dependency

Declare the existing workspace zod version as an Electron runtime dependency so probe imports do not rely on root hoisting. Update only the matching lockfile workspace entry, without changing resolved versions or parsing behavior.
2026-09-09 17:44:38 +03:00

135 lines
4.8 KiB
JavaScript

import { sanitizeRuntimeRequestHeaders } from './runtime-request-headers.mjs';
import { z } from 'zod';
const optionalIdentity = z.string().catch('').transform((value) => value.trim());
const versionEnvelope = z.object({
status: z.literal('ok'),
// Arrays historically classify as incompatible rather than wrong-service.
compatibility: z.union([z.looseObject({}), z.array(z.unknown())]),
});
const buildProbeUrl = (url, pathname) => {
try {
const parsed = new URL(url);
parsed.pathname = `${parsed.pathname.replace(/\/$/, '') || ''}${pathname}`;
return parsed.toString();
} catch {
return null;
}
};
const classifyVersionPayload = (payload) => {
const parsed = versionEnvelope.safeParse(payload);
if (!parsed.success) {
return 'wrong-service';
}
const { compatibility } = parsed.data;
if (!Array.isArray(compatibility.capabilities) || !compatibility.capabilities.includes('api.runtime-url.v1')) {
return 'incompatible';
}
if (compatibility.apiVersion !== 1 || compatibility.minClientApiVersion > 1) {
return 'update-recommended';
}
return 'ok';
};
export const probeElectronHostWithDeadline = async ({
url,
timeoutMs,
clientToken = '',
requestHeaders = {},
expectedServerId = '',
chromiumFetch,
isReady,
now = Date.now,
scheduleTimeout = setTimeout,
cancelTimeout = clearTimeout,
}) => {
const started = now();
const result = (status) => ({ status, latencyMs: now() - started });
if (!isReady()) return result('unreachable');
const versionUrl = buildProbeUrl(url, '/api/version');
const sessionUrl = buildProbeUrl(url, '/auth/session');
if (!versionUrl || !sessionUrl) throw new Error('Invalid URL');
const controller = new AbortController();
let rejectDeadline;
const deadline = new Promise((_, reject) => {
rejectDeadline = reject;
});
const timer = scheduleTimeout(() => {
controller.abort();
rejectDeadline(new Error('Host probe deadline exceeded'));
}, timeoutMs);
const responses = new Set();
const discardBody = async (response) => {
if (response.body && !response.body.locked) await response.body.cancel().catch(() => {});
};
const fetchProbe = async (requestUrl, headers) => {
controller.signal.throwIfAborted();
const response = await chromiumFetch(requestUrl, {
headers,
signal: controller.signal,
redirect: 'manual',
});
if (controller.signal.aborted) {
await discardBody(response);
controller.signal.throwIfAborted();
}
responses.add(response);
return response;
};
const run = async () => {
const expectedIdentity = optionalIdentity.parse(expectedServerId);
if (expectedIdentity) {
const healthUrl = buildProbeUrl(url, '/health');
if (healthUrl) {
try {
const response = await fetchProbe(healthUrl, { Accept: 'application/json' });
// A redirected identity belongs to another candidate, even if its ID matches.
if (response.status >= 300 && response.status < 400 || response.redirected) return result('wrong-service');
if (response.ok) {
const payload = await response.json().catch(() => null);
const reported = optionalIdentity.parse(payload?.serverId);
if (reported && reported !== expectedIdentity) return result('wrong-service');
}
} catch (error) {
if (controller.signal.aborted) throw error;
// Electron 43 net.fetch rejects manual redirects instead of returning a 3xx response.
if (error instanceof Error && error.message === 'Redirect was cancelled') return result('wrong-service');
// Identity is optional on older servers; the authenticated request remains authoritative.
}
}
}
if (controller.signal.aborted) throw new Error('Host probe deadline exceeded');
const headers = { ...sanitizeRuntimeRequestHeaders(requestHeaders), Accept: 'application/json' };
const token = optionalIdentity.parse(clientToken);
if (token) headers.Authorization = `Bearer ${token}`;
const versionResponse = await fetchProbe(versionUrl, headers);
if (versionResponse.status === 401 || versionResponse.status === 403) return result('auth');
if (!versionResponse.ok) return result('unreachable');
const versionStatus = classifyVersionPayload(await versionResponse.json().catch(() => null));
if (versionStatus !== 'ok') return result(versionStatus);
const sessionResponse = await fetchProbe(sessionUrl, headers);
if (sessionResponse.status === 401 || sessionResponse.status === 403) return result('auth');
if (!sessionResponse.ok) return result('unreachable');
return result('ok');
};
try {
return await Promise.race([run(), deadline]);
} catch {
return result('unreachable');
} finally {
controller.abort();
await Promise.allSettled([...responses].map(discardBody));
cancelTimeout(timer);
}
};