Repository: envoyproxy/gateway

Issue:
GRPCRoutes panic the controller translator when RequestMirror filter is used
*Description*:

GRPCRoutes panic the translator if they specify a requestMirror filter

*Repro steps*:

```
gateways:
- apiVersion: gateway.networking.k8s.io/v1
  kind: Gateway
  metadata:
    namespace: envoy-gateway
    name: gateway-1
  spec:
    gatewayClassName: envoy-gateway-class
    listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: All
grpcRoutes:
- apiVersion: gateway.networking.k8s.io/v1alpha2
  kind: GRPCRoute
  metadata:
    namespace: default
    name: grpcroute-1
  spec:
    parentRefs:
    - namespace: envoy-gateway
      name: gateway-1
      sectionName: http
    rules:
    - matches:
      - headers:
        - type: Exact
          name: magic
          value: foo
      backendRefs:
      - name: service-1
        port: 8080
      filters:
        - type: RequestMirror
          requestMirror:
            backendRef:
                kind: Service
                name: service-2
                port: 8080

```

*Environment*:

Present in main, but at least since 1.4.1; EKS in this case, but repros on local macos machine as well.

*Logs*:

From 1.4.1 -- 

```
observed a panic	{"runner": "gateway-api", "stackTrace": "goroutine 257 [running]:
runtime/debug.Stack()
\t/opt/hostedtoolcache/go/1.24.4/x64/src/runtime/debug/stack.go:26 +0x5e
github.com/envoyproxy/gateway/internal/message.handleWithCrashRecovery[...].func1()
\t/home/runner/work/gateway/gateway/internal/message/watchutil.go:54 +0x1fe
panic({0x33e0920?, 0xc009ffc1b0?})
\t/opt/hostedtoolcache/go/1.24.4/x64/src/runtime/panic.go:792 +0x132
github.com/envoyproxy/gateway/internal/gatewayapi.(*Translator).validateBackendRefFilters(0xc0033a58f0, {0x35d6120?, 0xc0024ab310?}, {0x2faa625, 0x9})
\t/home/runner/work/gateway/gateway/internal/gatewayapi/validate.go:111 +0x485
github.com/envoyproxy/gateway/internal/gatewayapi.(*Translator).validateBackendRef(0xc0033a58f0, {0x35d6120, 0xc0024ab310}, {0x8b5c5f0, 0xc0087000e0}, 0xc003c29188, {0xc0061a3620, 0x11}, {0x2faa625, 0x9})
\t/home/runner/work/gateway/gateway/internal/gatewayapi/validate.go:33 +0x97
github.com/envoyproxy/gateway/internal/gatewayapi.(*Translator).processDestination(0xc0033a58f0, {0xc003b98620, 0x69}, {0x35d6120, 0xc0024ab310}, 0xc0024ab270, {0x8b5c5f0, 0xc0087000e0}, 0xc003c29188)
\t/home/runner/work/gateway/gateway/internal/gatewayapi/route.go:1383 +0x16f
github.com/envoyproxy/gateway/internal/gatewayapi.(*Translator).processRequestMirrorFilter(0xc0033a58f0, 0x0, 0xc005915e40, 0xc009ffc0c0, 0xc003c29188)
\t/home/runner/work/gateway/gateway/internal/gatewayapi/filters.go:958 +0x408
github.com/envoyproxy/gateway/internal/gatewayapi.(*Translator).ProcessGRPCFilters(0xc0033a58f0, 0xc0024ab270, {0x8b5c5f0, 0xc0087000e0}, {0xc007b4bc20, 0x1, 0x38645a0?}, 0xc003c29188)
\t/home/runner/work/gateway/gateway/internal/gatewayapi/filters.go:150 +0x238
github.com/envoyproxy/gateway/internal/gatewayapi.(*Translator).processGRPCRouteRules(0xc0033a58f0, 0xc0087000e0, 0xc0024ab270, 0xc003c29188)
\t/home/runner/work/gateway/gateway/internal/gatewayapi/route.go:641 +0x192
github.com/envoyproxy/gateway/internal/gatewayapi.(*Translator).processGRPCRouteParentRefs(0xc0033a58f0, 0xc0087000e0, 0xc003c29188, 0xc00821cde0)
\t/home/runner/work/gateway/gateway/internal/gatewayapi/route.go:573 +0xd6
github.com/envoyproxy/gateway/internal/gatewayapi.(*Translator).ProcessGRPCRoutes(0xc0033a58f0, {0xc004663880, 0xb, 0x20?}, {0xc009b81aa0, 0x1, 0x1}, 0xc003c29188, 0xc00821cde0)
\t/home/runner/work/gateway/gateway/internal/gatewayapi/route.go:111 +0x233
github.com/envoyproxy/gateway/internal/gatewayapi.(*Translator).Translate(0xc0033a58f0, 0xc003c29188)
\t/home/runner/work/gateway/gateway/internal/gatewayapi/translator.go:195 +0x27e
github.com/envoyproxy/gateway/internal/gatewayapi/runner.(*Runner).subscribeAndTranslate.func1({{0xc00173a9f0?, 0x47dac5749611dfbe?}, 0x80?, 0xc006636150?}, 0xc002290000)
\t/home/runner/work/gateway/gateway/internal/gatewayapi/runner/runner.go:177 +0x51e
github.com/envoyproxy/gateway/internal/message.handleWithCrashRecovery[...](0xc00ad9bfa0?, {{0xc00173a9f0, 0x663324?}, 0xb2?, 0xc006636150?}, {{0x3ad5163, 0xb}, {0x3ae891d, 0x12}}, 0xc002290000?)
\t/home/runner/work/gateway/gateway/internal/message/watchutil.go:60 +0x137
github.com/envoyproxy/gateway/internal/message.HandleSubscription[...]({{0x3ad5163, 0xc001c01340?}, {0x3ae891d?, 0xc000f70fa8?}}, 0xc002fd4850?, 0xc0033a5fa0)
\t/home/runner/work/gateway/gateway/internal/message/watchutil.go:99 +0x7b0
github.com/envoyproxy/gateway/internal/gatewayapi/runner.(*Runner).subscribeAndTranslate(0xc000b6bae0, 0x3333332133333332?)
\t/home/runner/work/gateway/gateway/internal/gatewayapi/runner/runner.go:127 +0x50
created by github.com/envoyproxy/gateway/internal/gatewayapi/runner.(*Runner).Start in goroutine 24
\t/home/runner/work/gateway/gateway/internal/gatewayapi/runner/runner.go:90 +0x34d
", "error": "interface conversion: interface {} is []v1.HTTPRouteFilter, not []v1.GRPCRouteFilter"}
```


