koala73/worldmonitor · warning · ApiError
Specify a fast/slow tier or one registered bootstrap key
Error message
Specify a fast/slow tier or one registered bootstrap key
What it means
buildRegistry enforces exactly one valid request shape: either a tier ('fast' or 'slow' with no explicit keys), or no tier plus exactly one key registered in BOOTSTRAP_CACHE_KEYS. Any other combination throws ApiError(400, 'Specify a fast/slow tier or one registered bootstrap key').
Solutions
- Send { tier: 'fast' } or { tier: 'slow' } with empty keys for the full tier registry
- For a single dataset, send exactly one key from BOOTSTRAP_CACHE_KEYS and omit tier
- Re-sync the client's key list against the server's BOOTSTRAP_CACHE_KEYS after deployments
Example fix
// before
await getBootstrapData(ctx, { tier: 'fast', keys: ['earthquakes'] });
// after
await getBootstrapData(ctx, { tier: 'fast' }); // or { keys: ['earthquakes'] } without tier Defensive patterns
Strategy: validation
Validate before calling
const valid = (req.tier === 'fast' || req.tier === 'slow') && (!req.keys || req.keys.length === 0)
|| (!req.tier && req.keys?.length === 1 && req.keys[0] in BOOTSTRAP_CACHE_KEYS);
if (!valid) throw new Error('send tier fast/slow alone, or exactly one registered bootstrap key'); Try / catch
try {
return await getBootstrapData(ctx, req);
} catch (e) {
if (e instanceof ApiError && e.status === 400) {
return await getBootstrapData(ctx, { tier: 'fast' }); // safe default tier
}
throw e;
} Prevention
- Treat tier and keys as mutually exclusive in the client
- Validate single keys against BOOTSTRAP_CACHE_KEYS before sending
- Never send an unknown tier string; constrain UI to fast/slow
- Default to { tier: 'fast' } when unsure
When it happens
Trigger: Sending tier AND keys together; tier='medium' or any unknown tier; no tier with zero or multiple keys; no tier with a single key not present in BOOTSTRAP_CACHE_KEYS.
Common situations: Client accumulating selected keys across UI toggles and sending several at once; typo in a bootstrap key name; server adding/removing a cache key so a previously valid client key is no longer registered; caching a default tier value that is not 'fast'/'slow'.
Related errors
- ${label} HTTP 400
- Could not resolve ${JSON.stringify(echoCountryInput(raw))} t
- INCOMPATIBLE_DELIVERY
- COUNTRIES_LIMIT_EXCEEDED
- TICKERS_LIMIT_EXCEEDED
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/970a46c6c3658bf9.
Report an issue: GitHub.
Appendix: source
Thrown at server/worldmonitor/infrastructure/v1/get-bootstrap-data.ts:25
} from '../../../../src/generated/server/worldmonitor/infrastructure/v1/service_server';
import { BOOTSTRAP_CACHE_KEYS, BOOTSTRAP_TIERS } from '../../../_shared/cache-keys';
// @ts-expect-error — Edge-safe JS helper
import { sanitizeBootstrapValue } from '../../../../api/_bootstrap-public-payload.js';
// @ts-expect-error — Edge-safe JS helper
import { extraCanadaAlertsCutoverReadKeys, canadaAlertsCutoverFallbackValue } from '../../../../api/_canada-alerts-cutover.js';
import { getCachedJsonBatch } from '../../../_shared/redis';
// Iran-events domain sunset (war ended 2026-07). Default OFF: this RPC bootstrap
// surface must also stop shipping iranEvents, mirroring api/bootstrap.js. It
// reads the SHARED BOOTSTRAP_CACHE_KEYS, so the gate lives here. Set
// IRAN_EVENTS_ENABLED=true to restore. See api/health.js.
const IRAN_EVENTS_ENABLED = (process.env.IRAN_EVENTS_ENABLED ?? 'false').toLowerCase() === 'true';
function buildRegistry(req: GetBootstrapDataRequest): Record<string, string> {
if ((req.tier && req.keys.length > 0)
|| (req.tier && req.tier !== 'fast' && req.tier !== 'slow')
|| (!req.tier && (req.keys.length !== 1 || !Object.prototype.hasOwnProperty.call(BOOTSTRAP_CACHE_KEYS, req.keys[0]!)))) {
throw new ApiError(400, 'Specify a fast/slow tier or one registered bootstrap key', '');
}
let registry: Record<string, string>;
if (req.tier === 'slow' || req.tier === 'fast') {
registry = Object.fromEntries(
Object.entries(BOOTSTRAP_CACHE_KEYS).filter(([key]) => BOOTSTRAP_TIERS[key] === req.tier),
);
} else {
registry = Object.fromEntries(
Object.entries(BOOTSTRAP_CACHE_KEYS).filter(([key]) => req.keys.includes(key)),
);
}
if (!IRAN_EVENTS_ENABLED) delete registry.iranEvents;
return registry;
}
/**
* Fetch one named dataset or a fixed public tier; never enumerate the full registry.View on GitHub (pinned to 7d06c8633d)