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
- Normalize client-side exactly like the server (trim, uppercase, keep ^[A-Z]{2}$, dedupe) and cap at 50 before calling.
- Restrict the picker to countries the user actually follows — more than 50 country-scoped alerts exceeds any real usage pattern.
- For imports, validate first and page/drop the remainder: there is no server-side partial write, the whole mutation rejects.
- 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
- Normalize before counting — junk shapes and duplicates don't count toward the server's 50.
- Cap the multi-select UI at 50 with a visible counter.
- Keep the client watchlist cap and COUNTRIES_MAX in sync (both 50 today).
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
- INCOMPATIBLE_DELIVERY
- TICKERS_LIMIT_EXCEEDED
- COMPANY_LIMIT_REACHED
- COMPANY_MONITORING_ADMISSION_QUERY_VERSION_INVALID
- COMPANY_MONITORING_EVIDENCE_REVISION_INVALID
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) andView on GitHub (pinned to eeab0a219f)