juanfont/headscale · error · ErrInvalidUsername
%w, got: %q
Error message
%w, got: %q
What it means
Username.Validate rejected a policy username because it does not contain '@' (the isUser check failed). In headscale's v2 policy, a username alias is always an email-style token; this error fires during policy unmarshalling/validation, before any user lookup happens.
Source
Thrown at hscontrol/policy/v2/types.go:346
if !seen.Contains(s) {
seen.Add(s)
routes = append(routes, s)
}
}
}
return routes
}
// Username is a string that represents a username, it must contain an @.
type Username string
func (u *Username) Validate() error {
if isUser(string(*u)) {
return nil
}
return fmt.Errorf("%w, got: %q", ErrInvalidUsername, *u)
}
func (u *Username) String() string {
return string(*u)
}
// MarshalJSON marshals the Username to JSON.
func (u *Username) MarshalJSON() ([]byte, error) {
return json.Marshal(string(*u))
}
// MarshalJSON marshals the Prefix to JSON.
func (p *Prefix) MarshalJSON() ([]byte, error) {
return json.Marshal(p.String())
}
func (u *Username) UnmarshalJSON(b []byte) error {
*u = Username(strings.Trim(string(b), `"`))View on GitHub (pinned to 565fd254d0)
Solutions
- Use the full email, e.g. 'alice@example.com'.
- If you intended a group, use 'group:engineering'; for a device label use 'tag:name'.
- If your IdP users genuinely lack emails, map them to groups or tags instead of username aliases.
Example fix
// before
{"action": "accept", "src": ["alice"], "dst": ["web:80"]}
// after
{"action": "accept", "src": ["alice@example.com"], "dst": ["web:80"]} Defensive patterns
Strategy: validation
Validate before calling
// Cheap pre-flight: a username alias must contain '@'.
func isValidUsernameAlias(s string) bool { return strings.Contains(s, "@") } Try / catch
if err := policy.Unmarshal(data); err != nil {
if errors.Is(err, v2.ErrInvalidUsername) {
// point the user at the offending token in the message ('got: ...')
}
return err
} Prevention
- Always use full emails as user aliases.
- If names lack a domain, wrap them in groups or tags instead.
- Run 'headscale policy check' (or equivalent loader) before applying ACL changes.
When it happens
Trigger: A grants/tests entry uses "alice", "alice@", or a bare LDAP uid without a domain as src/dst/user. Username.UnmarshalJSON -> Validate fails via isUser(string) == false.
Common situations: Migrating from ACL systems that allow bare usernames; using local-part-only names from an OIDC provider that does not supply emails; typos deleting the domain.
Related errors
- %w: %q
- %w: got %q, must be one of %v
- must be a positive duration
- checkPeriod is only valid with action "check"
- invalid localpart format, must be localpart:*@<domain>
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/a67bf3fbe519815e.
Report an issue: GitHub.