From 49132e828a0b49a25649f5b97065c8bb371b5483 Mon Sep 17 00:00:00 2001 From: Cezar Sa Espinola Date: Mon, 15 Jun 2026 10:06:24 -0300 Subject: [PATCH 1/3] Add test cases demonstrating input order dependency in rule precedence --- .../routeutils/route_rule_precedence_test.go | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/pkg/gateway/routeutils/route_rule_precedence_test.go b/pkg/gateway/routeutils/route_rule_precedence_test.go index 9f137e023a..2875d4d54a 100644 --- a/pkg/gateway/routeutils/route_rule_precedence_test.go +++ b/pkg/gateway/routeutils/route_rule_precedence_test.go @@ -151,6 +151,95 @@ func Test_SortAllRulesByPrecedence(t *testing.T) { }, }, } + + httpRouteOneDotHost := &httpRouteDescription{ + route: &gwv1.HTTPRoute{ + ObjectMeta: v1.ObjectMeta{Name: "a-host", Namespace: "ns"}, + Spec: gwv1.HTTPRouteSpec{Hostnames: []gwv1.Hostname{"zzz.com"}}, + }, + rules: []RouteRule{ + &convertedHTTPRouteRule{rule: &gwv1.HTTPRouteRule{ + Matches: []gwv1.HTTPRouteMatch{{ + Path: &gwv1.HTTPPathMatch{Value: new("/"), Type: new(gwv1.PathMatchPathPrefix)}, + }}, + }}, + }, + } + + httpRouteNoHost := &httpRouteDescription{ + route: &gwv1.HTTPRoute{ + ObjectMeta: v1.ObjectMeta{Name: "b-nohost", Namespace: "ns"}, + }, + rules: []RouteRule{ + &convertedHTTPRouteRule{rule: &gwv1.HTTPRouteRule{ + Matches: []gwv1.HTTPRouteMatch{{ + Path: &gwv1.HTTPPathMatch{Value: new("/"), Type: new(gwv1.PathMatchPathPrefix)}, + }}, + }}, + }, + } + + httpRouteThreeDotsHost := &httpRouteDescription{ + route: &gwv1.HTTPRoute{ + ObjectMeta: v1.ObjectMeta{Name: "c-host", Namespace: "ns"}, + Spec: gwv1.HTTPRouteSpec{Hostnames: []gwv1.Hostname{"a.b.c.com"}}, + }, + rules: []RouteRule{ + &convertedHTTPRouteRule{rule: &gwv1.HTTPRouteRule{ + Matches: []gwv1.HTTPRouteMatch{{ + Path: &gwv1.HTTPPathMatch{Value: new("/"), Type: new(gwv1.PathMatchPathPrefix)}, + }}, + }}, + }, + } + + httpRouteNoHostWithPost := &httpRouteDescription{ + route: &gwv1.HTTPRoute{ + ObjectMeta: v1.ObjectMeta{Name: "b-nohost", Namespace: "ns"}, + }, + rules: []RouteRule{ + &convertedHTTPRouteRule{rule: &gwv1.HTTPRouteRule{ + Matches: []gwv1.HTTPRouteMatch{ + { + Method: new(gwv1.HTTPMethodPost), + Path: &gwv1.HTTPPathMatch{Value: new("/"), Type: new(gwv1.PathMatchPathPrefix)}, + }, + }, + }}, + }, + } + + makeRulePrecedenceForRouteDescription := func(desc *httpRouteDescription, fn func(*RulePrecedence)) RulePrecedence { + rule := RulePrecedence{ + CommonRulePrecedence: CommonRulePrecedence{ + RouteNamespacedName: desc.route.Namespace + "/" + desc.route.Name, + Hostnames: func() []string { + hostnames := make([]string, len(desc.route.Spec.Hostnames)) + for i := range desc.route.Spec.Hostnames { + hostnames[i] = string(desc.route.Spec.Hostnames[i]) + } + return hostnames + }(), + RouteDescriptor: desc, + Rule: desc.rules[0], + RuleIndexInRoute: 0, + MatchIndexInRule: 0, + RouteCreateTimestamp: desc.GetRouteCreateTimestamp(), + }, + HttpSpecificRulePrecedenceFactor: &HttpSpecificRulePrecedenceFactor{ + PathType: 2, + PathLength: 1, + }, + HTTPMatch: &gwv1.HTTPRouteMatch{ + Path: &gwv1.HTTPPathMatch{Value: new("/"), Type: new(gwv1.PathMatchPathPrefix)}, + }, + } + if fn != nil { + fn(&rule) + } + return rule + } + testCases := []struct { name string input []RouteDescriptor @@ -393,6 +482,64 @@ func Test_SortAllRulesByPrecedence(t *testing.T) { }, }, }, + { + name: "input order independent result for mixed hostname and no-hostname rules - input order A", + input: []RouteDescriptor{ + httpRouteOneDotHost, // zzz.com (1 dot) + httpRouteNoHost, // no hostname + httpRouteThreeDotsHost, // a.b.c.com (3 dots) + }, + output: []RulePrecedence{ + makeRulePrecedenceForRouteDescription(httpRouteNoHost, nil), // no hostname + makeRulePrecedenceForRouteDescription(httpRouteThreeDotsHost, nil), // a.b.c.com (3 dots) + makeRulePrecedenceForRouteDescription(httpRouteOneDotHost, nil), // zzz.com (1 dot) + }, + }, + { + name: "input order independent result for mixed hostname and no-hostname rules - input order B", + input: []RouteDescriptor{ + httpRouteThreeDotsHost, // a.b.c.com (3 dots) + httpRouteNoHost, // no hostname + httpRouteOneDotHost, // zzz.com (1 dot) + }, + output: []RulePrecedence{ + makeRulePrecedenceForRouteDescription(httpRouteNoHost, nil), // no hostname + makeRulePrecedenceForRouteDescription(httpRouteThreeDotsHost, nil), // a.b.c.com (3 dots) + makeRulePrecedenceForRouteDescription(httpRouteOneDotHost, nil), // zzz.com (1 dot) + }, + }, + { + name: "input order independent result for mixed hostname and no-hostname rules, with method - input order A", + input: []RouteDescriptor{ + httpRouteOneDotHost, // zzz.com (1 dot) + httpRouteNoHostWithPost, // no hostname with POST + httpRouteThreeDotsHost, // a.b.c.com (3 dots) + }, + output: []RulePrecedence{ + makeRulePrecedenceForRouteDescription(httpRouteNoHostWithPost, func(r *RulePrecedence) { // no hostname with POST + r.HTTPMatch.Method = new(gwv1.HTTPMethodPost) + r.HttpSpecificRulePrecedenceFactor.HasMethod = true + }), + makeRulePrecedenceForRouteDescription(httpRouteThreeDotsHost, nil), // a.b.c.com (3 dots) + makeRulePrecedenceForRouteDescription(httpRouteOneDotHost, nil), // zzz.com (1 dot) + }, + }, + { + name: "input order independent result for mixed hostname and no-hostname rules, with method - input order B", + input: []RouteDescriptor{ + httpRouteThreeDotsHost, // a.b.c.com (3 dots) + httpRouteNoHostWithPost, // no hostname with POST + httpRouteOneDotHost, // zzz.com (1 dot) + }, + output: []RulePrecedence{ + makeRulePrecedenceForRouteDescription(httpRouteNoHostWithPost, func(r *RulePrecedence) { // no hostname with POST + r.HTTPMatch.Method = new(gwv1.HTTPMethodPost) + r.HttpSpecificRulePrecedenceFactor.HasMethod = true + }), + makeRulePrecedenceForRouteDescription(httpRouteThreeDotsHost, nil), // a.b.c.com (3 dots) + makeRulePrecedenceForRouteDescription(httpRouteOneDotHost, nil), // zzz.com (1 dot) + }, + }, } for _, tc := range testCases { From 61ef2a8c975a1a9ff5cae382c90fde26e0b783ff Mon Sep 17 00:00:00 2001 From: Cezar Sa Espinola Date: Mon, 15 Jun 2026 11:31:55 -0300 Subject: [PATCH 2/3] Tentative fix for rule precedence determinism --- pkg/gateway/routeutils/route_rule_precedence.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/gateway/routeutils/route_rule_precedence.go b/pkg/gateway/routeutils/route_rule_precedence.go index 48dd6ad0dc..45667970bb 100644 --- a/pkg/gateway/routeutils/route_rule_precedence.go +++ b/pkg/gateway/routeutils/route_rule_precedence.go @@ -183,7 +183,11 @@ func getHostnameListPrecedenceOrder(hostnameListOne, hostnameListTwo []string) i return precedence } } - // can not complete tie breaking at hostname level + if len(hostnameListOne) < len(hostnameListTwo) { + return -1 + } else if len(hostnameListOne) > len(hostnameListTwo) { + return 1 + } return 0 } From 47648bb4c0d088ae2a3137f22ffbcd04238b4233 Mon Sep 17 00:00:00 2001 From: Cezar Sa Espinola Date: Mon, 15 Jun 2026 12:21:59 -0300 Subject: [PATCH 3/3] Better fix for rule precedence with different hostname match sizes --- pkg/gateway/routeutils/route_rule_precedence.go | 9 ++++----- pkg/gateway/routeutils/route_rule_precedence_test.go | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pkg/gateway/routeutils/route_rule_precedence.go b/pkg/gateway/routeutils/route_rule_precedence.go index 45667970bb..906202dc71 100644 --- a/pkg/gateway/routeutils/route_rule_precedence.go +++ b/pkg/gateway/routeutils/route_rule_precedence.go @@ -183,11 +183,7 @@ func getHostnameListPrecedenceOrder(hostnameListOne, hostnameListTwo []string) i return precedence } } - if len(hostnameListOne) < len(hostnameListTwo) { - return -1 - } else if len(hostnameListOne) > len(hostnameListTwo) { - return 1 - } + // can not complete tie breaking at hostname level return 0 } @@ -217,6 +213,9 @@ func compareHttpRulePrecedence(ruleOne RulePrecedence, ruleTwo RulePrecedence) b if ruleOne.HttpSpecificRulePrecedenceFactor.QueryParamCount != ruleTwo.HttpSpecificRulePrecedenceFactor.QueryParamCount { return ruleOne.HttpSpecificRulePrecedenceFactor.QueryParamCount > ruleTwo.HttpSpecificRulePrecedenceFactor.QueryParamCount } + if len(ruleOne.CommonRulePrecedence.Hostnames) != len(ruleTwo.CommonRulePrecedence.Hostnames) { + return len(ruleOne.CommonRulePrecedence.Hostnames) > len(ruleTwo.CommonRulePrecedence.Hostnames) + } return compareCommonTieBreakers(ruleOne, ruleTwo) } diff --git a/pkg/gateway/routeutils/route_rule_precedence_test.go b/pkg/gateway/routeutils/route_rule_precedence_test.go index 2875d4d54a..5a7d108134 100644 --- a/pkg/gateway/routeutils/route_rule_precedence_test.go +++ b/pkg/gateway/routeutils/route_rule_precedence_test.go @@ -490,9 +490,9 @@ func Test_SortAllRulesByPrecedence(t *testing.T) { httpRouteThreeDotsHost, // a.b.c.com (3 dots) }, output: []RulePrecedence{ - makeRulePrecedenceForRouteDescription(httpRouteNoHost, nil), // no hostname makeRulePrecedenceForRouteDescription(httpRouteThreeDotsHost, nil), // a.b.c.com (3 dots) makeRulePrecedenceForRouteDescription(httpRouteOneDotHost, nil), // zzz.com (1 dot) + makeRulePrecedenceForRouteDescription(httpRouteNoHost, nil), // no hostname }, }, { @@ -503,9 +503,9 @@ func Test_SortAllRulesByPrecedence(t *testing.T) { httpRouteOneDotHost, // zzz.com (1 dot) }, output: []RulePrecedence{ - makeRulePrecedenceForRouteDescription(httpRouteNoHost, nil), // no hostname makeRulePrecedenceForRouteDescription(httpRouteThreeDotsHost, nil), // a.b.c.com (3 dots) makeRulePrecedenceForRouteDescription(httpRouteOneDotHost, nil), // zzz.com (1 dot) + makeRulePrecedenceForRouteDescription(httpRouteNoHost, nil), // no hostname }, }, {