fatedier/frp · error

failed to marshal JSON: %w

Error message

failed to marshal JSON: %w

What it means

saveToFileUnlocked could not serialize the in-memory proxies/visitors into JSON (MarshalIndent failed) before writing. With the standard v1 configurers this is nearly impossible — it only fires if a ProxyConfigurer/VisitorConfigurer value carries a field type encoding/json cannot handle (func, chan) or implements MarshalJSON with a bug.

Source

Thrown at pkg/config/source/store.go:140

	return nil
}

func (s *StoreSource) saveToFileUnlocked() error {
	stored := storeData{
		Proxies:  make([]v1.TypedProxyConfig, 0, len(s.proxies)),
		Visitors: make([]v1.TypedVisitorConfig, 0, len(s.visitors)),
	}

	for _, p := range s.proxies {
		stored.Proxies = append(stored.Proxies, v1.TypedProxyConfig{ProxyConfigurer: p})
	}
	for _, v := range s.visitors {
		stored.Visitors = append(stored.Visitors, v1.TypedVisitorConfig{VisitorConfigurer: v})
	}

	data, err := jsonx.MarshalIndent(stored, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal JSON: %w", err)
	}

	dir := filepath.Dir(s.config.Path)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return fmt.Errorf("failed to create directory: %w", err)
	}

	tmpPath := s.config.Path + ".tmp"

	f, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
	if err != nil {
		return fmt.Errorf("failed to create temp file: %w", err)
	}

	if _, err := f.Write(data); err != nil {
		f.Close()
		os.Remove(tmpPath)
		return fmt.Errorf("failed to write temp file: %w", err)

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Unwrap the error chain to see the underlying marshal error (fmt %w preserves it)
  2. If a custom ProxyConfigurer/VisitorConfigurer was added, test its JSON marshaling in isolation: json.Marshal(cfg)
  3. Remove func/chan fields or fix the custom MarshalJSON implementation
  4. If no custom types are involved, report a bug with the full error and the config structs used

Example fix

// before: custom configurer with unsupported field
type MyProxy struct {
    v1.ProxyBaseConfig
    Handler func() `json:"-"` // ok if tagged, fails if not
}

// after: exclude non-serializable fields from JSON
type MyProxy struct {
    v1.ProxyBaseConfig
    Handler func() `json:"-"`
}
Defensive patterns

Strategy: try-catch

Validate before calling

func canMarshal(v any) bool {
	_, err := json.Marshal(v)
	return err == nil
}

// call before AddProxy with a custom configurer:
// if !canMarshal(cfg) { /* reject early */ }

Try / catch

if err := store.AddProxy(cfg); err != nil {
	if strings.Contains(err.Error(), "failed to marshal JSON") {
		// custom configurer is not JSON-serializable; do NOT retry, fix the type
	}
}

Prevention

When it happens

Trigger: Injecting a custom ProxyConfigurer implementation (via AddProxy) whose MarshalJSON returns an error or that contains a func/chan field; a config struct corrupted by an unsupported type assertion. Standard frp types never trigger this.

Common situations: Embedding frp as a library and registering custom proxy types with faulty custom marshalers; extremely rare in normal frp usage.

Related errors


AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15). Data as JSON: /api/errors/49f95dd227284533. Report an issue: GitHub.