Skip to content

Commit 2ee4206

Browse files
Merge #570
570: Feat support content encoding r=curquiza a=Ja7ad # Pull Request ## Related issue Fixes #566 ## What does this PR do? This PR support content encoding `gzip`, `deflate`, `brotli`. ```go client := meilisearch.New("127.0.0.1:7700", meilisearch.WithContentEncoding(meilisearch.GzipEncoding, meilisearch.DefaultCompression)) ``` Note: This PR is not breaking changes. ## PR checklist Please check if your PR fulfills the following requirements: - [x] Does this PR fix an existing issue, or have you listed the changes applied in the PR description (and why they are needed)? - [x] Have you read the contributing guidelines? - [x] Have you made sure that the title is accurate and descriptive of the changes? Thank you so much for contributing to Meilisearch! Co-authored-by: Javad <[email protected]> Co-authored-by: Javad Rajabzadeh <[email protected]>
2 parents 63161f2 + 5ecb35a commit 2ee4206

14 files changed

+1390
-45
lines changed

client.go

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,18 @@ import (
1313
)
1414

1515
type client struct {
16-
client *http.Client
17-
host string
18-
apiKey string
19-
bufferPool *sync.Pool
16+
client *http.Client
17+
host string
18+
apiKey string
19+
bufferPool *sync.Pool
20+
encoder encoder
21+
contentEncoding ContentEncoding
2022
}
2123

