tailscale/tailscale · error
non-localhost target %q must include a scheme
Error message
non-localhost target %q must include a scheme
What it means
The target had no scheme (no "://" present), so the default scheme was prepended internally (hasScheme == false) — but implicit defaulting is only permitted for loopback hosts. For remote hosts the caller must state the scheme explicitly, because guessing http vs https for an external origin changes proxy semantics.
Source
Thrown at ipn/serve.go:823
if !slices.Contains(supportedSchemes, u.Scheme) {
return "", fmt.Errorf("must be a URL starting with one of the supported schemes: %v", supportedSchemes)
}
// validate port according to host.
if u.Hostname() == "localhost" || u.Hostname() == "127.0.0.1" || u.Hostname() == "::1" {
// require port for localhost targets
if u.Port() == "" {
return "", fmt.Errorf("port required for localhost target %q", target)
}
} else {
validHN := dnsname.ValidHostname(u.Hostname()) == nil
validIP := net.ParseIP(u.Hostname()) != nil
if !validHN && !validIP {
return "", fmt.Errorf("invalid hostname or IP address %q", u.Hostname())
}
// require scheme for non-localhost targets
if !hasScheme {
return "", fmt.Errorf("non-localhost target %q must include a scheme", target)
}
}
port, err := strconv.ParseUint(u.Port(), 10, 16)
if err != nil || port == 0 {
if u.Port() == "" {
return u.String(), nil // allow no port for remote destinations
}
return "", fmt.Errorf("invalid port %q", u.Port())
}
u.Host = net.JoinHostPort(u.Hostname(), strconv.Itoa(int(port)))
return u.String(), nil
}
// TCPs returns an iterator over both background and foreground TCP
// listeners.
//View on GitHub (pinned to 57c3357fdb)
Solutions
- Include the scheme: https://example.com:8080.
- Keep local backends on localhost/127.0.0.1 where defaulting (or the bare "3000" shorthand) applies.
- Lint serve configs so remote targets always start with scheme://.
Example fix
# before tailscale serve / example.com:8080 # err: non-localhost target "http://example.com:8080" must include a scheme # after tailscale serve / https://example.com:8080
Defensive patterns
Strategy: validation
Validate before calling
needsScheme := !strings.Contains(target, "://") &&
!strings.HasPrefix(target, "localhost:") && !strings.HasPrefix(target, "127.0.0.1:")
if needsScheme && !isLocalhostShorthand(target) {
if _, err := netip.ParseAddr(target); err != nil { // bare-port/IP shorthands are local
return errors.New("remote targets must include scheme://")
}
}
return ipn.ExpandProxyTargetValue(target, supportedSchemes, defaultScheme) Prevention
- Always write remote backends as scheme://host:port in configs.
- Lint serve configs: any target whose host is not loopback must contain "://".
When it happens
Trigger: ipn.ExpandProxyTargetValue("example.com:8080", schemes, "http") — host is non-localhost and hasScheme is false, so after defaulting/validation the function returns this error rather than silently proxying over the assumed scheme.
Common situations: Omitting https:// for an external backend; configs written for localhost targets reused with a remote host; CLI shorthand habits applied to remote origins.
Related errors
- must be a URL starting with one of the supported schemes: %v
- empty target
- unix sockets are not supported for this target type
- invalid URL %w
- getting serve config: %w
AI-assisted analysis of tailscale/tailscale@57c3357fdb (2026-08-18).
Data as JSON: /api/errors/5e1332a847a3d7b1.
Report an issue: GitHub.