fatedier/frp · error

unknown plugin type: %s

Error message

unknown plugin type: %s

What it means

DecodeClientPluginOptionsJSON resolves the plugin options struct from clientPluginOptionsTypeMap keyed by the envelope "type". An empty type is rejected earlier ('plugin type is empty'), so this error means a non-empty type string that has no registered options struct — an unknown or unsupported client plugin type. Raised before any plugin field decoding.

Source

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

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

	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 {

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Use the exact documented plugin type identifier (snake_case, e.g. static_file, http2https, socks5).
  2. Upgrade (or align) the frp build so the plugin is registered in clientPluginOptionsTypeMap.
  3. Check for stray whitespace or capitalization in the type string.

Example fix

// before
{"type": "Static_File", "localPath": "/srv"}

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

Strategy: validation

Validate before calling

// Validate plugin type against the registry before decoding.
func validClientPluginName(t string) error {
	if strings.TrimSpace(t) == "" {
		return fmt.Errorf("plugin type is empty")
	}
	if !isKnownClientPlugin(strings.TrimSpace(t)) {
		return fmt.Errorf("unknown plugin type %q", t)
	}
	return nil
}

Type guard

func isKnownClientPlugin(t string) bool {
	switch t {
	case "static_file", "http2https", "https2http", "unix_domain_socket", "socks5", "proxy_protocol":
		return true
	}
	return false
}

Try / catch

if _, err := v1.DecodeClientPluginOptionsJSON(b, opts); err != nil {
	if strings.Contains(err.Error(), "unknown plugin type") {
		// correct the type string or upgrade frp to a build that registers it
	}
	return err
}

Prevention

When it happens

Trigger: Calling DecodeClientPluginOptionsJSON with {"type":"webdav2",...} where webdav2 is not registered, or a type with whitespace/case drift like "Static_File". Any non-empty plugin type absent from the registry for this binary.

Common situations: Plugins added in newer frp releases used against an older frp; community forks with extra plugins; typos or casing differences in plugin names in generated configs.

Related errors


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