koala73/worldmonitor · error · ValidationError

Invalid scorecard bloc selection.

Error message

Invalid scorecard bloc selection.

What it means

getBlocScorecard validates the bloc request via resolveBlocSelection, which enforces: exactly one of preset or members, preset must be one of USMCA/EU27/BRICS/GCC/ASEAN/NATO, custom members must be 2-30 unique uppercase ISO-2 codes from the rankable universe. Any resolveBlocSelection throw is wrapped in a ValidationError naming the offending field ('preset' or 'members').

Source

Thrown at server/worldmonitor/scorecard/v1/get-bloc-scorecard.ts:26

// @ts-expect-error — JS module, no declaration file
import { captureSilentError } from '../../../../api/_sentry-edge.js';
import { markNoStoreFallbackResponse } from '../../../_shared/response-headers';
import { resolveBlocSelection } from './_bloc-presets';
import { asFiveFactorSnapshot, readFiveFactorSnapshot, type ScorecardSnapshotReader } from './_read-snapshot';
import { toPublicBlocScorecard } from './_response';
import { scoreBloc } from './_score-bloc';
import type { CountryScorecardEvidence } from './_types';

export async function getBlocScorecardWithReader(
  ctx: ServerContext,
  req: GetBlocScorecardRequest,
  reader: ScorecardSnapshotReader,
): Promise<GetBlocScorecardResponse> {
  let selection;
  try {
    selection = resolveBlocSelection(req);
  } catch (error) {
    throw new ValidationError([{
      field: req.preset ? 'preset' : 'members',
      description: error instanceof Error ? error.message : 'Invalid scorecard bloc selection.',
    }]);
  }
  let snapshotValue: unknown;
  try {
    snapshotValue = await reader(selection.members);
  } catch (error) {
    console.warn('[scorecard] snapshot read failed operation=get-bloc-scorecard', error instanceof Error ? error.message : 'unknown');
    void captureSilentError(error, { tags: { route: 'scorecard/get-bloc-scorecard', step: 'snapshot-read' } });
    return markNoStoreFallbackResponse(ctx.request, {
      unavailable: true,
      unavailableReason: 'scorecard-snapshot-unavailable',
    });
  }
  const snapshot = asFiveFactorSnapshot(snapshotValue);
  if (!snapshot) {
    return markNoStoreFallbackResponse(ctx.request, {

View on GitHub (pinned to 9361220cc0)

Solutions

  1. Send exactly one of preset or members, not both
  2. Use a preset from SCORECARD_BLOC_PRESETS: USMCA, EU27, BRICS, GCC, ASEAN, NATO (case-sensitive)
  3. For custom blocs, send 2-30 unique uppercase ISO-2 codes that are in the rankable universe
  4. Normalize client input: trim, uppercase, dedupe, and filter members before calling

Example fix

// before
getBlocScorecard({ preset: 'EU', members: ['DE'] })
// after
getBlocScorecard({ preset: 'EU27' })
// or custom:
getBlocScorecard({ members: ['DE', 'FR'] })
Defensive patterns

Strategy: validation

Validate before calling

const PRESETS = ['USMCA','EU27','BRICS','GCC','ASEAN','NATO'];
function validBlocInput(req) {
  const hasPreset = Boolean(String(req.preset || '').trim());
  const members = Array.isArray(req.members) ? req.members : [];
  if (hasPreset === (members.length > 0)) return false;
  if (hasPreset) return PRESETS.includes(req.preset.trim());
  const uniq = [...new Set(members.map((m) => String(m).trim().toUpperCase()))];
  return uniq.length >= 2 && uniq.length <= 30 && uniq.every((m) => /^[A-Z]{2}$/.test(m));
}

Type guard

function isBlocRequest(req) {
  return typeof req === 'object' && req !== null &&
    (typeof req.preset === 'string' || Array.isArray(req.members));
}

Try / catch

try {
  const res = await getBlocScorecard(req);
} catch (e) {
  if (e.name === 'ValidationError' && (e.field === 'preset' || e.field === 'members')) {
    // surface e.description, correct the bloc selection
  } else throw e;
}

Prevention

When it happens

Trigger: Passing both preset and members; passing neither; unknown preset string (e.g. 'EU' or 'brics' lowercase); custom members with <2 or >30 entries; non-uppercase or non-2-letter member codes; duplicate members; countries outside the rankable universe.

Common situations: Client UI sending both a preset and a stale members array; hardcoded preset names that drift from SCORECARD_BLOC_PRESETS; lowercase country codes from user input; including countries like 'HK' or 'TW' that are not in the rankable universe.

Related errors


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