tailscale/tailscale · error

URL %q is missing scheme or host

Error message

URL %q is missing scheme or host

What it means

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.

Source

Thrown at clientupdate/distsign/url.go:30

	"tailscale.com/types/logger"
)

// DownloadVerified is a convenience wrapper around [Client.Download]
// for callers that have a full URL (e.g.
// https://pkgs.tailscale.com/unstable/foo.gaf) rather than a base URL
// plus path. It splits srcURL into a base ("scheme://host") and a path,
// constructs a [Client] for the base, and downloads with signature
// verification to dstPath.
func DownloadVerified(ctx context.Context, logf logger.Logf, srcURL, dstPath string) error {
	if logf == nil {
		logf = logger.Discard
	}
	u, err := url.Parse(srcURL)
	if err != nil {
		return fmt.Errorf("parsing URL %q: %w", srcURL, err)
	}
	if u.Scheme == "" || u.Host == "" {
		return fmt.Errorf("URL %q is missing scheme or host", srcURL)
	}
	base := &url.URL{Scheme: u.Scheme, User: u.User, Host: u.Host}
	path := strings.TrimPrefix(u.Path, "/")
	if path == "" {
		return fmt.Errorf("URL %q has no path component", srcURL)
	}
	c, err := NewClient(logf, base.String())
	if err != nil {
		return err
	}
	return c.Download(ctx, path, dstPath)
}

View on GitHub (pinned to cfe32b8be6)

Solutions

  1. Pass a fully-qualified URL including scheme, e.g. https://pkgs.tailscale.com/unstable/foo.gaf
  2. Normalize at config load: if the stored value lacks "://", prepend "https://"
  3. Pre-validate with u, _ := url.Parse(srcURL); reject when u.Scheme == "" || u.Host == ""

Example fix

// before
err := distsign.DownloadVerified(ctx, logf, "pkgs.tailscale.com/tailscale.tgz", dst)

// after
err := distsign.DownloadVerified(ctx, logf, "https://pkgs.tailscale.com/tailscale.tgz", dst)
Defensive patterns

Strategy: validation

Validate before calling

func requireAbsoluteURL(raw string) (string, error) {
	raw = strings.TrimSpace(raw)
	if !strings.Contains(raw, "://") {
		raw = "https://" + raw // or reject, per policy
	}
	u, err := url.Parse(raw)
	if err != nil {
		return "", err
	}
	if u.Scheme == "" || u.Host == "" {
		return "", fmt.Errorf("URL %q is missing scheme or host", raw)
	}
	return u.String(), nil
}

Type guard

func hasSchemeAndHost(s string) bool {
	u, err := url.Parse(strings.TrimSpace(s))
	return err == nil && u.Scheme != "" && u.Host != ""
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15). Data as JSON: /api/errors/c8df7637569df1c1. Report an issue: GitHub.