fatedier/frp · error
failed to persist: %w
Error message
failed to persist: %w
What it means
The generic persistence wrapper: any failure from saveToFileUnlocked while committing an Add/Update/Remove operation surfaces here, and the rollback closure undoes the in-memory map change first. This keeps the in-memory store and the file consistent — the mutation is neither applied in memory nor persisted. The real cause is always in the wrapped error chain.
Source
Thrown at pkg/config/source/store.go:183
}
if err := f.Close(); err != nil {
os.Remove(tmpPath)
return fmt.Errorf("failed to close temp file: %w", err)
}
if err := os.Rename(tmpPath, s.config.Path); err != nil {
os.Remove(tmpPath)
return fmt.Errorf("failed to rename temp file: %w", err)
}
return nil
}
func (s *StoreSource) persistOrRollbackUnlocked(rollback func()) error {
if err := s.saveToFileUnlocked(); err != nil {
rollback()
return fmt.Errorf("failed to persist: %w", err)
}
return nil
}
// Store map selectors return the target map for generic helpers.
func proxyStoreEntries(s *StoreSource) map[string]v1.ProxyConfigurer {
return s.proxies
}
func visitorStoreEntries(s *StoreSource) map[string]v1.VisitorConfigurer {
return s.visitors
}
// Store entry helpers share mutation, persistence, and rollback for proxy and visitor maps.
// T is intentionally limited by callers to v1.ProxyConfigurer or v1.VisitorConfigurer.
func addStoreEntry[T any](
s *StoreSource,
entriesFn func(*StoreSource) map[string]T,View on GitHub (pinned to 6c8a8d0a97)
Solutions
- Unwrap the chain to find the root cause: errors.Unwrap / fmt %w already nests it — log err and read the innermost message
- Apply the fix for the underlying failure (free disk space, fix permissions, exclude AV, remount rw)
- Retry the original Add/Update/Remove call — rollback guarantees no duplicate or half-applied state
- If persistence keeps failing, stop making changes and back up the store file before further operations
Example fix
// before: swallowing the cause
if err := store.AddProxy(p); err != nil { log.Fatal("add failed") }
// after: surface the root cause
if err := store.AddProxy(p); err != nil {
log.Fatalf("add failed: %+v", err) // prints full chain e.g. write temp file: no space left on device
} Defensive patterns
Strategy: try-catch
Try / catch
func commit(store *source.StoreSource, op func() error) error {
err := op()
if err == nil {
return nil
}
if errors.Is(err, source.ErrAlreadyExists) || errors.Is(err, source.ErrNotFound) {
return err // semantic conflict: not a persistence failure
}
// persistence failure: root cause is in the chain; state was rolled back
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
log.Printf("store io failure on %s: %v", pathErr.Path, pathErr.Err)
}
return err
} Prevention
- Always log the full error chain (%+v / %w) so the underlying save failure is visible
- Ensure disk space, directory permissions, and mount rw-ness at startup, not at first write
- Because rollback reverts memory, never re-apply a mutation after a persist failure without retrying the same op
When it happens
Trigger: Any of the save-time failures (marshal, MkdirAll, temp-file create/write/sync/close/rename) triggered by AddProxy, AddVisitor, UpdateProxy, UpdateVisitor, RemoveProxy, or RemoveVisitor. E.g. AddProxy on a full disk returns 'failed to persist: failed to write temp file: ...'.
Common situations: Disk full, read-only container FS, AV file locking on Windows, permission changes on the store directory after startup.
Related errors
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/ccce1de294a3092c.
Report an issue: GitHub.