netbirdio/netbird · error

user group name cannot be empty

Error message

user group name cannot be empty

What it means

Returned by ExposeServiceRequest.Validate when the user_groups slice of a peer-expose request contains an empty string. User groups name the distribution groups used for bearer (SSO) token distribution, so an empty name cannot map to any group. Empty entries are almost always client-side parsing artifacts, not intentional input.

Source

Thrown at management/internals/modules/reverseproxy/service/service.go:1499

	switch r.Mode {
	case ModeHTTP, ModeTCP, ModeUDP, ModeTLS:
	default:
		return fmt.Errorf("unsupported mode %q", r.Mode)
	}

	if IsL4Protocol(r.Mode) {
		if r.Pin != "" || r.Password != "" || len(r.UserGroups) > 0 {
			return fmt.Errorf("authentication is not supported for %s mode", r.Mode)
		}
	}

	if r.Pin != "" && !pinRegexp.MatchString(r.Pin) {
		return errors.New("invalid pin: must be exactly 6 digits")
	}

	for _, g := range r.UserGroups {
		if g == "" {
			return errors.New("user group name cannot be empty")
		}
	}

	if r.NamePrefix != "" && !validNamePrefix.MatchString(r.NamePrefix) {
		return fmt.Errorf("invalid name prefix %q: must be lowercase alphanumeric with optional hyphens, 1-32 characters", r.NamePrefix)
	}

	return nil
}

// ToService builds a Service from the expose request.
func (r *ExposeServiceRequest) ToService(accountID, peerID, serviceName string) *Service {
	svc := &Service{
		AccountID: accountID,
		Name:      serviceName,
		Mode:      r.Mode,
		Enabled:   true,
	}

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Filter out empty entries before sending: only non-empty group names in user_groups.
  2. Trim whitespace from each entry when parsing user input, then drop empties.
  3. Verify each name matches an existing distribution group in the account so the bearer config applies.

Example fix

// before
groups := strings.Split("admins,", ",") // ["admins", ""]
req := ExposeServiceRequest{ Mode: "http", Port: 8080, UserGroups: groups }

// after
var groups []string
for _, g := range strings.Split(raw, ",") {
    if g = strings.TrimSpace(g); g != "" {
        groups = append(groups, g)
    }
}
req := ExposeServiceRequest{ Mode: "http", Port: 8080, UserGroups: groups }
Defensive patterns

Strategy: validation

Validate before calling

func cleanUserGroups(raw []string) ([]string, error) {
	var out []string
	for _, g := range raw {
		g = strings.TrimSpace(g)
		if g == "" {
			return nil, errors.New("user group name cannot be empty")
		}
		out = append(out, g)
	}
	return out, nil
}

Type guard

func hasNoEmptyUserGroups(groups []string) bool {
	for _, g := range groups {
		if strings.TrimSpace(g) == "" {
			return false
		}
	}
	return true
}

Try / catch

if err := req.Validate(); err != nil {
	if strings.Contains(err.Error(), "user group name cannot be empty") {
		return respondBadRequest(errors.New("filter empty entries from user_groups (trailing comma?)"))
	}
	return respondBadRequest(err)
}

Prevention

When it happens

Trigger: Building user_groups with strings.Split(input, ",") where the input has a trailing comma ("admins,,devs" or "admins,"); serializing [""] from a form field that was left blank; trimming nothing so " " passes but a true empty string fails.

Common situations: CLI flags parsed as comma-separated lists without filtering empties. JSON payloads constructed by joining optional fields where one was absent. Copy-paste between requests where a group placeholder was never filled in.

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/cecc873ec0ba0a14. Report an issue: GitHub.