router-for-me/CLIProxyAPI · warning
plugin sync response expired
Error message
plugin sync response expired
What it means
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.
Source
Thrown at internal/pluginstore/home_sync.go:58
type PluginSyncResponse struct {
SchemaVersion int `json:"schema_version"`
ExpiresAt time.Time `json:"expires_at"`
Items []PluginSyncItem `json:"items"`
}
func (r *PluginSyncResponse) Validate(now time.Time) error {
if r == nil {
return fmt.Errorf("plugin sync response is nil")
}
if r.SchemaVersion != PluginSyncSchemaVersion {
return fmt.Errorf("unsupported plugin sync schema_version %d", r.SchemaVersion)
}
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)View on GitHub (pinned to 78f0c4079e)
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
Defensive patterns
Strategy: fallback
Validate before calling
if !time.Now().Before(resp.ExpiresAt) {
// expired: refetch before use
resp, err = refetchSync(ctx)
} Try / catch
if err != nil && strings.Contains(err.Error(), "plugin sync response expired") {
// refetch the index; if offline, degrade gracefully without stale installs
} Prevention
- 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'
When it happens
Trigger: 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.
Common situations: Long-running daemon resuming after sleep/hibernate; system clock jumped forward (or is wrong); air-gapped environment where the cached index aged out.
Related errors
- unsupported plugin sync schema_version %d
- plugin sync response missing expires_at
- plugin sync item %d: %w
- plugin sync response contains duplicate plugin %q
- plugin sync item %d auth %d: %w
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/1a89a7a7183379f7.
Report an issue: GitHub.