{"record":{"id":"443b71f3e0bb614a","repo":"XTLS/Xray-core","slug":"portal-tag-is-empty","errorCode":null,"errorMessage":"portal tag is empty","messagePattern":"portal tag is empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/reverse/portal.go","lineNumber":33,"sourceCode":"\t\"github.com/xtls/xray-core/common/signal\"\n\t\"github.com/xtls/xray-core/common/task\"\n\t\"github.com/xtls/xray-core/features/outbound\"\n\t\"github.com/xtls/xray-core/transport\"\n\t\"github.com/xtls/xray-core/transport/pipe\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\ntype Portal struct {\n\tohm    outbound.Manager\n\ttag    string\n\tdomain string\n\tpicker *StaticMuxPicker\n\tclient *mux.ClientManager\n}\n\nfunc NewPortal(config *PortalConfig, ohm outbound.Manager) (*Portal, error) {\n\tif config.Tag == \"\" {\n\t\treturn nil, errors.New(\"portal tag is empty\")\n\t}\n\n\tif config.Domain == \"\" {\n\t\treturn nil, errors.New(\"portal domain is empty\")\n\t}\n\n\tpicker, err := NewStaticMuxPicker()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Portal{\n\t\tohm:    ohm,\n\t\ttag:    config.Tag,\n\t\tdomain: config.Domain,\n\t\tpicker: picker,\n\t\tclient: &mux.ClientManager{\n\t\t\tPicker: picker,","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/XTLS/Xray-core/blob/7d214f8b094f75322fa3990f8aadad1c912f24f5/app/reverse/portal.go#L15-L51","documentation":"Reverse-proxy portal constructor validation: a Portal (the component on the public/outer side that accepts bridge connections and dispatches traffic into them) must have a tag, since the portal registers itself as an outbound handler under that tag so routing can send traffic to it. NewPortal fails fast when PortalConfig.Tag is empty.","triggerScenarios":"A reverse config portals[] entry without a \"tag\" field during Portal construction; the portal later calls ohm.RemoveHandler with this same tag on Close, so an empty tag would corrupt outbound manager state.","commonSituations":"Hand-authored reverse tunnel configs missing the portal tag; field-name casing mistakes when hand-translating protobuf JSON; editing examples and removing the tag line. Instance startup fails immediately.","solutions":["Add a unique \"tag\" to the portals[] entry (e.g. 'portal').","Make sure routing rules/balancers that should send traffic through the tunnel reference this exact tag.","Run xray -test on the config to catch it before deployment."],"exampleFix":"// before\n\"reverse\": { \"portals\": [ { \"domain\": \"svc.reverse.internal\" } ] }\n\n// after\n\"reverse\": { \"portals\": [ { \"tag\": \"portal\", \"domain\": \"svc.reverse.internal\" } ] }","handlingStrategy":"validation","validationCode":"for _, p := range cfg.Reverse.Portals {\n    if p.Tag == \"\" { return errors.New(\"reverse config: portal tag is empty\") }\n}","typeGuard":"func validPortalConfig(p PortalConfig) bool { return p.Tag != \"\" }","tryCatchPattern":"if _, err := reverse.NewPortal(cfg, ohm); err != nil {\n    if strings.Contains(err.Error(), \"portal tag is empty\") {\n        return fmt.Errorf(\"reverse.portals[%d]: tag required\", i)\n    }\n    return err\n}","preventionTips":["Emit tag+domain together for portals in templates","Cross-check the portal tag against routing rules that reference it","Prefer copy-editing a known-good reverse example"],"tags":["go","xray","reverse-proxy","config","validation"],"backgroundTag":null,"analyzedSha":"7d214f8b094f75322fa3990f8aadad1c912f24f5","analyzedAt":"2026-08-15T14:26:24.325Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}