fatedier/frp · error

unmarshal VisitorPluginOptions error: %v

Error message

unmarshal VisitorPluginOptions error: %v

What it means

DecodeVisitorPluginOptionsJSON recognized the visitor plugin type and constructed its options struct, but decodeJSONWithOptions failed to unmarshal the JSON body into it. The underlying error is wrapped as 'unmarshal VisitorPluginOptions error', pointing at a field-level mismatch inside the visitor plugin options object.

Source

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

	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")
	}

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

func DecodeClientConfigJSON(b []byte, options DecodeOptions) (ClientConfig, error) {
	type rawClientConfig struct {
		ClientCommonConfig
		Proxies  []jsonx.RawMessage `json:"proxies,omitempty"`
		Visitors []jsonx.RawMessage `json:"visitors,omitempty"`
	}

	raw := rawClientConfig{}
	if err := decodeJSONWithOptions(b, &raw, options); err != nil {
		return ClientConfig{}, err
	}

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Use the wrapped error to locate the exact field; fix its name or JSON type.
  2. Match the current visitor plugin schema for your frp version.
  3. Remove legacy/unknown keys when strict decoding is enabled.

Example fix

// before
{"type": "my_visitor_plugin", "bindPort": "6000"}

// after
{"type": "my_visitor_plugin", "bindPort": 6000}
Defensive patterns

Strategy: try-catch

Validate before calling

// Lint common numeric fields before decoding visitor plugin options.
func lintVisitorPluginOptions(b []byte) error {
	var probe map[string]any
	if err := json.Unmarshal(b, &probe); err != nil {
		return err
	}
	for _, numKey := range []string{"bindPort", "port"} {
		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.DecodeVisitorPluginOptionsJSON(b, opts); err != nil {
	if strings.Contains(err.Error(), "unmarshal VisitorPluginOptions error") {
		// wrapped error names the field; correct its type or name
	}
	return err
}

Prevention

When it happens

Trigger: A visitor plugin options object containing a wrong-typed field (string vs number) or unknown fields under strict DecodeOptions, passed to DecodeVisitorPluginOptionsJSON with a valid type.

Common situations: Hand-crafted visitor plugin JSON with legacy field names; schema drift between frp versions for visitor plugin options; quoted numbers from YAML pipelines.

Related errors


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