Skip to content

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:

fmt.Println(browser.AllowedSchemes()) // [https http mailto]

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 than MaxURLLength (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:

go run .

How to tell it worked

The first OpenURL call opens https://example.com/docs in your default browser and prints nothing. The second prints:

scheme not allowed — only [https http mailto]

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