{"record":{"id":"886ee45b63f15a24","repo":"koala73/worldmonitor","slug":"convex-embed-key-validation-unavailable-fetch-error","errorCode":null,"errorMessage":"Convex embed key validation unavailable: fetch-error","messagePattern":"Convex embed key validation unavailable: fetch-error","errorType":"exception","errorClass":"EmbedKeyUnavailableError","httpStatus":null,"severity":"error","filePath":"server/_shared/embed-key.ts","lineNumber":155,"sourceCode":"  const convexSharedSecret = process.env.CONVEX_SERVER_SHARED_SECRET;\n  if (!convexSiteUrl || !convexSharedSecret) {\n    throw new EmbedKeyUnavailableError('Convex embed key validation unavailable: missing-config');\n  }\n\n  let resp: Response;\n  try {\n    resp = await fetch(`${convexSiteUrl}/api/internal-validate-embed-key`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'User-Agent': 'worldmonitor-gateway/1.0',\n        'x-convex-shared-secret': convexSharedSecret,\n      },\n      body: JSON.stringify({ keyHash }),\n      signal: AbortSignal.timeout(3_000),\n    });\n  } catch {\n    throw new EmbedKeyUnavailableError('Convex embed key validation unavailable: fetch-error');\n  }\n\n  if (!resp.ok) {\n    throw new EmbedKeyUnavailableError(\n      `Convex embed key validation unavailable: http-${resp.status}`,\n    );\n  }\n\n  let value: unknown;\n  try {\n    value = await resp.json();\n  } catch {\n    throw new EmbedKeyUnavailableError('Convex embed key validation unavailable: invalid-json');\n  }\n\n  if (value === null) return null;\n  if (!isEmbedKeyResult(value)) {\n    throw new EmbedKeyUnavailableError('Convex embed key validation unavailable: invalid-payload');","sourceCodeStart":137,"sourceCodeEnd":173,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/server/_shared/embed-key.ts#L137-L173","documentation":"fetchFromConvex wraps its POST to `<convexSiteUrl>/api/internal-validate-embed-key` in a try/catch with a 3-second AbortSignal timeout. If fetch rejects — DNS failure, connection refused, TLS error, or the 3s timeout abort — it throws EmbedKeyUnavailableError with `fetch-error`, signaling the Convex validator could not be reached at all.","triggerScenarios":"The `await fetch(...)` inside the try block rejects: CONVEX_SITE_URL is unreachable/wrong-host, the network is down, DNS fails, or the request exceeds `AbortSignal.timeout(3_000)` and is aborted.","commonSituations":"Typo'd or stale CONVEX_SITE_URL (e.g. pointing at the deprecated .convex.cloud host instead of .convex.site); Convex deployment paused or deleted; transient network outage or cold-start latency exceeding the 3-second timeout; egress blocked from the worker's network.","solutions":["Verify CONVEX_SITE_URL is the correct `https://<deployment>.convex.site` URL and reachable: `curl -v $CONVEX_SITE_URL/api/internal-validate-embed-key`.","Check for transient issues and retry — a 3s timeout can be exceeded during Convex cold starts; confirm the deployment is running.","Verify network egress/DNS from the hosting environment (Railway/Edge) to *.convex.site is allowed.","If timeouts recur under load, investigate Convex deployment performance or revisit the 3s budget deliberately rather than in an ad-hoc patch."],"exampleFix":"// before: typo'd host times out\nCONVEX_SITE_URL=https://my-app.convex.cloud\n\n// after: correct HTTP actions host\nCONVEX_SITE_URL=https://my-app.convex.site","handlingStrategy":"retry","validationCode":"let reachable = false;\ntry {\n  reachable = (await fetch(`${process.env.CONVEX_SITE_URL}/api/internal-validate-embed-key`, { method: 'HEAD', signal: AbortSignal.timeout(3_000) })).status !== undefined;\n} catch { /* unreachable */ }\nif (!reachable) throw new Error('Convex validator endpoint unreachable; check CONVEX_SITE_URL and network egress');","typeGuard":null,"tryCatchPattern":"try {\n  const verdict = await result(keyHash);\n} catch (err) {\n  if (err instanceof EmbedKeyUnavailableError && err.message.endsWith('fetch-error')) {\n    // transient network/timeout — retry once with backoff, then fail closed\n    await delay(500);\n    return retryValidation(keyHash).catch(() => failClosed());\n  }\n  throw err;\n}","preventionTips":["Point CONVEX_SITE_URL at the https://<deployment>.convex.site HTTP actions host, not .convex.cloud.","Add a health check that pings the internal endpoint before processing embed-key traffic.","Allow reasonable retry with backoff for the 3s timeout; Convex cold starts can occasionally exceed it.","Ensure the hosting environment's egress allows connections to *.convex.site over HTTPS."],"tags":["network","fetch","timeout","convex"],"backgroundTag":"network-request-failed","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}