koala73/worldmonitor · error · ValidationError

must contain between 1 and ${MAX_BATCH_OPERATIONS} operation

Error message

must contain between 1 and ${MAX_BATCH_OPERATIONS} operations

What it means

executeBatch throws ValidationError (HTTP 400 with a violations array) when req.operations is not an array of 1 to MAX_BATCH_OPERATIONS (20) entries. A missing or non-array operations field coerces to an empty array and hits the same check, so 'forgot the field' and 'too many operations' produce this one error. Unlike per-operation path problems (which become per-result errors), count violations reject the entire request.

Source

Thrown at server/worldmonitor/batch/v1/execute-batch.ts:199

}

export function createExecuteBatch(
  fetchImpl: FetchLike = (input, init) => fetch(input, init),
) {
  return async function executeBatch(
    ctx: ServerContext,
    req: ExecuteBatchRequest,
  ): Promise<ExecuteBatchResponse> {
    // Recursion guard: the gateway forwards the marker untouched, so a batch
    // arriving with it was issued BY a batch — refuse regardless of the
    // per-path nested_batch check below.
    if (ctx.request.headers.has(BATCH_MARKER_HEADER)) {
      throw new ApiError(400, 'Nested batch requests are not allowed', '');
    }

    const operations = Array.isArray(req.operations) ? req.operations : [];
    if (operations.length < 1 || operations.length > MAX_BATCH_OPERATIONS) {
      throw new ValidationError([{
        field: 'operations',
        description: `must contain between 1 and ${MAX_BATCH_OPERATIONS} operations`,
      }]);
    }

    const origin = new URL(ctx.request.url).origin;
    const { validated, violations } = validateOperations(operations, origin);
    if (violations.length > 0) {
      throw new ValidationError(violations);
    }

    const headers = buildSubRequestHeaders(ctx.request.headers);
    const results = await Promise.all(
      validated.map((op) => runOperation(op, headers, fetchImpl)),
    );

    const succeeded = results.filter((r) => r.status >= 200 && r.status < 300 && !r.error).length;
    return { results, succeeded, failed: results.length - succeeded };

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Keep each request between 1 and 20 operations
  2. Chunk larger sets client-side into multiple batch calls (respecting per-request rate limits)
  3. Guard before sending: Array.isArray(ops) && ops.length >= 1, and log the count when it is 0 to find upstream filtering bugs

Example fix

// before — one request with everything
await post('/api/batch/v1/execute', { operations: allOps }); // 350 ops -> 400

// after — chunked at the documented ceiling
const MAX_BATCH_OPERATIONS = 20;
for (let i = 0; i < allOps.length; i += MAX_BATCH_OPERATIONS) {
  const chunk = allOps.slice(i, i + MAX_BATCH_OPERATIONS);
  await post('/api/batch/v1/execute', { operations: chunk });
}
Defensive patterns

Strategy: validation

Validate before calling

const MAX_BATCH_OPERATIONS = 20; // mirror of the server constant
function chunk<T>(items: T[], size = MAX_BATCH_OPERATIONS): T[][] {
  const out: T[][] = [];
  for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
  return out;
}
if (!Array.isArray(ops) || ops.length === 0) throw new Error('nothing to batch');

Type guard

function isOperationsCountViolation(body: unknown): boolean {
  const v = (body as { violations?: { field?: string }[] })?.violations;
  return Array.isArray(v) && v.some((x) => x.field === 'operations');
}

Try / catch

try {
  await post('/api/batch/v1/execute', { operations: chunk(ops) });
} catch (e) {
  if (e instanceof HttpError && e.status === 400 && isOperationsCountViolation(e.body)) {
    // re-chunk smaller (<=20) and resend; empty lists are a caller bug
  }
}

Prevention

When it happens

Trigger: POST with operations: []; POST with 21+ operations in one request; operations sent as an object or omitted entirely (Array.isArray fails, treated as empty); a client building operations dynamically ends up with zero after filtering.

Common situations: Fan-out loop groups items into one oversized batch instead of chunks; JSON serialization bug drops the operations field; paginated caller batches 'all N items' without a ceiling.

Related errors


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