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
- 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
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
- 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
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
- DNS resolution errors: ENOTFOUND and getaddrinfo failures — how hostname lookups fail and how to debug them.
Related errors
- DNS ${recordType} lookup failed: HTTP ${response.status}
- serverUrl DNS resolution failed
- callbackUrl DNS resolution failed: ${message}
- callbackUrl resolves to a private/reserved address
- relay returned ${resp.status}
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)