URL Inspector — Go source
Break any URL into its components - protocol, host, port, path, query params, hash, and credentials. Detects default ports and security at a glance, with a decode toggle for query values. Runs entirely in your browser.
This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Package urlinspector is the Go twin of CosmoDev's src/lib/url-inspector.ts
// (dual source: the web lib is TypeScript, the CLI lib is Go — kept in
// lock-step). Pure + deterministic, never panics: InspectUrl returns a report
// with Valid=false instead of throwing, mirroring the TS lib's never-throwing
// contract. The table-driven tests in urlinspector_test.go share vectors with
// src/lib/url-inspector.test.ts so the two implementations are held to the
// same contract.
//
// The twin mirrors the TS lib exactly: a flat, serialisable report of every
// URL component — including the signals the WHATWG URL API hides (credentials,
// default-vs-explicit ports, root-only/fragment-only URLs). Go's net/url is
// RFC-3986 rather than WHATWG, so the same edge cases are reproduced by hand:
// the input is trimmed before parsing, an empty hierarchical path normalises
// to "/", an explicit port is recovered straight from the raw input, IPv6
// hostnames keep their brackets, the scheme-default port is stripped from the
// origin, and the order of query parameters is preserved.
package urlinspector
import (
"net/url"
"regexp"
"strings"
"unicode/utf8"
)
// UrlParam is a single decoded query parameter, preserving insertion order.
type UrlParam struct {
Key string
Value string
}
// UrlReport is the flat, serialisable decomposition of a URL. Pointer fields
// mirror the TS lib's optional (undefined-when-absent) fields: a nil pointer
// means "not present", exactly like the TS `|| undefined` semantics. Plain
// string/bool fields are always populated for a valid report.
type UrlReport struct {
Valid bool
Protocol string
Username *string
Password *string
Host string
Hostname string
Port *string
Pathname string
Search *string
Hash *string
SearchParams []UrlParam
Origin string
IsSecure bool
DefaultPort *bool
Warnings []string
}
// defaultPorts mirrors DEFAULT_PORTS in the TS lib. TS keys carry a trailing
// ':' (e.g. "https:"); here the bare scheme is the key and ':' is re-appended
// only when a warning is emitted, so the warning text matches the TS verbatim.
var defaultPorts = map[string]string{
"http": "80",
"https": "443",
"ftp": "21",
"ws": "80",
"wss": "443",
}
var (
// schemeRe mirrors /^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/(.*)$/s — the (?s) flag
// makes '.' match newlines like the TS 's' (dotAll) flag.
schemeRe = regexp.MustCompile(`(?s)^([a-zA-Z][a-zA-Z0-9+.-]*)://(.*)$`)
digitsRe = regexp.MustCompile(`^\d+$`)
)
// DecodeParam percent-decodes a query value, treating '+' as a space; it never
// returns an error — malformed percent-encoding yields the original string. It
// is the Go twin of decodeParam() in src/lib/url-inspector.ts.
//
// Go's url.PathUnescape returns the decoded bytes without UTF-8 validation,
// whereas the TS decodeURIComponent THROWS on a byte sequence that is not valid
// UTF-8 (e.g. the incomplete "%E0%A4"). We therefore validate the result and
// fall back to the original input whenever the decoded bytes are not a valid
// UTF-8 string — mirroring the TS try/catch exactly.
func DecodeParam(v string) string {
s := strings.ReplaceAll(v, "+", " ")
out, err := url.PathUnescape(s)
if err != nil || !utf8.ValidString(out) {
return v // malformed percent-encoding — return the original
}
return out
}
// rawPort reads an explicitly-written port straight from the raw input. Go's
// net/url does not normalise default ports the way the WHATWG URL API does, but
// re-parsing the authority keeps the twin byte-for-byte faithful to the TS
// rawPort() helper (which exists precisely to recover what WHATWG hides).
// Handles userinfo (user:pass@) and IPv6 literals ([::1]:8080). ok is false
// when no port is present or it is non-numeric.
func rawPort(trimmed string) (port string, ok bool) {
m := schemeRe.FindStringSubmatch(trimmed)
if m == nil {
return "", false
}
rest := m[2]
authorityEnd := strings.IndexAny(rest, "/?#")
var authority string
if authorityEnd == -1 {
authority = rest
} else {
authority = rest[:authorityEnd]
}
atIdx := strings.LastIndex(authority, "@")
var hostport string
if atIdx == -1 {
hostport = authority
} else {
hostport = authority[atIdx+1:]
}
var portCandidate string
found := false
if strings.HasPrefix(hostport, "[") {
closeBracket := strings.IndexByte(hostport, ']')
if closeBracket == -1 {
return "", false
}
tail := hostport[closeBracket+1:]
if strings.HasPrefix(tail, ":") {
portCandidate = tail[1:]
found = true
}
} else {
colon := strings.IndexByte(hostport, ':')
if colon != -1 {
portCandidate = hostport[colon+1:]
found = true
}
}
if !found {
return "", false
}
if digitsRe.MatchString(portCandidate) {
return portCandidate, true
}
return "", false
}
// hostnameOf returns the WHATWG-style hostname: a trailing port is stripped and
// IPv6 literals keep their brackets (Go's Hostname() drops the brackets that
// WHATWG url.hostname preserves).
func hostnameOf(u *url.URL) string {
if strings.HasPrefix(u.Host, "[") {
if idx := strings.IndexByte(u.Host, ']'); idx != -1 {
return u.Host[:idx+1]
}
}
return u.Hostname()
}
// invalidReport mirrors the TS lib's { valid: false, warnings: [...] } shape.
func invalidReport(warning string) UrlReport {
return UrlReport{Valid: false, Warnings: []string{warning}}
}
// InspectUrl parses and decomposes a URL into a structured report; it never
// panics. It is the Go twin of inspectUrl() in src/lib/url-inspector.ts and
// must agree with it on every shared vector.
func InspectUrl(raw string) UrlReport {
warnings := []string{}
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return invalidReport("URL is empty")
}
u, err := url.Parse(trimmed)
if err != nil {
return invalidReport("Invalid URL — could not be parsed (include the scheme, e.g. https://)")
}
// WHATWG treats a URL as valid only when it has an absolute scheme and an
// authority; Go's lenient url.Parse accepts scheme-less / host-less input
// (e.g. "example.com/path", "https://") that WHATWG rejects.
if u.Scheme == "" || u.Host == "" {
return invalidReport("Invalid URL — could not be parsed (include the scheme, e.g. https://)")
}
// Decode query params preserving insertion order (url.Values loses order).
params := []UrlParam{}
if u.RawQuery != "" {
for _, pair := range strings.Split(u.RawQuery, "&") {
var k, v string
if i := strings.IndexByte(pair, '='); i >= 0 {
k = pair[:i]
v = pair[i+1:]
} else {
k = pair
}
dk, derr := url.QueryUnescape(k)
dv, verr := url.QueryUnescape(v)
if derr != nil || verr != nil {
dk, dv = k, v // fall back to the raw pair on malformed encoding
}
params = append(params, UrlParam{Key: dk, Value: dv})
}
}
// pathname — WHATWG normalises an empty hierarchical path to "/".
pathname := u.EscapedPath()
if pathname == "" {
pathname = "/"
}
var (
explicitPort string
hasPort bool
defaultPort *bool
)
report := UrlReport{
Valid: true,
Protocol: u.Scheme + ":",
Host: u.Host,
Hostname: hostnameOf(u),
Pathname: pathname,
SearchParams: params,
IsSecure: u.Scheme == "https" || u.Scheme == "wss",
}
// Credentials — present only when non-empty (TS `|| undefined`).
if u.User != nil {
if un := u.User.Username(); un != "" {
report.Username = &un
warnings = append(warnings, "URL contains a username credential")
}
if pw, ok := u.User.Password(); ok && pw != "" {
report.Password = &pw
warnings = append(warnings, "URL contains a password credential")
}
}
// Explicit port recovered from the raw input, mirroring TS rawPort().
explicitPort, hasPort = rawPort(trimmed)
if hasPort {
ep := explicitPort
report.Port = &ep
expected, known := defaultPorts[u.Scheme]
def := known && explicitPort == expected
defaultPort = &def
report.DefaultPort = defaultPort
if def {
warnings = append(warnings, "Port "+explicitPort+" is the default for "+u.Scheme+":")
}
}
// Root-only URL warning (mirrors the TS pathname/search/params condition).
if pathname == "/" && u.RawQuery == "" && len(params) == 0 {
warnings = append(warnings, "URL points to the site root (no path or query)")
}
if u.RawQuery != "" {
s := "?" + u.RawQuery
report.Search = &s
}
// EscapedFragment() preserves the original encoding, matching WHATWG url.hash
// (the raw fragment field is not reliably populated by url.Parse).
if frag := u.EscapedFragment(); frag != "" {
h := "#" + frag
report.Hash = &h
}
// origin — scheme://host, with a scheme-default port stripped (WHATWG).
originHost := u.Host
if defaultPort != nil && *defaultPort {
originHost = strings.TrimSuffix(originHost, ":"+explicitPort)
}
report.Origin = u.Scheme + "://" + originHost
report.Warnings = warnings
return report
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →