koala73/worldmonitor · error · ConvexError

COUNTRIES_LIMIT_EXCEEDED

COUNTRIES_LIMIT_EXCEEDED

Error message

countries list capped at ${COUNTRIES_MAX} entries

What it means

normalizeCountries (convex/alertRules.ts:117) shape-validates, trims, uppercases, and dedupes the countries array, then enforces the defensive ceiling COUNTRIES_MAX = 50. The cap fires only after cleaning — invalid shapes are silently dropped and duplicates collapse — so it triggers on more than 50 distinct codes matching ^[A-Z]{2}$, throwing ConvexError { code: 'COUNTRIES_LIMIT_EXCEEDED', message: 'countries list capped at 50 entries' }. There are ~250 ISO-3166 alpha-2 codes, so a full 'select all' list will always exceed it.

Solutions

  1. Normalize client-side exactly like the server (trim, uppercase, keep ^[A-Z]{2}$, dedupe) and cap at 50 before calling.
  2. Restrict the picker to countries the user actually follows — more than 50 country-scoped alerts exceeds any real usage pattern.
  3. For imports, validate first and page/drop the remainder: there is no server-side partial write, the whole mutation rejects.
  4. If a higher cap is truly required, change COUNTRIES_MAX in convex/alertRules.ts and the client watchlist cap together — 50 is a deliberate defensive ceiling, not a platform limit.

Example fix

// before
await mutateAPI.alertRules.setAlertRules({ variant: 'default', enabled: true, eventTypes: [], channels: ['email'], countries: ALL_ISO_COUNTRIES }); // ~249 entries
// ConvexError: COUNTRIES_LIMIT_EXCEEDED

// after — same normalization as the server, capped at 50
const normalizeCountries = (input: string[]) =>
  [...new Set(input.map((c) => c.trim().toUpperCase()).filter((c) => /^[A-Z]{2}$/.test(c)))].slice(0, 50);
await mutateAPI.alertRules.setAlertRules({ variant: 'default', enabled: true, eventTypes: [], channels: ['email'], countries: normalizeCountries(selected) });
Defensive patterns

Strategy: validation

Validate before calling

// Replicate the server normalization, then cap and surface truncation.
function normalizeCountries(input: string[]): string[] {
  const cleaned = [...new Set(input.map((c) => c.trim().toUpperCase()).filter((c) => /^[A-Z]{2}$/.test(c)))];
  return cleaned.slice(0, 50);
}
const countries = normalizeCountries(selectedCountries);
if (new Set(selectedCountries.map((c) => c.trim().toUpperCase()).filter((c) => /^[A-Z]{2}$/.test(c))).size > 50) {
  showToast('Only the first 50 countries are saved.');
}
await mutateAPI.alertRules.setAlertRules({ ...args, countries });

Type guard

const isWithinCountriesCap = (v: unknown): v is string[] =>
  Array.isArray(v) &&
  new Set((v as string[]).map((c) => String(c).trim().toUpperCase()).filter((c) => /^[A-Z]{2}$/.test(c))).size <= 50;

Try / catch

try {
  await mutateAPI.alertRules.setAlertRules(args);
} catch (err) {
  if (err instanceof ConvexError && (err.data as { code?: string }).code === 'COUNTRIES_LIMIT_EXCEEDED') {
    setPickerError('Pick at most 50 countries'); // keep first 50 by user priority
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: setAlertRules (or setAlertRulesForUser / setDigestSettingsForUser / setNotificationConfigForUser) with a countries array containing more than 50 distinct valid alpha-2 codes — e.g. a bulk 'select all countries' action submitting ~249 codes.

Common situations: A country multi-select with a 'select all' control mirroring the full ISO-3166 list; importing alert preferences from another product with no cap; migration scripts bulk-writing country scopes; client code diverging from the 50-entry watchlist cap that mirrors this server limit (src/services/market-watchlist.ts also stops at 50).

Related errors


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/bb6825c40d4e9b93. Report an issue: GitHub.

Appendix: source

Thrown at convex/alertRules.ts:129

 * NOT a strict ISO-3166 registry check — invalid alpha-2 codes (e.g. "XX")
 * pass shape validation but the relay's includes() check just won't match
 * any real event country. We deliberately don't soft-couple this file to a
 * canonical registry list (that lives elsewhere as part of the
 * followed-countries primitive) — keep alertRules independently shippable.
 */
function normalizeCountries(input: string[]): string[] {
  const cleaned: string[] = [];
  const seen = new Set<string>();
  for (const raw of input) {
    if (typeof raw !== "string") continue;
    const upper = raw.trim().toUpperCase();
    if (!/^[A-Z]{2}$/.test(upper)) continue;
    if (seen.has(upper)) continue;
    seen.add(upper);
    cleaned.push(upper);
  }
  if (cleaned.length > COUNTRIES_MAX) {
    throw new ConvexError({
      code: "COUNTRIES_LIMIT_EXCEEDED",
      message: `countries list capped at ${COUNTRIES_MAX} entries`,
    });
  }
  return cleaned;
}

// Same defensive ceiling as COUNTRIES_MAX — mirrors the client-side market
// watchlist cap (src/services/market-watchlist.ts stops at 50 entries).
const TICKERS_MAX = 50;

/**
 * Shape-validate + normalize an inbound `tickers` array (#4922 U3).
 * Modeled EXACTLY on normalizeCountries above:
 *  - trim each entry
 *  - uppercase
 *  - keep only `^[A-Z][A-Z0-9&-]{0,11}(\.[A-Z]{1,3})?$` shapes — plain
 *    symbols (AAPL), share-class/conglomerate forms (BRK-B, M&M.NS) and

View on GitHub (pinned to eeab0a219f)