different-ai/openwork · error · ApiError

invalid_payload

invalid_payload

Error message

operations must include <= ${FILE_SESSION_MAX_BATCH_ITEMS} items

What it means

The file batch endpoint validates that the operations array is non-empty and does not exceed FILE_SESSION_MAX_BATCH_ITEMS; oversized batches are rejected with a 400 invalid_payload ApiError before any operation is processed. This caps batch size to protect the server and file-session approval flow from unbounded work.

Source

Thrown at apps/server/src/routes/files.ts:759

  });

  addRoute(routes, "POST", "/files/sessions/:sessionId/ops", "client", async (ctx) => {
    ensureWritable(config);
    requireClientScope(ctx, "collaborator");
    const { session, workspace } = resolveFileSession(ctx, ctx.params.sessionId);
    if (!session.canWrite) {
      throw new ApiError(403, "forbidden", "File session is read-only");
    }

    const body = await readJsonBody(ctx.request);
    const operations = Array.isArray(body.operations)
      ? (body.operations as Array<Record<string, unknown>>)
      : null;
    if (!operations || !operations.length) {
      throw new ApiError(400, "invalid_payload", "operations must be a non-empty array");
    }
    if (operations.length > FILE_SESSION_MAX_BATCH_ITEMS) {
      throw new ApiError(400, "invalid_payload", `operations must include <= ${FILE_SESSION_MAX_BATCH_ITEMS} items`);
    }

    const items: Array<Record<string, unknown>> = [];
    const approvalPaths: string[] = [];
    for (const op of operations) {
      if (typeof op?.path === "string" && op.path.trim()) {
        approvalPaths.push(resolveSafeChildPath(workspace.path, normalizeWorkspaceRelativePath(op.path, { allowSubdirs: true })));
      }
      if (typeof op?.from === "string" && op.from.trim()) {
        approvalPaths.push(resolveSafeChildPath(workspace.path, normalizeWorkspaceRelativePath(op.from, { allowSubdirs: true })));
      }
      if (typeof op?.to === "string" && op.to.trim()) {
        approvalPaths.push(resolveSafeChildPath(workspace.path, normalizeWorkspaceRelativePath(op.to, { allowSubdirs: true })));
      }
    }

    if (approvalPaths.length) {
      await requireApproval(ctx, {

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Split the operations array into chunks of at most FILE_SESSION_MAX_BATCH_ITEMS and issue one request per chunk
  2. Check the error message/constant for the exact cap before batching
  3. Reduce batch size by grouping only related operations per session
  4. Contact the operator if a larger cap is legitimately needed (server-side constant)

Example fix

// before
await api.post(`/workspace/${id}/files/batch`, { operations: allOps });
// after
const MAX = 50; // FILE_SESSION_MAX_BATCH_ITEMS
for (let i = 0; i < allOps.length; i += MAX) {
  await api.post(`/workspace/${id}/files/batch`, { operations: allOps.slice(i, i + MAX) });
}
Defensive patterns

Strategy: validation

Validate before calling

import { FILE_SESSION_MAX_BATCH_ITEMS } from "@server/files";
const ops = buildOperations();
if (ops.length === 0) throw new Error("operations must be non-empty");
if (ops.length > FILE_SESSION_MAX_BATCH_ITEMS) {
  throw new Error(`split into chunks of <= ${FILE_SESSION_MAX_BATCH_ITEMS}`);
}

Type guard

function isValidBatch(ops: unknown): ops is Array<Record<string, unknown>> {
  return Array.isArray(ops) && ops.length > 0 && ops.length <= FILE_SESSION_MAX_BATCH_ITEMS;
}

Try / catch

try {
  await api.post(`/workspace/${id}/files/batch`, { operations: chunk });
} catch (e) {
  if (e.code === "invalid_payload" && /<= \d+ items/.test(e.message)) {
    for (const c of toChunks(ops, FILE_SESSION_MAX_BATCH_ITEMS)) await api.post(`...`, { operations: c });
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: POSTing a batch file-session payload whose operations array length is greater than FILE_SESSION_MAX_BATCH_ITEMS; also triggered when operations is missing/null or empty (same code, different message).

Common situations: A script generating a large refactor submits hundreds of file writes in one request; a migration tool dumps all its work into a single call; a client ignores pagination and batches an entire directory tree.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/583139209bfb83cb. Report an issue: GitHub.