Skip to content

Commit

Permalink
Merge pull request #344 from supertokens/feat/rate-limiting-12
Browse files Browse the repository at this point in the history
refactor: Add handling for when Saas returns 429 because of rate limiting
  • Loading branch information
rishabhpoddar authored Aug 29, 2023
2 parents 930f2fb + 652a13d commit 8ad1877
Show file tree
Hide file tree
Showing 4 changed files with 340 additions and 10 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

## [0.12.11] - 2023-08-28

- Adds logic to retry network calls if the core returns status 429

## [0.12.10] - 2023-07-31

- Fixes error handling with regenerate access token when the access token of the session is revoked.
Expand Down
285 changes: 285 additions & 0 deletions recipe/session/querier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
package session

import (
"encoding/json"
"errors"
"github.com/stretchr/testify/assert"
"github.com/supertokens/supertokens-golang/supertokens"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
)

func resetQuerier() {
supertokens.SetQuerierApiVersionForTests("")
}

func TestThatNetworkCallIsRetried(t *testing.T) {
resetAll()
mux := http.NewServeMux()

numberOfTimesCalled := 0
numberOfTimesSecondCalled := 0
numberOfTimesThirdCalled := 0

mux.HandleFunc("/testing", func(rw http.ResponseWriter, r *http.Request) {
numberOfTimesCalled++
rw.WriteHeader(supertokens.RateLimitStatusCode)
rw.Header().Set("Content-Type", "application/json")
response, err := json.Marshal(map[string]interface{}{})
if err != nil {
t.Error(err.Error())
}
rw.Write(response)
})

mux.HandleFunc("/testing2", func(rw http.ResponseWriter, r *http.Request) {
numberOfTimesSecondCalled++
rw.Header().Set("Content-Type", "application/json")

if numberOfTimesSecondCalled == 3 {
rw.WriteHeader(200)
} else {
rw.WriteHeader(supertokens.RateLimitStatusCode)
}

response, err := json.Marshal(map[string]interface{}{})
if err != nil {
t.Error(err.Error())
}
rw.Write(response)
})

mux.HandleFunc("/testing3", func(rw http.ResponseWriter, r *http.Request) {
numberOfTimesThirdCalled++
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(200)
response, err := json.Marshal(map[string]interface{}{})
if err != nil {
t.Error(err.Error())
}
rw.Write(response)
})

testServer := httptest.NewServer(mux)

defer func() {
testServer.Close()
}()

config := supertokens.TypeInput{
Supertokens: &supertokens.ConnectionInfo{
// We need the querier to call the test server and not the core
ConnectionURI: testServer.URL,
},
AppInfo: supertokens.AppInfo{
AppName: "SuperTokens",
WebsiteDomain: "supertokens.io",
APIDomain: "api.supertokens.io",
},
RecipeList: []supertokens.Recipe{
Init(nil),
},
}

err := supertokens.Init(config)

if err != nil {
t.Error(err.Error())
}

q, err := supertokens.GetNewQuerierInstanceOrThrowError("")
supertokens.SetQuerierApiVersionForTests("3.0")
defer resetQuerier()

if err != nil {
t.Error(err.Error())
}

_, err = q.SendGetRequest("/testing", map[string]string{})
if err == nil {
t.Error(errors.New("request should have failed but didnt").Error())
} else {
if !strings.Contains(err.Error(), "with status code: 429") {
t.Error(errors.New("request failed with an unexpected error").Error())
}
}

_, err = q.SendGetRequest("/testing2", map[string]string{})
if err != nil {
t.Error(err.Error())
}

_, err = q.SendGetRequest("/testing3", map[string]string{})
if err != nil {
t.Error(err.Error())
}

// One initial call + 5 retries
assert.Equal(t, numberOfTimesCalled, 6)
assert.Equal(t, numberOfTimesSecondCalled, 3)
assert.Equal(t, numberOfTimesThirdCalled, 1)
}

func TestThatRateLimitErrorsAreThrownBackToTheUser(t *testing.T) {
resetAll()
mux := http.NewServeMux()

mux.HandleFunc("/testing", func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(supertokens.RateLimitStatusCode)
rw.Header().Set("Content-Type", "application/json")
response, err := json.Marshal(map[string]interface{}{
"status": "RATE_LIMIT_ERROR",
})
if err != nil {
t.Error(err.Error())
}
rw.Write(response)
})

