Flutter Chopper Authentication + Retry on 401 Unauthorized

Alexander
5 min readAug 28, 2023

I will show you how to add an HTTP Authorization header to the Chopper requests using interceptor and how to refresh token and retry requests in case of 401 Unauthorized response. The full source code link is at the end 🙂

For this purpose, the Chopper package gives us 2 instruments:

  1. RequestInterceptor — intercepts requests and can add the HTTP Authorization header.
  2. Authenticator — intercepts responses and, in case of invalid credentials, should return a new request with updated credentials, or if the response is satisfiable, return null.

In Chopper, the basic request flow looks like this:

New request -> Request interceptors -> Network call -> Response -> Authenticator -> Based on what Authenticator returned: Retry the request OR Return the response.

If the Authenticator returns a new request, the above flow repeats, including the Request interceptors step.

How to authenticate Chopper requests in Flutter

In my example, the authentication is done through the HTTP Authorization header, but with other methods, the idea would be the same.

Before working with Chopper, let’s imagine we have an auth repository that looks like this. accessToken getter and refreshToken() method:

class AuthRepository {
AuthRepository(this._fakeRemoteServer);

final FakeRemoteServer _fakeRemoteServer;

var _accessToken = 'initial-valid-token';
String get accessToken => _accessToken;

Future<void> refreshToken() async {
_accessToken = await _fakeRemoteServer.getNewToken();
}
}

Chopper AuthInterceptor

Firstly, we need a request interceptor to add the authorization header to each request.

class AuthInterceptor implements RequestInterceptor {
const AuthInterceptor(this._repo);

final AuthRepository _repo;

@override
FutureOr<Request> onRequest(Request request) {
final updatedRequest = applyHeader(
request,
HttpHeaders.authorizationHeader,
_repo.accessToken,
// Do not override existing header
override: false,
);

print(
'[AuthInterceptor] accessToken: ${updatedRequest.headers[HttpHeaders.authorizationHeader]}',
);

return updatedRequest;
}
}

I have set the override to false in case we have updated the authorization header in the Authenticator, but for some reason, the auth repository still has an invalid access token.

Chopper Authenticator

The Chopper FAQ page has a basic example of Authenticator, but it misses 2 critical points:

  1. If the updated request also fails, it will end up in an infinite loop of retries.
  2. It will potentially call a token refresh for each failed response, even if they failed simultaneously, and it may be better to make only one token refresh.

So here is an Authenticator that fixes the above points:

class MyAuthenticator implements Authenticator {
MyAuthenticator(this._repo);

final AuthRepository _repo;

@override
FutureOr<Request?> authenticate(
Request request,
Response response, [
Request? originalRequest,
]) async {
print('[MyAuthenticator] response.statusCode: ${response.statusCode}');
print(
'[MyAuthenticator] request Retry-Count: ${request.headers['Retry-Count'] ?? 0}',
);

// 401
if (response.statusCode == HttpStatus.unauthorized) {
// Trying to update token only 1 time
if (request.headers['Retry-Count'] != null) {
print(
'[MyAuthenticator] Unable to refresh token, retry count exceeded',
);
return null;
}

try {
final newToken = await _refreshToken();

return applyHeaders(
request,
{
HttpHeaders.authorizationHeader: newToken,
// Setting the retry count to not end up in an infinite loop
// of unsuccessful updates
'Retry-Count': '1',
},
);
} catch (e) {
print('[MyAuthenticator] Unable to refresh token: $e');
return null;
}
}

return null;
}

Future<String> _refreshToken() { ... }
}

Notes:

  • The Authenticator will be called for every response, so we need to check if the token needs to be updated.
  • If we exceed the retry count, return null to avoid an infinite loop.

_refreshToken() method:

class MyAuthenticator implements Authenticator {
...

// Completer to prevent multiple token refreshes at the same time
Completer<String>? _completer;

Future<String> _refreshToken() {
var completer = _completer;
if (completer != null && !completer.isCompleted) {
print('Token refresh is already in progress');
return completer.future;
}

completer = Completer<String>();
_completer = completer;

_repo.refreshToken().then((_) {
// Completing with a new token
completer?.complete(_repo.accessToken);
}).onError((error, stackTrace) {
// Completing with an error
completer?.completeError(error ?? 'Refresh token error', stackTrace);
});

return completer.future;
}
}

Note: If we had several failed requests simultaneously, we would call _refreshToken() for each of them. But we don’t need to call _repo.refreshToken() several times, so I have used Completer to return the same future if the token refresh is already in progress.

ChopperClient

Now, initialize the ChopperClient with your AuthInterceptor and Authenticator. In this example, I’ve used Riverpod’s ref to locate Authenticator and AuthInterceptor, but the idea would be the same with any solution.

@ChopperApi()
abstract class ApiService extends ChopperService {
static ApiService create(ApiServiceRef ref) {
final client = ChopperClient(
client: mockClient(ref),
// Authenticator
authenticator: ref.read(myAuthenticatorProvider),
interceptors: [
// AuthInterceptor
ref.read(authInterceptorProvider),
],
services: [
_$ApiService(),
],
);

return _$ApiService(client);
}

static MockClient mockClient(ApiServiceRef ref) {
// Returns data if the token is valid or 401 otherwise
...
}

@Get(path: '/data')
Future<Response> getData();
}

Note: You should use the same instance of the Authenticator in each client that relies on the same refresh logic because it uses an internal state (Completer) to not make duplicate refresh requests.

Final results

Here are a few log examples of how it all works.

The access token is valid, and nothing should be done:

The access token is invalid, and the refresh is successful (2 simultaneous requests, but only 1 refresh):

The access token is invalid, and the refresh is NOT successful because of some exception:

The access token is invalid and reaching max retry count:

Full app source code:

What can be extended

You can improve the Authenticator code to support more than 1 retry attempt if necessary.

Thank you for reading. Bye 👋

--

--