Skip to content

Commit 1bcfc83

Browse files
brbrrrodrodros
andcommitted
feat(clients/gateway): Add content compression to gateway POSTs (#3505)
* feat(gateway): Add content compression to gateway POSTs * feat: gzip-compress payloads > 1KB * refactor: extract gzip code into a function * refactor(clients): some minor improvmenets --------- Co-authored-by: Rodrigo <rodrodpino@gmail.com>
1 parent 0f4e28f commit 1bcfc83

File tree

2 files changed

+81
-4
lines changed

2 files changed

+81
-4
lines changed

clients/gateway/gateway.go

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package gateway
22

33
import (
44
"bytes"
5+
"compress/gzip"
56
"context"
67
"encoding/json"
78
"errors"
@@ -39,6 +40,9 @@ var (
3940
InvalidProof ErrorCode = "StarknetErrorCode.INVALID_PROOF"
4041
)
4142

43+
// Payload size threshold for Gzip compression. 1KB
44+
const gzipMinSize = 1024
45+
4246
type Client struct {
4347
url string
4448
client *http.Client
@@ -79,13 +83,31 @@ func newTestServer(t *testing.T) *httptest.Server {
7983
assert.Equal(t, []string{"API_KEY"}, r.Header["X-Throttling-Bypass"])
8084
assert.Equal(t, []string{"Juno/v0.0.1-test Starknet Implementation"}, r.Header["User-Agent"])
8185

82-
b, err := io.ReadAll(r.Body)
86+
var bodyReader io.Reader = r.Body
87+
isGzip := r.Header.Get("Content-Encoding") == "gzip"
88+
if isGzip {
89+
gzReader, gzErr := gzip.NewReader(r.Body)
90+
if gzErr != nil {
91+
w.WriteHeader(http.StatusBadRequest)
92+
w.Write([]byte(gzErr.Error())) //nolint:errcheck // That's fine for the test server
93+
return
94+
}
95+
defer gzReader.Close()
96+
bodyReader = gzReader
97+
}
98+
99+
b, err := io.ReadAll(bodyReader)
83100
if err != nil {
84101
w.WriteHeader(http.StatusBadRequest)
85102
w.Write([]byte(err.Error())) //nolint:errcheck
86103
return
87104
}
88105

106+
// Assert that large payloads are gzip-compressed
107+
if len(b) >= gzipMinSize {
108+
assert.True(t, isGzip, "expected Content-Encoding: gzip for payload of size %d", len(b))
109+
}
110+
89111
// empty request: "{}"
90112
emptyReqLen := 4
91113
if string(b) == "null" {
@@ -97,7 +119,8 @@ func newTestServer(t *testing.T) *httptest.Server {
97119
return
98120
}
99121

100-
hash := new(felt.Felt).SetBytes([]byte("random"))
122+
// todo(rdr): consider using a random generator here
123+
hash := felt.FromBytes[felt.Felt]([]byte("random"))
101124
resp := fmt.Sprintf("{\"code\": \"TRANSACTION_RECEIVED\", \"transaction_hash\": %q, \"address\": %q}", hash.String(), hash.String())
102125
w.Write([]byte(resp)) //nolint:errcheck
103126
}))
@@ -147,17 +170,25 @@ func (c *Client) post(ctx context.Context, url string, data any) ([]byte, error)
147170
// doPost performs a "POST" http request with the given URL and a JSON payload derived from the provided data
148171
// it returns response without additional error handling
149172
func (c *Client) doPost(ctx context.Context, url string, data any) (*http.Response, error) {
150-
body, err := json.Marshal(data)
173+
jsonBody, err := json.Marshal(data)
174+
if err != nil {
175+
return nil, err
176+
}
177+
178+
bodyReader, compressed, err := prepareRequestBody(jsonBody)
151179
if err != nil {
152180
return nil, err
153181
}
154182

155-
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
183+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bodyReader)
156184
if err != nil {
157185
return nil, err
158186
}
159187

160188
req.Header.Set("Content-Type", "application/json")
189+
if compressed {
190+
req.Header.Set("Content-Encoding", "gzip")
191+
}
161192
if c.userAgent != "" {
162193
req.Header.Set("User-Agent", c.userAgent)
163194
}
@@ -173,6 +204,23 @@ func (c *Client) doPost(ctx context.Context, url string, data any) (*http.Respon
173204
return resp, nil
174205
}
175206

207+
func prepareRequestBody(jsonBody []byte) (io.Reader, bool, error) {
208+
if len(jsonBody) <= gzipMinSize {
209+
return bytes.NewReader(jsonBody), false, nil
210+
}
211+
212+
var buf bytes.Buffer
213+
gzWriter := gzip.NewWriter(&buf)
214+
if _, err := gzWriter.Write(jsonBody); err != nil {
215+
return nil, false, fmt.Errorf("writing gzip content: %w", err)
216+
}
217+
if err := gzWriter.Close(); err != nil {
218+
return nil, false, fmt.Errorf("closing gzip writer: %w", err)
219+
}
220+
221+
return &buf, true, nil
222+
}
223+
176224
type ErrorCode string
177225

178226
type Error struct {

clients/gateway/gateway_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package gateway_test
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"testing"
67

78
"github.com/NethermindEth/juno/clients/gateway"
@@ -25,6 +26,34 @@ func TestAddInvokeTx(t *testing.T) {
2526
assert.NoError(t, err)
2627
})
2728

29+
t.Run("Large requests are gzip compressed", func(t *testing.T) {
30+
largeCalldata := make([]string, 200)
31+
for i := range largeCalldata {
32+
largeCalldata[i] = "0xcafebabe"
33+
}
34+
35+
calldataJSON, err := json.Marshal(largeCalldata)
36+
require.NoError(t, err)
37+
38+
invokeTx := fmt.Sprintf(
39+
`{
40+
"max_fee":"0x1",
41+
"version":"0x1",
42+
"signature":[],
43+
"nonce":"0x1",
44+
"type":"INVOKE",
45+
"sender_address":"0x326e3db4580b94948ca9d1d87fa359f2fa047a31a34757734a86aa4231fb9bb",
46+
"calldata": %s
47+
}`,
48+
calldataJSON,
49+
)
50+
invokeTxBytes, err := json.Marshal(invokeTx)
51+
require.NoError(t, err)
52+
53+
_, err = client.AddTransaction(t.Context(), invokeTxBytes)
54+
assert.NoError(t, err)
55+
})
56+
2857
t.Run("Incorrect empty request", func(t *testing.T) {
2958
invokeTx := "{}"
3059
invokeTxByte, err := json.Marshal(invokeTx)

0 commit comments

Comments
 (0)