{"record":{"id":"12a3b8364789a5ea","repo":"koala73/worldmonitor","slug":"input-too-large","errorCode":"INPUT_TOO_LARGE","errorMessage":"INPUT_TOO_LARGE","messagePattern":"INPUT_TOO_LARGE","errorType":"exception","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/followedCountries.ts","lineNumber":502,"sourceCode":" * downgrade is a separate concern handled by NOT auto-deleting on\n * downgrade; merge is the FIRST sign-in and has no PRO history to\n * grandfather.)\n */\nexport const mergeAnonymousLocal = mutation({\n  args: { countries: v.array(v.string()) },\n  handler: async (ctx, args): Promise<MergeAnonymousLocalResult> => {\n    const identity = await ctx.auth.getUserIdentity();\n    if (!identity) throw new ConvexError({ kind: \"UNAUTHENTICATED\" });\n    const userId = identity.subject;\n\n    // Step 2: empty-input guard.\n    if (args.countries.length === 0) {\n      throw new ConvexError({ kind: \"EMPTY_INPUT\" });\n    }\n\n    // Step 3: defensive upper-bound on input length.\n    if (args.countries.length > MAX_MERGE_INPUT) {\n      throw new ConvexError({\n        kind: \"INPUT_TOO_LARGE\",\n        max: MAX_MERGE_INPUT,\n        received: args.countries.length,\n      });\n    }\n\n    // Step 4: ISO-2 registry filter; collect droppedInvalid in input order.\n    const droppedInvalid: string[] = [];\n    const validInputs: string[] = [];\n    for (const code of args.countries) {\n      if (isValidIso2(code)) {\n        validInputs.push(code);\n      } else {\n        droppedInvalid.push(code);\n      }\n    }\n\n    // Step 5: canonicalize — dedupe in first-seen order. Without this, a","sourceCodeStart":484,"sourceCodeEnd":520,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/followedCountries.ts#L484-L520","documentation":"Thrown by `mergeAnonymousLocal` (Step 3) when `args.countries.length > MAX_MERGE_INPUT`. This is a defensive upper-bound to prevent an enormous payload from a tampered client or a corrupted local-store from doing unbounded work in the ISO2-filter loop and subsequent per-country inserts. Carries `{ kind: \"INPUT_TOO_LARGE\", max: MAX_MERGE_INPUT, received }`. Distinct from INVALID_COUNTRY (per-code) and EMPTY_INPUT.","triggerScenarios":"Calling the mutation with an array larger than `MAX_MERGE_INPUT`; a corrupted/anonymized local-store holding thousands of entries; a client that accidentally passes a paginated/global country list instead of the user's follows.","commonSituations":"A bug where the entire ISO country registry (~250 codes) is passed instead of the user's selections; a test fixture with a huge array; a tampered client attempting to flood the merge.","solutions":["Client-side: cap the local follows array before calling; `countries.slice(0, MAX_MERGE_INPUT)`.","On `err.data.kind === \"INPUT_TOO_LARGE\"`, truncate and retry, or investigate why local state exceeds the bound (likely a bug).","If the legitimate use case exceeds the cap, raise `MAX_MERGE_INPUT` in the mutation — do not silently drop."],"exampleFix":"// before\nawait convex.mutation(api.followedCountries.mergeAnonymousLocal, { countries: allFollows });\n\n// after\nconst MAX = 100; // keep in sync with server MAX_MERGE_INPUT\nconst capped = allFollows.slice(0, MAX);\nif (allFollows.length > MAX) console.warn(\"truncating merge input\", allFollows.length);\nawait convex.mutation(api.followedCountries.mergeAnonymousLocal, { countries: capped });","handlingStrategy":"validation","validationCode":"const MAX_MERGE_INPUT = 100; // keep in sync with server\nconst capped = localFollows.slice(0, MAX_MERGE_INPUT);\nif (localFollows.length > MAX_MERGE_INPUT) {\n  console.warn(\"merge input exceeds cap; truncating\", localFollows.length);\n}\nif (capped.length === 0) return;","typeGuard":"function withinMergeCap(countries: string[]): boolean {\n  return countries.length > 0 && countries.length <= MAX_MERGE_INPUT;\n}","tryCatchPattern":"try {\n  await convex.mutation(api.followedCountries.mergeAnonymousLocal, { countries: capped });\n} catch (err) {\n  if (err.data?.kind === \"INPUT_TOO_LARGE\") {\n    await convex.mutation(api.followedCountries.mergeAnonymousLocal, { countries: localFollows.slice(0, err.data.max) });\n  } else throw err;\n}","preventionTips":["Cap the local follows array client-side to MAX_MERGE_INPUT.","Investigate if local state legitimately exceeds the cap (likely a bug).","Read err.data.max from the error for the authoritative bound.","Don't silently drop — decide explicitly whether to truncate or raise the cap."],"tags":["validation","convex","merge","input-guard"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}