Discussion:
@AndyMoreland: Reproducing test data checked in here: https://github.com/AndyMoreland/gateway/pull/1/files

I'm working on a fix, but would appreciate a second look at the filter definition to verify I'm not doing something that isn't supposed to be supported.

I think what's happening is that:

- GRPCRouteFilterRequestHeaderModifier
- GRPCRouteFilterResponseHeaderModifier
- GRPCRouteFilterRequestMirror

were all implemented at the same time in the same way, but `GRPCRouteFilterRequestMirror` is missing coverage, and `GRPCRouteFilterRequestMirror` uniquely calls a particular path that has an unchecked (incorrect) cast.@AndyMoreland: I believe I've fixed the issue in my fork PR, but I'm still working to check in a verifying test. I'm not a go expert, so I'm taking a fairly brute-force hammer to the factoring -- basically just splitting the process function up for http vs grpc at a very coarse level, but it does fix the panic.@AndyMoreland: I've [submitted a PR](https://github.com/envoyproxy/gateway/pull/6875); waiting for tests.

Solution patch:
diff --git a/.github/workflows/license-scan.yml b/.github/workflows/license-scan.yml
index 0c2f65e5b..b5d167b3e 100644
--- a/.github/workflows/license-scan.yml
+++ b/.github/workflows/license-scan.yml
@@ -18,7 +18,7 @@ jobs:
     - name: Checkout code
       uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8  # v5.0.0
     - name: Run scanner
-      uses: google/osv-scanner-action/osv-scanner-action@456ceb78310755116e0a3738121351006286b797  # v2.2.1
+      uses: google/osv-scanner-action/osv-scanner-action@90b209d0ea55cea1da9fc0c4e65782cc6acb6e2e  # v2.2.2
       with:
         scan-args: |-  # See allowed licenses at https://github.com/cncf/foundation/blob/main/policies-guidance/allowed-third-party-license-policy.md#approved-licenses-for-allowlist
           --licenses=Apache-2.0,0BSD,BSD-2-Clause,BSD-2-Clause-FreeBSD,BSD-3-Clause,MIT,MIT-0,ISC,OpenSSL,OpenSSL-standalone,PSF-2.0,Python-2.0,Python-2.0.1,PostgreSQL,SSLeay-standalone,UPL-1.0,X11,Zlib
diff --git a/.github/workflows/osv-scanner.yml b/.github/workflows/osv-scanner.yml
index ced8c05e6..1b81dacf5 100644
--- a/.github/workflows/osv-scanner.yml
+++ b/.github/workflows/osv-scanner.yml
@@ -19,7 +19,7 @@ permissions:
 jobs:
   scan-scheduled:
     if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }}
