netbirdio/netbird · error

connector type change not allowed

Error message

connector type change not allowed

What it means

Thrown by Provider.UpdateConnector in the Dex IdP integration (idp/dex/connector.go:100). A stored Dex connector's type is immutable identity: an update may only leave the type field empty (keep current) or set it to the value inferred from the stored connector (inferIdentityProviderType(old.Type, cfg.ID, nil)). Any other type value is rejected before the config overlay is applied.

Source

Thrown at idp/dex/connector.go:100

			p.logger.Warn("failed to parse connector", "id", conn.ID, "error", err)
			continue
		}
		result = append(result, cfg)
	}

	return result, nil
}

// UpdateConnector updates an existing connector in Dex storage.
// It overlays user-mutable config fields (issuer, clientID, clientSecret,
// redirectURI) onto the stored connector config, and updates the connector name
// when cfg.Name is set. Empty fields on cfg leave stored values unchanged, so
// partial updates preserve create-time defaults such as scopes, claimMapping,
// and userIDKey.
func (p *Provider) UpdateConnector(ctx context.Context, cfg *ConnectorConfig) error {
	if err := p.storage.UpdateConnector(ctx, cfg.ID, func(old storage.Connector) (storage.Connector, error) {
		if cfg.Type != "" && cfg.Type != inferIdentityProviderType(old.Type, cfg.ID, nil) {
			return storage.Connector{}, errors.New("connector type change not allowed")
		}

		configData, err := overlayConnectorConfig(old.Config, cfg)
		if err != nil {
			return storage.Connector{}, fmt.Errorf("failed to overlay connector config: %w", err)
		}

		name := cfg.Name
		if name == "" {
			name = old.Name
		}

		return storage.Connector{
			ID:     cfg.ID,
			Type:   old.Type,
			Name:   name,
			Config: configData,
		}, nil

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Omit the type field on updates (empty means unchanged).
  2. To switch provider type, delete the connector and create a new one with the desired type.
  3. If you believe the type is unchanged, compare against inferIdentityProviderType's expected value for the stored connector and send exactly that.

Example fix

// before
err := provider.UpdateConnector(ctx, &ConnectorConfig{
    ID: "netbird", Type: "oidc", // stored connector was created as "google"
})

// after: leave type empty to preserve it
err := provider.UpdateConnector(ctx, &ConnectorConfig{
    ID: "netbird", Type: "", // type unchanged; only overlay fields are updated
    ClientID: "new-client", ClientSecret: "new-secret",
})
Defensive patterns

Strategy: validation

Validate before calling

// Type is immutable: only send it when it matches the stored connector.
if cfg.Type != "" && cfg.Type != expectedStoredType {
    return fmt.Errorf("cannot change connector type %q to %q; recreate the connector instead", expectedStoredType, cfg.Type)
}

Try / catch

if err := provider.UpdateConnector(ctx, cfg); err != nil {
    if strings.Contains(err.Error(), "connector type change not allowed") {
        // recreate with the new type instead of updating
    }
    return err
}

Prevention

When it happens

Trigger: Calling UpdateConnector with cfg.Type set to a different connector type than the stored one, e.g. updating a connector created as 'google' with Type: "oidc", or an editor round-tripping a full config object that carries a stale/edited type field.

Common situations: Management UI edit forms that submit the whole object including a type dropdown the user changed; API clients PUTting a full config where the type was normalized differently (case or alias); attempting to 'convert' a connector in place instead of recreating it.

Related errors


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