Getting started¶
This tutorial walks you through your first safe URL open, end to end. You will
open a valid https URL, then deliberately try a dangerous one and see the
validation gate reject it — and learn how to tell the two failure kinds apart.
Everything happens in one self-contained Go program so you can see each moving
part. By the end you will understand what OpenURL guarantees before it ever
reaches the operating system.
Allow about ten minutes. You need a machine with a browser for step 1 to do anything visible — on a headless box the call still runs, it just returns an error instead of opening a window, and the rest of the tutorial works exactly the same.
What you need before you start¶
- Go 1.26 or newer.
- A new module to experiment in:
mkdir browser-tutorial && cd browser-tutorial
go mod init example.test/browser-tutorial
go get gitlab.com/phpboyscout/go/browser
Step 1 — Open a URL the gate accepts¶
OpenURL takes a context.Context, the raw URL string, and zero or more
options. With a well-formed https URL it validates, then hands the URL to the
OS handler for the user's default browser:
ctx := context.Background()
if err := browser.OpenURL(ctx, "https://example.com/docs"); err != nil {
log.Fatalf("open failed: %v", err)
}
The context is respected up to the moment of opening: if it is already cancelled
when OpenURL runs, the open is skipped and the context's error is returned.
The opener itself is not context-aware — once the OS has spawned a browser,
there is nothing left to cancel.
Step 2 — Watch a javascript: URL get rejected¶
Now try a scheme that is not on the allowlist. javascript: is exactly the
kind of URL you never want reaching an OS handler:
err := browser.OpenURL(ctx, "javascript:alert(1)")
// err is non-nil, and errors.Is(err, browser.ErrDisallowedScheme) is true.
Only https, http, and mailto pass the gate. file://, data:,
javascript:, and custom protocol handlers all fail with
ErrDisallowedScheme. You can read the current allowlist at runtime:
Step 3 — Tell a bad scheme from a malformed URL¶
OpenURL returns two sentinel errors, and you match them with errors.Is:
ErrDisallowedScheme— the URL parsed fine, but its scheme is not permitted.ErrInvalidURL— the URL failed hygiene: empty, longer thanMaxURLLength(8192 bytes), contained an ASCII control character, or could not be parsed.
switch {
case errors.Is(err, browser.ErrDisallowedScheme):
fmt.Println("scheme not allowed — only https, http, mailto")
case errors.Is(err, browser.ErrInvalidURL):
fmt.Println("malformed URL — empty, too long, control chars, or unparseable")
case err != nil:
fmt.Println("the OS opener failed:", err)
}
The complete program¶
package main
import (
"context"
"fmt"
"github.com/cockroachdb/errors"
"gitlab.com/phpboyscout/go/browser"
)
func main() {
ctx := context.Background()
// A valid URL opens in the default browser.
if err := browser.OpenURL(ctx, "https://example.com/docs"); err != nil {
reportOpenError(err)
}
// A disallowed scheme is rejected before the OS ever sees it.
if err := browser.OpenURL(ctx, "javascript:alert(1)"); err != nil {
reportOpenError(err)
}
}
func reportOpenError(err error) {
switch {
case errors.Is(err, browser.ErrDisallowedScheme):
fmt.Println("scheme not allowed — only", browser.AllowedSchemes())
case errors.Is(err, browser.ErrInvalidURL):
fmt.Println("malformed URL:", err)
default:
fmt.Println("OS opener failed:", err)
}
}
Run it:
How to tell it worked¶
The first OpenURL call opens https://example.com/docs in your default
browser and prints nothing. The second prints:
and opens nothing. If you never see a browser launch for the javascript: URL,
the gate is doing its job.
On a headless machine — a server, a container, a CI runner — the first call prints an OS-opener error instead, because there is no launcher to run:
OS opener failed: invoking URL opener: exec: "xdg-open,x-www-browser,www-browser,wslview": executable file not found in $PATH
That is the expected result there, and it is the third branch of the switch doing its job: the URL was fine, the machine could not open it.
Next steps¶
- Make every URL in your codebase go through the gate: Open URLs safely.
- Keep tests from launching real browsers: Test without launching a browser.
- Look up exactly which URLs are accepted and which error each rejection gives: Validation rules and Errors.
- Understand why the gate is shaped the way it is:
Threat model & the validation gate, and
What
browserdoes not do for where its guarantee stops.