decolua/9router · error · Error
Blocked URL: internal host
Error message
Blocked URL: internal host
What it means
assertPublicUrl is an SSRF guard for server-side fetches: before fetching a caller-supplied URL, it rejects hostnames explicitly listed as internal (localhost, ip6-localhost, ip6-loopback). Line 52 throws when the URL's hostname (lowercased) is in BLOCKED_HOSTNAMES. The guard exists so an API caller cannot make the server request itself or loopback services.
Source
Thrown at src/shared/utils/ssrfGuard.js:52
const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0;
return (ip & mask) === (base & mask);
});
}
function isBlockedIpv6(host) {
const h = host.replace(/^\[|\]$/g, "").toLowerCase();
const v4Mapped = h.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
if (v4Mapped) return isBlockedIpv4(v4Mapped[1]);
if (h === "::1" || h === "::") return true;
return h.startsWith("fe80:") || h.startsWith("fc") || h.startsWith("fd");
}
// Throw if URL targets a non-public host. Caller should map to 400.
export function assertPublicUrl(rawUrl) {
const parsed = new URL(rawUrl);
const host = parsed.hostname.toLowerCase();
if (BLOCKED_HOSTNAMES.has(host)) throw new Error("Blocked URL: internal host");
if (BLOCKED_SUFFIXES.some((s) => host.endsWith(s))) throw new Error("Blocked URL: internal host");
if (isBlockedIpv4(host)) throw new Error("Blocked URL: private IP");
if (host.includes(":") && isBlockedIpv6(host)) throw new Error("Blocked URL: private IP");
}
View on GitHub (pinned to 90b52e06ff)
Solutions
- Point the URL at the public hostname of the target service instead of localhost
- If the target really must be loopback, perform the fetch outside this guarded API
- Use a fully-qualified public domain reachable from the server
Example fix
// before
await fetchViaProxy("http://localhost:8080/api");
// throws: Blocked URL: internal host
// after
await fetchViaProxy("https://api.example.com/api"); Defensive patterns
Strategy: validation
Validate before calling
const BLOCKED = new Set(["localhost", "ip6-localhost", "ip6-loopback"]);
function isInternalHost(rawUrl) {
try { return BLOCKED.has(new URL(rawUrl).hostname.toLowerCase()); }
catch { return true; }
}
if (isInternalHost(targetUrl)) console.warn("Refusing to send internal host through proxy"); Type guard
function isPublicHttpUrl(v) {
if (typeof v !== "string") return false;
try {
const u = new URL(v);
return (u.protocol === "http:" || u.protocol === "https:") && !BLOCKED.has(u.hostname.toLowerCase());
} catch { return false; }
} Try / catch
try {
await proxyFetch(url);
} catch (e) {
if (e.message === "Blocked URL: internal host") {
throw new HttpError(400, "Target URL must be a public hostname, not localhost");
}
throw e;
} Prevention
- Never configure this server-side fetch proxy with localhost targets — it runs on the server, not your machine
- Use environment-specific public hostnames in config rather than localhost
- For local development, call local services directly instead of via the guarded endpoint
- Add the hostname check to your own pre-flight validation before calling the proxy API
When it happens
Trigger: Calling resolveBaseUrl / POST / handleFetch with a URL whose hostname is exactly `localhost`, `ip6-localhost`, or `ip6-loopback` (e.g. `http://localhost:8080/api`).
Common situations: Developer points the fetch-proxy at a locally running service during development; config/env still contains `localhost` from a local setup moved to a server; testing the proxy endpoint with a loopback URL from an HTTP client.
Related errors
- Blocked URL: private IP
- err.message (SSRF guard: blocked internal/private/metadata U
- Invalid baseUrl: ${override}
- Invalid baseUrl protocol: ${parsed.protocol}
- `xai discovery ${field} host ${host} is not on x.ai`
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/3d5e61decdc6fada.
Report an issue: GitHub.