koala73/worldmonitor · error · ValidationError

duplicate id "${id}" — results would be ambiguous

Error message

duplicate id "${id}" — results would be ambiguous

What it means

validateOperations tracks every operation id in a Set; a second operation with the same id (after trimming, and counting index-defaulted ids) is a FieldViolation 'duplicate id "<id>" — results would be ambiguous' and the entire batch is rejected with ValidationError. Duplicate ids would make it impossible to correlate a result row with its request, so the server refuses rather than guess.

Source

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

    // 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 };
  };
}

export const executeBatch = createExecuteBatch();

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Deduplicate ids (or entire operations) before submitting
  2. Derive ids from the loop index or a counter when generating operations programmatically
  3. If two identical calls are genuinely needed, give them distinct ids like 'flights-a' and 'flights-b'

Example fix

// before — template collision when the same route appears twice
ops.map((p) => ({ id: `flights-${p.origin}-${p.destination}`, path: p.path }));

// after — index guarantees uniqueness
ops.map((p, i) => ({ id: `flights-${i}`, path: p.path }));
Defensive patterns

Strategy: validation

Validate before calling

function dedupeOperationIds<T extends { id?: string; path: string }>(ops: T[]): T[] {
  const seen = new Set<string>();
  return ops.map((op, i) => {
    const base = (op.id ?? String(i)).trim();
    let id = base;
    let n = 2;
    while (seen.has(id)) id = `${base.slice(0, 60)}-${n++}`;
    seen.add(id);
    return { ...op, id };
  });
}

Type guard

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

Try / catch

try {
  await post('/api/batch/v1/execute', { operations });
} catch (e) {
  if (e instanceof HttpError && e.status === 400 && isDuplicateIdViolation(e.body)) {
    // dedupe ids (see validationCode) and resend — paths themselves are fine
  }
}

Prevention

When it happens

Trigger: Copy-pasted operation objects without updating id; template-generated ids that collide (e.g. same origin/destination pair in two ops both ided 'flights'); ids that differ only by surrounding whitespace ('a' vs ' a ') which trim to the same value.

Common situations: Deduplicating operations by path but not by id; building ids from a domain key that repeats across categories; a loop reusing a constant id string.

Related errors


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