koala73/worldmonitor · error · EmbedKeyUnavailableError
Convex embed key validation unavailable: invalid-payload
Error message
Convex embed key validation unavailable: invalid-payload
What it means
After successfully parsing the Convex response body as JSON, fetchFromConvex validates its shape with isEmbedKeyResult. If the parsed value is a JSON document but does not match the expected embed-key result schema, this EmbedKeyUnavailableError with reason 'invalid-payload' is thrown. It means the Convex function responded but returned an unexpected structure — usually a contract drift between the Convex query and this client.
Solutions
- Inspect the actual payload from the Convex endpoint and diff it against isEmbedKeyResult's expected fields
- Ensure the Convex query and the server/_shared/embed-key.ts contract are deployed in lockstep
- Verify the Convex deployment URL points at the intended environment (staging vs production)
- Check for an auth or proxy layer returning its own JSON error body with HTTP 200
- Broaden the error message to include the unexpected keys for faster diagnosis
Example fix
// before
if (!isEmbedKeyResult(value)) {
throw new EmbedKeyUnavailableError('Convex embed key validation unavailable: invalid-payload');
}
// after
if (!isEmbedKeyResult(value)) {
throw new EmbedKeyUnavailableError(`Convex embed key validation unavailable: invalid-payload ${JSON.stringify(value).slice(0, 200)}`);
} Defensive patterns
Strategy: type-guard
Validate before calling
// Before the call: assert deployment/contract version
const res = await fetch(`${CONVEX_URL}/api/query`, { method: 'POST' });
if (res.status !== 200) throw new Error(`Convex returned ${res.status}; deploy is unhealthy`); Type guard
function isEmbedKeyResult(v: unknown): v is { key: string; valid: boolean } {
return typeof v === 'object' && v !== null
&& 'key' in v && 'valid' in v
&& typeof (v as any).key === 'string'
&& typeof (v as any).valid === 'boolean';
} Try / catch
try {
const result = await fetchFromConvex(hash);
} catch (err) {
if (err instanceof EmbedKeyUnavailableError && err.message.includes('invalid-payload')) {
logger.error({ hash, err }, 'Convex payload contract drift; check deploy versions');
return null; // or fall back to local validation
}
throw err;
} Prevention
- Keep the Convex query schema and embed-key.ts types in the same repo and deploy them together
- Add a contract test that runs isEmbedKeyResult against a live/staged Convex response
- Point staging workers at staging Convex deployments, production at production
- Version the Convex function response shape when it changes
When it happens
Trigger: The Convex query was changed or renamed and now returns a different object shape; a deploy returned an error object like {error: ...} with HTTP 200; a proxy returned JSON (e.g. {message:'unauthorized'}) instead of the expected result; the Convex function returns null wrapped differently or omits required fields.
Common situations: Version skew after a Convex deploy (server updated, API worker not, or vice versa); environment pointing at the wrong Convex deployment (staging vs production schema); auth/proxy layer injecting its own JSON error body.
Related errors
- INCOMPATIBLE_DELIVERY
- COUNTRIES_LIMIT_EXCEEDED
- TICKERS_LIMIT_EXCEEDED
- digestHour must be an integer 0–23
- digestTimezone must be a valid IANA timezone (e.g. America/N
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/e19dee95b5a12ec2.
Report an issue: GitHub.
Appendix: source
Thrown at server/_shared/embed-key.ts:173
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)