From 099c77699a197ec326e2287588664410410e6967 Mon Sep 17 00:00:00 2001 From: joaopandolfi Date: Tue, 23 Jan 2024 17:42:28 -0300 Subject: [PATCH 1/6] defining package and using gzip --- go.mod | 15 +++++++ go.sum | 14 +++++++ graphql.go | 95 ++++++++++++++++++++++++++++++++------------ graphql_gzip_test.go | 53 ++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 26 deletions(-) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 graphql_gzip_test.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c4c8bc6 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module graphql + +go 1.20 + +require ( + github.com/matryer/is v1.4.1 + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.8.4 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a42da6f --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/graphql.go b/graphql.go index 05c29b7..17bdeb9 100644 --- a/graphql.go +++ b/graphql.go @@ -1,37 +1,39 @@ // Package graphql provides a low level GraphQL client. // -// // create a client (safe to share across requests) -// client := graphql.NewClient("https://machinebox.io/graphql") +// // create a client (safe to share across requests) +// client := graphql.NewClient("https://machinebox.io/graphql") // -// // make a request -// req := graphql.NewRequest(` -// query ($key: String!) { -// items (id:$key) { -// field1 -// field2 -// field3 -// } -// } -// `) +// // make a request +// req := graphql.NewRequest(` +// query ($key: String!) { +// items (id:$key) { +// field1 +// field2 +// field3 +// } +// } +// `) // -// // set any variables -// req.Var("key", "value") +// // set any variables +// req.Var("key", "value") // -// // run it and capture the response -// var respData ResponseStruct -// if err := client.Run(ctx, req, &respData); err != nil { -// log.Fatal(err) -// } +// // run it and capture the response +// var respData ResponseStruct +// if err := client.Run(ctx, req, &respData); err != nil { +// log.Fatal(err) +// } // -// Specify client +// # Specify client // // To specify your own http.Client, use the WithHTTPClient option: -// httpclient := &http.Client{} -// client := graphql.NewClient("https://machinebox.io/graphql", graphql.WithHTTPClient(httpclient)) +// +// httpclient := &http.Client{} +// client := graphql.NewClient("https://machinebox.io/graphql", graphql.WithHTTPClient(httpclient)) package graphql import ( "bytes" + "compress/gzip" "context" "encoding/json" "fmt" @@ -47,6 +49,7 @@ type Client struct { endpoint string httpClient *http.Client useMultipartForm bool + useGzip bool // closeReq will close the request body immediately allowing for reuse of client closeReq bool @@ -113,6 +116,17 @@ func (c *Client) runWithJSON(ctx context.Context, req *Request, resp interface{} gr := &graphResponse{ Data: resp, } + + if c.useGzip { + var compressedData bytes.Buffer + gzipBuff := gzip.NewWriter(&compressedData) + if _, err := gzipBuff.Write(requestBody.Bytes()); err != nil { + return errors.Wrap(err, "gzipping body") + } + gzipBuff.Close() + requestBody = compressedData + } + r, err := http.NewRequest(http.MethodPost, c.endpoint, &requestBody) if err != nil { return err @@ -120,6 +134,11 @@ func (c *Client) runWithJSON(ctx context.Context, req *Request, resp interface{} r.Close = c.closeReq r.Header.Set("Content-Type", "application/json; charset=utf-8") r.Header.Set("Accept", "application/json; charset=utf-8") + + if c.useGzip { + r.Header.Set("Content-Encoding", "gzip") + } + for key, values := range req.Header { for _, value := range values { r.Header.Add(key, value) @@ -133,9 +152,25 @@ func (c *Client) runWithJSON(ctx context.Context, req *Request, resp interface{} } defer res.Body.Close() var buf bytes.Buffer - if _, err := io.Copy(&buf, res.Body); err != nil { - return errors.Wrap(err, "reading body") + + if res.Header.Get("Content-Encoding") != "gzip" { + if _, err := io.Copy(&buf, res.Body); err != nil { + return errors.Wrap(err, "reading body") + } + } else { + r, err := gzip.NewReader(res.Body) + if err != nil { + return errors.Wrap(err, "reading gzip body") + } + var resB bytes.Buffer + _, err = resB.ReadFrom(r) + if err != nil { + return errors.Wrap(err, "reading gzip bytes") + } + r.Close() + buf = resB } + c.logf("<< %s", buf.String()) if err := json.NewDecoder(&buf).Decode(&gr); err != nil { if res.StatusCode != http.StatusOK { @@ -223,7 +258,8 @@ func (c *Client) runWithPostFields(ctx context.Context, req *Request, resp inter // WithHTTPClient specifies the underlying http.Client to use when // making requests. -// NewClient(endpoint, WithHTTPClient(specificHTTPClient)) +// +// NewClient(endpoint, WithHTTPClient(specificHTTPClient)) func WithHTTPClient(httpclient *http.Client) ClientOption { return func(client *Client) { client.httpClient = httpclient @@ -238,7 +274,14 @@ func UseMultipartForm() ClientOption { } } -//ImmediatelyCloseReqBody will close the req body immediately after each request body is ready +// UseGzip to perform requiests and reduce the payload +func UseGzip() ClientOption { + return func(client *Client) { + client.useGzip = true + } +} + +// ImmediatelyCloseReqBody will close the req body immediately after each request body is ready func ImmediatelyCloseReqBody() ClientOption { return func(client *Client) { client.closeReq = true diff --git a/graphql_gzip_test.go b/graphql_gzip_test.go new file mode 100644 index 0000000..4c52edb --- /dev/null +++ b/graphql_gzip_test.go @@ -0,0 +1,53 @@ +package graphql + +import ( + "bytes" + "compress/gzip" + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestDoJSONGzipServerError(t *testing.T) { + + var calls int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Header.Get("Content-Encoding"), "gzip") + + calls++ + assert.Equal(t, r.Method, http.MethodPost) + + body, err := io.ReadAll(r.Body) + assert.Nil(t, err) + + compressedData := bytes.NewReader(body) + + reader, err := gzip.NewReader(compressedData) + assert.Nil(t, err) + + decodedData, err := io.ReadAll(reader) + assert.Nil(t, err) + + b := decodedData + + assert.Equal(t, string(b), `{"query":"query {}","variables":null}`+"\n") + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, `Internal Server Error`) + })) + defer srv.Close() + + ctx := context.Background() + client := NewClient(srv.URL, UseGzip()) + + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + var responseData map[string]interface{} + err := client.Run(ctx, &Request{q: "query {}"}, &responseData) + assert.Equal(t, calls, 1) // calls + assert.Equal(t, err.Error(), "graphql: server returned a non-200 status code: 500") +} From 8f6060ad7ead1660ab683e31ee72a93066297a1a Mon Sep 17 00:00:00 2001 From: joaopandolfi Date: Tue, 23 Jan 2024 17:45:56 -0300 Subject: [PATCH 2/6] rename module --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c4c8bc6..51f477c 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module graphql +module github.com/joaopandolfi/graphql go 1.20 From 4a7f7e7bb3f00ca2541201f09ea6d17a03d64f5e Mon Sep 17 00:00:00 2001 From: joaopandolfi Date: Wed, 24 Jan 2024 18:45:08 -0300 Subject: [PATCH 3/6] remove header gzip on response --- graphql.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphql.go b/graphql.go index 17bdeb9..7f505a1 100644 --- a/graphql.go +++ b/graphql.go @@ -153,7 +153,7 @@ func (c *Client) runWithJSON(ctx context.Context, req *Request, resp interface{} defer res.Body.Close() var buf bytes.Buffer - if res.Header.Get("Content-Encoding") != "gzip" { + if !c.useGzip { if _, err := io.Copy(&buf, res.Body); err != nil { return errors.Wrap(err, "reading body") } From b17e640b0744683475713ce7b992d879e2de0189 Mon Sep 17 00:00:00 2001 From: joaopandolfi Date: Wed, 24 Jan 2024 18:49:34 -0300 Subject: [PATCH 4/6] add accept encoding header --- graphql.go | 1 + 1 file changed, 1 insertion(+) diff --git a/graphql.go b/graphql.go index 7f505a1..a4d4d64 100644 --- a/graphql.go +++ b/graphql.go @@ -134,6 +134,7 @@ func (c *Client) runWithJSON(ctx context.Context, req *Request, resp interface{} r.Close = c.closeReq r.Header.Set("Content-Type", "application/json; charset=utf-8") r.Header.Set("Accept", "application/json; charset=utf-8") + r.Header.Set("Accept-Enconding", "gzip") if c.useGzip { r.Header.Set("Content-Encoding", "gzip") From 5cef134812177f50255ca69995a24383c309b5f3 Mon Sep 17 00:00:00 2001 From: joaopandolfi Date: Wed, 24 Jan 2024 18:54:56 -0300 Subject: [PATCH 5/6] encapsuling gzip headers --- graphql.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/graphql.go b/graphql.go index a4d4d64..4461e70 100644 --- a/graphql.go +++ b/graphql.go @@ -132,12 +132,14 @@ func (c *Client) runWithJSON(ctx context.Context, req *Request, resp interface{} return err } r.Close = c.closeReq - r.Header.Set("Content-Type", "application/json; charset=utf-8") - r.Header.Set("Accept", "application/json; charset=utf-8") - r.Header.Set("Accept-Enconding", "gzip") if c.useGzip { r.Header.Set("Content-Encoding", "gzip") + r.Header.Set("Accept-Enconding", "gzip") + r.Header.Set("Accept", "gzip") + } else { + r.Header.Set("Content-Type", "application/json; charset=utf-8") + r.Header.Set("Accept", "application/json; charset=utf-8") } for key, values := range req.Header { From e9b4a42dbed13a6a82a39530819b2bfcd1f83315 Mon Sep 17 00:00:00 2001 From: joaopandolfi Date: Wed, 24 Jan 2024 19:56:41 -0300 Subject: [PATCH 6/6] separating gzip send and receive methods --- graphql.go | 37 ++++++++++++++++++++++++------------- graphql_gzip_test.go | 2 +- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/graphql.go b/graphql.go index 4461e70..ba684b2 100644 --- a/graphql.go +++ b/graphql.go @@ -49,7 +49,8 @@ type Client struct { endpoint string httpClient *http.Client useMultipartForm bool - useGzip bool + sendGzip bool + receiveGzip bool // closeReq will close the request body immediately allowing for reuse of client closeReq bool @@ -116,8 +117,7 @@ func (c *Client) runWithJSON(ctx context.Context, req *Request, resp interface{} gr := &graphResponse{ Data: resp, } - - if c.useGzip { + if c.sendGzip { var compressedData bytes.Buffer gzipBuff := gzip.NewWriter(&compressedData) if _, err := gzipBuff.Write(requestBody.Bytes()); err != nil { @@ -133,13 +133,16 @@ func (c *Client) runWithJSON(ctx context.Context, req *Request, resp interface{} } r.Close = c.closeReq - if c.useGzip { + r.Header.Set("Content-Type", "application/json; charset=utf-8") + r.Header.Set("Accept", "application/json; charset=utf-8") + + if c.sendGzip { r.Header.Set("Content-Encoding", "gzip") - r.Header.Set("Accept-Enconding", "gzip") - r.Header.Set("Accept", "gzip") - } else { - r.Header.Set("Content-Type", "application/json; charset=utf-8") - r.Header.Set("Accept", "application/json; charset=utf-8") + } + + if c.receiveGzip { + r.Header.Set("Accept-Encoding", "deflate, gzip") + r.Header.Set("Accept", "*/*") } for key, values := range req.Header { @@ -153,10 +156,11 @@ func (c *Client) runWithJSON(ctx context.Context, req *Request, resp interface{} if err != nil { return err } + defer res.Body.Close() var buf bytes.Buffer - if !c.useGzip { + if res.Header.Get("Content-Encoding") != "gzip" { if _, err := io.Copy(&buf, res.Body); err != nil { return errors.Wrap(err, "reading body") } @@ -277,10 +281,17 @@ func UseMultipartForm() ClientOption { } } -// UseGzip to perform requiests and reduce the payload -func UseGzip() ClientOption { +// ReceiveGzip to perform requiests parsing payload response in gzip and reduce the payload +func ReceiveGzip() ClientOption { + return func(client *Client) { + client.receiveGzip = true + } +} + +// SendGzip to perform requiests sending body in gzip and reduce the payload +func SendGzip() ClientOption { return func(client *Client) { - client.useGzip = true + client.sendGzip = true } } diff --git a/graphql_gzip_test.go b/graphql_gzip_test.go index 4c52edb..4ca0f6d 100644 --- a/graphql_gzip_test.go +++ b/graphql_gzip_test.go @@ -42,7 +42,7 @@ func TestDoJSONGzipServerError(t *testing.T) { defer srv.Close() ctx := context.Background() - client := NewClient(srv.URL, UseGzip()) + client := NewClient(srv.URL, SendGzip(), ReceiveGzip()) ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel()