{"record":{"id":"d933aba78cf754bd","repo":"koala73/worldmonitor","slug":"must-contain-between-1-and-max-batch-operations","errorCode":null,"errorMessage":"must contain between 1 and ${MAX_BATCH_OPERATIONS} operations","messagePattern":"must contain between 1 and (.+?) operations","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"error","filePath":"server/worldmonitor/batch/v1/execute-batch.ts","lineNumber":199,"sourceCode":"}\n\nexport function createExecuteBatch(\n  fetchImpl: FetchLike = (input, init) => fetch(input, init),\n) {\n  return async function executeBatch(\n    ctx: ServerContext,\n    req: ExecuteBatchRequest,\n  ): Promise<ExecuteBatchResponse> {\n    // Recursion guard: the gateway forwards the marker untouched, so a batch\n    // arriving with it was issued BY a batch — refuse regardless of the\n    // per-path nested_batch check below.\n    if (ctx.request.headers.has(BATCH_MARKER_HEADER)) {\n      throw new ApiError(400, 'Nested batch requests are not allowed', '');\n    }\n\n    const operations = Array.isArray(req.operations) ? req.operations : [];\n    if (operations.length < 1 || operations.length > MAX_BATCH_OPERATIONS) {\n      throw new ValidationError([{\n        field: 'operations',\n        description: `must contain between 1 and ${MAX_BATCH_OPERATIONS} operations`,\n      }]);\n    }\n\n    const origin = new URL(ctx.request.url).origin;\n    const { validated, violations } = validateOperations(operations, origin);\n    if (violations.length > 0) {\n      throw new ValidationError(violations);\n    }\n\n    const headers = buildSubRequestHeaders(ctx.request.headers);\n    const results = await Promise.all(\n      validated.map((op) => runOperation(op, headers, fetchImpl)),\n    );\n\n    const succeeded = results.filter((r) => r.status >= 200 && r.status < 300 && !r.error).length;\n    return { results, succeeded, failed: results.length - succeeded };","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/server/worldmonitor/batch/v1/execute-batch.ts#L181-L217","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Keep each request between 1 and 20 operations","Chunk larger sets client-side into multiple batch calls (respecting per-request rate limits)","Guard before sending: Array.isArray(ops) && ops.length >= 1, and log the count when it is 0 to find upstream filtering bugs"],"exampleFix":"// before — one request with everything\nawait post('/api/batch/v1/execute', { operations: allOps }); // 350 ops -> 400\n\n// after — chunked at the documented ceiling\nconst MAX_BATCH_OPERATIONS = 20;\nfor (let i = 0; i < allOps.length; i += MAX_BATCH_OPERATIONS) {\n  const chunk = allOps.slice(i, i + MAX_BATCH_OPERATIONS);\n  await post('/api/batch/v1/execute', { operations: chunk });\n}","handlingStrategy":"validation","validationCode":"const MAX_BATCH_OPERATIONS = 20; // mirror of the server constant\nfunction chunk<T>(items: T[], size = MAX_BATCH_OPERATIONS): T[][] {\n  const out: T[][] = [];\n  for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));\n  return out;\n}\nif (!Array.isArray(ops) || ops.length === 0) throw new Error('nothing to batch');","typeGuard":"function isOperationsCountViolation(body: unknown): boolean {\n  const v = (body as { violations?: { field?: string }[] })?.violations;\n  return Array.isArray(v) && v.some((x) => x.field === 'operations');\n}","tryCatchPattern":"try {\n  await post('/api/batch/v1/execute', { operations: chunk(ops) });\n} catch (e) {\n  if (e instanceof HttpError && e.status === 400 && isOperationsCountViolation(e.body)) {\n    // re-chunk smaller (<=20) and resend; empty lists are a caller bug\n  }\n}","preventionTips":["Chunk at the documented ceiling (20) in every fan-out helper instead of assuming list sizes","Assert operations.length > 0 before sending — a zero-length build is usually an upstream filter bug","Keep the client constant in sync with the API docs; the server value is MAX_BATCH_OPERATIONS = 20"],"tags":["batch","validation","request-size"],"backgroundTag":"batch-size-limit-exceeded","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}