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

  1. Inspect the 'found:' list in the error and delete or rename the duplicate users ('headscale users rename' / 'headscale users destroy').
  2. Reference one of the users by its unique ProviderIdentifier instead of the shared email.
  3. Fix the IdP or registration flow that created duplicates, then deduplicate.
  4. 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

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.