fatedier/frp · error

unmarshal ClientPluginOptions error: %v

Error message

unmarshal ClientPluginOptions error: %v

What it means

DecodeClientPluginOptionsJSON recognized the plugin type and reflect-constructed its options struct, but decodeJSONWithOptions failed to unmarshal the plugin JSON into that struct. The underlying error is wrapped as 'unmarshal ClientPluginOptions error'. It identifies a field-level mismatch inside the plugin options object, not a bad type.

Source

Thrown at pkg/config/v1/decode.go:119

	if isJSONNull(b) {
		return TypedClientPluginOptions{}, nil
	}

	var env typedEnvelope
	if err := jsonx.Unmarshal(b, &env); err != nil {
		return TypedClientPluginOptions{}, err
	}
	if env.Type == "" {
		return TypedClientPluginOptions{}, errors.New("plugin type is empty")
	}

	v, ok := clientPluginOptionsTypeMap[env.Type]
	if !ok {
		return TypedClientPluginOptions{}, fmt.Errorf("unknown plugin type: %s", env.Type)
	}
	optionsStruct := reflect.New(v).Interface().(ClientPluginOptions)
	if err := decodeJSONWithOptions(b, optionsStruct, options); err != nil {
		return TypedClientPluginOptions{}, fmt.Errorf("unmarshal ClientPluginOptions error: %v", err)
	}
	return TypedClientPluginOptions{
		Type:                env.Type,
		ClientPluginOptions: optionsStruct,
	}, nil
}

func DecodeVisitorPluginOptionsJSON(b []byte, options DecodeOptions) (TypedVisitorPluginOptions, error) {
	if isJSONNull(b) {
		return TypedVisitorPluginOptions{}, nil
	}

	var env typedEnvelope
	if err := jsonx.Unmarshal(b, &env); err != nil {
		return TypedVisitorPluginOptions{}, err
	}
	if env.Type == "" {
		return TypedVisitorPluginOptions{}, errors.New("visitor plugin type is empty")

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Follow the wrapped error to the exact field; correct its value type or name for that plugin's schema.
  2. Use current option names (e.g. localPath for static_file) rather than legacy hyphenated keys.
  3. If strict mode is on and the field is obsolete, delete it instead of suppressing the error.

Example fix

// before
{"type": "static_file", "localPath": 8080}

// after
{"type": "static_file", "localPath": "/srv/files"}
Defensive patterns

Strategy: try-catch

Validate before calling

// Optional schema lint for the most common mistake: string where number expected.
func lintPluginOptions(b []byte) error {
	var probe map[string]any
	if err := json.Unmarshal(b, &probe); err != nil {
		return err
	}
	for _, numKey := range []string{"localPort", "remotePort", "bindPort", "httpsPort"} {
		if v, ok := probe[numKey].(string); ok {
			return fmt.Errorf("%s must be a number, got string %q", numKey, v)
		}
	}
	return nil
}

Try / catch

if _, err := v1.DecodeClientPluginOptionsJSON(b, opts); err != nil {
	if strings.Contains(err.Error(), "unmarshal ClientPluginOptions error") {
		// wrapped error names the field; fix type/name in the plugin options object
	}
	return err
}

Prevention

When it happens

Trigger: A plugin options object with a wrong-typed field (e.g. static_file's localPath given as a number, or an array where a string is expected), or unknown fields under strict DecodeOptions. Any DecodeClientPluginOptionsJSON call whose body doesn't match the struct for the named plugin.

Common situations: Plugin option renames across frp versions (INI-era hyphenated names like plugin_local_path vs TOML-era localPath); hand-written JSON with quoted numbers; strict decoding flagging legacy keys.

Related errors


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