koala73/worldmonitor · error · ApiError

Nested batch requests are not allowed

Error message

Nested batch requests are not allowed

What it means

executeBatch (POST /api/batch/v1/execute) refuses any inbound request that already carries the x-wm-batch marker header. Sub-requests dispatched by a batch get this header set by buildSubRequestHeaders, and the gateway forwards it untouched, so its presence proves the request was issued by another batch — batching a batch would multiply per-request resource use recursively. It is an ApiError 400 raised before any operation validation.

Source

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

  } catch {
    return { id: op.id, status: response.status, error: 'invalid_json' };
  }

  return { id: op.id, status: response.status, body: body as BatchOperationBody, error: '' };
}

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(

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Stop forwarding the x-wm-batch header in your HTTP client — only the server sets it on sub-requests
  2. If you intended to run multiple batches, issue separate top-level POST /api/batch/v1/execute requests instead of nesting
  3. If you meant to include another endpoint in a batch, use its documented /api/<domain>/v<N>/<rpc> path; it will run as a normal operation

Example fix

// before — headers object reused from a prior (server-dispatched) request
const resp = await fetch(`${origin}/api/batch/v1/execute`, {
  method: 'POST',
  headers: capturedHeaders, // contains x-wm-batch: 1 -> 400
  body: JSON.stringify({ operations }),
});

// after — explicit allowlist of client headers only
const resp = await fetch(`${origin}/api/batch/v1/execute`, {
  method: 'POST',
  headers: { authorization: capturedHeaders.authorization, 'content-type': 'application/json' },
  body: JSON.stringify({ operations }),
});
Defensive patterns

Strategy: validation

Validate before calling

// strip any batch marker and batch paths before submitting
const safeHeaders = new Headers(myHeaders); // never copy x-wm-batch from prior traffic
safeHeaders.delete('x-wm-batch');
const operations = ops.filter((o) => !o.path.startsWith('/api/batch/'));

Type guard

function isNestedBatchError(body: unknown): boolean {
  return typeof body === 'object' && body !== null && 'message' in body
    && (body as { message?: string }).message === 'Nested batch requests are not allowed';
}

Try / catch

try {
  await post('/api/batch/v1/execute', { operations });
} catch (e) {
  if (e instanceof HttpError && e.status === 400 && isNestedBatchError(e.body)) {
    // fix the client's header forwarding; retrying unchanged will fail again
  }
}

Prevention

When it happens

Trigger: A client (or test harness) that copies response/request headers wholesale from a previous batch call onto a new top-level batch request, including x-wm-batch: 1; a proxy that injects unknown x-* headers; manually curling the batch endpoint with -H 'x-wm-batch: 1'. Note: including a /api/batch/* path inside operations does NOT normally reach this throw — validateOperations marks that operation as a per-result error 'nested_batch' instead.

Common situations: SDK or fetch wrapper with default headers captured from an earlier sub-request; an integration test replaying recorded headers; an agent framework that forwards all inbound headers when proxying the API.

Related errors


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