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

  1. Unwrap the chain to find the root cause: errors.Unwrap / fmt %w already nests it — log err and read the innermost message
  2. Apply the fix for the underlying failure (free disk space, fix permissions, exclude AV, remount rw)
  3. Retry the original Add/Update/Remove call — rollback guarantees no duplicate or half-applied state
  4. 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

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.