Skip to content

Validation rules — which URLs are accepted

The complete set of checks OpenURL applies, the order it applies them in, and worked examples of what each one accepts and rejects. Every example on this page was run against the current implementation.

The five checks, in order

Validation is fail-fast: the first check that fails returns immediately and the rest never run. That ordering decides which error you get when a URL breaks more than one rule.

# Check Rejects with
1 URL is not empty ErrInvalidURL
2 len(rawURL)MaxURLLength (8192 bytes) ErrInvalidURL
3 No ASCII control character (0x000x1F, 0x7F) anywhere in the string ErrInvalidURL
4 net/url.Parse succeeds ErrInvalidURL
5 Parsed scheme is https, http, or mailto, compared case-insensitively ErrDisallowedScheme

Only after all five pass is the context checked, and only then is the opener called. A URL with a disallowed scheme and an already-cancelled context returns ErrDisallowedScheme, not context.Canceled.

What counts as a control character

The rejected set is 0x000x1F inclusive, plus 0x7F (DEL). That covers NUL, tab (0x09), CR (0x0D), and LF (0x0A). It is a byte-range test applied to every rune in the string, wherever it appears — scheme, host, path, query, or fragment.

Space (0x20) is not a control character and is not rejected by this check. A space can still fail the URL at check 4 if it lands somewhere net/url.Parse refuses it, such as inside the host. A space inside the path does not: https://example.com/a b is accepted and reaches the opener unchanged.

Nothing above 0x7F is rejected here. Non-ASCII characters, including bidi control characters such as U+202E, pass this check.

How the length limit is counted

MaxURLLength is 8192 bytes, measured with len() on the raw string before any parsing. Multi-byte characters count for every byte they occupy: a URL of 4,110 characters, most of them two-byte runes, measures 8,200 bytes and is rejected despite being well under 8,192 characters.

The URL at exactly 8192 bytes is accepted; 8193 is not.

Why example.com is a scheme error, not a parse error

net/url.Parse is permissive. It accepts a string with no scheme at all, returning an empty Scheme rather than an error, so the failure surfaces one check later as ErrDisallowedScheme with scheme "" is not permitted.

This catches people out, because "example.com is not a valid URL" feels like a hygiene problem. In this package it is a scheme problem, and the error you match on is ErrDisallowedScheme. The same is true of //example.com/path and /just/a/path.

If you accept bare hostnames from users, prefix a scheme yourself before calling OpenURL — the gate will not do it for you, because guessing https:// for an attacker-supplied string is exactly the kind of rewriting a validation boundary should not be doing.

Worked examples — accepted

URL Why it passes
https://example.com The ordinary case.
https://example.com/path?q=1&r=2#frag Query and fragment are not inspected.
HTTPS://EXAMPLE.COM Scheme comparison is case-insensitive per RFC 3986.
Https://example.com Same.
http://localhost:8080 http is on the allowlist; the host is not checked.
http://[::1]:8080/ IPv6 literal, parsed and passed through.
https://user:pass@example.com Userinfo is not inspected or stripped.
https://exámple.com/ünicode Non-ASCII is not a control character.
https://example.com/a b A space in the path parses fine.
https: Scheme-only. net/url.Parse accepts it and the scheme is allowed.
https:// No host at all. Accepted for the same reason.
mailto: Accepted with no address; mailto: bodies are never parsed.
An 8192-byte URL Exactly at the cap.

https:, https:// and mailto: are the surprising ones: the gate does not require a host, an address, or anything at all after the scheme. It checks the scheme and the string's shape, not whether opening the URL will achieve anything.

Worked examples — rejected with ErrDisallowedScheme

URL Parsed scheme
file:///etc/passwd file
javascript:alert(1) javascript
data:text/html,<h1>hi</h1> data
vbscript:MsgBox("hi") vbscript
ftp://ftp.example.com/ ftp
tel:+441234 tel
myapp://callback myapp
example.com (empty)
//example.com/path (empty)

The attached hint names the offending scheme and lists the permitted ones.

Worked examples — rejected with ErrInvalidURL

URL Which check caught it
"" 1 — empty
8193 bytes or more 2 — length
https://example.com/\x00path 3 — NUL
https://example.com/\r\npath 3 — CR and LF
https://example.com/\tpath 3 — tab
https://example.com/\x7f 3 — DEL
://bad 4 — missing protocol scheme
https://example.com 4 — leading spaces make the parse fail
https://example.com 4 — trailing spaces land in the host
https:// 4 — space in the host
h%74tps://example.com 4 — percent-encoding does not smuggle a scheme past the check
http://[::1:8080/ 4 — unclosed IPv6 bracket

Does validation modify the URL?

No. OpenURL parses the URL to read its scheme and throws the parsed value away; the opener receives the exact string that was passed in, byte for byte. There is no normalisation, no percent-encoding, no trailing-slash handling, and no punycode conversion.

That matters if you are comparing what your code asked for against what the browser eventually loaded: any difference came from the platform handler or the browser, not from here.

See also