Repository: envoyproxy/gateway (go)

Issue:
Issue with validation with incorrect header values
While migrating from Gloo to Envoy Gateway I discovered that a single malformed header can stall the controller for every other host on the same Gateway.
If a ResponseHeaderModifier filter sets a header using the YAML pipe (|), the resulting multi-line value slips through validation, but afterwards no further config changes are reconciled. New routes/hosts return 404 because the Gateway is stuck on the bad object.

Envoy Gateway: 1.3.2
Kubernetes: v1.31.7

*Steps to reproduce:*
```
rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
    filters:
    - type: ResponseHeaderModifier
      responseHeaderModifier:
        set:
        - name: Content-Security-Policy
          value: |
            default-src 'none'; connect-src https://*.example.com
```

1. Confirm the route shows Accepted: true.
2. Add another HTTPRoute (or edit the same one) to introduce a new host.
3. Curl the new host → 404.

Pull request description:
fix: add validation for header values
**What type of PR is this?**
<!--
Your PR title should be descriptive, and generally start with type that contains a subsystem name with `()` if necessary
and summary followed by a colon. format `chore/docs/api/feat/fix/refactor/style/test: summary`.
Examples:
* "docs: fix grammar error"
* "feat(translator): add new feature"
* "fix: fix xx bug"
* "chore: change ci & build tools etc"
* "api: add xxx fields in ClientTrafficPolicy"

Before raising a PR, please go through this section of the developer guide, https://gateway.envoyproxy.io/contributions/develop/#raising-a-pr
-->

<!--
NOTE: If your PR contains any API changes (changes under `/api`), we recommend you to separate these API changes into
a new PR, and we will review the API part first. It will save you lots of implementation time if the API get accepted.
-->

**What this PR does / why we need it**:

A single malformed header can stall the entire controller. We need to validate header values according to RFC 7230.

The proposed changes catch invalid header values and flag them in status messages.

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes #5901

<!--
For any non-trivial changes, you need to provide a brief description of the changes in the release notes.
Please add the description to the release-notes/current.yaml file and include this file in the PR.
-->
Release Notes: Yes


Patch:
diff --git a/internal/gatewayapi/clienttrafficpolicy.go b/internal/gatewayapi/clienttrafficpolicy.go
index f2585a7dd..843109838 100644
--- a/internal/gatewayapi/clienttrafficpolicy.go
+++ b/internal/gatewayapi/clienttrafficpolicy.go
@@ -952,7 +952,12 @@ func translateEarlyRequestHeaders(headerModifier *gwapiv1.HTTPHeaderFilter) ([]i
 			}
 			// Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names
 			if strings.ContainsAny(string(addHeader.Name), "/:") {
-				errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders Filter cannot set headers with a '/' or ':' character in them. Header: %q", string(addHeader.Name)))
+				errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders cannot add a header with a '/' or ':' character in them. Header: '%q'", string(addHeader.Name)))
+				continue
+			}
+			// Gateway API specification allows only valid value as defined by RFC 7230
+			if !HeaderValueRegexp.MatchString(addHeader.Value) {
+				errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders cannot add a header with an invalid value. Header: '%q'", string(addHeader.Name)))
 				continue
 			}
 			// Check if the header is a duplicate
@@ -992,7 +997,12 @@ func translateEarlyRequestHeaders(headerModifier *gwapiv1.HTTPHeaderFilter) ([]i
 			}
 			// Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names
 			if strings.ContainsAny(string(setHeader.Name), "/:") {
-				errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders cannot set headers with a '/' or ':' character in them. Header: '%s'", string(setHeader.Name)))
+				errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders cannot set a header with a '/' or ':' character in them. Header: '%q'", string(setHeader.Name)))
+				continue
+			}
+			// Gateway API specification allows only valid value as defined by RFC 7230
+			if !HeaderValueRegexp.MatchString(setHeader.Value) {
+				errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders cannot set a header with an invalid value. Header: '%q'", string(setHeader.Name)))
 				continue
 			}
 
