The Problem: IPv6 Zone IDs in URLs

IPv6 link-local addresses (fe80::/10) are ambiguous when a machine has multiple network interfaces. Both eth0 and wlan0 could have the same address fe80::4. To resolve this, Linux uses zone IDs (e.g., fe80::4%eth0). Windows uses interface indices. These zones are essential for routing, but they break URL parsing.

In a URL, the host part for IPv6 is enclosed in brackets: [fe80::4]:80. Adding a zone gives [fe80::4%eth0]:80. Yet Go's net/url.Parse chokes on this:

package main

import "net/url"

func main() {
    if _, err := url.Parse("http://[fe80::4%eth0]:80"); err != nil {
        panic(err)
    }
}

Output:

panic: parse "http://[fe80::4%eth0]:80": invalid URL escape "%et"

Why? The % character in URLs introduces percent-encoding (e.g., %20 for space). %et is not a valid hex code, so the parser rejects it.

The Fix: Double Percent-Encoding

The correct encoding requires replacing % with %25. So fe80::4%eth0 becomes fe80::4%25eth0. Then Go parses it correctly:

package main

import (
    "fmt"
    "net/url"
)

func main() {
    u, err := url.Parse("http://[fe80::4%25eth0]:80")
    if err != nil {
        panic(err)
    }
    fmt.Println(u.Hostname())
}

Output:

fe80::4%eth0

The zone ID is preserved after decoding. This behavior is actually compliant with RFC 6874, which defines the IPv6addrz production rule:

IP-literal = "[" ( IPv6address / IPv6addrz / IPvFuture ) "]"
ZoneID = 1*( unreserved / pct-encoded )
IPv6addrz = IPv6address "%25" ZoneID

So the %25 is mandatory per the RFC. But this is a usability disaster. Developers expect %eth0 to work, not %25eth0.

The Impact: Cross-Platform and Cross-Library Inconsistencies

This isn't just a Go problem. The same issue affects nginx (ticket #623), Python's requests library (issue #6808), and most HTTP clients. Browsers avoid it entirely: they don't support IPv6 zone IDs in URLs because it breaks the origin concept used for security policies like CORS. An IETF draft (draft-schinazi-httpbis-link-local-uri-bcp-03) attempts to define a zone-aware origin, but it's still a draft.

For Anubis (the author's project), pointing to IPv6 zoned addresses requires this ugly %25 encoding. The author laments: "My policy of not forking the Go standard library means this somewhat terrible UX for an edge case is acceptable. I hate it, but what can you do?"

Practical Workarounds

If you must use IPv6 zones in URLs:

The Deeper Issue: URL Grammar vs. Networking Reality

The URL spec (RFC 3986) predates widespread IPv6 zone usage. Percent-encoding was designed for reserved characters, but using % as part of the zone ID creates ambiguity. The RFC 6874 fix (%25) is technically correct but ergonomically terrible. It's a classic standards conflict: the networking folks defined zones with %, the URL folks reserved % for encoding.

What You Should Do Now

If your application uses IPv6 link-local addresses with zones, audit your URL handling. Write a test that exercises the %25 encoding. Consider using global unicast addresses instead if possible—they don't need zones. If you're building a library, document this quirk prominently. And if you're frustrated, join the chorus: file a complaint with your language's URL parser maintainer. The draft BCP might eventually push browsers to support zones, but until then, we're stuck with double percent-encoding.


Published 2026-06-05. Source: xeiaso.net/notes/2026/ipv6-zones-go-url