{"record":{"id":"970a46c6c3658bf9","repo":"koala73/worldmonitor","slug":"specify-a-fast-slow-tier-or-one-registered-bootstrap-key","errorCode":null,"errorMessage":"Specify a fast/slow tier or one registered bootstrap key","messagePattern":"Specify a fast/slow tier or one registered bootstrap key","errorType":"http","errorClass":"ApiError","httpStatus":400,"severity":"warning","filePath":"server/worldmonitor/infrastructure/v1/get-bootstrap-data.ts","lineNumber":25,"sourceCode":"} from '../../../../src/generated/server/worldmonitor/infrastructure/v1/service_server';\nimport { BOOTSTRAP_CACHE_KEYS, BOOTSTRAP_TIERS } from '../../../_shared/cache-keys';\n// @ts-expect-error — Edge-safe JS helper\nimport { sanitizeBootstrapValue } from '../../../../api/_bootstrap-public-payload.js';\n// @ts-expect-error — Edge-safe JS helper\nimport { extraCanadaAlertsCutoverReadKeys, canadaAlertsCutoverFallbackValue } from '../../../../api/_canada-alerts-cutover.js';\nimport { getCachedJsonBatch } from '../../../_shared/redis';\n\n// Iran-events domain sunset (war ended 2026-07). Default OFF: this RPC bootstrap\n// surface must also stop shipping iranEvents, mirroring api/bootstrap.js. It\n// reads the SHARED BOOTSTRAP_CACHE_KEYS, so the gate lives here. Set\n// IRAN_EVENTS_ENABLED=true to restore. See api/health.js.\nconst IRAN_EVENTS_ENABLED = (process.env.IRAN_EVENTS_ENABLED ?? 'false').toLowerCase() === 'true';\n\nfunction buildRegistry(req: GetBootstrapDataRequest): Record<string, string> {\n  if ((req.tier && req.keys.length > 0)\n    || (req.tier && req.tier !== 'fast' && req.tier !== 'slow')\n    || (!req.tier && (req.keys.length !== 1 || !Object.prototype.hasOwnProperty.call(BOOTSTRAP_CACHE_KEYS, req.keys[0]!)))) {\n    throw new ApiError(400, 'Specify a fast/slow tier or one registered bootstrap key', '');\n  }\n  let registry: Record<string, string>;\n  if (req.tier === 'slow' || req.tier === 'fast') {\n    registry = Object.fromEntries(\n      Object.entries(BOOTSTRAP_CACHE_KEYS).filter(([key]) => BOOTSTRAP_TIERS[key] === req.tier),\n    );\n  } else {\n    registry = Object.fromEntries(\n      Object.entries(BOOTSTRAP_CACHE_KEYS).filter(([key]) => req.keys.includes(key)),\n    );\n  }\n\n  if (!IRAN_EVENTS_ENABLED) delete registry.iranEvents;\n  return registry;\n}\n\n/**\n * Fetch one named dataset or a fixed public tier; never enumerate the full registry.","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/server/worldmonitor/infrastructure/v1/get-bootstrap-data.ts#L7-L43","documentation":"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').","triggerScenarios":"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.","commonSituations":"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'.","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"],"exampleFix":"// before\nawait getBootstrapData(ctx, { tier: 'fast', keys: ['earthquakes'] });\n// after\nawait getBootstrapData(ctx, { tier: 'fast' }); // or { keys: ['earthquakes'] } without tier","handlingStrategy":"validation","validationCode":"const valid = (req.tier === 'fast' || req.tier === 'slow') && (!req.keys || req.keys.length === 0)\n  || (!req.tier && req.keys?.length === 1 && req.keys[0] in BOOTSTRAP_CACHE_KEYS);\nif (!valid) throw new Error('send tier fast/slow alone, or exactly one registered bootstrap key');","typeGuard":null,"tryCatchPattern":"try {\n  return await getBootstrapData(ctx, req);\n} catch (e) {\n  if (e instanceof ApiError && e.status === 400) {\n    return await getBootstrapData(ctx, { tier: 'fast' }); // safe default tier\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["validation","request-shape","bootstrap"],"backgroundTag":"mutually-exclusive-options","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"}