Repository: nginx/nginx-gateway-fabric
Pull request #3306: Add support for RequestMirror filter
Author: login: sjberman
Labels: name: enhancement
description: New feature or request
name: release-notes
Created: 2025-04-17T16:59:02Z | Merged: 2025-04-22T20:13:01Z

Problem: As a user, I want to be able to mirror requests to another backend(s) using the RequestMirror filter on an HTTPRoute or GRPCRoute.

Solution: Add support for the RequestMirror filter, allowing users to mirror requests with HTTPRoutes or GRPCRoutes.

Testing: Manually verified single mirror filter, multiple mirror filter, mirror filter with other filters.

Note: the RequestMirror conformance tests seem to have a bug in 1.2.1 Gateway API, which may be fixed in 1.3. We can hopefully enable them at that point.

Closes #533

### Checklist

Before creating a PR, run through this checklist and mark each as complete.

- [x] I have read the [CONTRIBUTING](https://github.com/nginx/nginx-gateway-fabric/blob/main/CONTRIBUTING.md) doc
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] I have checked that all unit tests pass after adding my changes
- [ ] I have updated necessary documentation
- [x] I have rebased my branch onto main
- [x] I will ensure my PR is targeting the main branch and pulling from my branch from my own fork

### Release notes

If this PR introduces a change that affects users and needs to be mentioned in the [release notes](../blob/main/CHANGELOG.md),
please add a brief note that summarizes the change.

<!-- If this PR does not require a release note, you can just write NONE in the release-note block below. -->

```release-note
Add support for request mirroring using the RequestMirror filter.
```


Linked issue #533: HTTP Request Mirror
**As a** user of NGF
**I want** to mirror my incoming request traffic to another destination
**So that** I can test using production traffic without impacting my users
**And so that** I can analyze incoming traffic for security issues.

### Acceptance
- The HTTPRequestMirrorFilter is implemented as specified [here](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io%2fv1.HTTPRequestMirrorFilter) for Kubernetes Services:

  BackendRef references a resource where mirrored requests are sent.

  Mirrored requests must be sent only to a single destination endpoint within this BackendRef, irrespective of how many endpoints are present within this BackendRef.

  If the referent cannot be found, this BackendRef is invalid and must be dropped from the Gateway. The controller must ensure the “ResolvedRefs” condition on the Route status is set to status: False and not configure this backend in the underlying implementation.

  If there is a cross-namespace reference to an existing object that is not allowed by a ReferenceGrant, the controller must ensure the “ResolvedRefs” condition on the Route is set to status: False, with the “RefNotPermitted” reason and not configure this backend in the underlying implementation.

  In either error case, the Message of the ResolvedRefs Condition should be used to provide more detail about the **problem.**
