{"record":{"id":"bb6825c40d4e9b93","repo":"koala73/worldmonitor","slug":"countries-limit-exceeded","errorCode":"COUNTRIES_LIMIT_EXCEEDED","errorMessage":"countries list capped at ${COUNTRIES_MAX} entries","messagePattern":"countries list capped at (.+?) entries","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/alertRules.ts","lineNumber":129,"sourceCode":" * NOT a strict ISO-3166 registry check — invalid alpha-2 codes (e.g. \"XX\")\n * pass shape validation but the relay's includes() check just won't match\n * any real event country. We deliberately don't soft-couple this file to a\n * canonical registry list (that lives elsewhere as part of the\n * followed-countries primitive) — keep alertRules independently shippable.\n */\nfunction normalizeCountries(input: string[]): string[] {\n  const cleaned: string[] = [];\n  const seen = new Set<string>();\n  for (const raw of input) {\n    if (typeof raw !== \"string\") continue;\n    const upper = raw.trim().toUpperCase();\n    if (!/^[A-Z]{2}$/.test(upper)) continue;\n    if (seen.has(upper)) continue;\n    seen.add(upper);\n    cleaned.push(upper);\n  }\n  if (cleaned.length > COUNTRIES_MAX) {\n    throw new ConvexError({\n      code: \"COUNTRIES_LIMIT_EXCEEDED\",\n      message: `countries list capped at ${COUNTRIES_MAX} entries`,\n    });\n  }\n  return cleaned;\n}\n\n// Same defensive ceiling as COUNTRIES_MAX — mirrors the client-side market\n// watchlist cap (src/services/market-watchlist.ts stops at 50 entries).\nconst TICKERS_MAX = 50;\n\n/**\n * Shape-validate + normalize an inbound `tickers` array (#4922 U3).\n * Modeled EXACTLY on normalizeCountries above:\n *  - trim each entry\n *  - uppercase\n *  - keep only `^[A-Z][A-Z0-9&-]{0,11}(\\.[A-Z]{1,3})?$` shapes — plain\n *    symbols (AAPL), share-class/conglomerate forms (BRK-B, M&M.NS) and","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/alertRules.ts#L111-L147","documentation":"Thrown by normalizeCountries when the cleaned/deduped country list exceeds COUNTRIES_MAX (50). Input is trimmed, uppercased, filtered to ^[A-Z]{2}$ shape, and deduped before the cap is checked, so only valid unique ISO-3166 alpha-2 codes count toward the limit. The cap is a defensive ceiling against patched-client abuse, not an ISO registry check.","triggerScenarios":"Calling setAlertRules, setAlertRulesForUser, setDigestSettingsForUser, setQuietHoursForUser, or setNotificationConfigForUser with a countries array containing more than 50 unique valid alpha-2 codes after normalization.","commonSituations":"A client patched to bypass UI limits submitting a bulk import; a migration script carrying an oversized country list; a watchlist sync that concatenates multiple region lists.","solutions":["Reduce the countries array to at most 50 unique ISO-3166 alpha-2 codes before submitting.","Drop invalid/duplicate entries client-side before the call (they waste nothing but make the count hard to reason about).","If a legitimate need exceeds 50, reconsider the scope — the comment notes ~250 countries exist and 50 is already generous."],"exampleFix":"// before\nconst countries = allWorldCountries; // 200+ entries\nawait setAlertRules(ctx, { ..., countries });\n// after\nconst countries = prioritizeCountries(allWorldCountries).slice(0, 50);\nawait setAlertRules(ctx, { ..., countries });","handlingStrategy":"validation","validationCode":"const COUNTRIES_MAX = 50;\nfunction normalizeAndCapCountries(input) {\n  const seen = new Set();\n  const out = [];\n  for (const raw of input) {\n    const upper = String(raw).trim().toUpperCase();\n    if (!/^[A-Z]{2}$/.test(upper) || seen.has(upper)) continue;\n    seen.add(upper);\n    out.push(upper);\n    if (out.length === COUNTRIES_MAX) break;\n  }\n  return out;\n}","typeGuard":"function isCountryList(input): input is string[] {\n  return Array.isArray(input) && input.every(c => typeof c === 'string' && /^[A-Z]{2}$/.test(c.trim().toUpperCase()));\n}","tryCatchPattern":"try {\n  await setAlertRules(args);\n} catch (e) {\n  if (e instanceof ConvexError && /countries list capped/.test(String(e.message))) {\n    // truncate and retry, or surface to user\n  } else throw e;\n}","preventionTips":["Client-side, mirror normalizeCountries exactly (trim, uppercase, ^[A-Z]{2}$ filter, dedupe) before submit.","Hard-cap the picker UI at 50 selections.","Reconcile against the same COUNTRIES_MAX constant used server-side."],"tags":["convex","validation","limits","notifications"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}