Skip to content

Commit d916440

Browse files
authored
feat: support response compression (#199)
1 parent b30cc95 commit d916440

13 files changed

Lines changed: 401 additions & 37 deletions

File tree

cmd/hasura-ndc-go/command/internal/testdata/basic/source/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/go-logr/stdr v1.2.2 // indirect
2121
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
2222
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
23+
github.com/klauspost/compress v1.18.0 // indirect
2324
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
2425
github.com/prometheus/client_golang v1.23.0 // indirect
2526
github.com/prometheus/client_model v0.6.2 // indirect

cmd/hasura-ndc-go/command/internal/testdata/empty/source/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ require (
1919
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
2020
github.com/google/uuid v1.6.0 // indirect
2121
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
22+
github.com/klauspost/compress v1.18.0 // indirect
2223
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
2324
github.com/prometheus/client_golang v1.23.0 // indirect
2425
github.com/prometheus/client_model v0.6.2 // indirect

cmd/hasura-ndc-go/command/internal/testdata/single_op/source/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
2121
github.com/google/uuid v1.6.0 // indirect
2222
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
23+
github.com/klauspost/compress v1.18.0 // indirect
2324
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
2425
github.com/prometheus/client_golang v1.23.0 // indirect
2526
github.com/prometheus/client_model v0.6.2 // indirect

cmd/hasura-ndc-go/command/internal/testdata/snake_case/source/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/go-logr/stdr v1.2.2 // indirect
2121
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
2222
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
23+
github.com/klauspost/compress v1.18.0 // indirect
2324
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
2425
github.com/prometheus/client_golang v1.23.0 // indirect
2526
github.com/prometheus/client_model v0.6.2 // indirect

connector/http.go

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -52,28 +52,6 @@ var allowedTraceEndpoints = map[string]string{
5252

5353
var debugApiPaths = []string{apiPathMetrics, apiPathHealth}
5454

55-
// define a custom response write to capture response information for logging.
56-
type customResponseWriter struct {
57-
http.ResponseWriter
58-
59-
statusCode int
60-
bodyLength int
61-
body []byte
62-
}
63-
64-
func (cw *customResponseWriter) WriteHeader(statusCode int) {
65-
cw.statusCode = statusCode
66-
cw.ResponseWriter.WriteHeader(statusCode)
67-
}
68-
69-
func (cw *customResponseWriter) Write(body []byte) (int, error) {
70-
cw.body = body
71-
bodyLength, err := cw.ResponseWriter.Write(body)
72-
cw.bodyLength = bodyLength
73-
74-
return bodyLength, err
75-
}
76-
7755
// implements a simple router to reuse for both configuration and connector servers.
7856
type router struct {
7957
routes map[string]map[string][]http.HandlerFunc
@@ -258,7 +236,7 @@ func (rt *router) createHandleFunc( //nolint:gocognit
258236

259237
logger := rt.logger.With(slog.String("request_id", requestID))
260238
req := r.WithContext(context.WithValue(ctx, logContextKey, logger))
261-
writer := &customResponseWriter{ResponseWriter: w}
239+
writer := newCustomResponseWriter(r, w)
262240

263241
for _, h := range handlers {
264242
h(writer, req)

connector/response.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package connector
2+
3+
import (
4+
"bytes"
5+
"net/http"
6+
"strings"
7+
8+
"github.com/hasura/ndc-sdk-go/utils/compression"
9+
)
10+
11+
const (
12+
acceptEncodingHeader = "Accept-Encoding"
13+
contentEncodingHeader = "Content-Encoding"
14+
)
15+
16+
// define a custom response write to capture response information for logging.
17+
type customResponseWriter struct {
18+
http.ResponseWriter
19+
20+
contentEncoding string
21+
statusCode int
22+
bodyLength int
23+
body []byte
24+
}
25+
26+
var _ http.ResponseWriter = (*customResponseWriter)(nil)
27+
28+
func newCustomResponseWriter(r *http.Request, w http.ResponseWriter) *customResponseWriter {
29+
var contentEncoding string
30+
31+
acceptEncoding := r.Header.Get(acceptEncodingHeader)
32+
33+
if acceptEncoding != "" {
34+
for enc := range strings.SplitSeq(acceptEncoding, ",") {
35+
enc = strings.ToLower(strings.TrimSpace(enc))
36+
37+
if compression.DefaultCompressor.IsEncodingSupported(enc) {
38+
contentEncoding = enc
39+
40+
break
41+
}
42+
}
43+
}
44+
45+
return &customResponseWriter{
46+
ResponseWriter: w,
47+
contentEncoding: contentEncoding,
48+
}
49+
}
50+
51+
func (cw *customResponseWriter) WriteHeader(statusCode int) {
52+
cw.statusCode = statusCode
53+
54+
if cw.contentEncoding != "" {
55+
cw.ResponseWriter.Header().Set(contentEncodingHeader, cw.contentEncoding)
56+
}
57+
58+
cw.ResponseWriter.WriteHeader(statusCode)
59+
}
60+
61+
func (cw *customResponseWriter) Write(body []byte) (int, error) {
62+
cw.body = body
63+
cw.bodyLength = len(body)
64+
65+
written, err := compression.DefaultCompressor.Compress(
66+
cw.ResponseWriter,
67+
cw.contentEncoding,
68+
bytes.NewReader(body),
69+
)
70+
71+
return int(written), err
72+
}

connector/server_test.go

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/hasura/ndc-sdk-go/schema"
1616
"github.com/hasura/ndc-sdk-go/utils"
17+
"github.com/hasura/ndc-sdk-go/utils/compression"
1718
"gotest.tools/v3/assert"
1819
)
1920

@@ -226,11 +227,11 @@ func (mc *mockConnector) Query(
226227
}, nil
227228
}
228229

229-
func httpPostJSON(url string, body any) (*http.Response, error) {
230-
return httpPostJSONWithNDCVersion(url, schema.NDCVersion, body)
230+
func httpPostJSON(url string, headers map[string]string, body any) (*http.Response, error) {
231+
return httpPostJSONWithNDCVersion(url, schema.NDCVersion, headers, body)
231232
}
232233

233-
func httpPostJSONWithNDCVersion(url string, ndcVersion string, body any) (*http.Response, error) {
234+
func httpPostJSONWithNDCVersion(url string, ndcVersion string, headers map[string]string, body any) (*http.Response, error) {
234235
bodyBytes, err := json.Marshal(body)
235236
if err != nil {
236237
return nil, err
@@ -244,6 +245,10 @@ func httpPostJSONWithNDCVersion(url string, ndcVersion string, body any) (*http.
244245
req.Header.Set("Content-Type", "application/json")
245246
req.Header.Set(schema.XHasuraNDCVersion, ndcVersion)
246247

248+
for key, value := range headers {
249+
req.Header.Set(key, value)
250+
}
251+
247252
return http.DefaultClient.Do(req)
248253
}
249254

@@ -252,9 +257,21 @@ func assertHTTPResponse[B any](t *testing.T, res *http.Response, statusCode int,
252257

253258
defer res.Body.Close()
254259

255-
bodyBytes, err := io.ReadAll(res.Body)
260+
var bodyBytes []byte
261+
var err error
262+
contentEnc := res.Header.Get(contentEncodingHeader)
263+
264+
if contentEnc != "" {
265+
db, err := compression.DefaultCompressor.Decompress(res.Body, contentEnc)
266+
assert.NilError(t, err)
267+
268+
bodyBytes, err = io.ReadAll(db)
269+
} else {
270+
bodyBytes, err = io.ReadAll(res.Body)
271+
}
272+
256273
if err != nil {
257-
t.Error("failed to read response body")
274+
t.Errorf("failed to read response body: %s", err)
258275
t.FailNow()
259276
}
260277

@@ -422,7 +439,7 @@ func TestServerConnector(t *testing.T) {
422439
})
423440

424441
t.Run("POST /query", func(t *testing.T) {
425-
res, err := httpPostJSON(fmt.Sprintf("%s/query", httpServer.URL), schema.QueryRequest{
442+
res, err := httpPostJSON(fmt.Sprintf("%s/query", httpServer.URL), nil, schema.QueryRequest{
426443
Collection: "articles",
427444
Arguments: schema.QueryRequestArguments{},
428445
CollectionRelationships: schema.QueryRequestCollectionRelationships{},
@@ -448,7 +465,7 @@ func TestServerConnector(t *testing.T) {
448465
})
449466

450467
t.Run("POST /query - json decode failure", func(t *testing.T) {
451-
res, err := httpPostJSON(fmt.Sprintf("%s/query", httpServer.URL), "")
468+
res, err := httpPostJSON(fmt.Sprintf("%s/query", httpServer.URL), nil, "")
452469
if err != nil {
453470
t.Errorf("expected no error, got %s", err)
454471
t.FailNow()
@@ -463,7 +480,7 @@ func TestServerConnector(t *testing.T) {
463480
})
464481

465482
t.Run("POST /query - collection not found", func(t *testing.T) {
466-
res, err := httpPostJSON(fmt.Sprintf("%s/query", httpServer.URL), schema.QueryRequest{
483+
res, err := httpPostJSON(fmt.Sprintf("%s/query", httpServer.URL), nil, schema.QueryRequest{
467484
Collection: "test",
468485
Arguments: schema.QueryRequestArguments{},
469486
CollectionRelationships: schema.QueryRequestCollectionRelationships{},
@@ -485,6 +502,7 @@ func TestServerConnector(t *testing.T) {
485502
res, err := httpPostJSONWithNDCVersion(
486503
fmt.Sprintf("%s/query", httpServer.URL),
487504
"unknown",
505+
nil,
488506
schema.QueryRequest{
489507
Collection: "test",
490508
Arguments: schema.QueryRequestArguments{},
@@ -506,7 +524,7 @@ func TestServerConnector(t *testing.T) {
506524
t.Run("POST_query_max_body_size", func(t *testing.T) {
507525
collectionName := strings.Repeat("xxxxxxxxxxxxxxxx", 1024*1024/16)
508526

509-
res, err := httpPostJSON(fmt.Sprintf("%s/query", httpServer.URL), schema.QueryRequest{
527+
res, err := httpPostJSON(fmt.Sprintf("%s/query", httpServer.URL), nil, schema.QueryRequest{
510528
Collection: collectionName,
511529
Arguments: schema.QueryRequestArguments{},
512530
CollectionRelationships: schema.QueryRequestCollectionRelationships{},
@@ -525,7 +543,7 @@ func TestServerConnector(t *testing.T) {
525543
})
526544

527545
t.Run("POST /mutation", func(t *testing.T) {
528-
res, err := httpPostJSON(fmt.Sprintf("%s/mutation", httpServer.URL), schema.MutationRequest{
546+
res, err := httpPostJSON(fmt.Sprintf("%s/mutation", httpServer.URL), nil, schema.MutationRequest{
529547
Operations: []schema.MutationOperation{
530548
{
531549
Type: "procedure",
@@ -547,7 +565,7 @@ func TestServerConnector(t *testing.T) {
547565
})
548566

549567
t.Run("POST /mutation - json decode failure", func(t *testing.T) {
550-
res, err := httpPostJSON(fmt.Sprintf("%s/mutation", httpServer.URL), "")
568+
res, err := httpPostJSON(fmt.Sprintf("%s/mutation", httpServer.URL), nil, "")
551569
if err != nil {
552570
t.Errorf("expected no error, got %s", err)
553571
t.FailNow()
@@ -562,7 +580,7 @@ func TestServerConnector(t *testing.T) {
562580
})
563581

564582
t.Run("POST /mutation - operation not found", func(t *testing.T) {
565-
res, err := httpPostJSON(fmt.Sprintf("%s/mutation", httpServer.URL), schema.MutationRequest{
583+
res, err := httpPostJSON(fmt.Sprintf("%s/mutation", httpServer.URL), nil, schema.MutationRequest{
566584
Operations: []schema.MutationOperation{
567585
{
568586
Type: "procedure",
@@ -586,6 +604,7 @@ func TestServerConnector(t *testing.T) {
586604
res, err := httpPostJSONWithNDCVersion(
587605
fmt.Sprintf("%s/mutation", httpServer.URL),
588606
"v0.1.6",
607+
nil,
589608
schema.MutationRequest{
590609
Operations: []schema.MutationOperation{
591610
{
@@ -613,7 +632,7 @@ func TestServerConnector(t *testing.T) {
613632

614633
t.Run("POST_mutation_max_body_size", func(t *testing.T) {
615634
collectionName := strings.Repeat("xxxxxxxxxxxxxxxx", 1024*1024/16)
616-
res, err := httpPostJSON(fmt.Sprintf("%s/mutation", httpServer.URL), schema.MutationRequest{
635+
res, err := httpPostJSON(fmt.Sprintf("%s/mutation", httpServer.URL), nil, schema.MutationRequest{
617636
Operations: []schema.MutationOperation{
618637
{
619638
Type: "procedure",
@@ -637,6 +656,7 @@ func TestServerConnector(t *testing.T) {
637656
t.Run("POST /query/explain", func(t *testing.T) {
638657
res, err := httpPostJSON(
639658
fmt.Sprintf("%s/query/explain", httpServer.URL),
659+
nil,
640660
schema.QueryRequest{
641661
Collection: "articles",
642662
Arguments: schema.QueryRequestArguments{},
@@ -655,8 +675,40 @@ func TestServerConnector(t *testing.T) {
655675
})
656676
})
657677

678+
for _, encoding := range []compression.CompressionFormat{
679+
compression.EncodingDeflate,
680+
compression.EncodingGzip,
681+
compression.EncodingZstd,
682+
} {
683+
t.Run("POST_query_explain_"+string(encoding), func(t *testing.T) {
684+
res, err := httpPostJSON(
685+
fmt.Sprintf("%s/query/explain", httpServer.URL),
686+
map[string]string{
687+
acceptEncodingHeader: string(encoding),
688+
},
689+
schema.QueryRequest{
690+
Collection: "articles",
691+
Arguments: schema.QueryRequestArguments{},
692+
CollectionRelationships: schema.QueryRequestCollectionRelationships{},
693+
Query: schema.Query{},
694+
Variables: []schema.QueryRequestVariablesElem{},
695+
},
696+
)
697+
if err != nil {
698+
t.Errorf("expected no error, got %s", err)
699+
t.FailNow()
700+
}
701+
702+
assert.Equal(t, string(encoding), res.Header.Get(contentEncodingHeader))
703+
704+
assertHTTPResponse(t, res, http.StatusOK, schema.ExplainResponse{
705+
Details: schema.ExplainResponseDetails{},
706+
})
707+
})
708+
}
709+
658710
t.Run("POST /query/explain - json decode failure", func(t *testing.T) {
659-
res, err := httpPostJSON(fmt.Sprintf("%s/query/explain", httpServer.URL), map[string]any{})
711+
res, err := httpPostJSON(fmt.Sprintf("%s/query/explain", httpServer.URL), nil, map[string]any{})
660712
if err != nil {
661713
t.Errorf("expected no error, got %s", err)
662714
t.FailNow()
@@ -674,6 +726,7 @@ func TestServerConnector(t *testing.T) {
674726
res, err := httpPostJSONWithNDCVersion(
675727
fmt.Sprintf("%s/query/explain", httpServer.URL),
676728
"unknown",
729+
nil,
677730
map[string]any{},
678731
)
679732
if err != nil {
@@ -690,6 +743,7 @@ func TestServerConnector(t *testing.T) {
690743
t.Run("POST /mutation/explain", func(t *testing.T) {
691744
res, err := httpPostJSON(
692745
fmt.Sprintf("%s/mutation/explain", httpServer.URL),
746+
nil,
693747
schema.MutationRequest{
694748
Operations: []schema.MutationOperation{},
695749
CollectionRelationships: make(schema.MutationRequestCollectionRelationships),
@@ -708,6 +762,7 @@ func TestServerConnector(t *testing.T) {
708762
t.Run("POST /mutation/explain - json decode failure", func(t *testing.T) {
709763
res, err := httpPostJSON(
710764
fmt.Sprintf("%s/mutation/explain", httpServer.URL),
765+
nil,
711766
map[string]any{},
712767
)
713768
if err != nil {
@@ -727,6 +782,7 @@ func TestServerConnector(t *testing.T) {
727782
res, err := httpPostJSONWithNDCVersion(
728783
fmt.Sprintf("%s/mutation/explain", httpServer.URL),
729784
"unknown",
785+
nil,
730786
map[string]any{},
731787
)
732788
if err != nil {

example/codegen/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ require (
2222
github.com/go-logr/stdr v1.2.2 // indirect
2323
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
2424
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
25+
github.com/klauspost/compress v1.18.0 // indirect
2526
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
2627
github.com/prometheus/client_golang v1.23.0 // indirect
2728
github.com/prometheus/client_model v0.6.2 // indirect

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/go-viper/mapstructure/v2 v2.4.0
1010
github.com/google/go-cmp v0.7.0
1111
github.com/google/uuid v1.6.0
12+
github.com/klauspost/compress v1.18.0
1213
github.com/prometheus/client_golang v1.23.0
1314
github.com/prometheus/common v0.65.0
1415
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0

0 commit comments

Comments
 (0)