koala73/worldmonitor · error · ValidationError

Invalid email

Error message

Invalid email

What it means

Contact-form email gate (submit-contact.ts:152): the email must be present and match /^[^\s@]+@[^\s@]+\.[^\s@]+$/ or a ValidationError on field 'email' is thrown. Unlike the waitlist endpoint there is no 320-char cap here, and this fires after Turnstile — so a 403 'Bot verification failed' will mask an invalid email until the captcha passes.

Source

Thrown at server/worldmonitor/leads/v1/submit-contact.ts:153

  }

  const ip = getClientIp(ctx.request);
  const country = ctx.request.headers.get('cf-ipcountry')
    || ctx.request.headers.get('x-vercel-ip-country');

  const turnstileOk = await verifyTurnstile({
    token: req.turnstileToken || '',
    ip,
    logPrefix: '[contact]',
  });
  if (!turnstileOk) {
    throw new ApiError(403, 'Bot verification failed', '');
  }

  const { email, name, organization, phone, message, source } = req;

  if (!email || !EMAIL_RE.test(email)) {
    throw new ValidationError([{ field: 'email', description: 'Invalid email' }]);
  }

  const emailDomain = email.split('@')[1]?.toLowerCase();
  if (emailDomain && FREE_EMAIL_DOMAINS.has(emailDomain)) {
    throw new ApiError(422, 'Please use your work email address', '');
  }

  if (!name || name.trim().length === 0) {
    throw new ValidationError([{ field: 'name', description: 'Name is required' }]);
  }
  if (!organization || organization.trim().length === 0) {
    throw new ValidationError([{ field: 'organization', description: 'Company is required' }]);
  }
  if (!phone || !PHONE_RE.test(phone.trim())) {
    throw new ValidationError([{ field: 'phone', description: 'Valid phone number is required' }]);
  }

  const safeName = name.slice(0, MAX_FIELD);

View on GitHub (pinned to a96956387a)

Solutions

  1. Validate with the same regex client-side before submit (and before spending a Turnstile token)
  2. Trim whitespace and set <input type="email" required>
  3. Order client checks: email syntax first, then captcha, so users never burn single-use tokens on doomed submits

Example fix

// before
const token = await solveCaptcha();
await submitContact({ ...form, email: 'not-an-email', turnstileToken: token }); // burns token, then 4xx

// after
const email = form.email.trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) throw new Error('invalid email');
const token = await solveCaptcha();
await submitContact({ ...form, email, turnstileToken: token });
Defensive patterns

Strategy: validation

Validate before calling

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const email = raw.trim();
if (!EMAIL_RE.test(email)) { showFieldError('email', 'Invalid email'); return; }
const turnstileToken = await solveCaptcha(); // spend the token only after syntax passes
await submitContact({ ...form, email, website: '', turnstileToken });

Type guard

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

Try / catch

try { await submitContact(form); }
catch (e) {
  if (isValidationError(e) && e.violations?.some(v => v.field === 'email')) {
    showFieldError('email', 'Invalid email'); return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Submitting "user@localhost", whitespace-containing emails, double-@ addresses, or an empty string; also any submit where the honeypot 'website' field is empty but the widget token was valid and the email was never validated client-side.

Common situations: Forms relying only on HTML validation that browsers skip (e.g. custom submit via JS with novalidate); pasting from spreadsheets that appends trailing delimiters; integration tests with placeholder emails.

Related errors


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