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
- Validate with the same regex client-side before submit (and before spending a Turnstile token)
- Trim whitespace and set <input type="email" required>
- 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
- Validate email syntax before solving the captcha — tokens are single-use and wasted on doomed submits
- Trim input; use type="email" required
- Never populate the honeypot 'website' field in tests — it silently swallows the submission
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
- Invalid email address
- emailCheck.reason
- Name is required
- Company is required
- Valid phone number is required
AI-assisted analysis of koala73/worldmonitor@a96956387a (2026-08-21).
Data as JSON: /api/errors/9764814e65e9c4fb.
Report an issue: GitHub.