{"record":{"id":"64f43cb107210a1b","repo":"koala73/worldmonitor","slug":"webhook-url-dns-resolution-failed-message","errorCode":null,"errorMessage":"Webhook URL DNS resolution failed: ${message}","messagePattern":"Webhook URL DNS resolution failed: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":400,"severity":"error","filePath":"api/_notification-webhook-ssrf.ts","lineNumber":232,"sourceCode":" * Fail fast at registration when the webhook hostname currently resolves to a\n * private or reserved address. Delivery repeats this check (and pins its\n * connection) because DNS can change after registration.\n */\nexport async function assertNotificationWebhookRegistrationUrlSafe(\n  rawUrl: string,\n  resolveHostname: ResolveHostname = defaultResolveHostname,\n): Promise<void> {\n  const staticError = blockedNotificationWebhookUrlReason(rawUrl);\n  if (staticError) throw new Error(staticError);\n\n  const hostname = new URL(rawUrl).hostname.toLowerCase();\n  if (isIpLiteral(hostname)) return;\n  let resolvedAddresses: string[];\n  try {\n    resolvedAddresses = await resolveHostname(hostname);\n  } catch (error) {\n    const message = error instanceof Error ? error.message : String(error);\n    throw new Error(`Webhook URL DNS resolution failed: ${message}`);\n  }\n  if (!resolvedAddresses.length) throw new Error('Webhook URL DNS resolution returned no addresses');\n  if (resolvedAddresses.some(isBlockedNotificationResolvedAddress)) {\n    throw new Error('Webhook URL must not point to a private/local address');\n  }\n}\n","sourceCodeStart":214,"sourceCodeEnd":239,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/api/_notification-webhook-ssrf.ts#L214-L239","documentation":"Thrown when the DNS-over-HTTPS resolution step (resolveHostname, which by default hits cloudflare-dns.com/dns-query for A and AAAA records) itself rejects — the DoH endpoint returned a non-200, a non-zero Status, or the fetch timed out/aborted within DNS_RESOLUTION_TIMEOUT_MS (3s). The underlying error message is interpolated into the thrown message so the caller can see why resolution failed.","triggerScenarios":"Cloudflare DoH endpoint unreachable from the Edge runtime (network blip, Cloudflare incident), the DoH lookup exceeding the 3s timeout, or the DNS query returning a non-zero Status code. With a custom resolveHostname injection (e.g. in tests), any rejection propagates the same way.","commonSituations":"Transient Cloudflare DoH outage; a hostname with a CNAME chain that the resolver times out on; misconfigured or rate-limited DoH; an injected test resolver that throws on a sentinel hostname.","solutions":["Retry the webhook registration after a short delay — DoH failures are usually transient.","Check Cloudflare status (cloudflarestatus.com) and the DoH endpoint reachability out-of-band.","If using a custom resolver, ensure it rejects with a descriptive Error message rather than throwing a non-Error.","Verify the hostname is well-formed and resolvable with a standard resolver (dig/host) before retrying."],"exampleFix":"// before\nregisterWebhook('https://good-host-with-doh-outage.example.com/hook')\n//   -> throws 'Webhook URL DNS resolution failed: DNS A lookup failed: HTTP 503'\n// after (retry after the transient DoH failure)\nawait retry(() => registerWebhook('https://good-host.example.com/hook'), { tries: 3 })","handlingStrategy":"retry","validationCode":"// No deterministic pre-check for DoH availability; probe with a short timeout.\nasync function dohReachable(): Promise<boolean> {\n  try {\n    const r = await fetch('https://cloudflare-dns.com/dns-query?name=cloudflare.com&type=A', {\n      headers: { Accept: 'application/dns-json' },\n      signal: AbortSignal.timeout(1500),\n    });\n    return r.ok;\n  } catch { return false; }\n}","typeGuard":null,"tryCatchPattern":"async function registerWithRetry(rawUrl: string, attempts = 3) {\n  for (let i = 0; i < attempts; i++) {\n    try {\n      await assertNotificationWebhookRegistrationUrlSafe(rawUrl);\n      return;\n    } catch (err) {\n      if (err.message.startsWith('Webhook URL DNS resolution failed:') && i < attempts - 1) {\n        await new Promise(r => setTimeout(r, 500 * (i + 1)));\n        continue;\n      }\n      throw err;\n    }\n  }\n}","preventionTips":["Treat DNS-resolution-failed as transient — retry with backoff before surfacing to the user.","Distinguish DoH-outage (retry) from invalid-hostname (do not retry) by parsing the wrapped message.","Monitor Cloudflare DoH status if webhook registration is a critical path."],"tags":["webhook","ssrf","dns","network","retryable"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}