{"record":{"id":"b09d0664975c4e3b","repo":"decolua/9router","slug":"health-check-timeout-after-health-check-timeoutm","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/cloudflare/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/cloudflare/healthCheck.js#L10-L30","documentation":"waitForHealth() polls the tunnel URL every HEALTH_CHECK.intervalMs for up to HEALTH_CHECK.timeoutMs; if probeUrlAlive() never returns true within that window it throws 'Health check timeout after <N>ms'. It means the tunnel was enabled but the public URL never answered HTTP within the allotted time.","triggerScenarios":"enableTunnel() creates a cloudflared tunnel whose public URL stays unreachable for the whole timeout window: cloudflared failed to connect to Cloudflare edge, the local service on localPort isn't listening, network egress to Cloudflare is blocked, or the tunnel binary crashed after startup.","commonSituations":"Local app not started yet (port 20128 not listening), firewall/proxy blocking outbound connections to Cloudflare, cloudflared binary failing auth or quota limits, DNS propagation delay for a newly named tunnel, or an overly small HEALTH_CHECK.timeoutMs for slow networks.","solutions":["Verify the local service is actually listening on the configured port (curl http://localhost:20128/dashboard).","Check cloudflared logs for edge connection or authentication failures.","Increase HEALTH_CHECK.timeoutMs / adjust intervalMs in src/lib/tunnel/cloudflare/healthCheck.js if the network is slow.","Confirm outbound HTTPS to Cloudflare edge (region1/region2.v2.argotunnel.com, port 7844 or 443) is not blocked.","Retry enableTunnel() once connectivity is restored."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Preflight: confirm local service is up before enabling the tunnel\nconst res = await fetch(`http://localhost:${port}/dashboard`).catch(() => null);\nif (!res || !res.ok) throw new Error(`Local service not listening on ${port}`);","typeGuard":"const isHealthTimeout = (e) => e instanceof Error && /Health check timeout after \\d+ms/.test(e.message);","tryCatchPattern":"try {\n  await waitForHealth(url, token);\n} catch (e) {\n  if (isHealthTimeout(e)) {\n    // retry once, or surface actionable message about local port / egress\n    return retryEnableOnce();\n  }\n  throw e;\n}","preventionTips":["Always verify the local app responds on localPort before enabling a tunnel.","Size HEALTH_CHECK.timeoutMs generously (e.g. 30-60s) for slow networks.","Monitor cloudflared process logs; exit of the binary guarantees timeout.","Check corporate firewall egress to Cloudflare edge before deploying."],"tags":["timeout","network","cloudflare","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"}