{"record":{"id":"45258db7be647709","repo":"koala73/worldmonitor","slug":"must-be-at-most-max-operation-id-length-charact","errorCode":null,"errorMessage":"must be at most ${MAX_OPERATION_ID_LENGTH} characters","messagePattern":"must be at most (.+?) characters","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"error","filePath":"server/worldmonitor/batch/v1/execute-batch.ts","lineNumber":208,"sourceCode":"    // 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 };\n  };\n}\n\nexport const executeBatch = createExecuteBatch();\n","sourceCodeStart":190,"sourceCodeEnd":222,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/server/worldmonitor/batch/v1/execute-batch.ts#L190-L222","documentation":"Inside validateOperations, an operation id (after trimming; missing ids default to the operation's index as a string) longer than MAX_OPERATION_ID_LENGTH (64 characters) is collected as a FieldViolation on operations[index].id and the whole request fails with ValidationError. Id length and duplicates reject the batch upfront because results are correlated by id; path problems, in contrast, degrade to per-operation errors.","triggerScenarios":"Ids built as UUID-plus-description or base64-encoded payloads exceeding 64 chars; natural-language ids like 'get-all-flights-for-JFK-to-LHR-roundtrip-december'; concatenating multiple identifiers into one id string.","commonSituations":"Client generates ids from user-supplied labels without a length cap; ids that embed query strings or encoded context; migrating from another batch API with a larger id limit.","solutions":["Keep ids at or under 64 characters — a short correlation key, not a payload","Omit id entirely: the server defaults it to the operation's index ('0', '1', ...) which is always valid","Hash or truncate long generated ids (e.g. crypto.randomUUID() at 36 chars fits comfortably)"],"exampleFix":"// before — id carries descriptive context\n{ id: `flights-${origin}-${destination}-${JSON.stringify(filters)}`, path: '/api/aviation/v1/search-google-flights' }\n\n// after — short unique key; parameters belong in the path/query\n{ id: `flights-${i}`, path: `/api/aviation/v1/search-google-flights?origin=${origin}&destination=${destination}` }","handlingStrategy":"validation","validationCode":"const MAX_OPERATION_ID_LENGTH = 64;\nfunction normalizeOpId(id: string | undefined, index: number): string {\n  const trimmed = (id ?? '').trim();\n  return trimmed.length > 0 && trimmed.length <= MAX_OPERATION_ID_LENGTH\n    ? trimmed\n    : String(index); // fall back to the server's own index-default scheme\n}","typeGuard":"function isIdLengthViolation(body: unknown): boolean {\n  const v = (body as { violations?: { field?: string; description?: string }[] })?.violations;\n  return Array.isArray(v) && v.some((x) => x.field?.startsWith('operations[') && x.field?.endsWith('.id')\n    && x.description?.includes('at most'));\n}","tryCatchPattern":"try {\n  await post('/api/batch/v1/execute', { operations });\n} catch (e) {\n  if (e instanceof HttpError && e.status === 400 && isIdLengthViolation(e.body)) {\n    // shorten ids to <=64 chars (or omit them) and resend the same batch\n  }\n}","preventionTips":["Treat operation ids as correlation keys, not metadata — 64 chars is ample for crypto.randomUUID()","Cap any user-derived id with .slice(0, 64) at construction time","Omit id when you do not need custom correlation; the server assigns stable index ids"],"tags":["batch","validation","field-length","operation-id"],"backgroundTag":"field-length-limit","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}