juanfont/headscale · error · ErrMultipleUsersFound
%w: token %q found: %s
Error message
%w: token %q found: %s
What it means
Username.resolve matched more than one user for the same token — the username or email is ambiguous across the database. headscale refuses to guess, so resolution fails with ErrMultipleUsersFound and lists the matching users.
Source
Thrown at hscontrol/policy/v2/types.go:413
uTrimmed := strings.TrimSuffix(u.String(), "@")
for _, user := range users {
if user.ProviderIdentifier.Valid && user.ProviderIdentifier.String == uTrimmed {
// Prioritize ProviderIdentifier match and exit early
return user, nil
}
if user.Email == uTrimmed || user.Name == uTrimmed {
potentialUsers = append(potentialUsers, user)
}
}
if len(potentialUsers) == 0 {
return types.User{}, fmt.Errorf("%w: token %q", ErrUserNotFound, u.String())
}
if len(potentialUsers) > 1 {
return types.User{}, fmt.Errorf("%w: token %q found: %s", ErrMultipleUsersFound, u.String(), potentialUsers.String())
}
return potentialUsers[0], nil
}
func (u *Username) Resolve(_ *Policy, users types.Users, nodes views.Slice[types.NodeView]) (ResolvedAddresses, error) {
return newResolvedAddresses(u.resolve(nil, users, nodes))
}
func (u *Username) resolve(_ *Policy, users types.Users, nodes views.Slice[types.NodeView]) (*netipx.IPSet, error) {
var (
ips netipx.IPSetBuilder
errs []error
)
user, err := u.resolveUser(users)
if err != nil {
errs = append(errs, err)View on GitHub (pinned to 565fd254d0)
Solutions
- Inspect the 'found:' list in the error and delete or rename the duplicate users ('headscale users rename' / 'headscale users destroy').
- Reference one of the users by its unique ProviderIdentifier instead of the shared email.
- Fix the IdP or registration flow that created duplicates, then deduplicate.
- In tests, ensure each seeded user has a distinct email and name.
Example fix
# before: two users both named alice headscale users list # alice (id 1), alice (id 2) # after: keep one, rename the other headscale users rename -i 2 alice-2
Defensive patterns
Strategy: try-catch
Validate before calling
// Detect ambiguous tokens before compile.
matches := 0
for _, u := range users {
if u.Email == token || u.Name == token { matches++ }
}
if matches > 1 { return fmt.Errorf("token %q matches %d users", token, matches) } Try / catch
user, err := username.Resolve(pol, users, nodes)
if err != nil {
if errors.Is(err, v2.ErrMultipleUsersFound) {
// error message lists the duplicates; deduplicate via users rename/destroy
}
return err
} Prevention
- Enforce unique emails and usernames when provisioning users.
- After IdP migrations, audit for duplicate rows before loading policies.
- Reference ProviderIdentifier (unique) rather than shared emails where possible.
When it happens
Trigger: Two users share the same Name (e.g. both named 'alice' under different providers) or the same Email, and the policy references that token. len(potentialUsers) > 1 after the scan in Username.resolve.
Common situations: OIDC migration leaving duplicate rows (one created by CLI, one by IdP) with identical emails; multiple providers feeding one headscale; test fixtures that seed users with colliding names.
Related errors
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/4f05f5ddcb54896c.
Report an issue: GitHub.