{"record":{"id":"8f60f9fb51ee7171","repo":"tailscale/tailscale","slug":"failed-to-parse-base-url-q-w","errorCode":null,"errorMessage":"failed to parse base URL %q: %w","messagePattern":"failed to parse base URL %q: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"k8s-operator/reconciler/tailnet/tailnet.go","lineNumber":260,"sourceCode":"const (\n\tclientIDKey     = \"client_id\"\n\tclientSecretKey = \"client_secret\"\n\taudienceKey     = \"audience\"\n)\n\nfunc (r *Reconciler) createClient(tailnet *tsapi.Tailnet, secret *corev1.Secret) (tsclient.Client, error) {\n\tif r.clientFunc != nil {\n\t\treturn r.clientFunc(tailnet, secret), nil\n\t}\n\n\tbaseURL := ipn.DefaultControlURL\n\tif tailnet.Spec.LoginURL != \"\" {\n\t\tbaseURL = tailnet.Spec.LoginURL\n\t}\n\n\tbase, err := url.Parse(baseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse base URL %q: %w\", baseURL, err)\n\t}\n\n\tvar auth tailscale.Auth\n\n\tclientID := string(secret.Data[clientIDKey])\n\taudience := string(secret.Data[audienceKey])\n\tclientSecret := string(secret.Data[clientSecretKey])\n\n\tswitch {\n\tcase audience != \"\":\n\t\t// If the audience field is present, we assume workload identity as the authentication method.\n\t\tauth = &tailscale.IdentityFederation{\n\t\t\tClientID:    clientID,\n\t\t\tIDTokenFunc: r.createToken(audience),\n\t\t}\n\tcase clientSecret != \"\":\n\t\t// For a client secret, we assume oauth.\n\t\tauth = &tailscale.OAuth{","sourceCodeStart":242,"sourceCodeEnd":278,"githubUrl":"https://github.com/tailscale/tailscale/blob/6e0912f97994f927632b34ae9e63b53d6516a6ac/k8s-operator/reconciler/tailnet/tailnet.go#L242-L278","documentation":"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.","triggerScenarios":"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.","commonSituations":"Copy-pasted control server URLs with trailing newlines; Helm/templating injecting stray characters; values round-tripped through ConfigMaps without trimming.","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"],"exampleFix":"# before\nspec:\n  loginURL: \"https://ctrl.example.com\\n\"   # trailing newline breaks url.Parse\n# after\nspec:\n  loginURL: \"https://ctrl.example.com\"","handlingStrategy":"validation","validationCode":"func validLoginURL(s string) bool {\n    if s == \"\" {\n        return true // default control URL\n    }\n    u, err := url.Parse(s)\n    return err == nil && u.Scheme != \"\" && u.Host != \"\"\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["go","url-parsing","tailscale","configuration"],"backgroundTag":"url-parse-failed","analyzedSha":"6e0912f97994f927632b34ae9e63b53d6516a6ac","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}