Repository: nginx/nginx-gateway-fabric

Issue:
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.

Hints from issue comments:
@mpstefan: NGINX can support this, making this a lighter lift comparatively.@kiyutink: 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. @sjberman: @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).

Solution patch:
diff --git a/internal/mode/static/nginx/config/http/config.go b/internal/mode/static/nginx/config/http/config.go
index b81a6771..d79fe708 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 f7d76e2e..d4f8d9b1 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 c3873ef1..6676426b 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/validation/http_validator.go b/internal/mode/static/nginx/config/validation/http_validator.go
index 29da381a..a17cfdaa 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/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go
index d3c86fcb..84dc4b0c 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/convert.go b/internal/mode/static/state/dataplane/convert.go
index d44a47ed..b1b1a4c6 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/types.go b/internal/mode/static/state/dataplane/types.go
index d94f8e60..55e5476f 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 76ee3ab3..ad676f59 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/common_filter.go b/internal/mode/static/state/graph/common_filter.go
index 6bd71cf8..c67d233c 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/extension_ref_filter.go b/internal/mode/static/state/graph/extension_ref_filter.go
index 7587a69c..b7585424 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/grpcroute.go b/internal/mode/static/state/graph/grpcroute.go
index 7da7bf20..aaacdb0f 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/httproute.go b/internal/mode/static/state/graph/httproute.go
index dd3258a4..f10df696 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/route_common.go b/internal/mode/static/state/graph/route_common.go
index 0e50c19f..f146a55a 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 0acf1cad..78b2378c 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/mirror/mirror.go b/internal/mode/static/state/mirror/mirror.go
new file mode 100644
index 00000000..485fe8cc
--- /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/validation/validationfakes/fake_httpfields_validator.go b/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go
index 49001835..71a907ac 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 dd485739..26bf281b 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 }



Test patch:
diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go
index 56da44f1..70991ecb 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/state/change_processor_test.go b/internal/mode/static/state/change_processor_test.go
index 18d3d044..d71ebd52 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_test.go b/internal/mode/static/state/dataplane/configuration_test.go
index d8219a1c..b13a7845 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_test.go b/internal/mode/static/state/dataplane/convert_test.go
index f3116fab..77b4c689 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/graph/backend_refs_test.go b/internal/mode/static/state/graph/backend_refs_test.go
index ba6e2a2d..0d43456e 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_test.go b/internal/mode/static/state/graph/common_filter_test.go
index 680938ea..f365a2cd 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_test.go b/internal/mode/static/state/graph/extension_ref_filter_test.go
index bcc5539b..ae485a66 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_test.go b/internal/mode/static/state/graph/grpcroute_test.go
index 2adf83ee..5417208d 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_test.go b/internal/mode/static/state/graph/httproute_test.go
index d08f00b9..ce0f794b 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/tlsroute_test.go b/internal/mode/static/state/graph/tlsroute_test.go
index ae4555ef..73cd8758 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_test.go b/internal/mode/static/state/mirror/mirror_test.go
new file mode 100644
index 00000000..06c63e7a
--- /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))
+		})
+	}
+}