koala73/worldmonitor · error · WebhookDeliverySsrfError

callbackUrl DNS resolution returned no addresses

Error message

callbackUrl DNS resolution returned no addresses

What it means

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.

Solutions

  1. Verify the callbackUrl hostname resolves publicly (dig/nslookup the host) and fix the DNS records or the typo
  2. Use a fully qualified, publicly resolvable URL for the callback endpoint
  3. Re-register the webhook with a corrected callbackUrl after DNS is fixed and allow revalidation
  4. If behind a firewall, expose a public HTTPS endpoint (tunnel or reverse proxy) instead of an internal-only name

Example fix

// before
await client.registerWebhook({ callbackUrl: 'http://internal-box.local/hook' });
// after
const url = new URL('https://hooks.example.com/hook'); // publicly resolvable
await fetch(url, { method: 'HEAD' }); // sanity check reachability first
await client.registerWebhook({ callbackUrl: url.toString() });
Defensive patterns

Strategy: validation

Validate before calling

const host = new URL(callbackUrl).hostname;
const addrs = await dns.promises.lookup(host, { all: true });
if (addrs.length === 0) throw new Error(`callbackUrl host does not resolve: ${host}`);

Type guard

function hasResolvableHost(u: string): boolean {
  try { return new URL(u).hostname.length > 0; } catch { return false; }
}

Try / catch

try {
  await client.registerWebhook({ callbackUrl });
} catch (e) {
  if (e.name === 'WebhookDeliverySsrfError' && /DNS resolution/.test(e.message)) {
    // fix DNS or switch to a public callback host, then re-register
  }
  throw e;
}

Prevention

When it happens

Trigger: Registering a webhook whose callbackUrl hostname fails DNS resolution (NXDOMAIN, no A/AAAA records), or a resolver returning an empty answer at delivery time.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/b70f048d70ded0a6. Report an issue: GitHub.

Appendix: source

Thrown at server/worldmonitor/shipping/v2/deliver-webhook.ts:53

export interface WebhookDeliveryResult {
  status: number;
  ok: boolean;
  resolvedAddresses: string[];
}

const WEBHOOK_DELIVERY_TIMEOUT_MS = 10_000;
const MAX_WEBHOOK_RESPONSE_BYTES = 1024 * 1024;

async function postJsonWithPinnedAddress(
  url: URL,
  body: string,
  headers: Record<string, string>,
  resolvedAddresses: string[],
): Promise<Pick<Response, 'status' | 'ok'>> {
  const pinnedAddress = resolvedAddresses.find(address => address.includes('.')) ?? resolvedAddresses[0];
  if (!pinnedAddress) {
    throw new WebhookDeliverySsrfError('callbackUrl DNS resolution returned no addresses');
  }
  const family: 4 | 6 = pinnedAddress.includes(':') ? 6 : 4;

  return new Promise((resolve, reject) => {
    let settled = false;
    let response: IncomingMessage | undefined;
    let hardDeadline: ReturnType<typeof setTimeout> | undefined;
    const fail = (error: Error) => {
      if (settled) return;
      settled = true;
      clearTimeout(hardDeadline);
      req.destroy();
      response?.destroy();
      reject(error);
    };
    const req = https.request({
      hostname: url.hostname,
      port: url.port || 443,

View on GitHub (pinned to 7d06c8633d)