Repository: envoyproxy/gateway
Pull request #5912 (2025-05-04 01:08:27)

docs(rate-limit): minor fix in 'Distinct Users Except Admin' section
**What type of PR is this?**
This is a documentation update. It improves clarity and correctness in the BackendTrafficPolicy example for global rate limiting. Specifically, it adds a missing header match condition (x-user-id: admin with invert: true) that is required to make the "Distinct Users Except Admin" use case work as described.
<!--
Your PR title should be descriptive, and generally start with type that contains a subsystem name with `()` if necessary
and summary followed by a colon. format `chore/docs/api/feat/fix/refactor/style/test: summary`.
Examples:
* "docs: fix grammar error"
* "feat(translator): add new feature"
* "fix: fix xx bug"
* "chore: change ci & build tools etc"
* "api: add xxx fields in ClientTrafficPolicy"

Before raising a PR, please go through this section of the developer guide, https://gateway.envoyproxy.io/contributions/develop/#raising-a-pr
-->

<!--
NOTE: If your PR contains any API changes (changes under `/api`), we recommend you to separate these API changes into
a new PR, and we will review the API part first. It will save you lots of implementation time if the API get accepted.
-->

**What this PR does / why we need it**:
This PR fixes a small but critical gap in the documentation for the **Global Rate Limit Distinct Users Except Admin** feature. The missing header configuration for excluding a specific user (e.g., x-user-id: admin) was causing the documented example to not behave as intended. This PR adds those missing lines to help users correctly configure rate limiting for distinct users while excluding admin traffic.

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes: N/A (no issue filed)

<!--
For any non-trivial changes, you need to provide a brief description of the changes in the release notes.
Please add the description to the release-notes/current.yaml file and include this file in the PR.
-->
Release Notes: No



