{"record":{"id":"1a89a7a7183379f7","repo":"router-for-me/CLIProxyAPI","slug":"plugin-sync-response-expired","errorCode":null,"errorMessage":"plugin sync response expired","messagePattern":"plugin sync response expired","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/pluginstore/home_sync.go","lineNumber":58,"sourceCode":"\ntype PluginSyncResponse struct {\n\tSchemaVersion int              `json:\"schema_version\"`\n\tExpiresAt     time.Time        `json:\"expires_at\"`\n\tItems         []PluginSyncItem `json:\"items\"`\n}\n\nfunc (r *PluginSyncResponse) Validate(now time.Time) error {\n\tif r == nil {\n\t\treturn fmt.Errorf(\"plugin sync response is nil\")\n\t}\n\tif r.SchemaVersion != PluginSyncSchemaVersion {\n\t\treturn fmt.Errorf(\"unsupported plugin sync schema_version %d\", r.SchemaVersion)\n\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)","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/pluginstore/home_sync.go#L40-L76","documentation":"The sync response's expires_at is not in the future relative to the 'now' passed to Validate (the check is !now.Before(r.ExpiresAt), so an expiry equal to now also fails). The plugin index is time-limited on purpose so clients cannot keep using stale plugin metadata; once expired the payload is rejected and must be refreshed.","triggerScenarios":"Validate(now) with a cached response whose expires_at has passed — e.g. the process was offline or the cache dir retained an old index beyond its TTL.","commonSituations":"Long-running daemon resuming after sleep/hibernate; system clock jumped forward (or is wrong); air-gapped environment where the cached index aged out.","solutions":["Re-fetch the sync index from the plugin store instead of using the cached copy","Check the system clock (NTP) — a fast clock expires caches prematurely","Clear the local sync cache so a fresh, unexpired payload is downloaded","If operating the sync server, lengthen the expiry window it stamps into responses"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":"if !time.Now().Before(resp.ExpiresAt) {\n    // expired: refetch before use\n    resp, err = refetchSync(ctx)\n}","typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"plugin sync response expired\") {\n    // refetch the index; if offline, degrade gracefully without stale installs\n}","preventionTips":["Refetch the index on expiry instead of trusting cached copies","Keep system clocks NTP-synced so caches do not expire early","Treat expired index as 'no data', never as 'use anyway'"],"tags":["expiry","cache","clock","sync","go"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}