Skip to content

Commit f10a931

Browse files
authored
fix: session refresh loop in all request interceptors (#71)
1 parent d16090c commit f10a931

15 files changed

+617
-6
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [0.5.0] - 2024-06-06
10+
11+
### Changes
12+
13+
- Fixed the session refresh loop in all the request interceptors that occurred when an API returned a 401 response despite a valid session. Interceptors now attempt to refresh the session a maximum of ten times before throwing an error. The retry limit is configurable via the `maxRetryAttemptsForSessionRefresh` option.
14+
15+
916
## [0.4.2] - 2024-05-28
1017

1118
- re-Adds FDI 2.0 and 3.0 support

app/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
apply plugin: 'com.android.library'
22
apply plugin: 'maven-publish'
3-
def publishVersionID = "0.4.2"
3+
def publishVersionID = "0.5.0"
44

55
android {
66
compileSdkVersion 32

app/src/main/java/com/supertokens/session/SuperTokens.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ private static void init(
5050
@NonNull String apiDomain,
5151
@Nullable String apiBasePath,
5252
@Nullable Integer sessionExpiredStatusCode,
53+
@Nullable Integer maxRetryAttemptsForSessionRefresh,
5354
@Nullable String sessionTokenBackendDomain,
5455
@Nullable String tokenTransferMethod,
5556
@Nullable CustomHeaderProvider customHeaderProvider,
@@ -63,6 +64,7 @@ private static void init(
6364
apiDomain,
6465
apiBasePath,
6566
sessionExpiredStatusCode,
67+
maxRetryAttemptsForSessionRefresh,
6668
sessionTokenBackendDomain,
6769
tokenTransferMethod,
6870
customHeaderProvider,
@@ -260,6 +262,7 @@ public static class Builder {
260262
Context applicationContext;
261263
String apiBasePath;
262264
Integer sessionExpiredStatusCode;
265+
Integer maxRetryAttemptsForSessionRefresh;
263266
String sessionTokenBackendDomain;
264267
CustomHeaderProvider customHeaderProvider;
265268
EventHandler eventHandler;
@@ -280,6 +283,11 @@ public Builder sessionExpiredStatusCode(Integer sessionExpiredStatusCode) {
280283
return this;
281284
}
282285

286+
public Builder maxRetryAttemptsForSessionRefresh(Integer maxRetryAttemptsForSessionRefresh) {
287+
this.maxRetryAttemptsForSessionRefresh = maxRetryAttemptsForSessionRefresh;
288+
return this;
289+
}
290+
283291
public Builder sessionTokenBackendDomain(String cookieDomain) {
284292
this.sessionTokenBackendDomain = cookieDomain;
285293
return this;
@@ -301,7 +309,7 @@ public Builder tokenTransferMethod(String tokenTransferMethod) {
301309
}
302310

303311
public void build() throws MalformedURLException {
304-
SuperTokens.init(applicationContext, apiDomain, apiBasePath, sessionExpiredStatusCode, sessionTokenBackendDomain, tokenTransferMethod, customHeaderProvider, eventHandler);
312+
SuperTokens.init(applicationContext, apiDomain, apiBasePath, sessionExpiredStatusCode, maxRetryAttemptsForSessionRefresh, sessionTokenBackendDomain, tokenTransferMethod, customHeaderProvider, eventHandler);
305313
}
306314
}
307315
}

app/src/main/java/com/supertokens/session/SuperTokensHttpURLConnection.java

+15
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ public static HttpURLConnection newRequest(URL url, PreConnectCallback preConnec
129129
}
130130

131131
try {
132+
int sessionRefreshAttempts = 0;
132133
while (true) {
133134
HttpURLConnection connection;
134135
SuperTokensCustomHttpURLConnection customConnection;
@@ -184,8 +185,22 @@ public static HttpURLConnection newRequest(URL url, PreConnectCallback preConnec
184185
}
185186

186187
if (responseCode == SuperTokens.config.sessionExpiredStatusCode) {
188+
/**
189+
* An API may return a 401 error response even with a valid session, causing a session refresh loop in the interceptor.
190+
* To prevent this infinite loop, we break out of the loop after retrying the original request a specified number of times.
191+
* The maximum number of retry attempts is defined by maxRetryAttemptsForSessionRefresh config variable.
192+
*/
193+
if (sessionRefreshAttempts >= SuperTokens.config.maxRetryAttemptsForSessionRefresh) {
194+
String errorMsg = "Received a 401 response from " + url + ". Attempted to refresh the session and retry the request with the updated session tokens " + SuperTokens.config.maxRetryAttemptsForSessionRefresh + " times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config.";
195+
System.err.println(errorMsg);
196+
throw new IllegalAccessException(errorMsg);
197+
}
198+
187199
// Network call threw UnauthorisedAccess, try to call the refresh token endpoint and retry original call
188200
Utils.Unauthorised unauthorisedResponse = SuperTokensHttpURLConnection.onUnauthorisedResponse(preRequestLocalSessionState, applicationContext);
201+
202+
sessionRefreshAttempts++;
203+
189204
if (unauthorisedResponse.status != Utils.Unauthorised.UnauthorisedStatus.RETRY) {
190205

191206
if (unauthorisedResponse.error != null) {

app/src/main/java/com/supertokens/session/SuperTokensInterceptor.java

+15
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public Response intercept(@NotNull Chain chain) throws IOException {
9898
}
9999

100100
try {
101+
int sessionRefreshAttempts = 0;
101102
while (true) {
102103
Request.Builder requestBuilder = chain.request().newBuilder();
103104
Utils.LocalSessionState preRequestLocalSessionState;
@@ -134,6 +135,17 @@ public Response intercept(@NotNull Chain chain) throws IOException {
134135
}
135136

136137
if (response.code() == SuperTokens.config.sessionExpiredStatusCode) {
138+
/**
139+
* An API may return a 401 error response even with a valid session, causing a session refresh loop in the interceptor.
140+
* To prevent this infinite loop, we break out of the loop after retrying the original request a specified number of times.
141+
* The maximum number of retry attempts is defined by maxRetryAttemptsForSessionRefresh config variable.
142+
*/
143+
if (sessionRefreshAttempts >= SuperTokens.config.maxRetryAttemptsForSessionRefresh) {
144+
String errorMsg = "Received a 401 response from " + requestUrl + ". Attempted to refresh the session and retry the request with the updated session tokens " + SuperTokens.config.maxRetryAttemptsForSessionRefresh + " times, but each attempt resulted in a 401 error. The maximum session refresh limit has been reached. Please investigate your API. To increase the session refresh attempts, update maxRetryAttemptsForSessionRefresh in the config.";
145+
System.err.println(errorMsg);
146+
throw new IOException(errorMsg);
147+
}
148+
137149
// Cloning the response object, if retry is false then we return this
138150
Response clonedResponse = new Response.Builder()
139151
.body(response.peekBody(Long.MAX_VALUE))
@@ -152,6 +164,9 @@ public Response intercept(@NotNull Chain chain) throws IOException {
152164
response.close();
153165

154166
Utils.Unauthorised unauthorisedResponse = onUnauthorisedResponse(preRequestLocalSessionState, applicationContext, chain);
167+
168+
sessionRefreshAttempts++;
169+
155170
if (unauthorisedResponse.status != Utils.Unauthorised.UnauthorisedStatus.RETRY) {
156171
if (unauthorisedResponse.error != null) {
157172
throw unauthorisedResponse.error;

app/src/main/java/com/supertokens/session/Utils.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ public static class NormalisedInputType {
8383
String apiDomain;
8484
String apiBasePath;
8585
int sessionExpiredStatusCode;
86+
87+
/**
88+
* This specifies the maximum number of times the interceptor will attempt to refresh
89+
* the session when a 401 Unauthorized response is received. If the number of retries
90+
* exceeds this limit, no further attempts will be made to refresh the session, and
91+
* and an error will be thrown.
92+
*/
93+
int maxRetryAttemptsForSessionRefresh;
8694
String sessionTokenBackendDomain;
8795
CustomHeaderProvider customHeaderMapper;
8896
EventHandler eventHandler;
@@ -93,13 +101,15 @@ public NormalisedInputType(
93101
String apiDomain,
94102
String apiBasePath,
95103
int sessionExpiredStatusCode,
104+
int maxRetryAttemptsForSessionRefresh,
96105
String sessionTokenBackendDomain,
97106
String tokenTransferMethod,
98107
CustomHeaderProvider customHeaderMapper,
99108
EventHandler eventHandler) {
100109
this.apiDomain = apiDomain;
101110
this.apiBasePath = apiBasePath;
102111
this.sessionExpiredStatusCode = sessionExpiredStatusCode;
112+
this.maxRetryAttemptsForSessionRefresh = maxRetryAttemptsForSessionRefresh;
103113
this.sessionTokenBackendDomain = sessionTokenBackendDomain;
104114
this.customHeaderMapper = customHeaderMapper;
105115
this.eventHandler = eventHandler;
@@ -153,6 +163,7 @@ public static NormalisedInputType normaliseInputOrThrowError(
153163
String apiDomain,
154164
@Nullable String apiBasePath,
155165
@Nullable Integer sessionExpiredStatusCode,
166+
@Nullable Integer maxRetryAttemptsForSessionRefresh,
156167
@Nullable String sessionTokenBackendDomain,
157168
@Nullable String tokenTransferMethod,
158169
@Nullable CustomHeaderProvider customHeaderProvider,
@@ -169,6 +180,11 @@ public static NormalisedInputType normaliseInputOrThrowError(
169180
_sessionExpiredStatusCode = sessionExpiredStatusCode;
170181
}
171182

183+
int _maxRetryAttemptsForSessionRefresh = 10;
184+
if (maxRetryAttemptsForSessionRefresh != null) {
185+
_maxRetryAttemptsForSessionRefresh = maxRetryAttemptsForSessionRefresh;
186+
}
187+
172188
String _sessionTokenBackendDomain = null;
173189
if (sessionTokenBackendDomain != null) {
174190
_sessionTokenBackendDomain = normaliseSessionScopeOrThrowError(sessionTokenBackendDomain);
@@ -190,7 +206,7 @@ public static NormalisedInputType normaliseInputOrThrowError(
190206
_tokenTransferMethod = tokenTransferMethod;
191207
}
192208

193-
return new NormalisedInputType(_apiDomain, _apiBasePath, _sessionExpiredStatusCode,
209+
return new NormalisedInputType(_apiDomain, _apiBasePath, _sessionExpiredStatusCode, _maxRetryAttemptsForSessionRefresh,
194210
_sessionTokenBackendDomain, _tokenTransferMethod, _customHeaderProvider, _eventHandler);
195211
}
196212
}

examples/with-thirdparty/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ dependencyResolutionManagement {
2020
}
2121
```
2222

23-
Add the folliwing to your app level `build.gradle`
23+
Add the following to your app level `build.gradle`
2424

2525
```gradle
26-
implementation("com.github.supertokens:supertokens-android:0.4.2")
26+
implementation("com.github.supertokens:supertokens-android:0.5.0")
2727
implementation ("com.google.android.gms:play-services-auth:20.7.0")
2828
implementation("com.squareup.retrofit2:retrofit:2.9.0")
2929
implementation("net.openid:appauth:0.11.1")

examples/with-thirdparty/app/build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ dependencies {
4141
implementation("androidx.appcompat:appcompat:1.6.1")
4242
implementation("com.google.android.material:material:1.8.0")
4343
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
44-
implementation("com.github.supertokens:supertokens-android:0.4.0")
44+
implementation("com.github.supertokens:supertokens-android:0.5.0")
4545
implementation ("com.google.android.gms:play-services-auth:20.7.0")
4646
implementation("com.squareup.retrofit2:retrofit:2.9.0")
4747
implementation("net.openid:appauth:0.11.1")

testHelpers/server/index.js

+4
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,10 @@ app.get("/testError", (req, res) => {
507507
res.status(500).send("test error message");
508508
});
509509

510+
app.get("/throw-401", (req, res) => {
511+
res.status(401).send("Unauthorised");
512+
})
513+
510514
app.get("/stop", async (req, res) => {
511515
process.exit();
512516
});

0 commit comments

Comments
 (0)