router-for-me/CLIProxyAPI · error

plugin sync response contains duplicate plugin

Error message

plugin sync response contains duplicate plugin %q

What it means

Two items in the sync response carry the same trimmed manifest ID. Validation tracks seen IDs in a map and rejects duplicates because plugin IDs must uniquely identify an install target — otherwise install/upgrade routing would be ambiguous.

Solutions

  1. Deduplicate items by manifest ID in the sync index before serving
  2. If two distinct plugins are meant, give one a different ID
  3. Re-fetch the index after the server-side fix; clear local cache
Defensive patterns

Strategy: validation

Validate before calling

ids := map[string]struct{}{}
for i := range resp.Items {
    id := strings.TrimSpace(resp.Items[i].Manifest.ID)
    if _, dup := ids[id]; dup {
        return fmt.Errorf("duplicate plugin %q at item %d", id, i)
    }
    ids[id] = struct{}{}
}

Try / catch

if err != nil && strings.Contains(err.Error(), "duplicate plugin") {
    // dedupe locally by ID (first wins) or reject the payload
}

Prevention

When it happens

Trigger: Validate on an index that lists the same plugin twice (identical or differently-cased/whitespace-padded IDs after TrimSpace; note the ID comparison itself is exact, so only exact matches after trim collide).

Common situations: Index merging two catalog sources that both include a popular plugin; a copy-paste entry; server-side dedup logic regressing.

Related errors


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/6351d486be54a7fe. Report an issue: GitHub.

Appendix: source

Thrown at internal/pluginstore/home_sync.go:71

	}
	if r.ExpiresAt.IsZero() {
		return fmt.Errorf("plugin sync response missing expires_at")
	}
	if !now.Before(r.ExpiresAt) {
		return fmt.Errorf("plugin sync response expired")
	}
	seen := make(map[string]struct{}, len(r.Items))
	for index := range r.Items {
		item := &r.Items[index]
		if errManifest := item.Manifest.Validate(); errManifest != nil {
			return fmt.Errorf("plugin sync item %d: %w", index, errManifest)
		}
		if errURLs := validatePluginSyncManifestURLs(item.Manifest); errURLs != nil {
			return fmt.Errorf("plugin sync item %d: %w", index, errURLs)
		}
		id := strings.TrimSpace(item.Manifest.ID)
		if _, exists := seen[id]; exists {
			return fmt.Errorf("plugin sync response contains duplicate plugin %q", id)
		}
		seen[id] = struct{}{}
		for authIndex := range item.Auth {
			if errAuth := ValidateResolvedAuthConfig(item.Auth[authIndex]); errAuth != nil {
				return fmt.Errorf("plugin sync item %d auth %d: %w", index, authIndex, errAuth)
			}
		}
	}
	return nil
}

func validatePluginSyncManifestURLs(manifest Manifest) error {
	if manifest.InstallType() != InstallTypeDirect {
		return nil
	}
	plan := NormalizeInstallPlan(manifest.Install)
	if len(plan.Artifacts) == 0 {
		return fmt.Errorf("direct plugin sync manifest requires pinned artifacts")

View on GitHub (pinned to 78f0c4079e)