Errors — what OpenURL returns and what to do about each¶
OpenURL returns one of four kinds of error, and they want different handling: two
mean the URL was refused, one means the caller gave up before the open, and one
means the operating system could not do it. This page lists every case, the exact
message and hint, and the response each calls for.
Errors are built with cockroachdb/errors.
Match them with errors.Is — from either errors package, both work — and never
by comparing message strings.
The four outcomes at a glance¶
| Outcome | Matches | Message | Retry? |
|---|---|---|---|
| URL failed hygiene | errors.Is(err, browser.ErrInvalidURL) |
invalid URL |
No — fix the input |
| Scheme not permitted | errors.Is(err, browser.ErrDisallowedScheme) |
disallowed URL scheme |
No — treat as hostile or misconfigured |
| Context already done | errors.Is(err, context.Canceled) or context.DeadlineExceeded |
context canceled / context deadline exceeded |
Nothing was opened; the caller asked to stop |
| Opener failed | Neither sentinel; wraps the opener's own error | invoking URL opener: … |
Maybe — the URL was fine, the machine could not open it |
ErrInvalidURL — the URL failed hygiene¶
Returned for four distinct inputs. All four match the same sentinel, and the attached hint is what tells them apart:
| Input | Hint |
|---|---|
| Empty string | URL is empty. |
Longer than MaxURLLength |
URL exceeds maximum length of 8192 bytes. |
| Contains an ASCII control character | URL contains control characters. |
net/url.Parse failed |
(none — the message reads parsing URL: invalid URL) |
Read the hint with errors.FlattenHints(err) or errors.GetAllHints(err) from
cockroachdb/errors:
if errors.Is(err, browser.ErrInvalidURL) {
log.Printf("URL rejected: %s", errors.FlattenHints(err))
// URL rejected: URL exceeds maximum length of 8192 bytes.
}
The parse-failure case is the odd one out. It carries no hint and does not
wrap the underlying *url.Error, so the reason net/url.Parse objected —
missing protocol scheme, invalid character " " in host name, and so on — is
not available from the returned error. If you need to tell a user why their URL
would not parse, call url.Parse on your own copy and report that error
yourself.
There is no way to distinguish the four cases programmatically: there is one
sentinel, not four, and hint text is not a stable API to branch on. If your code
needs to behave differently for "too long" than for "unparseable", check length
before calling OpenURL.
ErrDisallowedScheme — the scheme is not on the allowlist¶
The URL parsed, but its scheme is not https, http, or mailto. The hint names
both sides:
An empty scheme produces scheme "" is not permitted, which is what you get for
a bare example.com — see
Validation rules.
Treat this as terminal. A disallowed scheme usually means the input is hostile,
or that something upstream is generating URLs it should not be. Do not retry, do
not fall back to another opener, and do not "fix" the URL by swapping the scheme
for https — if the string came from somewhere you do not trust, neither does
the rest of it.
Context errors — nothing was opened¶
If the context is already cancelled or its deadline has passed when OpenURL
runs, the open is skipped and ctx.Err() is returned unwrapped: the error is
exactly context.Canceled or context.DeadlineExceeded, with no hint and no
added message.
The context is checked after validation, so an invalid URL with a cancelled context reports the URL problem, not the cancellation. That ordering is deliberate and covered by a test: the validation result is the more useful diagnostic.
Cancellation cannot stop an open that has already happened. Once the opener has been called, the browser process is the operating system's, not yours.
Opener errors — validation passed, the open did not¶
Anything the opener returns is wrapped with invoking URL opener and returned:
invoking URL opener: exec: "xdg-open,x-www-browser,www-browser,wslview": executable file not found in $PATH
Neither sentinel matches, so errors.Is(err, browser.ErrInvalidURL) is false —
which is the check to use when you want "was my URL wrong?" rather than "did it
work?". The original error is preserved, so if you supplied the opener you can
match your own sentinel:
This is the error a headless machine produces. It is also the only error class where a retry might be reasonable, and the only one that says nothing about the URL.
Handle all four in one switch¶
switch err := browser.OpenURL(ctx, target); {
case err == nil:
// opened
case errors.Is(err, browser.ErrDisallowedScheme):
return errors.WithHint(err, "only https, http, and mailto URLs can be opened")
case errors.Is(err, browser.ErrInvalidURL):
return errors.WithHint(err, "the URL is malformed")
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
return err // the caller asked to stop; nothing was opened
default:
// The URL was fine; the OS could not open it. Print the target so the
// user can open it themselves.
return errors.Wrap(err, "opening browser")
}
Do the errors contain the URL?¶
No. No error returned by OpenURL includes the URL, and the package never logs
it. The scheme appears in the ErrDisallowedScheme hint; nothing else from the
URL is exposed.
That is intentional — a URL's path and query routinely carry tokens, session identifiers, or personal data, and errors end up in logs. If you want to tell the user which URL failed, parse your own copy and log the scheme and host only:
if u, perr := url.Parse(target); perr == nil {
log.Printf("could not open %s://%s", u.Scheme, u.Host)
}
Do the errors carry stack traces?¶
Yes. Every error path attaches a cockroachdb/errors stack trace, so %+v
prints the trace along with the message and hint. %v and Error() print the
message alone.
Printing %+v at a user-facing boundary will therefore dump a trace into your
CLI output. Use %v there, and keep %+v for logs.
See also¶
- Validation rules — which input produces which error.
- Platform behaviour — what an opener failure means on each OS.
- Open URLs safely — the calling discipline these errors belong to.