- The HTTPRequestMirrorFilter is supported in both HTTPRoute and GRPCRoute.
Linked issue comments: user: login: mpstefan
created_at: 2023-07-20T16:27:05Z
body: NGINX can support this, making this a lighter lift comparatively.
author_association: CONTRIBUTOR
user: login: kiyutink
created_at: 2025-03-24T16:26:36Z
body: Hi folks, I was planning to maybe use this when this gets out, and until then use [ngx_http_mirror_module](https://nginx.org/en/docs/http/ngx_http_mirror_module.html). However, I discovered, that this module has a crucial limitation - when using `keepalive`, connections won't be reused for new requests until all mirrored requests finish. This is described in more detail here: https://alex.dzyoba.com/blog/nginx-mirror/ .

Therefore, I was wondering if you plan to implement the `RequestMirror` using this module and whether you would consider another option (sadly, I don't know what that would be) for it. 
author_association: NONE
user: login: sjberman
created_at: 2025-03-24T16:38:13Z
body: @kiyutink Our plan is to use the nginx mirror module that you linked.

It could be worth mentioning this limitation in either a discussion or issue in the [nginx repository](https://github.com/nginx/nginx) to see if it's something that can be addressed in the module itself. (or maybe someone will have insight on how it could be configured in such a way to solve this problem, which we could utilize in our implementation).
author_association: COLLABORATOR

Diff:
diff --git a/internal/mode/static/nginx/config/http/config.go b/internal/mode/static/nginx/config/http/config.go
index b81a6771b8..d79fe708e9 100644
--- a/internal/mode/static/nginx/config/http/config.go
+++ b/internal/mode/static/nginx/config/http/config.go
@@ -5,8 +5,9 @@ import (
 )
 
 const (
-	InternalRoutePathPrefix = "/_ngf-internal"
-	HTTPSScheme             = "https"
+	InternalRoutePathPrefix       = "/_ngf-internal"
+	InternalMirrorRoutePathPrefix = InternalRoutePathPrefix + "-mirror"
+	HTTPSScheme                   = "https"
 )
 
 // Server holds all configuration for an HTTP server.
@@ -41,6 +42,7 @@ type Location struct {
 	Return          *Return
 	ResponseHeaders ResponseHeaders
 	Rewrites        []string
+	MirrorPaths     []string
 	Includes        []shared.Include
 	GRPC            bool
 }
diff --git a/internal/mode/static/nginx/config/servers.go b/internal/mode/static/nginx/config/servers.go
index f7d76e2e80..d4f8d9b14f 100644
--- a/internal/mode/static/nginx/config/servers.go
+++ b/internal/mode/static/nginx/config/servers.go
@@ -237,7 +237,7 @@ func createLocations(
 	matchPairs := make(httpMatchPairs)
 
 	var rootPathExists bool
-	var grpc bool
+	var grpcServer bool
 
 	for pathRuleIdx, rule := range server.PathRules {
 		matches := make([]routeMatch, 0, len(rule.MatchRules))
@@ -247,7 +247,7 @@ func createLocations(
 		}
 
 		if rule.GRPC {
-			grpc = true
+			grpcServer = true
 		}
 
 		extLocations := initializeExternalLocations(rule, pathsAndTypes)
@@ -277,7 +277,7 @@ func createLocations(
 		internalLocations := make([]http.Location, 0, len(rule.MatchRules))
 
 		for matchRuleIdx, r := range rule.MatchRules {
-			intLocation, match := initializeInternalLocation(pathRuleIdx, matchRuleIdx, r.Match, grpc)
+			intLocation, match := initializeInternalLocation(pathRuleIdx, matchRuleIdx, r.Match, rule.GRPC)
 			intLocation.Includes = createIncludesFromPolicyGenerateResult(
 				generator.GenerateForInternalLocation(rule.Policies),
 			)
@@ -313,7 +313,7 @@ func createLocations(
 		locs = append(locs, createDefaultRootLocation())
 	}
 
-	return locs, matchPairs, grpc
+	return locs, matchPairs, grpcServer
 }
 
 func needsInternalLocations(rule dataplane.PathRule) bool {
@@ -433,6 +433,13 @@ func updateLocation(
 		return location
 	}
 
+	if strings.HasPrefix(path, http.InternalMirrorRoutePathPrefix) {
+		location.Type = http.InternalLocationType
+		if grpc {
+			location.Rewrites = []string{"^ $request_uri break"}
+		}
+	}
+
 	location.Includes = append(location.Includes, createIncludesFromLocationSnippetsFilters(filters.SnippetsFilters)...)
 
 	if filters.RequestRedirect != nil {
@@ -445,6 +452,20 @@ func updateLocation(
 	}
 
 	rewrites := createRewritesValForRewriteFilter(filters.RequestURLRewrite, path)
+	if rewrites != nil {
+		if location.Type == http.InternalLocationType && rewrites.InternalRewrite != "" {
+			location.Rewrites = append(location.Rewrites, rewrites.InternalRewrite)
+		}
+		if rewrites.MainRewrite != "" {
+			location.Rewrites = append(location.Rewrites, rewrites.MainRewrite)
+		}
+	}
+
+	for _, filter := range filters.RequestMirrors {
+		if filter.Target != nil {
+			location.MirrorPaths = append(location.MirrorPaths, *filter.Target)
+		}
+	}
 
 	extraHeaders := make([]http.Header, 0, 3)
 	if grpc {
@@ -457,15 +478,6 @@ func updateLocation(
 	proxySetHeaders := generateProxySetHeaders(&matchRule.Filters, createBaseProxySetHeaders(extraHeaders...))
 	responseHeaders := generateResponseHeaders(&matchRule.Filters)
 
-	if rewrites != nil {
-		if location.Type == http.InternalLocationType && rewrites.InternalRewrite != "" {
-			location.Rewrites = append(location.Rewrites, rewrites.InternalRewrite)
-		}
-		if rewrites.MainRewrite != "" {
-			location.Rewrites = append(location.Rewrites, rewrites.MainRewrite)
-		}
-	}
-
 	location.ProxySetHeaders = proxySetHeaders
 	location.ProxySSLVerify = createProxyTLSFromBackends(matchRule.BackendGroup.Backends)
 	proxyPass := createProxyPass(
diff --git a/internal/mode/static/nginx/config/servers_template.go b/internal/mode/static/nginx/config/servers_template.go
index c3873ef105..6676426b86 100644
--- a/internal/mode/static/nginx/config/servers_template.go
+++ b/internal/mode/static/nginx/config/servers_template.go
@@ -101,6 +101,10 @@ server {
         rewrite {{ $r }};
         {{- end }}
 
+        {{- range $m := $l.MirrorPaths }}
+        mirror {{ $m }};
+        {{- end }}
+
         {{- if $l.Return }}
         return {{ $l.Return.Code }} "{{ $l.Return.Body }}";
         {{- end }}
diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go
index 56da44f1fa..70991ecb54 100644
--- a/internal/mode/static/nginx/config/servers_test.go
+++ b/internal/mode/static/nginx/config/servers_test.go
@@ -7,6 +7,7 @@ import (
 	"testing"
 
 	. "github.com/onsi/gomega"
+	"github.com/onsi/gomega/format"
 	"k8s.io/apimachinery/pkg/types"
 
 	"github.com/nginx/nginx-gateway-fabric/internal/framework/helpers"
@@ -862,6 +863,66 @@ func TestCreateServers(t *testing.T) {
 				},
 			},
 		},
+		{
+			Path:     "/mirror",
+			PathType: dataplane.PathTypePrefix,
+			MatchRules: []dataplane.MatchRule{
+				{
+					Match: dataplane.Match{},
+					Filters: dataplane.HTTPFilters{
+						RequestMirrors: []*dataplane.HTTPRequestMirrorFilter{
+							{
+								Name:      helpers.GetPointer("mirror-filter"),
+								Namespace: helpers.GetPointer("test-ns"),
+								Target:    helpers.GetPointer(http.InternalMirrorRoutePathPrefix + "-my-backend"),
+							},
+						},
+					},
+					BackendGroup: fooGroup,
+				},
+			},
+		},
+		{
+			Path:     http.InternalMirrorRoutePathPrefix + "-my-backend",
+			PathType: dataplane.PathTypeExact,
+			MatchRules: []dataplane.MatchRule{
+				{
+					Match:        dataplane.Match{},
+					BackendGroup: fooGroup,
+				},
+			},
+		},
+		{
+			Path:     "/grpc/mirror",
+			PathType: dataplane.PathTypeExact,
+			MatchRules: []dataplane.MatchRule{
+				{
+					Match: dataplane.Match{},
+					Filters: dataplane.HTTPFilters{
+						RequestMirrors: []*dataplane.HTTPRequestMirrorFilter{
+							{
+								Name:      helpers.GetPointer("grpc-mirror-filter"),
+								Namespace: helpers.GetPointer("test-ns"),
+								Target:    helpers.GetPointer(http.InternalMirrorRoutePathPrefix + "-my-grpc-backend"),
+							},
+						},
+					},
+					BackendGroup: fooGroup,
+				},
+			},
+			GRPC: true,
+		},
+		{
+			Path:     http.InternalMirrorRoutePathPrefix + "-my-grpc-backend",
+			PathType: dataplane.PathTypeExact,
+			MatchRules: []dataplane.MatchRule{
+				{
+					Match:        dataplane.Match{},
+					BackendGroup: fooGroup,
+				},
+			},
+			GRPC: true,
+		},
 		{
 			Path:     "/invalid-filter",
 			PathType: dataplane.PathTypePrefix,
@@ -1093,25 +1154,25 @@ func TestCreateServers(t *testing.T) {
 				RedirectPath: "/_ngf-internal-rule8-route0",
 			},
 		},
-		"1_10": {
+		"1_14": {
 			{
 				Headers:      []string{"filter:Exact:this"},
-				RedirectPath: "/_ngf-internal-rule10-route0",
+				RedirectPath: "/_ngf-internal-rule14-route0",
 			},
 		},
-		"1_12": {
+		"1_16": {
 			{
 				Method:       "GET",
-				RedirectPath: "/_ngf-internal-rule12-route0",
+				RedirectPath: "/_ngf-internal-rule16-route0",
 				Headers:      nil,
 				QueryParams:  nil,
 				Any:          false,
 			},
 		},
-		"1_17": {
+		"1_21": {
 			{
 				Method:       "GET",
-				RedirectPath: "/_ngf-internal-rule17-route0",
+				RedirectPath: "/_ngf-internal-rule21-route0",
 			},
 		},
 	}
@@ -1342,6 +1403,47 @@ func TestCreateServers(t *testing.T) {
 				Type:            http.InternalLocationType,
 				Includes:        internalIncludes,
 			},
+			{
+				Path:            "/mirror/",
+				ProxyPass:       "http://test_foo_80$request_uri",
+				ProxySetHeaders: httpBaseHeaders,
+				MirrorPaths:     []string{"/_ngf-internal-mirror-my-backend"},
+				Type:            http.ExternalLocationType,
+				Includes:        externalIncludes,
+			},
+			{
+				Path:            "= /mirror",
+				ProxyPass:       "http://test_foo_80$request_uri",
+				ProxySetHeaders: httpBaseHeaders,
+				MirrorPaths:     []string{"/_ngf-internal-mirror-my-backend"},
+				Type:            http.ExternalLocationType,
+				Includes:        externalIncludes,
+			},
+			{
+				Path:            "= /_ngf-internal-mirror-my-backend",
+				ProxyPass:       "http://test_foo_80$request_uri",
+				ProxySetHeaders: httpBaseHeaders,
+				Type:            http.InternalLocationType,
+				Includes:        externalIncludes,
+			},
+			{
+				Path:            "= /grpc/mirror",
+				GRPC:            true,
+				ProxyPass:       "grpc://test_foo_80",
+				ProxySetHeaders: grpcBaseHeaders,
+				MirrorPaths:     []string{"/_ngf-internal-mirror-my-grpc-backend"},
+				Type:            http.ExternalLocationType,
+				Includes:        externalIncludes,
+			},
+			{
+				Path:            "= /_ngf-internal-mirror-my-grpc-backend",
+				GRPC:            true,
+				ProxyPass:       "grpc://test_foo_80",
+				Rewrites:        []string{"^ $request_uri break"},
+				ProxySetHeaders: grpcBaseHeaders,
+				Type:            http.InternalLocationType,
+				Includes:        externalIncludes,
+			},
 			{
 				Path: "/invalid-filter/",
 				Return: &http.Return{
@@ -1360,18 +1462,18 @@ func TestCreateServers(t *testing.T) {
 			},
 			{
 				Path:         "/invalid-filter-with-headers/",
-				HTTPMatchKey: ssl + "1_10",
+				HTTPMatchKey: ssl + "1_14",
 				Type:         http.RedirectLocationType,
 				Includes:     externalIncludes,
 			},
 			{
 				Path:         "= /invalid-filter-with-headers",
-				HTTPMatchKey: ssl + "1_10",
+				HTTPMatchKey: ssl + "1_14",
 				Type:         http.RedirectLocationType,
 				Includes:     externalIncludes,
 			},
 			{
-				Path: "/_ngf-internal-rule10-route0",
+				Path: "/_ngf-internal-rule14-route0",
 				Return: &http.Return{
 					Code: http.StatusInternalServerError,
 				},
@@ -1387,12 +1489,12 @@ func TestCreateServers(t *testing.T) {
 			},
 			{
 				Path:         "= /test",
-				HTTPMatchKey: ssl + "1_12",
+				HTTPMatchKey: ssl + "1_16",
 				Type:         http.RedirectLocationType,
 				Includes:     externalIncludes,
 			},
 			{
-				Path:            "/_ngf-internal-rule12-route0",
+				Path:            "/_ngf-internal-rule16-route0",
 				ProxyPass:       "http://test_foo_80$request_uri",
 				ProxySetHeaders: httpBaseHeaders,
 				Type:            http.InternalLocationType,
@@ -1471,15 +1573,14 @@ func TestCreateServers(t *testing.T) {
 			},
 			{
 				Path:         "= /include-header-match",
-				HTTPMatchKey: ssl + "1_17",
+				HTTPMatchKey: ssl + "1_21",
 				Type:         http.RedirectLocationType,
 				Includes:     externalIncludes,
 			},
 			{
-				Path:            "/_ngf-internal-rule17-route0",
+				Path:            "/_ngf-internal-rule21-route0",
 				ProxyPass:       "http://test_foo_80$request_uri",
 				ProxySetHeaders: httpBaseHeaders,
-				Rewrites:        []string{"^ $request_uri break"},
 				Type:            http.InternalLocationType,
 				Includes:        internalIncludes,
 			},
@@ -1572,6 +1673,7 @@ func TestCreateServers(t *testing.T) {
 
 	result, httpMatchPair := createServers(conf, fakeGenerator, keepAliveCheck)
 
+	format.MaxLength = 10000
 	g.Expect(httpMatchPair).To(Equal(allExpMatchPair))
 	g.Expect(helpers.Diff(expectedServers, result)).To(BeEmpty())
 }
diff --git a/internal/mode/static/nginx/config/validation/http_validator.go b/internal/mode/static/nginx/config/validation/http_validator.go
index 29da381ad6..a17cfdaa00 100644
--- a/internal/mode/static/nginx/config/validation/http_validator.go
+++ b/internal/mode/static/nginx/config/validation/http_validator.go
@@ -15,4 +15,6 @@ type HTTPValidator struct {
 	HTTPPathValidator
 }
 
+func (HTTPValidator) SkipValidation() bool { return false }
+
 var _ validation.HTTPFieldsValidator = HTTPValidator{}
diff --git a/internal/mode/static/state/change_processor_test.go b/internal/mode/static/state/change_processor_test.go
index 18d3d044cf..d71ebd5212 100644
--- a/internal/mode/static/state/change_processor_test.go
+++ b/internal/mode/static/state/change_processor_test.go
@@ -1029,19 +1029,22 @@ var _ = Describe("ChangeProcessor", func() {
 							// no ref grant exists yet for the routes
 							expGraph.Routes[httpRouteKey1].Conditions = []conditions.Condition{
 								staticConds.NewRouteBackendRefRefNotPermitted(
-									"Backend ref to Service service-ns/service not permitted by any ReferenceGrant",
+									"spec.rules[0].backendRefs[0].namespace: Forbidden: " +
+										"Backend ref to Service service-ns/service not permitted by any ReferenceGrant",
 								),
 							}
 
 							expGraph.Routes[grpcRouteKey1].Conditions = []conditions.Condition{
 								staticConds.NewRouteBackendRefRefNotPermitted(
-									"Backend ref to Service grpc-service-ns/grpc-service not permitted by any ReferenceGrant",
+									"spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " +
+										"grpc-service-ns/grpc-service not permitted by any ReferenceGrant",
 								),
 							}
 
 							expGraph.L4Routes[trKey1].Conditions = []conditions.Condition{
 								staticConds.NewRouteBackendRefRefNotPermitted(
-									"Backend ref to Service tls-service-ns/tls-service not permitted by any ReferenceGrant",
+									"spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " +
+										"tls-service-ns/tls-service not permitted by any ReferenceGrant",
 								),
 							}
 
@@ -1118,7 +1121,8 @@ var _ = Describe("ChangeProcessor", func() {
 					expGraph.Routes[httpRouteKey1].Conditions = []conditions.Condition{
 						staticConds.NewRouteInvalidListener(),
 						staticConds.NewRouteBackendRefRefNotPermitted(
-							"Backend ref to Service service-ns/service not permitted by any ReferenceGrant",
+							"spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " +
+								"service-ns/service not permitted by any ReferenceGrant",
 						),
 					}
 					expGraph.Routes[httpRouteKey1].ParentRefs[0].Attachment = expAttachment80
@@ -1128,7 +1132,8 @@ var _ = Describe("ChangeProcessor", func() {
 					expGraph.Routes[grpcRouteKey1].Conditions = []conditions.Condition{
 						staticConds.NewRouteInvalidListener(),
 						staticConds.NewRouteBackendRefRefNotPermitted(
-							"Backend ref to Service grpc-service-ns/grpc-service not permitted by any ReferenceGrant",
+							"spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " +
+								"grpc-service-ns/grpc-service not permitted by any ReferenceGrant",
 						),
 					}
 					expGraph.Routes[grpcRouteKey1].ParentRefs[0].Attachment = expAttachment80
@@ -1137,7 +1142,8 @@ var _ = Describe("ChangeProcessor", func() {
 					// no ref grant exists yet for tr1
 					expGraph.L4Routes[trKey1].Conditions = []conditions.Condition{
 						staticConds.NewRouteBackendRefRefNotPermitted(
-							"Backend ref to Service tls-service-ns/tls-service not permitted by any ReferenceGrant",
+							"spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " +
+								"tls-service-ns/tls-service not permitted by any ReferenceGrant",
 						),
 					}
 
@@ -1158,21 +1164,24 @@ var _ = Describe("ChangeProcessor", func() {
 					// no ref grant exists yet for hr1
 					expGraph.Routes[httpRouteKey1].Conditions = []conditions.Condition{
 						staticConds.NewRouteBackendRefRefNotPermitted(
-							"Backend ref to Service service-ns/service not permitted by any ReferenceGrant",
+							"spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " +
+								"service-ns/service not permitted by any ReferenceGrant",
 						),
 					}
 
 					// no ref grant exists yet for gr1
 					expGraph.Routes[grpcRouteKey1].Conditions = []conditions.Condition{
 						staticConds.NewRouteBackendRefRefNotPermitted(
-							"Backend ref to Service grpc-service-ns/grpc-service not permitted by any ReferenceGrant",
+							"spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " +
+								"grpc-service-ns/grpc-service not permitted by any ReferenceGrant",
 						),
 					}
 
 					// no ref grant exists yet for tr1
 					expGraph.L4Routes[trKey1].Conditions = []conditions.Condition{
 						staticConds.NewRouteBackendRefRefNotPermitted(
-							"Backend ref to Service tls-service-ns/tls-service not permitted by any ReferenceGrant",
+							"spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " +
+								"tls-service-ns/tls-service not permitted by any ReferenceGrant",
 						),
 					}
 
@@ -1203,7 +1212,8 @@ var _ = Describe("ChangeProcessor", func() {
 					// no ref grant exists yet for gr1
 					expGraph.Routes[grpcRouteKey1].Conditions = []conditions.Condition{
 						staticConds.NewRouteBackendRefRefNotPermitted(
-							"Backend ref to Service grpc-service-ns/grpc-service not permitted by any ReferenceGrant",
+							"spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " +
+								"grpc-service-ns/grpc-service not permitted by any ReferenceGrant",
 						),
 					}
 					delete(expGraph.ReferencedServices, refGRPCSvc)
@@ -1212,7 +1222,8 @@ var _ = Describe("ChangeProcessor", func() {
 					// no ref grant exists yet for tr1
 					expGraph.L4Routes[trKey1].Conditions = []conditions.Condition{
 						staticConds.NewRouteBackendRefRefNotPermitted(
-							"Backend ref to Service tls-service-ns/tls-service not permitted by any ReferenceGrant",
+							"spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " +
+								"tls-service-ns/tls-service not permitted by any ReferenceGrant",
 						),
 					}
 					delete(expGraph.ReferencedServices, refTLSSvc)
@@ -1240,7 +1251,8 @@ var _ = Describe("ChangeProcessor", func() {
 					// no ref grant exists yet for tr1
 					expGraph.L4Routes[trKey1].Conditions = []conditions.Condition{
 						staticConds.NewRouteBackendRefRefNotPermitted(
-							"Backend ref to Service tls-service-ns/tls-service not permitted by any ReferenceGrant",
+							"spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " +
+								"tls-service-ns/tls-service not permitted by any ReferenceGrant",
 						),
 					}
 					delete(expGraph.ReferencedServices, types.NamespacedName{Namespace: "tls-service-ns", Name: "tls-service"})
diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go
index d3c86fcb3e..84dc4b0cdb 100644
--- a/internal/mode/static/state/dataplane/configuration.go
+++ b/internal/mode/static/state/dataplane/configuration.go
@@ -345,6 +345,10 @@ func newBackendGroup(refs []graph.BackendRef, sourceNsName types.NamespacedName,
 	}
 
 	for _, ref := range refs {
+		if ref.IsMirrorBackend {
+			continue
+		}
+
 		backends = append(backends, Backend{
 			UpstreamName: ref.ServicePortReference(),
 			Weight:       ref.Weight,
@@ -506,14 +510,14 @@ func (hpr *hostPathRules) upsertRoute(
 		}
 	}
 
-	for i, rule := range route.Spec.Rules {
+	for idx, rule := range route.Spec.Rules {
 		if !rule.ValidMatches {
 			continue
 		}
 
 		var filters HTTPFilters
 		if rule.Filters.Valid {
-			filters = createHTTPFilters(rule.Filters.Filters)
+			filters = createHTTPFilters(rule.Filters.Filters, idx)
 		} else {
 			filters = HTTPFilters{
 				InvalidFilter: &InvalidHTTPFilter{},
@@ -544,7 +548,7 @@ func (hpr *hostPathRules) upsertRoute(
 
 				hostRule.MatchRules = append(hostRule.MatchRules, MatchRule{
 					Source:       objectSrc,
-					BackendGroup: newBackendGroup(rule.BackendRefs, routeNsName, i),
+					BackendGroup: newBackendGroup(rule.BackendRefs, routeNsName, idx),
 					Filters:      filters,
 					Match:        convertMatch(m),
 				})
@@ -741,7 +745,7 @@ func getPath(path *v1.HTTPPathMatch) string {
 	return *path.Value
 }
 
-func createHTTPFilters(filters []graph.Filter) HTTPFilters {
+func createHTTPFilters(filters []graph.Filter, ruleIdx int) HTTPFilters {
 	var result HTTPFilters
 
 	for _, f := range filters {
@@ -756,6 +760,11 @@ func createHTTPFilters(filters []graph.Filter) HTTPFilters {
 				// using the first filter
 				result.RequestURLRewrite = convertHTTPURLRewriteFilter(f.URLRewrite)
 			}
+		case graph.FilterRequestMirror:
+			result.RequestMirrors = append(
+				result.RequestMirrors,
+				convertHTTPRequestMirrorFilter(f.RequestMirror, ruleIdx),
+			)
 		case graph.FilterRequestHeaderModifier:
 			if result.RequestHeaderModifiers == nil {
 				// using the first filter
diff --git a/internal/mode/static/state/dataplane/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go
index d8219a1c9a..b13a7845a5 100644
--- a/internal/mode/static/state/dataplane/configuration_test.go
+++ b/internal/mode/static/state/dataplane/configuration_test.go
@@ -668,6 +668,52 @@ func TestBuildConfiguration(t *testing.T) {
 
 	l7HTTPSRouteWithPolicy.Policies = []*graph.Policy{hrPolicy2, invalidPolicy}
 
+	hrWithMirror, expHRWithMirrorGroups, routeHRWithMirror := createTestResources(
+		"hr-with-mirror",
+		"foo.example.com",
+		"listener-80-1",
+		pathAndType{
+			path:     "/mirror",
+			pathType: prefix,
+		},
+	)
+
+	mirrorUpstreamName := "test_mirror-backend_80"
+	mirrorUpstream := Upstream{
+		Name: mirrorUpstreamName,
+		Endpoints: []resolver.Endpoint{
+			{
+				Address: "10.0.0.1",
+				Port:    8080,
+			},
+		},
+	}
+
+	fakeResolver.ResolveStub = func(
+		_ context.Context,
+		nsName types.NamespacedName,
+		_ apiv1.ServicePort,
+		_ []discoveryV1.AddressType,
+	) ([]resolver.Endpoint, error) {
+		if nsName.Name == "mirror-backend" {
+			return mirrorUpstream.Endpoints, nil
+		}
+		return fooEndpoints, nil
+	}
+
+	addFilters(routeHRWithMirror, []graph.Filter{
+		{
+			FilterType: graph.FilterRequestMirror,
+			RequestMirror: &v1.HTTPRequestMirrorFilter{
+				BackendRef: v1.BackendObjectReference{
+					Group: helpers.GetPointer(v1.Group("core")),
+					Kind:  helpers.GetPointer(v1.Kind("Service")),
+					Name:  v1.ObjectName("mirror-backend"),
+				},
+			},
+		},
+	})
+
 	secret1NsName := types.NamespacedName{Namespace: "test", Name: "secret-1"}
 	secret1 := &graph.Secret{
 		Source: &apiv1.Secret{
@@ -2057,6 +2103,56 @@ func TestBuildConfiguration(t *testing.T) {
 			}),
 			msg: "https listener with httproute with backend that has a backend TLS policy with binaryData attached",
 		},
+		{
+			graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph {
+				g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{
+					Name:   "listener-80-1",
+					Source: listener80,
+					Valid:  true,
+					Routes: map[graph.RouteKey]*graph.L7Route{
+						graph.CreateRouteKey(hrWithMirror): routeHRWithMirror,
+					},
+				})
+				g.Routes = map[graph.RouteKey]*graph.L7Route{
+					graph.CreateRouteKey(hrWithMirror): routeHRWithMirror,
+				}
+				return g
+			}),
+			expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration {
+				conf.HTTPServers = append(conf.HTTPServers, []VirtualServer{
+					{
+						Hostname: "foo.example.com",
+						PathRules: []PathRule{
+							{
+								Path:     "/mirror",
+								PathType: PathTypePrefix,
+								MatchRules: []MatchRule{
+									{
+										BackendGroup: expHRWithMirrorGroups[0],
+										Source:       &hrWithMirror.ObjectMeta,
+										Filters: HTTPFilters{
+											RequestMirrors: []*HTTPRequestMirrorFilter{
+												{
+													Name:   helpers.GetPointer("mirror-backend"),
+													Target: helpers.GetPointer("/_ngf-internal-mirror-mirror-backend-0"),
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+						Port: 80,
+					},
+				}...)
+				conf.SSLServers = []VirtualServer{}
+				conf.Upstreams = []Upstream{fooUpstream}
+				conf.BackendGroups = []BackendGroup{expHRWithMirrorGroups[0]}
+				conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{}
+				return conf
+			}),
+			msg: "one http listener with one route containing a request mirror filter",
+		},
 		{
 			graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph {
 				g.Gateway.Source.ObjectMeta = metav1.ObjectMeta{
@@ -2562,6 +2658,22 @@ func TestBuildConfiguration_Plus(t *testing.T) {
 	}
 }
 
+func TestNewBackendGroup_Mirror(t *testing.T) {
+	t.Parallel()
+	g := NewWithT(t)
+
+	backendRef := graph.BackendRef{
+		SvcNsName:       types.NamespacedName{Name: "mirror-backend", Namespace: "test"},
+		ServicePort:     apiv1.ServicePort{Port: 80},
+		Valid:           true,
+		IsMirrorBackend: true,
+	}
+
+	group := newBackendGroup([]graph.BackendRef{backendRef}, types.NamespacedName{}, 0)
+
+	g.Expect(group.Backends).To(BeEmpty())
+}
+
 func TestGetPath(t *testing.T) {
 	t.Parallel()
 	tests := []struct {
@@ -2628,6 +2740,26 @@ func TestCreateFilters(t *testing.T) {
 			Hostname: helpers.GetPointer[v1.PreciseHostname]("bar.example.com"),
 		},
 	}
+	mirror1 := graph.Filter{
+		FilterType: graph.FilterRequestMirror,
+		RequestMirror: &v1.HTTPRequestMirrorFilter{
+			BackendRef: v1.BackendObjectReference{
+				Group: helpers.GetPointer(v1.Group("core")),
+				Kind:  helpers.GetPointer(v1.Kind("Service")),
+				Name:  v1.ObjectName("mirror-backend"),
+			},
+		},
+	}
+	mirror2 := graph.Filter{
+		FilterType: graph.FilterRequestMirror,
+		RequestMirror: &v1.HTTPRequestMirrorFilter{
+			BackendRef: v1.BackendObjectReference{
+				Group: helpers.GetPointer(v1.Group("core")),
+				Kind:  helpers.GetPointer(v1.Kind("Service")),
+				Name:  v1.ObjectName("mirror-backend2"),
+			},
+		},
+	}
 	requestHeaderModifiers1 := graph.Filter{
 		FilterType: graph.FilterRequestHeaderModifier,
 		RequestHeaderModifier: &v1.HTTPHeaderFilter{
@@ -2681,6 +2813,16 @@ func TestCreateFilters(t *testing.T) {
 	expectedRewrite1 := HTTPURLRewriteFilter{
 		Hostname: helpers.GetPointer("foo.example.com"),
 	}
+
+	expectedMirror1 := HTTPRequestMirrorFilter{
+		Name:   helpers.GetPointer("mirror-backend"),
+		Target: helpers.GetPointer("/_ngf-internal-mirror-mirror-backend-0"),
+	}
+	expectedMirror2 := HTTPRequestMirrorFilter{
+		Name:   helpers.GetPointer("mirror-backend2"),
+		Target: helpers.GetPointer("/_ngf-internal-mirror-mirror-backend2-0"),
+	}
+
 	expectedHeaderModifier1 := HTTPHeaderFilter{
 		Set: []HTTPHeader{
 			{
@@ -2780,6 +2922,8 @@ func TestCreateFilters(t *testing.T) {
 				redirect2,
 				rewrite1,
 				rewrite2,
+				mirror1,
+				mirror2,
 				requestHeaderModifiers1,
 				requestHeaderModifiers2,
 				responseHeaderModifiers1,
@@ -2788,8 +2932,12 @@ func TestCreateFilters(t *testing.T) {
 				snippetsFilter2,
 			},
 			expected: HTTPFilters{
-				RequestRedirect:         &expectedRedirect1,
-				RequestURLRewrite:       &expectedRewrite1,
+				RequestRedirect:   &expectedRedirect1,
+				RequestURLRewrite: &expectedRewrite1,
+				RequestMirrors: []*HTTPRequestMirrorFilter{
+					&expectedMirror1,
+					&expectedMirror2,
+				},
 				RequestHeaderModifiers:  &expectedHeaderModifier1,
 				ResponseHeaderModifiers: &expectedresponseHeaderModifier,
 				SnippetsFilters: []SnippetsFilter{
@@ -2827,7 +2975,7 @@ func TestCreateFilters(t *testing.T) {
 					},
 				},
 			},
-			msg: "two of each filter, first value for each standard filter wins, all ext ref filters added",
+			msg: "two of each filter, first value for each standard filter wins, all mirror and ext ref filters added",
 		},
 	}
 
@@ -2835,7 +2983,7 @@ func TestCreateFilters(t *testing.T) {
 		t.Run(test.msg, func(t *testing.T) {
 			t.Parallel()
 			g := NewWithT(t)
-			result := createHTTPFilters(test.filters)
+			result := createHTTPFilters(test.filters, 0)
 
 			g.Expect(helpers.Diff(test.expected, result)).To(BeEmpty())
 		})
diff --git a/internal/mode/static/state/dataplane/convert.go b/internal/mode/static/state/dataplane/convert.go
index d44a47ed7a..b1b1a4c635 100644
--- a/internal/mode/static/state/dataplane/convert.go
+++ b/internal/mode/static/state/dataplane/convert.go
@@ -7,7 +7,9 @@ import (
 	v1 "sigs.k8s.io/gateway-api/apis/v1"
 
 	ngfAPI "github.com/nginx/nginx-gateway-fabric/apis/v1alpha1"
+	"github.com/nginx/nginx-gateway-fabric/internal/framework/helpers"
 	"github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/graph"
+	"github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/mirror"
 )
 
 func convertMatch(m v1.HTTPRouteMatch) Match {
@@ -60,6 +62,25 @@ func convertHTTPURLRewriteFilter(filter *v1.HTTPURLRewriteFilter) *HTTPURLRewrit
 	}
 }
 
+func convertHTTPRequestMirrorFilter(filter *v1.HTTPRequestMirrorFilter, ruleIdx int) *HTTPRequestMirrorFilter {
+	if filter.BackendRef.Name == "" {
+		return &HTTPRequestMirrorFilter{}
+	}
+
+	result := &HTTPRequestMirrorFilter{
+		Name: helpers.GetPointer(string(filter.BackendRef.Name)),
+	}
+
+	namespace := (*string)(filter.BackendRef.Namespace)
+	if namespace != nil && len(*namespace) > 0 {
+		result.Namespace = namespace
+	}
+
+	result.Target = mirror.BackendPath(ruleIdx, namespace, *result.Name)
+
+	return result
+}
+
 func convertHTTPHeaderFilter(filter *v1.HTTPHeaderFilter) *HTTPHeaderFilter {
 	result := &HTTPHeaderFilter{
 		Remove: filter.Remove,
diff --git a/internal/mode/static/state/dataplane/convert_test.go b/internal/mode/static/state/dataplane/convert_test.go
index f3116fab79..77b4c689ee 100644
--- a/internal/mode/static/state/dataplane/convert_test.go
+++ b/internal/mode/static/state/dataplane/convert_test.go
@@ -316,6 +316,57 @@ func TestConvertHTTPURLRewriteFilter(t *testing.T) {
 	}
 }
 
+func TestConvertHTTPMirrorFilter(t *testing.T) {
+	tests := []struct {
+		filter   *v1.HTTPRequestMirrorFilter
+		expected *HTTPRequestMirrorFilter
+		name     string
+	}{
+		{
+			filter:   &v1.HTTPRequestMirrorFilter{},
+			expected: &HTTPRequestMirrorFilter{},
+			name:     "empty",
+		},
+		{
+			filter: &v1.HTTPRequestMirrorFilter{
+				BackendRef: v1.BackendObjectReference{
+					Name:      "backend",
+					Namespace: nil,
+				},
+			},
+			expected: &HTTPRequestMirrorFilter{
+				Name:      helpers.GetPointer("backend"),
+				Namespace: nil,
+				Target:    helpers.GetPointer("/_ngf-internal-mirror-backend-0"),
+			},
+			name: "missing namespace",
+		},
+		{
+			filter: &v1.HTTPRequestMirrorFilter{
+				BackendRef: v1.BackendObjectReference{
+					Name:      "backend",
+					Namespace: helpers.GetPointer[v1.Namespace]("namespace"),
+				},
+			},
+			expected: &HTTPRequestMirrorFilter{
+				Name:      helpers.GetPointer("backend"),
+				Namespace: helpers.GetPointer("namespace"),
+				Target:    helpers.GetPointer("/_ngf-internal-mirror-namespace/backend-0"),
+			},
+			name: "full",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			result := convertHTTPRequestMirrorFilter(test.filter, 0)
+			g.Expect(result).To(Equal(test.expected))
+		})
+	}
+}
+
 func TestConvertHTTPHeaderFilter(t *testing.T) {
 	t.Parallel()
 	tests := []struct {
diff --git a/internal/mode/static/state/dataplane/types.go b/internal/mode/static/state/dataplane/types.go
index d94f8e6032..55e5476f51 100644
--- a/internal/mode/static/state/dataplane/types.go
+++ b/internal/mode/static/state/dataplane/types.go
@@ -149,6 +149,8 @@ type HTTPFilters struct {
 	RequestRedirect *HTTPRequestRedirectFilter
 	// RequestURLRewrite holds the HTTPURLRewriteFilter.
 	RequestURLRewrite *HTTPURLRewriteFilter
+	// RequestMirrors holds the HTTPRequestMirrorFilters. There could be more than one specified.
+	RequestMirrors []*HTTPRequestMirrorFilter
 	// RequestHeaderModifiers holds the HTTPHeaderFilter.
 	RequestHeaderModifiers *HTTPHeaderFilter
 	// ResponseHeaderModifiers holds the HTTPHeaderFilter.
@@ -207,6 +209,16 @@ type HTTPURLRewriteFilter struct {
 	Path *HTTPPathModifier
 }
 
+// HTTPRequestMirrorFilter mirrors HTTP requests.
+type HTTPRequestMirrorFilter struct {
+	// Name is the service name.
+	Name *string
+	// Namespace is the namespace of the service.
+	Namespace *string
+	// Target is the target of the mirror (path with hostname and service name).
+	Target *string
+}
+
 // PathModifierType is the type of the PathModifier in a redirect or rewrite rule.
 type PathModifierType string
 
diff --git a/internal/mode/static/state/graph/backend_refs.go b/internal/mode/static/state/graph/backend_refs.go
index 76ee3ab35e..ad676f59c5 100644
--- a/internal/mode/static/state/graph/backend_refs.go
+++ b/internal/mode/static/state/graph/backend_refs.go
@@ -32,6 +32,8 @@ type BackendRef struct {
 	// Valid indicates whether the backendRef is valid.
 	// No configuration should be generated for an invalid BackendRef.
 	Valid bool
+	// IsMirrorBackend indicates whether the BackendGroup is for a mirrored backend.
+	IsMirrorBackend bool
 }
 
 // ServicePortReference returns a string representation for the service and port that is referenced by the BackendRef.
@@ -83,7 +85,11 @@ func addBackendRefsToRules(
 		backendRefs := make([]BackendRef, 0, len(rule.RouteBackendRefs))
 
 		for refIdx, ref := range rule.RouteBackendRefs {
-			refPath := field.NewPath("spec").Child("rules").Index(idx).Child("backendRefs").Index(refIdx)
+			basePath := field.NewPath("spec").Child("rules").Index(idx)
+			refPath := basePath.Child("backendRefs").Index(refIdx)
+			if ref.MirrorBackendIdx != nil {
+				refPath = basePath.Child("filters").Index(*ref.MirrorBackendIdx).Child("backendRef")
+			}
 			routeNs := route.Source.GetNamespace()
 
 			ref, cond := createBackendRef(
@@ -143,8 +149,9 @@ func createBackendRef(
 	valid, cond := validateRouteBackendRef(ref, sourceNamespace, refGrantResolver, refPath)
 	if !valid {
 		backendRef = BackendRef{
-			Weight: weight,
-			Valid:  false,
+			Weight:          weight,
+			Valid:           false,
+			IsMirrorBackend: ref.MirrorBackendIdx != nil,
 		}
 
 		return backendRef, &cond
@@ -158,10 +165,11 @@ func createBackendRef(
 	svcIPFamily, svcPort, err := getIPFamilyAndPortFromRef(ref.BackendRef, svcNsName, services, refPath)
 	if err != nil {
 		backendRef = BackendRef{
-			Weight:      weight,
-			Valid:       false,
-			SvcNsName:   svcNsName,
-			ServicePort: v1.ServicePort{},
+			Weight:          weight,
+			Valid:           false,
+			SvcNsName:       svcNsName,
+			ServicePort:     v1.ServicePort{},
+			IsMirrorBackend: ref.MirrorBackendIdx != nil,
 		}
 
 		cond := staticConds.NewRouteBackendRefRefBackendNotFound(err.Error())
@@ -170,10 +178,11 @@ func createBackendRef(
 
 	if err := verifyIPFamily(npCfg, svcIPFamily); err != nil {
 		backendRef = BackendRef{
-			SvcNsName:   svcNsName,
-			ServicePort: svcPort,
-			Weight:      weight,
-			Valid:       false,
+			SvcNsName:       svcNsName,
+			ServicePort:     svcPort,
+			Weight:          weight,
+			Valid:           false,
+			IsMirrorBackend: ref.MirrorBackendIdx != nil,
 		}
 
 		cond := staticConds.NewRouteInvalidIPFamily(err.Error())
@@ -188,10 +197,11 @@ func createBackendRef(
 	)
 	if err != nil {
 		backendRef = BackendRef{
-			SvcNsName:   svcNsName,
-			ServicePort: svcPort,
-			Weight:      weight,
-			Valid:       false,
+			SvcNsName:       svcNsName,
+			ServicePort:     svcPort,
+			Weight:          weight,
+			Valid:           false,
+			IsMirrorBackend: ref.MirrorBackendIdx != nil,
 		}
 
 		cond := staticConds.NewRouteBackendRefUnsupportedValue(err.Error())
@@ -204,6 +214,7 @@ func createBackendRef(
 		ServicePort:      svcPort,
 		Valid:            true,
 		Weight:           weight,
+		IsMirrorBackend:  ref.MirrorBackendIdx != nil,
 	}
 
 	return backendRef, nil
@@ -385,8 +396,9 @@ func validateBackendRef(
 
 		if !refGrantResolver(toService(refNsName)) {
 			msg := fmt.Sprintf("Backend ref to Service %s not permitted by any ReferenceGrant", refNsName)
+			valErr := field.Forbidden(path.Child("namespace"), msg)
 
-			return false, staticConds.NewRouteBackendRefRefNotPermitted(msg)
+			return false, staticConds.NewRouteBackendRefRefNotPermitted(valErr.Error())
 		}
 	}
 
diff --git a/internal/mode/static/state/graph/backend_refs_test.go b/internal/mode/static/state/graph/backend_refs_test.go
index ba6e2a2d2d..0d43456eed 100644
--- a/internal/mode/static/state/graph/backend_refs_test.go
+++ b/internal/mode/static/state/graph/backend_refs_test.go
@@ -176,7 +176,7 @@ func TestValidateBackendRef(t *testing.T) {
 			refGrantResolver: alwaysFalseRefGrantResolver,
 			expectedValid:    false,
 			expectedCondition: staticConds.NewRouteBackendRefRefNotPermitted(
-				"Backend ref to Service invalid/service1 not permitted by any ReferenceGrant",
+				"test.namespace: Forbidden: Backend ref to Service invalid/service1 not permitted by any ReferenceGrant",
 			),
 		},
 		{
@@ -1024,14 +1024,15 @@ func TestCreateBackend(t *testing.T) {
 
 	refPath := field.NewPath("test")
 
+	alwaysTrueRefGrantResolver := func(_ toResource) bool { return true }
+
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			t.Parallel()
 			g := NewWithT(t)
 
-			alwaysTrueRefGrantResolver := func(_ toResource) bool { return true }
-
 			rbr := RouteBackendRef{
+				nil,
 				test.ref.BackendRef,
 				[]any{},
 			}
@@ -1052,6 +1053,27 @@ func TestCreateBackend(t *testing.T) {
 			g.Expect(servicePortRef).To(Equal(test.expectedServicePortReference))
 		})
 	}
+
+	// test mirror backend case
+	g := NewWithT(t)
+	ref := RouteBackendRef{
+		helpers.GetPointer(0), // mirrorFilterIdx
+		getNormalRef(),
+		[]any{},
+	}
+
+	backend, conds := createBackendRef(
+		ref,
+		"test-ns",
+		alwaysTrueRefGrantResolver,
+		services,
+		refPath,
+		policies,
+		nil,
+	)
+
+	g.Expect(conds).To(BeNil())
+	g.Expect(backend.IsMirrorBackend).To(BeTrue())
 }
 
 func TestGetServicePort(t *testing.T) {
diff --git a/internal/mode/static/state/graph/common_filter.go b/internal/mode/static/state/graph/common_filter.go
index 6bd71cf8e2..c67d233cda 100644
--- a/internal/mode/static/state/graph/common_filter.go
+++ b/internal/mode/static/state/graph/common_filter.go
@@ -160,6 +160,7 @@ func processRouteRuleFilters(
 var supportedGRPCFilterTypes = []FilterType{
 	FilterResponseHeaderModifier,
 	FilterRequestHeaderModifier,
+	FilterRequestMirror,
 	FilterExtensionRef,
 }
 
@@ -169,6 +170,7 @@ var supportedHTTPFilterTypes = []FilterType{
 	FilterExtensionRef,
 	FilterRequestRedirect,
 	FilterURLRewrite,
+	FilterRequestMirror,
 }
 
 func validateFilterType(filter Filter, filterPath *field.Path) *field.Error {
@@ -200,6 +202,8 @@ func validateFilter(
 		return validateFilterRedirect(validator, filter.RequestRedirect, filterPath)
 	case FilterURLRewrite:
 		return validateFilterRewrite(validator, filter.URLRewrite, filterPath)
+	case FilterRequestMirror:
+		return validateFilterMirror(filter.RequestMirror, filterPath)
 	case FilterRequestHeaderModifier:
 		return validateFilterHeaderModifier(
 			validator,
@@ -219,6 +223,16 @@ func validateFilter(
 	}
 }
 
+func validateFilterMirror(mirror *v1.HTTPRequestMirrorFilter, filterPath *field.Path) field.ErrorList {
+	mirrorPath := filterPath.Child("requestMirror")
+
+	if mirror == nil {
+		return field.ErrorList{field.Required(mirrorPath, "cannot be nil")}
+	}
+
+	return nil
+}
+
 func validateFilterHeaderModifier(
 	validator validation.HTTPFieldsValidator,
 	headerModifier *v1.HTTPHeaderFilter,
diff --git a/internal/mode/static/state/graph/common_filter_test.go b/internal/mode/static/state/graph/common_filter_test.go
index 680938ea2c..f365a2cde0 100644
--- a/internal/mode/static/state/graph/common_filter_test.go
+++ b/internal/mode/static/state/graph/common_filter_test.go
@@ -39,6 +39,24 @@ func TestValidateFilter(t *testing.T) {
 			expectErrCount: 0,
 			name:           "valid HTTP rewrite filter",
 		},
+		{
+			filter: Filter{
+				RouteType:     RouteTypeHTTP,
+				FilterType:    FilterRequestMirror,
+				RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{},
+			},
+			expectErrCount: 0,
+			name:           "valid HTTP mirror filter",
+		},
+		{
+			filter: Filter{
+				RouteType:     RouteTypeHTTP,
+				FilterType:    FilterRequestMirror,
+				RequestMirror: nil,
+			},
+			expectErrCount: 1,
+			name:           "invalid HTTP mirror filter",
+		},
 		{
 			filter: Filter{
 				RouteType:             RouteTypeHTTP,
@@ -73,11 +91,20 @@ func TestValidateFilter(t *testing.T) {
 		{
 			filter: Filter{
 				RouteType:  RouteTypeHTTP,
-				FilterType: FilterRequestMirror,
+				FilterType: "invalid-filter",
 			},
 			expectErrCount: 1,
 			name:           "unsupported HTTP filter type",
 		},
+		{
+			filter: Filter{
+				RouteType:     RouteTypeGRPC,
+				FilterType:    FilterRequestMirror,
+				RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{},
+			},
+			expectErrCount: 0,
+			name:           "valid GRPC mirror filter",
+		},
 		{
 			filter: Filter{
 				RouteType:             RouteTypeGRPC,
diff --git a/internal/mode/static/state/graph/extension_ref_filter.go b/internal/mode/static/state/graph/extension_ref_filter.go
index 7587a69c0d..b758542465 100644
--- a/internal/mode/static/state/graph/extension_ref_filter.go
+++ b/internal/mode/static/state/graph/extension_ref_filter.go
@@ -28,7 +28,7 @@ func validateExtensionRefFilter(ref *v1.LocalObjectReference, path *field.Path)
 	extRefPath := path.Child("extensionRef")
 
 	if ref == nil {
-		return field.ErrorList{field.Required(extRefPath, "extensionRef cannot be nil")}
+		return field.ErrorList{field.Required(extRefPath, "cannot be nil")}
 	}
 
 	if ref.Name == "" {
diff --git a/internal/mode/static/state/graph/extension_ref_filter_test.go b/internal/mode/static/state/graph/extension_ref_filter_test.go
index bcc5539b73..ae485a664f 100644
--- a/internal/mode/static/state/graph/extension_ref_filter_test.go
+++ b/internal/mode/static/state/graph/extension_ref_filter_test.go
@@ -26,7 +26,7 @@ func TestValidateExtensionRefFilter(t *testing.T) {
 			ref:         nil,
 			expErrCount: 1,
 			errSubString: []string{
-				`test.extensionRef: Required value: extensionRef cannot be nil`,
+				`test.extensionRef: Required value: cannot be nil`,
 			},
 		},
 		{
diff --git a/internal/mode/static/state/graph/grpcroute.go b/internal/mode/static/state/graph/grpcroute.go
index 7da7bf20d6..aaacdb0fff 100644
--- a/internal/mode/static/state/graph/grpcroute.go
+++ b/internal/mode/static/state/graph/grpcroute.go
@@ -1,13 +1,18 @@
 package graph
 
 import (
+	"fmt"
+	"strings"
+
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 	v1 "sigs.k8s.io/gateway-api/apis/v1"
 
 	"github.com/nginx/nginx-gateway-fabric/internal/framework/conditions"
 	"github.com/nginx/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginx/nginx-gateway-fabric/internal/mode/static/nginx/config/http"
 	staticConds "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/conditions"
+	"github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/mirror"
 	"github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/validation"
 )
 
@@ -69,6 +74,92 @@ func buildGRPCRoute(
 	return r
 }
 
+func buildGRPCMirrorRoutes(
+	routes map[RouteKey]*L7Route,
+	l7route *L7Route,
+	route *v1.GRPCRoute,
+	gatewayNsNames []types.NamespacedName,
+	snippetsFilters map[types.NamespacedName]*SnippetsFilter,
+	http2disabled bool,
+) {
+	for idx, rule := range l7route.Spec.Rules {
+		if rule.Filters.Valid {
+			for _, filter := range rule.Filters.Filters {
+				if filter.RequestMirror == nil {
+					continue
+				}
+
+				objectMeta := route.ObjectMeta.DeepCopy()
+				backendRef := filter.RequestMirror.BackendRef
+				namespace := route.GetNamespace()
+				if backendRef.Namespace != nil {
+					namespace = string(*backendRef.Namespace)
+				}
+				name := mirror.RouteName(route.GetName(), string(backendRef.Name), namespace, idx)
+				objectMeta.SetName(name)
+
+				tmpMirrorRoute := &v1.GRPCRoute{
+					ObjectMeta: *objectMeta,
+					Spec: v1.GRPCRouteSpec{
+						CommonRouteSpec: route.Spec.CommonRouteSpec,
+						Hostnames:       route.Spec.Hostnames,
+						Rules:           buildGRPCMirrorRouteRule(idx, route.Spec.Rules[idx].Filters, filter),
+					},
+				}
+
+				mirrorRoute := buildGRPCRoute(
+					validation.SkipValidator{},
+					tmpMirrorRoute,
+					gatewayNsNames,
+					http2disabled,
+					snippetsFilters,
+				)
+
+				if mirrorRoute != nil {
+					routes[CreateRouteKey(tmpMirrorRoute)] = mirrorRoute
+				}
+			}
+		}
+	}
+}
+
+func buildGRPCMirrorRouteRule(
+	ruleIdx int,
+	filters []v1.GRPCRouteFilter,
+	filter Filter,
+) []v1.GRPCRouteRule {
+	return []v1.GRPCRouteRule{
+		{
+			Matches: []v1.GRPCRouteMatch{
+				{
+					Method: &v1.GRPCMethodMatch{
+						Type:    helpers.GetPointer(v1.GRPCMethodMatchExact),
+						Service: mirror.PathWithBackendRef(ruleIdx, filter.RequestMirror.BackendRef),
+					},
+				},
+			},
+			Filters: removeGRPCMirrorFilters(filters),
+			BackendRefs: []v1.GRPCBackendRef{
+				{
+					BackendRef: v1.BackendRef{
+						BackendObjectReference: filter.RequestMirror.BackendRef,
+					},
+				},
+			},
+		},
+	}
+}
+
+func removeGRPCMirrorFilters(filters []v1.GRPCRouteFilter) []v1.GRPCRouteFilter {
+	var newFilters []v1.GRPCRouteFilter
+	for _, filter := range filters {
+		if filter.Type != v1.GRPCRouteFilterRequestMirror {
+			newFilters = append(newFilters, filter)
+		}
+	}
+	return newFilters
+}
+
 func processGRPCRouteRule(
 	specRule v1.GRPCRouteRule,
 	rulePath *field.Path,
@@ -116,6 +207,22 @@ func processGRPCRouteRule(
 		backendRefs = append(backendRefs, rbr)
 	}
 
+	if routeFilters.Valid {
+		for i, filter := range routeFilters.Filters {
+			if filter.RequestMirror == nil {
+				continue
+			}
+
+			rbr := RouteBackendRef{
+				BackendRef: v1.BackendRef{
+					BackendObjectReference: filter.RequestMirror.BackendRef,
+				},
+				MirrorBackendIdx: helpers.GetPointer(i),
+			}
+			backendRefs = append(backendRefs, rbr)
+		}
+	}
+
 	return RouteRule{
 		ValidMatches:     validMatches,
 		Matches:          ConvertGRPCMatches(specRule.Matches),
@@ -139,7 +246,12 @@ func processGRPCRouteRules(
 	for i, rule := range specRules {
 		rulePath := field.NewPath("spec").Child("rules").Index(i)
 
-		rr, errors := processGRPCRouteRule(rule, rulePath, validator, resolveExtRefFunc)
+		rr, errors := processGRPCRouteRule(
+			rule,
+			rulePath,
+			validator,
+			resolveExtRefFunc,
+		)
 
 		if rr.ValidMatches && rr.Filters.Valid {
 			atLeastOneValid = true
@@ -204,12 +316,19 @@ func ConvertGRPCMatches(grpcMatches []v1.GRPCRouteMatch) []v1.HTTPRouteMatch {
 		}
 		hm.Headers = hmHeaders
 
-		if gm.Method != nil && gm.Method.Service != nil && gm.Method.Method != nil {
-			// if method match is provided, service and method are required
-			// as the only method type supported is exact.
-			// Validation has already been done at this point, and the condition will
-			// have been added there if required.
-			pathValue = "/" + *gm.Method.Service + "/" + *gm.Method.Method
+		if gm.Method != nil && gm.Method.Service != nil {
+			// service path used in mirror routes are special case; method is not specified
+			if strings.HasPrefix(*gm.Method.Service, http.InternalMirrorRoutePathPrefix) {
+				pathValue = *gm.Method.Service
+			}
+
+			if gm.Method.Method != nil {
+				// if method match is provided, service and method are required
+				// as the only method type supported is exact.
+				// Validation has already been done at this point, and the condition will
+				// have been added there if required.
+				pathValue = "/" + *gm.Method.Service + "/" + *gm.Method.Method
+			}
 			pathType = v1.PathMatchType("Exact")
 		}
 		hm.Path = &v1.HTTPPathMatch{
@@ -243,6 +362,11 @@ func validateGRPCMatch(
 ) field.ErrorList {
 	var allErrs field.ErrorList
 
+	// for internally-created routes used for request mirroring, we don't need to validate
+	if validator.SkipValidation() {
+		return nil
+	}
+
 	methodPath := matchPath.Child("method")
 	allErrs = append(allErrs, validateGRPCMethodMatch(validator, match.Method, methodPath)...)
 
@@ -275,6 +399,14 @@ func validateGRPCMethodMatch(
 		if method.Service == nil || *method.Service == "" {
 			allErrs = append(allErrs, field.Required(methodServicePath, "service is required"))
 		} else {
+			if strings.HasPrefix(*method.Service, http.InternalRoutePathPrefix) {
+				msg := fmt.Sprintf(
+					"service cannot start with %s. This prefix is reserved for internal use",
+					http.InternalRoutePathPrefix,
+				)
+				return field.ErrorList{field.Invalid(methodPath.Child("service"), *method.Service, msg)}
+			}
+
 			pathValue := "/" + *method.Service
 			if err := validator.ValidatePathInMatch(pathValue); err != nil {
 				valErr := field.Invalid(methodServicePath, *method.Service, err.Error())
diff --git a/internal/mode/static/state/graph/grpcroute_test.go b/internal/mode/static/state/graph/grpcroute_test.go
index 2adf83ee12..5417208d9e 100644
--- a/internal/mode/static/state/graph/grpcroute_test.go
+++ b/internal/mode/static/state/graph/grpcroute_test.go
@@ -15,6 +15,7 @@ import (
 	"github.com/nginx/nginx-gateway-fabric/internal/framework/helpers"
 	"github.com/nginx/nginx-gateway-fabric/internal/framework/kinds"
 	staticConds "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/conditions"
+	"github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/mirror"
 	"github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/validation/validationfakes"
 )
 
@@ -338,7 +339,7 @@ func TestBuildGRPCRoute(t *testing.T) {
 
 	grInvalidFilterRule.Filters = []v1.GRPCRouteFilter{
 		{
-			Type: "RequestMirror",
+			Type: "InvalidFilter",
 		},
 	}
 
@@ -902,8 +903,8 @@ func TestBuildGRPCRoute(t *testing.T) {
 				Conditions: []conditions.Condition{
 					staticConds.NewRouteUnsupportedValue(
 						`All rules are invalid: spec.rules[0].filters[0].type: Unsupported value: ` +
-							`"RequestMirror": supported values: "ResponseHeaderModifier", ` +
-							`"RequestHeaderModifier", "ExtensionRef"`,
+							`"InvalidFilter": supported values: "ResponseHeaderModifier", ` +
+							`"RequestHeaderModifier", "RequestMirror", "ExtensionRef"`,
 					),
 				},
 				Spec: L7RouteSpec{
@@ -1090,6 +1091,150 @@ func TestBuildGRPCRoute(t *testing.T) {
 	}
 }
 
+func TestBuildGRPCRouteWithMirrorRoutes(t *testing.T) {
+	t.Parallel()
+	gatewayNsName := types.NamespacedName{Namespace: "test", Name: "gateway"}
+
+	// Create a route with a request mirror filter and another random filter
+	mirrorFilter := v1.GRPCRouteFilter{
+		Type: v1.GRPCRouteFilterRequestMirror,
+		RequestMirror: &v1.HTTPRequestMirrorFilter{
+			BackendRef: v1.BackendObjectReference{
+				Name: "mirror-backend",
+			},
+		},
+	}
+
+	headerFilter := v1.GRPCRouteFilter{
+		Type: v1.GRPCRouteFilterRequestHeaderModifier,
+		RequestHeaderModifier: &v1.HTTPHeaderFilter{
+			Add: []v1.HTTPHeader{
+				{Name: "X-Custom-Header", Value: "some-value"},
+			},
+		},
+	}
+
+	gr := createGRPCRoute(
+		"gr",
+		gatewayNsName.Name,
+		"example.com",
+		[]v1.GRPCRouteRule{
+			{
+				Matches: []v1.GRPCRouteMatch{
+					{
+						Method: &v1.GRPCMethodMatch{
+							Type:    helpers.GetPointer(v1.GRPCMethodMatchExact),
+							Service: helpers.GetPointer("svc1"),
+							Method:  helpers.GetPointer("method"),
+						},
+					},
+				},
+				Filters: []v1.GRPCRouteFilter{mirrorFilter, headerFilter},
+			},
+		},
+	)
+
+	// Expected mirror route
+	expectedMirrorRoute := &L7Route{
+		RouteType: RouteTypeGRPC,
+		Source: &v1.GRPCRoute{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "test",
+				Name:      mirror.RouteName("gr", "mirror-backend", "test", 0),
+			},
+			Spec: v1.GRPCRouteSpec{
+				CommonRouteSpec: gr.Spec.CommonRouteSpec,
+				Hostnames:       gr.Spec.Hostnames,
+				Rules: []v1.GRPCRouteRule{
+					{
+						Matches: []v1.GRPCRouteMatch{
+							{
+								Method: &v1.GRPCMethodMatch{
+									Type:    helpers.GetPointer(v1.GRPCMethodMatchExact),
+									Service: helpers.GetPointer("/_ngf-internal-mirror-mirror-backend-0"),
+								},
+							},
+						},
+						Filters: []v1.GRPCRouteFilter{headerFilter},
+						BackendRefs: []v1.GRPCBackendRef{
+							{
+								BackendRef: v1.BackendRef{
+									BackendObjectReference: v1.BackendObjectReference{
+										Name: "mirror-backend",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		ParentRefs: []ParentRef{
+			{
+				Idx:         0,
+				Gateway:     gatewayNsName,
+				SectionName: gr.Spec.ParentRefs[0].SectionName,
+			},
+		},
+		Valid:      true,
+		Attachable: true,
+		Spec: L7RouteSpec{
+			Hostnames: gr.Spec.Hostnames,
+			Rules: []RouteRule{
+				{
+					ValidMatches: true,
+					Filters: RouteRuleFilters{
+						Valid: true,
+						Filters: []Filter{
+							{
+								RouteType:             RouteTypeGRPC,
+								FilterType:            FilterRequestHeaderModifier,
+								RequestHeaderModifier: headerFilter.RequestHeaderModifier,
+							},
+						},
+					},
+					Matches: []v1.HTTPRouteMatch{
+						{
+							Path: &v1.HTTPPathMatch{
+								Type:  helpers.GetPointer(v1.PathMatchExact),
+								Value: helpers.GetPointer("/_ngf-internal-mirror-mirror-backend-0"),
+							},
+							Headers: []v1.HTTPHeaderMatch{},
+						},
+					},
+					RouteBackendRefs: []RouteBackendRef{
+						{
+							BackendRef: v1.BackendRef{
+								BackendObjectReference: v1.BackendObjectReference{
+									Name: "mirror-backend",
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	validator := &validationfakes.FakeHTTPFieldsValidator{}
+	gatewayNsNames := []types.NamespacedName{gatewayNsName}
+	snippetsFilters := map[types.NamespacedName]*SnippetsFilter{}
+
+	g := NewWithT(t)
+
+	routes := map[RouteKey]*L7Route{}
+	l7route := buildGRPCRoute(validator, gr, gatewayNsNames, false, snippetsFilters)
+	g.Expect(l7route).NotTo(BeNil())
+
+	buildGRPCMirrorRoutes(routes, l7route, gr, gatewayNsNames, snippetsFilters, false)
+
+	obj, ok := expectedMirrorRoute.Source.(*v1.GRPCRoute)
+	g.Expect(ok).To(BeTrue())
+	mirrorRouteKey := CreateRouteKey(obj)
+	g.Expect(routes).To(HaveKey(mirrorRouteKey))
+	g.Expect(helpers.Diff(expectedMirrorRoute, routes[mirrorRouteKey])).To(BeEmpty())
+}
+
 func TestConvertGRPCMatches(t *testing.T) {
 	t.Parallel()
 	methodMatch := createGRPCMethodMatch("myService", "myMethod", "Exact").Matches
diff --git a/internal/mode/static/state/graph/httproute.go b/internal/mode/static/state/graph/httproute.go
index dd3258a4cd..f10df6965b 100644
--- a/internal/mode/static/state/graph/httproute.go
+++ b/internal/mode/static/state/graph/httproute.go
@@ -9,9 +9,10 @@ import (
 	v1 "sigs.k8s.io/gateway-api/apis/v1"
 
 	"github.com/nginx/nginx-gateway-fabric/internal/framework/conditions"
-
+	"github.com/nginx/nginx-gateway-fabric/internal/framework/helpers"
 	"github.com/nginx/nginx-gateway-fabric/internal/mode/static/nginx/config/http"
 	staticConds "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/conditions"
+	"github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/mirror"
 	"github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/validation"
 )
 
@@ -70,6 +71,90 @@ func buildHTTPRoute(
 	return r
 }
 
+func buildHTTPMirrorRoutes(
+	routes map[RouteKey]*L7Route,
+	l7route *L7Route,
+	route *v1.HTTPRoute,
+	gatewayNsNames []types.NamespacedName,
+	snippetsFilters map[types.NamespacedName]*SnippetsFilter,
+) {
+	for idx, rule := range l7route.Spec.Rules {
+		if rule.Filters.Valid {
+			for _, filter := range rule.Filters.Filters {
+				if filter.RequestMirror == nil {
+					continue
+				}
+
+				objectMeta := route.ObjectMeta.DeepCopy()
+				backendRef := filter.RequestMirror.BackendRef
+				namespace := route.GetNamespace()
+				if backendRef.Namespace != nil {
+					namespace = string(*backendRef.Namespace)
+				}
+				name := mirror.RouteName(route.GetName(), string(backendRef.Name), namespace, idx)
+				objectMeta.SetName(name)
+
+				tmpMirrorRoute := &v1.HTTPRoute{
+					ObjectMeta: *objectMeta,
+					Spec: v1.HTTPRouteSpec{
+						CommonRouteSpec: route.Spec.CommonRouteSpec,
+						Hostnames:       route.Spec.Hostnames,
+						Rules:           buildHTTPMirrorRouteRule(idx, route.Spec.Rules[idx].Filters, filter),
+					},
+				}
+
+				mirrorRoute := buildHTTPRoute(
+					validation.SkipValidator{},
+					tmpMirrorRoute,
+					gatewayNsNames,
+					snippetsFilters,
+				)
+
+				if mirrorRoute != nil {
+					routes[CreateRouteKey(tmpMirrorRoute)] = mirrorRoute
+				}
+			}
+		}
+	}
+}
+
+func buildHTTPMirrorRouteRule(
+	ruleIdx int,
+	filters []v1.HTTPRouteFilter,
+	filter Filter,
+) []v1.HTTPRouteRule {
+	return []v1.HTTPRouteRule{
+		{
+			Matches: []v1.HTTPRouteMatch{
+				{
+					Path: &v1.HTTPPathMatch{
+						Type:  helpers.GetPointer(v1.PathMatchExact),
+						Value: mirror.PathWithBackendRef(ruleIdx, filter.RequestMirror.BackendRef),
+					},
+				},
+			},
+			Filters: removeHTTPMirrorFilters(filters),
+			BackendRefs: []v1.HTTPBackendRef{
+				{
+					BackendRef: v1.BackendRef{
+						BackendObjectReference: filter.RequestMirror.BackendRef,
+					},
+				},
+			},
+		},
+	}
+}
+
+func removeHTTPMirrorFilters(filters []v1.HTTPRouteFilter) []v1.HTTPRouteFilter {
+	var newFilters []v1.HTTPRouteFilter
+	for _, filter := range filters {
+		if filter.Type != v1.HTTPRouteFilterRequestMirror {
+			newFilters = append(newFilters, filter)
+		}
+	}
+	return newFilters
+}
+
 func processHTTPRouteRule(
 	specRule v1.HTTPRouteRule,
 	rulePath *field.Path,
@@ -117,6 +202,22 @@ func processHTTPRouteRule(
 		backendRefs = append(backendRefs, rbr)
 	}
 
+	if routeFilters.Valid {
+		for i, filter := range routeFilters.Filters {
+			if filter.RequestMirror == nil {
+				continue
+			}
+
+			rbr := RouteBackendRef{
+				BackendRef: v1.BackendRef{
+					BackendObjectReference: filter.RequestMirror.BackendRef,
+				},
+				MirrorBackendIdx: helpers.GetPointer[int](i),
+			}
+			backendRefs = append(backendRefs, rbr)
+		}
+	}
+
 	return RouteRule{
 		ValidMatches:     validMatches,
 		Matches:          specRule.Matches,
@@ -140,7 +241,12 @@ func processHTTPRouteRules(
 	for i, rule := range specRules {
 		rulePath := field.NewPath("spec").Child("rules").Index(i)
 
-		rr, errors := processHTTPRouteRule(rule, rulePath, validator, resolveExtRefFunc)
+		rr, errors := processHTTPRouteRule(
+			rule,
+			rulePath,
+			validator,
+			resolveExtRefFunc,
+		)
 
 		if rr.ValidMatches && rr.Filters.Valid {
 			atLeastOneValid = true
@@ -183,6 +289,11 @@ func validateMatch(
 ) field.ErrorList {
 	var allErrs field.ErrorList
 
+	// for internally-created routes used for request mirroring, we don't need to validate
+	if validator.SkipValidation() {
+		return nil
+	}
+
 	pathPath := matchPath.Child("path")
 	allErrs = append(allErrs, validatePathMatch(validator, match.Path, pathPath)...)
 
diff --git a/internal/mode/static/state/graph/httproute_test.go b/internal/mode/static/state/graph/httproute_test.go
index d08f00b9ed..ce0f794bc6 100644
--- a/internal/mode/static/state/graph/httproute_test.go
+++ b/internal/mode/static/state/graph/httproute_test.go
@@ -16,6 +16,7 @@ import (
 	"github.com/nginx/nginx-gateway-fabric/internal/framework/helpers"
 	"github.com/nginx/nginx-gateway-fabric/internal/framework/kinds"
 	staticConds "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/conditions"
+	"github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/mirror"
 	"github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/validation/validationfakes"
 )
 
@@ -912,6 +913,129 @@ func TestBuildHTTPRoute(t *testing.T) {
 	}
 }
 
+func TestBuildHTTPRouteWithMirrorRoutes(t *testing.T) {
+	t.Parallel()
+	gatewayNsName := types.NamespacedName{Namespace: "test", Name: "gateway"}
+
+	// Create a route with a request mirror filter and another random filter
+	mirrorFilter := gatewayv1.HTTPRouteFilter{
+		Type: gatewayv1.HTTPRouteFilterRequestMirror,
+		RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{
+			BackendRef: gatewayv1.BackendObjectReference{
+				Name: "mirror-backend",
+			},
+		},
+	}
+	urlRewriteFilter := gatewayv1.HTTPRouteFilter{
+		Type: gatewayv1.HTTPRouteFilterURLRewrite,
+		URLRewrite: &gatewayv1.HTTPURLRewriteFilter{
+			Hostname: helpers.GetPointer[gatewayv1.PreciseHostname]("hostname"),
+		},
+	}
+	hr := createHTTPRoute("hr", gatewayNsName.Name, "example.com", "/mirror")
+	addFilterToPath(hr, "/mirror", mirrorFilter)
+	addFilterToPath(hr, "/mirror", urlRewriteFilter)
+
+	// Expected mirror route
+	expectedMirrorRoute := &L7Route{
+		RouteType: RouteTypeHTTP,
+		Source: &gatewayv1.HTTPRoute{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "test",
+				Name:      mirror.RouteName("hr", "mirror-backend", "test", 0),
+			},
+			Spec: gatewayv1.HTTPRouteSpec{
+				CommonRouteSpec: hr.Spec.CommonRouteSpec,
+				Hostnames:       hr.Spec.Hostnames,
+				Rules: []gatewayv1.HTTPRouteRule{
+					{
+						Matches: []gatewayv1.HTTPRouteMatch{
+							{
+								Path: &gatewayv1.HTTPPathMatch{
+									Type:  helpers.GetPointer(gatewayv1.PathMatchExact),
+									Value: helpers.GetPointer("/_ngf-internal-mirror-mirror-backend-0"),
+								},
+							},
+						},
+						Filters: []gatewayv1.HTTPRouteFilter{urlRewriteFilter},
+						BackendRefs: []gatewayv1.HTTPBackendRef{
+							{
+								BackendRef: gatewayv1.BackendRef{
+									BackendObjectReference: gatewayv1.BackendObjectReference{
+										Name: "mirror-backend",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		ParentRefs: []ParentRef{
+			{
+				Idx:         0,
+				Gateway:     gatewayNsName,
+				SectionName: hr.Spec.ParentRefs[0].SectionName,
+			},
+		},
+		Valid:      true,
+		Attachable: true,
+		Spec: L7RouteSpec{
+			Hostnames: hr.Spec.Hostnames,
+			Rules: []RouteRule{
+				{
+					ValidMatches: true,
+					Filters: RouteRuleFilters{
+						Valid: true,
+						Filters: []Filter{
+							{
+								RouteType:  RouteTypeHTTP,
+								FilterType: FilterURLRewrite,
+								URLRewrite: urlRewriteFilter.URLRewrite,
+							},
+						},
+					},
+					Matches: []gatewayv1.HTTPRouteMatch{
+						{
+							Path: &gatewayv1.HTTPPathMatch{
+								Type:  helpers.GetPointer(gatewayv1.PathMatchExact),
+								Value: helpers.GetPointer("/_ngf-internal-mirror-mirror-backend-0"),
+							},
+						},
+					},
+					RouteBackendRefs: []RouteBackendRef{
+						{
+							BackendRef: gatewayv1.BackendRef{
+								BackendObjectReference: gatewayv1.BackendObjectReference{
+									Name: "mirror-backend",
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	validator := &validationfakes.FakeHTTPFieldsValidator{}
+	gatewayNsNames := []types.NamespacedName{gatewayNsName}
+	snippetsFilters := map[types.NamespacedName]*SnippetsFilter{}
+
+	g := NewWithT(t)
+
+	routes := map[RouteKey]*L7Route{}
+	l7route := buildHTTPRoute(validator, hr, gatewayNsNames, snippetsFilters)
+	g.Expect(l7route).NotTo(BeNil())
+
+	buildHTTPMirrorRoutes(routes, l7route, hr, gatewayNsNames, snippetsFilters)
+
+	obj, ok := expectedMirrorRoute.Source.(*gatewayv1.HTTPRoute)
+	g.Expect(ok).To(BeTrue())
+	mirrorRouteKey := CreateRouteKey(obj)
+	g.Expect(routes).To(HaveKey(mirrorRouteKey))
+	g.Expect(helpers.Diff(expectedMirrorRoute, routes[mirrorRouteKey])).To(BeEmpty())
+}
+
 func TestValidateMatch(t *testing.T) {
 	t.Parallel()
 	createAllValidValidator := func() *validationfakes.FakeHTTPFieldsValidator {
@@ -920,6 +1044,9 @@ func TestValidateMatch(t *testing.T) {
 		return v
 	}
 
+	skipValidator := validationfakes.FakeHTTPFieldsValidator{}
+	skipValidator.SkipValidationReturns(true)
+
 	tests := []struct {
 		match          gatewayv1.HTTPRouteMatch
 		validator      *validationfakes.FakeHTTPFieldsValidator
@@ -1165,6 +1292,11 @@ func TestValidateMatch(t *testing.T) {
 			expectErrCount: 3,
 			name:           "multiple errors",
 		},
+		{
+			validator:      &skipValidator,
+			expectErrCount: 0,
+			name:           "skip validation",
+		},
 	}
 
 	for _, test := range tests {
diff --git a/internal/mode/static/state/graph/route_common.go b/internal/mode/static/state/graph/route_common.go
index 0e50c19f9c..f146a55adb 100644
--- a/internal/mode/static/state/graph/route_common.go
+++ b/internal/mode/static/state/graph/route_common.go
@@ -139,6 +139,9 @@ type RouteRule struct {
 
 // RouteBackendRef is a wrapper for v1.BackendRef and any BackendRef filters from the HTTPRoute or GRPCRoute.
 type RouteBackendRef struct {
+	// If this backend is defined in a RequestMirror filter, this value will indicate the filter's index.
+	MirrorBackendIdx *int
+
 	v1.BackendRef
 	Filters []any
 }
@@ -229,16 +232,26 @@ func buildRoutesForGateways(
 
 	for _, route := range httpRoutes {
 		r := buildHTTPRoute(validator, route, gatewayNsNames, snippetsFilters)
-		if r != nil {
-			routes[CreateRouteKey(route)] = r
+		if r == nil {
+			continue
 		}
+
+		routes[CreateRouteKey(route)] = r
+
+		// if this route has a RequestMirror filter, build a duplicate route for the mirror
+		buildHTTPMirrorRoutes(routes, r, route, gatewayNsNames, snippetsFilters)
 	}
 
 	for _, route := range grpcRoutes {
 		r := buildGRPCRoute(validator, route, gatewayNsNames, http2disabled, snippetsFilters)
-		if r != nil {
-			routes[CreateRouteKey(route)] = r
+		if r == nil {
+			continue
 		}
+
+		routes[CreateRouteKey(route)] = r
+
+		// if this route has a RequestMirror filter, build a duplicate route for the mirror
+		buildGRPCMirrorRoutes(routes, r, route, gatewayNsNames, snippetsFilters, http2disabled)
 	}
 
 	return routes
diff --git a/internal/mode/static/state/graph/tlsroute.go b/internal/mode/static/state/graph/tlsroute.go
index 0acf1cadd7..78b2378c36 100644
--- a/internal/mode/static/state/graph/tlsroute.go
+++ b/internal/mode/static/state/graph/tlsroute.go
@@ -67,7 +67,8 @@ func buildTLSRoute(
 	return r
 }
 
-func validateBackendRefTLSRoute(gtr *v1alpha2.TLSRoute,
+func validateBackendRefTLSRoute(
+	gtr *v1alpha2.TLSRoute,
 	services map[types.NamespacedName]*apiv1.Service,
 	npCfg *NginxProxy,
 	refGrantResolver func(resource toResource) bool,
diff --git a/internal/mode/static/state/graph/tlsroute_test.go b/internal/mode/static/state/graph/tlsroute_test.go
index ae4555ef24..73cd8758a1 100644
--- a/internal/mode/static/state/graph/tlsroute_test.go
+++ b/internal/mode/static/state/graph/tlsroute_test.go
@@ -429,7 +429,8 @@ func TestBuildTLSRoute(t *testing.T) {
 					},
 				},
 				Conditions: []conditions.Condition{staticConds.NewRouteBackendRefRefNotPermitted(
-					"Backend ref to Service diff/hi not permitted by any ReferenceGrant",
+					"spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " +
+						"diff/hi not permitted by any ReferenceGrant",
 				)},
 				Attachable: true,
 				Valid:      true,
diff --git a/internal/mode/static/state/mirror/mirror.go b/internal/mode/static/state/mirror/mirror.go
new file mode 100644
index 0000000000..485fe8ccf3
--- /dev/null
+++ b/internal/mode/static/state/mirror/mirror.go
@@ -0,0 +1,40 @@
+package mirror
+
+import (
+	"fmt"
+	"strings"
+
+	v1 "sigs.k8s.io/gateway-api/apis/v1"
+
+	"github.com/nginx/nginx-gateway-fabric/internal/mode/static/nginx/config/http"
+)
+
+// RouteName builds the name for the internal mirror route, using the user route name,
+// service namespace/name, and index of the rule.
+// The prefix is used to prevent a user from creating a route with a conflicting name.
+func RouteName(routeName, service, namespace string, idx int) string {
+	prefix := strings.TrimPrefix(http.InternalMirrorRoutePathPrefix, "/")
+	return fmt.Sprintf("%s-%s-%s/%s-%d", prefix, routeName, namespace, service, idx)
+}
+
+// BackendPath builds the path for the internal mirror location, using the BackendRef.
+func PathWithBackendRef(idx int, backendRef v1.BackendObjectReference) *string {
+	svcName := string(backendRef.Name)
+	if backendRef.Namespace == nil {
+		return BackendPath(idx, nil, svcName)
+	}
+	return BackendPath(idx, (*string)(backendRef.Namespace), svcName)
+}
+
+// BackendPath builds the path for the internal mirror location.
+func BackendPath(idx int, namespace *string, service string) *string {
+	var mirrorPath string
+
+	if namespace != nil {
+		mirrorPath = fmt.Sprintf("%s-%s/%s-%d", http.InternalMirrorRoutePathPrefix, *namespace, service, idx)
+	} else {
+		mirrorPath = fmt.Sprintf("%s-%s-%d", http.InternalMirrorRoutePathPrefix, service, idx)
+	}
+
+	return &mirrorPath
+}
diff --git a/internal/mode/static/state/mirror/mirror_test.go b/internal/mode/static/state/mirror/mirror_test.go
new file mode 100644
index 0000000000..06c63e7adb
--- /dev/null
+++ b/internal/mode/static/state/mirror/mirror_test.go
@@ -0,0 +1,94 @@
+package mirror
+
+import (
+	"testing"
+
+	. "github.com/onsi/gomega"
+	v1 "sigs.k8s.io/gateway-api/apis/v1"
+
+	"github.com/nginx/nginx-gateway-fabric/internal/framework/helpers"
+)
+
+func TestRouteName(t *testing.T) {
+	t.Parallel()
+	g := NewWithT(t)
+
+	result := RouteName("route1", "service1", "namespace1", 1)
+	g.Expect(result).To(Equal("_ngf-internal-mirror-route1-namespace1/service1-1"))
+}
+
+func TestPathWithBackendRef(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		backendRef v1.BackendObjectReference
+		expected   *string
+		name       string
+		idx        int
+	}{
+		{
+			name: "with namespace",
+			idx:  1,
+			backendRef: v1.BackendObjectReference{
+				Name:      "service1",
+				Namespace: helpers.GetPointer[v1.Namespace]("namespace1"),
+			},
+			expected: helpers.GetPointer("/_ngf-internal-mirror-namespace1/service1-1"),
+		},
+		{
+			name: "without namespace",
+			idx:  2,
+			backendRef: v1.BackendObjectReference{
+				Name: "service2",
+			},
+			expected: helpers.GetPointer("/_ngf-internal-mirror-service2-2"),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			g := NewWithT(t)
+
+			result := PathWithBackendRef(tt.idx, tt.backendRef)
+			g.Expect(result).To(Equal(tt.expected))
+		})
+	}
+}
+
+func TestBackendPath(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		namespace *string
+		expected  *string
+		name      string
+		service   string
+		idx       int
+	}{
+		{
+			name:      "With namespace",
+			idx:       1,
+			namespace: helpers.GetPointer("namespace1"),
+			service:   "service1",
+			expected:  helpers.GetPointer("/_ngf-internal-mirror-namespace1/service1-1"),
+		},
+		{
+			name:      "Without namespace",
+			idx:       2,
+			namespace: nil,
+			service:   "service2",
+			expected:  helpers.GetPointer("/_ngf-internal-mirror-service2-2"),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			g := NewWithT(t)
+
+			result := BackendPath(tt.idx, tt.namespace, tt.service)
+			g.Expect(result).To(Equal(tt.expected))
+		})
+	}
+}
diff --git a/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go b/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go
index 490018358f..71a907acbf 100644
--- a/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go
+++ b/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go
@@ -8,6 +8,16 @@ import (
 )
 
 type FakeHTTPFieldsValidator struct {
+	SkipValidationStub        func() bool
+	skipValidationMutex       sync.RWMutex
+	skipValidationArgsForCall []struct {
+	}
+	skipValidationReturns struct {
+		result1 bool
+	}
+	skipValidationReturnsOnCall map[int]struct {
+		result1 bool
+	}
 	ValidateFilterHeaderNameStub        func(string) error
 	validateFilterHeaderNameMutex       sync.RWMutex
 	validateFilterHeaderNameArgsForCall []struct {
@@ -161,6 +171,59 @@ type FakeHTTPFieldsValidator struct {
 	invocationsMutex sync.RWMutex
 }
 
+func (fake *FakeHTTPFieldsValidator) SkipValidation() bool {
+	fake.skipValidationMutex.Lock()
+	ret, specificReturn := fake.skipValidationReturnsOnCall[len(fake.skipValidationArgsForCall)]
+	fake.skipValidationArgsForCall = append(fake.skipValidationArgsForCall, struct {
+	}{})
+	stub := fake.SkipValidationStub
+	fakeReturns := fake.skipValidationReturns
+	fake.recordInvocation("SkipValidation", []interface{}{})
+	fake.skipValidationMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakeHTTPFieldsValidator) SkipValidationCallCount() int {
+	fake.skipValidationMutex.RLock()
+	defer fake.skipValidationMutex.RUnlock()
+	return len(fake.skipValidationArgsForCall)
+}
+
+func (fake *FakeHTTPFieldsValidator) SkipValidationCalls(stub func() bool) {
+	fake.skipValidationMutex.Lock()
+	defer fake.skipValidationMutex.Unlock()
+	fake.SkipValidationStub = stub
+}
+
+func (fake *FakeHTTPFieldsValidator) SkipValidationReturns(result1 bool) {
+	fake.skipValidationMutex.Lock()
+	defer fake.skipValidationMutex.Unlock()
+	fake.SkipValidationStub = nil
+	fake.skipValidationReturns = struct {
+		result1 bool
+	}{result1}
+}
+
+func (fake *FakeHTTPFieldsValidator) SkipValidationReturnsOnCall(i int, result1 bool) {
+	fake.skipValidationMutex.Lock()
+	defer fake.skipValidationMutex.Unlock()
+	fake.SkipValidationStub = nil
+	if fake.skipValidationReturnsOnCall == nil {
+		fake.skipValidationReturnsOnCall = make(map[int]struct {
+			result1 bool
+		})
+	}
+	fake.skipValidationReturnsOnCall[i] = struct {
+		result1 bool
+	}{result1}
+}
+
 func (fake *FakeHTTPFieldsValidator) ValidateFilterHeaderName(arg1 string) error {
 	fake.validateFilterHeaderNameMutex.Lock()
 	ret, specificReturn := fake.validateFilterHeaderNameReturnsOnCall[len(fake.validateFilterHeaderNameArgsForCall)]
@@ -966,6 +1029,8 @@ func (fake *FakeHTTPFieldsValidator) ValidateRedirectStatusCodeReturnsOnCall(i i
 func (fake *FakeHTTPFieldsValidator) Invocations() map[string][][]interface{} {
 	fake.invocationsMutex.RLock()
 	defer fake.invocationsMutex.RUnlock()
+	fake.skipValidationMutex.RLock()
+	defer fake.skipValidationMutex.RUnlock()
 	fake.validateFilterHeaderNameMutex.RLock()
 	defer fake.validateFilterHeaderNameMutex.RUnlock()
 	fake.validateFilterHeaderValueMutex.RLock()
diff --git a/internal/mode/static/state/validation/validator.go b/internal/mode/static/state/validation/validator.go
index dd48573902..26bf281b70 100644
--- a/internal/mode/static/state/validation/validator.go
+++ b/internal/mode/static/state/validation/validator.go
@@ -22,6 +22,7 @@ type Validators struct {
 //
 //counterfeiter:generate . HTTPFieldsValidator
 type HTTPFieldsValidator interface {
+	SkipValidation() bool
 	ValidatePathInMatch(path string) error
 	ValidateHeaderNameInMatch(name string) error
 	ValidateHeaderValueInMatch(value string) error
@@ -58,3 +59,22 @@ type PolicyValidator interface {
 	// Conflicts returns true if the two Policies conflict.
 	Conflicts(a, b policies.Policy) bool
 }
+
+// SkipValidator is used to skip validation on internally-created routes for request mirroring.
+type SkipValidator struct{}
+
+func (SkipValidator) SkipValidation() bool { return true }
+
+func (SkipValidator) ValidatePathInMatch(string) error                { return nil }
+func (SkipValidator) ValidateHeaderNameInMatch(string) error          { return nil }
+func (SkipValidator) ValidateHeaderValueInMatch(string) error         { return nil }
+func (SkipValidator) ValidateQueryParamNameInMatch(string) error      { return nil }
+func (SkipValidator) ValidateQueryParamValueInMatch(string) error     { return nil }
+func (SkipValidator) ValidateMethodInMatch(string) (bool, []string)   { return true, nil }
+func (SkipValidator) ValidateRedirectScheme(string) (bool, []string)  { return true, nil }
+func (SkipValidator) ValidateRedirectPort(int32) error                { return nil }
+func (SkipValidator) ValidateRedirectStatusCode(int) (bool, []string) { return true, nil }
+func (SkipValidator) ValidateHostname(string) error                   { return nil }
+func (SkipValidator) ValidateFilterHeaderName(string) error           { return nil }
+func (SkipValidator) ValidateFilterHeaderValue(string) error          { return nil }
+func (SkipValidator) ValidatePath(string) error                       { return nil }


Reviews:
user: login: bjee19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T21:31:11Z
user: login: sjberman
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T21:46:47Z
user: login: ciarams87
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T15:28:52Z
user: login: bjee19
body: lgtm! 🚀 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T19:54:25Z

Review comments:
diff_hunk: @@ -73,11 +91,20 @@ func TestValidateFilter(t *testing.T) {
 		{
 			filter: Filter{
 				RouteType:  RouteTypeHTTP,
-				FilterType: FilterRequestMirror,
+				FilterType: "invalid-filter",
 			},
 			expectErrCount: 1,
 			name:           "unsupported HTTP filter type",
 		},
+		{
+			filter: Filter{
+				RouteType:     RouteTypeGRPC,
+				FilterType:    FilterRequestMirror,
+				RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{},
+			},
+			expectErrCount: 0,
+			name:           "valid GRPC filter",
path: internal/mode/static/state/graph/common_filter_test.go
user: login: bjee19
body: ```suggestion
			name:           "valid GRPC mirror filter",
```

Should this have mirror in it? 
created_at: 2025-04-21T21:13:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,44 @@
+package mirror
+
+import (
+	"fmt"
+	"strings"
+
+	v1 "sigs.k8s.io/gateway-api/apis/v1"
+
+	"github.com/nginx/nginx-gateway-fabric/internal/mode/static/nginx/config/http"
+)
+
+// RouteName builds the name for the internal mirror route, using the user route name,
+// service namespace/name, and index of the rule.
+// The prefix is used to prevent a user from creating a route with a conflicting name.
+func RouteName(routeName, service string, namespace *string, idx int) string {
path: internal/mode/static/state/mirror/mirror.go
user: login: bjee19
body: In the edge case where the namespace is not defined, but the service name ends with "-(insert namespace of another route)", does that matter?

And I guess this works in any case where resource generate identical names when they're not meant to. 
created_at: 2025-04-21T21:25:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,44 @@
+package mirror
+
+import (
+	"fmt"
+	"strings"
+
+	v1 "sigs.k8s.io/gateway-api/apis/v1"
+
+	"github.com/nginx/nginx-gateway-fabric/internal/mode/static/nginx/config/http"
+)
+
+// RouteName builds the name for the internal mirror route, using the user route name,
+// service namespace/name, and index of the rule.
+// The prefix is used to prevent a user from creating a route with a conflicting name.
+func RouteName(routeName, service string, namespace *string, idx int) string {
path: internal/mode/static/state/mirror/mirror.go
user: login: sjberman
body: Yeah good catch. This is where naming these things gets tough. I'll have to figure out how to structure this to avoid those edge cases...
created_at: 2025-04-21T21:46:47Z
author_association: COLLABORATOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-17T17:01:04Z
body: ## [Codecov](https://app.codecov.io/gh/nginx/nginx-gateway-fabric/pull/3306?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
Attention: Patch coverage is `90.09585%` with `31 lines` in your changes missing coverage. Please review.
> Project coverage is 86.20%. Comparing base [(`8d5ef4d`)](https://app.codecov.io/gh/nginx/nginx-gateway-fabric/commit/8d5ef4d3c6ce1f7dad6bd5b270d1b56f89f2893c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) to head [(`0b0bec8`)](https://app.codecov.io/gh/nginx/nginx-gateway-fabric/commit/0b0bec811d00f4d590cb3040839c7344e535db24?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/nginx/nginx-gateway-fabric/pull/3306?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Patch % | Lines |
|---|---|---|
| [internal/mode/static/state/validation/validator.go](https://app.codecov.io/gh/nginx/nginx-gateway-fabric/pull/3306?src=pr&el=tree&filepath=internal%2Fmode%2Fstatic%2Fstate%2Fvalidation%2Fvalidator.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvbW9kZS9zdGF0aWMvc3RhdGUvdmFsaWRhdGlvbi92YWxpZGF0b3IuZ28=) | 0.00% | [14 Missing :warning: ](https://app.codecov.io/gh/nginx/nginx-gateway-fabric/pull/3306?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |
| [internal/mode/static/state/graph/grpcroute.go](https://app.codecov.io/gh/nginx/nginx-gateway-fabric/pull/3306?src=pr&el=tree&filepath=internal%2Fmode%2Fstatic%2Fstate%2Fgraph%2Fgrpcroute.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvbW9kZS9zdGF0aWMvc3RhdGUvZ3JhcGgvZ3JwY3JvdXRlLmdv) | 90.29% | [8 Missing and 2 partials :warning: ](https://app.codecov.io/gh/nginx/nginx-gateway-fabric/pull/3306?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |
| [internal/mode/static/state/graph/backend\_refs.go](https://app.codecov.io/gh/nginx/nginx-gateway-fabric/pull/3306?src=pr&el=tree&filepath=internal%2Fmode%2Fstatic%2Fstate%2Fgraph%2Fbackend_refs.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvbW9kZS9zdGF0aWMvc3RhdGUvZ3JhcGgvYmFja2VuZF9yZWZzLmdv) | 88.46% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/nginx/nginx-gateway-fabric/pull/3306?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |
| [internal/mode/static/state/graph/httproute.go](https://app.codecov.io/gh/nginx/nginx-gateway-fabric/pull/3306?src=pr&el=tree&filepath=internal%2Fmode%2Fstatic%2Fstate%2Fgraph%2Fhttproute.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvbW9kZS9zdGF0aWMvc3RhdGUvZ3JhcGgvaHR0cHJvdXRlLmdv) | 96.38% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/nginx/nginx-gateway-fabric/pull/3306?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |
| [...e/static/nginx/config/validation/http\_validator.go](https://app.codecov.io/gh/nginx/nginx-gateway-fabric/pull/3306?src=pr&el=tree&filepath=internal%2Fmode%2Fstatic%2Fnginx%2Fconfig%2Fvalidation%2Fhttp_validator.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvbW9kZS9zdGF0aWMvbmdpbngvY29uZmlnL3ZhbGlkYXRpb24vaHR0cF92YWxpZGF0b3IuZ28=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/nginx/nginx-gateway-fabric/pull/3306?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #3306      +/-   ##
==========================================
+ Coverage   86.16%   86.20%   +0.04%     
==========================================
  Files         113      116       +3     
  Lines       11664    11928     +264     
  Branches       62       62              
==========================================
+ Hits        10050    10283     +233     
- Misses       1553     1580      +27     
- Partials       61       65       +4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/nginx/nginx-gateway-fabric/pull/3306?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: ciarams87
created_at: 2025-04-22T10:54:39Z
body: > Note: the RequestMirror conformance tests seem to have a bug in 1.2.1 Gateway API, which may be fixed in 1.3. We can hopefully enable them at that point.

Do we have something tracking this?

Other than that, nice work! 🚀 
author_association: CONTRIBUTOR
user: login: sjberman
created_at: 2025-04-22T13:55:44Z
body: @ciarams87 I was planning on adding it as acceptance criteria to the 1.3 story when we create that
author_association: COLLABORATOR