2224
type internalRequest struct {
23-
endpoint string
24-
method string
25-
contentType string
26-
25+
endpoint string
26+
method string
27+
contentType string
2728
withRequest interface{}
2829
withResponse interface{}
2930
withQueryParams map[string]string
@@ -33,8 +34,8 @@ type internalRequest struct {
3334
functionName string
3435
}
3536

36-
func newClient(cli *http.Client, host, apiKey string) *client {
37-
return &client{
37+
func newClient(cli *http.Client, host, apiKey string, ce ContentEncoding, cl EncodingCompressionLevel) *client {
38+
c := &client{
3839
client: cli,
3940
host: host,
4041
apiKey: apiKey,
@@ -44,6 +45,13 @@ func newClient(cli *http.Client, host, apiKey string) *client {
4445
},
4546
},
4647
}
48+
49+
if !ce.IsZero() {
50+
c.contentEncoding = ce
51+
c.encoder = newEncoding(ce, cl)
52+
}
53+
54+
return c
4755
}
4856

4957
func (c *client) executeRequest(ctx context.Context, req *internalRequest) error {
@@ -57,6 +65,7 @@ func (c *client) executeRequest(ctx context.Context, req *internalRequest) error
5765
Message: "empty meilisearch message",
5866
},
5967
StatusCodeExpected: req.acceptedStatusCodes,
68+
encoder: c.encoder,
6069
}
6170

6271
resp, err := c.sendRequest(ctx, req, internalError)
@@ -118,10 +127,11 @@ func (c *client) sendRequest(
118127
}
119128

120129
rawRequest := req.withRequest
130+
131+
buf := c.bufferPool.Get().(*bytes.Buffer)
132+
buf.Reset()
133+
121134
if b, ok := rawRequest.([]byte); ok {
122-
// If the request body is already a []byte then use it directly
123-
buf := c.bufferPool.Get().(*bytes.Buffer)
124-
buf.Reset()
125135
buf.Write(b)
126136
body = buf
127137
} else if reader, ok := rawRequest.(io.Reader); ok {
@@ -136,22 +146,31 @@ func (c *client) sendRequest(
136146
if marshaler, ok := rawRequest.(json.Marshaler); ok {
137147
data, err = marshaler.MarshalJSON()
138148
if err != nil {
139-
return nil, internalError.WithErrCode(ErrCodeMarshalRequest, fmt.Errorf("failed to marshal with MarshalJSON: %w", err))
149+
return nil, internalError.WithErrCode(ErrCodeMarshalRequest,
150+
fmt.Errorf("failed to marshal with MarshalJSON: %w", err))
140151
}
141152
if data == nil {
142-
return nil, internalError.WithErrCode(ErrCodeMarshalRequest, errors.New("MarshalJSON returned nil data"))
153+
return nil, internalError.WithErrCode(ErrCodeMarshalRequest,
154+
errors.New("MarshalJSON returned nil data"))
143155
}
144156
} else {
145157
data, err = json.Marshal(rawRequest)
146158
if err != nil {
147-
return nil, internalError.WithErrCode(ErrCodeMarshalRequest, fmt.Errorf("failed to marshal with json.Marshal: %w", err))
159+
return nil, internalError.WithErrCode(ErrCodeMarshalRequest,
160+
fmt.Errorf("failed to marshal with json.Marshal: %w", err))
148161
}
149162
}
150-
buf := c.bufferPool.Get().(*bytes.Buffer)
151-
buf.Reset()
152163
buf.Write(data)
153164
body = buf
154165
}
166+
167+
if !c.contentEncoding.IsZero() {
168+
body, err = c.encoder.Encode(body)
169+
if err != nil {
170+
return nil, internalError.WithErrCode(ErrCodeMarshalRequest,
171+
fmt.Errorf("failed to marshal with json.Marshal: %w", err))
172+
}
173+
}
155174
}
156175

157176
// Create the HTTP request
@@ -168,6 +187,14 @@ func (c *client) sendRequest(
168187
request.Header.Set("Authorization", "Bearer "+c.apiKey)
169188
}
170189

190+
if req.withResponse != nil && !c.contentEncoding.IsZero() {
191+
request.Header.Set("Accept-Encoding", c.contentEncoding.String())
192+
}
193+
194+
if req.withRequest != nil && !c.contentEncoding.IsZero() {
195+
request.Header.Set("Content-Encoding", c.contentEncoding.String())
196+
}
197+
171198
request.Header.Set("User-Agent", GetQualifiedVersion())
172199

173200
resp, err := c.client.Do(request)
@@ -210,18 +237,23 @@ func (c *client) handleStatusCode(req *internalRequest, statusCode int, body []b
210237

211238
func (c *client) handleResponse(req *internalRequest, body []byte, internalError *Error) (err error) {
212239
if req.withResponse != nil {
213-
214-
internalError.ResponseToString = string(body)
215-
216-
var err error
217-
if resp, ok := req.withResponse.(json.Unmarshaler); ok {
218-
err = resp.UnmarshalJSON(body)
219-
req.withResponse = resp
240+
if !c.contentEncoding.IsZero() {
241+
if err := c.encoder.Decode(body, req.withResponse); err != nil {
242+
return internalError.WithErrCode(ErrCodeResponseUnmarshalBody, err)
243+
}
220244
} else {
221-
err = json.Unmarshal(body, req.withResponse)
222-
}
223-
if err != nil {
224-
return internalError.WithErrCode(ErrCodeResponseUnmarshalBody, err)
245+
internalError.ResponseToString = string(body)
246+
247+
var err error
248+
if resp, ok := req.withResponse.(json.Unmarshaler); ok {
249+
err = resp.UnmarshalJSON(body)
250+
req.withResponse = resp
251+
} else {
252+
err = json.Unmarshal(body, req.withResponse)
253+
}
254+
if err != nil {
255+
return internalError.WithErrCode(ErrCodeResponseUnmarshalBody, err)
256+
}
225257
}
226258
}
227259
return nil

client_test.go

Lines changed: 162 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package meilisearch
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
67
"errors"
78
"github.com/stretchr/testify/require"
9+
"io"
810
"net/http"
911
"net/http/httptest"
1012
"strings"
@@ -29,9 +31,65 @@ func TestExecuteRequest(t *testing.T) {
2931
if r.Method == http.MethodGet && r.URL.Path == "/test-get" {
3032
w.WriteHeader(http.StatusOK)
3133
_, _ = w.Write([]byte(`{"message":"get successful"}`))
34+
} else if r.Method == http.MethodGet && r.URL.Path == "/test-get-encoding" {
35+
encode := r.Header.Get("Accept-Encoding")
36+
if len(encode) != 0 {
37+
enc := newEncoding(ContentEncoding(encode), DefaultCompression)
38+
d := &mockData{Name: "foo", Age: 30}
39+
40+
b, err := json.Marshal(d)
41+
require.NoError(t, err)
42+
43+
res, err := enc.Encode(bytes.NewReader(b))
44+
require.NoError(t, err)
45+
_, _ = w.Write(res.Bytes())
46+
w.WriteHeader(http.StatusOK)
47+
return
48+
}
49+
_, _ = w.Write([]byte("invalid message"))
50+
w.WriteHeader(http.StatusInternalServerError)
51+
} else if r.Method == http.MethodPost && r.URL.Path == "/test-req-resp-encoding" {
52+
accept := r.Header.Get("Accept-Encoding")
53+
ce := r.Header.Get("Content-Encoding")
54+
55+
reqEnc := newEncoding(ContentEncoding(ce), DefaultCompression)
56+
respEnc := newEncoding(ContentEncoding(accept), DefaultCompression)
57+
req := new(mockData)
58+
59+
if len(ce) != 0 {
60+
b, err := io.ReadAll(r.Body)
61+
require.NoError(t, err)
62+
63+
err = reqEnc.Decode(b, req)
64+
require.NoError(t, err)
65+
}
66+
67+
if len(accept) != 0 {
68+
d, err := json.Marshal(req)
69+
require.NoError(t, err)
70+
res, err := respEnc.Encode(bytes.NewReader(d))
71+
require.NoError(t, err)
72+
_, _ = w.Write(res.Bytes())
73+
w.WriteHeader(http.StatusOK)
74+
}
3275
} else if r.Method == http.MethodPost && r.URL.Path == "/test-post" {
3376
w.WriteHeader(http.StatusCreated)
34-
_, _ = w.Write([]byte(`{"message":"post successful"}`))
77+
msg := []byte(`{"message":"post successful"}`)
78+
_, _ = w.Write(msg)
79+
80+
} else if r.Method == http.MethodPost && r.URL.Path == "/test-post-encoding" {
81+
w.WriteHeader(http.StatusCreated)
82+
msg := []byte(`{"message":"post successful"}`)
83+
84+
enc := r.Header.Get("Accept-Encoding")
85+
if len(enc) != 0 {
86+
e := newEncoding(ContentEncoding(enc), DefaultCompression)
87+
b, err := e.Encode(bytes.NewReader(msg))
88+
require.NoError(t, err)
89+
_, _ = w.Write(b.Bytes())
90+
return
91+
}
92+
_, _ = w.Write(msg)
3593
} else if r.URL.Path == "/test-bad-request" {
3694
w.WriteHeader(http.StatusBadRequest)
3795
_, _ = w.Write([]byte(`{"message":"bad request"}`))
@@ -47,13 +105,12 @@ func TestExecuteRequest(t *testing.T) {
47105
}))
48106
defer ts.Close()
49107

50-
client := newClient(&http.Client{}, ts.URL, "testApiKey")
51-
52108
tests := []struct {
53-
name string
54-
internalReq *internalRequest
55-
expectedResp interface{}
56-
wantErr bool
109+
name string
110+
internalReq *internalRequest
111+
expectedResp interface{}
112+
contentEncoding ContentEncoding
113+
wantErr bool
57114
}{
58115
{
59116
name: "Successful GET request",
@@ -190,11 +247,108 @@ func TestExecuteRequest(t *testing.T) {
190247
expectedResp: nil,
191248
wantErr: true,
192249
},
250+
{
251+
name: "Test request encoding gzip",
252+
internalReq: &internalRequest{
253+
endpoint: "/test-post-encoding",
254+
method: http.MethodPost,
255+
withRequest: map[string]string{"key": "value"},
256+
contentType: contentTypeJSON,
257+
withResponse: &mockResponse{},
258+
acceptedStatusCodes: []int{http.StatusCreated},
259+
},
260+
expectedResp: &mockResponse{Message: "post successful"},
261+
contentEncoding: GzipEncoding,
262+
wantErr: false,
263+
},
264+
{
265+
name: "Test request encoding deflate",
266+
internalReq: &internalRequest{
267+
endpoint: "/test-post-encoding",
268+
method: http.MethodPost,
269+
withRequest: map[string]string{"key": "value"},
270+
contentType: contentTypeJSON,
271+
withResponse: &mockResponse{},
272+
acceptedStatusCodes: []int{http.StatusCreated},
273+
},
274+
expectedResp: &mockResponse{Message: "post successful"},
275+
contentEncoding: DeflateEncoding,
276+
wantErr: false,
277+
},
278+
{
279+
name: "Test request encoding brotli",
280+
internalReq: &internalRequest{
281+
endpoint: "/test-post-encoding",
282+
method: http.MethodPost,
283+
withRequest: map[string]string{"key": "value"},
284+
contentType: contentTypeJSON,
285+
withResponse: &mockResponse{},
286+
acceptedStatusCodes: []int{http.StatusCreated},
287+
},
288+
expectedResp: &mockResponse{Message: "post successful"},
289+
contentEncoding: BrotliEncoding,
290+
wantErr: false,
291+
},
292+
{
293+
name: "Test response decoding gzip",
294+
internalReq: &internalRequest{
295+
endpoint: "/test-get-encoding",
296+
method: http.MethodGet,
297+
withRequest: nil,
298+
withResponse: &mockData{},
299+
acceptedStatusCodes: []int{http.StatusOK},
300+
},
301+
expectedResp: &mockData{Name: "foo", Age: 30},
302+
contentEncoding: GzipEncoding,
303+
wantErr: false,
304+
},
305+
{
306+
name: "Test response decoding deflate",
307+
internalReq: &internalRequest{
308+
endpoint: "/test-get-encoding",
309+
method: http.MethodGet,
310+
withRequest: nil,
311+
withResponse: &mockData{},
312+
acceptedStatusCodes: []int{http.StatusOK},
313+
},
314+
expectedResp: &mockData{Name: "foo", Age: 30},
315+
contentEncoding: DeflateEncoding,
316+
wantErr: false,
317+
},
318+
{
319+
name: "Test response decoding brotli",
320+
internalReq: &internalRequest{
321+
endpoint: "/test-get-encoding",
322+
method: http.MethodGet,
323+
withRequest: nil,
324+
withResponse: &mockData{},
325+
acceptedStatusCodes: []int{http.StatusOK},
326+
},
327+
expectedResp: &mockData{Name: "foo", Age: 30},
328+
contentEncoding: BrotliEncoding,
329+
wantErr: false,
330+
},
331+
{
332+
name: "Test request and response encoding",
333+
internalReq: &internalRequest{
334+
endpoint: "/test-req-resp-encoding",
335+
method: http.MethodPost,
336+
contentType: contentTypeJSON,
337+
withRequest: &mockData{Name: "foo", Age: 30},
338+
withResponse: &mockData{},
339+
acceptedStatusCodes: []int{http.StatusOK},
340+
},
341+
expectedResp: &mockData{Name: "foo", Age: 30},
342+
contentEncoding: GzipEncoding,
343+
wantErr: false,
344+
},
193345
}
194346

195347
for _, tt := range tests {
196348
t.Run(tt.name, func(t *testing.T) {
197-
err := client.executeRequest(context.Background(), tt.internalReq)
349+
c := newClient(&http.Client{}, ts.URL, "testApiKey", tt.contentEncoding, DefaultCompression)
350+
351+
err := c.executeRequest(context.Background(), tt.internalReq)
198352
if tt.wantErr {
199353
require.Error(t, err)
200354
} else {

0 commit comments

Comments
 (0)