fatedier/frp · error
unknown visitor plugin type
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.
Solutions
- Use only visitor plugin types registered in this frp build (check visitorPluginOptionsTypeMap for your version).
- Remove the plugin block if the visitor doesn't need one.
- 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
- Never assume proxy plugins work as visitor plugins.
- Route plugin decoding through the proxy path unless the type is a registered visitor plugin.
- Keep a version-pinned allowlist of visitor plugin types.
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
- unmarshal visitor plugin error
- unmarshal VisitorPluginOptions error
- decode visitor at index
- unknown plugin type
- unknown visitor type
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/2b77f0a3deeb215f.
Report an issue: GitHub.
Appendix: 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)