Skip to content

Commit 9ce781f

Browse files
Merge pull request #343 from supertokens/feat/rate-limiting
refactor: Add handling for when Saas returns 429 because of rate limiting
2 parents 7c98fee + 8614ee5 commit 9ce781f

File tree

4 files changed

+334
-9
lines changed

4 files changed

+334
-9
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [unreleased]
99

10+
## [0.13.2] - 2023-08-28
11+
12+
- Adds logic to retry network calls if the core returns status 429
13+
1014
## [0.13.1] - 2023-08-24
1115

1216
- Fixes login methods API to return empty provider array instead of `null`

recipe/session/querier_test.go

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
package session
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"github.com/stretchr/testify/assert"
7+
"github.com/supertokens/supertokens-golang/supertokens"
8+
"net/http"
9+
"net/http/httptest"
10+
"strings"
11+
"sync"
12+
"testing"
13+
)
14+
15+
func resetQuerier() {
16+
supertokens.SetQuerierApiVersionForTests("")
17+
}
18+
19+
func TestThatNetworkCallIsRetried(t *testing.T) {
20+
mux := http.NewServeMux()
21+
22+
numberOfTimesCalled := 0
23+
numberOfTimesSecondCalled := 0
24+
numberOfTimesThirdCalled := 0
25+
26+
mux.HandleFunc("/testing", func(rw http.ResponseWriter, r *http.Request) {
27+
numberOfTimesCalled++
28+
rw.WriteHeader(supertokens.RateLimitStatusCode)
29+
rw.Header().Set("Content-Type", "application/json")
30+
response, err := json.Marshal(map[string]interface{}{})
31+
if err != nil {
32+
t.Error(err.Error())
33+
}
34+
rw.Write(response)
35+
})
36+
37+
mux.HandleFunc("/testing2", func(rw http.ResponseWriter, r *http.Request) {
38+
numberOfTimesSecondCalled++
39+
rw.Header().Set("Content-Type", "application/json")
40+
41+
if numberOfTimesSecondCalled == 3 {
42+
rw.WriteHeader(200)
43+
} else {
44+
rw.WriteHeader(supertokens.RateLimitStatusCode)
45+
}
46+
47+
response, err := json.Marshal(map[string]interface{}{})
48+
if err != nil {
49+
t.Error(err.Error())
50+
}
51+
rw.Write(response)
52+
})
53+
54+
mux.HandleFunc("/testing3", func(rw http.ResponseWriter, r *http.Request) {
55+
numberOfTimesThirdCalled++
56+
rw.Header().Set("Content-Type", "application/json")
57+
rw.WriteHeader(200)
58+
response, err := json.Marshal(map[string]interface{}{})
59+
if err != nil {
60+
t.Error(err.Error())
61+
}
62+
rw.Write(response)
63+
})
64+
65+
testServer := httptest.NewServer(mux)
66+
67+
defer func() {
68+
testServer.Close()
69+
}()
70+
71+
config := supertokens.TypeInput{
72+
Supertokens: &supertokens.ConnectionInfo{
73+
// We need the querier to call the test server and not the core
74+
ConnectionURI: testServer.URL,
75+
},
76+
AppInfo: supertokens.AppInfo{
77+
AppName: "SuperTokens",
78+
WebsiteDomain: "supertokens.io",
79+
APIDomain: "api.supertokens.io",
80+
},
81+
RecipeList: []supertokens.Recipe{
82+
Init(nil),
83+
},
84+
}
85+
86+
err := supertokens.Init(config)
87+
88+
if err != nil {
89+
t.Error(err.Error())
90+
}
91+
92+
q, err := supertokens.GetNewQuerierInstanceOrThrowError("")
93+
supertokens.SetQuerierApiVersionForTests("3.0")
94+
defer resetQuerier()
95+
96+
if err != nil {
97+
t.Error(err.Error())
98+
}
99+
100+
_, err = q.SendGetRequest("/testing", map[string]string{})
101+
if err == nil {
102+
t.Error(errors.New("request should have failed but didnt").Error())
103+
} else {
104+
if !strings.Contains(err.Error(), "with status code: 429") {
105+
t.Error(errors.New("request failed with an unexpected error").Error())
106+
}
107+
}
108+
109+
_, err = q.SendGetRequest("/testing2", map[string]string{})
110+
if err != nil {
111+
t.Error(err.Error())
112+
}
113+
114+
_, err = q.SendGetRequest("/testing3", map[string]string{})
115+
if err != nil {
116+
t.Error(err.Error())
117+
}
118+
119+
// One initial call + 5 retries
120+
assert.Equal(t, numberOfTimesCalled, 6)
121+
assert.Equal(t, numberOfTimesSecondCalled, 3)
122+
assert.Equal(t, numberOfTimesThirdCalled, 1)
123+
}
124+
125+
func TestThatRateLimitErrorsAreThrownBackToTheUser(t *testing.T) {
126+
mux := http.NewServeMux()
127+
128+
mux.HandleFunc("/testing", func(rw http.ResponseWriter, r *http.Request) {
129+
rw.WriteHeader(supertokens.RateLimitStatusCode)
130+
rw.Header().Set("Content-Type", "application/json")
131+
response, err := json.Marshal(map[string]interface{}{
132+
"status": "RATE_LIMIT_ERROR",
133+
})
134+
if err != nil {
135+
t.Error(err.Error())
136+
}
137+
rw.Write(response)
138+
})
139+
140+
testServer := httptest.NewServer(mux)
141+
142+
defer func() {
143+
testServer.Close()
144+
}()
145+
146+
config := supertokens.TypeInput{
147+
Supertokens: &supertokens.ConnectionInfo{
148+
// We need the querier to call the test server and not the core
149+
ConnectionURI: testServer.URL,
150+
},
151+
AppInfo: supertokens.AppInfo{
152+
AppName: "SuperTokens",
153+
WebsiteDomain: "supertokens.io",
154+
APIDomain: "api.supertokens.io",
155+
},
156+
RecipeList: []supertokens.Recipe{
157+
Init(nil),
158+
},
159+
}
160+
161+
err := supertokens.Init(config)
162+
163+
if err != nil {
164+
t.Error(err.Error())
165+
}
166+
167+
q, err := supertokens.GetNewQuerierInstanceOrThrowError("")
168+
supertokens.SetQuerierApiVersionForTests("3.0")
169+
defer resetQuerier()
170+
171+
if err != nil {
172+
t.Error(err.Error())
173+
}
174+
175+
_, err = q.SendGetRequest("/testing", map[string]string{})
176+
if err == nil {
177+
t.Error(errors.New("request should have failed but didnt").Error())
178+
} else {
179+
if !strings.Contains(err.Error(), "with status code: 429") {
180+
t.Error(errors.New("request failed with an unexpected error").Error())
181+
}
182+
183+
assert.True(t, strings.Contains(err.Error(), "message: {\"status\":\"RATE_LIMIT_ERROR\"}"))
184+
}
185+
}
186+
187+
func TestThatParallelCallsHaveIndependentRetryCounters(t *testing.T) {
188+
mux := http.NewServeMux()
189+
190+
numberOfTimesFirstCalled := 0
191+
numberOfTimesSecondCalled := 0
192+
193+
mux.HandleFunc("/testing", func(rw http.ResponseWriter, r *http.Request) {
194+
if r.URL.Query().Get("id") == "1" {
195+
numberOfTimesFirstCalled++
196+
} else {
197+
numberOfTimesSecondCalled++
198+
}
199+
200+
rw.WriteHeader(supertokens.RateLimitStatusCode)
201+
rw.Header().Set("Content-Type", "application/json")
202+
response, err := json.Marshal(map[string]interface{}{})
203+
if err != nil {
204+
t.Error(err.Error())
205+
}
206+
rw.Write(response)
207+
})
208+
209+
testServer := httptest.NewServer(mux)
210+
211+
defer func() {
212+
testServer.Close()
213+
}()
214+
215+
config := supertokens.TypeInput{
216+
Supertokens: &supertokens.ConnectionInfo{
217+
// We need the querier to call the test server and not the core
218+
ConnectionURI: testServer.URL,
219+
},
220+
AppInfo: supertokens.AppInfo{
221+
AppName: "SuperTokens",
222+
WebsiteDomain: "supertokens.io",
223+
APIDomain: "api.supertokens.io",
224+
},
225+
RecipeList: []supertokens.Recipe{
226+
Init(nil),
227+
},
228+
}
229+
230+
err := supertokens.Init(config)
231+
232+
if err != nil {
233+
t.Error(err.Error())
234+
}
235+
236+
q, err := supertokens.GetNewQuerierInstanceOrThrowError("")
237+
supertokens.SetQuerierApiVersionForTests("3.0")
238+
defer resetQuerier()
239+
240+
if err != nil {
241+
t.Error(err.Error())
242+
}
243+
244+
var wg sync.WaitGroup
245+
246+
wg.Add(2)
247+
248+
go func() {
249+
_, err = q.SendGetRequest("/testing", map[string]string{
250+
"id": "1",
251+
})
252+
if err == nil {
253+
t.Error(errors.New("request should have failed but didnt").Error())
254+
} else {
255+
if !strings.Contains(err.Error(), "with status code: 429") {
256+
t.Error(errors.New("request failed with an unexpected error").Error())
257+
}
258+
}
259+
260+
wg.Done()
261+
}()
262+
263+
go func() {
264+
_, err = q.SendGetRequest("/testing", map[string]string{
265+
"id": "2",
266+
})
267+
if err == nil {
268+
t.Error(errors.New("request should have failed but didnt").Error())
269+
} else {
270+
if !strings.Contains(err.Error(), "with status code: 429") {
271+
t.Error(errors.New("request failed with an unexpected error").Error())
272+
}
273+
}
274+
275+
wg.Done()
276+
}()
277+
278+
wg.Wait()
279+
280+
assert.Equal(t, numberOfTimesFirstCalled, 6)
281+
assert.Equal(t, numberOfTimesSecondCalled, 6)
282+
}

supertokens/constants.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const (
2121
)
2222

2323
// VERSION current version of the lib
24-
const VERSION = "0.13.1"
24+
const VERSION = "0.13.2"
2525

2626
var (
2727
cdiSupported = []string{"3.0"}
@@ -30,3 +30,5 @@ var (
3030
const DashboardVersion = "0.7"
3131

3232
const DefaultTenantId string = "public"
33+
34+
const RateLimitStatusCode = 429

0 commit comments

Comments
 (0)