{"record":{"id":"9a8140c412bf0e72","repo":"fatedier/frp","slug":"plugin-type-is-empty","errorCode":null,"errorMessage":"plugin type is empty","messagePattern":"plugin type is empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/config/v1/decode.go","lineNumber":110,"sourceCode":"\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unmarshal visitor plugin error: %v\", err)\n\t\t}\n\t\tconfigurer.GetBaseConfig().Plugin = plugin\n\t}\n\treturn configurer, nil\n}\n\nfunc DecodeClientPluginOptionsJSON(b []byte, options DecodeOptions) (TypedClientPluginOptions, error) {\n\tif isJSONNull(b) {\n\t\treturn TypedClientPluginOptions{}, nil\n\t}\n\n\tvar env typedEnvelope\n\tif err := jsonx.Unmarshal(b, &env); err != nil {\n\t\treturn TypedClientPluginOptions{}, err\n\t}\n\tif env.Type == \"\" {\n\t\treturn TypedClientPluginOptions{}, errors.New(\"plugin type is empty\")\n\t}\n\n\tv, ok := clientPluginOptionsTypeMap[env.Type]\n\tif !ok {\n\t\treturn TypedClientPluginOptions{}, fmt.Errorf(\"unknown plugin type: %s\", env.Type)\n\t}\n\toptionsStruct := reflect.New(v).Interface().(ClientPluginOptions)\n\tif err := decodeJSONWithOptions(b, optionsStruct, options); err != nil {\n\t\treturn TypedClientPluginOptions{}, fmt.Errorf(\"unmarshal ClientPluginOptions error: %v\", err)\n\t}\n\treturn TypedClientPluginOptions{\n\t\tType:                env.Type,\n\t\tClientPluginOptions: optionsStruct,\n\t}, nil\n}\n\nfunc DecodeVisitorPluginOptionsJSON(b []byte, options DecodeOptions) (TypedVisitorPluginOptions, error) {\n\tif isJSONNull(b) {","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/pkg/config/v1/decode.go#L92-L128","documentation":"Thrown by DecodeClientPluginOptionsJSON in frp's config v1 package when the JSON payload for a client plugin (e.g. an [[proxies]] entry's plugin options) is not JSON null but contains no top-level \"type\" field (or it is an empty string). The decoder uses a typedEnvelope to route the payload to the concrete plugin options struct; without a type there is no mapping to dispatch to. It is a strict config-decoding guard that fires before unknown-type checking.","triggerScenarios":"Calling DecodeClientPluginOptionsJSON(b, options) where b unmarshals to a typedEnvelope with Type == \"\" — e.g. the JSON is {} , {\"type\":\"\"}, or an object whose plugin type key is missing/misnamed (case-sensitive \"type\"). Passing JSON null does NOT trigger it (early return).","commonSituations":"Hand-writing frpc TOML where plugin = \"http_proxy\" is set but the plugin options are given as a bare table without type, converting YAML/JSON configs that drop the type field, or programmatic config generation that serializes plugin options from a struct while forgetting to include Type.","solutions":["Ensure the plugin options JSON object includes a non-empty top-level \"type\" field matching a registered client plugin (e.g. \"http_proxy\", \"unix_domain_socket\").","If the plugin options are intentionally absent, pass JSON null (or omit the section) — the function returns an empty TypedClientPluginOptions for null.","Check for key-name typos: the field is exactly \"type\" (case-sensitive); \"plugin\" or \"Type\" will not be recognized.","If using TOML, verify the plugin type is declared via plugin = \"<type>\" so the loader can inject the envelope type before decoding."],"exampleFix":"# before (frpc.toml)\n[[proxies]]\nname = \"web\"\nplugin = \"\"\n[proxies.pluginOptions]\nlocalAddr = \":80\"\n\n# after\n[[proxies]]\nname = \"web\"\ntype = \"tcp\"\n[proxies.plugin]\ntype = \"http_proxy\"\nlocalAddr = \":80\"","handlingStrategy":"validation","validationCode":"// before calling DecodeClientPluginOptionsJSON\nvar env struct{ Type string `json:\"type\"` }\n_ = jsonx.Unmarshal(b, &env)\nif !isJSONNull(b) && env.Type == \"\" {\n    return fmt.Errorf(\"plugin options JSON must declare a non-empty top-level \\\"type\\\" field\")\n}","typeGuard":"func hasClientPluginType(b []byte) bool {\n    if isJSONNull(b) {\n        return true // null is allowed\n    }\n    var env struct{ Type string `json:\"type\"` }\n    if err := jsonx.Unmarshal(b, &env); err != nil {\n        return false\n    }\n    _, ok := v1.ClientPluginOptionsTypeMap()[env.Type]\n    return env.Type != \"\"\n}","tryCatchPattern":"if _, err := v1.DecodeClientPluginOptionsJSON(raw, v1.DecodeOptions{}); err != nil {\n    if strings.Contains(err.Error(), \"plugin type is empty\") {\n        // config bug: add/repair the \"type\" key, or use null to omit options\n    }\n    return err\n}","preventionTips":["Always emit a top-level \"type\" when serializing plugin options programmatically.","Treat missing plugin options as JSON null, never {}.","Lint frpc TOML files for plugin blocks that lack a type."],"tags":["config","plugin","json","decode","frpc"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}