{"record":{"id":"6351d486be54a7fe","repo":"router-for-me/CLIProxyAPI","slug":"plugin-sync-response-contains-duplicate-plugin-q","errorCode":null,"errorMessage":"plugin sync response contains duplicate plugin %q","messagePattern":"plugin sync response contains duplicate plugin %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/pluginstore/home_sync.go","lineNumber":71,"sourceCode":"\t}\n\tif r.ExpiresAt.IsZero() {\n\t\treturn fmt.Errorf(\"plugin sync response missing expires_at\")\n\t}\n\tif !now.Before(r.ExpiresAt) {\n\t\treturn fmt.Errorf(\"plugin sync response expired\")\n\t}\n\tseen := make(map[string]struct{}, len(r.Items))\n\tfor index := range r.Items {\n\t\titem := &r.Items[index]\n\t\tif errManifest := item.Manifest.Validate(); errManifest != nil {\n\t\t\treturn fmt.Errorf(\"plugin sync item %d: %w\", index, errManifest)\n\t\t}\n\t\tif errURLs := validatePluginSyncManifestURLs(item.Manifest); errURLs != nil {\n\t\t\treturn fmt.Errorf(\"plugin sync item %d: %w\", index, errURLs)\n\t\t}\n\t\tid := strings.TrimSpace(item.Manifest.ID)\n\t\tif _, exists := seen[id]; exists {\n\t\t\treturn fmt.Errorf(\"plugin sync response contains duplicate plugin %q\", id)\n\t\t}\n\t\tseen[id] = struct{}{}\n\t\tfor authIndex := range item.Auth {\n\t\t\tif errAuth := ValidateResolvedAuthConfig(item.Auth[authIndex]); errAuth != nil {\n\t\t\t\treturn fmt.Errorf(\"plugin sync item %d auth %d: %w\", index, authIndex, errAuth)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validatePluginSyncManifestURLs(manifest Manifest) error {\n\tif manifest.InstallType() != InstallTypeDirect {\n\t\treturn nil\n\t}\n\tplan := NormalizeInstallPlan(manifest.Install)\n\tif len(plan.Artifacts) == 0 {\n\t\treturn fmt.Errorf(\"direct plugin sync manifest requires pinned artifacts\")","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/pluginstore/home_sync.go#L53-L89","documentation":"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.","triggerScenarios":"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).","commonSituations":"Index merging two catalog sources that both include a popular plugin; a copy-paste entry; server-side dedup logic regressing.","solutions":["Deduplicate items by manifest ID in the sync index before serving","If two distinct plugins are meant, give one a different ID","Re-fetch the index after the server-side fix; clear local cache"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"ids := map[string]struct{}{}\nfor i := range resp.Items {\n    id := strings.TrimSpace(resp.Items[i].Manifest.ID)\n    if _, dup := ids[id]; dup {\n        return fmt.Errorf(\"duplicate plugin %q at item %d\", id, i)\n    }\n    ids[id] = struct{}{}\n}","typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"duplicate plugin\") {\n    // dedupe locally by ID (first wins) or reject the payload\n}","preventionTips":["Deduplicate by trimmed ID when building or merging sync indexes","Index CI: assert uniqueness of IDs across the payload","When consuming, keep only the first occurrence rather than failing hard if partial data is acceptable"],"tags":["duplicate","manifest","sync","validation","go"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}