{"record":{"id":"73ed7555ea876a56","repo":"fatedier/frp","slug":"exactly-one-visitor-type-block-is-required","errorCode":null,"errorMessage":"exactly one visitor type block is required","messagePattern":"exactly one visitor type block is required","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"client/http/model/visitor_definition.go","lineNumber":32,"sourceCode":"\tSTCP *v1.STCPVisitorConfig `json:\"stcp,omitempty\"`\n\tSUDP *v1.SUDPVisitorConfig `json:\"sudp,omitempty\"`\n\tXTCP *v1.XTCPVisitorConfig `json:\"xtcp,omitempty\"`\n}\n\nfunc (p *VisitorDefinition) Validate(pathName string, isUpdate bool) error {\n\tif strings.TrimSpace(p.Name) == \"\" {\n\t\treturn fmt.Errorf(\"visitor name is required\")\n\t}\n\tif !IsVisitorType(p.Type) {\n\t\treturn fmt.Errorf(\"invalid visitor type: %s\", p.Type)\n\t}\n\tif isUpdate && pathName != \"\" && pathName != p.Name {\n\t\treturn fmt.Errorf(\"visitor name in URL must match name in body\")\n\t}\n\n\t_, blockType, blockCount := p.activeBlock()\n\tif blockCount != 1 {\n\t\treturn fmt.Errorf(\"exactly one visitor type block is required\")\n\t}\n\tif blockType != p.Type {\n\t\treturn fmt.Errorf(\"visitor type block %q does not match type %q\", blockType, p.Type)\n\t}\n\treturn nil\n}\n\nfunc (p *VisitorDefinition) ToConfigurer() (v1.VisitorConfigurer, error) {\n\tblock, _, _ := p.activeBlock()\n\tif block == nil {\n\t\treturn nil, fmt.Errorf(\"exactly one visitor type block is required\")\n\t}\n\n\tcfg := block\n\tcfg.GetBaseConfig().Name = p.Name\n\tcfg.GetBaseConfig().Type = p.Type\n\treturn cfg, nil\n}","sourceCodeStart":14,"sourceCodeEnd":50,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/client/http/model/visitor_definition.go#L14-L50","documentation":"Returned by VisitorDefinition.Validate when the count of populated visitor type blocks (stcp/sudp/xtcp) is not exactly one. Zero blocks (only name/type) or multiple blocks (e.g. both stcp and xtcp set) both fail with this error.","triggerScenarios":"POST/PUT /api/visitors with {\"name\":\"v\",\"type\":\"stcp\"} and no \"stcp\" block; or a body with both \"stcp\" and \"sudp\" objects present.","commonSituations":"Forgetting the nested block; switching visitor type by adding a new block while leaving the old one non-null; JSON merge tools accumulating blocks.","solutions":["Include exactly one block matching the type: \"stcp\", \"sudp\", or \"xtcp\" object.","When changing the visitor type, send a fully replaced definition with only the new block.","Pre-validate the payload: exactly one of the three block keys is non-null."],"exampleFix":"// before\n{\"name\": \"v\", \"type\": \"stcp\"}\n\n// after\n{\"name\": \"v\", \"type\": \"stcp\", \"stcp\": {\"serverName\": \"svc\", \"secretKey\": \"k\", \"bindAddr\": \"127.0.0.1\", \"bindPort\": 6000}}","handlingStrategy":"validation","validationCode":"func countVisitorBlocks(def map[string]any) int {\n    n := 0\n    for _, k := range []string{\"stcp\",\"sudp\",\"xtcp\"} {\n        if def[k] != nil { n++ }\n    }\n    return n\n}\n// require countVisitorBlocks(def) == 1","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Use a fresh struct when changing visitor type so old blocks disappear.","Serialize with omitempty and never merge visitor payloads.","Unit-test generated payloads for exactly one block."],"tags":["api","validation","visitor","frpc-admin"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}