{"record":{"id":"b70f048d70ded0a6","repo":"koala73/worldmonitor","slug":"callbackurl-dns-resolution-returned-no-addresses-deliver","errorCode":null,"errorMessage":"callbackUrl DNS resolution returned no addresses","messagePattern":"callbackUrl DNS resolution returned no addresses","errorType":"exception","errorClass":"WebhookDeliverySsrfError","httpStatus":null,"severity":"error","filePath":"server/worldmonitor/shipping/v2/deliver-webhook.ts","lineNumber":53,"sourceCode":"\nexport interface WebhookDeliveryResult {\n  status: number;\n  ok: boolean;\n  resolvedAddresses: string[];\n}\n\nconst WEBHOOK_DELIVERY_TIMEOUT_MS = 10_000;\nconst MAX_WEBHOOK_RESPONSE_BYTES = 1024 * 1024;\n\nasync function postJsonWithPinnedAddress(\n  url: URL,\n  body: string,\n  headers: Record<string, string>,\n  resolvedAddresses: string[],\n): Promise<Pick<Response, 'status' | 'ok'>> {\n  const pinnedAddress = resolvedAddresses.find(address => address.includes('.')) ?? resolvedAddresses[0];\n  if (!pinnedAddress) {\n    throw new WebhookDeliverySsrfError('callbackUrl DNS resolution returned no addresses');\n  }\n  const family: 4 | 6 = pinnedAddress.includes(':') ? 6 : 4;\n\n  return new Promise((resolve, reject) => {\n    let settled = false;\n    let response: IncomingMessage | undefined;\n    let hardDeadline: ReturnType<typeof setTimeout> | undefined;\n    const fail = (error: Error) => {\n      if (settled) return;\n      settled = true;\n      clearTimeout(hardDeadline);\n      req.destroy();\n      response?.destroy();\n      reject(error);\n    };\n    const req = https.request({\n      hostname: url.hostname,\n      port: url.port || 443,","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/server/worldmonitor/shipping/v2/deliver-webhook.ts#L35-L71","documentation":"Before delivering a webhook, the server resolves the callbackUrl's DNS and pins the connection to one of the returned addresses (SSRF protection). postJsonWithPinnedAddress throws WebhookDeliverySsrfError('callbackUrl DNS resolution returned no addresses') when the resolution yields an empty address list, so delivery is aborted rather than falling back to a hostname-based connection.","triggerScenarios":"Registering a webhook whose callbackUrl hostname fails DNS resolution (NXDOMAIN, no A/AAAA records), or a resolver returning an empty answer at delivery time.","commonSituations":"Typo in the callback hostname; internal service hostname only resolvable inside another network; DNS records removed after registration; IPv6-only/IPv4-only mismatches producing an empty resolved set; test URLs like 'http://localhost:9999' where the resolver returns nothing in the runtime environment.","solutions":["Verify the callbackUrl hostname resolves publicly (dig/nslookup the host) and fix the DNS records or the typo","Use a fully qualified, publicly resolvable URL for the callback endpoint","Re-register the webhook with a corrected callbackUrl after DNS is fixed and allow revalidation","If behind a firewall, expose a public HTTPS endpoint (tunnel or reverse proxy) instead of an internal-only name"],"exampleFix":"// before\nawait client.registerWebhook({ callbackUrl: 'http://internal-box.local/hook' });\n// after\nconst url = new URL('https://hooks.example.com/hook'); // publicly resolvable\nawait fetch(url, { method: 'HEAD' }); // sanity check reachability first\nawait client.registerWebhook({ callbackUrl: url.toString() });","handlingStrategy":"validation","validationCode":"const host = new URL(callbackUrl).hostname;\nconst addrs = await dns.promises.lookup(host, { all: true });\nif (addrs.length === 0) throw new Error(`callbackUrl host does not resolve: ${host}`);","typeGuard":"function hasResolvableHost(u: string): boolean {\n  try { return new URL(u).hostname.length > 0; } catch { return false; }\n}","tryCatchPattern":"try {\n  await client.registerWebhook({ callbackUrl });\n} catch (e) {\n  if (e.name === 'WebhookDeliverySsrfError' && /DNS resolution/.test(e.message)) {\n    // fix DNS or switch to a public callback host, then re-register\n  }\n  throw e;\n}","preventionTips":["Pre-register webhooks only with publicly resolvable HTTPS hosts","Run a HEAD/GET self-check against the callback URL before registering","Monitor DNS TTL/expiry for callback domains"],"tags":["network","dns","webhook","ssrf"],"backgroundTag":"dns-resolution-failed","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"}