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.
The single-gate idea¶
Opening a URL means handing a string to open (macOS), xdg-open and friends
(Linux), or ShellExecute (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, andmailtoreach the OS.file://,javascript:,data:, and custom protocol handlers are rejected withErrDisallowedScheme. 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 withErrInvalidURL. - Control-character rejection. Any ASCII control character (
0x00–0x1F, including NUL, and0x7F) rejects the URL withErrInvalidURLbefore a platform handler can receive it. An empty or unparseable URL fails the same way.
What the gate does not check is just as important: the host is never
examined, the allowlist cannot be extended by configuration, and mailto:
parameters are the caller's responsibility. See
What browser does not do.
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¶
Where to go next¶
The documentation follows the Diátaxis framework:
- Tutorial — learning-oriented, start here:
- Getting started — open a valid URL, then watch a dangerous scheme get rejected and handle the error.
- How-to guides — task-oriented recipes:
- Reference — look-it-up detail:
- API reference — every exported symbol and what it guarantees
- Validation rules — exactly which URLs are accepted, with worked examples
- Errors — every error, its hint, and what to do
- Platform behaviour — what the default opener runs on each OS
- Explanation — understanding-oriented background:
The generated godoc, with runnable examples, is on pkg.go.dev.
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.
Further reading¶
The blog carries a curated route through this subject: Building a command-line tool in Go collects everything written about it, ordered so you can start at the beginning rather than newest-first.
Ask phpbotscout

He answers questions about the projects over on the Discord, citing the docs where they already cover it, and offering to raise an issue where they don't. Bring a bug, an idea, or a questionable engineering decision.