Repository: envoyproxy/gateway
Pull request #4415 (2024-10-09 09:55:40)

impl: custom error response
Implement: https://github.com/envoyproxy/gateway/pull/4259 

Issue: https://github.com/envoyproxy/gateway/issues/1400

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/4415?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) Report
Attention: Patch coverage is `33.84615%` with `43 lines` in your changes missing coverage. Please review.
> Project coverage is 65.65%. Comparing base [(`87b8418`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/87b841871d4cc44b7443187c318feeee493d01f6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`157737f`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/157737fa03f4596b42787bb3dbd11cfaf48504dc?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).

| [Files with missing lines](https://app.codecov.io/gh/envoyproxy/gateway/pull/4415?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) | Patch % | Lines |
|---|---|---|
| [internal/gatewayapi/backendtrafficpolicy.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/4415?src=pr&el=tree&filepath=internal%2Fgatewayapi%2Fbackendtrafficpolicy.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwvZ2F0ZXdheWFwaS9iYWNrZW5kdHJhZmZpY3BvbGljeS5nbw==) | 32.81% | [41 Missing and 2 partials :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/4415?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |

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


```diff
@@            Coverage Diff             @@
##             main    #4415      +/-   ##
==========================================
- Coverage   65.67%   65.65%   -0.03%     
==========================================
  Files         200      200              
  Lines       24128    24178      +50     
==========================================
+ Hits        15845    15873      +28     
- Misses       7145     7171      +26     
+ Partials     1138     1134       -4     
```

</details>

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

zirain: @zhaohuabing can you make CI happy?
arkodg: e2es are failing ...

Patch:
diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go
index 4e6118e70..4183c1283 100644
--- a/api/v1alpha1/backendtrafficpolicy_types.go
+++ b/api/v1alpha1/backendtrafficpolicy_types.go
@@ -74,7 +74,6 @@ type BackendTrafficPolicySpec struct {
 	// If multiple configurations are specified, the first one to match wins.
 	//
 	// +optional
-	// +notImplementedHide
 	ResponseOverride []*ResponseOverride `json:"responseOverride,omitempty"`
 }
 
diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go
index d7a2a73ab..4bf7920f6 100644
--- a/api/v1alpha1/envoyproxy_types.go
+++ b/api/v1alpha1/envoyproxy_types.go
@@ -124,6 +124,8 @@ type EnvoyProxySpec struct {
 	//
 	// - envoy.filters.http.ratelimit
 	//
+	// - envoy.filters.http.custom_response
+	//
 	// - envoy.filters.http.router
 	//
 	// Note: "envoy.filters.http.router" cannot be reordered, it's always the last filter in the chain.
@@ -174,7 +176,7 @@ type FilterPosition struct {
 }
 
 // EnvoyFilter defines the type of Envoy HTTP filter.
-// +kubebuilder:validation:Enum=envoy.filters.http.health_check;envoy.filters.http.fault;envoy.filters.http.cors;envoy.filters.http.ext_authz;envoy.filters.http.basic_auth;envoy.filters.http.oauth2;envoy.filters.http.jwt_authn;envoy.filters.http.stateful_session;envoy.filters.http.ext_proc;envoy.filters.http.wasm;envoy.filters.http.rbac;envoy.filters.http.local_ratelimit;envoy.filters.http.ratelimit
+// +kubebuilder:validation:Enum=envoy.filters.http.health_check;envoy.filters.http.fault;envoy.filters.http.cors;envoy.filters.http.ext_authz;envoy.filters.http.basic_auth;envoy.filters.http.oauth2;envoy.filters.http.jwt_authn;envoy.filters.http.stateful_session;envoy.filters.http.ext_proc;envoy.filters.http.wasm;envoy.filters.http.rbac;envoy.filters.http.local_ratelimit;envoy.filters.http.ratelimit;envoy.filters.http.custom_response
 type EnvoyFilter string
 
 const (
@@ -217,6 +219,9 @@ const (
 	// EnvoyFilterRateLimit defines the Envoy HTTP rate limit filter.
 	EnvoyFilterRateLimit EnvoyFilter = "envoy.filters.http.ratelimit"
 
+	// EnvoyFilterCustomResponse defines the Envoy HTTP custom response filter.
+	EnvoyFilterCustomResponse EnvoyFilter = "envoy.filters.http.custom_response"
+
 	// EnvoyFilterRouter defines the Envoy HTTP router filter.
 	EnvoyFilterRouter EnvoyFilter = "envoy.filters.http.router"
 )
diff --git a/api/v1alpha1/shared_types.go b/api/v1alpha1/shared_types.go
index fe795c833..617151e29 100644
--- a/api/v1alpha1/shared_types.go
+++ b/api/v1alpha1/shared_types.go
@@ -627,33 +627,48 @@ type ResponseOverride struct {
 // CustomResponseMatch defines the configuration for matching a user response to return a custom one.
 type CustomResponseMatch struct {
 	// Status code to match on. The match evaluates to true if any of the matches are successful.
-	StatusCode []StatusCodeMatch `json:"statusCode"`
+	// +kubebuilder:validation:MinItems=1
+	// +kubebuilder:validation:MaxItems=50
+	StatusCodes []StatusCodeMatch `json:"statusCodes"`
 }
 
 // StatusCodeValueType defines the types of values for the status code match supported by Envoy Gateway.
 // +kubebuilder:validation:Enum=Value;Range
 type StatusCodeValueType string
 
+const (
+	// StatusCodeValueTypeValue defines the "Value" status code match type.
+	StatusCodeValueTypeValue StatusCodeValueType = "Value"
+
+	// StatusCodeValueTypeRange defines the "Range" status code match type.
+	StatusCodeValueTypeRange StatusCodeValueType = "Range"
+)
+
+// StatusCodeMatch defines the configuration for matching a status code.
+// +kubebuilder:validation:XValidation:message="value must be set for type Value",rule="(!has(self.type) || self.type == 'Value')? has(self.value) : true"
+// +kubebuilder:validation:XValidation:message="range must be set for type Range",rule="(has(self.type) && self.type == 'Range')? has(self.range) : true"
 type StatusCodeMatch struct {
 	// Type is the type of value.
+	// Valid values are Value and Range, default is Value.
 	//
 	// +kubebuilder:default=Value
+	// +kubebuilder:validation:Enum=Value;Range
 	// +unionDiscriminator
 	Type *StatusCodeValueType `json:"type"`
 
 	// Value contains the value of the status code.
 	//
 	// +optional
-	Value *string `json:"value,omitempty"`
-	// ValueRef contains the contents of the body
-	// specified as a local object reference.
-	// Only a reference to ConfigMap is supported.
+	Value *int `json:"value,omitempty"`
+
+	// Range contains the range of status codes.
 	//
 	// +optional
 	Range *StatusCodeRange `json:"range,omitempty"`
 }
 
 // StatusCodeRange defines the configuration for define a range of status codes.
+// +kubebuilder:validation:XValidation: message="end must be greater than start",rule="self.end > self.start"
 type StatusCodeRange struct {
 	// Start of the range, including the start value.
 	Start int `json:"start"`
@@ -669,19 +684,31 @@ type CustomResponse struct {
 	ContentType *string `json:"contentType,omitempty"`
 
 	// Body of the Custom Response
-	//
-	// +optional
-	Body *CustomResponseBody `json:"body,omitempty"`
+	Body CustomResponseBody `json:"body"`
 }
 
 // ResponseValueType defines the types of values for the response body supported by Envoy Gateway.
 // +kubebuilder:validation:Enum=Inline;ValueRef
 type ResponseValueType string
 
+const (
+	// ResponseValueTypeInline defines the "Inline" response body type.
+	ResponseValueTypeInline ResponseValueType = "Inline"
+
+	// ResponseValueTypeValueRef defines the "ValueRef" response body type.
+	ResponseValueTypeValueRef ResponseValueType = "ValueRef"
+)
+
 // CustomResponseBody
+// +kubebuilder:validation:XValidation:message="inline must be set for type Inline",rule="(!has(self.type) || self.type == 'Inline')? has(self.inline) : true"
+// +kubebuilder:validation:XValidation:message="valueRef must be set for type ValueRef",rule="(has(self.type) && self.type == 'ValueRef')? has(self.valueRef) : true"
+// +kubebuilder:validation:XValidation:message="only ConfigMap is supported for ValueRef",rule="has(self.valueRef) ? self.valueRef.kind == 'ConfigMap' : true"
 type CustomResponseBody struct {
 	// Type is the type of method to use to read the body value.
+	// Valid values are Inline and ValueRef, default is Inline.
 	//
+	// +kubebuilder:default=Inline
+	// +kubebuilder:validation:Enum=Inline;ValueRef
 	// +unionDiscriminator
 	Type *ResponseValueType `json:"type"`
 
@@ -689,10 +716,14 @@ type CustomResponseBody struct {
 	//
 	// +optional
 	Inline *string `json:"inline,omitempty"`
+
 	// ValueRef contains the contents of the body
 	// specified as a local object reference.
 	// Only a reference to ConfigMap is supported.
 	//
+	// The value of key `response.body` in the ConfigMap will be used as the response body.
+	// If the key is not found, the first value in the ConfigMap will be used.
+	//
 	// +optional
 	ValueRef *gwapiv1.LocalObjectReference `json:"valueRef,omitempty"`
 }
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 696c99259..c225d65d3 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -1148,11 +1148,7 @@ func (in *CustomResponse) DeepCopyInto(out *CustomResponse) {
 		*out = new(string)
 		**out = **in
 	}
-	if in.Body != nil {
-		in, out := &in.Body, &out.Body
-		*out = new(CustomResponseBody)
-		(*in).DeepCopyInto(*out)
-	}
+	in.Body.DeepCopyInto(&out.Body)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResponse.
@@ -1198,8 +1194,8 @@ func (in *CustomResponseBody) DeepCopy() *CustomResponseBody {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CustomResponseMatch) DeepCopyInto(out *CustomResponseMatch) {
 	*out = *in
-	if in.StatusCode != nil {
-		in, out := &in.StatusCode, &out.StatusCode
+	if in.StatusCodes != nil {
+		in, out := &in.StatusCodes, &out.StatusCodes
 		*out = make([]StatusCodeMatch, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
@@ -5166,7 +5162,7 @@ func (in *StatusCodeMatch) DeepCopyInto(out *StatusCodeMatch) {
 	}
 	if in.Value != nil {
 		in, out := &in.Value, &out.Value
-		*out = new(string)
+		*out = new(int)
 		**out = **in
 	}
 	if in.Range != nil {
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
index 7b2e93731..f9fb0f329 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
@@ -940,16 +940,15 @@ spec:
                     match:
                       description: Match configuration.
                       properties:
-                        statusCode:
+                        statusCodes:
                           description: Status code to match on. The match evaluates
                             to true if any of the matches are successful.
                           items:
+                            description: StatusCodeMatch defines the configuration
+                              for matching a status code.
                             properties:
                               range:
-                                description: |-
-                                  ValueRef contains the contents of the body
-                                  specified as a local object reference.
-                                  Only a reference to ConfigMap is supported.
+                                description: Range contains the range of status codes.
                                 properties:
                                   end:
                                     description: End of the range, including the end
@@ -963,23 +962,41 @@ spec:
                                 - end
                                 - start
                                 type: object
+                                x-kubernetes-validations:
+                                - message: end must be greater than start
+                                  rule: self.end > self.start
                               type:
+                                allOf:
+                                - enum:
+                                  - Value
+                                  - Range
+                                - enum:
+                                  - Value
+                                  - Range
                                 default: Value
-                                description: Type is the type of value.
-                                enum:
-                                - Value
-                                - Range
+                                description: |-
+                                  Type is the type of value.
+                                  Valid values are Value and Range, default is Value.
                                 type: string
                               value:
                                 description: Value contains the value of the status
                                   code.
-                                type: string
+                                type: integer
                             required:
                             - type
                             type: object
+                            x-kubernetes-validations:
+                            - message: value must be set for type Value
+                              rule: '(!has(self.type) || self.type == ''Value'')?
+                                has(self.value) : true'
+                            - message: range must be set for type Range
+                              rule: '(has(self.type) && self.type == ''Range'')? has(self.range)
+                                : true'
+                          maxItems: 50
+                          minItems: 1
                           type: array
                       required:
-                      - statusCode
+                      - statusCodes
                       type: object
                     response:
                       description: Response configuration.
@@ -992,17 +1009,26 @@ spec:
                                 string.
                               type: string
                             type:
-                              description: Type is the type of method to use to read
-                                the body value.
-                              enum:
-                              - Inline
-                              - ValueRef
+                              allOf:
+                              - enum:
+                                - Inline
+                                - ValueRef
+                              - enum:
+                                - Inline
+                                - ValueRef
+                              default: Inline
+                              description: |-
+                                Type is the type of method to use to read the body value.
+                                Valid values are Inline and ValueRef, default is Inline.
                               type: string
                             valueRef:
                               description: |-
                                 ValueRef contains the contents of the body
                                 specified as a local object reference.
                                 Only a reference to ConfigMap is supported.
+
+                                The value of key `response.body` in the ConfigMap will be used as the response body.
+                                If the key is not found, the first value in the ConfigMap will be used.
                               properties:
                                 group:
                                   description: |-
@@ -1031,10 +1057,22 @@ spec:
                           required:
                           - type
                           type: object
+                          x-kubernetes-validations:
+                          - message: inline must be set for type Inline
+                            rule: '(!has(self.type) || self.type == ''Inline'')? has(self.inline)
+                              : true'
+                          - message: valueRef must be set for type ValueRef
+                            rule: '(has(self.type) && self.type == ''ValueRef'')?
+                              has(self.valueRef) : true'
+                          - message: only ConfigMap is supported for ValueRef
+                            rule: 'has(self.valueRef) ? self.valueRef.kind == ''ConfigMap''
+                              : true'
                         contentType:
                           description: Content Type of the response. This will be
                             set in the Content-Type header.
                           type: string
+                      required:
+                      - body
                       type: object
                   required:
                   - match
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
index 5b0130f27..0733ed112 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
@@ -305,6 +305,8 @@ spec:
 
                   - envoy.filters.http.ratelimit
 
+                  - envoy.filters.http.custom_response
+
                   - envoy.filters.http.router
 
                   Note: "envoy.filters.http.router" cannot be reordered, it's always the last filter in the chain.
@@ -330,6 +332,7 @@ spec:
                       - envoy.filters.http.rbac
                       - envoy.filters.http.local_ratelimit
                       - envoy.filters.http.ratelimit
+                      - envoy.filters.http.custom_response
                       type: string
                     before:
                       description: |-
@@ -349,6 +352,7 @@ spec:
                       - envoy.filters.http.rbac
                       - envoy.filters.http.local_ratelimit
                       - envoy.filters.http.ratelimit
+                      - envoy.filters.http.custom_response
                       type: string
                     name:
                       description: Name of the filter.
@@ -366,6 +370,7 @@ spec:
                       - envoy.filters.http.rbac
                       - envoy.filters.http.local_ratelimit
                       - envoy.filters.http.ratelimit
+                      - envoy.filters.http.custom_response
                       type: string
                   required:
                   - name
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml
index 8a75fec42..672cfb59d 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml
@@ -60,17 +60,26 @@ spec:
                         description: Inline contains the value as an inline string.
                         type: string
                       type:
-                        description: Type is the type of method to use to read the
-                          body value.
-                        enum:
-                        - Inline
-                        - ValueRef
+                        allOf:
+                        - enum:
+                          - Inline
+                          - ValueRef
+                        - enum:
+                          - Inline
+                          - ValueRef
+                        default: Inline
+                        description: |-
+                          Type is the type of method to use to read the body value.
+                          Valid values are Inline and ValueRef, default is Inline.
                         type: string
                       valueRef:
                         description: |-
                           ValueRef contains the contents of the body
                           specified as a local object reference.
                           Only a reference to ConfigMap is supported.
+
+                          The value of key `response.body` in the ConfigMap will be used as the response body.
+                          If the key is not found, the first value in the ConfigMap will be used.
                         properties:
                           group:
                             description: |-
@@ -99,6 +108,16 @@ spec:
                     required:
                     - type
                     type: object
+                    x-kubernetes-validations:
+                    - message: inline must be set for type Inline
+                      rule: '(!has(self.type) || self.type == ''Inline'')? has(self.inline)
+                        : true'
+                    - message: valueRef must be set for type ValueRef
+                      rule: '(has(self.type) && self.type == ''ValueRef'')? has(self.valueRef)
+                        : true'
+                    - message: only ConfigMap is supported for ValueRef
+                      rule: 'has(self.valueRef) ? self.valueRef.kind == ''ConfigMap''
+                        : true'
                   contentType:
                     description: Content Type of the response. This will be set in
                       the Content-Type header.
diff --git a/go.mod b/go.mod
index 4090b698e..a10aa68bb 100644
--- a/go.mod
+++ b/go.mod
@@ -281,7 +281,7 @@ require (
 	golang.org/x/time v0.5.0 // indirect
 	golang.org/x/tools v0.24.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go
index b8f289a9d..89b6804a2 100644
--- a/internal/gatewayapi/backendtrafficpolicy.go
+++ b/internal/gatewayapi/backendtrafficpolicy.go
@@ -10,9 +10,11 @@ import (
 	"fmt"
 	"math"
 	"sort"
+	"strconv"
 	"strings"
 
 	perr "github.com/pkg/errors"
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/sets"
@@ -34,6 +36,7 @@ func (t *Translator) ProcessBackendTrafficPolicies(backendTrafficPolicies []*egv
 	gateways []*GatewayContext,
 	routes []RouteContext,
 	xdsIR resource.XdsIRMap,
+	configMaps []*corev1.ConfigMap,
 ) []*egv1a1.BackendTrafficPolicy {
 	res := []*egv1a1.BackendTrafficPolicy{}
 
@@ -127,7 +130,7 @@ func (t *Translator) ProcessBackendTrafficPolicies(backendTrafficPolicies []*egv
 				}
 
 				// Set conditions for translation error if it got any
-				if err := t.translateBackendTrafficPolicyForRoute(policy, route, xdsIR); err != nil {
+				if err := t.translateBackendTrafficPolicyForRoute(policy, route, xdsIR, configMaps); err != nil {
 					status.SetTranslationErrorForPolicyAncestors(&policy.Status,
 						ancestorRefs,
 						t.GatewayControllerName,
@@ -181,7 +184,7 @@ func (t *Translator) ProcessBackendTrafficPolicies(backendTrafficPolicies []*egv
 				}
 
 				// Set conditions for translation error if it got any
-				if err := t.translateBackendTrafficPolicyForGateway(policy, currTarget, gateway, xdsIR); err != nil {
+				if err := t.translateBackendTrafficPolicyForGateway(policy, currTarget, gateway, xdsIR, configMaps); err != nil {
 					status.SetTranslationErrorForPolicyAncestors(&policy.Status,
 						ancestorRefs,
 						t.GatewayControllerName,
@@ -281,7 +284,12 @@ func resolveBTPolicyRouteTargetRef(policy *egv1a1.BackendTrafficPolicy, target g
 	return route.RouteContext, nil
 }
 
-func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.BackendTrafficPolicy, route RouteContext, xdsIR resource.XdsIRMap) error {
+func (t *Translator) translateBackendTrafficPolicyForRoute(
+	policy *egv1a1.BackendTrafficPolicy,
+	route RouteContext,
+	xdsIR resource.XdsIRMap,
+	configMaps []*corev1.ConfigMap,
+) error {
 	var (
 		rl        *ir.RateLimit
 		lb        *ir.LoadBalancer
@@ -295,6 +303,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.Backen
 		bc        *ir.BackendConnection
 		ds        *ir.DNS
 		h2        *ir.HTTP2Settings
+		ro        *ir.ResponseOverride
 		err, errs error
 	)
 
@@ -340,6 +349,11 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.Backen
 		errs = errors.Join(errs, err)
 	}
 
+	if ro, err = buildResponseOverride(policy, configMaps); err != nil {
+		err = perr.WithMessage(err, "ResponseOverride")
+		errs = errors.Join(errs, err)
+	}
+
 	ds = translateDNS(policy.Spec.ClusterSettings)
 
 	// Apply IR to all relevant routes
@@ -402,6 +416,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.Backen
 						HTTP2:             h2,
 						DNS:               ds,
 						Timeout:           to,
+						ResponseOverride:  ro,
 					}
 
 					// Update the Host field in HealthCheck, now that we have access to the Route Hostname.
@@ -418,7 +433,13 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.Backen
 	return errs
 }
 
-func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.BackendTrafficPolicy, target gwapiv1a2.LocalPolicyTargetReferenceWithSectionName, gateway *GatewayContext, xdsIR resource.XdsIRMap) error {
+func (t *Translator) translateBackendTrafficPolicyForGateway(
+	policy *egv1a1.BackendTrafficPolicy,
+	target gwapiv1a2.LocalPolicyTargetReferenceWithSectionName,
+	gateway *GatewayContext,
+	xdsIR resource.XdsIRMap,
+	configMaps []*corev1.ConfigMap,
+) error {
 	var (
 		rl        *ir.RateLimit
 		lb        *ir.LoadBalancer
@@ -431,6 +452,7 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.Back
 		rt        *ir.Retry
 		ds        *ir.DNS
 		h2        *ir.HTTP2Settings
+		ro        *ir.ResponseOverride
 		err, errs error
 	)
 
@@ -469,6 +491,10 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.Back
 		err = perr.WithMessage(err, "HTTP2")
 		errs = errors.Join(errs, err)
 	}
+	if ro, err = buildResponseOverride(policy, configMaps); err != nil {
+		err = perr.WithMessage(err, "ResponseOverride")
+		errs = errors.Join(errs, err)
+	}
 
 	ds = translateDNS(policy.Spec.ClusterSettings)
 
@@ -542,16 +568,17 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.Back
 			}
 
 			r.Traffic = &ir.TrafficFeatures{
-				RateLimit:      rl,
-				LoadBalancer:   lb,
-				ProxyProtocol:  pp,
-				HealthCheck:    hc,
-				CircuitBreaker: cb,
-				FaultInjection: fi,
-				TCPKeepalive:   ka,
-				Retry:          rt,
-				HTTP2:          h2,
-				DNS:            ds,
+				RateLimit:        rl,
+				LoadBalancer:     lb,
+				ProxyProtocol:    pp,
+				HealthCheck:      hc,
+				CircuitBreaker:   cb,
+				FaultInjection:   fi,
+				TCPKeepalive:     ka,
+				Retry:            rt,
+				HTTP2:            h2,
+				DNS:              ds,
+				ResponseOverride: ro,
 			}
 
 			// Update the Host field in HealthCheck, now that we have access to the Route Hostname.
@@ -836,3 +863,81 @@ func makeIrTriggerSet(in []egv1a1.TriggerEnum) []ir.TriggerEnum {
 	}
 	return irTriggers
 }
+
+func buildResponseOverride(policy *egv1a1.BackendTrafficPolicy, configMaps []*corev1.ConfigMap) (*ir.ResponseOverride, error) {
+	if len(policy.Spec.ResponseOverride) == 0 {
+		return nil, nil
+	}
+
+	rules := make([]ir.ResponseOverrideRule, 0, len(policy.Spec.ResponseOverride))
+	for index, ro := range policy.Spec.ResponseOverride {
+		match := ir.CustomResponseMatch{
+			StatusCodes: make([]ir.StatusCodeMatch, 0, len(ro.Match.StatusCodes)),
+		}
+
+		for _, code := range ro.Match.StatusCodes {
+			if code.Type != nil && *code.Type == egv1a1.StatusCodeValueTypeRange {
+				match.StatusCodes = append(match.StatusCodes, ir.StatusCodeMatch{
+					Range: &ir.StatusCodeRange{
+						Start: code.Range.Start,
+						End:   code.Range.End,
+					},
+				})
+			} else {
+				match.StatusCodes = append(match.StatusCodes, ir.StatusCodeMatch{
+					Value: code.Value,
+				})
+			}
+		}
+
+		response := ir.CustomResponse{
+			ContentType: ro.Response.ContentType,
+		}
+
+		if ro.Response.Body.Type != nil && *ro.Response.Body.Type == egv1a1.ResponseValueTypeValueRef {
+			foundCM := false
+			for _, cm := range configMaps {
+				if cm.Namespace == policy.Namespace && cm.Name == string(ro.Response.Body.ValueRef.Name) {
+					body, dataOk := cm.Data["response.body"]
+					switch {
+					case dataOk:
+						response.Body = body
+					case len(cm.Data) > 0: // Fallback to the first key if response.body is not found
+						for _, value := range cm.Data {
+							body = value
+							break
+						}
+						response.Body = body
+					default:
+						return nil, fmt.Errorf("can't find the key response.body in the referenced configmap %s", ro.Response.Body.ValueRef.Name)
+					}
+
+					foundCM = true
+					break
+				}
+			}
+			if !foundCM {
+				return nil, fmt.Errorf("can't find the referenced configmap %s", ro.Response.Body.ValueRef.Name)
+			}
+		} else {
+			response.Body = *ro.Response.Body.Inline
+		}
+
+		rules = append(rules, ir.ResponseOverrideRule{
+			Name:     defaultResponseOverrideRuleName(policy, index),
+			Match:    match,
+			Response: response,
+		})
+	}
+	return &ir.ResponseOverride{
+		Name:  irConfigName(policy),
+		Rules: rules,
+	}, nil
+}
+
+func defaultResponseOverrideRuleName(policy *egv1a1.BackendTrafficPolicy, index int) string {
+	return fmt.Sprintf(
+		"%s/responseoverride/rule/%s",
+		irConfigName(policy),
+		strconv.Itoa(index))
+}
diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go
index 0e6d683d8..0f518b710 100644
--- a/internal/gatewayapi/translator.go
+++ b/internal/gatewayapi/translator.go
@@ -211,7 +211,7 @@ func (t *Translator) Translate(resources *resource.Resources) (*TranslateResult,
 
 	// Process BackendTrafficPolicies
 	backendTrafficPolicies := t.ProcessBackendTrafficPolicies(
-		resources.BackendTrafficPolicies, gateways, routes, xdsIR)
+		resources.BackendTrafficPolicies, gateways, routes, xdsIR, resources.ConfigMaps)
 
 	// Process SecurityPolicies
 	securityPolicies := t.ProcessSecurityPolicies(
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index fdcace324..cb5021f4c 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -477,6 +477,63 @@ type HTTP2Settings struct {
 	ResetStreamOnError *bool `json:"resetStreamOnError,omitempty" yaml:"resetStreamOnError,omitempty"`
 }
 
+// ResponseOverride defines the configuration to override specific responses with a custom one.
+// +k8s:deepcopy-gen=true
+type ResponseOverride struct {
+	// Name is a unique name for a ResponseOverride configuration.
+	// The xds translator only generates one CustomResponse filter for each unique name.
+	Name string `json:"name" yaml:"name"`
+
+	// Rules contains the list of rules to override responses.
+	Rules []ResponseOverrideRule `json:"rules,omitempty"`
+}
+
+// ResponseOverrideRule defines the configuration for overriding a response.
+// +k8s:deepcopy-gen=true
+type ResponseOverrideRule struct {
+	// Name is a generated name for the rule.
+	Name string `json:"name"`
+	// Match configuration.
+	Match CustomResponseMatch `json:"match"`
+	// Response configuration.
+	Response CustomResponse `json:"response"`
+}
+
+// CustomResponseMatch defines the configuration for matching a user response to return a custom one.
+// +k8s:deepcopy-gen=true
+type CustomResponseMatch struct {
+	// Status code to match on. The match evaluates to true if any of the matches are successful.
+	StatusCodes []StatusCodeMatch `json:"statusCodes"`
+}
+
+// StatusCodeMatch defines the configuration for matching a status code.
+// +k8s:deepcopy-gen=true
+type StatusCodeMatch struct {
+	// Value contains the value of the status code.
+	Value *int `json:"value,omitempty"`
+
+	// Range contains a range of status codes.
+	Range *StatusCodeRange `json:"range,omitempty"`
+}
+
+// StatusCodeRange defines the configuration for define a range of status codes.
+type StatusCodeRange struct {
+	// Start of the range, including the start value.
+	Start int `json:"start"`
+	// End of the range, including the end value.
+	End int `json:"end"`
+}
+
+// CustomResponse defines the configuration for returning a custom response.
+// +k8s:deepcopy-gen=true
+type CustomResponse struct {
+	// Content Type of the response. This will be set in the Content-Type header.
+	ContentType *string `json:"contentType,omitempty"`
+
+	// Body of the Custom Response
+	Body string `json:"body"`
+}
+
 // HealthCheckSettings provides HealthCheck configuration on the HTTP/HTTPS listener.
 // +k8s:deepcopy-gen=true
 type HealthCheckSettings egv1a1.HealthCheckSettings
@@ -657,6 +714,8 @@ type TrafficFeatures struct {
 	HTTP2 *HTTP2Settings `json:"http2,omitempty" yaml:"http2,omitempty"`
 	// DNS is used to configure how DNS resolution is handled by the Envoy Proxy cluster
 	DNS *DNS `json:"dns,omitempty" yaml:"dns,omitempty"`
+	// ResponseOverride defines the schema for overriding the response.
+	ResponseOverride *ResponseOverride `json:"responseOverride,omitempty" yaml:"responseOverride,omitempty"`
 }
 
 func (b *TrafficFeatures) Validate() error {
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index 3c0c1135f..791b6d5dd 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -602,6 +602,48 @@ func (in *CoreListenerDetails) DeepCopy() *CoreListenerDetails {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CustomResponse) DeepCopyInto(out *CustomResponse) {
+	*out = *in
+	if in.ContentType != nil {
+		in, out := &in.ContentType, &out.ContentType
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResponse.
+func (in *CustomResponse) DeepCopy() *CustomResponse {
+	if in == nil {
+		return nil
+	}
+	out := new(CustomResponse)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CustomResponseMatch) DeepCopyInto(out *CustomResponseMatch) {
+	*out = *in
+	if in.StatusCodes != nil {
+		in, out := &in.StatusCodes, &out.StatusCodes
+		*out = make([]StatusCodeMatch, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResponseMatch.
+func (in *CustomResponseMatch) DeepCopy() *CustomResponseMatch {
+	if in == nil {
+		return nil
+	}
+	out := new(CustomResponseMatch)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *DNS) DeepCopyInto(out *DNS) {
 	*out = *in
@@ -2399,6 +2441,45 @@ func (in *ResourceMetadata) DeepCopy() *ResourceMetadata {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ResponseOverride) DeepCopyInto(out *ResponseOverride) {
+	*out = *in
+	if in.Rules != nil {
+		in, out := &in.Rules, &out.Rules
+		*out = make([]ResponseOverrideRule, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResponseOverride.
+func (in *ResponseOverride) DeepCopy() *ResponseOverride {
+	if in == nil {
+		return nil
+	}
+	out := new(ResponseOverride)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ResponseOverrideRule) DeepCopyInto(out *ResponseOverrideRule) {
+	*out = *in
+	in.Match.DeepCopyInto(&out.Match)
+	in.Response.DeepCopyInto(&out.Response)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResponseOverrideRule.
+func (in *ResponseOverrideRule) DeepCopy() *ResponseOverrideRule {
+	if in == nil {
+		return nil
+	}
+	out := new(ResponseOverrideRule)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Retry) DeepCopyInto(out *Retry) {
 	*out = *in
@@ -2590,6 +2671,31 @@ func (in *SlowStart) DeepCopy() *SlowStart {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *StatusCodeMatch) DeepCopyInto(out *StatusCodeMatch) {
+	*out = *in
+	if in.Value != nil {
+		in, out := &in.Value, &out.Value
+		*out = new(int)
+		**out = **in
+	}
+	if in.Range != nil {
+		in, out := &in.Range, &out.Range
+		*out = new(StatusCodeRange)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatusCodeMatch.
+func (in *StatusCodeMatch) DeepCopy() *StatusCodeMatch {
+	if in == nil {
+		return nil
+	}
+	out := new(StatusCodeMatch)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *StringMatch) DeepCopyInto(out *StringMatch) {
 	*out = *in
@@ -3159,6 +3265,11 @@ func (in *TrafficFeatures) DeepCopyInto(out *TrafficFeatures) {
 		*out = new(DNS)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.ResponseOverride != nil {
+		in, out := &in.ResponseOverride, &out.ResponseOverride
+		*out = new(ResponseOverride)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficFeatures.
diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go
index 7fe3c3d32..de020dfce 100644
--- a/internal/provider/kubernetes/controller.go
+++ b/internal/provider/kubernetes/controller.go
@@ -239,7 +239,7 @@ func (r *gatewayAPIReconciler) Reconcile(ctx context.Context, _ reconcile.Reques
 		}
 
 		// Add all BackendTrafficPolicies to the resourceTree
-		if err = r.processBackendTrafficPolicies(ctx, gwcResource); err != nil {
+		if err = r.processBackendTrafficPolicies(ctx, gwcResource, resourceMappings); err != nil {
 			return reconcile.Result{}, err
 		}
 
@@ -748,6 +748,39 @@ func (r *gatewayAPIReconciler) processConfigMapRef(
 	return nil
 }
 
+// processBtpConfigMapRefs adds the referenced ConfigMaps in BackendTrafficPolicies
+// to the resourceTree
+func (r *gatewayAPIReconciler) processBtpConfigMapRefs(
+	ctx context.Context, resourceTree *resource.Resources, resourceMap *resourceMappings,
+) {
+	for _, policy := range resourceTree.BackendTrafficPolicies {
+		for _, ro := range policy.Spec.ResponseOverride {
+			if ro.Response.Body.ValueRef != nil && string(ro.Response.Body.ValueRef.Kind) == resource.KindConfigMap {
+				configMap := new(corev1.ConfigMap)
+				err := r.client.Get(ctx,
+					types.NamespacedName{Namespace: policy.Namespace, Name: string(ro.Response.Body.ValueRef.Name)},
+					configMap,
+				)
+				// we don't return an error here, because we want to continue
+				// reconciling the rest of the BackendTrafficPolicies despite that this
+				// reference is invalid.
+				// This BackendTrafficPolicies will be marked as invalid in its status
+				// when translating to IR because the referenced configmap can't be
+				// found.
+				if err != nil {
+					r.log.Error(err,
+						"failed to process ResponseOverride ValueRef for BackendTrafficPolicy",
+						"policy", policy, "ValueRef", ro.Response.Body.ValueRef.Name)
+				}
+
+				resourceMap.allAssociatedNamespaces.Insert(policy.Namespace)
+				resourceTree.ConfigMaps = append(resourceTree.ConfigMaps, configMap)
+				r.log.Info("processing ConfigMap", "namespace", policy.Namespace, "name", string(ro.Response.Body.ValueRef.Name))
+			}
+		}
+	}
+}
+
 func (r *gatewayAPIReconciler) getNamespace(ctx context.Context, name string) (*corev1.Namespace, error) {
 	nsKey := types.NamespacedName{Name: name}
 	ns := new(corev1.Namespace)
@@ -942,7 +975,8 @@ func (r *gatewayAPIReconciler) processClientTrafficPolicies(
 }
 
 // processBackendTrafficPolicies adds BackendTrafficPolicies to the resourceTree
-func (r *gatewayAPIReconciler) processBackendTrafficPolicies(ctx context.Context, resourceTree *resource.Resources) error {
+func (r *gatewayAPIReconciler) processBackendTrafficPolicies(ctx context.Context, resourceTree *resource.Resources, resourceMap *resourceMappings,
+) error {
 	backendTrafficPolicies := egv1a1.BackendTrafficPolicyList{}
 	if err := r.client.List(ctx, &backendTrafficPolicies); err != nil {
 		return fmt.Errorf("error listing BackendTrafficPolicies: %w", err)
@@ -955,6 +989,7 @@ func (r *gatewayAPIReconciler) processBackendTrafficPolicies(ctx context.Context
 		policy.Status = gwapiv1a2.PolicyStatus{}
 		resourceTree.BackendTrafficPolicies = append(resourceTree.BackendTrafficPolicies, &policy)
 	}
+	r.processBtpConfigMapRefs(ctx, resourceTree, resourceMap)
 	return nil
 }
 
@@ -1348,7 +1383,7 @@ func (r *gatewayAPIReconciler) watchResources(ctx context.Context, mgr manager.M
 		return err
 	}
 
-	// Watch ConfigMap CRUDs and process affected ClienTraffiPolicies and BackendTLSPolicies.
+	// Watch ConfigMap CRUDs and process affected EG Resources.
 	configMapPredicates := []predicate.TypedPredicate[*corev1.ConfigMap]{
 		predicate.NewTypedPredicateFuncs[*corev1.ConfigMap](func(cm *corev1.ConfigMap) bool {
 			return r.validateConfigMapForReconcile(cm)
@@ -1492,6 +1527,10 @@ func (r *gatewayAPIReconciler) watchResources(ctx context.Context, mgr manager.M
 		return err
 	}
 
+	if err := addBtpIndexers(ctx, mgr); err != nil {
+		return err
+	}
+
 	// Watch SecurityPolicy
 	spPredicates := []predicate.TypedPredicate[*egv1a1.SecurityPolicy]{
 		predicate.TypedGenerationChangedPredicate[*egv1a1.SecurityPolicy]{},
diff --git a/internal/provider/kubernetes/indexers.go b/internal/provider/kubernetes/indexers.go
index 462a70542..2ad12069f 100644
--- a/internal/provider/kubernetes/indexers.go
+++ b/internal/provider/kubernetes/indexers.go
@@ -46,6 +46,7 @@ const (
 	secretEnvoyProxyIndex            = "secretEnvoyProxyIndex"
 	secretEnvoyExtensionPolicyIndex  = "secretEnvoyExtensionPolicyIndex"
 	httpRouteFilterHTTPRouteIndex    = "httpRouteFilterHTTPRouteIndex"
+	configMapBtpIndex                = "configMapBtpIndex"
 )
 
 func addReferenceGrantIndexers(ctx context.Context, mgr manager.Manager) error {
@@ -641,6 +642,36 @@ func secretCtpIndexFunc(rawObj client.Object) []string {
 	return secretReferences
 }
 
+// addBtpIndexers adds indexing on BackendTrafficPolicy, for ConfigMap objects that are
+// referenced in BackendTrafficPolicy objects. This helps in querying for BackendTrafficPolies that are
+// affected by a particular ConfigMap CRUD.
+func addBtpIndexers(ctx context.Context, mgr manager.Manager) error {
+	if err := mgr.GetFieldIndexer().IndexField(ctx, &egv1a1.BackendTrafficPolicy{}, configMapBtpIndex, configMapBtpIndexFunc); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func configMapBtpIndexFunc(rawObj client.Object) []string {
+	btp := rawObj.(*egv1a1.BackendTrafficPolicy)
+	var configMapReferences []string
+
+	for _, ro := range btp.Spec.ResponseOverride {
+		if ro.Response.Body.ValueRef != nil {
+			if string(ro.Response.Body.ValueRef.Kind) == resource.KindConfigMap {
+				configMapReferences = append(configMapReferences,
+					types.NamespacedName{
+						Namespace: btp.Namespace,
+						Name:      string(ro.Response.Body.ValueRef.Name),
+					}.String(),
+				)
+			}
+		}
+	}
+	return configMapReferences
+}
+
 // addBtlsIndexers adds indexing on BackendTLSPolicy, for ConfigMap objects that are
 // referenced in BackendTLSPolicy objects. This helps in querying for BackendTLSPolicies that are
 // affected by a particular ConfigMap CRUD.
diff --git a/internal/provider/kubernetes/predicates.go b/internal/provider/kubernetes/predicates.go
index 9c4d582b5..a885d58ca 100644
--- a/internal/provider/kubernetes/predicates.go
+++ b/internal/provider/kubernetes/predicates.go
@@ -588,7 +588,7 @@ func (r *gatewayAPIReconciler) handleNode(obj client.Object) bool {
 	return true
 }
 
-// validateConfigMapForReconcile checks whether the ConfigMap belongs to a valid ClientTrafficPolicy.
+// validateConfigMapForReconcile checks whether the ConfigMap belongs to a valid EG resource.
 func (r *gatewayAPIReconciler) validateConfigMapForReconcile(obj client.Object) bool {
 	configMap, ok := obj.(*corev1.ConfigMap)
 	if !ok {
@@ -604,8 +604,8 @@ func (r *gatewayAPIReconciler) validateConfigMapForReconcile(obj client.Object)
 		return false
 	}
 
-	if len(ctpList.Items) == 0 {
-		return false
+	if len(ctpList.Items) > 0 {
+		return true
 	}
 
 	btlsList := &gwapiv1a3.BackendTLSPolicyList{}
@@ -616,11 +616,23 @@ func (r *gatewayAPIReconciler) validateConfigMapForReconcile(obj client.Object)
 		return false
 	}
 
-	if len(btlsList.Items) == 0 {
+	if len(btlsList.Items) > 0 {
+		return true
+	}
+
+	btpList := &egv1a1.BackendTrafficPolicyList{}
+	if err := r.client.List(context.Background(), btpList, &client.ListOptions{
+		FieldSelector: fields.OneTermEqualSelector(configMapBtpIndex, utils.NamespacedName(configMap).String()),
+	}); err != nil {
+		r.log.Error(err, "unable to find associated BackendTrafficPolicy")
 		return false
 	}
 
-	return true
+	if len(btpList.Items) > 0 {
+		return true
+	}
+
+	return false
 }
 
 func (r *gatewayAPIReconciler) isEnvoyExtensionPolicyReferencingBackend(nsName *types.NamespacedName) bool {
diff --git a/internal/xds/translator/custom_response.go b/internal/xds/translator/custom_response.go
new file mode 100644
index 000000000..1d1bf3a5d
--- /dev/null
+++ b/internal/xds/translator/custom_response.go
@@ -0,0 +1,450 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+package translator
+
+import (
+	"errors"
+	"fmt"
+	"strconv"
+
+	cncfv3 "github.com/cncf/xds/go/xds/core/v3"
+	matcherv3 "github.com/cncf/xds/go/xds/type/matcher/v3"
+	typev3 "github.com/cncf/xds/go/xds/type/v3"
+	corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
+	respv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/custom_response/v3"
+	hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
+	policyv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/custom_response/local_response_policy/v3"
+	envoymatcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
+	expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
+	"google.golang.org/protobuf/types/known/anypb"
+
+	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
+	"github.com/envoyproxy/gateway/internal/ir"
+	"github.com/envoyproxy/gateway/internal/xds/types"
+)
+
+func init() {
+	registerHTTPFilter(&customResponse{})
+}
+
+type customResponse struct{}
+
+var _ httpFilter = &customResponse{}
+
+// patchHCM builds and appends the customResponse Filters to the HTTP Connection Manager
+// if applicable, and it does not already exist.
+// Note: this method creates an customResponse filter for each route that contains an ResponseOverride config.
+// the filter is disabled by default. It is enabled on the route level.
+func (c *customResponse) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error {
+	var errs error
+
+	if mgr == nil {
+		return errors.New("hcm is nil")
+	}
+
+	if irListener == nil {
+		return errors.New("ir listener is nil")
+	}
+
+	for _, route := range irListener.Routes {
+		if !c.routeContainsResponseOverride(route) {
+			continue
+		}
+
+		// Only generates one CustomResponse Envoy filter for each unique name.
+		// For example, if there are two routes under the same gateway with the
+		// same CustomResponse config, only one CustomResponse filter will be generated.
+		if hcmContainsFilter(mgr, c.customResponseFilterName(route.Traffic.ResponseOverride)) {
+			continue
+		}
+
+		filter, err := c.buildHCMCustomResponseFilter(route.Traffic.ResponseOverride)
+		if err != nil {
+			errs = errors.Join(errs, err)
+			continue
+		}
+
+		mgr.HttpFilters = append(mgr.HttpFilters, filter)
+	}
+
+	return errs
+}
+
+// buildHCMCustomResponseFilter returns an OAuth2 HTTP filter from the provided IR HTTPRoute.
+func (c *customResponse) buildHCMCustomResponseFilter(ro *ir.ResponseOverride) (*hcmv3.HttpFilter, error) {
+	proto, err := c.customResponseConfig(ro)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := proto.ValidateAll(); err != nil {
+		return nil, err
+	}
+
+	any, err := anypb.New(proto)
+	if err != nil {
+		return nil, err
+	}
+
+	return &hcmv3.HttpFilter{
+		Name:     c.customResponseFilterName(ro),
+		Disabled: true,
+		ConfigType: &hcmv3.HttpFilter_TypedConfig{
+			TypedConfig: any,
+		},
+	}, nil
+}
+
+func (c *customResponse) customResponseFilterName(ro *ir.ResponseOverride) string {
+	return perRouteFilterName(egv1a1.EnvoyFilterCustomResponse, ro.Name)
+}
+
+func (c *customResponse) customResponseConfig(ro *ir.ResponseOverride) (*respv3.CustomResponse, error) {
+	var matchers []*matcherv3.Matcher_MatcherList_FieldMatcher
+
+	for _, r := range ro.Rules {
+		var (
+			action    *matcherv3.Matcher_OnMatch_Action
+			predicate *matcherv3.Matcher_MatcherList_Predicate
+			err       error
+		)
+
+		if action, err = c.buildAction(r); err != nil {
+			return nil, err
+		}
+
+		switch {
+		case len(r.Match.StatusCodes) == 0:
+			// This is just a sanity check, as the CRD validation should have caught this.
+			return nil, fmt.Errorf("missing status code in response override rule")
+		case len(r.Match.StatusCodes) == 1:
+			if predicate, err = c.buildSinglePredicate(r.Match.StatusCodes[0]); err != nil {
+				return nil, err
+			}
+
+			matcher := &matcherv3.Matcher_MatcherList_FieldMatcher{
+				Predicate: predicate,
+				OnMatch: &matcherv3.Matcher_OnMatch{
+					OnMatch: action,
+				},
+			}
+
+			matchers = append(matchers, matcher)
+		case len(r.Match.StatusCodes) > 1:
+			var predicates []*matcherv3.Matcher_MatcherList_Predicate
+
+			for _, codeMatch := range r.Match.StatusCodes {
+				if predicate, err = c.buildSinglePredicate(codeMatch); err != nil {
+					return nil, err
+				}
+
+				predicates = append(predicates, predicate)
+			}
+
+			// Create a single matcher that ORs all the predicates together.
+			// The rule will match if any of the codes match.
+			matcher := &matcherv3.Matcher_MatcherList_FieldMatcher{
+				Predicate: &matcherv3.Matcher_MatcherList_Predicate{
+					MatchType: &matcherv3.Matcher_MatcherList_Predicate_OrMatcher{
+						OrMatcher: &matcherv3.Matcher_MatcherList_Predicate_PredicateList{
+							Predicate: predicates,
+						},
+					},
+				},
+				OnMatch: &matcherv3.Matcher_OnMatch{
+					OnMatch: action,
+				},
+			}
+
+			matchers = append(matchers, matcher)
+		}
+
+	}
+
+	// Create a MatcherList.
+	// The rules will be evaluated in order, and the first match wins.
+	cr := &respv3.CustomResponse{
+		CustomResponseMatcher: &matcherv3.Matcher{
+			MatcherType: &matcherv3.Matcher_MatcherList_{
+				MatcherList: &matcherv3.Matcher_MatcherList{
+					Matchers: matchers,
+				},
+			},
+		},
+	}
+
+	return cr, nil
+}
+
+func (c *customResponse) buildSinglePredicate(codeMatch ir.StatusCodeMatch) (*matcherv3.Matcher_MatcherList_Predicate, error) {
+	var (
+		httpAttributeCELInput *cncfv3.TypedExtensionConfig
+		statusCodeInput       *cncfv3.TypedExtensionConfig
+		statusCodeCELMatcher  *cncfv3.TypedExtensionConfig
+		err                   error
+	)
+
+	// Use CEL to match a range of status codes.
+	if codeMatch.Range != nil {
+		if httpAttributeCELInput, err = c.buildHTTPAttributeCELInput(); err != nil {
+			return nil, err
+		}
+
+		if statusCodeCELMatcher, err = c.buildStatusCodeCELMatcher(*codeMatch.Range); err != nil {
+			return nil, err
+		}
+
+		return &matcherv3.Matcher_MatcherList_Predicate{
+			MatchType: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate_{
+				SinglePredicate: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate{
+					Input: httpAttributeCELInput,
+					Matcher: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate_CustomMatch{
+						CustomMatch: statusCodeCELMatcher,
+					},
+				},
+			},
+		}, nil
+	} else {
+		// Use exact string match to match a single status code.
+		if statusCodeInput, err = c.buildStatusCodeInput(); err != nil {
+			return nil, err
+		}
+
+		return &matcherv3.Matcher_MatcherList_Predicate{
+			MatchType: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate_{
+				SinglePredicate: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate{
+					Input: statusCodeInput,
+					Matcher: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate_ValueMatch{
+						ValueMatch: &matcherv3.StringMatcher{
+							MatchPattern: &matcherv3.StringMatcher_Exact{
+								Exact: strconv.Itoa(*codeMatch.Value),
+							},
+						},
+					},
+				},
+			},
+		}, nil
+	}
+}
+
+func (c *customResponse) buildHTTPAttributeCELInput() (*cncfv3.TypedExtensionConfig, error) {
+	var (
+		pb  *anypb.Any
+		err error
+	)
+
+	if pb, err = anypb.New(&matcherv3.HttpAttributesCelMatchInput{}); err != nil {
+		return nil, err
+	}
+
+	return &cncfv3.TypedExtensionConfig{
+		Name:        "http-attributes-cel-match-input",
+		TypedConfig: pb,
+	}, nil
+}
+
+func (c *customResponse) buildStatusCodeInput() (*cncfv3.TypedExtensionConfig, error) {
+	var (
+		pb  *anypb.Any
+		err error
+	)
+
+	if pb, err = anypb.New(&envoymatcherv3.HttpResponseStatusCodeMatchInput{}); err != nil {
+		return nil, err
+	}
+
+	return &cncfv3.TypedExtensionConfig{
+		Name:        "http-response-status-code-match-input",
+		TypedConfig: pb,
+	}, nil
+}
+
+func (c *customResponse) buildStatusCodeCELMatcher(codeRange ir.StatusCodeRange) (*cncfv3.TypedExtensionConfig, error) {
+	var (
+		pb  *anypb.Any
+		err error
+	)
+
+	// Build the CEL expression AST: response.code >= codeRange.Start && response.code <= codeRange.End
+	matcher := &matcherv3.CelMatcher{
+		ExprMatch: &typev3.CelExpression{
+			ExprSpecifier: &typev3.CelExpression_ParsedExpr{
+				ParsedExpr: &expr.ParsedExpr{
+					Expr: &expr.Expr{
+						Id: 9,
+						ExprKind: &expr.Expr_CallExpr{
+							CallExpr: &expr.Expr_Call{
+								Function: "_&&_",
+								Args: []*expr.Expr{
+									{
+										Id: 3,
+										ExprKind: &expr.Expr_CallExpr{
+											CallExpr: &expr.Expr_Call{
+												Function: "_>=_",
+												Args: []*expr.Expr{
+													{
+														Id: 2,
+														ExprKind: &expr.Expr_SelectExpr{
+															SelectExpr: &expr.Expr_Select{
+																Operand: &expr.Expr{
+																	Id: 1,
+																	ExprKind: &expr.Expr_IdentExpr{
+																		IdentExpr: &expr.Expr_Ident{
+																			Name: "response",
+																		},
+																	},
+																},
+																Field: "code",
+															},
+														},
+													},
+													{
+														Id: 4,
+														ExprKind: &expr.Expr_ConstExpr{
+															ConstExpr: &expr.Constant{
+																ConstantKind: &expr.Constant_Int64Value{
+																	Int64Value: int64(codeRange.Start),
+																},
+															},
+														},
+													},
+												},
+											},
+										},
+									},
+									{
+										Id: 7,
+										ExprKind: &expr.Expr_CallExpr{
+											CallExpr: &expr.Expr_Call{
+												Function: "_<=_",
+												Args: []*expr.Expr{
+													{
+														Id: 6,
+														ExprKind: &expr.Expr_SelectExpr{
+															SelectExpr: &expr.Expr_Select{
+																Operand: &expr.Expr{
+																	Id: 5,
+																	ExprKind: &expr.Expr_IdentExpr{
+																		IdentExpr: &expr.Expr_Ident{
+																			Name: "response",
+																		},
+																	},
+																},
+																Field: "code",
+															},
+														},
+													},
+													{
+														Id: 8,
+														ExprKind: &expr.Expr_ConstExpr{
+															ConstExpr: &expr.Constant{
+																ConstantKind: &expr.Constant_Int64Value{
+																	Int64Value: int64(codeRange.End),
+																},
+															},
+														},
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	if err := matcher.ValidateAll(); err != nil {
+		return nil, err
+	}
+
+	if pb, err = anypb.New(matcher); err != nil {
+		return nil, err
+	}
+
+	return &cncfv3.TypedExtensionConfig{
+		Name:        "cel-matcher",
+		TypedConfig: pb,
+	}, nil
+}
+
+func (c *customResponse) buildAction(r ir.ResponseOverrideRule) (*matcherv3.Matcher_OnMatch_Action, error) {
+	response := &policyv3.LocalResponsePolicy{
+		Body: &corev3.DataSource{
+			Specifier: &corev3.DataSource_InlineString{
+				InlineString: r.Response.Body,
+			},
+		},
+	}
+
+	if r.Response.ContentType != nil && *r.Response.ContentType != "" {
+		response.ResponseHeadersToAdd = append(response.ResponseHeadersToAdd, &corev3.HeaderValueOption{
+			Header: &corev3.HeaderValue{
+				Key:   "Content-Type",
+				Value: *r.Response.ContentType,
+			},
+			AppendAction: corev3.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD,
+		})
+	}
+
+	var (
+		pb  *anypb.Any
+		err error
+	)
+
+	if err := response.ValidateAll(); err != nil {
+		return nil, err
+	}
+
+	if pb, err = anypb.New(response); err != nil {
+		return nil, err
+	}
+
+	return &matcherv3.Matcher_OnMatch_Action{
+		Action: &cncfv3.TypedExtensionConfig{
+			Name:        r.Name,
+			TypedConfig: pb,
+		},
+	}, nil
+}
+
+// routeContainsResponseOverride returns true if ResponseOverride exists for the provided route.
+func (c *customResponse) routeContainsResponseOverride(irRoute *ir.HTTPRoute) bool {
+	if irRoute != nil &&
+		irRoute.Traffic != nil &&
+		irRoute.Traffic.ResponseOverride != nil {
+		return true
+	}
+	return false
+}
+
+func (c *customResponse) patchResources(tCtx *types.ResourceVersionTable,
+	routes []*ir.HTTPRoute,
+) error {
+	return nil
+}
+
+// patchRoute patches the provided route with the customResponse config if applicable.
+// Note: this method enables the corresponding customResponse filter for the provided route.
+func (c *customResponse) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute) error {
+	if route == nil {
+		return errors.New("xds route is nil")
+	}
+	if irRoute == nil {
+		return errors.New("ir route is nil")
+	}
+	if irRoute.Traffic == nil || irRoute.Traffic.ResponseOverride == nil {
+		return nil
+	}
+	filterName := c.customResponseFilterName(irRoute.Traffic.ResponseOverride)
+	if err := enableFilterOnRoute(route, filterName); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/internal/xds/translator/oidc.go b/internal/xds/translator/oidc.go
index 963b7c804..e4e7b4a02 100644
--- a/internal/xds/translator/oidc.go
+++ b/internal/xds/translator/oidc.go
@@ -53,9 +53,9 @@ func (*oidc) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListe
 			continue
 		}
 
-		// Only generates one BasicAuth Envoy filter for each unique name.
+		// Only generates one OAuth2 Envoy filter for each unique name.
 		// For example, if there are two routes under the same gateway with the
-		// same BasicAuth config, only one BasicAuth filter will be generated.
+		// same OAuth2 config, only one OAuth2 filter will be generated.
 		if hcmContainsFilter(mgr, oauth2FilterName(route.Security.OIDC)) {
 			continue
 		}


Test patch:
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override-invalid-valueref.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override-invalid-valueref.in.yaml
new file mode 100644
index 000000000..e44a8473d
--- /dev/null
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override-invalid-valueref.in.yaml
@@ -0,0 +1,141 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1
+    kind: Gateway
+    metadata:
+      namespace: default
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: http
+          protocol: HTTP
+          port: 80
+          allowedRoutes:
+            namespaces:
+              from: All
+  - apiVersion: gateway.networking.k8s.io/v1
+    kind: Gateway
+    metadata:
+      namespace: default
+      name: gateway-2
+    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: default
+          name: gateway-1
+          sectionName: http
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8080
+httpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1
+    kind: HTTPRoute
+    metadata:
+      namespace: default
+      name: httproute-1
+    spec:
+      hostnames:
+        - gateway.envoyproxy.io
+      parentRefs:
+        - namespace: default
+          name: gateway-2
+          sectionName: http
+      rules:
+        - matches:
+            - path:
+                value: "/"
+          backendRefs:
+            - name: service-1
+              port: 8080
+configMaps:
+  - apiVersion: v1
+    kind: ConfigMap
+    metadata:
+      name: response-override-config
+      namespace: default
+    data: {}
+backendTrafficPolicies:
+  - apiVersion: gateway.envoyproxy.io/v1alpha1
+    kind: BackendTrafficPolicy
+    metadata:
+      namespace: default
+      name: policy-for-gateway
+    spec:
+      targetRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+      responseOverride:
+        - match:
+            statusCodes:
+              - type: Value
+                value: 404
+          response:
+            contentType: text/plain
+            body:
+              type: Inline
+              inline: "gateway-1 Not Found"
+        - match:
+            statusCodes:
+              - type: Value
+                value: 500
+              - type: Range
+                range:
+                  start: 501
+                  end: 511
+          response:
+            contentType: application/json
+            body:
+              type: ValueRef
+              valueRef:
+                group: ""
+                kind: ConfigMap
+                name: response-override-config
+  - apiVersion: gateway.envoyproxy.io/v1alpha1
+    kind: BackendTrafficPolicy
+    metadata:
+      namespace: default
+      name: policy-for-route
+    spec:
+      targetRef:
+        group: gateway.networking.k8s.io
+        kind: HTTPRoute
+        name: httproute-1
+      responseOverride:
+        - match:
+            statusCodes:
+              - value: 404
+          response:
+            contentType: text/plain
+            body:
+              inline: "httproute-1 Not Found"
+        - match:
+            statusCodes:
+              - value: 500
+              - type: Range
+                range:
+                  start: 501
+                  end: 511
+          response:
+            contentType: application/json
+            body:
+              type: ValueRef
+              valueRef:
+                group: ""
+                kind: ConfigMap
+                name: response-override-config-1
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override-invalid-valueref.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override-invalid-valueref.out.yaml
new file mode 100644
index 000000000..c1542d9ca
--- /dev/null
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override-invalid-valueref.out.yaml
@@ -0,0 +1,371 @@
+backendTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-for-route
+    namespace: default
+  spec:
+    responseOverride:
+    - match:
+        statusCodes:
+        - type: null
+          value: 404
+      response:
+        body:
+          inline: httproute-1 Not Found
+          type: null
+        contentType: text/plain
+    - match:
+        statusCodes:
+        - type: null
+          value: 500
+        - range:
+            end: 511
+            start: 501
+          type: Range
+      response:
+        body:
+          type: ValueRef
+          valueRef:
+            group: ""
+            kind: ConfigMap
+            name: response-override-config-1
+        contentType: application/json
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-1
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-2
+        namespace: default
+        sectionName: http
+      conditions:
+      - lastTransitionTime: null
+        message: 'ResponseOverride: can''t find the referenced configmap response-override-config-1.'
+        reason: Invalid
+        status: "False"
+        type: Accepted
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-for-gateway
+    namespace: default
+  spec:
+    responseOverride:
+    - match:
+        statusCodes:
+        - type: Value
+          value: 404
+      response:
+        body:
+          inline: gateway-1 Not Found
+          type: Inline
+        contentType: text/plain
+    - match:
+        statusCodes:
+        - type: Value
+          value: 500
+        - range:
+            end: 511
+            start: 501
+          type: Range
+      response:
+        body:
+          type: ValueRef
+          valueRef:
+            group: ""
+            kind: ConfigMap
+            name: response-override-config
+        contentType: application/json
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: default
+      conditions:
+      - lastTransitionTime: null
+        message: 'ResponseOverride: can''t find the key response.body in the referenced
+          configmap response-override-config.'
+        reason: Invalid
+        status: "False"
+        type: Accepted
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: default
+  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
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-2
+    namespace: default
+  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: default
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+  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: default
+        sectionName: http
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-1
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-2
+      namespace: default
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - path:
+          value: /
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-2
+        namespace: default
+        sectionName: http
+infraIR:
+  default/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: default/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: default
+      name: default/gateway-1
+  default/gateway-2:
+    proxy:
+      listeners:
+      - address: null
+        name: default/gateway-2/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-2
+          gateway.envoyproxy.io/owning-gateway-namespace: default
+      name: default/gateway-2
+xdsIR:
+  default/gateway-1:
+    accessLog:
+      text:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      isHTTP2: true
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: default
+        sectionName: http
+      name: default/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - destination:
+          name: grpcroute/default/grpcroute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            protocol: GRPC
+            weight: 1
+        directResponse:
+          statusCode: 500
+        hostname: '*'
+        isHTTP2: true
+        metadata:
+          kind: GRPCRoute
+          name: grpcroute-1
+          namespace: default
+        name: grpcroute/default/grpcroute-1/rule/0/match/-1/*
+  default/gateway-2:
+    accessLog:
+      text:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-2
+        namespace: default
+        sectionName: http
+      name: default/gateway-2/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - destination:
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            protocol: HTTP
+            weight: 1
+        directResponse:
+          statusCode: 500
+        hostname: gateway.envoyproxy.io
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+        name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.in.yaml
new file mode 100644
index 000000000..51dd9fd71
--- /dev/null
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.in.yaml
@@ -0,0 +1,145 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1
+    kind: Gateway
+    metadata:
+      namespace: default
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: http
+          protocol: HTTP
+          port: 80
+          allowedRoutes:
+            namespaces:
+              from: All
+  - apiVersion: gateway.networking.k8s.io/v1
+    kind: Gateway
+    metadata:
+      namespace: default
+      name: gateway-2
+    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: default
+          name: gateway-1
+          sectionName: http
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8080
+httpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1
+    kind: HTTPRoute
+    metadata:
+      namespace: default
+      name: httproute-1
+    spec:
+      hostnames:
+        - gateway.envoyproxy.io
+      parentRefs:
+        - namespace: default
+          name: gateway-2
+          sectionName: http
+      rules:
+        - matches:
+            - path:
+                value: "/"
+          backendRefs:
+            - name: service-1
+              port: 8080
+configMaps:
+  - apiVersion: v1
+    kind: ConfigMap
+    metadata:
+      name: response-override-config
+      namespace: default
+    data:
+      response.body: |
+        {
+          "error": "Internal Server Error"
+        }
+backendTrafficPolicies:
+  - apiVersion: gateway.envoyproxy.io/v1alpha1
+    kind: BackendTrafficPolicy
+    metadata:
+      namespace: default
+      name: policy-for-gateway
+    spec:
+      targetRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+      responseOverride:
+        - match:
+            statusCodes:
+              - type: Value
+                value: 404
+          response:
+            contentType: text/plain
+            body:
+              type: Inline
+              inline: "gateway-1 Not Found"
+        - match:
+            statusCodes:
+              - type: Value
+                value: 500
+              - type: Range
+                range:
+                  start: 501
+                  end: 511
+          response:
+            contentType: application/json
+            body:
+              type: ValueRef
+              valueRef:
+                group: ""
+                kind: ConfigMap
+                name: response-override-config
+  - apiVersion: gateway.envoyproxy.io/v1alpha1
+    kind: BackendTrafficPolicy
+    metadata:
+      namespace: default
+      name: policy-for-route
+    spec:
+      targetRef:
+        group: gateway.networking.k8s.io
+        kind: HTTPRoute
+        name: httproute-1
+      responseOverride:
+        - match:
+            statusCodes:
+              - value: 404
+          response:
+            contentType: text/plain
+            body:
+              inline: "httproute-1 Not Found"
+        - match:
+            statusCodes:
+              - value: 500
+              - type: Range
+                range:
+                  start: 501
+                  end: 511
+          response:
+            contentType: application/json
+            body:
+              type: ValueRef
+              valueRef:
+                group: ""
+                kind: ConfigMap
+                name: response-override-config
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.out.yaml
new file mode 100644
index 000000000..568a57af4
--- /dev/null
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.out.yaml
@@ -0,0 +1,414 @@
+backendTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-for-route
+    namespace: default
+  spec:
+    responseOverride:
+    - match:
+        statusCodes:
+        - type: null
+          value: 404
+      response:
+        body:
+          inline: httproute-1 Not Found
+          type: null
+        contentType: text/plain
+    - match:
+        statusCodes:
+        - type: null
+          value: 500
+        - range:
+            end: 511
+            start: 501
+          type: Range
+      response:
+        body:
+          type: ValueRef
+          valueRef:
+            group: ""
+            kind: ConfigMap
+            name: response-override-config
+        contentType: application/json
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-1
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-2
+        namespace: default
+        sectionName: http
+      conditions:
+      - lastTransitionTime: null
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-for-gateway
+    namespace: default
+  spec:
+    responseOverride:
+    - match:
+        statusCodes:
+        - type: Value
+          value: 404
+      response:
+        body:
+          inline: gateway-1 Not Found
+          type: Inline
+        contentType: text/plain
+    - match:
+        statusCodes:
+        - type: Value
+          value: 500
+        - range:
+            end: 511
+            start: 501
+          type: Range
+      response:
+        body:
+          type: ValueRef
+          valueRef:
+            group: ""
+            kind: ConfigMap
+            name: response-override-config
+        contentType: application/json
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: default
+      conditions:
+      - lastTransitionTime: null
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: default
+  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
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-2
+    namespace: default
+  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: default
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+  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: default
+        sectionName: http
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-1
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-2
+      namespace: default
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - path:
+          value: /
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-2
+        namespace: default
+        sectionName: http
+infraIR:
+  default/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: default/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: default
+      name: default/gateway-1
+  default/gateway-2:
+    proxy:
+      listeners:
+      - address: null
+        name: default/gateway-2/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-2
+          gateway.envoyproxy.io/owning-gateway-namespace: default
+      name: default/gateway-2
+xdsIR:
+  default/gateway-1:
+    accessLog:
+      text:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      isHTTP2: true
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: default
+        sectionName: http
+      name: default/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - destination:
+          name: grpcroute/default/grpcroute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            protocol: GRPC
+            weight: 1
+        hostname: '*'
+        isHTTP2: true
+        metadata:
+          kind: GRPCRoute
+          name: grpcroute-1
+          namespace: default
+        name: grpcroute/default/grpcroute-1/rule/0/match/-1/*
+        traffic:
+          responseOverride:
+            name: backendtrafficpolicy/default/policy-for-gateway
+            rules:
+            - match:
+                statusCodes:
+                - value: 404
+              name: backendtrafficpolicy/default/policy-for-gateway/responseoverride/rule/0
+              response:
+                body: gateway-1 Not Found
+                contentType: text/plain
+            - match:
+                statusCodes:
+                - value: 500
+                - range:
+                    end: 511
+                    start: 501
+              name: backendtrafficpolicy/default/policy-for-gateway/responseoverride/rule/1
+              response:
+                body: |
+                  {
+                    "error": "Internal Server Error"
+                  }
+                contentType: application/json
+  default/gateway-2:
+    accessLog:
+      text:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-2
+        namespace: default
+        sectionName: http
+      name: default/gateway-2/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - destination:
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            protocol: HTTP
+            weight: 1
+        hostname: gateway.envoyproxy.io
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+        name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /
+        traffic:
+          responseOverride:
+            name: backendtrafficpolicy/default/policy-for-route
+            rules:
+            - match:
+                statusCodes:
+                - value: 404
+              name: backendtrafficpolicy/default/policy-for-route/responseoverride/rule/0
+              response:
+                body: httproute-1 Not Found
+                contentType: text/plain
+            - match:
+                statusCodes:
+                - value: 500
+                - range:
+                    end: 511
+                    start: 501
+              name: backendtrafficpolicy/default/policy-for-route/responseoverride/rule/1
+              response:
+                body: |
+                  {
+                    "error": "Internal Server Error"
+                  }
+                contentType: application/json
diff --git a/internal/xds/translator/testdata/in/xds-ir/custom-response.yaml b/internal/xds/translator/testdata/in/xds-ir/custom-response.yaml
new file mode 100644
index 000000000..cb00ac65a
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/custom-response.yaml
@@ -0,0 +1,56 @@
+http:
+  - address: 0.0.0.0
+    hostnames:
+      - "*"
+    isHTTP2: false
+    metadata:
+      kind: Gateway
+      name: gateway-1
+      namespace: default
+      sectionName: http
+    name: default/gateway-1/http
+    path:
+      escapedSlashesAction: UnescapeAndRedirect
+      mergeSlashes: true
+    port: 10080
+    routes:
+      - destination:
+          name: httproute/default/httproute-1/rule/0
+          settings:
+            - addressType: IP
+              endpoints:
+                - host: 7.7.7.7
+                  port: 8080
+              protocol: HTTP
+              weight: 1
+        hostname: "*"
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+        name: httproute/default/httproute-1/rule/0/match/-1/*
+        traffic:
+          responseOverride:
+            name: backendtrafficpolicy/default/policy-for-gateway
+            rules:
+              - match:
+                  statusCodes:
+                    - value: 404
+                name: backendtrafficpolicy/default/policy-for-gateway/responseoverride/rule/0
+                response:
+                  body: gateway-1 Not Found
+                  contentType: text/plain
+              - match:
+                  statusCodes:
+                    - value: 500
+                    - range:
+                        end: 511
+                        start: 501
+                name: backendtrafficpolicy/default/policy-for-gateway/responseoverride/rule/1
+                response:
+                  body: |
+                    {
+                      "error": "Internal Server Error"
+                    }
+                  contentType: application/json
diff --git a/internal/xds/translator/testdata/out/xds-ir/custom-response.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/custom-response.clusters.yaml
new file mode 100644
index 000000000..9714612e3
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/custom-response.clusters.yaml
@@ -0,0 +1,17 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_ONLY
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: httproute/default/httproute-1/rule/0
+  lbPolicy: LEAST_REQUEST
+  name: httproute/default/httproute-1/rule/0
+  outlierDetection: {}
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/custom-response.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/custom-response.endpoints.yaml
new file mode 100644
index 000000000..29bb6b4e4
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/custom-response.endpoints.yaml
@@ -0,0 +1,12 @@
+- clusterName: httproute/default/httproute-1/rule/0
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 7.7.7.7
+            portValue: 8080
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: httproute/default/httproute-1/rule/0/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/custom-response.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/custom-response.listeners.yaml
new file mode 100644
index 000000000..19c565869
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/custom-response.listeners.yaml
@@ -0,0 +1,130 @@
+- address:
+    socketAddress:
+      address: 0.0.0.0
+      portValue: 10080
+  defaultFilterChain:
+    filters:
+    - name: envoy.filters.network.http_connection_manager
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+        commonHttpProtocolOptions:
+          headersWithUnderscoresAction: REJECT_REQUEST
+        http2ProtocolOptions:
+          initialConnectionWindowSize: 1048576
+          initialStreamWindowSize: 65536
+          maxConcurrentStreams: 100
+        httpFilters:
+        - disabled: true
+          name: envoy.filters.http.custom_response/backendtrafficpolicy/default/policy-for-gateway
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.custom_response.v3.CustomResponse
+            customResponseMatcher:
+              matcherList:
+                matchers:
+                - onMatch:
+                    action:
+                      name: backendtrafficpolicy/default/policy-for-gateway/responseoverride/rule/0
+                      typedConfig:
+                        '@type': type.googleapis.com/envoy.extensions.http.custom_response.local_response_policy.v3.LocalResponsePolicy
+                        body:
+                          inlineString: gateway-1 Not Found
+                        responseHeadersToAdd:
+                        - appendAction: OVERWRITE_IF_EXISTS_OR_ADD
+                          header:
+                            key: Content-Type
+                            value: text/plain
+                  predicate:
+                    singlePredicate:
+                      input:
+                        name: http-response-status-code-match-input
+                        typedConfig:
+                          '@type': type.googleapis.com/envoy.type.matcher.v3.HttpResponseStatusCodeMatchInput
+                      valueMatch:
+                        exact: "404"
+                - onMatch:
+                    action:
+                      name: backendtrafficpolicy/default/policy-for-gateway/responseoverride/rule/1
+                      typedConfig:
+                        '@type': type.googleapis.com/envoy.extensions.http.custom_response.local_response_policy.v3.LocalResponsePolicy
+                        body:
+                          inlineString: |
+                            {
+                              "error": "Internal Server Error"
+                            }
+                        responseHeadersToAdd:
+                        - appendAction: OVERWRITE_IF_EXISTS_OR_ADD
+                          header:
+                            key: Content-Type
+                            value: application/json
+                  predicate:
+                    orMatcher:
+                      predicate:
+                      - singlePredicate:
+                          input:
+                            name: http-response-status-code-match-input
+                            typedConfig:
+                              '@type': type.googleapis.com/envoy.type.matcher.v3.HttpResponseStatusCodeMatchInput
+                          valueMatch:
+                            exact: "500"
+                      - singlePredicate:
+                          customMatch:
+                            name: cel-matcher
+                            typedConfig:
+                              '@type': type.googleapis.com/xds.type.matcher.v3.CelMatcher
+                              exprMatch:
+                                parsedExpr:
+                                  expr:
+                                    callExpr:
+                                      args:
+                                      - callExpr:
+                                          args:
+                                          - id: "2"
+                                            selectExpr:
+                                              field: code
+                                              operand:
+                                                id: "1"
+                                                identExpr:
+                                                  name: response
+                                          - constExpr:
+                                              int64Value: "501"
+                                            id: "4"
+                                          function: _>=_
+                                        id: "3"
+                                      - callExpr:
+                                          args:
+                                          - id: "6"
+                                            selectExpr:
+                                              field: code
+                                              operand:
+                                                id: "5"
+                                                identExpr:
+                                                  name: response
+                                          - constExpr:
+                                              int64Value: "511"
+                                            id: "8"
+                                          function: _<=_
+                                        id: "7"
+                                      function: _&&_
+                                    id: "9"
+                          input:
+                            name: http-attributes-cel-match-input
+                            typedConfig:
+                              '@type': type.googleapis.com/xds.type.matcher.v3.HttpAttributesCelMatchInput
+        - name: envoy.filters.http.router
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+            suppressEnvoyHeaders: true
+        mergeSlashes: true
+        normalizePath: true
+        pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT
+        rds:
+          configSource:
+            ads: {}
+            resourceApiVersion: V3
+          routeConfigName: default/gateway-1/http
+        serverHeaderTransformation: PASS_THROUGH
+        statPrefix: http-10080
+        useRemoteAddress: true
+    name: default/gateway-1/http
+  name: default/gateway-1/http
+  perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/custom-response.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/custom-response.routes.yaml
new file mode 100644
index 000000000..8262bb6f3
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/custom-response.routes.yaml
@@ -0,0 +1,33 @@
+- ignorePortInHostMatching: true
+  name: default/gateway-1/http
+  virtualHosts:
+  - domains:
+    - '*'
+    metadata:
+      filterMetadata:
+        envoy-gateway:
+          resources:
+          - kind: Gateway
+            name: gateway-1
+            namespace: default
+            sectionName: http
+    name: default/gateway-1/http/*
+    routes:
+    - match:
+        prefix: /
+      metadata:
+        filterMetadata:
+          envoy-gateway:
+            resources:
+            - kind: HTTPRoute
+              name: httproute-1
+              namespace: default
+      name: httproute/default/httproute-1/rule/0/match/-1/*
+      route:
+        cluster: httproute/default/httproute-1/rule/0
+        upgradeConfigs:
+        - upgradeType: websocket
+      typedPerFilterConfig:
+        envoy.filters.http.custom_response/backendtrafficpolicy/default/policy-for-gateway:
+          '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig
+          config: {}
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index fe361099a..4f562fce6 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -483,6 +483,7 @@ _Appears in:_
 | `rateLimit` | _[RateLimitSpec](#ratelimitspec)_ |  false  | RateLimit allows the user to limit the number of incoming requests<br />to a predefined value based on attributes within the traffic flow. |
 | `faultInjection` | _[FaultInjection](#faultinjection)_ |  false  | FaultInjection defines the fault injection policy to be applied. This configuration can be used to<br />inject delays and abort requests to mimic failure scenarios such as service failures and overloads |
 | `useClientProtocol` | _boolean_ |  false  | UseClientProtocol configures Envoy to prefer sending requests to backends using<br />the same HTTP protocol that the incoming request used. Defaults to false, which means<br />that Envoy will use the protocol indicated by the attached BackendRef. |
+| `responseOverride` | _[ResponseOverride](#responseoverride) array_ |  false  | ResponseOverride defines the configuration to override specific responses with a custom one.<br />If multiple configurations are specified, the first one to match wins. |
 
 
 #### BasicAuth
@@ -866,7 +867,7 @@ _Appears in:_
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
 | `contentType` | _string_ |  false  | Content Type of the response. This will be set in the Content-Type header. |
-| `body` | _[CustomResponseBody](#customresponsebody)_ |  false  | Body of the Custom Response |
+| `body` | _[CustomResponseBody](#customresponsebody)_ |  true  | Body of the Custom Response |
 
 
 #### CustomResponseBody
@@ -881,9 +882,9 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `type` | _[ResponseValueType](#responsevaluetype)_ |  true  | Type is the type of method to use to read the body value. |
+| `type` | _[ResponseValueType](#responsevaluetype)_ |  true  | Type is the type of method to use to read the body value.<br />Valid values are Inline and ValueRef, default is Inline. |
 | `inline` | _string_ |  false  | Inline contains the value as an inline string. |
-| `valueRef` | _[LocalObjectReference](#localobjectreference)_ |  false  | ValueRef contains the contents of the body<br />specified as a local object reference.<br />Only a reference to ConfigMap is supported. |
+| `valueRef` | _[LocalObjectReference](#localobjectreference)_ |  false  | ValueRef contains the contents of the body<br />specified as a local object reference.<br />Only a reference to ConfigMap is supported.<br /><br />The value of key `response.body` in the ConfigMap will be used as the response body.<br />If the key is not found, the first value in the ConfigMap will be used. |
 
 
 #### CustomResponseMatch
@@ -897,7 +898,7 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `statusCode` | _[StatusCodeMatch](#statuscodematch) array_ |  true  | Status code to match on. The match evaluates to true if any of the matches are successful. |
+| `statusCodes` | _[StatusCodeMatch](#statuscodematch) array_ |  true  | Status code to match on. The match evaluates to true if any of the matches are successful. |
 
 
 #### CustomTag
@@ -1026,6 +1027,7 @@ _Appears in:_
 | `envoy.filters.http.rbac` | EnvoyFilterRBAC defines the Envoy RBAC filter.<br /> | 
 | `envoy.filters.http.local_ratelimit` | EnvoyFilterLocalRateLimit defines the Envoy HTTP local rate limit filter.<br /> | 
 | `envoy.filters.http.ratelimit` | EnvoyFilterRateLimit defines the Envoy HTTP rate limit filter.<br /> | 
+| `envoy.filters.http.custom_response` | EnvoyFilterCustomResponse defines the Envoy HTTP custom response filter.<br /> | 
 | `envoy.filters.http.router` | EnvoyFilterRouter defines the Envoy HTTP router filter.<br /> | 
 
 
@@ -1479,7 +1481,7 @@ _Appears in:_
 | `extraArgs` | _string array_ |  false  | ExtraArgs defines additional command line options that are provided to Envoy.<br />More info: https://www.envoyproxy.io/docs/envoy/latest/operations/cli#command-line-options<br />Note: some command line options are used internally(e.g. --log-level) so they cannot be provided here. |
 | `mergeGateways` | _boolean_ |  false  | MergeGateways defines if Gateway resources should be merged onto the same Envoy Proxy Infrastructure.<br />Setting this field to true would merge all Gateway Listeners under the parent Gateway Class.<br />This means that the port, protocol and hostname tuple must be unique for every listener.<br />If a duplicate listener is detected, the newer listener (based on timestamp) will be rejected and its status will be updated with a "Accepted=False" condition. |
 | `shutdown` | _[ShutdownConfig](#shutdownconfig)_ |  false  | Shutdown defines configuration for graceful envoy shutdown process. |
-| `filterOrder` | _[FilterPosition](#filterposition) array_ |  false  | FilterOrder defines the order of filters in the Envoy proxy's HTTP filter chain.<br />The FilterPosition in the list will be applied in the order they are defined.<br />If unspecified, the default filter order is applied.<br />Default filter order is:<br /><br />- envoy.filters.http.health_check<br /><br />- envoy.filters.http.fault<br /><br />- envoy.filters.http.cors<br /><br />- envoy.filters.http.ext_authz<br /><br />- envoy.filters.http.basic_auth<br /><br />- envoy.filters.http.oauth2<br /><br />- envoy.filters.http.jwt_authn<br /><br />- envoy.filters.http.stateful_session<br /><br />- envoy.filters.http.ext_proc<br /><br />- envoy.filters.http.wasm<br /><br />- envoy.filters.http.rbac<br /><br />- envoy.filters.http.local_ratelimit<br /><br />- envoy.filters.http.ratelimit<br /><br />- envoy.filters.http.router<br /><br />Note: "envoy.filters.http.router" cannot be reordered, it's always the last filter in the chain. |
+| `filterOrder` | _[FilterPosition](#filterposition) array_ |  false  | FilterOrder defines the order of filters in the Envoy proxy's HTTP filter chain.<br />The FilterPosition in the list will be applied in the order they are defined.<br />If unspecified, the default filter order is applied.<br />Default filter order is:<br /><br />- envoy.filters.http.health_check<br /><br />- envoy.filters.http.fault<br /><br />- envoy.filters.http.cors<br /><br />- envoy.filters.http.ext_authz<br /><br />- envoy.filters.http.basic_auth<br /><br />- envoy.filters.http.oauth2<br /><br />- envoy.filters.http.jwt_authn<br /><br />- envoy.filters.http.stateful_session<br /><br />- envoy.filters.http.ext_proc<br /><br />- envoy.filters.http.wasm<br /><br />- envoy.filters.http.rbac<br /><br />- envoy.filters.http.local_ratelimit<br /><br />- envoy.filters.http.ratelimit<br /><br />- envoy.filters.http.custom_response<br /><br />- envoy.filters.http.router<br /><br />Note: "envoy.filters.http.router" cannot be reordered, it's always the last filter in the chain. |
 | `backendTLS` | _[BackendTLSConfig](#backendtlsconfig)_ |  false  | BackendTLS is the TLS configuration for the Envoy proxy to use when connecting to backends.<br />These settings are applied on backends for which TLS policies are specified. |
 
 
@@ -3573,6 +3575,10 @@ ResponseValueType defines the types of values for the response body supported by
 _Appears in:_
 - [CustomResponseBody](#customresponsebody)
 
+| Value | Description |
+| ----- | ----------- |
+| `Inline` | ResponseValueTypeInline defines the "Inline" response body type.<br /> | 
+| `ValueRef` | ResponseValueTypeValueRef defines the "ValueRef" response body type.<br /> | 
 
 
 
@@ -3821,16 +3827,16 @@ _Appears in:_
 
 
 
-
+StatusCodeMatch defines the configuration for matching a status code.
 
 _Appears in:_
 - [CustomResponseMatch](#customresponsematch)
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `type` | _[StatusCodeValueType](#statuscodevaluetype)_ |  true  | Type is the type of value. |
-| `value` | _string_ |  false  | Value contains the value of the status code. |
-| `range` | _[StatusCodeRange](#statuscoderange)_ |  false  | ValueRef contains the contents of the body<br />specified as a local object reference.<br />Only a reference to ConfigMap is supported. |
+| `type` | _[StatusCodeValueType](#statuscodevaluetype)_ |  true  | Type is the type of value.<br />Valid values are Value and Range, default is Value. |
+| `value` | _integer_ |  false  | Value contains the value of the status code. |
+| `range` | _[StatusCodeRange](#statuscoderange)_ |  false  | Range contains the range of status codes. |
 
 
 #### StatusCodeRange
@@ -3857,6 +3863,10 @@ StatusCodeValueType defines the types of values for the status code match suppor
 _Appears in:_
 - [StatusCodeMatch](#statuscodematch)
 
+| Value | Description |
+| ----- | ----------- |
+| `Value` | StatusCodeValueTypeValue defines the "Value" status code match type.<br /> | 
+| `Range` | StatusCodeValueTypeRange defines the "Range" status code match type.<br /> | 
 
 
 #### StringMatch
diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md
index fe361099a..4f562fce6 100644
--- a/site/content/zh/latest/api/extension_types.md
+++ b/site/content/zh/latest/api/extension_types.md
@@ -483,6 +483,7 @@ _Appears in:_
 | `rateLimit` | _[RateLimitSpec](#ratelimitspec)_ |  false  | RateLimit allows the user to limit the number of incoming requests<br />to a predefined value based on attributes within the traffic flow. |
 | `faultInjection` | _[FaultInjection](#faultinjection)_ |  false  | FaultInjection defines the fault injection policy to be applied. This configuration can be used to<br />inject delays and abort requests to mimic failure scenarios such as service failures and overloads |
 | `useClientProtocol` | _boolean_ |  false  | UseClientProtocol configures Envoy to prefer sending requests to backends using<br />the same HTTP protocol that the incoming request used. Defaults to false, which means<br />that Envoy will use the protocol indicated by the attached BackendRef. |
+| `responseOverride` | _[ResponseOverride](#responseoverride) array_ |  false  | ResponseOverride defines the configuration to override specific responses with a custom one.<br />If multiple configurations are specified, the first one to match wins. |
 
 
 #### BasicAuth
@@ -866,7 +867,7 @@ _Appears in:_
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
 | `contentType` | _string_ |  false  | Content Type of the response. This will be set in the Content-Type header. |
-| `body` | _[CustomResponseBody](#customresponsebody)_ |  false  | Body of the Custom Response |
+| `body` | _[CustomResponseBody](#customresponsebody)_ |  true  | Body of the Custom Response |
 
 
 #### CustomResponseBody
@@ -881,9 +882,9 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `type` | _[ResponseValueType](#responsevaluetype)_ |  true  | Type is the type of method to use to read the body value. |
+| `type` | _[ResponseValueType](#responsevaluetype)_ |  true  | Type is the type of method to use to read the body value.<br />Valid values are Inline and ValueRef, default is Inline. |
 | `inline` | _string_ |  false  | Inline contains the value as an inline string. |
-| `valueRef` | _[LocalObjectReference](#localobjectreference)_ |  false  | ValueRef contains the contents of the body<br />specified as a local object reference.<br />Only a reference to ConfigMap is supported. |
+| `valueRef` | _[LocalObjectReference](#localobjectreference)_ |  false  | ValueRef contains the contents of the body<br />specified as a local object reference.<br />Only a reference to ConfigMap is supported.<br /><br />The value of key `response.body` in the ConfigMap will be used as the response body.<br />If the key is not found, the first value in the ConfigMap will be used. |
 
 
 #### CustomResponseMatch
@@ -897,7 +898,7 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `statusCode` | _[StatusCodeMatch](#statuscodematch) array_ |  true  | Status code to match on. The match evaluates to true if any of the matches are successful. |
+| `statusCodes` | _[StatusCodeMatch](#statuscodematch) array_ |  true  | Status code to match on. The match evaluates to true if any of the matches are successful. |
 
 
 #### CustomTag
@@ -1026,6 +1027,7 @@ _Appears in:_
 | `envoy.filters.http.rbac` | EnvoyFilterRBAC defines the Envoy RBAC filter.<br /> | 
 | `envoy.filters.http.local_ratelimit` | EnvoyFilterLocalRateLimit defines the Envoy HTTP local rate limit filter.<br /> | 
 | `envoy.filters.http.ratelimit` | EnvoyFilterRateLimit defines the Envoy HTTP rate limit filter.<br /> | 
+| `envoy.filters.http.custom_response` | EnvoyFilterCustomResponse defines the Envoy HTTP custom response filter.<br /> | 
 | `envoy.filters.http.router` | EnvoyFilterRouter defines the Envoy HTTP router filter.<br /> | 
 
 
@@ -1479,7 +1481,7 @@ _Appears in:_
 | `extraArgs` | _string array_ |  false  | ExtraArgs defines additional command line options that are provided to Envoy.<br />More info: https://www.envoyproxy.io/docs/envoy/latest/operations/cli#command-line-options<br />Note: some command line options are used internally(e.g. --log-level) so they cannot be provided here. |
 | `mergeGateways` | _boolean_ |  false  | MergeGateways defines if Gateway resources should be merged onto the same Envoy Proxy Infrastructure.<br />Setting this field to true would merge all Gateway Listeners under the parent Gateway Class.<br />This means that the port, protocol and hostname tuple must be unique for every listener.<br />If a duplicate listener is detected, the newer listener (based on timestamp) will be rejected and its status will be updated with a "Accepted=False" condition. |
 | `shutdown` | _[ShutdownConfig](#shutdownconfig)_ |  false  | Shutdown defines configuration for graceful envoy shutdown process. |
-| `filterOrder` | _[FilterPosition](#filterposition) array_ |  false  | FilterOrder defines the order of filters in the Envoy proxy's HTTP filter chain.<br />The FilterPosition in the list will be applied in the order they are defined.<br />If unspecified, the default filter order is applied.<br />Default filter order is:<br /><br />- envoy.filters.http.health_check<br /><br />- envoy.filters.http.fault<br /><br />- envoy.filters.http.cors<br /><br />- envoy.filters.http.ext_authz<br /><br />- envoy.filters.http.basic_auth<br /><br />- envoy.filters.http.oauth2<br /><br />- envoy.filters.http.jwt_authn<br /><br />- envoy.filters.http.stateful_session<br /><br />- envoy.filters.http.ext_proc<br /><br />- envoy.filters.http.wasm<br /><br />- envoy.filters.http.rbac<br /><br />- envoy.filters.http.local_ratelimit<br /><br />- envoy.filters.http.ratelimit<br /><br />- envoy.filters.http.router<br /><br />Note: "envoy.filters.http.router" cannot be reordered, it's always the last filter in the chain. |
+| `filterOrder` | _[FilterPosition](#filterposition) array_ |  false  | FilterOrder defines the order of filters in the Envoy proxy's HTTP filter chain.<br />The FilterPosition in the list will be applied in the order they are defined.<br />If unspecified, the default filter order is applied.<br />Default filter order is:<br /><br />- envoy.filters.http.health_check<br /><br />- envoy.filters.http.fault<br /><br />- envoy.filters.http.cors<br /><br />- envoy.filters.http.ext_authz<br /><br />- envoy.filters.http.basic_auth<br /><br />- envoy.filters.http.oauth2<br /><br />- envoy.filters.http.jwt_authn<br /><br />- envoy.filters.http.stateful_session<br /><br />- envoy.filters.http.ext_proc<br /><br />- envoy.filters.http.wasm<br /><br />- envoy.filters.http.rbac<br /><br />- envoy.filters.http.local_ratelimit<br /><br />- envoy.filters.http.ratelimit<br /><br />- envoy.filters.http.custom_response<br /><br />- envoy.filters.http.router<br /><br />Note: "envoy.filters.http.router" cannot be reordered, it's always the last filter in the chain. |
 | `backendTLS` | _[BackendTLSConfig](#backendtlsconfig)_ |  false  | BackendTLS is the TLS configuration for the Envoy proxy to use when connecting to backends.<br />These settings are applied on backends for which TLS policies are specified. |
 
 
@@ -3573,6 +3575,10 @@ ResponseValueType defines the types of values for the response body supported by
 _Appears in:_
 - [CustomResponseBody](#customresponsebody)
 
+| Value | Description |
+| ----- | ----------- |
+| `Inline` | ResponseValueTypeInline defines the "Inline" response body type.<br /> | 
+| `ValueRef` | ResponseValueTypeValueRef defines the "ValueRef" response body type.<br /> | 
 
 
 
@@ -3821,16 +3827,16 @@ _Appears in:_
 
 
 
-
+StatusCodeMatch defines the configuration for matching a status code.
 
 _Appears in:_
 - [CustomResponseMatch](#customresponsematch)
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `type` | _[StatusCodeValueType](#statuscodevaluetype)_ |  true  | Type is the type of value. |
-| `value` | _string_ |  false  | Value contains the value of the status code. |
-| `range` | _[StatusCodeRange](#statuscoderange)_ |  false  | ValueRef contains the contents of the body<br />specified as a local object reference.<br />Only a reference to ConfigMap is supported. |
+| `type` | _[StatusCodeValueType](#statuscodevaluetype)_ |  true  | Type is the type of value.<br />Valid values are Value and Range, default is Value. |
+| `value` | _integer_ |  false  | Value contains the value of the status code. |
+| `range` | _[StatusCodeRange](#statuscoderange)_ |  false  | Range contains the range of status codes. |
 
 
 #### StatusCodeRange
@@ -3857,6 +3863,10 @@ StatusCodeValueType defines the types of values for the status code match suppor
 _Appears in:_
 - [StatusCodeMatch](#statuscodematch)
 
+| Value | Description |
+| ----- | ----------- |
+| `Value` | StatusCodeValueTypeValue defines the "Value" status code match type.<br /> | 
+| `Range` | StatusCodeValueTypeRange defines the "Range" status code match type.<br /> | 
 
 
 #### StringMatch
diff --git a/test/cel-validation/backendtrafficpolicy_test.go b/test/cel-validation/backendtrafficpolicy_test.go
index 49f033eb6..d5e6a1b2d 100644
--- a/test/cel-validation/backendtrafficpolicy_test.go
+++ b/test/cel-validation/backendtrafficpolicy_test.go
@@ -1257,6 +1257,251 @@ func TestBackendTrafficPolicyTarget(t *testing.T) {
 			},
 			wantErrors: []string{},
 		},
+		{
+			desc: "both targetref and targetrefs specified",
+			mutate: func(btp *egv1a1.BackendTrafficPolicy) {
+				btp.Spec = egv1a1.BackendTrafficPolicySpec{
+					PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+						TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+							LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+								Group: gwapiv1a2.Group("gateway.networking.k8s.io"),
+								Kind:  gwapiv1a2.Kind("Gateway"),
+								Name:  gwapiv1a2.ObjectName("eg"),
+							},
+						},
+					},
+					ResponseOverride: []*egv1a1.ResponseOverride{
+						{
+							Match: egv1a1.CustomResponseMatch{
+								StatusCodes: []egv1a1.StatusCodeMatch{
+									{
+										Type: ptr.To(egv1a1.StatusCodeValueTypeValue),
+										Range: &egv1a1.StatusCodeRange{
+											Start: 100,
+											End:   200,
+										},
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{
+				"value must be set for type Value",
+			},
+		},
+		{
+			desc: "both targetref and targetrefs specified",
+			mutate: func(btp *egv1a1.BackendTrafficPolicy) {
+				btp.Spec = egv1a1.BackendTrafficPolicySpec{
+					PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+						TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+							LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+								Group: gwapiv1a2.Group("gateway.networking.k8s.io"),
+								Kind:  gwapiv1a2.Kind("Gateway"),
+								Name:  gwapiv1a2.ObjectName("eg"),
+							},
+						},
+					},
+					ResponseOverride: []*egv1a1.ResponseOverride{
+						{
+							Match: egv1a1.CustomResponseMatch{
+								StatusCodes: []egv1a1.StatusCodeMatch{
+									{
+										Range: &egv1a1.StatusCodeRange{
+											Start: 100,
+											End:   200,
+										},
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{
+				"value must be set for type Value",
+			},
+		},
+		{
+			desc: "both targetref and targetrefs specified",
+			mutate: func(btp *egv1a1.BackendTrafficPolicy) {
+				btp.Spec = egv1a1.BackendTrafficPolicySpec{
+					PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+						TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+							LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+								Group: gwapiv1a2.Group("gateway.networking.k8s.io"),
+								Kind:  gwapiv1a2.Kind("Gateway"),
+								Name:  gwapiv1a2.ObjectName("eg"),
+							},
+						},
+					},
+					ResponseOverride: []*egv1a1.ResponseOverride{
+						{
+							Match: egv1a1.CustomResponseMatch{
+								StatusCodes: []egv1a1.StatusCodeMatch{
+									{
+										Type:  ptr.To(egv1a1.StatusCodeValueTypeRange),
+										Value: ptr.To(100),
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{
+				"range must be set for type Range",
+			},
+		},
+		{
+			desc: "both targetref and targetrefs specified",
+			mutate: func(btp *egv1a1.BackendTrafficPolicy) {
+				btp.Spec = egv1a1.BackendTrafficPolicySpec{
+					PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+						TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+							LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+								Group: gwapiv1a2.Group("gateway.networking.k8s.io"),
+								Kind:  gwapiv1a2.Kind("Gateway"),
+								Name:  gwapiv1a2.ObjectName("eg"),
+							},
+						},
+					},
+					ResponseOverride: []*egv1a1.ResponseOverride{
+						{
+							Match: egv1a1.CustomResponseMatch{
+								StatusCodes: []egv1a1.StatusCodeMatch{
+									{
+										Type: ptr.To(egv1a1.StatusCodeValueTypeRange),
+										Range: &egv1a1.StatusCodeRange{
+											Start: 200,
+											End:   100,
+										},
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{
+				"end must be greater than start",
+			},
+		},
+		{
+			desc: "both targetref and targetrefs specified",
+			mutate: func(btp *egv1a1.BackendTrafficPolicy) {
+				btp.Spec = egv1a1.BackendTrafficPolicySpec{
+					PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+						TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+							LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+								Group: gwapiv1a2.Group("gateway.networking.k8s.io"),
+								Kind:  gwapiv1a2.Kind("Gateway"),
+								Name:  gwapiv1a2.ObjectName("eg"),
+							},
+						},
+					},
+					ResponseOverride: []*egv1a1.ResponseOverride{
+						{
+							Match: egv1a1.CustomResponseMatch{
+								StatusCodes: []egv1a1.StatusCodeMatch{
+									{
+										Value: ptr.To(100),
+									},
+								},
+							},
+							Response: egv1a1.CustomResponse{
+								Body: egv1a1.CustomResponseBody{
+									ValueRef: &gwapiv1a2.LocalObjectReference{
+										Kind: gwapiv1a2.Kind("ConfigMap"),
+										Name: gwapiv1a2.ObjectName("eg"),
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{
+				"inline must be set for type Inline",
+			},
+		},
+		{
+			desc: "both targetref and targetrefs specified",
+			mutate: func(btp *egv1a1.BackendTrafficPolicy) {
+				btp.Spec = egv1a1.BackendTrafficPolicySpec{
+					PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+						TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+							LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+								Group: gwapiv1a2.Group("gateway.networking.k8s.io"),
+								Kind:  gwapiv1a2.Kind("Gateway"),
+								Name:  gwapiv1a2.ObjectName("eg"),
+							},
+						},
+					},
+					ResponseOverride: []*egv1a1.ResponseOverride{
+						{
+							Match: egv1a1.CustomResponseMatch{
+								StatusCodes: []egv1a1.StatusCodeMatch{
+									{
+										Value: ptr.To(100),
+									},
+								},
+							},
+							Response: egv1a1.CustomResponse{
+								Body: egv1a1.CustomResponseBody{
+									Type:   ptr.To(egv1a1.ResponseValueTypeValueRef),
+									Inline: ptr.To("foo"),
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{
+				"valueRef must be set for type ValueRef",
+			},
+		},
+		{
+			desc: "both targetref and targetrefs specified",
+			mutate: func(btp *egv1a1.BackendTrafficPolicy) {
+				btp.Spec = egv1a1.BackendTrafficPolicySpec{
+					PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+						TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+							LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+								Group: gwapiv1a2.Group("gateway.networking.k8s.io"),
+								Kind:  gwapiv1a2.Kind("Gateway"),
+								Name:  gwapiv1a2.ObjectName("eg"),
+							},
+						},
+					},
+					ResponseOverride: []*egv1a1.ResponseOverride{
+						{
+							Match: egv1a1.CustomResponseMatch{
+								StatusCodes: []egv1a1.StatusCodeMatch{
+									{
+										Value: ptr.To(100),
+									},
+								},
+							},
+							Response: egv1a1.CustomResponse{
+								Body: egv1a1.CustomResponseBody{
+									Type: ptr.To(egv1a1.ResponseValueTypeValueRef),
+									ValueRef: &gwapiv1a2.LocalObjectReference{
+										Kind: gwapiv1a2.Kind("Foo"),
+										Name: gwapiv1a2.ObjectName("eg"),
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{
+				"only ConfigMap is supported for ValueRe",
+			},
+		},
 	}
 
 	for _, tc := range cases {
diff --git a/test/e2e/testdata/response-override.yaml b/test/e2e/testdata/response-override.yaml
new file mode 100644
index 000000000..084747aaa
--- /dev/null
+++ b/test/e2e/testdata/response-override.yaml
@@ -0,0 +1,62 @@
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: response-override
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+    - name: same-namespace
+  rules:
+    - matches:
+        - path:
+            type: PathPrefix
+            value: /
+      backendRefs:
+        - name: infra-backend-v1
+          port: 8080
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: response-override-config
+  namespace: gateway-conformance-infra
+data:
+  response.body: '{"error": "Internal Server Error"}'
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: response-override
+  namespace: gateway-conformance-infra
+spec:
+  targetRef:
+    group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: response-override
+  responseOverride:
+    - match:
+        statusCodes:
+          - type: Value
+            value: 404
+      response:
+        contentType: text/plain
+        body:
+          type: Inline
+          inline: "Oops! Your request is not found."
+    - match:
+        statusCodes:
+          - type: Value
+            value: 500
+          - type: Range
+            range:
+              start: 501
+              end: 511
+      response:
+        contentType: application/json
+        body:
+          type: ValueRef
+          valueRef:
+            group: ""
+            kind: ConfigMap
+            name: response-override-config
diff --git a/test/e2e/tests/response-override.go b/test/e2e/tests/response-override.go
new file mode 100644
index 000000000..b21db88e2
--- /dev/null
+++ b/test/e2e/tests/response-override.go
@@ -0,0 +1,83 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+//go:build e2e
+
+package tests
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"testing"
+
+	"k8s.io/apimachinery/pkg/types"
+	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+	gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+	httputils "sigs.k8s.io/gateway-api/conformance/utils/http"
+	"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+
+	"github.com/envoyproxy/gateway/internal/gatewayapi"
+	"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
+)
+
+func init() {
+	ConformanceTests = append(ConformanceTests, ResponseOverrideTest)
+}
+
+var ResponseOverrideTest = suite.ConformanceTest{
+	ShortName:   "ResponseOverrideSpecificUser",
+	Description: "Response Override",
+	Manifests:   []string{"testdata/response-override.yaml"},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		t.Run("response override", func(t *testing.T) {
+			ns := "gateway-conformance-infra"
+			routeNN := types.NamespacedName{Name: "response-override", Namespace: ns}
+			gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
+			gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
+
+			ancestorRef := gwapiv1a2.ParentReference{
+				Group:     gatewayapi.GroupPtr(gwapiv1.GroupName),
+				Kind:      gatewayapi.KindPtr(resource.KindGateway),
+				Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
+				Name:      gwapiv1.ObjectName(gwNN.Name),
+			}
+			BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "response-override", Namespace: ns}, suite.ControllerName, ancestorRef)
+			verifyResponseOverride(t, gwAddr, 404, "text/plain", "Oops! Your request is not found.")
+			verifyResponseOverride(t, gwAddr, 500, "application/json", `{"error": "Internal Server Error"}`)
+		})
+	},
+}
+
+func verifyResponseOverride(t *testing.T, gwAddr string, statusCode int, expectedContentType string, expectedBody string) {
+	reqURL := url.URL{
+		Scheme: "http",
+		Host:   httputils.CalculateHost(t, gwAddr, "http"),
+		Path:   fmt.Sprintf("/status/%d", statusCode),
+	}
+
+	rsp, err := http.Get(reqURL.String())
+	if err != nil {
+		t.Fatalf("failed to get response: %v", err)
+	}
+
+	// Verify that the response body is overridden
+	defer rsp.Body.Close()
+	body, err := io.ReadAll(rsp.Body)
+	if err != nil {
+		t.Fatalf("failed to read response body: %v", err)
+	}
+	if string(body) != expectedBody {
+		t.Errorf("expected response body to be %s but got %s", expectedBody, string(body))
+	}
+
+	// Verify that the content type is overridden
+	contentType := rsp.Header.Get("Content-Type")
+	if contentType != expectedContentType {
+		t.Errorf("expected content type to be %s but got %s", expectedContentType, contentType)
+	}
+}