fix: retry the relay instance probe before reporting it unreachable
The tunnel rejects everything waiting on its channel the moment one connect attempt fails, even though it has already scheduled the next one with backoff. That is right for app traffic, which retries for itself, but it made the one-shot probe report a durable red "Unreachable" for a host that answers when the user presses refresh a second later — a cold start is exactly when that first attempt loses. The probe now spans the tunnel's own reconnects within a 15s budget, and ends immediately on a terminal tunnel state (auth failed, duplicate client, limit), which waiting cannot resolve.
This commit is contained in:
@@ -408,6 +408,9 @@ export const desktopInstallIdGet = async (): Promise<string> => {
|
||||
};
|
||||
|
||||
const RELAY_PROBE_TIMEOUT_MS = 8_000;
|
||||
// Whole-probe budget, spanning the tunnel's own reconnect attempts.
|
||||
const RELAY_PROBE_DEADLINE_MS = 15_000;
|
||||
const RELAY_PROBE_RETRY_DELAY_MS = 400;
|
||||
|
||||
const fetchRelayProbe = async (
|
||||
tunnel: ReturnType<typeof createRelayTunnelClient>,
|
||||
@@ -423,13 +426,47 @@ const fetchRelayProbe = async (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reach the host, letting the tunnel's own reconnect do the work.
|
||||
*
|
||||
* The tunnel rejects everything waiting on its channel the moment ONE connect
|
||||
* attempt fails, even though it has already scheduled the next one with
|
||||
* backoff. That is right for app traffic — `runtime-fetch` retries for itself —
|
||||
* but it made a one-shot probe report a durable red "Unreachable" for a host
|
||||
* that answers when the user presses refresh a second later. A cold start is
|
||||
* exactly when that first attempt loses: DNS and TLS to the relay are cold, the
|
||||
* remote host may still be re-establishing its control connection, and the
|
||||
* probe competes with the app's own bootstrap traffic.
|
||||
*
|
||||
* A terminal tunnel state (auth failed, duplicate client, limit reached) will
|
||||
* not resolve by waiting, so it ends the probe immediately.
|
||||
*/
|
||||
const fetchRelayProbeUntilDeadline = async (
|
||||
tunnel: ReturnType<typeof createRelayTunnelClient>,
|
||||
path: string,
|
||||
deadline: number,
|
||||
init?: RequestInit,
|
||||
): Promise<Response> => {
|
||||
for (;;) {
|
||||
try {
|
||||
return await fetchRelayProbe(tunnel, path, init);
|
||||
} catch (error) {
|
||||
if (tunnel.getStatus().state === 'error') throw error;
|
||||
if (Date.now() >= deadline) throw error;
|
||||
await new Promise((resolve) => window.setTimeout(resolve, RELAY_PROBE_RETRY_DELAY_MS));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reachability and client-auth check for a relay host: open a throwaway E2EE
|
||||
* tunnel, verify `/health`, then verify `/auth/session` with the saved bearer.
|
||||
* Relay hosts have no HTTP address for `desktopHostProbe`. Hard timeout: a
|
||||
* ghost relay registration (relay lost the host, host doesn't know) leaves the
|
||||
* tunnel in `connecting` forever — the probe must report unreachable instead
|
||||
* of hanging every status/switch flow with it.
|
||||
* Relay hosts have no HTTP address for `desktopHostProbe`. Bounded by
|
||||
* `RELAY_PROBE_DEADLINE_MS`: a ghost relay registration (relay lost the host,
|
||||
* host doesn't know) leaves the tunnel reconnecting forever — the probe must
|
||||
* report unreachable rather than hang every status/switch flow with it — while
|
||||
* still spanning enough reconnect attempts that a cold first attempt is not
|
||||
* mistaken for an unreachable instance.
|
||||
*/
|
||||
export const probeRelayDesktopHost = async (
|
||||
relay: DesktopHostRelay,
|
||||
@@ -444,9 +481,10 @@ export const probeRelayDesktopHost = async (
|
||||
hostEncPubJwk: relay.hostEncPubJwk,
|
||||
});
|
||||
const startedAt = Date.now();
|
||||
const deadline = startedAt + RELAY_PROBE_DEADLINE_MS;
|
||||
let keep = false;
|
||||
try {
|
||||
const response = await fetchRelayProbe(tunnel, '/health');
|
||||
const response = await fetchRelayProbeUntilDeadline(tunnel, '/health', deadline);
|
||||
if (!response.ok) return { status: 'unreachable', latencyMs: 0 };
|
||||
const headers = new Headers({ Accept: 'application/json' });
|
||||
for (const [name, value] of Object.entries(options?.requestHeaders || {})) {
|
||||
@@ -454,7 +492,7 @@ export const probeRelayDesktopHost = async (
|
||||
}
|
||||
const clientToken = options?.clientToken?.trim();
|
||||
if (clientToken) headers.set('Authorization', `Bearer ${clientToken}`);
|
||||
const sessionResponse = await fetchRelayProbe(tunnel, '/auth/session', { headers });
|
||||
const sessionResponse = await fetchRelayProbeUntilDeadline(tunnel, '/auth/session', deadline, { headers });
|
||||
if (sessionResponse.status === 401 || sessionResponse.status === 403) {
|
||||
return { status: 'auth', latencyMs: Math.max(0, Date.now() - startedAt) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user