{"record":{"id":"67a33f2abd4fd801","repo":"fatedier/frp","slug":"visitor-plugin-type-is-empty","errorCode":null,"errorMessage":"visitor plugin type is empty","messagePattern":"visitor plugin type is empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/config/v1/decode.go","lineNumber":137,"sourceCode":"\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) {\n\t\treturn TypedVisitorPluginOptions{}, nil\n\t}\n\n\tvar env typedEnvelope\n\tif err := jsonx.Unmarshal(b, &env); err != nil {\n\t\treturn TypedVisitorPluginOptions{}, err\n\t}\n\tif env.Type == \"\" {\n\t\treturn TypedVisitorPluginOptions{}, errors.New(\"visitor plugin type is empty\")\n\t}\n\n\tv, ok := visitorPluginOptionsTypeMap[env.Type]\n\tif !ok {\n\t\treturn TypedVisitorPluginOptions{}, fmt.Errorf(\"unknown visitor plugin type: %s\", env.Type)\n\t}\n\toptionsStruct := reflect.New(v).Interface().(VisitorPluginOptions)\n\tif err := decodeJSONWithOptions(b, optionsStruct, options); err != nil {\n\t\treturn TypedVisitorPluginOptions{}, fmt.Errorf(\"unmarshal VisitorPluginOptions error: %v\", err)\n\t}\n\treturn TypedVisitorPluginOptions{\n\t\tType:                 env.Type,\n\t\tVisitorPluginOptions: optionsStruct,\n\t}, nil\n}\n\nfunc DecodeClientConfigJSON(b []byte, options DecodeOptions) (ClientConfig, error) {\n\ttype rawClientConfig struct {","sourceCodeStart":119,"sourceCodeEnd":155,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/pkg/config/v1/decode.go#L119-L155","documentation":"Thrown by DecodeVisitorPluginOptionsJSON when decoding visitor plugin options JSON that is not null but whose top-level \"type\" is empty. Visitor plugins (used by stcp/sudp/xtcp visitors for things like TLS termination or secret-key handling) go through the same typedEnvelope mechanism as client plugins, dispatched via visitorPluginOptionsTypeMap; a missing type makes dispatch impossible. This is a strict decoding guard fired before the unknown-visitor-plugin-type check.","triggerScenarios":"Calling DecodeVisitorPluginOptionsJSON(b, options) where the JSON object lacks a \"type\" key or has type: \"\" — e.g. a [[visitors]] section in frpc config whose plugin options table was written without a type, or where null was expected but an empty object {} was supplied.","commonSituations":"Writing an stcp/xtcp visitor entry and omitting or blanking the plugin type, migrating configs from a format that represented 'no plugin' as {} instead of null, or programmatic generation that always emits an options object even when there is no visitor plugin.","solutions":["Add a valid non-empty \"type\" to the visitor plugin options JSON if a visitor plugin is intended.","If no visitor plugin options are needed, remove the options object or set it to JSON null — the decoder returns an empty TypedVisitorPluginOptions without error.","Verify the exact key spelling \"type\" (case-sensitive) and that it is at the top level of the options object, not nested."],"exampleFix":"# before\n[[visitors]]\nname = \"stcpv\"\ntype = \"stcp\"\nsecretKey = \"s\"\nbindAddr = \"127.0.0.1\"\nbindPort = 9000\n[visitors.plugin]\n# type missing\n\n# after\n[visitors.plugin]\ntype = \"https2http\"\nlocalAddr = \":8443\"","handlingStrategy":"validation","validationCode":"// before calling DecodeVisitorPluginOptionsJSON\nvar env struct{ Type string `json:\"type\"` }\n_ = jsonx.Unmarshal(b, &env)\nif !isJSONNull(b) && env.Type == \"\" {\n    return fmt.Errorf(\"visitor plugin options must declare a non-empty \\\"type\\\" or be null\")\n}","typeGuard":"func hasVisitorPluginType(b []byte) bool {\n    if isJSONNull(b) {\n        return true\n    }\n    var env struct{ Type string `json:\"type\"` }\n    if err := jsonx.Unmarshal(b, &env); err != nil {\n        return false\n    }\n    return env.Type != \"\"\n}","tryCatchPattern":"if _, err := v1.DecodeVisitorPluginOptionsJSON(raw, v1.DecodeOptions{}); err != nil {\n    if strings.Contains(err.Error(), \"visitor plugin type is empty\") {\n        // fix the config: add \"type\" to visitor plugin options or set them to null\n    }\n    return err\n}","preventionTips":["Omit the visitor plugin block entirely when no plugin options are needed.","Never represent 'no options' as an empty JSON object.","Validate generated configs in CI with frpc verify before deploy."],"tags":["config","visitor","plugin","json","decode","frpc"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}