Skip to content

browser

A safe entry point for opening URLs in the user's default browser or mail client — one validation gate in front of the OS handler, as a light, framework-free library.

browser constrains every URL-opening path through a single validation point, so a URL built from user- or config-influenced data can never hand a dangerous scheme or a malformed string to the operating system. It is the same helper behind go-tool-base's URL-opening, extracted so any project can adopt the discipline without pulling in the framework.

import "gitlab.com/phpboyscout/go/browser"

The single-gate idea

Opening a URL means handing a string to open (macOS), xdg-open (Linux), or rundll32 (Windows). If that string is attacker- or config-influenced, the OS handler is a dangerous place to send it unchecked. The whole point of this module is that there is exactly one function — OpenURL — that any code path may use to reach the OS, and it validates before it opens. Scatter direct calls to the OS opener across a codebase and each one is a hole; route them all through one gate and you have one place to reason about.

The three defences

Every URL passes three checks before the OS ever sees it. Validation is fail-fast, in this order:

  • Scheme allowlist. Only https, http, and mailto reach the OS. file://, javascript:, data:, and custom protocol handlers are rejected with ErrDisallowedScheme. The comparison is case-insensitive per RFC 3986, and the list is not configurable at runtime — AllowedSchemes() returns a defensive copy.
  • Length bound. URLs longer than MaxURLLength (8192 bytes — chosen to sit below every supported platform's command-line length limit) are rejected with ErrInvalidURL.
  • Control-character rejection. Any ASCII control character (0x000x1F, including NUL, and 0x7F) rejects the URL with ErrInvalidURL before a platform handler can receive it. An empty or unparseable URL fails the same way.

Framework-free & injectable

The module graph is deliberately tiny: only cli/browser (the cross-platform OS opener) and cockroachdb/errors are in the graph, and a depfootprint_test.go guard fails the build if anything else enters it. No config framework, no TUI, no go-tool-base.

The actual OS open is an injectable seam — an Opener func(rawURL string) error supplied through WithOpener. Tests inject a fake so nothing ever launches a real browser (see Test without launching a browser).

Quick start

package main

import (
    "context"
    "log"

    "gitlab.com/phpboyscout/go/browser"
)

func main() {
    if err := browser.OpenURL(context.Background(), "https://example.com/docs"); err != nil {
        log.Fatal(err)
    }
}

OpenURL returns ErrDisallowedScheme for a scheme outside the allowlist, ErrInvalidURL for an empty, oversize, control-character-bearing, or unparseable URL, the context's error if the context was cancelled before the open, or a wrapped error from the OS opener itself. Match the sentinels with errors.Is.

Install

go get gitlab.com/phpboyscout/go/browser

Where to go next

The documentation follows the Diátaxis framework:

Using go-tool-base? The framework routes all of its URL-opening through this helper. That wiring lives in go-tool-base; this module is standalone and framework-agnostic.