{"record":{"id":"204d79aaf7c6be3d","repo":"grpc/grpc-go","slug":"empty-contains-is-not-allowed-in-stringmatcher","errorCode":null,"errorMessage":"empty contains is not allowed in StringMatcher","messagePattern":"empty contains is not allowed in StringMatcher","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/matcher/string_matcher.go","lineNumber":125,"sourceCode":"\t\tif matcherProto.GetPrefix() == \"\" {\n\t\t\treturn StringMatcher{}, errors.New(\"empty prefix is not allowed in StringMatcher\")\n\t\t}\n\t\tmatcher.prefixMatch = newStrPtr(&mt.Prefix, matcher.ignoreCase)\n\tcase *v3matcherpb.StringMatcher_Suffix:\n\t\tif matcherProto.GetSuffix() == \"\" {\n\t\t\treturn StringMatcher{}, errors.New(\"empty suffix is not allowed in StringMatcher\")\n\t\t}\n\t\tmatcher.suffixMatch = newStrPtr(&mt.Suffix, matcher.ignoreCase)\n\tcase *v3matcherpb.StringMatcher_SafeRegex:\n\t\tregex := matcherProto.GetSafeRegex().GetRegex()\n\t\tre, err := CompileSafeRegex(regex)\n\t\tif err != nil {\n\t\t\treturn StringMatcher{}, fmt.Errorf(\"safe_regex matcher %q is invalid\", regex)\n\t\t}\n\t\tmatcher = NewRegexStringMatcher(re)\n\tcase *v3matcherpb.StringMatcher_Contains:\n\t\tif matcherProto.GetContains() == \"\" {\n\t\t\treturn StringMatcher{}, errors.New(\"empty contains is not allowed in StringMatcher\")\n\t\t}\n\t\tmatcher.containsMatch = newStrPtr(&mt.Contains, matcher.ignoreCase)\n\tdefault:\n\t\treturn StringMatcher{}, fmt.Errorf(\"unrecognized string matcher: %+v\", matcherProto)\n\t}\n\treturn matcher, nil\n}\n\n// NewExactStringMatcher creates a string matcher that requires the input string\n// to exactly match the pattern specified here. The match will be case\n// insensitive if ignore_case is true.\nfunc NewExactStringMatcher(pattern string, ignoreCase bool) StringMatcher {\n\treturn StringMatcher{\n\t\texactMatch: newStrPtr(&pattern, ignoreCase),\n\t\tignoreCase: ignoreCase,\n\t}\n}\n","sourceCodeStart":107,"sourceCodeEnd":143,"githubUrl":"https://github.com/grpc/grpc-go/blob/03255a9237b6eb32710f6bc4f2de9a675b99fe36/internal/xds/matcher/string_matcher.go#L107-L143","documentation":"Returned by StringMatcherFromProto (string_matcher.go:124-125) for the Contains match variant when the contains substring is empty. An empty contains substring would match every string and is disallowed; this mirrors the prefix/suffix guards.","triggerScenarios":"A StringMatcher proto whose match pattern is StringMatcher_Contains with Contains=\"\". The switch detects the Contains case, checks GetContains()==\"\", and rejects it.","commonSituations":"Generated config with a contains variable that resolved to empty; a copy-paste of a header matcher where the substring was left blank.","solutions":["Provide a non-empty contains substring.","If matching everything is intended, drop the matcher or use an always-allow permission.","Add a pre-publish validation pass that rejects empty contains matchers."],"exampleFix":"// before: { \"contains\": \"\" }\n// after:  { \"contains\": \"admin\" }","handlingStrategy":"validation","validationCode":"func validContainsStringMatcher(m *v3matcherpb.StringMatcher) error {\n    if m.GetContains() == \"\" {\n        return errors.New(\"contains matcher must have a non-empty substring\")\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Reject empty contains substrings when generating matchers.","Avoid templating matchers from variables that may resolve to empty.","Add a matcher-validation pass in your control plane."],"tags":["xds","matcher","string-matcher","config","validation"],"analyzedSha":"03255a9237b6eb32710f6bc4f2de9a675b99fe36","analyzedAt":"2026-08-07T00:29:34.215Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}