mastra-ai/mastra · error · WebFetchError

URL resolves to a private or reserved address.

Error message

URL resolves to a private or reserved address.

What it means

The built-in web-fetch tool enforces an SSRF guard: before fetching, assertAllowedUrl normalizes the hostname and rejects hostnames that are blocked (localhost/internal names) or resolve to private/reserved IP ranges (loopback, link-local, RFC1918, metadata endpoints, etc.). If the target URL's hostname matches a blocked name or IP, the tool throws a WebFetchError instead of performing the request, protecting internal networks from agent-driven requests.

Source

Thrown at packages/core/src/tools/builtin/web-fetch.ts:132

    (first & 0xff00) === 0xff00
  );
}

function isBlockedIp(address: string): boolean {
  const normalizedAddress = normalizeHostname(address);
  const ipVersion = net.isIP(normalizedAddress);
  return ipVersion === 4
    ? isBlockedIpv4(normalizedAddress)
    : ipVersion === 6
      ? isBlockedIpv6(normalizedAddress)
      : false;
}

function assertAllowedUrl(url: URL): void {
  const hostname = normalizeHostname(url.hostname);

  if (isBlockedHostname(hostname) || isBlockedIp(hostname)) {
    throw new WebFetchError('URL resolves to a private or reserved address.');
  }
}

function createLookup() {
  return (
    hostname: string,
    options: LookupOptions,
    callback: (error: NodeJS.ErrnoException | null, address: string | LookupAddress[], family?: number) => void,
  ) => {
    dnsLookup(hostname, options, (error, address, family) => {
      if (error) {
        callback(error, address, family);
        return;
      }

      const resolvedAddresses = Array.isArray(address) ? address.map(result => result.address) : [address];
      const blockedAddress = resolvedAddresses.find(isBlockedIp);

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Fetch a publicly routable URL instead — the guard is intentional SSRF protection and should not be bypassed lightly.
  2. If local/internal fetching is a legitimate requirement, reconfigure or extend the tool's allowlist options (if the tool exposes allowed-hosts configuration) rather than editing the guard.
  3. Use a publicly reachable hostname/tunnel (e.g. a tunneling service) for the dev server so the DNS resolution is public.
  4. If this blocks a valid public site, check DNS: the hostname may resolve to a private IP (misconfigured DNS or /etc/hosts entry).

Example fix

// before
await webFetchTool.execute({ context: { url: 'http://localhost:3000/api/data' } });

// after (dev: expose via a tunnel or use the public URL)
await webFetchTool.execute({ context: { url: 'https://dev.example.com/api/data' } });
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check a URL against the same class of rules before calling webFetch
function isPublicHttpUrl(raw: string): boolean {
  try {
    const u = new URL(raw);
    if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
    const host = u.hostname.toLowerCase();
    if (host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.internal') || host.endsWith('.local')) return false;
    if (/^(127\.|10\.|192\.168\.|169\.254\.|0\.)/.test(host)) return false;
    if (/^::1$|^fc00:|^fe80:|^fd/i.test(host)) return false;
    const m172 = host.match(/^172\.(\d+)\./);
    if (m172 && +m172[1] >= 16 && +m172[1] <= 31) return false;
    return true;
  } catch { return false; }
}

Type guard

function isPublicHostname(hostname: string): boolean {
  return !/^(localhost|127\.|10\.|192\.168\.|169\.254\.)/.test(hostname) && !/^172\.(1[6-9]|2\d|3[01])\./.test(hostname);
}

Try / catch

try {
  await webFetchTool.execute({ context: { url } });
} catch (err) {
  if (err instanceof Error && err.message.includes('private or reserved address')) {
    console.warn(`blocked non-public URL: ${url}`);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling the webFetch tool (or its request path) with a URL whose hostname is localhost, 127.0.0.1, ::1, 169.254.169.254, an RFC1918 address (10.x, 192.168.x, 172.16-31.x), or a DNS name that resolves to any of those; also triggered when a hostname resolves to a private IP at DNS-lookup time via the createLookup hook.

Common situations: Pointing the tool at a local dev server (http://localhost:3000) during development; fetching cloud metadata endpoints; testing against internal/staging services behind private DNS; on-prem deployments where legitimate targets are on private networks.

Related errors


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