koala73/worldmonitor · error · Error

batch rows must share one clientImportId

Error message

batch rows must share one clientImportId

What it means

Thrown by normalizeCompanyImportBatch() when, after per-row normalization and sorting, any row carries a clientImportId different from the first row's. The contract requires every row in one batch to share a single clientImportId because that ID identifies the whole import session (idempotency and attribution unit), not individual rows.

Solutions

  1. Generate one clientImportId (e.g. crypto.randomUUID()) once per batch and stamp it onto every row before calling normalizeCompanyImportBatch()
  2. Pre-validate: const id = rows[0]?.clientImportId; assert rows.every(r => r.clientImportId === id)
  3. When retrying or resuming an import, keep rows from different sessions in separate calls rather than concatenating

Example fix

// before
const rows = inputs.map((input, i) => ({ ...input, ordinal: i, clientImportId: crypto.randomUUID() }));
const normalized = normalizeCompanyImportBatch(rows); // every row has a different ID -> throws

// after
const clientImportId = crypto.randomUUID(); // one ID for the whole batch
const rows = inputs.map((input, i) => ({ ...input, ordinal: i, clientImportId }));
const normalized = normalizeCompanyImportBatch(rows);
Defensive patterns

Strategy: validation

Validate before calling

const clientImportId = crypto.randomUUID();
const rows = rawRows.map((r, i) => ({ ...r, ordinal: i, clientImportId }));
const shared = rows.length === 0 || rows.every(r => r.clientImportId === rows[0]!.clientImportId);
if (!shared) throw new Error('rows carry mixed clientImportId — regenerate before submit');

Type guard

function hasSingleImportId(rows: { clientImportId: string }[]): boolean {
  const first = rows[0]?.clientImportId;
  return first !== undefined && rows.every(r => r.clientImportId === first);
}

Try / catch

catch (e) { if (e instanceof Error && e.message === 'batch rows must share one clientImportId') { rebuildBatchWithOneId(); } else throw e; }

Prevention

When it happens

Trigger: Concatenating rows prepared for two different import sessions into one call; generating a fresh clientImportId per row instead of once per batch; retry logic that mixes leftover rows from a previous partial import with new rows.

Common situations: Client code that creates the ID inside the per-row mapping function instead of once before the loop; merging 'retry the failed rows' with 'continue with new rows' in one request; a CSV uploader that restarts ID generation mid-file after an error.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at shared/company-monitoring-contract.ts:318

  if (utf8Bytes(JSON.stringify(normalized)) > COMPANY_MONITORING_LIMITS.maxImportRowBytes) {
    throw new Error(`row exceeds ${COMPANY_MONITORING_LIMITS.maxImportRowBytes} bytes`);
  }
  return normalized;
}

export function normalizeCompanyImportBatch(inputs: CompanyImportRowInput[]): NormalizedCompanyImportRow[] {
  if (!Array.isArray(inputs)) throw new Error('import batch must be a list');
  if (inputs.length === 0) throw new Error('import batch requires at least one row');
  if (inputs.length > COMPANY_MONITORING_LIMITS.maxImportRows) {
    throw new Error(`batch exceeds ${COMPANY_MONITORING_LIMITS.maxImportRows} rows`);
  }

  const normalized = inputs.map(normalizeCompanyImportRow).sort((left, right) => left.ordinal - right.ordinal);
  const importId = normalized[0]?.clientImportId;
  for (let index = 0; index < normalized.length; index += 1) {
    const row = normalized[index]!;
    if (row.clientImportId !== importId) throw new Error('batch rows must share one clientImportId');
    if (row.ordinal !== index) throw new Error('batch ordinals must be contiguous from 0');
  }
  if (utf8Bytes(JSON.stringify(normalized)) > COMPANY_MONITORING_LIMITS.maxImportBatchBytes) {
    throw new Error(`batch exceeds ${COMPANY_MONITORING_LIMITS.maxImportBatchBytes} bytes`);
  }
  return normalized;
}

export function assertCompanyMonitoringPayloadSize(byteLength: number): void {
  if (!Number.isSafeInteger(byteLength) || byteLength < 0) throw new Error('request byte length is invalid');
  if (byteLength > COMPANY_MONITORING_LIMITS.maxRequestBytes) {
    throw new Error(`request exceeds ${COMPANY_MONITORING_LIMITS.maxRequestBytes} bytes`);
  }
}

export function assertCompanyMonitoringAccountContext(
  context: { ownerAccountId?: string } | null | undefined,
): string {

View on GitHub (pinned to eeab0a219f)