testServer := httptest.NewServer(mux)

defer func() {
testServer.Close()
}()

config := supertokens.TypeInput{
Supertokens: &supertokens.ConnectionInfo{
// We need the querier to call the test server and not the core
ConnectionURI: testServer.URL,
},
AppInfo: supertokens.AppInfo{
AppName: "SuperTokens",
WebsiteDomain: "supertokens.io",
APIDomain: "api.supertokens.io",
},
RecipeList: []supertokens.Recipe{
Init(nil),
},
}

err := supertokens.Init(config)

if err != nil {
t.Error(err.Error())
}

q, err := supertokens.GetNewQuerierInstanceOrThrowError("")
supertokens.SetQuerierApiVersionForTests("3.0")
defer resetQuerier()

if err != nil {
t.Error(err.Error())
}

_, err = q.SendGetRequest("/testing", map[string]string{})
if err == nil {
t.Error(errors.New("request should have failed but didnt").Error())
} else {
if !strings.Contains(err.Error(), "with status code: 429") {
t.Error(errors.New("request failed with an unexpected error").Error())
}

assert.True(t, strings.Contains(err.Error(), "message: {\"status\":\"RATE_LIMIT_ERROR\"}"))
}
}

func TestThatParallelCallsHaveIndependentRetryCounters(t *testing.T) {
resetAll()
mux := http.NewServeMux()

numberOfTimesFirstCalled := 0
numberOfTimesSecondCalled := 0

mux.HandleFunc("/testing", func(rw http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("id") == "1" {
numberOfTimesFirstCalled++
} else {
numberOfTimesSecondCalled++
}

rw.WriteHeader(supertokens.RateLimitStatusCode)
rw.Header().Set("Content-Type", "application/json")
response, err := json.Marshal(map[string]interface{}{})
if err != nil {
t.Error(err.Error())
}
rw.Write(response)
})

testServer := httptest.NewServer(mux)

defer func() {
testServer.Close()
}()

config := supertokens.TypeInput{
Supertokens: &supertokens.ConnectionInfo{
// We need the querier to call the test server and not the core
ConnectionURI: testServer.URL,
},
AppInfo: supertokens.AppInfo{
AppName: "SuperTokens",
WebsiteDomain: "supertokens.io",
APIDomain: "api.supertokens.io",
},
RecipeList: []supertokens.Recipe{
Init(nil),
},
}

err := supertokens.Init(config)

if err != nil {
t.Error(err.Error())
}

q, err := supertokens.GetNewQuerierInstanceOrThrowError("")
supertokens.SetQuerierApiVersionForTests("3.0")
defer resetQuerier()

if err != nil {
t.Error(err.Error())
}

var wg sync.WaitGroup

wg.Add(2)

go func() {
_, err = q.SendGetRequest("/testing", map[string]string{
"id": "1",
})
if err == nil {
t.Error(errors.New("request should have failed but didnt").Error())
} else {
if !strings.Contains(err.Error(), "with status code: 429") {
t.Error(errors.New("request failed with an unexpected error").Error())
}
}

wg.Done()
}()

go func() {
_, err = q.SendGetRequest("/testing", map[string]string{
"id": "2",
})
if err == nil {
t.Error(errors.New("request should have failed but didnt").Error())
} else {
if !strings.Contains(err.Error(), "with status code: 429") {
t.Error(errors.New("request failed with an unexpected error").Error())
}
}

wg.Done()
}()

wg.Wait()

assert.Equal(t, numberOfTimesFirstCalled, 6)
assert.Equal(t, numberOfTimesSecondCalled, 6)
}
7 changes: 5 additions & 2 deletions supertokens/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ const (
HeaderFDI = "fdi-version"
)

// VERSION current version of the lib
const VERSION = "0.12.10"
// VERSION current version
//of the lib
const VERSION = "0.12.11"

var (
cdiSupported = []string{"2.21"}
)

const DashboardVersion = "0.6"

const RateLimitStatusCode = 429
Loading

0 comments on commit 8ad1877

Please sign in to comment.