fatedier/frp · error

unknown visitor plugin type: %s

Error message

unknown visitor plugin type: %s

What it means

DecodeVisitorPluginOptionsJSON resolves the visitor plugin options struct from visitorPluginOptionsTypeMap keyed by the envelope "type". Empty types are rejected earlier ('visitor plugin type is empty'); this error means a non-empty type with no registered visitor plugin options struct. The visitor plugin registry is much smaller than the client plugin registry, so most proxy plugin names fail here.

Source

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

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

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

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Use only visitor plugin types registered in this frp build (check visitorPluginOptionsTypeMap for your version).
  2. Remove the plugin block if the visitor doesn't need one.
  3. Move plugin-based tunneling into a proxies[] entry where the full client plugin set is available.

Example fix

// before
{"type": "static_file", "localPath": "/srv"}  // passed to DecodeVisitorPluginOptionsJSON

// after
// pass it to DecodeClientPluginOptionsJSON from a proxy instead:
{"type": "tcp", "plugin": {"type": "static_file", "localPath": "/srv"}}
Defensive patterns

Strategy: validation

Validate before calling

// Guard: most plugin types are proxy-only; verify before calling the visitor decoder.
func canDecodeVisitorPlugin(t string) error {
	if !isVisitorPluginCandidate(strings.TrimSpace(t)) {
		return fmt.Errorf("type %q is not a visitor plugin; use DecodeClientPluginOptionsJSON on a proxy instead", t)
	}
	return nil
}

Type guard

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

Try / catch

if _, err := v1.DecodeVisitorPluginOptionsJSON(b, opts); err != nil {
	if strings.Contains(err.Error(), "unknown visitor plugin type") {
		// move the plugin to a proxy or drop it
	}
	return err
}

Prevention

When it happens

Trigger: Calling DecodeVisitorPluginOptionsJSON with a type such as "static_file", "socks5", or any other plugin registered only in clientPluginOptionsTypeMap, or a misspelled visitor plugin name.

Common situations: Reusing a proxy's plugin block inside a visitor config; assuming plugin parity between proxies and visitors; fork-specific visitor plugins not present in the running build.

Related errors


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