Repository: envoyproxy/gateway
Pull request #5001 (2025-01-03 09:04:31)

feat: implement response compression
This PR introduces support for response compression in the `BackendTrafficPolicy`.

Currently, brotli and gzip are supported.

Implements #2451

Release Notes: Yes


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/5001?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 `28.57143%` with `80 lines` in your changes missing coverage. Please review.
> Project coverage is 66.63%. Comparing base [(`f71fa99`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/f71fa9997b0c8c1181b18eb163578db41ed1a318?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`9c0ddb8`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/9c0ddb8af757626a3a22dc06eab4cdd57c5b500a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/envoyproxy/gateway/pull/5001?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/xds/translator/compressor.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/5001?src=pr&el=tree&filepath=internal%2Fxds%2Ftranslator%2Fcompressor.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwveGRzL3RyYW5zbGF0b3IvY29tcHJlc3Nvci5nbw==) | 19.19% | [74 Missing and 6 partials :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5001?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    #5001      +/-   ##
==========================================
- Coverage   66.77%   66.63%   -0.15%     
==========================================
  Files         209      210       +1     
  Lines       32052    32169     +117     
==========================================
+ Hits        21404    21435      +31     
- Misses       9374     9449      +75     
- Partials     1274     1285      +11     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/5001?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).


Patch:
diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go
index 4183c1283..79b0e5a54 100644
--- a/api/v1alpha1/backendtrafficpolicy_types.go
+++ b/api/v1alpha1/backendtrafficpolicy_types.go
@@ -67,7 +67,6 @@ type BackendTrafficPolicySpec struct {
 	// The compression config for the http streams.
 	//
 	// +optional
-	// +notImplementedHide
 	Compression []*Compression `json:"compression,omitempty"`
 
 	// ResponseOverride defines the configuration to override specific responses with a custom one.
diff --git a/api/v1alpha1/compression_types.go b/api/v1alpha1/compression_types.go
index 73cb0109a..edd36fe6e 100644
--- a/api/v1alpha1/compression_types.go
+++ b/api/v1alpha1/compression_types.go
@@ -7,14 +7,25 @@ package v1alpha1
 
 // CompressorType defines the types of compressor library supported by Envoy Gateway.
 //
-// +kubebuilder:validation:Enum=Gzip
+// +kubebuilder:validation:Enum=Gzip;Brotli
 type CompressorType string
 
+const (
+	GzipCompressorType CompressorType = "Gzip"
+
+	BrotliCompressorType CompressorType = "Brotli"
+)
+
 // GzipCompressor defines the config for the Gzip compressor.
 // The default values can be found here:
 // https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/compression/gzip/compressor/v3/gzip.proto#extension-envoy-compression-gzip-compressor
 type GzipCompressor struct{}
 
+// BrotliCompressor defines the config for the Brotli compressor.
+// The default values can be found here:
+// https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/compression/brotli/compressor/v3/brotli.proto#extension-envoy-compression-brotli-compressor
+type BrotliCompressor struct{}
+
 // Compression defines the config of enabling compression.
 // This can help reduce the bandwidth at the expense of higher CPU.
 type Compression struct {
@@ -23,6 +34,11 @@ type Compression struct {
 	// +required
 	Type CompressorType `json:"type"`
 
+	// The configuration for Brotli compressor.
+	//
+	// +optional
+	Brotli *BrotliCompressor `json:"brotli,omitempty"`
+
 	// The configuration for GZIP compressor.
 	//
 	// +optional
diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go
index bc7aa3d3d..d1649a00c 100644
--- a/api/v1alpha1/envoyproxy_types.go
+++ b/api/v1alpha1/envoyproxy_types.go
@@ -245,6 +245,9 @@ const (
 	// EnvoyFilterCustomResponse defines the Envoy HTTP custom response filter.
 	EnvoyFilterCustomResponse EnvoyFilter = "envoy.filters.http.custom_response"
 
+	// EnvoyFilterCompressor defines the Envoy HTTP compressor filter.
+	EnvoyFilterCompressor EnvoyFilter = "envoy.filters.http.compressor"
+
 	// EnvoyFilterRouter defines the Envoy HTTP router filter.
 	EnvoyFilterRouter EnvoyFilter = "envoy.filters.http.router"
 )
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index e09588433..a0b728391 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -643,6 +643,21 @@ func (in *BodyToExtAuth) DeepCopy() *BodyToExtAuth {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BrotliCompressor) DeepCopyInto(out *BrotliCompressor) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BrotliCompressor.
+func (in *BrotliCompressor) DeepCopy() *BrotliCompressor {
+	if in == nil {
+		return nil
+	}
+	out := new(BrotliCompressor)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CORS) DeepCopyInto(out *CORS) {
 	*out = *in
@@ -1074,6 +1089,11 @@ func (in *ClusterSettings) DeepCopy() *ClusterSettings {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Compression) DeepCopyInto(out *Compression) {
 	*out = *in
+	if in.Brotli != nil {
+		in, out := &in.Brotli, &out.Brotli
+		*out = new(BrotliCompressor)
+		**out = **in
+	}
 	if in.Gzip != nil {
 		in, out := &in.Gzip, &out.Gzip
 		*out = new(GzipCompressor)
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 38c5d9a3c..505544795 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
@@ -106,6 +106,9 @@ spec:
                     Compression defines the config of enabling compression.
                     This can help reduce the bandwidth at the expense of higher CPU.
                   properties:
+                    brotli:
+                      description: The configuration for Brotli compressor.
+                      type: object
                     gzip:
                       description: The configuration for GZIP compressor.
                       type: object
@@ -114,6 +117,7 @@ spec:
                         for compression.
                       enum:
                       - Gzip
+                      - Brotli
                       type: string
                   required:
                   - type
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 aee0cc5d4..601d96f93 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
@@ -12497,6 +12497,9 @@ spec:
                               scarce and large payloads can be effectively compressed
                               at the expense of higher CPU load.
                             properties:
+                              brotli:
+                                description: The configuration for Brotli compressor.
+                                type: object
                               gzip:
                                 description: The configuration for GZIP compressor.
                                 type: object
@@ -12505,6 +12508,7 @@ spec:
                                   type to use for compression.
                                 enum:
                                 - Gzip
+                                - Brotli
                                 type: string
                             required:
                             - type
diff --git a/go.mod b/go.mod
index 0ba0911ab..6f77869b6 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
 	fortio.org/fortio v1.68.0
 	fortio.org/log v1.17.1
 	github.com/Masterminds/semver/v3 v3.3.1
+	github.com/andybalholm/brotli v1.0.1
 	github.com/cenkalti/backoff/v4 v4.3.0
 	github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
diff --git a/go.sum b/go.sum
index 4b566e535..2a884833c 100644
--- a/go.sum
+++ b/go.sum
@@ -86,6 +86,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
+github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
+github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
 github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
 github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
 github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go
index 093462942..2d07982cf 100644
--- a/internal/gatewayapi/backendtrafficpolicy.go
+++ b/internal/gatewayapi/backendtrafficpolicy.go
@@ -305,6 +305,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(
 		ds        *ir.DNS
 		h2        *ir.HTTP2Settings
 		ro        *ir.ResponseOverride
+		cp        []*ir.Compression
 		err, errs error
 	)
 
@@ -354,6 +355,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(
 		err = perr.WithMessage(err, "ResponseOverride")
 		errs = errors.Join(errs, err)
 	}
+	cp = buildCompression(policy.Spec.Compression)
 
 	ds = translateDNS(policy.Spec.ClusterSettings)
 
@@ -417,6 +419,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(
 						DNS:               ds,
 						Timeout:           to,
 						ResponseOverride:  ro,
+						Compression:       cp,
 					}
 
 					// Update the Host field in HealthCheck, now that we have access to the Route Hostname.
@@ -453,6 +456,7 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(
 		ds        *ir.DNS
 		h2        *ir.HTTP2Settings
 		ro        *ir.ResponseOverride
+		cp        []*ir.Compression
 		err, errs error
 	)
 
@@ -495,6 +499,7 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(
 		err = perr.WithMessage(err, "ResponseOverride")
 		errs = errors.Join(errs, err)
 	}
+	cp = buildCompression(policy.Spec.Compression)
 
 	ds = translateDNS(policy.Spec.ClusterSettings)
 
@@ -579,6 +584,7 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(
 				HTTP2:            h2,
 				DNS:              ds,
 				ResponseOverride: ro,
+				Compression:      cp,
 			}
 
 			// Update the Host field in HealthCheck, now that we have access to the Route Hostname.
@@ -930,3 +936,17 @@ func defaultResponseOverrideRuleName(policy *egv1a1.BackendTrafficPolicy, index
 		irConfigName(policy),
 		strconv.Itoa(index))
 }
+
+func buildCompression(compression []*egv1a1.Compression) []*ir.Compression {
+	if compression == nil {
+		return nil
+	}
+	irCompression := make([]*ir.Compression, 0, len(compression))
+	for _, c := range compression {
+		irCompression = append(irCompression, &ir.Compression{
+			Type: c.Type,
+		})
+	}
+
+	return irCompression
+}
diff --git a/internal/gatewayapi/clustersettings.go b/internal/gatewayapi/clustersettings.go
index 40266553b..5706504f4 100644
--- a/internal/gatewayapi/clustersettings.go
+++ b/internal/gatewayapi/clustersettings.go
@@ -11,6 +11,7 @@ import (
 	"math"
 	"math/big"
 	"net/http"
+	"reflect"
 	"strings"
 	"time"
 
@@ -76,7 +77,7 @@ func translateTrafficFeatures(policy *egv1a1.ClusterSettings) (*ir.TrafficFeatur
 	// If nothing was set in any of the above calls, return nil instead of an empty
 	// container
 	var empty ir.TrafficFeatures
-	if empty == *ret {
+	if reflect.DeepEqual(empty, *ret) {
 		ret = nil
 	}
 
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index 9aedf8920..e87a27fd4 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -767,6 +767,13 @@ type HeaderBasedSessionPersistence struct {
 	Name string `json:"name"`
 }
 
+// Compression holds the configuration for HTTP compression.
+// +k8s:deepcopy-gen=true
+type Compression struct {
+	// Type of compression to be used.
+	Type egv1a1.CompressorType `json:"type" yaml:"type"`
+}
+
 // TrafficFeatures holds the information associated with the Backend Traffic Policy.
 // +k8s:deepcopy-gen=true
 type TrafficFeatures struct {
@@ -798,6 +805,8 @@ type TrafficFeatures struct {
 	DNS *DNS `json:"dns,omitempty" yaml:"dns,omitempty"`
 	// ResponseOverride defines the schema for overriding the response.
 	ResponseOverride *ResponseOverride `json:"responseOverride,omitempty" yaml:"responseOverride,omitempty"`
+	// Compression settings for HTTP Response
+	Compression []*Compression `json:"compression,omitempty" yaml:"compression,omitempty"`
 }
 
 func (b *TrafficFeatures) Validate() error {
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index 8fae004f3..5b0071241 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -548,6 +548,21 @@ func (in *ClientTimeout) DeepCopy() *ClientTimeout {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Compression) DeepCopyInto(out *Compression) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Compression.
+func (in *Compression) DeepCopy() *Compression {
+	if in == nil {
+		return nil
+	}
+	out := new(Compression)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ConnectionLimit) DeepCopyInto(out *ConnectionLimit) {
 	*out = *in
@@ -3432,6 +3447,17 @@ func (in *TrafficFeatures) DeepCopyInto(out *TrafficFeatures) {
 		*out = new(ResponseOverride)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Compression != nil {
+		in, out := &in.Compression, &out.Compression
+		*out = make([]*Compression, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(Compression)
+				**out = **in
+			}
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficFeatures.
diff --git a/internal/xds/translator/compressor.go b/internal/xds/translator/compressor.go
new file mode 100644
index 000000000..a5af6363f
--- /dev/null
+++ b/internal/xds/translator/compressor.go
@@ -0,0 +1,222 @@
+// 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"
+	"strings"
+
+	corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
+	brotliv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/brotli/compressor/v3"
+	gzipv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/compression/gzip/compressor/v3"
+	compressorv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/compressor/v3"
+	hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
+	"google.golang.org/protobuf/proto"
+	"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/utils/protocov"
+	"github.com/envoyproxy/gateway/internal/xds/types"
+)
+
+func init() {
+	registerHTTPFilter(&compressor{})
+}
+
+type compressor struct{}
+
+var _ httpFilter = &compressor{}
+
+// patchHCM builds and appends the compressor Filter to the HTTP Connection Manager
+// if applicable, and it does not already exist.
+func (*compressor) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error {
+	if mgr == nil {
+		return errors.New("hcm is nil")
+	}
+	if irListener == nil {
+		return errors.New("ir listener is nil")
+	}
+
+	var (
+		brotli bool
+		gzip   bool
+		filter *hcmv3.HttpFilter
+		err    error
+	)
+
+	for _, route := range irListener.Routes {
+		if route.Traffic != nil && route.Traffic.Compression != nil {
+			for _, irComp := range route.Traffic.Compression {
+				if irComp.Type == egv1a1.BrotliCompressorType {
+					brotli = true
+				}
+				if irComp.Type == egv1a1.GzipCompressorType {
+					gzip = true
+				}
+			}
+		}
+	}
+
+	// Add the compressor filters for all the compression types required by the routes.
+	// All the compressor filters are disabled at the HCM level.
+	// The per route filter config will enable the compressor filters for the routes that require them.
+	if brotli {
+		brotliFilterName := compressorFilterName(egv1a1.BrotliCompressorType)
+		if !hcmContainsFilter(mgr, brotliFilterName) {
+			if filter, err = buildCompressorFilter(egv1a1.BrotliCompressorType); err != nil {
+				return err
+			}
+			mgr.HttpFilters = append(mgr.HttpFilters, filter)
+		}
+	}
+
+	if gzip {
+		gzipFilterName := compressorFilterName(egv1a1.GzipCompressorType)
+		if !hcmContainsFilter(mgr, gzipFilterName) {
+			if filter, err = buildCompressorFilter(egv1a1.GzipCompressorType); err != nil {
+				return err
+			}
+			mgr.HttpFilters = append(mgr.HttpFilters, filter)
+		}
+	}
+
+	return err
+}
+
+func compressorFilterName(compressorType egv1a1.CompressorType) string {
+	return fmt.Sprintf("%s.%s", egv1a1.EnvoyFilterCompressor.String(), strings.ToLower(string(compressorType)))
+}
+
+// buildCompressorFilter builds a compressor filter with the provided compressionType.
+func buildCompressorFilter(compressionType egv1a1.CompressorType) (*hcmv3.HttpFilter, error) {
+	var (
+		compressorProto *compressorv3.Compressor
+		extensionName   string
+		extensionMsg    proto.Message
+		extensionAny    *anypb.Any
+		compressorAny   *anypb.Any
+		err             error
+	)
+
+	switch compressionType {
+	case egv1a1.BrotliCompressorType:
+		extensionName = "envoy.compression.brotli.compressor"
+		extensionMsg = &brotliv3.Brotli{}
+	case egv1a1.GzipCompressorType:
+		extensionName = "envoy.compression.gzip.compressor"
+		extensionMsg = &gzipv3.Gzip{}
+	}
+
+	if extensionAny, err = protocov.ToAnyWithValidation(extensionMsg); err != nil {
+		return nil, err
+	}
+
+	compressorProto = &compressorv3.Compressor{
+		CompressorLibrary: &corev3.TypedExtensionConfig{
+			Name:        extensionName,
+			TypedConfig: extensionAny,
+		},
+	}
+
+	if compressorAny, err = protocov.ToAnyWithValidation(compressorProto); err != nil {
+		return nil, err
+	}
+
+	return &hcmv3.HttpFilter{
+		Name: compressorFilterName(compressionType),
+		ConfigType: &hcmv3.HttpFilter_TypedConfig{
+			TypedConfig: compressorAny,
+		},
+		Disabled: true,
+	}, nil
+}
+
+func (*compressor) patchResources(*types.ResourceVersionTable, []*ir.HTTPRoute) error {
+	return nil
+}
+
+// patchRoute patches the provided route with the compressor config if applicable.
+// Note: this method overwrites the HCM level filter config with the per route filter config.
+func (*compressor) 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 || len(irRoute.Traffic.Compression) == 0 {
+		return nil
+	}
+
+	var (
+		brotli        bool
+		gzip          bool
+		perFilterCfg  map[string]*anypb.Any
+		compressorAny *anypb.Any
+		err           error
+	)
+
+	for _, irComp := range irRoute.Traffic.Compression {
+		if irComp.Type == egv1a1.BrotliCompressorType {
+			brotli = true
+		}
+		if irComp.Type == egv1a1.GzipCompressorType {
+			gzip = true
+		}
+	}
+
+	if !brotli && !gzip {
+		return nil
+	}
+
+	// Overwrite the HCM level filter config with the per route filter config.
+	perFilterCfg = route.GetTypedPerFilterConfig()
+	if perFilterCfg == nil {
+		route.TypedPerFilterConfig = make(map[string]*anypb.Any)
+	}
+
+	compressorProto := compressorPerRouteConfig()
+	if compressorAny, err = protocov.ToAnyWithValidation(compressorProto); err != nil {
+		return err
+	}
+
+	if brotli {
+		brotliFilterName := compressorFilterName(egv1a1.BrotliCompressorType)
+		if _, ok := perFilterCfg[brotliFilterName]; ok {
+			// This should not happen since this is the only place where the filter
+			// config is added in a route.
+			return fmt.Errorf("route already contains filter config: %s, %+v",
+				brotliFilterName, route)
+		}
+		route.TypedPerFilterConfig[brotliFilterName] = compressorAny
+	}
+	if gzip {
+		gzipFilterName := compressorFilterName(egv1a1.GzipCompressorType)
+		if _, ok := perFilterCfg[gzipFilterName]; ok {
+			// This should not happen since this is the only place where the filter
+			// config is added in a route.
+			return fmt.Errorf("route already contains filter config: %s, %+v",
+				gzipFilterName, route)
+		}
+		route.TypedPerFilterConfig[gzipFilterName] = compressorAny
+	}
+
+	return nil
+}
+
+func compressorPerRouteConfig() *compressorv3.CompressorPerRoute {
+	// Enable compression on this route if compression is configured.
+	return &compressorv3.CompressorPerRoute{
+		Override: &compressorv3.CompressorPerRoute_Overrides{
+			Overrides: &compressorv3.CompressorOverrides{
+				ResponseDirectionConfig: &compressorv3.ResponseDirectionOverrides{},
+			},
+		},
+	}
+}
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index 76d77a75e..03d9dfab9 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -20,6 +20,7 @@ new features: |
   Added support for patching EnvoyProxy.spec.provider.kubernetes.envoyHpa and EnvoyProxy.spec.provider.kubernetes.envoyPDB
   Added support for defining rateLimitHpa in EnvoyGateway API
   Added support for preserving the user defined HTTPRoute match order in EnvoyProxy API
+  Added support for response compression in the BackendTrafficPolicy API
   Added support for specifying dynamic metadata namespaces that External Processing services can access read from and write to in EnvoyExtensionPolicy API
   Added support for API Key Authentication in the SecurityPolicy API
   Added support for GEP-1731 (HTTPRoute Retries)
diff --git a/tools/make/kube.mk b/tools/make/kube.mk
index faf6cc4e8..7b400b651 100644
--- a/tools/make/kube.mk
+++ b/tools/make/kube.mk
@@ -1,9 +1,10 @@
 # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
 # To know the available versions check:
 # - https://github.com/kubernetes-sigs/controller-tools/blob/main/envtest-releases.yaml
-ENVTEST_K8S_VERSION ?= 1.28.3
+ENVTEST_K8S_VERSION ?= 1.29.4
 # Need run cel validation across multiple versions of k8s
-ENVTEST_K8S_VERSIONS ?= 1.28.3 1.29.5 1.30.3 1.31.0
+ENVTEST_K8S_VERSIONS ?= 1.29.4 1.30.3 1.31.0 1.32.0
+
 # GATEWAY_API_VERSION refers to the version of Gateway API CRDs.
 # For more details, see https://gateway-api.sigs.k8s.io/guides/getting-started/#installing-gateway-api
 GATEWAY_API_VERSION ?= $(shell go list -m -f '{{.Version}}' sigs.k8s.io/gateway-api)


Test patch:
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compression.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.in.yaml
new file mode 100644
index 000000000..3e644a055
--- /dev/null
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.in.yaml
@@ -0,0 +1,49 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: http
+          protocol: HTTP
+          port: 80
+          allowedRoutes:
+            namespaces:
+              from: All
+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
+backendTrafficPolicies:
+  - 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
+      compression:
+        - type: Brotli
+        - type: Gzip
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml
new file mode 100644
index 000000000..8bd4eac94
--- /dev/null
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-compression.out.yaml
@@ -0,0 +1,171 @@
+backendTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: BackendTrafficPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-for-route
+    namespace: default
+  spec:
+    compression:
+    - type: Brotli
+    - type: Gzip
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-1
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+      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: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 1
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+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
+      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-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
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      text:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      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
+      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:
+          compression:
+          - type: Brotli
+          - type: Gzip
diff --git a/internal/xds/translator/testdata/in/xds-ir/compression.yaml b/internal/xds/translator/testdata/in/xds-ir/compression.yaml
new file mode 100644
index 000000000..f4e6c3383
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/compression.yaml
@@ -0,0 +1,41 @@
+http:
+- address: 0.0.0.0
+  hostnames:
+  - '*'
+  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
+  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:
+      compression:
+      - type: Brotli
+      - type: Gzip
+      - type: Brotli
diff --git a/internal/xds/translator/testdata/out/xds-ir/compression.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/compression.clusters.yaml
new file mode 100644
index 000000000..c24d059ee
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/compression.clusters.yaml
@@ -0,0 +1,17 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_PREFERRED
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: httproute/default/httproute-1/rule/0
+  ignoreHealthOnHostRemoval: true
+  lbPolicy: LEAST_REQUEST
+  name: httproute/default/httproute-1/rule/0
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/compression.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/compression.endpoints.yaml
new file mode 100644
index 000000000..29bb6b4e4
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/compression.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/compression.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/compression.listeners.yaml
new file mode 100644
index 000000000..c042cdcfa
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/compression.listeners.yaml
@@ -0,0 +1,50 @@
+- 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.compressor.brotli
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.compressor.v3.Compressor
+            compressorLibrary:
+              name: envoy.compression.brotli.compressor
+              typedConfig:
+                '@type': type.googleapis.com/envoy.extensions.compression.brotli.compressor.v3.Brotli
+        - disabled: true
+          name: envoy.filters.http.compressor.gzip
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.compressor.v3.Compressor
+            compressorLibrary:
+              name: envoy.compression.gzip.compressor
+              typedConfig:
+                '@type': type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip
+        - 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: envoy-gateway/gateway-1/http
+        serverHeaderTransformation: PASS_THROUGH
+        statPrefix: http-10080
+        useRemoteAddress: true
+    name: envoy-gateway/gateway-1/http
+  name: envoy-gateway/gateway-1/http
+  perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/compression.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/compression.routes.yaml
new file mode 100644
index 000000000..666c80b48
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/compression.routes.yaml
@@ -0,0 +1,38 @@
+- ignorePortInHostMatching: true
+  name: envoy-gateway/gateway-1/http
+  virtualHosts:
+  - domains:
+    - gateway.envoyproxy.io
+    metadata:
+      filterMetadata:
+        envoy-gateway:
+          resources:
+          - kind: Gateway
+            name: gateway-1
+            namespace: envoy-gateway
+            sectionName: http
+    name: envoy-gateway/gateway-1/http/gateway_envoyproxy_io
+    routes:
+    - match:
+        prefix: /
+      metadata:
+        filterMetadata:
+          envoy-gateway:
+            resources:
+            - kind: HTTPRoute
+              name: httproute-1
+              namespace: default
+      name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
+      route:
+        cluster: httproute/default/httproute-1/rule/0
+        upgradeConfigs:
+        - upgradeType: websocket
+      typedPerFilterConfig:
+        envoy.filters.http.compressor.brotli:
+          '@type': type.googleapis.com/envoy.extensions.filters.http.compressor.v3.CompressorPerRoute
+          overrides:
+            responseDirectionConfig: {}
+        envoy.filters.http.compressor.gzip:
+          '@type': type.googleapis.com/envoy.extensions.filters.http.compressor.v3.CompressorPerRoute
+          overrides:
+            responseDirectionConfig: {}
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 0e8c5339a..d7fdcb477 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -466,6 +466,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. |
+| `compression` | _[Compression](#compression) array_ |  false  |  | The compression config for the http streams. |
 | `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. |
 
 
@@ -513,6 +514,19 @@ _Appears in:_
 | `JSONPatch` | JSONPatch applies the provided JSONPatches to the default bootstrap.<br /> | 
 
 
+#### BrotliCompressor
+
+
+
+BrotliCompressor defines the config for the Brotli compressor.
+The default values can be found here:
+https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/compression/brotli/compressor/v3/brotli.proto#extension-envoy-compression-brotli-compressor
+
+_Appears in:_
+- [Compression](#compression)
+
+
+
 #### CIDR
 
 _Underlying type:_ _string_
@@ -755,6 +769,7 @@ _Appears in:_
 | Field | Type | Required | Default | Description |
 | ---   | ---  | ---      | ---     | ---         |
 | `type` | _[CompressorType](#compressortype)_ |  true  |  | CompressorType defines the compressor type to use for compression. |
+| `brotli` | _[BrotliCompressor](#brotlicompressor)_ |  false  |  | The configuration for Brotli compressor. |
 | `gzip` | _[GzipCompressor](#gzipcompressor)_ |  false  |  | The configuration for GZIP compressor. |
 
 
@@ -767,6 +782,10 @@ CompressorType defines the types of compressor library supported by Envoy Gatewa
 _Appears in:_
 - [Compression](#compression)
 
+| Value | Description |
+| ----- | ----------- |
+| `Gzip` |  | 
+| `Brotli` |  | 
 
 
 #### ConnectionLimit
@@ -1024,6 +1043,7 @@ _Appears in:_
 | `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.compressor` | EnvoyFilterCompressor defines the Envoy HTTP compressor filter.<br /> | 
 | `envoy.filters.http.router` | EnvoyFilterRouter defines the Envoy HTTP router filter.<br /> | 
 
 
diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md
index 0e8c5339a..d7fdcb477 100644
--- a/site/content/zh/latest/api/extension_types.md
+++ b/site/content/zh/latest/api/extension_types.md
@@ -466,6 +466,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. |
+| `compression` | _[Compression](#compression) array_ |  false  |  | The compression config for the http streams. |
 | `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. |
 
 
@@ -513,6 +514,19 @@ _Appears in:_
 | `JSONPatch` | JSONPatch applies the provided JSONPatches to the default bootstrap.<br /> | 
 
 
+#### BrotliCompressor
+
+
+
+BrotliCompressor defines the config for the Brotli compressor.
+The default values can be found here:
+https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/compression/brotli/compressor/v3/brotli.proto#extension-envoy-compression-brotli-compressor
+
+_Appears in:_
+- [Compression](#compression)
+
+
+
 #### CIDR
 
 _Underlying type:_ _string_
@@ -755,6 +769,7 @@ _Appears in:_
 | Field | Type | Required | Default | Description |
 | ---   | ---  | ---      | ---     | ---         |
 | `type` | _[CompressorType](#compressortype)_ |  true  |  | CompressorType defines the compressor type to use for compression. |
+| `brotli` | _[BrotliCompressor](#brotlicompressor)_ |  false  |  | The configuration for Brotli compressor. |
 | `gzip` | _[GzipCompressor](#gzipcompressor)_ |  false  |  | The configuration for GZIP compressor. |
 
 
@@ -767,6 +782,10 @@ CompressorType defines the types of compressor library supported by Envoy Gatewa
 _Appears in:_
 - [Compression](#compression)
 
+| Value | Description |
+| ----- | ----------- |
+| `Gzip` |  | 
+| `Brotli` |  | 
 
 
 #### ConnectionLimit
@@ -1024,6 +1043,7 @@ _Appears in:_
 | `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.compressor` | EnvoyFilterCompressor defines the Envoy HTTP compressor filter.<br /> | 
 | `envoy.filters.http.router` | EnvoyFilterRouter defines the Envoy HTTP router filter.<br /> | 
 
 
diff --git a/test/e2e/testdata/compression.yaml b/test/e2e/testdata/compression.yaml
new file mode 100644
index 000000000..d2e5b030b
--- /dev/null
+++ b/test/e2e/testdata/compression.yaml
@@ -0,0 +1,48 @@
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: compression
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+    - name: same-namespace
+  rules:
+    - matches:
+        - path:
+            type: PathPrefix
+            value: /compression
+      backendRefs:
+        - name: infra-backend-v1
+          port: 8080
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: no-compression
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+    - name: same-namespace
+  rules:
+    - matches:
+        - path:
+            type: PathPrefix
+            value: /no-compression
+      backendRefs:
+        - name: infra-backend-v1
+          port: 8080
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: compression
+  namespace: gateway-conformance-infra
+spec:
+  targetRef:
+    group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: compression
+  compression:
+    - type: Brotli
+    - type: Gzip
diff --git a/test/e2e/tests/compression.go b/test/e2e/tests/compression.go
new file mode 100644
index 000000000..2d67a2a47
--- /dev/null
+++ b/test/e2e/tests/compression.go
@@ -0,0 +1,231 @@
+// 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 (
+	"compress/gzip"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net"
+	nethttp "net/http"
+	"net/http/httputil"
+	"testing"
+
+	"github.com/andybalholm/brotli"
+	"k8s.io/apimachinery/pkg/types"
+	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+	gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+	"sigs.k8s.io/gateway-api/conformance/utils/config"
+	"sigs.k8s.io/gateway-api/conformance/utils/http"
+	"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
+	"sigs.k8s.io/gateway-api/conformance/utils/roundtripper"
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+	"sigs.k8s.io/gateway-api/conformance/utils/tlog"
+
+	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
+	"github.com/envoyproxy/gateway/internal/gatewayapi"
+	"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
+)
+
+func init() {
+	ConformanceTests = append(ConformanceTests, CompressionTest)
+}
+
+var CompressionTest = suite.ConformanceTest{
+	ShortName:   "Compression",
+	Description: "Test response compression on HTTPRoute",
+	Manifests:   []string{"testdata/compression.yaml"},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		t.Run("HTTPRoute with brotli compression", func(t *testing.T) {
+			testCompression(t, suite, egv1a1.BrotliCompressorType)
+		})
+
+		t.Run("HTTPRoute with gzip compression", func(t *testing.T) {
+			testCompression(t, suite, egv1a1.GzipCompressorType)
+		})
+
+		t.Run("HTTPRoute without compression", func(t *testing.T) {
+			ns := "gateway-conformance-infra"
+			routeNN := types.NamespacedName{Name: "no-compression", 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: "compression", Namespace: ns}, suite.ControllerName, ancestorRef)
+
+			expectedResponse := http.ExpectedResponse{
+				Request: http.Request{
+					Path: "/no-compression",
+					Headers: map[string]string{
+						"Accept-encoding": "gzip",
+					},
+				},
+				Response: http.Response{
+					StatusCode:    200,
+					AbsentHeaders: []string{"content-encoding"},
+				},
+				Namespace: ns,
+			}
+
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+		})
+	},
+}
+
+func testCompression(t *testing.T, suite *suite.ConformanceTestSuite, compressionType egv1a1.CompressorType) {
+	ns := "gateway-conformance-infra"
+	routeNN := types.NamespacedName{Name: "compression", 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: "compression", Namespace: ns}, suite.ControllerName, ancestorRef)
+
+	var encoding string
+	switch compressionType {
+	case egv1a1.BrotliCompressorType:
+		encoding = "br"
+	case egv1a1.GzipCompressorType:
+		encoding = "gzip"
+	}
+
+	expectedResponse := http.ExpectedResponse{
+		Request: http.Request{
+			Path: "/compression",
+			Headers: map[string]string{
+				"Accept-encoding": encoding,
+			},
+		},
+		Response: http.Response{
+			StatusCode: 200,
+			Headers: map[string]string{
+				"content-encoding": encoding,
+			},
+		},
+		Namespace: ns,
+	}
+	roundTripper := &CompressionRoundTripper{Debug: suite.Debug, TimeoutConfig: suite.TimeoutConfig}
+	http.MakeRequestAndExpectEventuallyConsistentResponse(t, roundTripper, suite.TimeoutConfig, gwAddr, expectedResponse)
+}
+
+// CompressionRoundTripper implements roundtripper.RoundTripper and adds support for compression.
+type CompressionRoundTripper struct {
+	Debug             bool
+	TimeoutConfig     config.TimeoutConfig
+	CustomDialContext func(context.Context, string, string) (net.Conn, error)
+}
+
+func (d *CompressionRoundTripper) CaptureRoundTrip(request roundtripper.Request) (*roundtripper.CapturedRequest, *roundtripper.CapturedResponse, error) {
+	return d.defaultRoundTrip(request, &nethttp.Transport{})
+}
+
+func (d *CompressionRoundTripper) defaultRoundTrip(request roundtripper.Request, transport nethttp.RoundTripper) (*roundtripper.CapturedRequest, *roundtripper.CapturedResponse, error) {
+	client := &nethttp.Client{}
+
+	client.Transport = transport
+
+	method := "GET"
+	if request.Method != "" {
+		method = request.Method
+	}
+	ctx, cancel := context.WithTimeout(context.Background(), d.TimeoutConfig.RequestTimeout)
+	defer cancel()
+	req, err := nethttp.NewRequestWithContext(ctx, method, request.URL.String(), nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	if request.Host != "" {
+		req.Host = request.Host
+	}
+
+	if request.Headers != nil {
+		for name, value := range request.Headers {
+			req.Header.Set(name, value[0])
+		}
+	}
+
+	if d.Debug {
+		var dump []byte
+		dump, err = httputil.DumpRequestOut(req, true)
+		if err != nil {
+			return nil, nil, err
+		}
+
+		tlog.Logf(request.T, "Sending Request:\n%s\n\n", formatDump(dump))
+	}
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, nil, err
+	}
+	defer resp.Body.Close()
+
+	if d.Debug {
+		var dump []byte
+		dump, err = httputil.DumpResponse(resp, true)
+		if err != nil {
+			return nil, nil, err
+		}
+
+		tlog.Logf(request.T, "Received Response:\n%s\n\n", formatDump(dump))
+	}
+
+	cReq := &roundtripper.CapturedRequest{}
+	var body []byte
+
+	var reader io.Reader
+	switch resp.Header.Get("Content-Encoding") {
+	case "gzip":
+		if reader, err = gzip.NewReader(resp.Body); err != nil {
+			return nil, nil, err
+		}
+	case "br":
+		reader = brotli.NewReader(resp.Body)
+	default:
+		reader = resp.Body
+	}
+	if body, err = io.ReadAll(reader); err != nil {
+		return nil, nil, err
+	}
+
+	// we cannot assume the response is JSON
+	if resp.Header.Get("Content-type") == "application/json" {
+		err = json.Unmarshal(body, cReq)
+		if err != nil {
+			return nil, nil, fmt.Errorf("unexpected error reading response: %w", err)
+		}
+	} else {
+		cReq.Method = method // assume it made the right request if the service being called isn't echoing
+	}
+
+	cRes := &roundtripper.CapturedResponse{
+		StatusCode:    resp.StatusCode,
+		ContentLength: resp.ContentLength,
+		Protocol:      resp.Proto,
+		Headers:       resp.Header,
+	}
+
+	if resp.TLS != nil {
+		cRes.PeerCertificates = resp.TLS.PeerCertificates
+	}
+
+	return cReq, cRes, nil
+}
diff --git a/test/e2e/tests/preservecase.go b/test/e2e/tests/preservecase.go
index 6c81dfe50..1754186f3 100644
--- a/test/e2e/tests/preservecase.go
+++ b/test/e2e/tests/preservecase.go
@@ -32,8 +32,8 @@ func init() {
 // Copied from the conformance suite because it's needed in casePreservingRoundTrip
 var startLineRegex = regexp.MustCompile(`(?m)^`)
 
-func formatDump(data []byte, prefix string) string {
-	data = startLineRegex.ReplaceAllLiteral(data, []byte(prefix))
+func formatDump(data []byte) string {
+	data = startLineRegex.ReplaceAllLiteral(data, []byte("< "))
 	return string(data)
 }
 
@@ -67,7 +67,7 @@ func casePreservingRoundTrip(request roundtripper.Request, transport nethttp.Rou
 			return nil, err
 		}
 
-		fmt.Printf("Sending Request:\n%s\n\n", formatDump(dump, "< "))
+		fmt.Printf("Sending Request:\n%s\n\n", formatDump(dump))
 	}
 
 	resp, err := client.Do(req)
@@ -83,7 +83,7 @@ func casePreservingRoundTrip(request roundtripper.Request, transport nethttp.Rou
 			return nil, err
 		}
 
-		fmt.Printf("Received Response:\n%s\n\n", formatDump(dump, "< "))
+		fmt.Printf("Received Response:\n%s\n\n", formatDump(dump))
 	}
 
 	cReq := map[string]any{}