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
- Fetch a publicly routable URL instead — the guard is intentional SSRF protection and should not be bypassed lightly.
- 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.
- Use a publicly reachable hostname/tunnel (e.g. a tunneling service) for the dev server so the DNS resolution is public.
- 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
- Only feed publicly routable http(s) URLs to the web-fetch tool.
- For local dev, use a tunnel with a public hostname.
- Check /etc/hosts and DNS for entries mapping public names to private IPs.
- Never try to bypass the SSRF guard; extend the tool's allowlist config instead if you legitimately need internal access.
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
- Cookie password must be at least 32 characters. Set OKTA_COO
- Cookie password must be at least 32 characters. Set WORKOS_C
- Invalid route path: "${path}". Path cannot contain '..', '?'
- Worker ${label} must stay within the deployed artifact root.
- push-failed
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/9c8d16c4c46db73b.
Report an issue: GitHub.