Repository: envoyproxy/gateway
Pull request #6468 (2025-07-04 10:09:55)

fix: custom backend weight not set properly
**What type of PR is this?**

This PR fixed the custom backendRef weight not set properly, which will cause the destination settings empty and always get a 500 direct response.

This fix has tested in envoy ai gateway.


Patch:
diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go
index 69ea451eb..974f2cebc 100644
--- a/internal/gatewayapi/route.go
+++ b/internal/gatewayapi/route.go
@@ -1427,7 +1427,11 @@ func (t *Translator) processDestination(name string, backendRefContext BackendRe
 		if t.isCustomBackendResource(backendRef.Group, KindDerefOr(backendRef.Kind, resource.KindService)) {
 			// Add the custom backend resource to ExtensionRefFilters so it can be processed by the extension system
 			unstructuredRef = t.processBackendExtensions(backendRef.BackendObjectReference, backendNamespace, resources)
-			return nil, unstructuredRef, nil
+			return &ir.DestinationSetting{
+				Name:            name,
+				Weight:          &weight,
+				IsCustomBackend: true,
+			}, unstructuredRef, nil
 		}
 	}
 
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index f9bac24c8..c1e609909 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -1600,6 +1600,8 @@ func (r *RouteDestination) ToBackendWeights() *BackendWeights {
 		switch {
 		case s.IsDynamicResolver: // Dynamic resolver has no endpoints
 			w.Valid += *s.Weight
+		case s.IsCustomBackend: // Custom backends has no endpoints
+			w.Valid += *s.Weight
 		case len(s.Endpoints) > 0:
 			w.Valid += *s.Weight
 		default:
@@ -1620,6 +1622,9 @@ type DestinationSetting struct {
 	// A dynamic resolver is a destination that is resolved dynamically using the request's host header.
 	IsDynamicResolver bool `json:"isDynamicResolver,omitempty" yaml:"isDynamicResolver,omitempty"`
 
+	// IsCustomBackend specifies whether the destination is a custom backend.
+	IsCustomBackend bool `json:"isCustomBackend,omitempty" yaml:"isCustomBackend,omitempty"`
+
 	// Weight associated with this destination,
 	// invalid endpoints are represents with a
 	// non-zero weight with an empty endpoints list
diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go
index 2ea4900aa..a4b809f68 100644
--- a/internal/provider/kubernetes/controller.go
+++ b/internal/provider/kubernetes/controller.go
@@ -657,7 +657,7 @@ func (r *gatewayAPIReconciler) processBackendRefs(ctx context.Context, gwcResour
 				}
 				if !resourceMappings.allAssociatedBackendRefExtensionFilters.Has(key) {
 					resourceMappings.allAssociatedBackendRefExtensionFilters.Insert(key)
-					if extRefFilter, exists := resourceMappings.extensionRefFilters[key]; !exists {
+					if extRefFilter, exists := resourceMappings.extensionRefFilters[key]; exists {
 						resourceMappings.extensionRefFilters[key] = extRefFilter
 						gwcResource.ExtensionRefFilters = append(gwcResource.ExtensionRefFilters, extRefFilter)
 						r.log.Info("added custom backend resource to resource tree",


Test patch:
diff --git a/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend-invalid-apiversion.out.yaml b/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend-invalid-apiversion.out.yaml
index eb6e27c75..c4a91292b 100644
--- a/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend-invalid-apiversion.out.yaml
+++ b/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend-invalid-apiversion.out.yaml
@@ -130,8 +130,16 @@ xdsIR:
         mergeSlashes: true
       port: 10080
       routes:
-      - directResponse:
-          statusCode: 500
+      - destination:
+          metadata:
+            kind: HTTPRoute
+            name: httproute-1
+            namespace: default
+          name: httproute/default/httproute-1/rule/1
+          settings:
+          - isCustomBackend: true
+            name: httproute/default/httproute-1/rule/1/backend/0
+            weight: 1
         hostname: gateway.envoyproxy.io
         isHTTP2: false
         metadata:
@@ -143,8 +151,16 @@ xdsIR:
           distinct: false
           name: ""
           prefix: /lambda
-      - directResponse:
-          statusCode: 500
+      - destination:
+          metadata:
+            kind: HTTPRoute
+            name: httproute-1
+            namespace: default
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - isCustomBackend: true
+            name: httproute/default/httproute-1/rule/0/backend/0
+            weight: 1
         hostname: gateway.envoyproxy.io
         isHTTP2: false
         metadata:
diff --git a/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend-mixed-multiple.out.yaml b/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend-mixed-multiple.out.yaml
index ca4c13b76..605ff6b2e 100644
--- a/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend-mixed-multiple.out.yaml
+++ b/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend-mixed-multiple.out.yaml
@@ -160,6 +160,12 @@ xdsIR:
             name: httproute/default/httproute-1/rule/0/backend/0
             protocol: HTTP
             weight: 1
+          - isCustomBackend: true
+            name: httproute/default/httproute-1/rule/0/backend/1
+            weight: 1
+          - isCustomBackend: true
+            name: httproute/default/httproute-1/rule/0/backend/2
+            weight: 1
         extensionRefs:
         - object:
             apiVersion: storage.example.io/v1alpha1
@@ -199,6 +205,12 @@ xdsIR:
             namespace: default
           name: httproute/default/httproute-1/rule/1
           settings:
+          - isCustomBackend: true
+            name: httproute/default/httproute-1/rule/1/backend/0
+            weight: 1
+          - isCustomBackend: true
+            name: httproute/default/httproute-1/rule/1/backend/1
+            weight: 1
           - addressType: IP
             endpoints:
             - host: 7.7.7.7
diff --git a/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend-mixed.out.yaml b/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend-mixed.out.yaml
index 0aac167ee..eb0053db4 100644
--- a/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend-mixed.out.yaml
+++ b/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend-mixed.out.yaml
@@ -152,6 +152,9 @@ xdsIR:
             name: httproute/default/httproute-1/rule/0/backend/0
             protocol: HTTP
             weight: 1
+          - isCustomBackend: true
+            name: httproute/default/httproute-1/rule/0/backend/1
+            weight: 1
         extensionRefs:
         - object:
             apiVersion: storage.example.io/v1alpha1
@@ -181,6 +184,9 @@ xdsIR:
             namespace: default
           name: httproute/default/httproute-1/rule/1
           settings:
+          - isCustomBackend: true
+            name: httproute/default/httproute-1/rule/1/backend/0
+            weight: 1
           - addressType: IP
             endpoints:
             - host: 7.7.7.7
diff --git a/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend-multiple.out.yaml b/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend-multiple.out.yaml
index ba10662f9..b782b65aa 100644
--- a/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend-multiple.out.yaml
+++ b/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend-multiple.out.yaml
@@ -138,8 +138,19 @@ xdsIR:
         mergeSlashes: true
       port: 10080
       routes:
-      - directResponse:
-          statusCode: 500
+      - destination:
+          metadata:
+            kind: HTTPRoute
+            name: httproute-1
+            namespace: default
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - isCustomBackend: true
+            name: httproute/default/httproute-1/rule/0/backend/0
+            weight: 1
+          - isCustomBackend: true
+            name: httproute/default/httproute-1/rule/0/backend/1
+            weight: 1
         extensionRefs:
         - object:
             apiVersion: storage.example.io/v1alpha1
@@ -172,8 +183,19 @@ xdsIR:
           distinct: false
           name: ""
           prefix: /service
-      - directResponse:
-          statusCode: 500
+      - destination:
+          metadata:
+            kind: HTTPRoute
+            name: httproute-1
+            namespace: default
+          name: httproute/default/httproute-1/rule/1
+          settings:
+          - isCustomBackend: true
+            name: httproute/default/httproute-1/rule/1/backend/0
+            weight: 1
+          - isCustomBackend: true
+            name: httproute/default/httproute-1/rule/1/backend/1
+            weight: 1
         extensionRefs:
         - object:
             apiVersion: storage.example.io/v1alpha1
diff --git a/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend.out.yaml b/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend.out.yaml
index f2d88f6f5..1484045d8 100644
--- a/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend.out.yaml
+++ b/internal/gatewayapi/testdata/extensions/httproute-with-custom-backend.out.yaml
@@ -130,8 +130,16 @@ xdsIR:
         mergeSlashes: true
       port: 10080
       routes:
-      - directResponse:
-          statusCode: 500
+      - destination:
+          metadata:
+            kind: HTTPRoute
+            name: httproute-1
+            namespace: default
+          name: httproute/default/httproute-1/rule/1
+          settings:
+          - isCustomBackend: true
+            name: httproute/default/httproute-1/rule/1/backend/0
+            weight: 1
         extensionRefs:
         - object:
             apiVersion: compute.example.io/v1alpha1
@@ -154,8 +162,16 @@ xdsIR:
           distinct: false
           name: ""
           prefix: /lambda
-      - directResponse:
-          statusCode: 500
+      - destination:
+          metadata:
+            kind: HTTPRoute
+            name: httproute-1
+            namespace: default
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - isCustomBackend: true
+            name: httproute/default/httproute-1/rule/0/backend/0
+            weight: 1
         extensionRefs:
         - object:
             apiVersion: storage.example.io/v1alpha1
diff --git a/internal/provider/kubernetes/controller_test.go b/internal/provider/kubernetes/controller_test.go
index da8d36cf9..68888116c 100644
--- a/internal/provider/kubernetes/controller_test.go
+++ b/internal/provider/kubernetes/controller_test.go
@@ -318,18 +318,8 @@ func TestProcessBackendRefsWithCustomBackends(t *testing.T) {
 
 			// Call the function under test
 			require.NoError(t, r.processBackendRefs(ctx, gwcResource, resourceMappings))
-
-			// Verify results
-			// Note: Due to a bug in the current implementation (line 542 in controller.go),
-			// custom backends are not properly added to ExtensionRefFilters when they exist
-			// in the extensionRefFilters map. The logic should be `exists` instead of `!exists`.
-			// For now, we test the current (buggy) behavior.
-			if tc.name == "skip non-custom backends" {
-				require.Len(t, gwcResource.ExtensionRefFilters, tc.expectedExtFiltersCount)
-			} else {
-				// Current buggy behavior: custom backends are not added to ExtensionRefFilters
-				require.Empty(t, gwcResource.ExtensionRefFilters)
-			}
+			// Compare the results
+			require.Len(t, gwcResource.ExtensionRefFilters, tc.expectedExtFiltersCount)
 
 			for _, expectedNS := range tc.expectedNamespaces {
 				require.True(t, resourceMappings.allAssociatedNamespaces.Has(expectedNS))