juanfont/headscale · error · ErrUsernameInvalidChar
username contains invalid character: '%c'
Error message
username contains invalid character: '%c'
What it means
Returned by the username validator in hscontrol/util when a character in a username is not a letter, digit, '-', '.', '_', or a single '@'. It wraps ErrUsernameInvalidChar. This constrains what can safely become part of MagicDNS names and DNS records derived from usernames.
Source
Thrown at hscontrol/util/dns.go:68
}
atCount := 0
for _, char := range username {
switch {
case unicode.IsLetter(char),
unicode.IsDigit(char),
char == '-',
char == '.',
char == '_':
// Valid characters
case char == '@':
atCount++
if atCount > 1 {
return ErrUsernameTooManyAt
}
default:
return fmt.Errorf("%w: '%c'", ErrUsernameInvalidChar, char)
}
}
return nil
}
// generateMagicDNSRootDomains generates a list of DNS entries to be included in [tailcfg.DNSConfig.Routes] in [tailcfg.MapResponse].
// This list of reverse DNS entries instructs the OS on what subnets and domains the Tailscale embedded DNS
// server (listening in 100.100.100.100 udp/53) should be used for.
//
// Tailscale.com includes in the list:
// - the [types.DNSConfig.BaseDomain] of the user
// - the reverse DNS entry for IPv6 (0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa., see below more on IPv6)
// - the reverse DNS entries for the IPv4 subnets covered by the user's `IPPrefix`.
// In the public SaaS this is [64-127].100.in-addr.arpa.
//
// The main purpose of this function is then generating the list of IPv4 entries. For the 100.64.0.0/10, this
// is clear, and could be hardcoded. But we are allowing any range as `IPPrefix`, so we need to find out theView on GitHub (pinned to 565fd254d0)
Solutions
- Pre-sanitize usernames before user creation: map disallowed characters to '-' or drop them
- If using OIDC, configure the provider to supply a compliant preferred_username claim
- Check the exact offending character printed by '%c' and adjust the source-of-truth naming scheme
Example fix
// before username := "ad\\bob" err := util.ValidateUsername(username) // invalid char '\\' // after username := "ad-bob" err := util.ValidateUsername(username)
Defensive patterns
Strategy: validation
Validate before calling
import (
"strings"
"unicode"
)
func normalizeUsername(name string) string {
var b strings.Builder
for _, r := range strings.ToLower(name) {
switch {
case unicode.IsLetter(r) || unicode.IsDigit(r), r == '-', r == '.', r == '_':
b.WriteRune(r)
case r == '@':
b.WriteRune('-') // or keep single @ if your flow allows
default:
b.WriteRune('-')
}
}
return b.String()
} Try / catch
if err := util.ValidateUsername(u); err != nil {
if errors.Is(err, util.ErrUsernameInvalidChar) {
u = normalizeUsername(u) // then re-validate
}
if errors.Is(err, util.ErrUsernameTooManyAt) {
u = strings.Replace(u, "@", "-", -1)
}
} Prevention
- Map OIDC preferred_username through a sanitizer before user creation
- Adopt a house style for usernames: letters, digits, '-', '.', '_', one '@'
- Validate on both write (API/OIDC) and read paths when migrating user stores
When it happens
Trigger: Validating a username containing characters such as ':', '$', spaces, or a second '@' (the second '@' yields ErrUsernameTooManyAt instead). Happens on OIDC login with an identity-provider profile name containing unusual characters, or when creating users via the API.
Common situations: OIDC providers that emit usernames with characters from email display names or external schemas (e.g. 'DOMAIN\\user' or 'user+tag'); migrating user databases with legacy character sets.
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
- username must be at least 2 characters long
- username must start with a letter
- username cannot contain more than one '@'
- username must contain @
- user not found
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/501676d699e9c397.
Report an issue: GitHub.