mastra-ai/mastra · error

Push notification URL must not target local or internal host

Error message

Push notification URL must not target local or internal hosts: ${hostname}

What it means

Even without an allowlist, the sender hard-blocks push notification URLs whose hostname is a well-known local/internal name (e.g. localhost, *.local, metadata endpoints, *.internal) via isDisallowedHostname. This prevents SSRF into loopback or cloud-internal services.

Source

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

  getStore() {
    return this.pushNotificationStore;
  }

  private async resolveValidatedDestination(rawUrl: string) {
    const url = new URL(rawUrl);

    if (url.protocol !== 'https:' && url.protocol !== 'http:') {
      throw new Error(`Push notification URL must use http or https: ${url.protocol}`);
    }

    const hostname = url.hostname.toLowerCase();
    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;

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Use a publicly resolvable hostname for the push notification endpoint
  2. For local development, expose the endpoint via a tunnel (ngrok/smee) or a machine's LAN IP if isDisallowedIpAddress permits it
  3. If this is a legitimate internal host, host the receiver externally or relax the sender's hostname blocklist via supported options

Example fix

// before
url: 'http://localhost:4111/a2a/push'
// after
url: 'https://dev-tunnel.example.com/a2a/push'
Defensive patterns

Strategy: validation

Validate before calling

const LOCAL_HOST_RE = /(^|\.)(local|internal|localhost)$/i;
function assertPublicHost(raw: string) {
  const host = new URL(raw).hostname.toLowerCase();
  if (host === 'localhost' || LOCAL_HOST_RE.test(host)) {
    throw new Error(`Host ${host} is local/internal`);
  }
}

Type guard

const isPublicHostname = (raw: string): boolean => {
  const h = new URL(raw).hostname.toLowerCase();
  return h !== 'localhost' && !/\.(local|internal)$/i.test(h);
};

Try / catch

try {
  await sender.sendNotifications(task, configs);
} catch (err) {
  if (err instanceof Error && err.message.includes('must not target local or internal hosts')) {
    logger.warn('Internal push target rejected', { err: err.message });
  } else throw err;
}

Prevention

When it happens

Trigger: Registering a push notification URL whose hostname resolves to a disallowed name such as 'localhost', 'localhost.localdomain', 'metadata.google.internal', or any '*.[internal/local]' style host.

Common situations: Developers pointing push webhooks at a locally running test server (localhost:4111), Docker-compose service names like 'http://backend:3000', or accidentally registering an internal-only callback host in production config.

Related errors


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