-    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@456ceb78310755116e0a3738121351006286b797"  # v2.2.1
+    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@90b209d0ea55cea1da9fc0c4e65782cc6acb6e2e"  # v2.2.2
     with:
       scan-args: |-
         --recursive
@@ -32,7 +32,7 @@ jobs:
 
   scan-pr:
     if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
-    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@456ceb78310755116e0a3738121351006286b797"  # v2.2.1
+    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@90b209d0ea55cea1da9fc0c4e65782cc6acb6e2e"  # v2.2.2
     with:
       scan-args: |-
         --recursive
diff --git a/go.mod b/go.mod
index e637301a4..217d0685e 100644
--- a/go.mod
+++ b/go.mod
@@ -86,7 +86,7 @@ require (
 	sigs.k8s.io/controller-runtime v0.21.0
 	sigs.k8s.io/gateway-api v1.3.1-0.20250527223622-54df0a899c1c
 	sigs.k8s.io/kubectl-validate v0.0.5-0.20241223122011-eb064d2f92d5
-	sigs.k8s.io/mcs-api v0.2.0
+	sigs.k8s.io/mcs-api v0.3.0
 	sigs.k8s.io/yaml v1.6.0
 )
 
diff --git a/go.sum b/go.sum
index 4b37b274d..887f41d5b 100644
--- a/go.sum
+++ b/go.sum
@@ -935,8 +935,8 @@ sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ
 sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o=
 sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA=
 sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY=
-sigs.k8s.io/mcs-api v0.2.0 h1:F8o/nIpQmog494Qwe94srDWjS3ltEu4y5IL9i3dB938=
-sigs.k8s.io/mcs-api v0.2.0/go.mod h1:zZ5CK8uS6HaLkxY4HqsmcBHfzHuNMrY2uJy8T7jffK4=
+sigs.k8s.io/mcs-api v0.3.0 h1:LjRvgzjMrvO1904GP6XBJSnIX221DJMyQlZOYt9LAnM=
+sigs.k8s.io/mcs-api v0.3.0/go.mod h1:zZ5CK8uS6HaLkxY4HqsmcBHfzHuNMrY2uJy8T7jffK4=
 sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
 sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
 sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
diff --git a/internal/gatewayapi/filters.go b/internal/gatewayapi/filters.go
index be65cb0be..4bb313f12 100644
--- a/internal/gatewayapi/filters.go
+++ b/internal/gatewayapi/filters.go
@@ -945,22 +945,34 @@ func (t *Translator) processRequestMirrorFilter(
 		return nil
 	}
 
+	// Get the route type from the filter context to determine the correct BackendRef type
+	routeType := GetRouteType(filterContext.Route)
+	weight := int32(1)
 	mirrorBackend := mirrorFilter.BackendRef
 
-	// Wrap the filter's BackendObjectReference into a BackendRef so we can use existing tooling to check it
-	weight := int32(1)
-	mirrorBackendRef := gwapiv1.HTTPBackendRef{
-		BackendRef: gwapiv1.BackendRef{
-			BackendObjectReference: mirrorBackend,
-			Weight:                 &weight,
-		},
+	// Create the appropriate BackendRef type based on the route type
+	var mirrorBackendRef BackendRefContext
+	if routeType == resource.KindGRPCRoute {
+		mirrorBackendRef = gwapiv1.GRPCBackendRef{
+			BackendRef: gwapiv1.BackendRef{
+				BackendObjectReference: mirrorBackend,
+				Weight:                 &weight,
+			},
+		}
+	} else {
+		mirrorBackendRef = gwapiv1.HTTPBackendRef{
+			BackendRef: gwapiv1.BackendRef{
+				BackendObjectReference: mirrorBackend,
+				Weight:                 &weight,
+			},
+		}
 	}
 
-	// This sets the status on the HTTPRoute, should the usage be changed so that the status message reflects that the backendRef is from the filter?
+	// This sets the status on the Route, should the usage be changed so that the status message reflects that the backendRef is from the filter?
 	filterNs := filterContext.Route.GetNamespace()
 	serviceNamespace := NamespaceDerefOr(mirrorBackend.Namespace, filterNs)
 	err = t.validateBackendRef(mirrorBackendRef, filterContext.Route,
-		resources, serviceNamespace, resource.KindHTTPRoute)
+		resources, serviceNamespace, routeType)
 	if err != nil {
 		return status.NewRouteStatusError(
 			fmt.Errorf("failed to validate the RequestMirror filter: %w", err), err.Reason()).WithType(gwapiv1.RouteConditionResolvedRefs)
diff --git a/site/package.json b/site/package.json
index a4cc4ad9a..6636dc0c8 100644
--- a/site/package.json
+++ b/site/package.json
@@ -38,7 +38,7 @@
     "@fortawesome/fontawesome-free": "^7.0.0",
     "autoprefixer": "^10.4.20",
     "cross-env": "^10.0.0",
-    "hugo-extended": "0.148.2",
+    "hugo-extended": "0.149.0",
     "postcss-cli": "^11.0.0",
     "rtlcss": "^4.3.0"
   },
