Repository: newrelic/newrelic-client-go
Pull request #899 (2022-06-02 14:38:43)

Retry condition for GraphQL API error: TOO_MANY_REQUESTS 
Add retry condition for **GraphQL** `TOO_MANY_REQUESTS` error response on `json`.


Patch:
diff --git a/go.mod b/go.mod
index 7b4041e..b65741a 100644
--- a/go.mod
+++ b/go.mod
@@ -19,6 +19,7 @@ require (
 	github.com/kr/text v0.2.0 // indirect
 	github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/valyala/fastjson v1.6.3 // indirect
 	golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect
 	gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index 9d0c036..90f1609 100644
--- a/go.sum
+++ b/go.sum
@@ -37,6 +37,8 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
+github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc=
+github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/internal/http/retry.go b/internal/http/retry.go
index 8951217..70cc886 100644
--- a/internal/http/retry.go
+++ b/internal/http/retry.go
@@ -1,11 +1,15 @@
 package http
 
 import (
+	"bytes"
 	"context"
 	"crypto/x509"
+	"io"
 	"net/http"
 	"net/url"
 	"regexp"
+
+	"github.com/valyala/fastjson"
 )
 
 var (
@@ -65,5 +69,17 @@ func RetryPolicy(ctx context.Context, resp *http.Response, err error) (bool, err
 		return true, nil
 	}
 
+	// Check for json response and description for retries.
+	// i.e.: when receiving TOO_MANY_REQUESTS it is a HTTP 200 code with an
+	// error status that needed to be retried.
+	json, jErr := io.ReadAll(resp.Body)
+	resp.Body = io.NopCloser(bytes.NewBuffer(json))
+	if jErr == nil {
+		errorClass := fastjson.GetString(json, "errors", "0", "extensions", "errorClass")
+		if errorClass == ErrClassTooManyRequests.Error() {
+			return true, ErrClassTooManyRequests
+		}
+	}
+
 	return false, nil
 }
diff --git a/internal/http/types.go b/internal/http/types.go
index 0e244ee..f87cedd 100644
--- a/internal/http/types.go
+++ b/internal/http/types.go
@@ -7,4 +7,8 @@ import (
 var (
 	// ErrNotFound is returned when the resource was not found in New Relic.
 	ErrNotFound = errors.New("newrelic: Resource not found")
+
+	// ErrClassTooManyRequests is returned in json messages when client is sending more RPM than the service limit.
+	// (RPM = Requests Per Minute)
+	ErrClassTooManyRequests = errors.New("TOO_MANY_REQUESTS")
 )


Test patch:
diff --git a/internal/http/client_test.go b/internal/http/client_test.go
index 4769a20..361b0e4 100644
--- a/internal/http/client_test.go
+++ b/internal/http/client_test.go
@@ -352,6 +352,24 @@ func TestErrNotFound(t *testing.T) {
 	assert.IsType(t, &errors.NotFound{}, err)
 }
 
+func TestRetryOnNerdGraphTooManyRequests(t *testing.T) {
+	t.Parallel()
+	attempts := 0
+	c := NewTestAPIClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		_, _ = w.Write([]byte(`{"errors":[{"message": "some error", "extensions":{"errorClass":"TOO_MANY_REQUESTS"}}]}`))
+		attempts++
+	}))
+
+	c.client.RetryWaitMax = 10 * time.Millisecond
+	c.errorValue = &GraphQLErrorResponse{}
+	_, err := c.Get(c.config.Region().NerdGraphURL("graphql"), nil, nil)
+
+	assert.Equal(t, 4, attempts)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), ErrClassTooManyRequests.Error())
+}
+
 func TestRetryOnNerdGraphTimeout(t *testing.T) {
 	t.Parallel()
 	attempts := 0