koala73/worldmonitor · error · ConvexError
Valid email is required
Error message
Valid email is required
What it means
Thrown by the `submit` contact-message Convex mutation after `clip()` strips C0 controls and trims the `email` field. Fires when the cleaned email is falsy (empty/whitespace-only) OR fails the basic shape regex `/^[^\s@]+@[^\s@]+\.[^\s@]+$/` (local-part@domain.tld). This is the first of three sequential email gates (shape -> corporate-domain -> throttle) and rejects obviously-bogus input before it can reach the table or downstream LLM cost. It is a plain-string ConvexError, so on the client `err.data === "Valid email is required"`.
Source
Thrown at convex/contactMessages.ts:69
organization: v.optional(v.string()),
phone: v.optional(v.string()),
message: v.optional(v.string()),
source: v.string(),
},
handler: async (ctx, args) => {
// Length / shape validation. Reject obviously-bogus input before
// it reaches the table — also a defence against prompt-injection
// payloads enormous enough to trip downstream LLM cost.
const name = clip(args.name, MAX_NAME);
const email = clip(args.email, MAX_EMAIL);
const organization = clip(args.organization, MAX_ORG);
const phone = clip(args.phone, MAX_PHONE);
const message = clip(args.message, MAX_MESSAGE, { preserveNewlines: true });
const source = clip(args.source, MAX_SOURCE) ?? "unknown";
if (!name) throw new ConvexError("Name is required");
if (!email || !EMAIL_RE.test(email)) {
throw new ConvexError("Valid email is required");
}
if (!isCorporateDomain(email)) {
throw new ConvexError({
kind: "FREE_EMAIL_NOT_ALLOWED",
message: "Please use a corporate email address.",
});
}
const normalizedEmail = email.toLowerCase();
// Throttle: cap recent submissions per email. Index lookup keeps this O(matches),
// which the limit caps at PER_EMAIL_LIMIT + 1.
const windowStart = Date.now() - PER_EMAIL_WINDOW_MS;
const recent = await ctx.db
.query("contactMessages")
.withIndex("by_normalized_email_received", (q) =>
q.eq("normalizedEmail", normalizedEmail).gte("receivedAt", windowStart),
)View on GitHub (pinned to ffec79ac33)
Solutions
- Validate email shape client-side with the same regex before calling the mutation: `/^[^\s@]+@[^\s@]+\.[^\s@]+$/`.
- Ensure the form input has `required` and `type="email"` and disable submit until non-empty.
- If calling Convex directly (bypassing the edge handler), replicate `clip()` (strip `[\x00-\x1F\x7F]`, trim) before validation so control-char-only payloads are caught early.
- Branch on `err.data === "Valid email is required"` in the catch to show a field-level message rather than a generic toast.
Example fix
// before
await convex.mutation(api.contactMessages.submit, { name, email: emailInput.value, source });
// after
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const cleaned = emailInput.value.replace(/[\x00-\x1F\x7F]/g, "").trim();
if (!EMAIL_RE.test(cleaned)) {
setFieldError("email", "Enter a valid email address.");
return;
}
await convex.mutation(api.contactMessages.submit, { name, email: cleaned, source }); Defensive patterns
Strategy: validation
Validate before calling
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function cleanEmail(raw: string): string | null {
const cleaned = raw.replace(/[\x00-\x1F\x7F]/g, "").trim();
return EMAIL_RE.test(cleaned) ? cleaned : null;
}
// before calling the mutation:
const email = cleanEmail(input);
if (!email) { showFieldError("Enter a valid email."); return; } Type guard
function isValidContactEmail(raw: unknown): raw is string {
return typeof raw === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw.replace(/[\x00-\x1F\x7F]/g, "").trim());
} Try / catch
try {
await convex.mutation(api.contactMessages.submit, { name, email, source });
} catch (err) {
if (err.data === "Valid email is required") setFieldError("email", "Enter a valid email.");
else throw err;
} Prevention
- Add `required type=email` and the shape regex on the input client-side.
- Strip C0 controls before validation to match the server's `clip()`.
- Disable submit until the email field passes the regex.
- Branch on the exact string `err.data` for field-level messaging.
When it happens
Trigger: Calling `api.contactMessages.submit` with `email: ""`, `email: " "` (trimmed to empty), `email: "a"` (no @), `email: "a@b"` (no dot in domain), `email: "a@b." `, or a value composed entirely of stripped control chars (e.g. `"\x00\x01"`). Also fires if the field is omitted despite the `v.string()` schema (Convex rejects undefined at the schema layer with a different error, but an empty string reaches this check).
Common situations: Contact form submitted before the user typed an email; client-side validation disabled or bypassed; an automated/probing client POSTing raw args directly to the Convex mutation to skip the edge handler (`server/worldmonitor/leads/v1/submit-contact.ts`) which enforces the same bound; copy-paste introducing a leading/trailing space that the regex tolerates but a control char that `clip` strips to empty does not.
Related errors
- email required for email channel
- OWNER_EMAIL_UNAVAILABLE
- OWNER_DOMAIN_NOT_CORPORATE
- INVITEE_DOMAIN_NOT_CORPORATE
- INVITEE_DOMAIN_MISMATCH
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/6fd434d7a9a3622c.
Report an issue: GitHub.