diff --git a/tools/docker/envoy-gateway/Dockerfile b/tools/docker/envoy-gateway/Dockerfile
index babb897f3..1be57388f 100644
--- a/tools/docker/envoy-gateway/Dockerfile
+++ b/tools/docker/envoy-gateway/Dockerfile
@@ -4,7 +4,7 @@ RUN mkdir -p /var/lib/eg && chmod -R 0777 /var/lib/eg
 
 # Use distroless as minimal base image to package the manager binary
 # Refer to https://github.com/GoogleContainerTools/distroless for more details
-FROM gcr.io/distroless/base-nossl:nonroot@sha256:9a1c8b2ff7412ea80ca8c531eb7542e0e7766c8cf79489a8f0f31f0aa341c0fe
+FROM gcr.io/distroless/base-nossl:nonroot@sha256:8981b63f968e829d21351ea9d28cc21127e5f034707f1d8483d2993d9577be0b
 ARG TARGETPLATFORM
 COPY $TARGETPLATFORM/envoy-gateway /usr/local/bin/
 COPY --from=source /var/lib /var/lib



Test patch:
diff --git a/internal/gatewayapi/testdata/grpcroute-with-mirror.in.yaml b/internal/gatewayapi/testdata/grpcroute-with-mirror.in.yaml
new file mode 100644
index 000000000..7e61207d1
--- /dev/null
+++ b/internal/gatewayapi/testdata/grpcroute-with-mirror.in.yaml
@@ -0,0 +1,37 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      allowedRoutes:
+        namespaces:
+          from: All
+grpcRoutes:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+  kind: GRPCRoute
+  metadata:
+    namespace: default
+    name: grpcroute-1
+  spec:
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      filters:
+      - type: RequestMirror
+        requestMirror:
+          backendRef:
+            kind: Service
+            name: service-2
+            port: 8080
diff --git a/internal/gatewayapi/testdata/grpcroute-with-mirror.out.yaml b/internal/gatewayapi/testdata/grpcroute-with-mirror.out.yaml
new file mode 100644
index 000000000..899cfa04e
--- /dev/null
+++ b/internal/gatewayapi/testdata/grpcroute-with-mirror.out.yaml
@@ -0,0 +1,184 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 1
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+grpcRoutes:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+  kind: GRPCRoute
+  metadata:
+    creationTimestamp: null
+    name: grpcroute-1
+    namespace: default
+  spec:
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      filters:
+      - requestMirror:
+          backendRef:
+            kind: Service
+            name: service-2
+            port: 8080
+        type: RequestMirror
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+        ownerReference:
+          kind: GatewayClass
+          name: envoy-gateway-class
+      name: envoy-gateway/gateway-1
+      namespace: envoy-gateway-system
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    globalResources:
+      proxyServiceCluster:
+        name: envoy-gateway/gateway-1
+        settings:
+        - addressType: IP
+          endpoints:
+          - host: 7.6.5.4
+            port: 8080
+            zone: zone1
+          metadata:
+            name: envoy-envoy-gateway-gateway-1-196ae069
+            namespace: envoy-gateway-system
+            sectionName: "8080"
+          name: envoy-gateway/gateway-1
+          protocol: TCP
+    http:
+    - address: 0.0.0.0
+      externalPort: 80
+      hostnames:
+      - '*'
+      isHTTP2: true
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+      name: envoy-gateway/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - destination:
+          metadata:
+            kind: GRPCRoute
+            name: grpcroute-1
+            namespace: default
+          name: grpcroute/default/grpcroute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            metadata:
+              name: service-1
+              namespace: default
+              sectionName: "8080"
+            name: grpcroute/default/grpcroute-1/rule/0/backend/0
+            protocol: GRPC
+            weight: 1
+        hostname: '*'
+        isHTTP2: true
+        metadata:
+          kind: GRPCRoute
+          name: grpcroute-1
+          namespace: default
+        mirrors:
+        - destination:
+            name: grpcroute/default/grpcroute-1/rule/0-mirror-0
+            settings:
+            - addressType: IP
+              endpoints:
+              - host: 7.7.7.7
+                port: 8080
+              metadata:
+                name: service-2
+                namespace: default
+                sectionName: "8080"
+              name: grpcroute/default/grpcroute-1/rule/0-mirror-0/backend/-1
+              protocol: GRPC
+              weight: 1
+        name: grpcroute/default/grpcroute-1/rule/0/match/-1/*
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003