Skip to content

Commit 6abdf74

Browse files
committed
fix: support gzip-encoded responses from the server
1 parent 067637c commit 6abdf74

3 files changed

Lines changed: 94 additions & 0 deletions

File tree

e2e/proxy_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"context"
77
"encoding/json"
88
"fmt"
9+
"strings"
910
"time"
1011

1112
v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
@@ -1197,6 +1198,86 @@ var _ = Describe("Proxy", func() {
11971198
})
11981199
})
11991200
})
1201+
1202+
When("creating and getting a large configmap through the proxy", func() {
1203+
// This test validates both gzip failure paths:
1204+
// 1. The workflow engine path (WriteToKube in activity.go): a large CREATE
1205+
// response is gzip-encoded by kube; without the fix, WriteToKube would
1206+
// forward Accept-Encoding from the client, preventing the REST transport
1207+
// from auto-decompressing, and the client receives raw gzip bytes.
1208+
// 2. The reverse proxy path (Director in server.go): a large GET response
1209+
// is gzip-encoded by kube; without the fix, FilterResp receives gzip
1210+
// bytes and fails to decode them as JSON.
1211+
BeforeEach(func() {
1212+
createConfigMap := proxyrule.Config{Spec: proxyrule.Spec{
1213+
Locking: proxyrule.OptimisticLockMode,
1214+
Matches: []proxyrule.Match{{
1215+
GroupVersion: "v1",
1216+
Resource: "configmaps",
1217+
Verbs: []string{"create"},
1218+
}},
1219+
Update: proxyrule.Update{
1220+
CreateRelationships: []proxyrule.StringOrTemplate{{
1221+
// testresource view = viewer + creator, so this grants
1222+
// the creator permission to view the configmap below.
1223+
Template: "testresource:{{namespacedName}}#creator@user:{{user.name}}",
1224+
}},
1225+
},
1226+
}}
1227+
1228+
getConfigMap := proxyrule.Config{Spec: proxyrule.Spec{
1229+
Matches: []proxyrule.Match{{
1230+
GroupVersion: "v1",
1231+
Resource: "configmaps",
1232+
Verbs: []string{"get"},
1233+
}},
1234+
Checks: []proxyrule.StringOrTemplate{{
1235+
Template: "testresource:{{namespacedName}}#view@user:{{user.name}}",
1236+
}},
1237+
}}
1238+
1239+
matcher, err := rules.NewMapMatcher([]proxyrule.Config{
1240+
createNamespace(),
1241+
createConfigMap,
1242+
getConfigMap,
1243+
})
1244+
Expect(err).To(Succeed())
1245+
*proxySrv.Matcher = matcher
1246+
})
1247+
1248+
It("handles gzip-encoded responses from the workflow engine and reverse proxy", func(ctx context.Context) {
1249+
// Create namespace via proxy (goes through workflow engine).
1250+
Expect(CreateNamespace(ctx, paulClient, paulNamespace)).To(Succeed())
1251+
1252+
// 300KB is large enough to trigger kube's gzip encoding (threshold ~128KB).
1253+
const dataSize = 300 * 1024
1254+
cmName := names.SimpleNameGenerator.GenerateName("large-cm-")
1255+
1256+
// CREATE the large configmap through the proxy.
1257+
// This goes through the workflow engine (WriteToKube in activity.go).
1258+
// kube gzip-encodes the ~300KB 201 Created response; without the fix
1259+
// in WriteToKube, the client receives raw gzip bytes and fails to decode.
1260+
_, err := paulClient.CoreV1().ConfigMaps(paulNamespace).Create(ctx, &corev1.ConfigMap{
1261+
ObjectMeta: metav1.ObjectMeta{
1262+
Name: cmName,
1263+
Namespace: paulNamespace,
1264+
},
1265+
Data: map[string]string{
1266+
"payload": strings.Repeat("x", dataSize),
1267+
},
1268+
}, metav1.CreateOptions{})
1269+
Expect(err).To(Succeed())
1270+
1271+
// GET the large configmap through the proxy.
1272+
// This goes through the reverse proxy (Director + FilterResp in server.go).
1273+
// kube gzip-encodes the ~300KB response; without the fix in Director,
1274+
// FilterResp receives gzip bytes and cannot decode them as JSON.
1275+
cm, err := paulClient.CoreV1().ConfigMaps(paulNamespace).Get(ctx, cmName, metav1.GetOptions{})
1276+
Expect(err).To(Succeed())
1277+
Expect(cm.Name).To(Equal(cmName))
1278+
Expect(cm.Data["payload"]).To(HaveLen(dataSize))
1279+
})
1280+
})
12001281
})
12011282
})
12021283

pkg/authz/distributedtx/activity.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,13 @@ func (h *ActivityHandler) WriteToKube(ctx context.Context, req *KubeReqInput) (*
205205

206206
kreq := h.KubeClient.Verb(verb).RequestURI(req.RequestURI).Body(req.Body)
207207
for h, v := range req.Header {
208+
// Don't forward Accept-Encoding: the REST client's transport must own
209+
// gzip negotiation so it can auto-decompress large responses. If we set
210+
// Accept-Encoding ourselves, the transport won't know to decompress and
211+
// res.Raw() returns compressed bytes.
212+
if http.CanonicalHeaderKey(h) == "Accept-Encoding" {
213+
continue
214+
}
208215
kreq.SetHeader(h, v...)
209216
}
210217

pkg/proxy/server.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ func NewServer(ctx context.Context, c *CompletedConfig) (*Server, error) {
9999
host := strings.TrimPrefix(clusterHost, "https://")
100100
req.URL.Host = strings.TrimSuffix(host, "/")
101101
req.URL.Scheme = "https"
102+
// Remove Accept-Encoding so the proxy's transport owns gzip negotiation.
103+
// When the transport adds Accept-Encoding: gzip itself, it also auto-decompresses
104+
// the response and strips Content-Encoding: gzip before ModifyResponse/FilterResp
105+
// runs. This ensures FilterResp always receives uncompressed bytes regardless of
106+
// response size.
107+
req.Header.Del("Accept-Encoding")
102108
},
103109
ModifyResponse: func(response *http.Response) error {
104110
klog.V(3).InfoSDepth(1, "upstream Kubernetes API response",

0 commit comments

Comments
 (0)