juanfont/headscale · error
errOIDCIssuerInvalid
errOIDCIssuerInvalid
Error message
oidc.issuer must be a valid http(s) URL: got %q
What it means
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.
Source
Thrown at hscontrol/types/config.go:384
return nil
}
// validateOIDCConfig validates the OIDC settings, called when oidc.issuer is
// set. It fails fast on a setup that cannot work: an invalid PKCE method, a
// malformed issuer URL (which would otherwise surface as an opaque discovery
// error or, worse, resolve to an unintended provider), or a missing client
// id/secret.
func validateOIDCConfig() error {
err := validatePKCEMethod(viper.GetString("oidc.pkce.method"))
if err != nil {
return err
}
issuer := viper.GetString("oidc.issuer")
u, err := url.Parse(issuer)
if err != nil || (u.Scheme != "https" && u.Scheme != "http") || u.Host == "" {
return fmt.Errorf("%w: got %q", errOIDCIssuerInvalid, issuer)
}
if viper.GetString("oidc.client_id") == "" {
return errOIDCClientIDRequired
}
if viper.GetString("oidc.client_secret") == "" && viper.GetString("oidc.client_secret_path") == "" {
return errOIDCClientSecretRequired
}
return nil
}
// Domain returns the hostname/domain part of the [Config.ServerURL].
// If the [Config.ServerURL] is not a valid URL, it returns the [Config.BaseDomain].
func (c *Config) Domain() string {
u, err := url.Parse(c.ServerURL)
if err != nil {View on GitHub (pinned to 565fd254d0)
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
Example fix
# before oidc: issuer: accounts.google.com # after oidc: issuer: https://accounts.google.com client_id: headscale client_secret: ...
Defensive patterns
Strategy: validation
Validate before calling
// Pre-flight check before server start:
if u, err := url.Parse(cfg.OIDC.Issuer); err != nil ||
(u.Scheme != "https" && u.Scheme != "http") || u.Host == "" {
return fmt.Errorf("oidc.issuer must be absolute http(s) URL, got %q", cfg.OIDC.Issuer)
} Type guard
func isValidOIDCIssuer(s string) bool {
u, err := url.Parse(s)
return err == nil && (u.Scheme == "https" || u.Scheme == "http") && u.Host != ""
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- oidc.client_id is required when oidc.issuer is set
- oidc.client_secret or oidc.client_secret_path is required wh
- pkce.method must be either 'plain' or 'S256'
- no IPv4 or IPv6 prefix configured, minimum one prefix is req
- Fatal config error: %s
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/91e1fbb3210c4717.
Report an issue: GitHub.