{"record":{"id":"d6e9cd0fc6d751d8","repo":"decolua/9router","slug":"health-check-timeout-after-health-check-timeoutm-d6e9cd","errorCode":null,"errorMessage":"Health check timeout after ${HEALTH_CHECK.timeoutMs}ms","messagePattern":"Health check timeout after (.+?)ms","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/tunnel/tailscale/healthCheck.js","lineNumber":28,"sourceCode":"\n  try {\n    const res = await fetch(`${url}/api/health`, {\n      signal: AbortSignal.timeout(HEALTH_CHECK.fetchTimeoutMs),\n    });\n    return res.ok;\n  } catch {\n    return false;\n  }\n}\n\nexport async function waitForHealth(url, cancelToken = { cancelled: false }) {\n  const start = Date.now();\n  while (Date.now() - start < HEALTH_CHECK.timeoutMs) {\n    if (cancelToken.cancelled) throw new Error(\"cancelled\");\n    if (await probeUrlAlive(url)) return true;\n    await new Promise((r) => setTimeout(r, HEALTH_CHECK.intervalMs));\n  }\n  throw new Error(`Health check timeout after ${HEALTH_CHECK.timeoutMs}ms`);\n}\n","sourceCodeStart":10,"sourceCodeEnd":30,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/tunnel/tailscale/healthCheck.js#L10-L30","documentation":"The tailscale waitForHealth() polls the funnel URL until probeUrlAlive() succeeds, bounded by HEALTH_CHECK.timeoutMs; if the funnel never becomes reachable in time it throws 'Health check timeout after <N>ms'. It indicates the tailscale funnel was started but its public URL never served HTTP within the window.","triggerScenarios":"enableTailscale() completes `tailscale funnel` spawn but the URL stays dead for the full timeout: tailscale not logged in / expired auth, funnel disabled on the tailnet (ACL/policy), tailnet DNS not propagated, the local port not listening, or tailscaled disconnected from the DERP/coordination server.","commonSituations":"Fresh tailscale install where `tailscale up` was never run, corporate networks blocking tailscale UDP, funnel feature not enabled for the tailnet, slow MagicDNS propagation for a new hostname, or the local app (port 20128) not yet started.","solutions":["Verify the local service answers on the configured port before enabling the funnel.","Run `tailscale status` and `tailscale funnel status` to confirm the daemon is connected and the funnel is active.","Re-authenticate tailscale (`tailscale login` / `tailscale up`) if the session expired.","Confirm funnel is permitted for your tailnet (tailscale admin console / ACLs) and that MagicDNS is enabled.","Increase HEALTH_CHECK.timeoutMs in src/lib/tunnel/tailscale/healthCheck.js on slow networks, then retry."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Preflight: is tailscale ready and is the local app up?\nexecSync(\"tailscale status\", { stdio: \"ignore\" }); // throws if not connected\nconst res = await fetch(`http://localhost:${port}`).catch(() => null);\nif (!res?.ok) throw new Error(\"local app not ready\");","typeGuard":"const isHealthTimeout = (e) => e instanceof Error && /Health check timeout after \\d+ms/.test(e.message);","tryCatchPattern":"try {\n  await waitForHealth(funnelUrl, token);\n} catch (e) {\n  if (isHealthTimeout(e)) {\n    console.error(\"Funnel never became healthy — check `tailscale status`, auth, and ACLs\");\n    return retryAfterCheck();\n  }\n  throw e;\n}","preventionTips":["Run `tailscale up`/login and `tailscale status` before enabling funnel.","Confirm funnel is allowed for your tailnet and MagicDNS is enabled.","Ensure the local service listens on the port passed to enableTailscale.","Increase timeout on high-latency networks and retry once before surfacing the error."],"tags":["timeout","network","tailscale","tunnel","health-check"],"backgroundTag":"health-check-timeout","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}