koala73/worldmonitor · error · ConvexError

INVALID_COUNTRY

INVALID_COUNTRY

Error message

INVALID_COUNTRY

What it means

Thrown by `followCountry` after the auth gate passes but `isValidIso2(args.country)` returns false. The mutation takes a single `country` string and requires it to be a valid ISO 3166-1 alpha-2 code (e.g. "US", "JP", "DE"). Carries object data `{ kind: "INVALID_COUNTRY", country: args.country }` so the client can echo back the offending value. This throw is intentionally NOT a return-value (unlike the FREE_CAP branch) — invalid input is a client bug and is wanted in Sentry.

Source

Thrown at convex/followedCountries.ts:347

 *
 * Without Tier 1, two parallel first-ever mutations could both read
 * `meta=undefined`, both INSERT, and produce duplicate meta rows that
 * break the `.unique()` read AND re-open the cap-bypass window. With
 * Tier 1 in place, the brand-new-user race is closed deterministically.
 *
 * Errors are typed `ConvexError({kind, ...})` with object data so callers
 * can branch on `err.data.kind` (memory:
 * `convex-error-string-data-strips-errordata-on-wire`).
 */
export const followCountry = mutation({
  args: { country: v.string() },
  handler: async (ctx, args): Promise<FollowMutationResult> => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new ConvexError({ kind: "UNAUTHENTICATED" });
    const userId = identity.subject;

    if (!isValidIso2(args.country)) {
      throw new ConvexError({
        kind: "INVALID_COUNTRY",
        country: args.country,
      });
    }

    // Tier-1 lock: pre-seeded shard row. Read at top, patch at end.
    const shard = await readShardOrThrow(ctx, userId);

    // Tier-2 read: per-user denormalized count (lazy-created, but safe
    // under the shard lock above).
    const { meta, count: currentCount } = await readUserMeta(ctx, userId);

    const existingRow = await ctx.db
      .query("followedCountries")
      .withIndex("by_user_country", (q) =>
        q.eq("userId", userId).eq("country", args.country),
      )
      .first();

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Pass only validated ISO 3166-1 alpha-2 codes (uppercase, 2 letters) from a static registry, not from user input or display names.
  2. Client-side: gate the follow button on `isValidIso2(country)` using the same registry the server uses.
  3. On `err.data.kind === "INVALID_COUNTRY"`, log `err.data.country` to find which source emits bad codes.
  4. Normalize upstream ISO3 -> ISO2 before calling (e.g. "USA" -> "US").

Example fix

// before
await convex.mutation(api.followedCountries.followCountry, { country: countryName });

// after — send the validated ISO2 code
const ISO2 = new Set(["US","JP","DE","GB","FR",/* ... full registry */]);
const code = toIso2(countryName); // your normalizer
if (!ISO2.has(code)) { console.warn("bad country", countryName); return; }
await convex.mutation(api.followedCountries.followCountry, { country: code });
Defensive patterns

Strategy: validation

Validate before calling

const ISO2 = new Set(["US","JP","DE","GB","FR","CA","AU",/* ... full ISO 3166-1 alpha-2 registry */]);
function toValidIso2(code: string): string | null {
  const c = code.trim().toUpperCase();
  return ISO2.has(c) ? c : null;
}
const code = toValidIso2(input);
if (!code) { console.warn("invalid country", input); return; }

Type guard

function isValidIso2(code: string): boolean {
  return /^[A-Z]{2}$/.test(code) && ISO2_REGISTRY.has(code);
}

Try / catch

try {
  await convex.mutation(api.followedCountries.followCountry, { country });
} catch (err) {
  if (err.data?.kind === "INVALID_COUNTRY") console.warn("bad code", err.data.country);
  else throw err;
}

Prevention

When it happens

Trigger: Calling `followCountry` with a non-ISO2 string: a full country name ("United States"), an ISO3 code ("USA"), lowercase ("us"), an empty string, a numeric code, or a typo ("UX"). Also a stale panel config referencing a retired/renamed code.

Common situations: Frontend passes the country display label instead of the code; an upstream data source emits ISO3 and the conversion was skipped; a user-follow action built from a free-text field; a code list that drifted from the ISO registry (e.g. "XK" for Kosovo, which is not in ISO 3166-1).

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/7919d31698302884. Report an issue: GitHub.