tailscale/tailscale · error
failed to parse base URL %q: %w
Error message
failed to parse base URL %q: %w
What it means
createClient parses the base URL for the Tailscale API client - spec.loginURL when set, otherwise ipn.DefaultControlURL. url.Parse is lenient (scheme-less strings still parse), so it errors only on genuinely malformed input such as control characters or invalid percent-escapes; when it does, the offending URL is included in the message.
Source
Thrown at k8s-operator/reconciler/tailnet/tailnet.go:260
const (
clientIDKey = "client_id"
clientSecretKey = "client_secret"
audienceKey = "audience"
)
func (r *Reconciler) createClient(tailnet *tsapi.Tailnet, secret *corev1.Secret) (tsclient.Client, error) {
if r.clientFunc != nil {
return r.clientFunc(tailnet, secret), nil
}
baseURL := ipn.DefaultControlURL
if tailnet.Spec.LoginURL != "" {
baseURL = tailnet.Spec.LoginURL
}
base, err := url.Parse(baseURL)
if err != nil {
return nil, fmt.Errorf("failed to parse base URL %q: %w", baseURL, err)
}
var auth tailscale.Auth
clientID := string(secret.Data[clientIDKey])
audience := string(secret.Data[audienceKey])
clientSecret := string(secret.Data[clientSecretKey])
switch {
case audience != "":
// If the audience field is present, we assume workload identity as the authentication method.
auth = &tailscale.IdentityFederation{
ClientID: clientID,
IDTokenFunc: r.createToken(audience),
}
case clientSecret != "":
// For a client secret, we assume oauth.
auth = &tailscale.OAuth{View on GitHub (pinned to 6e0912f979)
Solutions
- Reveal invisible characters: kubectl get tailnet <name> -o jsonpath='{.spec.loginURL}' | xxd | head
- Normalize to a clean https URL and re-apply the Tailnet
- Drop the field entirely to use the default control plane
- Validate the field at admission with a CEL rule or webhook
Example fix
# before spec: loginURL: "https://ctrl.example.com\n" # trailing newline breaks url.Parse # after spec: loginURL: "https://ctrl.example.com"
Defensive patterns
Strategy: validation
Validate before calling
func validLoginURL(s string) bool {
if s == "" {
return true // default control URL
}
u, err := url.Parse(s)
return err == nil && u.Scheme != "" && u.Host != ""
} Prevention
- Trim URL inputs at the boundary (Helm templates, CR defaults)
- Use cat -A/xxd when debugging pasted URLs
- Prefer CEL rules on URL-shaped CR fields
When it happens
Trigger: url.Parse(baseURL) failing: spec.loginURL containing control characters (newline, NUL), invalid escapes like %zz, or other characters Go's URL parser rejects. The default control URL is a constant and always valid.
Common situations: Copy-pasted control server URLs with trailing newlines; Helm/templating injecting stray characters; values round-tripped through ConfigMaps without trimming.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- at least one disablement secret required
- cluster tag must be provided
- invalid pkgsAddr %q: %w
- failed to create tailnet client: %w
- invalid port in address %q
AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18).
Data as JSON: /api/errors/8f60f9fb51ee7171.
Report an issue: GitHub.