diff --git a/internal/gatewayapi/filters.go b/internal/gatewayapi/filters.go
index 0ca0dcd6f..27c4a93ea 100644
--- a/internal/gatewayapi/filters.go
+++ b/internal/gatewayapi/filters.go
@@ -66,6 +66,9 @@ type HTTPFilterIR struct {
 	ExtensionRefs []*ir.UnstructuredRef
 }
 
+// Header value pattern according to RFC 7230
+var HeaderValueRegexp = regexp.MustCompile(`^[!-~]+([\t ]?[!-~]+)*$`)
+
 // ProcessHTTPFilters translates gateway api http filters to IRs.
 func (t *Translator) ProcessHTTPFilters(parentRef *RouteParentContext,
 	route RouteContext,
@@ -376,6 +379,7 @@ func (t *Translator) processRequestHeaderModifierFilter(
 			emptyFilterConfig = false
 		}
 		for _, addHeader := range headersToAdd {
+
 			emptyFilterConfig = false
 			if addHeader.Name == "" {
 				updateRouteStatusForFilter(
@@ -384,16 +388,27 @@ func (t *Translator) processRequestHeaderModifierFilter(
 				// try to process the rest of the headers and produce a valid config.
 				continue
 			}
+
 			if !isModifiableHeader(string(addHeader.Name)) {
 				updateRouteStatusForFilter(
 					filterContext,
 					fmt.Sprintf(
-						"Header: %q. The RequestHeaderModifier filter cannot set the Host header or headers with a '/' "+
+						"Header: %q. The RequestHeaderModifier filter cannot add the Host header or headers with a '/' "+
 							"or ':' character in them. To modify the Host header use the URLRewrite or the HTTPRouteFilter filter.",
 						string(addHeader.Name)),
 				)
 				continue
 			}
+
+			if !HeaderValueRegexp.MatchString(addHeader.Value) {
+				updateRouteStatusForFilter(
+					filterContext,
+					fmt.Sprintf(
+						"Header: %q. RequestHeaderModifier Filter cannot add a header with an invalid value.",
+						string(addHeader.Name)))
+				continue
+			}
+
 			// Check if the header is a duplicate
 			headerKey := string(addHeader.Name)
 			canAddHeader := true
@@ -443,6 +458,15 @@ func (t *Translator) processRequestHeaderModifierFilter(
 				continue
 			}
 
+			if !HeaderValueRegexp.MatchString(setHeader.Value) {
+				updateRouteStatusForFilter(
+					filterContext,
+					fmt.Sprintf(
+						"Header: %q. RequestHeaderModifier Filter cannot set a header with an invalid value.",
+						string(setHeader.Name)))
+				continue
+			}
+
 			// Check if the header to be set has already been configured
 			headerKey := string(setHeader.Name)
 			canAddHeader := true
@@ -556,6 +580,7 @@ func (t *Translator) processResponseHeaderModifierFilter(
 				// try to process the rest of the headers and produce a valid config.
 				continue
 			}
+
 			if !isModifiableHeader(string(addHeader.Name)) {
 				updateRouteStatusForFilter(
 					filterContext,
@@ -565,6 +590,16 @@ func (t *Translator) processResponseHeaderModifierFilter(
 						string(addHeader.Name)))
 				continue
 			}
+
+			if !HeaderValueRegexp.MatchString(addHeader.Value) {
+				updateRouteStatusForFilter(
+					filterContext,
+					fmt.Sprintf(
+						"Header: %q. ResponseHeaderModifier Filter cannot add a header with an invalid value.",
+						string(addHeader.Name)))
+				continue
+			}
+
 			// Check if the header is a duplicate
 			headerKey := string(addHeader.Name)
 			canAddHeader := true
@@ -613,6 +648,15 @@ func (t *Translator) processResponseHeaderModifierFilter(
 				continue
 			}
 
+			if !HeaderValueRegexp.MatchString(setHeader.Value) {
+				updateRouteStatusForFilter(
+					filterContext,
+					fmt.Sprintf(
+						"Header: %q. ResponseHeaderModifier Filter cannot set a header with an invalid value.",
+						string(setHeader.Name)))
+				continue
+			}
+
 			// Check if the header to be set has already been configured
 			headerKey := string(setHeader.Name)
 			canAddHeader := true
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index e4a3d75cd..ea823a6d3 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -11,6 +11,7 @@ new features: |
 
 bug fixes: |
   Fix reference grant from SecurityPolicy to referenced remoteJWKS backend not respected.
+  Added validation for header values.
 
 # Enhancements that improve performance.
 performance improvements: |


Test patch:
diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.in.yaml
index 3b2331bba..5fb1efb47 100644
--- a/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.in.yaml
+++ b/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.in.yaml
@@ -13,13 +13,18 @@ clientTrafficPolicies:
         add:
         - name: ""
           value: "empty"
-        - name: "invalid"
+        - name: "Example/Header/1"
           value: ":/"
+        - name: "example-header-2"
+          value: |
+            multi-line-header-value
         set:
         - name: ""
           value: "empty"
-        - name: "invalid"
+        - name: "Example:Header:3"
           value: ":/"
+        - name: "example-header-4"
+          value: "  invalid"
         remove:
         - ""
     targetRef:
diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.out.yaml
index abab24926..4a12476ea 100644
--- a/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.out.yaml
+++ b/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.out.yaml
@@ -11,15 +11,20 @@ clientTrafficPolicies:
         add:
         - name: ""
           value: empty
-        - name: invalid
+        - name: Example/Header/1
           value: :/
+        - name: example-header-2
+          value: |
+            multi-line-header-value
         remove:
         - ""
         set:
         - name: ""
           value: empty
-        - name: invalid
+        - name: Example:Header:3
           value: :/
+        - name: example-header-4
+          value: '  invalid'
       enableEnvoyHeaders: true
       preserveXRequestID: true
       withUnderscoresAction: Allow
@@ -38,8 +43,13 @@ clientTrafficPolicies:
       - lastTransitionTime: null
         message: |-
           Headers: EarlyRequestHeaders cannot add a header with an empty name
+          EarlyRequestHeaders cannot add a header with a '/' or ':' character in them. Header: '"Example/Header/1"'
+          EarlyRequestHeaders cannot add a header with an invalid value. Header: '"example-header-2"'
           EarlyRequestHeaders cannot set a header with an empty name
-          EarlyRequestHeaders cannot remove a header with an empty name.
+          EarlyRequestHeaders cannot set a header with a '/' or ':' character in them. Header: '"Example:Header:3"'
+          EarlyRequestHeaders cannot set a header with an invalid value. Header: '"example-header-4"'
+          EarlyRequestHeaders cannot remove a header with an empty name
+          EarlyRequestHeaders did not provide valid configuration to add/set/remove any headers.
         reason: Invalid
         status: "False"
         type: Accepted
diff --git a/internal/gatewayapi/testdata/httproute-with-header-filter-empty-header-values.in.yaml b/internal/gatewayapi/testdata/httproute-with-header-filter-empty-header-values.in.yaml
index c965d3296..0037ee554 100644
--- a/internal/gatewayapi/testdata/httproute-with-header-filter-empty-header-values.in.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-header-filter-empty-header-values.in.yaml
@@ -39,7 +39,7 @@ httpRoutes:
         requestHeaderModifier:
           set:
           - name: "example-header-1"
-            value: ""
+            value: "dummy"
           add:
           - name: "example-header-2"
             value: ""
diff --git a/internal/gatewayapi/testdata/httproute-with-header-filter-empty-header-values.out.yaml b/internal/gatewayapi/testdata/httproute-with-header-filter-empty-header-values.out.yaml
index 98c6a40af..ba290a7ff 100644
--- a/internal/gatewayapi/testdata/httproute-with-header-filter-empty-header-values.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-header-filter-empty-header-values.out.yaml
@@ -65,7 +65,7 @@ httpRoutes:
             value: ""
           set:
           - name: example-header-1
-            value: ""
+            value: dummy
         type: RequestHeaderModifier
       matches:
       - path:
@@ -74,9 +74,10 @@ httpRoutes:
     parents:
     - conditions:
       - lastTransitionTime: null
-        message: Route is accepted
-        reason: Accepted
-        status: "True"
+        message: 'Header: "example-header-2". RequestHeaderModifier Filter cannot
+          add a header with an invalid value.'
+        reason: UnsupportedValue
+        status: "False"
         type: Accepted
       - lastTransitionTime: null
         message: Resolved all the Object references for the Route
@@ -125,37 +126,6 @@ xdsIR:
         escapedSlashesAction: UnescapeAndRedirect
         mergeSlashes: true
       port: 10080
-      routes:
-      - addRequestHeaders:
-        - append: true
-          name: example-header-2
-          value:
-          - ""
-        - append: false
-          name: example-header-1
-          value:
-          - ""
-        destination:
-          name: httproute/default/httproute-1/rule/0
-          settings:
-          - addressType: IP
-            endpoints:
-            - host: 7.7.7.7
-              port: 8080
-            name: httproute/default/httproute-1/rule/0/backend/0
-            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: /
     readyListener:
       address: 0.0.0.0
       ipFamily: IPv4
diff --git a/internal/gatewayapi/testdata/httproute-with-header-filter-invalid-header-values.in.yaml b/internal/gatewayapi/testdata/httproute-with-header-filter-invalid-header-values.in.yaml
new file mode 100644
index 000000000..75d1c1669
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-header-filter-invalid-header-values.in.yaml
@@ -0,0 +1,47 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      hostname: "*.envoyproxy.io"
+      allowedRoutes:
+        namespaces:
+          from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-1
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          value: "/"
+      backendRefs:
+      - name: service-1
+        port: 8080
+      filters:
+      - type: RequestHeaderModifier
+        requestHeaderModifier:
+          set:
+          - name: "example-header-1"
+            value: |
+              multi-line-header-value
+          add:
+          - name: "example-header-2"
+            value: "dummy"
+
diff --git a/internal/gatewayapi/testdata/httproute-with-header-filter-invalid-header-values.out.yaml b/internal/gatewayapi/testdata/httproute-with-header-filter-invalid-header-values.out.yaml
new file mode 100644
index 000000000..fe02e7a8c
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-header-filter-invalid-header-values.out.yaml
@@ -0,0 +1,134 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      hostname: '*.envoyproxy.io'
+      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
+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-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      filters:
+      - requestHeaderModifier:
+          add:
+          - name: example-header-2
+            value: dummy
+          set:
+          - name: example-header-1
+            value: |
+              multi-line-header-value
+        type: RequestHeaderModifier
+      matches:
+      - path:
+          value: /
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: 'Header: "example-header-1". RequestHeaderModifier Filter cannot
+          set a header with an invalid value.'
+        reason: UnsupportedValue
+        status: "False"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+      name: envoy-gateway/gateway-1
+      namespace: envoy-gateway-system
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*.envoyproxy.io'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+      name: envoy-gateway/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003
diff --git a/internal/gatewayapi/testdata/httproute-with-response-header-filter-empty-header-values.in.yaml b/internal/gatewayapi/testdata/httproute-with-response-header-filter-empty-header-values.in.yaml
index 1c0016800..b84f703b6 100644
--- a/internal/gatewayapi/testdata/httproute-with-response-header-filter-empty-header-values.in.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-response-header-filter-empty-header-values.in.yaml
@@ -39,8 +39,7 @@ httpRoutes:
         responseHeaderModifier:
           set:
           - name: "example-header-1"
-            value: ""
+            value: "dummy"
           add:
           - name: "example-header-2"
             value: ""
-
diff --git a/internal/gatewayapi/testdata/httproute-with-response-header-filter-empty-header-values.out.yaml b/internal/gatewayapi/testdata/httproute-with-response-header-filter-empty-header-values.out.yaml
index f89eab6f3..94dae9d73 100644
--- a/internal/gatewayapi/testdata/httproute-with-response-header-filter-empty-header-values.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-response-header-filter-empty-header-values.out.yaml
@@ -65,7 +65,7 @@ httpRoutes:
             value: ""
           set:
           - name: example-header-1
-            value: ""
+            value: dummy
         type: ResponseHeaderModifier
       matches:
       - path:
@@ -74,9 +74,10 @@ httpRoutes:
     parents:
     - conditions:
       - lastTransitionTime: null
-        message: Route is accepted
-        reason: Accepted
-        status: "True"
+        message: 'Header: "example-header-2". ResponseHeaderModifier Filter cannot
+          add a header with an invalid value.'
+        reason: UnsupportedValue
+        status: "False"
         type: Accepted
       - lastTransitionTime: null
         message: Resolved all the Object references for the Route
@@ -125,37 +126,6 @@ xdsIR:
         escapedSlashesAction: UnescapeAndRedirect
         mergeSlashes: true
       port: 10080
-      routes:
-      - addResponseHeaders:
-        - append: true
-          name: example-header-2
-          value:
-          - ""
-        - append: false
-          name: example-header-1
-          value:
-          - ""
-        destination:
-          name: httproute/default/httproute-1/rule/0
-          settings:
-          - addressType: IP
-            endpoints:
-            - host: 7.7.7.7
-              port: 8080
-            name: httproute/default/httproute-1/rule/0/backend/0
-            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: /
     readyListener:
       address: 0.0.0.0
       ipFamily: IPv4
diff --git a/internal/gatewayapi/testdata/httproute-with-response-header-filter-invalid-header-values.in.yaml b/internal/gatewayapi/testdata/httproute-with-response-header-filter-invalid-header-values.in.yaml
new file mode 100644
index 000000000..802391150
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-response-header-filter-invalid-header-values.in.yaml
@@ -0,0 +1,46 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      hostname: "*.envoyproxy.io"
+      allowedRoutes:
+        namespaces:
+          from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-1
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          value: "/"
+      backendRefs:
+      - name: service-1
+        port: 8080
+      filters:
+      - type: ResponseHeaderModifier
+        responseHeaderModifier:
+          set:
+          - name: "example-header-1"
+            value: |
+              multi-line-header-value
+          add:
+          - name: "example-header-2"
+            value: "dummy"
diff --git a/internal/gatewayapi/testdata/httproute-with-response-header-filter-invalid-header-values.out.yaml b/internal/gatewayapi/testdata/httproute-with-response-header-filter-invalid-header-values.out.yaml
new file mode 100644
index 000000000..2ceb5619c
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-response-header-filter-invalid-header-values.out.yaml
@@ -0,0 +1,134 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      hostname: '*.envoyproxy.io'
+      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
+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-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      filters:
+      - responseHeaderModifier:
+          add:
+          - name: example-header-2
+            value: dummy
+          set:
+          - name: example-header-1
+            value: |
+              multi-line-header-value
+        type: ResponseHeaderModifier
+      matches:
+      - path:
+          value: /
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: 'Header: "example-header-1". ResponseHeaderModifier Filter cannot
+          set a header with an invalid value.'
+        reason: UnsupportedValue
+        status: "False"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+      name: envoy-gateway/gateway-1
+      namespace: envoy-gateway-system
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*.envoyproxy.io'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+      name: envoy-gateway/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003