koala73/worldmonitor · error · EmbedKeyUnavailableError

Convex embed key validation unavailable: invalid-json

Error message

Convex embed key validation unavailable: invalid-json

What it means

fetchFromConvex fetches embed-key validation data from a Convex endpoint and parses the HTTP response body as JSON. If resp.json() throws (body is not valid JSON — empty, HTML error page, truncated, or wrong content type), the function converts that low-level failure into EmbedKeyUnavailableError with reason 'invalid-json'. It signals the upstream Convex validation source is unreachable or malformed rather than that the key itself is invalid.

Solutions

  1. Log the raw response status and body text before json() to identify what the Convex endpoint actually returned
  2. Verify the Convex deployment URL / environment config is correct and the backend is deployed and healthy
  3. Retry with backoff — transient gateway errors commonly produce HTML bodies
  4. Add a content-type check (application/json) and fail fast with a clearer message if it is not JSON
  5. Treat EmbedKeyUnavailableError as 'validation unavailable' in callers and fall back to a cached or secondary validation path

Example fix

// before
value = await resp.json();
// after
const raw = await resp.text();
try { value = JSON.parse(raw); }
catch { throw new EmbedKeyUnavailableError(`Convex embed key validation unavailable: invalid-json (status=${resp.status}, body=${raw.slice(0, 120)})`); }
Defensive patterns

Strategy: try-catch

Validate before calling

// Before the call: verify config
if (!process.env.CONVEX_URL || !/^https:\/\/.+\.convex\.(site|cloud)/.test(process.env.CONVEX_URL)) {
  throw new Error('CONVEX_URL is not a valid Convex deployment URL');
}

Type guard

function isJsonObject(v: unknown): v is Record<string, unknown> {
  return typeof v === 'object' && v !== null && !Array.isArray(v);
}

Try / catch

try {
  const key = await fetchFromConvex(hash);
} catch (err) {
  if (err instanceof EmbedKeyUnavailableError && err.message.includes('invalid-json')) {
    // validation source unreachable: fall back to cached/secondary validation
    logger.warn({ hash, err }, 'convex embed-key source returned non-JSON; using fallback');
    return fallbackValidation(hash);
  }
  throw err;
}

Prevention

When it happens

Trigger: The Convex HTTP endpoint returns a non-JSON body: a 502/504 HTML gateway error page, an empty body, a plain-text error, or a gzip/encoding mismatch that corrupts the body before parsing.

Common situations: Convex deployment URL misconfigured (pointing at a non-Convex host), Convex backend outage or deploy in progress, a proxy/CDN intercepting the request and returning an HTML error page, or network truncation.

Understand the failure class

Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.

Related errors


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

Appendix: source

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

      },
      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');
  }
  return value;
}

/**
 * Delete the Redis cache entry for a specific embed key hash.
 * Called after revocation so the key cannot be used during the TTL window.
 * Uses prefixed keys (no raw=true) matching the cache writes above.
 */
export async function invalidateEmbedKeyCache(keyHash: string): Promise<void> {
  await deleteRedisKey(`${CACHE_KEY_PREFIX}${keyHash}`);
}

View on GitHub (pinned to 7d06c8633d)