{"record":{"id":"5e1332a847a3d7b1","repo":"tailscale/tailscale","slug":"non-localhost-target-q-must-include-a-scheme","errorCode":null,"errorMessage":"non-localhost target %q must include a scheme","messagePattern":"non-localhost target %q must include a scheme","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"ipn/serve.go","lineNumber":823,"sourceCode":"\tif !slices.Contains(supportedSchemes, u.Scheme) {\n\t\treturn \"\", fmt.Errorf(\"must be a URL starting with one of the supported schemes: %v\", supportedSchemes)\n\t}\n\n\t// validate port according to host.\n\tif u.Hostname() == \"localhost\" || u.Hostname() == \"127.0.0.1\" || u.Hostname() == \"::1\" {\n\t\t// require port for localhost targets\n\t\tif u.Port() == \"\" {\n\t\t\treturn \"\", fmt.Errorf(\"port required for localhost target %q\", target)\n\t\t}\n\t} else {\n\t\tvalidHN := dnsname.ValidHostname(u.Hostname()) == nil\n\t\tvalidIP := net.ParseIP(u.Hostname()) != nil\n\t\tif !validHN && !validIP {\n\t\t\treturn \"\", fmt.Errorf(\"invalid hostname or IP address %q\", u.Hostname())\n\t\t}\n\t\t// require scheme for non-localhost targets\n\t\tif !hasScheme {\n\t\t\treturn \"\", fmt.Errorf(\"non-localhost target %q must include a scheme\", target)\n\t\t}\n\t}\n\tport, err := strconv.ParseUint(u.Port(), 10, 16)\n\tif err != nil || port == 0 {\n\t\tif u.Port() == \"\" {\n\t\t\treturn u.String(), nil // allow no port for remote destinations\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"invalid port %q\", u.Port())\n\t}\n\n\tu.Host = net.JoinHostPort(u.Hostname(), strconv.Itoa(int(port)))\n\n\treturn u.String(), nil\n}\n\n// TCPs returns an iterator over both background and foreground TCP\n// listeners.\n//","sourceCodeStart":805,"sourceCodeEnd":841,"githubUrl":"https://github.com/tailscale/tailscale/blob/57c3357fdb542d26c6f9e9f0b815ae5077e63d77/ipn/serve.go#L805-L841","documentation":"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.","triggerScenarios":"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.","commonSituations":"Omitting https:// for an external backend; configs written for localhost targets reused with a remote host; CLI shorthand habits applied to remote origins.","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://."],"exampleFix":"# before\ntailscale serve / example.com:8080\n# err: non-localhost target \"http://example.com:8080\" must include a scheme\n\n# after\ntailscale serve / https://example.com:8080","handlingStrategy":"validation","validationCode":"needsScheme := !strings.Contains(target, \"://\") &&\n\t!strings.HasPrefix(target, \"localhost:\") && !strings.HasPrefix(target, \"127.0.0.1:\")\nif needsScheme && !isLocalhostShorthand(target) {\n\tif _, err := netip.ParseAddr(target); err != nil { // bare-port/IP shorthands are local\n\t\treturn errors.New(\"remote targets must include scheme://\")\n\t}\n}\nreturn ipn.ExpandProxyTargetValue(target, supportedSchemes, defaultScheme)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always write remote backends as scheme://host:port in configs.","Lint serve configs: any target whose host is not loopback must contain \"://\"."],"tags":["tailscale","serve","url-scheme","proxy-target","go"],"backgroundTag":"missing-url-scheme","analyzedSha":"57c3357fdb542d26c6f9e9f0b815ae5077e63d77","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}