router-for-me/CLIProxyAPI · error

failed to merge metadata: %w

Error message

failed to merge metadata: %w

What it means

Before writing the credential, SaveTokenToFile merges ts.Metadata into the storage struct via misc.MergeMetadata, which round-trips through JSON. This error means that merge failed — typically because the metadata (or an exported field on the token storage) contains a value JSON cannot marshal, such as a channel, func, or cyclic reference.

Source

Thrown at internal/auth/claude/token.go:85

//
// Parameters:
//   - authFilePath: The full path where the token file should be saved
//
// Returns:
//   - error: An error if the operation fails, nil otherwise
func (ts *ClaudeTokenStorage) SaveTokenToFile(authFilePath string) error {
	misc.LogSavingCredentials(authFilePath)
	ts.Type = "claude"

	// Create directory structure if it doesn't exist
	if err := os.MkdirAll(filepath.Dir(authFilePath), 0700); err != nil {
		return fmt.Errorf("failed to create directory: %v", err)
	}

	// Merge metadata using helper
	data, errMerge := misc.MergeMetadata(ts, ts.Metadata)
	if errMerge != nil {
		return fmt.Errorf("failed to merge metadata: %w", errMerge)
	}

	// Create the token file
	f, err := os.Create(authFilePath)
	if err != nil {
		return fmt.Errorf("failed to create token file: %w", err)
	}
	defer func() {
		if errClose := f.Close(); errClose != nil {
			log.Errorf("claude token storage: close token file error: %v", errClose)
		}
	}()

	// Encode and write the token data as JSON
	if err = json.NewEncoder(f).Encode(data); err != nil {
		return fmt.Errorf("failed to write token to file: %w", err)
	}
	return nil

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Inspect ts.Metadata and ensure every value is JSON-serializable (strings, numbers, bools, plain maps/slices).
  2. Keep runtime objects (loggers, hooks) in separate struct fields tagged json:"-" rather than in Metadata.
  3. Log the wrapped marshal error to identify the offending key/type.

Example fix

// before
storage.Metadata["cancel"] = someFunc

// after
storage.Metadata["note"] = "renewed 2026-08-15" // metadata holds JSON values only
Defensive patterns

Strategy: validation

Validate before calling

if _, err := json.Marshal(ts.Metadata); err != nil {
    return fmt.Errorf("metadata must be JSON-serializable: %w", err)
}
err := ts.SaveTokenToFile(path)

Type guard

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

Try / catch

if err := ts.SaveTokenToFile(p); err != nil && strings.Contains(err.Error(), "merge metadata") {
    ts.Metadata = map[string]any{} // strip non-serializable extras and retry
    err = ts.SaveTokenToFile(p)
}

Prevention

When it happens

Trigger: A ClaudeTokenStorage whose Metadata map was populated with non-serializable values (func, chan, complex, or a cyclic struct) before SaveTokenToFile is called; metadata injected programmatically by an embedding SDK consumer rather than from parsed OAuth JSON.

Common situations: SDK/embedding users attaching runtime objects (loggers, callbacks) into Metadata instead of plain JSON values; custom builds that add struct fields of non-JSON types to ClaudeTokenStorage; virtually never happens in the stock CLI flow where metadata comes from JSON responses.

Related errors


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/78c261191bcd8b42. Report an issue: GitHub.