Patch:
diff --git a/internal/gatewayapi/sort.go b/internal/gatewayapi/sort.go
index e8042c85e..bd859d6c0 100644
--- a/internal/gatewayapi/sort.go
+++ b/internal/gatewayapi/sort.go
@@ -63,6 +63,7 @@ func (x XdsIRRoutes) Less(i, j int) bool {
 	// Equal case
 
 	// 3. Sort based on the number of Header matches.
+	// When the number is same, sort based on number of Exact Header matches.
 	hCountI := len(x[i].HeaderMatches)
 	hCountJ := len(x[j].HeaderMatches)
 	if hCountI < hCountJ {
@@ -71,12 +72,31 @@ func (x XdsIRRoutes) Less(i, j int) bool {
 	if hCountI > hCountJ {
 		return false
 	}
+
+	hExtNumberI := numberOfExactMatches(x[i].HeaderMatches)
+	hExtNumberJ := numberOfExactMatches(x[j].HeaderMatches)
+	if hExtNumberI < hExtNumberJ {
+		return true
+	}
+	if hExtNumberI > hExtNumberJ {
+		return false
+	}
 	// Equal case
 
 	// 4. Sort based on the number of Query param matches.
+	// When the number is same, sort based on number of Exact Query param matches.
 	qCountI := len(x[i].QueryParamMatches)
 	qCountJ := len(x[j].QueryParamMatches)
-	return qCountI < qCountJ
+	if qCountI < qCountJ {
+		return true
+	}
+	if qCountI > qCountJ {
+		return false
+	}
+
+	qExtNumberI := numberOfExactMatches(x[i].QueryParamMatches)
+	qExtNumberJ := numberOfExactMatches(x[j].QueryParamMatches)
+	return qExtNumberI < qExtNumberJ
 }
 
 // sortXdsIR sorts the xdsIR based on the match precedence
@@ -107,3 +127,13 @@ func pathMatchCount(pathMatch *ir.StringMatch) int {
 	}
 	return 0
 }
+
+func numberOfExactMatches(stringMatches []*ir.StringMatch) int {
+	var cnt int
+	for _, stringMatch := range stringMatches {
+		if stringMatch != nil && stringMatch.Exact != nil {
+			cnt++
+		}
+	}
+	return cnt
+}
diff --git a/tools/make/golang.mk b/tools/make/golang.mk
index 2518df660..50a182527 100644
--- a/tools/make/golang.mk
+++ b/tools/make/golang.mk
@@ -56,6 +56,7 @@ go.test.unit: ## Run go unit tests
 .PHONY: go.testdata.complete
 go.testdata.complete: ## Override test ouputdata
 	@$(LOG_TARGET)
+	go test -timeout 30s github.com/envoyproxy/gateway/internal/utils --override-testdata=true
 	go test -timeout 30s github.com/envoyproxy/gateway/internal/xds/translator --override-testdata=true
 	go test -timeout 30s github.com/envoyproxy/gateway/internal/cmd/egctl --override-testdata=true
 	go test -timeout 30s github.com/envoyproxy/gateway/internal/infrastructure/kubernetes/ratelimit --override-testdata=true
diff --git a/tools/make/helm.mk b/tools/make/helm.mk
index 2dc788de6..21797c33b 100644
--- a/tools/make/helm.mk
+++ b/tools/make/helm.mk
@@ -73,11 +73,11 @@ helm-generate.%:
 	@for file in $(wildcard test/helm/${CHART_NAME}/*.in.yaml); do \
   		filename=$$(basename $${file}); \
   		output="$${filename%.in.*}.out.yaml"; \
-                if [ ${CHART_NAME} == "gateway-addons-helm" ]; then \
-                        helm template ${CHART_NAME} charts/${CHART_NAME} -f $${file} > test/helm/${CHART_NAME}/$$output --namespace=monitoring; \
-                elif [ ${CHART_NAME} == "gateway-crds-helm" ]; then \
-                        helm template ${CHART_NAME} charts/${CHART_NAME} -f $${file} > test/helm/${CHART_NAME}/$$output; \
-                else \
-			helm template ${CHART_NAME} charts/${CHART_NAME} -f $${file} > test/helm/${CHART_NAME}/$$output --namespace=envoy-gateway-system; \
+        if [ ${CHART_NAME} == "gateway-addons-helm" ]; then \
+			helm template ${CHART_NAME} charts/${CHART_NAME} -f $${file} > test/helm/${CHART_NAME}/$$output --namespace=monitoring || exit 1; \
+        elif [ ${CHART_NAME} == "gateway-crds-helm" ]; then \
+			helm template ${CHART_NAME} charts/${CHART_NAME} -f $${file} > test/helm/${CHART_NAME}/$$output || exit 1; \
+        else \
+			helm template ${CHART_NAME} charts/${CHART_NAME} -f $${file} > test/helm/${CHART_NAME}/$$output --namespace=envoy-gateway-system || exit 1; \
   	  	fi; \
 	done


Test patch:
diff --git a/internal/cmd/egctl/config_test.go b/internal/cmd/egctl/config_test.go
index e7fb062f9..7d5407253 100644
--- a/internal/cmd/egctl/config_test.go
+++ b/internal/cmd/egctl/config_test.go
@@ -29,6 +29,7 @@ import (
 	kube "github.com/envoyproxy/gateway/internal/kubernetes"
 	"github.com/envoyproxy/gateway/internal/utils/file"
 	netutil "github.com/envoyproxy/gateway/internal/utils/net"
+	"github.com/envoyproxy/gateway/internal/utils/test"
 )
 
 const (
@@ -118,7 +119,7 @@ func TestExtractAllConfigDump(t *testing.T) {
 			aggregated := sampleAggregatedConfigDump(configDump)
 			got, err := marshalEnvoyProxyConfig(aggregated, tc.output)
 			require.NoError(t, err)
-			if *overrideTestData {
+			if test.OverrideTestData() {
 				require.NoError(t, file.Write(string(got), filepath.Join("testdata", "config", "out", tc.expected)))
 			}
 			out, err := readOutputConfig(tc.expected)
@@ -206,7 +207,7 @@ func TestExtractSubResourcesConfigDump(t *testing.T) {
 			aggregated := sampleAggregatedConfigDump(configDump)
 			got, err := marshalEnvoyProxyConfig(aggregated, tc.output)
 			require.NoError(t, err)
-			if *overrideTestData {
+			if test.OverrideTestData() {
 				require.NoError(t, file.Write(string(got), filepath.Join("testdata", "config", "out", tc.expected)))
 			}
 			out, err := readOutputConfig(tc.expected)
diff --git a/internal/cmd/egctl/translate_test.go b/internal/cmd/egctl/translate_test.go
index 030ca31ac..f7c401a9a 100644
--- a/internal/cmd/egctl/translate_test.go
+++ b/internal/cmd/egctl/translate_test.go
@@ -25,10 +25,9 @@ import (
 	"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
 	"github.com/envoyproxy/gateway/internal/utils/field"
 	"github.com/envoyproxy/gateway/internal/utils/file"
+	"github.com/envoyproxy/gateway/internal/utils/test"
 )
 
-var overrideTestData = flag.Bool("override-testdata", false, "if override the test output data.")
-
 func TestTranslate(t *testing.T) {
 	testCases := []struct {
 		name         string
@@ -363,7 +362,7 @@ func TestTranslate(t *testing.T) {
 				out, err = yaml.Marshal(got)
 				require.NoError(t, err)
 			}
-			if *overrideTestData {
+			if test.OverrideTestData() {
 				require.NoError(t, file.Write(string(out), filepath.Join("testdata", "translate", "out", fn)))
 			}
 			want := &TranslationResult{}
diff --git a/internal/gatewayapi/resource/load_test.go b/internal/gatewayapi/resource/load_test.go
index fcfbfe1cf..3fe008366 100644
--- a/internal/gatewayapi/resource/load_test.go
+++ b/internal/gatewayapi/resource/load_test.go
@@ -6,7 +6,6 @@
 package resource
 
 import (
-	"flag"
 	"fmt"
 	"os"
 	"path/filepath"
@@ -18,10 +17,9 @@ import (
 	"sigs.k8s.io/yaml"
 
 	"github.com/envoyproxy/gateway/internal/utils/file"
+	"github.com/envoyproxy/gateway/internal/utils/test"
 )
 
-var overrideTestData = flag.Bool("override-testdata", false, "if override the test output data.")
-
 func TestIterYAMLBytes(t *testing.T) {
 	inputs := `test: foo1
 ---
@@ -53,7 +51,7 @@ func TestLoadAllSupportedResourcesFromYAMLBytes(t *testing.T) {
 	got, err := LoadResourcesFromYAMLBytes(inFile, true)
 	require.NoError(t, err)
 
-	if *overrideTestData {
+	if test.OverrideTestData() {
 		out, err := yaml.Marshal(got)
 		require.NoError(t, err)
 		require.NoError(t, file.Write(string(out), filepath.Join("testdata", "all-resources.out.yaml")))
diff --git a/internal/gatewayapi/testdata/httproute-with-header-match-diff-number.in.yaml b/internal/gatewayapi/testdata/httproute-with-header-match-diff-number.in.yaml
new file mode 100644
index 000000000..7223aef58
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-header-match-diff-number.in.yaml
@@ -0,0 +1,68 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      hostname: '*.envoyproxy.io'
+      allowedRoutes:
+        namespaces:
+          from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-1
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          type: PathPrefix
+          value: '/'
+        headers:
+        - type: RegularExpression
+          name: x-org-id
+          value: '.*'
+        - type: RegularExpression
+          name: hostname
+          value: '.*'
+      backendRefs:
+      - name: service-1
+        port: 8080
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-2
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          type: PathPrefix
+          value: '/'
+        headers:
+        - type: Exact
+          name: x-org-id
+          value: test-org
+      backendRefs:
+      - name: service-1
+        port: 8080
diff --git a/internal/gatewayapi/testdata/httproute-with-header-match-diff-number.out.yaml b/internal/gatewayapi/testdata/httproute-with-header-match-diff-number.out.yaml
new file mode 100644
index 000000000..895279238
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-header-match-diff-number.out.yaml
@@ -0,0 +1,228 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      hostname: '*.envoyproxy.io'
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 2
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-1
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - headers:
+        - name: x-org-id
+          type: RegularExpression
+          value: .*
+        - name: hostname
+          type: RegularExpression
+          value: .*
+        path:
+          type: PathPrefix
+          value: /
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-2
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - headers:
+        - name: x-org-id
+          type: Exact
+          value: test-org
+        path:
+          type: PathPrefix
+          value: /
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+      name: envoy-gateway/gateway-1
+      namespace: envoy-gateway-system
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*.envoyproxy.io'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+      name: envoy-gateway/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - destination:
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            name: httproute/default/httproute-1/rule/0/backend/0
+            protocol: HTTP
+            weight: 1
+        headerMatches:
+        - distinct: false
+          name: x-org-id
+          safeRegex: .*
+        - distinct: false
+          name: hostname
+          safeRegex: .*
+        hostname: gateway.envoyproxy.io
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+        name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /
+      - destination:
+          name: httproute/default/httproute-2/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            name: httproute/default/httproute-2/rule/0/backend/0
+            protocol: HTTP
+            weight: 1
+        headerMatches:
+        - distinct: false
+          exact: test-org
+          name: x-org-id
+        hostname: gateway.envoyproxy.io
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-2
+          namespace: default
+        name: httproute/default/httproute-2/rule/0/match/0/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003
diff --git a/internal/gatewayapi/testdata/httproute-with-header-match-diff-type.in.yaml b/internal/gatewayapi/testdata/httproute-with-header-match-diff-type.in.yaml
new file mode 100644
index 000000000..06ae961ae
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-header-match-diff-type.in.yaml
@@ -0,0 +1,65 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      hostname: '*.envoyproxy.io'
+      allowedRoutes:
+        namespaces:
+          from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-1
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          type: PathPrefix
+          value: '/'
+        headers:
+        - type: RegularExpression
+          name: x-org-id
+          value: '.*'
+      backendRefs:
+      - name: service-1
+        port: 8080
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-2
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          type: PathPrefix
+          value: '/'
+        headers:
+        - type: Exact
+          name: x-org-id
+          value: test-org
+      backendRefs:
+      - name: service-1
+        port: 8080
diff --git a/internal/gatewayapi/testdata/httproute-with-header-match-diff-type.out.yaml b/internal/gatewayapi/testdata/httproute-with-header-match-diff-type.out.yaml
new file mode 100644
index 000000000..454c35bc9
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-header-match-diff-type.out.yaml
@@ -0,0 +1,222 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      hostname: '*.envoyproxy.io'
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 2
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-1
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - headers:
+        - name: x-org-id
+          type: RegularExpression
+          value: .*
+        path:
+          type: PathPrefix
+          value: /
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-2
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - headers:
+        - name: x-org-id
+          type: Exact
+          value: test-org
+        path:
+          type: PathPrefix
+          value: /
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+      name: envoy-gateway/gateway-1
+      namespace: envoy-gateway-system
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*.envoyproxy.io'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+      name: envoy-gateway/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - destination:
+          name: httproute/default/httproute-2/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            name: httproute/default/httproute-2/rule/0/backend/0
+            protocol: HTTP
+            weight: 1
+        headerMatches:
+        - distinct: false
+          exact: test-org
+          name: x-org-id
+        hostname: gateway.envoyproxy.io
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-2
+          namespace: default
+        name: httproute/default/httproute-2/rule/0/match/0/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /
+      - destination:
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            name: httproute/default/httproute-1/rule/0/backend/0
+            protocol: HTTP
+            weight: 1
+        headerMatches:
+        - distinct: false
+          name: x-org-id
+          safeRegex: .*
+        hostname: gateway.envoyproxy.io
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+        name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003
diff --git a/internal/gatewayapi/testdata/httproute-with-query-match-diff-number.in.yaml b/internal/gatewayapi/testdata/httproute-with-query-match-diff-number.in.yaml
new file mode 100644
index 000000000..adc40a7ed
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-query-match-diff-number.in.yaml
@@ -0,0 +1,68 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      hostname: '*.envoyproxy.io'
+      allowedRoutes:
+        namespaces:
+          from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-1
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          type: PathPrefix
+          value: '/'
+        queryParams:
+        - type: RegularExpression
+          name: id
+          value: '.*'
+        - type: RegularExpression
+          name: name
+          value: '.*'
+      backendRefs:
+      - name: service-1
+        port: 8080
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-2
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          type: PathPrefix
+          value: '/'
+        queryParams:
+        - type: Exact
+          name: id
+          value: 1234
+      backendRefs:
+      - name: service-1
+        port: 8080
diff --git a/internal/gatewayapi/testdata/httproute-with-query-match-diff-number.out.yaml b/internal/gatewayapi/testdata/httproute-with-query-match-diff-number.out.yaml
new file mode 100644
index 000000000..2929ac417
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-query-match-diff-number.out.yaml
@@ -0,0 +1,228 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      hostname: '*.envoyproxy.io'
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 2
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-1
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - path:
+          type: PathPrefix
+          value: /
+        queryParams:
+        - name: id
+          type: RegularExpression
+          value: .*
+        - name: name
+          type: RegularExpression
+          value: .*
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-2
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - path:
+          type: PathPrefix
+          value: /
+        queryParams:
+        - name: id
+          type: Exact
+          value: "1234"
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+      name: envoy-gateway/gateway-1
+      namespace: envoy-gateway-system
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*.envoyproxy.io'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+      name: envoy-gateway/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - destination:
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            name: httproute/default/httproute-1/rule/0/backend/0
+            protocol: HTTP
+            weight: 1
+        hostname: gateway.envoyproxy.io
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+        name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /
+        queryParamMatches:
+        - distinct: false
+          name: id
+          safeRegex: .*
+        - distinct: false
+          name: name
+          safeRegex: .*
+      - destination:
+          name: httproute/default/httproute-2/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            name: httproute/default/httproute-2/rule/0/backend/0
+            protocol: HTTP
+            weight: 1
+        hostname: gateway.envoyproxy.io
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-2
+          namespace: default
+        name: httproute/default/httproute-2/rule/0/match/0/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /
+        queryParamMatches:
+        - distinct: false
+          exact: "1234"
+          name: id
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003
diff --git a/internal/gatewayapi/testdata/httproute-with-query-match-diff-type.in.yaml b/internal/gatewayapi/testdata/httproute-with-query-match-diff-type.in.yaml
new file mode 100644
index 000000000..128cefe8c
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-query-match-diff-type.in.yaml
@@ -0,0 +1,65 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      hostname: '*.envoyproxy.io'
+      allowedRoutes:
+        namespaces:
+          from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-1
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          type: PathPrefix
+          value: '/'
+        queryParams:
+        - type: RegularExpression
+          name: id
+          value: '.*'
+      backendRefs:
+      - name: service-1
+        port: 8080
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-2
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          type: PathPrefix
+          value: '/'
+        queryParams:
+        - type: Exact
+          name: id
+          value: 1234
+      backendRefs:
+      - name: service-1
+        port: 8080
diff --git a/internal/gatewayapi/testdata/httproute-with-query-match-diff-type.out.yaml b/internal/gatewayapi/testdata/httproute-with-query-match-diff-type.out.yaml
new file mode 100644
index 000000000..b1dee4482
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-query-match-diff-type.out.yaml
@@ -0,0 +1,222 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      hostname: '*.envoyproxy.io'
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 2
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-1
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - path:
+          type: PathPrefix
+          value: /
+        queryParams:
+        - name: id
+          type: RegularExpression
+          value: .*
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-2
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - path:
+          type: PathPrefix
+          value: /
+        queryParams:
+        - name: id
+          type: Exact
+          value: "1234"
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+      name: envoy-gateway/gateway-1
+      namespace: envoy-gateway-system
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*.envoyproxy.io'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+      name: envoy-gateway/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - destination:
+          name: httproute/default/httproute-2/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            name: httproute/default/httproute-2/rule/0/backend/0
+            protocol: HTTP
+            weight: 1
+        hostname: gateway.envoyproxy.io
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-2
+          namespace: default
+        name: httproute/default/httproute-2/rule/0/match/0/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /
+        queryParamMatches:
+        - distinct: false
+          exact: "1234"
+          name: id
+      - destination:
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            name: httproute/default/httproute-1/rule/0/backend/0
+            protocol: HTTP
+            weight: 1
+        hostname: gateway.envoyproxy.io
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+        name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /
+        queryParamMatches:
+        - distinct: false
+          name: id
+          safeRegex: .*
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003
diff --git a/internal/gatewayapi/translator_test.go b/internal/gatewayapi/translator_test.go
index af8349df7..519394429 100644
--- a/internal/gatewayapi/translator_test.go
+++ b/internal/gatewayapi/translator_test.go
@@ -10,7 +10,6 @@ import (
 	"context"
 	"crypto/sha256"
 	"encoding/hex"
-	"flag"
 	"fmt"
 	"os"
 	"path/filepath"
@@ -38,11 +37,10 @@ import (
 	"github.com/envoyproxy/gateway/internal/ir"
 	"github.com/envoyproxy/gateway/internal/utils/field"
 	"github.com/envoyproxy/gateway/internal/utils/file"
+	"github.com/envoyproxy/gateway/internal/utils/test"
 	"github.com/envoyproxy/gateway/internal/wasm"
 )
 
-var overrideTestData = flag.Bool("override-testdata", false, "if override the test output data.")
-
 func mustUnmarshal(t *testing.T, val []byte, out interface{}) {
 	require.NoError(t, yaml.UnmarshalStrict(val, out, yaml.DisallowUnknownFields))
 }
@@ -326,7 +324,7 @@ func TestTranslate(t *testing.T) {
 			out, err := yaml.Marshal(got)
 			require.NoError(t, err)
 
-			if *overrideTestData {
+			if test.OverrideTestData() {
 				overrideOutputConfig(t, string(out), outputFilePath)
 			}
 
@@ -527,7 +525,7 @@ func TestTranslateWithExtensionKinds(t *testing.T) {
 			out, err := yaml.Marshal(got)
 			require.NoError(t, err)
 
-			if *overrideTestData {
+			if test.OverrideTestData() {
 				require.NoError(t, file.Write(string(out), outputFilePath))
 			}
 
diff --git a/internal/infrastructure/kubernetes/proxy/resource_provider_test.go b/internal/infrastructure/kubernetes/proxy/resource_provider_test.go
index ea622b987..0651a433a 100644
--- a/internal/infrastructure/kubernetes/proxy/resource_provider_test.go
+++ b/internal/infrastructure/kubernetes/proxy/resource_provider_test.go
@@ -6,7 +6,6 @@
 package proxy
 
 import (
-	"flag"
 	"fmt"
 	"os"
 	"sort"
@@ -30,10 +29,9 @@ import (
 	"github.com/envoyproxy/gateway/internal/envoygateway/config"
 	"github.com/envoyproxy/gateway/internal/gatewayapi"
 	"github.com/envoyproxy/gateway/internal/ir"
+	"github.com/envoyproxy/gateway/internal/utils/test"
 )
 
-var overrideTestData = flag.Bool("override-testdata", false, "if override the test output data.")
-
 const (
 	// envoyHTTPPort is the container port number of Envoy's HTTP endpoint.
 	envoyHTTPPort = int32(8080)
@@ -634,7 +632,7 @@ func TestDeployment(t *testing.T) {
 			dp, err := r.Deployment()
 			require.NoError(t, err)
 
-			if *overrideTestData {
+			if test.OverrideTestData() {
 				deploymentYAML, err := yaml.Marshal(dp)
 				require.NoError(t, err)
 				// nolint: gosec
@@ -1072,7 +1070,7 @@ func TestDaemonSet(t *testing.T) {
 				})
 			}
 
-			if *overrideTestData {
+			if test.OverrideTestData() {
 				deploymentYAML, err := yaml.Marshal(ds)
 				require.NoError(t, err)
 				// nolint: gosec
diff --git a/internal/infrastructure/kubernetes/ratelimit/resource_provider_test.go b/internal/infrastructure/kubernetes/ratelimit/resource_provider_test.go
index a7bc1479e..f6b9b3a91 100644
--- a/internal/infrastructure/kubernetes/ratelimit/resource_provider_test.go
+++ b/internal/infrastructure/kubernetes/ratelimit/resource_provider_test.go
@@ -6,7 +6,6 @@
 package ratelimit
 
 import (
-	"flag"
 	"fmt"
 	"os"
 	"strconv"
@@ -27,10 +26,9 @@ import (
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/envoyproxy/gateway/internal/envoygateway/config"
+	"github.com/envoyproxy/gateway/internal/utils/test"
 )
 
-var overrideTestData = flag.Bool("override-testdata", false, "if override the test output data.")
-
 const (
 	// RedisAuthEnvVar is the redis auth.
 	RedisAuthEnvVar = "REDIS_AUTH"
@@ -200,7 +198,7 @@ func TestConfigmap(t *testing.T) {
 	cm, err := r.ConfigMap("")
 	require.NoError(t, err)
 
-	if *overrideTestData {
+	if test.OverrideTestData() {
 		cmYAML, err := yaml.Marshal(cm)
 		require.NoError(t, err)
 		// nolint:gosec
@@ -770,7 +768,7 @@ func TestDeployment(t *testing.T) {
 			dp, err := r.Deployment()
 			require.NoError(t, err)
 
-			if *overrideTestData {
+			if test.OverrideTestData() {
 				deploymentYAML, err := yaml.Marshal(dp)
 				require.NoError(t, err)
 				// nolint:gosec
@@ -886,7 +884,7 @@ func TestHorizontalPodAutoscaler(t *testing.T) {
 			hpa, err := r.HorizontalPodAutoscaler()
 			require.NoError(t, err)
 
-			if *overrideTestData {
+			if test.OverrideTestData() {
 				hpaYAML, err := yaml.Marshal(hpa)
 				require.NoError(t, err)
 				// nolint:gosec
diff --git a/internal/metrics/metrics_test.go b/internal/metrics/metrics_test.go
index 55b682aff..fcd05c37d 100644
--- a/internal/metrics/metrics_test.go
+++ b/internal/metrics/metrics_test.go
@@ -10,7 +10,6 @@ import (
 	"context"
 	"encoding/json"
 	"errors"
-	"flag"
 	"fmt"
 	"io"
 	"os"
@@ -28,9 +27,9 @@ import (
 	"go.opentelemetry.io/otel/sdk/metric/metricdata"
 	"go.opentelemetry.io/otel/sdk/resource"
 	semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
-)
 
-var overrideTestData = flag.Bool("override-testdata", false, "if override the test output data.")
+	"github.com/envoyproxy/gateway/internal/utils/test"
+)
 
 func TestCounter(t *testing.T) {
 	name := "counter_metric"
@@ -213,7 +212,7 @@ func newTestMetricsProvider(metricType string, writer io.Writer) (*metric.MeterP
 }
 
 func loadMetricsFile(t *testing.T, name string, reader io.Reader) {
-	if !*overrideTestData {
+	if !test.OverrideTestData() {
 		fname := fmt.Sprintf("testdata/%s.json", name)
 
 		// nolint:gosec
@@ -229,7 +228,7 @@ func loadMetricsFile(t *testing.T, name string, reader io.Reader) {
 }
 
 func exporterWriter(name string, origin io.ReadWriter) (io.ReadWriter, error) {
-	if *overrideTestData {
+	if test.OverrideTestData() {
 		fname := fmt.Sprintf("testdata/%s.json", name)
 
 		// nolint:gosec
diff --git a/internal/utils/merge_test.go b/internal/utils/merge_test.go
index 690a331f9..73f6a584c 100644
--- a/internal/utils/merge_test.go
+++ b/internal/utils/merge_test.go
@@ -6,146 +6,63 @@
 package utils
 
 import (
-	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/require"
-	"k8s.io/apimachinery/pkg/api/resource"
-	"k8s.io/utils/ptr"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/yaml"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
+	"github.com/envoyproxy/gateway/internal/utils/test"
 )
 
 func TestMergeBackendTrafficPolicy(t *testing.T) {
-	r := resource.MustParse("100m")
+	baseDir := "testdata"
+	caseFiles, err := filepath.Glob(filepath.Join(baseDir, "backendtrafficpolicy_*.in.yaml"))
+	require.NoError(t, err)
 
-	cases := []struct {
-		name     string
-		original *egv1a1.BackendTrafficPolicy
-		patch    *egv1a1.BackendTrafficPolicy
+	for _, caseFile := range caseFiles {
+		// get case name from path
+		caseName := strings.TrimPrefix(strings.TrimSuffix(caseFile, ".in.yaml"), baseDir+"/backendtrafficpolicy_")
+		t.Run(caseName, func(t *testing.T) {
+			for _, mergeType := range []egv1a1.MergeType{egv1a1.StrategicMerge, egv1a1.JSONMerge} {
+				patchedInput := strings.Replace(caseFile, ".in.yaml", ".patch.yaml", 1)
+				var output string
+				if mergeType == egv1a1.StrategicMerge {
+					output = strings.Replace(caseFile, ".in.yaml", ".strategicmerge.out.yaml", 1)
+				} else {
+					output = strings.Replace(caseFile, ".in.yaml", ".jsonmerge.out.yaml", 1)
+				}
 
-		expected          *egv1a1.BackendTrafficPolicy
-		jsonMergeExpected *egv1a1.BackendTrafficPolicy
-	}{
-		{
-			name: "merge",
-			original: &egv1a1.BackendTrafficPolicy{
-				Spec: egv1a1.BackendTrafficPolicySpec{
-					ClusterSettings: egv1a1.ClusterSettings{
-						Connection: &egv1a1.BackendConnection{
-							BufferLimit: &r,
-						},
-						Retry: &egv1a1.Retry{
-							NumRetries: ptr.To[int32](2),
-						},
-					},
-					HTTPUpgrade: []*egv1a1.ProtocolUpgradeConfig{
-						{
-							Type: "original",
-						},
-					},
-				},
-			},
-			patch: &egv1a1.BackendTrafficPolicy{
-				Spec: egv1a1.BackendTrafficPolicySpec{
-					ClusterSettings: egv1a1.ClusterSettings{
-						Retry: &egv1a1.Retry{
-							NumRetries: ptr.To[int32](3),
-						},
-					},
-					HTTPUpgrade: []*egv1a1.ProtocolUpgradeConfig{
-						{
-							Type: "patched",
-						},
-					},
-				},
-			},
-			expected: &egv1a1.BackendTrafficPolicy{
-				Spec: egv1a1.BackendTrafficPolicySpec{
-					ClusterSettings: egv1a1.ClusterSettings{
-						Connection: &egv1a1.BackendConnection{
-							BufferLimit: &r,
-						},
-						Retry: &egv1a1.Retry{
-							NumRetries: ptr.To[int32](3),
-						},
-					},
-					HTTPUpgrade: []*egv1a1.ProtocolUpgradeConfig{
-						{
-							Type: "patched",
-						},
-						{
-							Type: "original",
-						},
-					},
-				},
-			},
-			jsonMergeExpected: &egv1a1.BackendTrafficPolicy{
-				Spec: egv1a1.BackendTrafficPolicySpec{
-					ClusterSettings: egv1a1.ClusterSettings{
-						Connection: &egv1a1.BackendConnection{
-							BufferLimit: &r,
-						},
-						Retry: &egv1a1.Retry{
-							NumRetries: ptr.To[int32](3),
-						},
-					},
-					HTTPUpgrade: []*egv1a1.ProtocolUpgradeConfig{
-						{
-							Type: "patched",
-						},
-					},
-				},
-			},
-		},
-		{
-			name: "override",
-			original: &egv1a1.BackendTrafficPolicy{
-				Spec: egv1a1.BackendTrafficPolicySpec{
-					ClusterSettings: egv1a1.ClusterSettings{
-						Retry: &egv1a1.Retry{
-							NumRetries: ptr.To[int32](13),
-						},
-					},
-				},
-			},
-			patch: &egv1a1.BackendTrafficPolicy{
-				Spec: egv1a1.BackendTrafficPolicySpec{
-					ClusterSettings: egv1a1.ClusterSettings{
-						Retry: &egv1a1.Retry{
-							NumRetries: ptr.To[int32](3),
-						},
-					},
-				},
-			},
-			expected: &egv1a1.BackendTrafficPolicy{
-				Spec: egv1a1.BackendTrafficPolicySpec{
-					ClusterSettings: egv1a1.ClusterSettings{
-						Retry: &egv1a1.Retry{
-							NumRetries: ptr.To[int32](3),
-						},
-					},
-				},
-			},
-		},
-	}
-	for _, tc := range cases {
-		for _, mergeType := range []egv1a1.MergeType{egv1a1.StrategicMerge, egv1a1.JSONMerge} {
-			t.Run(fmt.Sprintf("%s/%s", mergeType, tc.name), func(t *testing.T) {
-				got, err := Merge[*egv1a1.BackendTrafficPolicy](tc.original, tc.patch, mergeType)
+				original := readObject[*egv1a1.BackendTrafficPolicy](t, caseFile)
+				patch := readObject[*egv1a1.BackendTrafficPolicy](t, patchedInput)
+
+				got, err := Merge(original, patch, mergeType)
 				require.NoError(t, err)
 
-				switch mergeType {
-				case egv1a1.StrategicMerge:
-					require.Equal(t, tc.expected, got)
-				case egv1a1.JSONMerge:
-					if tc.jsonMergeExpected != nil {
-						require.Equal(t, tc.jsonMergeExpected, got)
-					} else {
-						require.Equal(t, tc.expected, got)
-					}
+				if test.OverrideTestData() {
+					b, err := yaml.Marshal(got)
+					require.NoError(t, err)
+					require.NoError(t, os.WriteFile(output, b, 0o600))
+					continue
 				}
-			})
-		}
+
+				expected := readObject[*egv1a1.BackendTrafficPolicy](t, output)
+				require.Equal(t, expected, got)
+			}
+		})
 	}
 }
+
+func readObject[T client.Object](t *testing.T, path string) T {
+	t.Helper()
+	b, err := os.ReadFile(path)
+	require.NoError(t, err)
+	btp := new(T)
+	err = yaml.Unmarshal(b, btp)
+	require.NoError(t, err)
+	return *btp
+}
diff --git a/internal/utils/test/flags.go b/internal/utils/test/flags.go
new file mode 100644
index 000000000..95e261b25
--- /dev/null
+++ b/internal/utils/test/flags.go
@@ -0,0 +1,14 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+package test
+
+import "flag"
+
+var overrideTestData = flag.Bool("override-testdata", false, "if override the test output data.")
+
+func OverrideTestData() bool {
+	return *overrideTestData
+}
diff --git a/internal/utils/testdata/backendtrafficpolicy_httpupgrade.in.yaml b/internal/utils/testdata/backendtrafficpolicy_httpupgrade.in.yaml
new file mode 100644
index 000000000..20af7411b
--- /dev/null
+++ b/internal/utils/testdata/backendtrafficpolicy_httpupgrade.in.yaml
@@ -0,0 +1,13 @@
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: original
+spec:
+  timeout:
+    tcp:
+      connectTimeout: 15s
+    http:
+      connectionIdleTimeout: 16s
+      maxConnectionDuration: 17s
+  httpUpgrade:
+    - type: websocket
diff --git a/internal/utils/testdata/backendtrafficpolicy_httpupgrade.jsonmerge.out.yaml b/internal/utils/testdata/backendtrafficpolicy_httpupgrade.jsonmerge.out.yaml
new file mode 100644
index 000000000..b9f7db73e
--- /dev/null
+++ b/internal/utils/testdata/backendtrafficpolicy_httpupgrade.jsonmerge.out.yaml
@@ -0,0 +1,18 @@
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  creationTimestamp: null
+  name: patched
+spec:
+  connection:
+    bufferLimit: 100M
+  httpUpgrade:
+  - type: websocket
+  timeout:
+    http:
+      connectionIdleTimeout: 16s
+      maxConnectionDuration: 17s
+    tcp:
+      connectTimeout: 15s
+status:
+  ancestors: null
diff --git a/internal/utils/testdata/backendtrafficpolicy_httpupgrade.patch.yaml b/internal/utils/testdata/backendtrafficpolicy_httpupgrade.patch.yaml
new file mode 100644
index 000000000..364d73ce0
--- /dev/null
+++ b/internal/utils/testdata/backendtrafficpolicy_httpupgrade.patch.yaml
@@ -0,0 +1,12 @@
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: patched
+spec:
+  timeout:
+    tcp:
+      connectTimeout: 15s
+  connection:
+    bufferLimit: 100M
+  httpUpgrade:
+    - type: "websocket"
diff --git a/internal/utils/testdata/backendtrafficpolicy_httpupgrade.strategicmerge.out.yaml b/internal/utils/testdata/backendtrafficpolicy_httpupgrade.strategicmerge.out.yaml
new file mode 100644
index 000000000..b9f7db73e
--- /dev/null
+++ b/internal/utils/testdata/backendtrafficpolicy_httpupgrade.strategicmerge.out.yaml
@@ -0,0 +1,18 @@
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  creationTimestamp: null
+  name: patched
+spec:
+  connection:
+    bufferLimit: 100M
+  httpUpgrade:
+  - type: websocket
+  timeout:
+    http:
+      connectionIdleTimeout: 16s
+      maxConnectionDuration: 17s
+    tcp:
+      connectTimeout: 15s
+status:
+  ancestors: null
diff --git a/internal/xds/bootstrap/bootstrap_test.go b/internal/xds/bootstrap/bootstrap_test.go
index 70216bb41..4ef244fd5 100644
--- a/internal/xds/bootstrap/bootstrap_test.go
+++ b/internal/xds/bootstrap/bootstrap_test.go
@@ -17,6 +17,7 @@ import (
 	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
+	"github.com/envoyproxy/gateway/internal/utils/test"
 )
 
 func TestGetRenderedBootstrapConfig(t *testing.T) {
@@ -187,7 +188,7 @@ func TestGetRenderedBootstrapConfig(t *testing.T) {
 			got, err := GetRenderedBootstrapConfig(tc.opts)
 			require.NoError(t, err)
 
-			if *overrideTestData {
+			if test.OverrideTestData() {
 				// nolint:gosec
 				err = os.WriteFile(path.Join("testdata", "render", fmt.Sprintf("%s.yaml", tc.name)), []byte(got), 0o644)
 				require.NoError(t, err)
diff --git a/internal/xds/bootstrap/util_test.go b/internal/xds/bootstrap/util_test.go
index b5cacb7af..d105456a5 100644
--- a/internal/xds/bootstrap/util_test.go
+++ b/internal/xds/bootstrap/util_test.go
@@ -6,7 +6,6 @@
 package bootstrap
 
 import (
-	"flag"
 	"fmt"
 	"os"
 	"path"
@@ -17,10 +16,9 @@ import (
 	"sigs.k8s.io/yaml"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
+	"github.com/envoyproxy/gateway/internal/utils/test"
 )
 
-var overrideTestData = flag.Bool("override-testdata", false, "if override the test output data.")
-
 func TestApplyBootstrapConfig(t *testing.T) {
 	str, _ := readTestData("enable-prometheus")
 	cases := []struct {
@@ -74,7 +72,7 @@ func TestApplyBootstrapConfig(t *testing.T) {
 			data, err := ApplyBootstrapConfig(tc.boostrapConfig, tc.defaultBootstrap)
 			require.NoError(t, err)
 
-			if *overrideTestData {
+			if test.OverrideTestData() {
 				// nolint:gosec
 				err = os.WriteFile(path.Join("testdata", "merge", fmt.Sprintf("%s.out.yaml", tc.name)), []byte(data), 0o644)
 				require.NoError(t, err)
diff --git a/internal/xds/translator/listener_ready_test.go b/internal/xds/translator/listener_ready_test.go
index ec522ad7b..70935476e 100644
--- a/internal/xds/translator/listener_ready_test.go
+++ b/internal/xds/translator/listener_ready_test.go
@@ -18,6 +18,7 @@ import (
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/envoyproxy/gateway/internal/ir"
 	"github.com/envoyproxy/gateway/internal/utils/proto"
+	"github.com/envoyproxy/gateway/internal/utils/test"
 )
 
 func TestBuildReadyListener(t *testing.T) {
@@ -61,7 +62,7 @@ func TestBuildReadyListener(t *testing.T) {
 				t.Errorf("unexpected error: %v", err)
 			}
 
-			if *overrideTestData {
+			if test.OverrideTestData() {
 				data, err := proto.ToYAML(got)
 				require.NoError(t, err)
 				err = os.WriteFile(path.Join("testdata", "readylistener", tc.name+".yaml"), data, 0o600)
diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go
index 3dd4edae9..5aefb9b37 100644
--- a/internal/xds/translator/translator_test.go
+++ b/internal/xds/translator/translator_test.go
@@ -7,7 +7,6 @@ package translator
 
 import (
 	"embed"
-	"flag"
 	"path/filepath"
 	"runtime"
 	"sort"
@@ -30,6 +29,7 @@ import (
 	"github.com/envoyproxy/gateway/internal/ir"
 	"github.com/envoyproxy/gateway/internal/utils/field"
 	"github.com/envoyproxy/gateway/internal/utils/file"
+	"github.com/envoyproxy/gateway/internal/utils/test"
 	xtypes "github.com/envoyproxy/gateway/internal/xds/types"
 	"github.com/envoyproxy/gateway/internal/xds/utils"
 )
@@ -39,8 +39,6 @@ var (
 	outFiles embed.FS
 	//go:embed testdata/in/*
 	inFiles embed.FS
-
-	overrideTestData = flag.Bool("override-testdata", false, "if override the test output data.")
 )
 
 type testFileConfig struct {
@@ -178,7 +176,7 @@ func TestTranslateXds(t *testing.T) {
 			routes := tCtx.XdsResources[resourcev3.RouteType]
 			clusters := tCtx.XdsResources[resourcev3.ClusterType]
 			endpoints := tCtx.XdsResources[resourcev3.EndpointType]
-			if *overrideTestData {
+			if test.OverrideTestData() {
 				require.NoError(t, file.Write(requireResourcesToYAMLString(t, listeners), filepath.Join("testdata", "out", "xds-ir", inputFileName+".listeners.yaml")))
 				require.NoError(t, file.Write(requireResourcesToYAMLString(t, routes), filepath.Join("testdata", "out", "xds-ir", inputFileName+".routes.yaml")))
 				require.NoError(t, file.Write(requireResourcesToYAMLString(t, clusters), filepath.Join("testdata", "out", "xds-ir", inputFileName+".clusters.yaml")))
@@ -191,7 +189,7 @@ func TestTranslateXds(t *testing.T) {
 
 			secrets, ok := tCtx.XdsResources[resourcev3.SecretType]
 			if ok && len(secrets) > 0 {
-				if *overrideTestData {
+				if test.OverrideTestData() {
 					require.NoError(t, file.Write(requireResourcesToYAMLString(t, secrets), filepath.Join("testdata", "out", "xds-ir", inputFileName+".secrets.yaml")))
 				}
 				require.Equal(t, requireTestDataOutFile(t, "xds-ir", inputFileName+".secrets.yaml"), requireResourcesToYAMLString(t, secrets))
@@ -202,7 +200,7 @@ func TestTranslateXds(t *testing.T) {
 				for _, e := range got {
 					require.NoError(t, field.SetValue(e, "LastTransitionTime", metav1.NewTime(time.Time{})))
 				}
-				if *overrideTestData {
+				if test.OverrideTestData() {
 					out, err := yaml.Marshal(got)
 					require.NoError(t, err)
 					require.NoError(t, file.Write(string(out), filepath.Join("testdata", "out", "xds-ir", inputFileName+".envoypatchpolicies.yaml")))
@@ -231,7 +229,7 @@ func TestTranslateRateLimitConfig(t *testing.T) {
 			// Call BuildRateLimitServiceConfig with the list of listeners
 			configs := BuildRateLimitServiceConfig(listeners)
 
-			if *overrideTestData {
+			if test.OverrideTestData() {
 				require.NoError(t, file.Write(requireRateLimitConfigsToYAMLString(t, configs), filepath.Join("testdata", "out", "ratelimit-config", inputFileName+".yaml")))
 			}
 			require.Equal(t, requireTestDataOutFile(t, "ratelimit-config", inputFileName+".yaml"), requireRateLimitConfigsToYAMLString(t, configs))
@@ -317,7 +315,7 @@ func TestTranslateXdsWithExtensionErrorsWhenFailOpen(t *testing.T) {
 			routes := tCtx.XdsResources[resourcev3.RouteType]
 			clusters := tCtx.XdsResources[resourcev3.ClusterType]
 			endpoints := tCtx.XdsResources[resourcev3.EndpointType]
-			if *overrideTestData {
+			if test.OverrideTestData() {
 				require.NoError(t, file.Write(requireResourcesToYAMLString(t, listeners), filepath.Join("testdata", "out", "extension-xds-ir", inputFileName+".listeners.yaml")))
 				require.NoError(t, file.Write(requireResourcesToYAMLString(t, routes), filepath.Join("testdata", "out", "extension-xds-ir", inputFileName+".routes.yaml")))
 				require.NoError(t, file.Write(requireResourcesToYAMLString(t, clusters), filepath.Join("testdata", "out", "extension-xds-ir", inputFileName+".clusters.yaml")))
@@ -330,7 +328,7 @@ func TestTranslateXdsWithExtensionErrorsWhenFailOpen(t *testing.T) {
 
 			secrets, ok := tCtx.XdsResources[resourcev3.SecretType]
 			if ok {
-				if *overrideTestData {
+				if test.OverrideTestData() {
 					require.NoError(t, file.Write(requireResourcesToYAMLString(t, secrets), filepath.Join("testdata", "out", "extension-xds-ir", inputFileName+".secrets.yaml")))
 				}
 				require.Equal(t, requireTestDataOutFile(t, "extension-xds-ir", inputFileName+".secrets.yaml"), requireResourcesToYAMLString(t, secrets))
diff --git a/site/content/en/latest/tasks/traffic/global-rate-limit.md b/site/content/en/latest/tasks/traffic/global-rate-limit.md
index 47eac33bc..63f940b15 100644
--- a/site/content/en/latest/tasks/traffic/global-rate-limit.md
+++ b/site/content/en/latest/tasks/traffic/global-rate-limit.md
@@ -493,6 +493,9 @@ spec:
         - headers:
           - type: Distinct
             name: x-user-id
+          - name: x-user-id
+            value: admin
+            invert: true
         limit:
           requests: 3
           unit: Hour