Skip to content

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:

scheme "file" is not permitted; allowed: [https http mailto]

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:

if errors.Is(err, myOpenerErr) {  }

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