{"record":{"id":"c95da7df795acbad","repo":"koala73/worldmonitor","slug":"too-many-recent-submissions-for-this-email-try-again-later","errorCode":null,"errorMessage":"Too many recent submissions for this email; try again later.","messagePattern":"Too many recent submissions for this email; try again later\\.","errorType":"http","errorClass":"ApiError","httpStatus":429,"severity":"warning","filePath":"server/worldmonitor/leads/v1/submit-contact.ts","lineNumber":180,"sourceCode":"        'Content-Type': 'application/json',\n        'User-Agent': 'worldmonitor-leads/1.0',\n        'x-convex-shared-secret': secret,\n      },\n      body: JSON.stringify({\n        name: safeName,\n        email: email.trim(),\n        organization: safeOrg,\n        phone: safePhone,\n        message: safeMsg,\n        source: safeSource,\n      }),\n      signal: AbortSignal.timeout(10_000),\n    });\n  } catch {\n    throw new ApiError(503, 'Service unavailable', '');\n  }\n  if (response.status === 429) {\n    throw new ApiError(429, 'Too many recent submissions for this email; try again later.', '');\n  }\n  if (response.status === 422) {\n    throw new ApiError(422, 'Please use a corporate email address.', '');\n  }\n  if (!response.ok) {\n    throw new ApiError(503, 'Service unavailable', '');\n  }\n  const result: unknown = await response.json().catch(() => null);\n  if (!result || typeof result !== 'object' || !('status' in result) || result.status !== 'sent') {\n    throw new ApiError(503, 'Service unavailable', '');\n  }\n\n  const emailSent = await sendNotificationEmail(\n    safeName,\n    email.trim(),\n    safeOrg,\n    safePhone,\n    safeMsg,","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/server/worldmonitor/leads/v1/submit-contact.ts#L162-L198","documentation":"The Convex backend answered 429, meaning this email has submitted contact requests too recently. The handler surfaces it as an explicit 429 ApiError with a retry-later message — this is a deliberate rate-limit signal, not an outage.","triggerScenarios":"Posting /submit-contact repeatedly with the same email within the backend's rate-limit window, causing the Convex action to respond with HTTP 429.","commonSituations":"A user double-clicking the submit button, automated scripts or retries hammering the endpoint, a shared corporate mail gateway reusing one address, or integration tests reusing a fixture email.","solutions":["Wait for the rate-limit window to elapse and retry with the same email","Back off client-side: disable submit after first click and honor the 429 with a countdown UI","Use a different email address if the request is legitimately for another contact","Raise or tune the rate-limit window in the Convex submitContact action if it is too strict for legitimate traffic"],"exampleFix":"// before\nif (response.status === 429) {\n  throw new ApiError(429, 'Too many recent submissions for this email; try again later.', '');\n}\n// after\nif (response.status === 429) {\n  const retryAfter = response.headers.get('retry-after');\n  throw new ApiError(429, 'Too many recent submissions for this email; try again later.', '', retryAfter ? { 'retry-after': retryAfter } : undefined);\n}","handlingStrategy":"retry","validationCode":"const lastSubmitAt = Number(localStorage.getItem('contact:lastSubmitAt') ?? 0);\nif (Date.now() - lastSubmitAt < RATE_WINDOW_MS) show('Please wait before submitting again.');","typeGuard":"null","tryCatchPattern":"try {\n  await submitContact(form);\n} catch (e) {\n  if (e instanceof ApiError && e.status === 429) {\n    showCountdownAndRetryLater(e.message);\n  } else throw e;\n}","preventionTips":["Disable the submit button after the first click to avoid duplicates","Honor Retry-After / show a countdown UI on 429","Never reuse one fixture email in automated tests; vary addresses"],"tags":["rate-limit","http-429","upstream"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}