koala73/worldmonitor · error · ValidationError

Invalid email address

Error message

Invalid email address

What it means

Syntactic gate on the waitlist registration (register-interest.ts:324): the email must be truthy, at most MAX_EMAIL_LENGTH=320 chars, and match /^[^\s@]+@[^^\s@]+\.[^s@]+$/. It rejects anything without exactly one @, a dot in the domain part, or containing whitespace. This runs BEFORE the deeper validateEmail() content checks (error 311).

Source

Thrown at server/worldmonitor/leads/v1/register-interest.ts:325

      throw new ApiError(503, 'Rate-limit service temporarily unavailable', '');
    }
    if (!scoped.allowed) {
      throw new ApiError(429, 'Too many requests', '');
    }
  } else {
    const turnstileOk = await verifyTurnstile({
      token: req.turnstileToken || '',
      ip,
      logPrefix: '[register-interest]',
    });
    if (!turnstileOk) {
      throw new ApiError(403, 'Bot verification failed', '');
    }
  }

  const { email, source, appVersion, referredBy } = req;
  if (!email || email.length > MAX_EMAIL_LENGTH || !EMAIL_RE.test(email)) {
    throw new ValidationError([{ field: 'email', description: 'Invalid email address' }]);
  }

  const emailCheck = await validateEmail(email);
  if (!emailCheck.valid) {
    throw new ValidationError([{ field: 'email', description: emailCheck.reason }]);
  }

  const safeSource = source ? source.slice(0, MAX_META_LENGTH) : 'unknown';
  const safeAppVersion = appVersion ? appVersion.slice(0, MAX_META_LENGTH) : 'unknown';
  const safeReferredBy = referredBy ? referredBy.slice(0, 20) : undefined;

  const convexUrl = process.env.CONVEX_URL;
  if (!convexUrl) {
    throw new ApiError(503, 'Registration service unavailable', '');
  }

  const client = new ConvexHttpClient(convexUrl);
  const result = (await client.mutation(api.registerInterest.register, {

View on GitHub (pinned to a96956387a)

Solutions

  1. Trim input and run the same regex client-side before submit
  2. Use <input type="email"> plus a required check in the form
  3. Enforce the 320-char cap on the input's maxlength
  4. Only after this passes, expect deeper domain checks — fix syntax first

Example fix

// before
await registerInterest({ email: '  jane@doe' , ... }); // no dot + spaces -> ValidationError

// after
const email = rawEmail.trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || email.length > 320) throw new Error('fix email');
await registerInterest({ email, ... });
Defensive patterns

Strategy: validation

Validate before calling

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const email = rawInput.trim();
if (!email || email.length > 320 || !EMAIL_RE.test(email)) {
  showFieldError('email', 'Enter a valid email address');
  return;
}
await registerInterest({ ...req, email });

Type guard

function isSyntacticEmail(v: unknown): v is string {
  return typeof v === 'string' && v.length <= 320 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
}

Try / catch

try { await registerInterest(req); }
catch (e) {
  if (isValidationError(e) && e.violations?.some(v => v.field === 'email')) {
    showFieldError('email', e.violations.find(v => v.field === 'email')!.description);
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Submitting "user@localhost" (no dot in domain), "user @mail.com" (space), "user@@mail.com", a >320-char address, undefined/null email, or untrimmed input with trailing spaces.

Common situations: Free-text input without client validation; pasting emails that carry trailing whitespace or a semicolon from an address book; test payloads with placeholder strings like "test" or "a@b".

Related errors


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