{"record":{"id":"dd1b284c6ee84889","repo":"XTLS/Xray-core","slug":"empty-domain-rule-list","errorCode":null,"errorMessage":"empty domain rule list","messagePattern":"empty domain rule list","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"common/geodata/domain_matcher.go","lineNumber":63,"sourceCode":"\t\t\tsb.WriteString(\":\")\n\t\t\tsb.WriteString(v.Geosite.Code)\n\t\t\tsb.WriteString(\"@\")\n\t\t\tsb.WriteString(v.Geosite.Attrs)\n\t\t\tsb.WriteString(\",\")\n\t\tdefault:\n\t\t\tpanic(\"unknown domain rule type\")\n\t\t}\n\t}\n\tif !cache {\n\t\treturn \"\"\n\t}\n\treturn sb.String()\n}\n\n// BuildMatcher implements DomainMatcherFactory.\nfunc (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {\n\tif len(rules) == 0 {\n\t\treturn nil, errors.New(\"empty domain rule list\")\n\t}\n\tkey := buildDomainRulesKey(rules)\n\tif key != \"\" {\n\t\tf.Lock()\n\t\tdefer f.Unlock()\n\t\tif g, ok := f.shared.Load(key); ok {\n\t\t\terrors.LogDebug(context.Background(), \"geodata mph domain matcher cache HIT for \", len(rules), \" rules\")\n\t\t\treturn g, nil\n\t\t}\n\t\terrors.LogDebug(context.Background(), \"geodata mph domain matcher cache MISS for \", len(rules), \" rules\")\n\t}\n\tg := strmatcher.NewMphValueMatcher()\n\tfor i, r := range rules {\n\t\tswitch v := r.Value.(type) {\n\t\tcase *DomainRule_Custom:\n\t\t\tm, err := parseDomain(v.Custom)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/XTLS/Xray-core/blob/7d214f8b094f75322fa3990f8aadad1c912f24f5/common/geodata/domain_matcher.go#L45-L81","documentation":"Returned by MphDomainMatcherFactory.BuildMatcher when the rules slice is empty. The minimal perfect hash (MPH) matcher used on desktop platforms requires at least one domain rule to build its strmatcher.MphValueMatcher; an empty set is treated as a caller bug or an upstream routing misconfiguration rather than 'match nothing'.","triggerScenarios":"Calling BuildMatcher with a zero-length []*geodata.DomainRule — e.g. a routing rule whose geosite/geoip resolution produced no entries (mistyped geosite code that matched nothing via fallback), or programmatically building a matcher from an empty filtered rule list.","commonSituations":"Routing configs referencing a geosite code that does not exist in geosite.dat (loader returns nothing); custom code that filters domain rules down to zero and still tries to build a matcher; empty 'domainStrategy' rule lists after JSON merge.","solutions":["Check len(rules) == 0 before building and skip/return a nil-matcher no-op policy for that rule.","Fix the source of emptiness: verify the geosite code exists (geosite:cn etc.) and that rule parsing actually appended entries.","Validate routing config so rules with empty effective domain lists are rejected at load time."],"exampleFix":"// before\nm, err := factory.BuildMatcher(rules) // rules may be empty -> error\n\n// after\nif len(rules) == 0 {\n    return nil // nothing to match; skip this rule\n}\nm, err := factory.BuildMatcher(rules)","handlingStrategy":"validation","validationCode":"if len(rules) == 0 { return nil /* skip rule */ }\nm, err := factory.BuildMatcher(rules)","typeGuard":"func hasDomainRules(rules []*geodata.DomainRule) bool { return len(rules) > 0 }","tryCatchPattern":"m, err := factory.BuildMatcher(rules)\nif err != nil && strings.Contains(err.Error(), \"empty domain rule list\") {\n    return nil // treat as no-op rule\n}","preventionTips":["Check rule counts after geosite resolution, before matcher build","Validate routing configs reject rules whose domain lists resolve empty"],"tags":["geodata","routing","domain-matcher","validation","xray"],"backgroundTag":null,"analyzedSha":"7d214f8b094f75322fa3990f8aadad1c912f24f5","analyzedAt":"2026-08-15T14:26:24.325Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}