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:
- In Go: Always use
%25instead of%in the zone. Validate withnet/url.Parse. - In nginx: Avoid zone IDs in
proxy_pass; use the interface-scoped address without the zone if possible. Or encode manually. - In Python requests: Same dance. The library hasn't fixed it as of 2026.
- In browsers: Don't even try. Use a numeric IPv6 address without zones if you need to test locally.
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