koala73/worldmonitor · error · ValidationError
disruptionPct must be an integer from 0 to 100 for a physica
Error message
disruptionPct must be an integer from 0 to 100 for a physical scenario
What it means
runScenario validates `disruptionPct` whenever it is supplied: it must be an integer in [0,100], and a physical (chokepoint) scenario template must actually have affectedChokepointIds. If the value is fractional, negative, above 100, or the template has no chokepoints, a ValidationError with field 'disruptionPct' is thrown.
Solutions
- Round or floor the percentage to an integer and clamp into 0..100 before calling: Math.max(0, Math.min(100, Math.round(pct)))
- Parse form/slider input with Number() and verify Number.isInteger before sending
- Omit disruptionPct entirely when it does not apply to the chosen scenario template
- Choose a scenario template that has affectedChokepointIds populated when a physical disruption percentage is required
Example fix
// before
await client.runScenario({ templateId, disruptionPct: slider.value });
// after
const pct = Math.max(0, Math.min(100, Math.round(Number(slider.value))));
if (!Number.isInteger(pct)) throw new Error('bad pct');
await client.runScenario({ templateId, disruptionPct: pct }); Defensive patterns
Strategy: validation
Validate before calling
const pct = req.disruptionPct;
if (pct !== undefined && (!Number.isInteger(pct) || pct < 0 || pct > 100)) {
throw new Error('disruptionPct must be an integer 0..100');
} Type guard
function isValidDisruptionPct(v: unknown): v is number {
return typeof v === 'number' && Number.isInteger(v) && v >= 0 && v <= 100;
} Try / catch
try {
return await client.runScenario(req);
} catch (e) {
if (e instanceof ValidationError && e.fields?.[0]?.field === 'disruptionPct') {
return client.runScenario({ ...req, disruptionPct: clampPercent(req.disruptionPct) });
}
throw e;
} Prevention
- Clamp and round slider/percentage inputs before sending
- Convert form strings to Number and validate with Number.isInteger
- Only send disruptionPct for templates that have chokepoints affected
When it happens
Trigger: Calling runScenario with disruptionPct = 12.5 (non-integer), -5, 150, NaN, or a string like "50"; or supplying disruptionPct against a scenario template whose affectedChokepointIds array is empty.
Common situations: UI slider emits fractional values; percentage computed from user input without rounding; client sends the value as a string from a form field; a custom/blank template without chokepoint mappings is combined with a physical disruption parameter.
Understand the failure class
Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.
Related errors
- Invalid scorecard bloc selection.
- alertThreshold must be between 0 and 100
- Expected an array of Sentry issues. A Sentry error body is a
- Sentry issues response was not an array
- Unknown scenario: ${scenarioId}
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/d8f58dc2b73c0d5a.
Report an issue: GitHub.
Appendix: source
Thrown at server/worldmonitor/scenario/v1/run-scenario.ts:48
req: RunScenarioRequest,
): Promise<RunScenarioResponse> {
await requirePremiumRpcAccess(ctx.request, ApiError, 'PRO subscription required');
const scenarioId = (req.scenarioId ?? '').trim();
if (!scenarioId) {
throw new ValidationError([{ field: 'scenarioId', description: 'scenarioId is required' }]);
}
const template = getScenarioTemplate(scenarioId);
if (!template) {
throw new ValidationError([{ field: 'scenarioId', description: `Unknown scenario: ${scenarioId}` }]);
}
const disruptionPct = req.disruptionPct;
if (disruptionPct !== undefined && (
!Number.isInteger(disruptionPct) || disruptionPct < 0 || disruptionPct > 100
|| template.affectedChokepointIds.length === 0
)) {
throw new ValidationError([{ field: 'disruptionPct', description: 'disruptionPct must be an integer from 0 to 100 for a physical scenario' }]);
}
const iso2 = req.iso2 ? req.iso2.trim() : '';
if (iso2 && !/^[A-Z]{2}$/.test(iso2)) {
throw new ValidationError([{ field: 'iso2', description: 'iso2 must be a 2-letter uppercase country code' }]);
}
// Queue-depth backpressure. Raw key: worker reads it unprefixed, so we must too.
const [depthEntry] = await runRedisPipeline([['LLEN', QUEUE_KEY]], true);
const depth = typeof depthEntry?.result === 'number' ? depthEntry.result : 0;
if (depth > MAX_QUEUE_DEPTH) {
throw new ApiError(429, 'Scenario queue is at capacity, please try again later', '');
}
const jobId = generateJobId();
const payload = JSON.stringify({
jobId,
scenarioId,View on GitHub (pinned to 7d06c8633d)