{"record":{"id":"d35af54defabcaa3","repo":"can1357/oh-my-pi","slug":"exposure-health-probe-for-destination-failed-wi","errorCode":null,"errorMessage":"Exposure health probe for ${destination} failed with status ${finalStatus}","messagePattern":"Exposure health probe for (.+?) failed with status (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/exposure.ts","lineNumber":206,"sourceCode":"\t\ttry {\n\t\t\tconst response = await fetchFn(healthUrl, {\n\t\t\t\tcache: \"no-store\",\n\t\t\t\tsignal: AbortSignal.timeout(timeoutMs),\n\t\t\t});\n\t\t\tif (response.status === 204) return;\n\t\t\tfinalStatus = `HTTP ${response.status}`;\n\t\t\ttry {\n\t\t\t\tawait response.body?.cancel();\n\t\t\t} catch {\n\t\t\t\t// The response status is authoritative even if body disposal fails.\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tfinalStatus = error instanceof DOMException && error.name === \"TimeoutError\" ? \"timeout\" : \"request failed\";\n\t\t}\n\t\tif (attempt + 1 < attempts && backoffMs > 0) await Bun.sleep(backoffMs);\n\t}\n\n\tthrow new Error(`Exposure health probe for ${destination} failed with status ${finalStatus}`);\n}\n\n/**\n * SIGTERM, escalating to SIGKILL after a grace period. `tailscale funnel`\n * observably survives a bare SIGTERM mid-startup, and a leaked funnel child\n * blocks every later funnel invocation on the machine.\n */\nfunction killTunnelProcess(proc: Bun.Subprocess): void {\n\tproc.kill();\n\tconst timer = setTimeout(() => {\n\t\tif (proc.exitCode === null) proc.kill(\"SIGKILL\");\n\t}, 2_000);\n\ttimer.unref();\n}\n\n/**\n * Spawn a tunnel process with its output redirected to a temp log file and\n * poll the file until `extract` yields the public URL. Kills the child and","sourceCodeStart":188,"sourceCodeEnd":224,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/exposure.ts#L188-L224","documentation":"probeExposureHealth verifies the public exposure (tunnel/forward URL) actually reaches the local blob server's /.well-known/omp-blob-health endpoint, requiring an exact 204. After exhausting its retry attempts (default 5 with 250ms backoff, 3s per-attempt timeout), it throws this error with the sanitized destination origin and the final status (HTTP <code>, timeout, or request failed). Callers degrade to inline base64 images.","triggerScenarios":"startExposure succeeded (tunnel process printed a URL) but the health probe never got a 204: tunnel not yet registered at the edge, remote server for \"ssh\" kind not proxying the forwarded port, publicBaseUrl pointing at the wrong origin, firewall blocking the edge, or the tunnel dying between URL print and probe.","commonSituations":"Tunnel URL advertised before edge registration completes (transient — retries usually fix it); ssh reverse forward set up but no web server configured on the remote host at sshRemotePort; publicBaseUrl misconfigured to a domain that doesn't route to the tunnel; corporate network blocking the tunnel provider.","solutions":["Verify publicBaseUrl actually routes to the tunnel/forwarded port (curl the health path manually: curl -i https://<origin>/.well-known/omp-blob-health).","For the \"ssh\" kind, ensure a reverse proxy on the remote host serves the sshRemotePort back to the forwarded port.","Increase the probe tolerance (attempts/timeoutMs) if the tunnel is slow to register, then retry.","Check the tunnel child's log (omp-blob-tunnel-*.log in tmpdir) for registration errors, and confirm network/firewall allows the tunnel provider.","Accept the fallback: the broker degrades to inline base64 images, which still works."],"exampleFix":"// before (config)\n{ kind: \"ssh\", publicBaseUrl: \"https://example.com\", sshTarget: \"user@host\" } // remote serves nothing on 8787\n// after: run a proxy on the remote host, e.g.\n// ssh -N -R 8787:127.0.0.1:<port> user@host  +  remote nginx: listen 8787 -> proxy_pass http://127.0.0.1:8787\n{ kind: \"ssh\", publicBaseUrl: \"https://example.com\", sshTarget: \"user@host\", sshRemotePort: 8787 }","handlingStrategy":"retry","validationCode":"// verify the public origin yourself before relying on it:\n// curl -fsSI https://<publicBaseUrl>/.well-known/omp-blob-health  (expect HTTP 204)\nconst res = await fetch(`${publicBaseUrl}/.well-known/omp-blob-health`, { signal: AbortSignal.timeout(3000) }).catch(() => null);\nif (!res || res.status !== 204) console.warn(\"public origin does not reach the blob server yet\");","typeGuard":null,"tryCatchPattern":"try {\n  await probeExposureHealth(baseUrl, fetch, { attempts: 5, backoffMs: 250 });\n} catch (err) {\n  logger.warn(\"exposure unreachable; falling back to inline base64\", {\n    baseUrl,\n    detail: err instanceof Error ? err.message : String(err),\n  });\n}","preventionTips":["Confirm the remote side of ssh forwards actually proxies the forwarded port to the internet-facing origin.","Point publicBaseUrl at the exact origin the tunnel publishes — no stale domains.","Allow tunnels a moment to register at the edge; the built-in retries cover transient slowness only.","Remember the design contract: probe failure degrades to inline base64, so build callers that tolerate null publications."],"tags":["network","health-check","tunnel"],"backgroundTag":"health-probe-failed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}