{"record":{"id":"a1d7a5f5a133ba51","repo":"XTLS/Xray-core","slug":"portal-domain-is-empty","errorCode":null,"errorMessage":"portal domain is empty","messagePattern":"portal domain is empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/reverse/portal.go","lineNumber":37,"sourceCode":"\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,\n\t\t},\n\t}, nil\n}\n","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/XTLS/Xray-core/blob/7d214f8b094f75322fa3990f8aadad1c912f24f5/app/reverse/portal.go#L19-L55","documentation":"Reverse-proxy portal constructor validation, checked right after the tag: the portal needs the domain under which tunneled targets are addressed, because HandleConnection matches the outbound target against p.domain (isDomain) to decide whether to spin up a mux client for this connection. An empty domain makes that matching meaningless, so NewPortal rejects it.","triggerScenarios":"A portals[] entry in the reverse config with a missing or empty \"domain\"; fires during instance start after the tag check succeeds.","commonSituations":"Same family as the other reverse validation errors: omitted domain when editing configs, mismatched field casing, or a domain intended only on the bridge side. Startup aborts with this error.","solutions":["Set \"domain\" on the portal entry to the same value used by the corresponding bridge.","Keep bridge and portal domain pairs in sync when editing either side of the tunnel.","Use a clearly reserved internal domain suffix for reverse traffic to avoid intercepting real hostnames."],"exampleFix":"// before\n\"reverse\": { \"portals\": [ { \"tag\": \"portal\" } ] }\n\n// after\n\"reverse\": { \"portals\": [ { \"tag\": \"portal\", \"domain\": \"svc.reverse.internal\" } ] }","handlingStrategy":"validation","validationCode":"for _, p := range cfg.Reverse.Portals {\n    if p.Domain == \"\" { return errors.New(\"reverse config: portal domain is empty\") }\n}","typeGuard":"func validPortalDomain(p PortalConfig) bool { return p.Domain != \"\" }","tryCatchPattern":"if _, err := reverse.NewPortal(cfg, ohm); err != nil {\n    if strings.Contains(err.Error(), \"portal domain is empty\") {\n        return fmt.Errorf(\"reverse.portals[%d]: domain required\", i)\n    }\n    return err\n}","preventionTips":["Keep portal.domain and the matching bridge.domain identical in config","Use a dedicated reserved domain suffix for reverse tunnels","Run -test validation before deploying reverse configs"],"tags":["go","xray","reverse-proxy","config","validation"],"backgroundTag":null,"analyzedSha":"7d214f8b094f75322fa3990f8aadad1c912f24f5","analyzedAt":"2026-08-15T14:26:24.325Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}