{"record":{"id":"7916bbba6bfcc065","repo":"koala73/worldmonitor","slug":"valid-phone-number-is-required","errorCode":null,"errorMessage":"Valid phone number is required","messagePattern":"Valid phone number is required","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"error","filePath":"server/worldmonitor/leads/v1/submit-contact.ts","lineNumber":168,"sourceCode":"  const { email, name, organization, phone, message, source } = req;\n\n  if (!email || !EMAIL_RE.test(email)) {\n    throw new ValidationError([{ field: 'email', description: 'Invalid email' }]);\n  }\n\n  const emailDomain = email.split('@')[1]?.toLowerCase();\n  if (emailDomain && FREE_EMAIL_DOMAINS.has(emailDomain)) {\n    throw new ApiError(422, 'Please use your work email address', '');\n  }\n\n  if (!name || name.trim().length === 0) {\n    throw new ValidationError([{ field: 'name', description: 'Name is required' }]);\n  }\n  if (!organization || organization.trim().length === 0) {\n    throw new ValidationError([{ field: 'organization', description: 'Company is required' }]);\n  }\n  if (!phone || !PHONE_RE.test(phone.trim())) {\n    throw new ValidationError([{ field: 'phone', description: 'Valid phone number is required' }]);\n  }\n\n  const safeName = name.slice(0, MAX_FIELD);\n  const safeOrg = organization.slice(0, MAX_FIELD);\n  const safePhone = phone.trim().slice(0, 30);\n  const safeMsg = message ? message.slice(0, MAX_MESSAGE) : undefined;\n  const safeSource = source ? source.slice(0, 100) : 'enterprise-contact';\n\n  const convexUrl = process.env.CONVEX_URL;\n  if (!convexUrl) {\n    throw new ApiError(503, 'Service unavailable', '');\n  }\n\n  const client = new ConvexHttpClient(convexUrl);\n  try {\n    await client.mutation(api.contactMessages.submit, {\n      name: safeName,\n      email: email.trim(),","sourceCodeStart":150,"sourceCodeEnd":186,"githubUrl":"https://github.com/koala73/worldmonitor/blob/a96956387a927b8cd7aa34b0c41fca357e746be9/server/worldmonitor/leads/v1/submit-contact.ts#L150-L186","documentation":"Phone format gate (submit-contact.ts:167): the trimmed value must match PHONE_RE = /^[+(]?\\d[\\d\\s()./-]{4,23}\\d$/. Optional leading '+' or '(', then a digit, then 4-23 characters of digits/space/parens/dots/slashes/dashes, ending with a digit — total length 6-25, letters anywhere fail. The input is trimmed but not normalized, so stored formatting is preserved.","triggerScenarios":"Extensions or labels containing letters (\"+1 555 123 4567 ext. 9\"); a string shorter than 6 chars (\"12345\"); trailing punctuation (\"555-1234-\"); '(+44) 20 7946 0000' — after '(' the next char must be a digit, so '+' fails; values longer than 25 chars; empty phone.","commonSituations":"International formats with '(' plus '+' combined; users pasting numbers with extension notes; UIs that capture phone as free text without a mask; edge cases like '00 44 20 7946 0000' which actually pass (leading 0 is a digit) while '(+...' does not.","solutions":["Strip non-dialable characters server- or client-side and keep 6-25 chars starting with optional '+'","Move extensions to a separate field","Validate with the exact same regex client-side before submit: /^[+(]?\\d[\\d\\s()./-]{4,23}\\d$/.test(phone.trim())","Prefer E.164-style input (+12125551234) which always matches"],"exampleFix":"// before\nawait submitContact({ phone: '(+44) 20 7946 0000', ... }); // '(' then '+' -> ValidationError\n\n// after\nawait submitContact({ phone: '+44 20 7946 0000', ... }); // matches PHONE_RE","handlingStrategy":"validation","validationCode":"const PHONE_RE = /^[+(]?\\d[\\d\\s()./-]{4,23}\\d$/;\nconst phone = rawPhone.trim();\nif (!phone || !PHONE_RE.test(phone)) {\n  showFieldError('phone', 'Enter a valid phone number (6-25 chars, digits and + ( ) . / - only, no extensions)');\n  return;\n}\nawait submitContact({ ...form, phone });","typeGuard":"function isContactPhone(v: unknown): v is string {\n  return typeof v === 'string' && /^[+(]?\\d[\\d\\s()./-]{4,23}\\d$/.test(v.trim());\n}","tryCatchPattern":"try { await submitContact(form); }\ncatch (e) {\n  if (isValidationError(e)) {\n    for (const v of e.violations ?? []) if (v.field === 'phone') showFieldError('phone', v.description);\n    return;\n  }\n  throw e;\n}","preventionTips":["Capture extensions in a separate field","Prefer E.164 (+12125551234), which always matches","Apply the exact server regex client-side before the captcha step"],"tags":["phone","validation","form-input","regex"],"backgroundTag":"phone-number-format-validation","analyzedSha":"a96956387a927b8cd7aa34b0c41fca357e746be9","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}