{"record":{"id":"823241416c190460","repo":"koala73/worldmonitor","slug":"batch-exceeds-company-monitoring-limits-maximpor","errorCode":null,"errorMessage":"batch exceeds ${COMPANY_MONITORING_LIMITS.maxImportRows} rows","messagePattern":"batch exceeds (.+?) rows","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"shared/company-monitoring-contract.ts","lineNumber":311,"sourceCode":"\n  const normalized: NormalizedCompanyImportRow = {\n    contractVersion: COMPANY_MONITORING_IMPORT_VERSION,\n    clientImportId,\n    ordinal: input.ordinal,\n    ...normalizeMonitoredCompanyInput(input),\n  };\n\n  if (utf8Bytes(JSON.stringify(normalized)) > COMPANY_MONITORING_LIMITS.maxImportRowBytes) {\n    throw new Error(`row exceeds ${COMPANY_MONITORING_LIMITS.maxImportRowBytes} bytes`);\n  }\n  return normalized;\n}\n\nexport function normalizeCompanyImportBatch(inputs: CompanyImportRowInput[]): NormalizedCompanyImportRow[] {\n  if (!Array.isArray(inputs)) throw new Error('import batch must be a list');\n  if (inputs.length === 0) throw new Error('import batch requires at least one row');\n  if (inputs.length > COMPANY_MONITORING_LIMITS.maxImportRows) {\n    throw new Error(`batch exceeds ${COMPANY_MONITORING_LIMITS.maxImportRows} rows`);\n  }\n\n  const normalized = inputs.map(normalizeCompanyImportRow).sort((left, right) => left.ordinal - right.ordinal);\n  const importId = normalized[0]?.clientImportId;\n  for (let index = 0; index < normalized.length; index += 1) {\n    const row = normalized[index]!;\n    if (row.clientImportId !== importId) throw new Error('batch rows must share one clientImportId');\n    if (row.ordinal !== index) throw new Error('batch ordinals must be contiguous from 0');\n  }\n  if (utf8Bytes(JSON.stringify(normalized)) > COMPANY_MONITORING_LIMITS.maxImportBatchBytes) {\n    throw new Error(`batch exceeds ${COMPANY_MONITORING_LIMITS.maxImportBatchBytes} bytes`);\n  }\n  return normalized;\n}\n\nexport function assertCompanyMonitoringPayloadSize(byteLength: number): void {\n  if (!Number.isSafeInteger(byteLength) || byteLength < 0) throw new Error('request byte length is invalid');\n  if (byteLength > COMPANY_MONITORING_LIMITS.maxRequestBytes) {","sourceCodeStart":293,"sourceCodeEnd":329,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/shared/company-monitoring-contract.ts#L293-L329","documentation":"Thrown by normalizeCompanyImportBatch() in shared/company-monitoring-contract.ts when the import batch array has more rows than COMPANY_MONITORING_LIMITS.maxImportRows (100). This is the first batch-level guard: the company-monitoring import API is designed around small, atomic batches of at most 100 rows per request. Note the same limit is also enforced per-row on the ordinal field (line 290), so ordinals 0..99 are the valid range.","triggerScenarios":"Calling normalizeCompanyImportBatch() (or the import RPC that wraps it) with a CompanyImportRowInput[] of 101 or more rows. Typical producers: a CSV/spreadsheet import where the user selected 150 companies and the client sends them in one call, or test fixtures that generate maxImportRows + 1 rows to probe the boundary.","commonSituations":"Bulk CSV import UIs without client-side chunking; merging several prepared batches into a single request before sending; a test fixture copy-pasted and doubled; raising maxImportRows in the shared contract without updating the client that still chunks at the old value (or vice versa).","solutions":["Chunk the rows client-side into batches of at most COMPANY_MONITORING_LIMITS.maxImportRows (100) and submit them as separate import calls, each with its own contiguous 0-based ordinals","Pre-validate before sending: if (rows.length > COMPANY_MONITORING_LIMITS.maxImportRows) split or reject with a UI message","If bigger batches are a real product requirement, raise maxImportRows in shared/company-monitoring-contract.ts and ship client and server together, remembering ordinals must still be < the new limit"],"exampleFix":"// before\nconst normalized = normalizeCompanyImportBatch(allRows); // throws if allRows.length > 100\n\n// after\nconst CHUNK = COMPANY_MONITORING_LIMITS.maxImportRows;\nfor (let i = 0; i < allRows.length; i += CHUNK) {\n  const slice = allRows.slice(i, i + CHUNK).map((row, j) => ({ ...row, ordinal: j }));\n  await submitImportBatch(slice); // each call: <=100 rows, ordinals 0..slice.length-1\n}","handlingStrategy":"validation","validationCode":"const LIMITS = COMPANY_MONITORING_LIMITS; // maxImportRows = 100\nfunction splitImportBatch(rows: CompanyImportRowInput[]): CompanyImportRowInput[][] {\n  const chunks: CompanyImportRowInput[][] = [];\n  for (let i = 0; i < rows.length; i += LIMITS.maxImportRows) {\n    chunks.push(rows.slice(i, i + LIMITS.maxImportRows));\n  }\n  return chunks;\n}\n// before every submit:\nif (rows.length > LIMITS.maxImportRows) throw new RangeError(`split first: ${rows.length} > ${LIMITS.maxImportRows}`);","typeGuard":"function isImportBatchSizeOk(rows: CompanyImportRowInput[]): boolean {\n  return Array.isArray(rows) && rows.length > 0 && rows.length <= COMPANY_MONITORING_LIMITS.maxImportRows;\n}","tryCatchPattern":"catch (e) { if (e instanceof Error && e.message.endsWith(' rows')) showBatchSplitPrompt(); else throw e; }","preventionTips":["Build chunking into the import UI so users never construct >100-row requests","Import COMPANY_MONITORING_LIMITS from the shared contract instead of hard-coding 100","Add a unit test at the boundary: exactly 100 rows passes, 101 throws"],"tags":["validation","import","batch-limits","company-monitoring"],"backgroundTag":"request-payload-too-large","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}