fatedier/frp · error

unmarshal visitor plugin error: %v

Error message

unmarshal visitor plugin error: %v

What it means

After the visitor's own fields decode, DecodeVisitorConfigurerJSON decodes an embedded "plugin" object with DecodeVisitorPluginOptionsJSON. If that block's type is not in visitorPluginOptionsTypeMap, or its fields fail to decode, the error is wrapped as 'unmarshal visitor plugin error'. Visitor plugin support is far narrower than proxy plugin support, so this is commonly hit by unsupported plugin types.

Source

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

	}

	var env typedEnvelope
	if err := jsonx.Unmarshal(b, &env); err != nil {
		return nil, err
	}

	configurer := NewVisitorConfigurerByType(VisitorType(env.Type))
	if configurer == nil {
		return nil, fmt.Errorf("unknown visitor type: %s", env.Type)
	}
	if err := decodeJSONWithOptions(b, configurer, options); err != nil {
		return nil, fmt.Errorf("unmarshal VisitorConfig error: %v", err)
	}

	if len(env.Plugin) > 0 && !isJSONNull(env.Plugin) {
		plugin, err := DecodeVisitorPluginOptionsJSON(env.Plugin, options)
		if err != nil {
			return nil, fmt.Errorf("unmarshal visitor plugin error: %v", err)
		}
		configurer.GetBaseConfig().Plugin = plugin
	}
	return configurer, nil
}

func DecodeClientPluginOptionsJSON(b []byte, options DecodeOptions) (TypedClientPluginOptions, error) {
	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")
	}

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Remove the plugin block from the visitor, or use only plugins registered in visitorPluginOptionsTypeMap for this frp build.
  2. Fix the wrapped field-level error if the plugin type is valid but an option has the wrong type/name.
  3. If you need a proxy plugin, move the config to a proxies[] entry instead of a visitor.

Example fix

// before
[[visitors]]
type = "stcp"
[visitors.plugin]
type = "static_file"

// after
[[visitors]]
type = "stcp"
# (plugin removed; static_file is a proxy plugin, not a visitor plugin)
Defensive patterns

Strategy: validation

Validate before calling

// Visitor plugin support is a small allowlist; check before shipping configs.
func hasVisitorPluginSupport(pluginType string) bool {
	// visitorPluginOptionsTypeMap is intentionally tiny; treat anything else as unsupported
	return false // replace with the allowlist for your frp version
}

Type guard

func isVisitorPluginCandidate(t string) bool {
	// enumerate visitorPluginOptionsTypeMap keys for your frp build here
	return false
}

Try / catch

if _, err := v1.DecodeClientConfigJSON(b, opts); err != nil {
	if strings.Contains(err.Error(), "unmarshal visitor plugin error") {
		// remove the plugin block from the visitor or move it to a proxy
	}
	return err
}

Prevention

When it happens

Trigger: A visitors[] entry with a plugin block whose type is anything but the supported visitor plugin (e.g. trying static_file or an arbitrary proxy plugin inside a visitor), or wrong-typed plugin option fields. Triggered via DecodeClientConfigJSON or direct DecodeVisitorConfigurerJSON.

Common situations: Assuming any proxy plugin works inside a visitor; only specific plugins (e.g. the encryption plugin family) are valid for visitors; plugin blocks accidentally left behind when converting a proxy into a visitor.

Related errors


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