{"record":{"id":"91e1fbb3210c4717","repo":"juanfont/headscale","slug":"erroidcissuerinvalid","errorCode":"errOIDCIssuerInvalid","errorMessage":"oidc.issuer must be a valid http(s) URL: got %q","messagePattern":"oidc\\.issuer must be a valid http\\(s\\) URL: got %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/types/config.go","lineNumber":384,"sourceCode":"\treturn nil\n}\n\n// validateOIDCConfig validates the OIDC settings, called when oidc.issuer is\n// set. It fails fast on a setup that cannot work: an invalid PKCE method, a\n// malformed issuer URL (which would otherwise surface as an opaque discovery\n// error or, worse, resolve to an unintended provider), or a missing client\n// id/secret.\nfunc validateOIDCConfig() error {\n\terr := validatePKCEMethod(viper.GetString(\"oidc.pkce.method\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tissuer := viper.GetString(\"oidc.issuer\")\n\n\tu, err := url.Parse(issuer)\n\tif err != nil || (u.Scheme != \"https\" && u.Scheme != \"http\") || u.Host == \"\" {\n\t\treturn fmt.Errorf(\"%w: got %q\", errOIDCIssuerInvalid, issuer)\n\t}\n\n\tif viper.GetString(\"oidc.client_id\") == \"\" {\n\t\treturn errOIDCClientIDRequired\n\t}\n\n\tif viper.GetString(\"oidc.client_secret\") == \"\" && viper.GetString(\"oidc.client_secret_path\") == \"\" {\n\t\treturn errOIDCClientSecretRequired\n\t}\n\n\treturn nil\n}\n\n// Domain returns the hostname/domain part of the [Config.ServerURL].\n// If the [Config.ServerURL] is not a valid URL, it returns the [Config.BaseDomain].\nfunc (c *Config) Domain() string {\n\tu, err := url.Parse(c.ServerURL)\n\tif err != nil {","sourceCodeStart":366,"sourceCodeEnd":402,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/types/config.go#L366-L402","documentation":"Startup OIDC configuration validation error (sentinel errOIDCIssuerInvalid): oidc.issuer could not be parsed as a URL, parsed but has a scheme other than http/https, or has an empty host. Viper hands the raw string to url.Parse; any of the three conditions rejects it. The server refuses to start.","triggerScenarios":"Config contains oidc.issuer values like 'accounts.google.com' (no scheme), 'ldap://provider' (wrong scheme), or a string that fails URL parsing entirely. validateOIDCConfig runs during config load, before PKCE passes but after the pkce.method check.","commonSituations":"Migrating from an older config format where the scheme was optional; copying the issuer from a provider doc that shows a bare domain; trailing whitespace or template placeholders (e.g. ${OIDC_ISSUER}) left unsubstituted by env templating.","solutions":["Set the fully-qualified issuer including scheme: oidc.issuer: https://accounts.google.com","Verify the URL with curl — it must serve the OIDC discovery document at <issuer>/.well-known/openid-configuration","Also confirm oidc.client_id and client_secret(_path) are set, as validation fails on those next"],"exampleFix":"# before\noidc:\n  issuer: accounts.google.com\n\n# after\noidc:\n  issuer: https://accounts.google.com\n  client_id: headscale\n  client_secret: ...","handlingStrategy":"validation","validationCode":"// Pre-flight check before server start:\nif u, err := url.Parse(cfg.OIDC.Issuer); err != nil ||\n    (u.Scheme != \"https\" && u.Scheme != \"http\") || u.Host == \"\" {\n    return fmt.Errorf(\"oidc.issuer must be absolute http(s) URL, got %q\", cfg.OIDC.Issuer)\n}","typeGuard":"func isValidOIDCIssuer(s string) bool {\n    u, err := url.Parse(s)\n    return err == nil && (u.Scheme == \"https\" || u.Scheme == \"http\") && u.Host != \"\"\n}","tryCatchPattern":null,"preventionTips":["Always include the scheme in oidc.issuer","Smoke-test the issuer: curl <issuer>/.well-known/openid-configuration must return JSON","Set oidc.client_id and client_secret(_path) at the same time — validation requires all three"],"tags":["config","oidc","validation","startup","go"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}