{"record":{"id":"c8df7637569df1c1","repo":"tailscale/tailscale","slug":"url-q-is-missing-scheme-or-host","errorCode":null,"errorMessage":"URL %q is missing scheme or host","messagePattern":"URL %q is missing scheme or host","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"clientupdate/distsign/url.go","lineNumber":30,"sourceCode":"\t\"tailscale.com/types/logger\"\n)\n\n// DownloadVerified is a convenience wrapper around [Client.Download]\n// for callers that have a full URL (e.g.\n// https://pkgs.tailscale.com/unstable/foo.gaf) rather than a base URL\n// plus path. It splits srcURL into a base (\"scheme://host\") and a path,\n// constructs a [Client] for the base, and downloads with signature\n// verification to dstPath.\nfunc DownloadVerified(ctx context.Context, logf logger.Logf, srcURL, dstPath string) error {\n\tif logf == nil {\n\t\tlogf = logger.Discard\n\t}\n\tu, err := url.Parse(srcURL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parsing URL %q: %w\", srcURL, err)\n\t}\n\tif u.Scheme == \"\" || u.Host == \"\" {\n\t\treturn fmt.Errorf(\"URL %q is missing scheme or host\", srcURL)\n\t}\n\tbase := &url.URL{Scheme: u.Scheme, User: u.User, Host: u.Host}\n\tpath := strings.TrimPrefix(u.Path, \"/\")\n\tif path == \"\" {\n\t\treturn fmt.Errorf(\"URL %q has no path component\", srcURL)\n\t}\n\tc, err := NewClient(logf, base.String())\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn c.Download(ctx, path, dstPath)\n}\n","sourceCodeStart":12,"sourceCodeEnd":43,"githubUrl":"https://github.com/tailscale/tailscale/blob/cfe32b8be6a33f8e24fbc369cbfbf7c729d9e042/clientupdate/distsign/url.go#L12-L43","documentation":"Returned by distsign.DownloadVerified when srcURL parsed successfully but has an empty Scheme or Host component. DownloadVerified needs a full absolute URL because it splits it into a scheme://host base for constructing the verified-download Client; a host-only or path-only string cannot be split that way.","triggerScenarios":"Calling DownloadVerified with \"pkgs.tailscale.com/tailscale_1.2.3_amd64.tgz\" (no scheme), \"https:///unstable/foo.gaf\" (no host), or a URL assembled by concatenating a bare hostname with a path.","commonSituations":"Configuration that stores a hostname instead of a full URL; code that builds the URL by string concatenation and forgets the scheme; switching from a Client+relative-path API to the DownloadVerified convenience wrapper without adding the scheme.","solutions":["Pass a fully-qualified URL including scheme, e.g. https://pkgs.tailscale.com/unstable/foo.gaf","Normalize at config load: if the stored value lacks \"://\", prepend \"https://\"","Pre-validate with u, _ := url.Parse(srcURL); reject when u.Scheme == \"\" || u.Host == \"\""],"exampleFix":"// before\nerr := distsign.DownloadVerified(ctx, logf, \"pkgs.tailscale.com/tailscale.tgz\", dst)\n\n// after\nerr := distsign.DownloadVerified(ctx, logf, \"https://pkgs.tailscale.com/tailscale.tgz\", dst)","handlingStrategy":"validation","validationCode":"func requireAbsoluteURL(raw string) (string, error) {\n\traw = strings.TrimSpace(raw)\n\tif !strings.Contains(raw, \"://\") {\n\t\traw = \"https://\" + raw // or reject, per policy\n\t}\n\tu, err := url.Parse(raw)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif u.Scheme == \"\" || u.Host == \"\" {\n\t\treturn \"\", fmt.Errorf(\"URL %q is missing scheme or host\", raw)\n\t}\n\treturn u.String(), nil\n}","typeGuard":"func hasSchemeAndHost(s string) bool {\n\tu, err := url.Parse(strings.TrimSpace(s))\n\treturn err == nil && u.Scheme != \"\" && u.Host != \"\"\n}","tryCatchPattern":null,"preventionTips":["Store full absolute URLs (scheme + host + path) in configuration rather than bare hostnames","Normalize scheme-less values to https:// at load time and log the normalization","Prefer building request URLs with url.URL fields instead of string concatenation"],"tags":["url","validation","go","distsign"],"backgroundTag":null,"analyzedSha":"cfe32b8be6a33f8e24fbc369cbfbf7c729d9e042","analyzedAt":"2026-08-15T19:58:31.583Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}