koala73/worldmonitor · error · Error

Custom scorecard bloc members must be uppercase ISO-2 codes.

Error message

Custom scorecard bloc members must be uppercase ISO-2 codes.

What it means

Each custom member in resolveBlocSelection must match /^[A-Z]{2}$/ — exactly two uppercase ASCII letters (ISO 3166-1 alpha-2). Lowercase codes, 3-letter ISO-3 codes, names, whitespace, or mixed-case entries cause this error. The strict format guarantees deterministic, safe cache keys (custom:<sorted codes joined>) and stable sorting.

Source

Thrown at server/worldmonitor/scorecard/v1/_bloc-presets.ts:38

    id: 'NATO',
    label: 'NATO',
    members: ['AL', 'BE', 'BG', 'CA', 'HR', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IS', 'IT', 'LV', 'LT', 'LU', 'ME', 'NL', 'MK', 'NO', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'TR', 'GB', 'US'],
  },
};

export function resolveBlocSelection(input: { preset?: string; members?: string[] }): ScorecardBlocSelection {
  const preset = String(input.preset || '').trim();
  const members = Array.isArray(input.members) ? input.members : [];
  if (Boolean(preset) === (members.length > 0)) {
    throw new Error('Select exactly one scorecard bloc preset or custom member list.');
  }
  if (preset) {
    if (!(preset in SCORECARD_BLOC_PRESETS)) throw new Error(`Unknown scorecard bloc preset: ${preset}`);
    return SCORECARD_BLOC_PRESETS[preset as ScorecardBlocPreset];
  }
  if (members.length < 2 || members.length > 30) throw new Error('Custom scorecard blocs require 2-30 members.');
  if (members.some((member) => !/^[A-Z]{2}$/.test(member))) {
    throw new Error('Custom scorecard bloc members must be uppercase ISO-2 codes.');
  }
  if (new Set(members).size !== members.length) throw new Error('Custom scorecard bloc members must be unique.');
  if (members.some((member) => !isInRankableUniverse(member))) {
    throw new Error('Custom scorecard bloc members must belong to the public rankable country universe.');
  }
  const sorted = [...members].sort();
  return { id: `custom:${sorted.join('-')}`, label: sorted.join(' + '), members: sorted };
}

View on GitHub (pinned to 9361220cc0)

Solutions

  1. Uppercase and trim each member before sending: m.trim().toUpperCase().
  2. Validate with /^[A-Z]{2}$/ client-side and reject or map non-conforming entries (names/ISO-3 → ISO-2 lookup).
  3. Use a dropdown of valid ISO-2 codes instead of free-text input.
  4. If a code can't be mapped, drop it (if ≥2 remain) rather than sending an invalid entry.

Example fix

// before
resolveBlocSelection({ members: ['de', 'fr'] }); // throws
// after
const members = raw.map((m) => m.trim().toUpperCase()).filter((m) => /^[A-Z]{2}$/.test(m));
if (members.length >= 2) resolveBlocSelection({ members });
Defensive patterns

Strategy: type-guard

Validate before calling

const members = raw.map((m) => String(m).trim().toUpperCase());
if (!members.every((m) => /^[A-Z]{2}$/.test(m))) throw new Error('members must be uppercase ISO-2 codes');

Type guard

function isUppercaseIso2(v: unknown): v is string {
  return typeof v === 'string' && /^[A-Z]{2}$/.test(v);
}

Try / catch

try {
  const bloc = await getBlocScorecard({ members });
} catch (err) {
  if (err.message.includes('uppercase ISO-2 codes')) {
    return badRequest({ invalid: members.filter((m) => !/^[A-Z]{2}$/.test(m)) });
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling getBlocScorecardWithReader with members like ['de','FR'] (lowercase), ['USA','FR'] (ISO-3), ['Germany','FR'] (names), ['D E','FR'], or ['US ','FR'] (trailing space — trim yourself first).

Common situations: 1) Client collects country names from free-text input instead of an ISO-2 picker; 2) codes arrive lowercase from a locale/URL slug; 3) server previously accepted case-insensitive input and an old client still sends lowercase; 4) CSV/clipboard paste introduces stray whitespace.

Related errors


AI-assisted analysis of koala73/worldmonitor@9361220cc0 (2026-09-01). Data as JSON: /api/errors/37f5fb1a15df0244. Report an issue: GitHub.