mastra-ai/mastra · error

Push notification URL resolved to a local or private IP: ${h

Error message

Push notification URL resolved to a local or private IP: ${hostname}

What it means

To defeat DNS-rebinding, the sender resolves the hostname itself (DNS lookup with all addresses) and rejects it if any resolved address is local/private, throwing this error before pinning requestUrl.hostname to the first resolved address. The request is never made against an unvalidated IP.

Source

Thrown at packages/server/src/server/a2a/push-notification-sender.ts:104

    if (this.options.allowedHosts && !this.options.allowedHosts.includes(hostname)) {
      throw new Error(`Push notification host is not allowed: ${hostname}`);
    }

    if (isDisallowedHostname(hostname)) {
      throw new Error(`Push notification URL must not target local or internal hosts: ${hostname}`);
    }

    if (isDisallowedIpAddress(hostname)) {
      throw new Error(`Push notification URL must not target local or private IPs: ${hostname}`);
    }

    const resolvedAddresses =
      isIP(hostname) === 0
        ? await (this.options.lookup ?? defaultLookup)(hostname, { all: true, verbatim: true })
        : [{ address: hostname, family: isIP(hostname) }];

    if (resolvedAddresses.some(result => isDisallowedIpAddress(result.address))) {
      throw new Error(`Push notification URL resolved to a local or private IP: ${hostname}`);
    }

    const requestUrl = new URL(url.toString());
    requestUrl.hostname = resolvedAddresses[0]!.address;

    return {
      originalUrl: url,
      requestUrl,
      hostHeader: url.host,
      servername: isIP(hostname) === 0 ? hostname : undefined,
    };
  }

  private async postTaskSnapshot({
    requestUrl,
    hostHeader,
    servername,
    headers,

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Fix the DNS record so the hostname resolves to a public IP
  2. Bypass split-horizon DNS by using a publicly resolvable name for the callback
  3. Check resolution with `dig`/`nslookup` from the server host to see what addresses come back
  4. Provide a custom `options.lookup` resolver if your environment requires specific DNS behavior (must still return public IPs)

Example fix

// before
$ dig push.example.com -> 10.0.0.7 (internal A record)
// after
$ dig push.example.com -> 203.0.113.7 (public A record)
Defensive patterns

Strategy: validation

Validate before calling

import { lookup } from 'node:dns/promises';
async function assertResolvesPublic(raw: string) {
  const host = new URL(raw).hostname;
  const addrs = await lookup(host, { all: true, verbatim: true });
  if (addrs.length === 0) throw new Error(`No DNS records for ${host}`);
  // additionally assert none are private before registering the push config
}

Try / catch

try {
  await sender.sendNotifications(task, configs);
} catch (err) {
  if (err instanceof Error && err.message.startsWith('Push notification URL resolved to a local or private IP')) {
    logger.error('DNS rebinding/split-horizon suspected', { err: err.message });
  } else throw err;
}

Prevention

When it happens

Trigger: A hostname-backed push URL whose DNS currently resolves to a private/loopback/link-local address — e.g. a DNS record pointing at 127.0.0.1, a split-horizon DNS returning 10.x inside the cluster, or a tunneled domain resolving to a CGNAT/private range.

Common situations: DNS-rebinding attempts (attacker-controlled domain flipping to 169.254.169.254), corporate DNS resolving public names to internal IPs, split-horizon setups where the server sees internal answers, or recently changed DNS records pointing at private targets.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/6008509f84007017. Report an issue: GitHub.