koala73/worldmonitor · error · EmbedKeyUnavailableError

Convex embed key validation unavailable: fetch-error

Error message

Convex embed key validation unavailable: fetch-error

What it means

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.

Solutions

  1. Verify CONVEX_SITE_URL is the correct `https://<deployment>.convex.site` URL and reachable: `curl -v $CONVEX_SITE_URL/api/internal-validate-embed-key`.
  2. Check for transient issues and retry — a 3s timeout can be exceeded during Convex cold starts; confirm the deployment is running.
  3. Verify network egress/DNS from the hosting environment (Railway/Edge) to *.convex.site is allowed.
  4. If timeouts recur under load, investigate Convex deployment performance or revisit the 3s budget deliberately rather than in an ad-hoc patch.

Example fix

// before: typo'd host times out
CONVEX_SITE_URL=https://my-app.convex.cloud

// after: correct HTTP actions host
CONVEX_SITE_URL=https://my-app.convex.site
Defensive patterns

Strategy: retry

Validate before calling

let reachable = false;
try {
  reachable = (await fetch(`${process.env.CONVEX_SITE_URL}/api/internal-validate-embed-key`, { method: 'HEAD', signal: AbortSignal.timeout(3_000) })).status !== undefined;
} catch { /* unreachable */ }
if (!reachable) throw new Error('Convex validator endpoint unreachable; check CONVEX_SITE_URL and network egress');

Try / catch

try {
  const verdict = await result(keyHash);
} catch (err) {
  if (err instanceof EmbedKeyUnavailableError && err.message.endsWith('fetch-error')) {
    // transient network/timeout — retry once with backoff, then fail closed
    await delay(500);
    return retryValidation(keyHash).catch(() => failClosed());
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/886ee45b63f15a24. Report an issue: GitHub.

Appendix: source

Thrown at server/_shared/embed-key.ts:155

  const convexSharedSecret = process.env.CONVEX_SERVER_SHARED_SECRET;
  if (!convexSiteUrl || !convexSharedSecret) {
    throw new EmbedKeyUnavailableError('Convex embed key validation unavailable: missing-config');
  }

  let resp: Response;
  try {
    resp = await fetch(`${convexSiteUrl}/api/internal-validate-embed-key`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'User-Agent': 'worldmonitor-gateway/1.0',
        'x-convex-shared-secret': convexSharedSecret,
      },
      body: JSON.stringify({ keyHash }),
      signal: AbortSignal.timeout(3_000),
    });
  } catch {
    throw new EmbedKeyUnavailableError('Convex embed key validation unavailable: fetch-error');
  }

  if (!resp.ok) {
    throw new EmbedKeyUnavailableError(
      `Convex embed key validation unavailable: http-${resp.status}`,
    );
  }

  let value: unknown;
  try {
    value = await resp.json();
  } catch {
    throw new EmbedKeyUnavailableError('Convex embed key validation unavailable: invalid-json');
  }

  if (value === null) return null;
  if (!isEmbedKeyResult(value)) {
    throw new EmbedKeyUnavailableError('Convex embed key validation unavailable: invalid-payload');

View on GitHub (pinned to 7d06c8633d)