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 into

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Pre-validate with the WHATWG parser: `new URL(input)` rejects malformed hosts with clearer errors
  2. Fix the IPv6 literal to contain only hex digits, colons, dots, and optionally a zone id
  3. 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

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


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/21bc39b74e77ff88. Report an issue: GitHub.