denoland/deno · error · TypeError
ERR_INVALID_URL
ERR_INVALID_URL
Error message
Invalid URL: ${url} What it means
The legacy `url.parse()` hardens IPv6 hostnames: for bracketed IPv6 hosts, the hostname must contain only characters allowed in IPv6 literals; anything in the forbidden set (control characters, spaces, %, /, ?, # and similar) throws ERR_INVALID_URL with the whole input URL embedded in the message. This mirrors Node's hardening of the legacy parser against malformed or crafted hostnames.
Source
Thrown at ext/node/polyfills/url.ts:898
// assume that it's an IPv6 address.
const ipv6Hostname = isIpv6Hostname(hostname);
// validate a little.
if (!ipv6Hostname) {
rest = getHostname(this, rest, hostname);
}
if (this.hostname.length > hostnameMaxLen) {
this.hostname = "";
} else {
// Hostnames are always lower case.
this.hostname = StringPrototypeToLowerCase(this.hostname);
}
if (this.hostname !== "") {
if (ipv6Hostname) {
if (RegExpPrototypeTest(forbiddenHostCharsIpv6, this.hostname)) {
throw new ERR_INVALID_URL(url);
}
} else {
// IDNA Support: Returns a punycoded representation of "domain".
// It only converts parts of the domain name that
// have non-ASCII characters, i.e. it doesn't matter if
// you call it with a domain that already is ASCII-only.
// Use lenient mode (`true`) to try to support even non-compliant
// URLs.
this.hostname = idnaToASCII(this.hostname);
// Prevent two potential routes of hostname spoofing.
// 1. If this.hostname is empty, it must have become empty due to toASCII
// since we checked this.hostname above.
// 2. If any of forbiddenHostChars appears in this.hostname, it must have
// also gotten in due to toASCII. This is since getHostname would have
// filtered them out otherwise.
// Rather than trying to correct this by moving the non-host part intoView on GitHub (pinned to 9ad36f7a2c)
Solutions
- Pre-validate with the WHATWG parser: `new URL(input)` rejects malformed hosts with clearer errors
- Fix the IPv6 literal to contain only hex digits, colons, dots, and optionally a zone id
- Percent-encode IPv6 zone ids as %25<zone> or strip them before parsing
Example fix
// before
const u = url.parse("http://[fe80::1/foo]"); // ERR_INVALID_URL
// after
const u = new URL("http://[fe80::1]/foo"); // strict WHATWG parsing
// or fix the literal, then keep the legacy parser:
const u2 = url.parse("http://[fe80::1]/foo"); Defensive patterns
Strategy: validation
Validate before calling
try {
new URL(input); // WHATWG pre-filter for malformed hosts
} catch {
throw new Error(`rejecting malformed URL: ${input}`);
}
const u = url.parse(input); Try / catch
try {
const u = url.parse(input);
} catch (e: any) {
if (e?.code === "ERR_INVALID_URL") {
throw new Error(`rejecting malformed URL: ${input}`);
}
throw e;
} Prevention
- Pre-validate untrusted input with new URL() before legacy url.parse
- Keep IPv6 literals to hex digits, colons and dots
- Percent-encode IPv6 zone ids (%25eth0) or strip them
When it happens
Trigger: `url.parse('http://[fe80::1/foo]')` with a slash inside the brackets; a stray #, ?, or space inside the bracket literal; hand-concatenated URLs where a path character lands inside the host portion.
Common situations: Parsing untrusted URLs from logs, Referer headers, or scraped HTML; building URLs by string concatenation instead of the URL API.
Understand the failure class
Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.
Related errors
- ERR_INVALID_URL
- ERR_HTTP2_UNSUPPORTED_PROTOCOL
- ERR_INVALID_URL
- ERR_INVALID_MIME_SYNTAX
- ERR_INVALID_ARG_TYPE
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/21bc39b74e77ff88.
Report an issue: GitHub.