@@ -2,9 +2,13 @@ import 'package:dio/dio.dart';
22import 'package:flutter/material.dart' ;
33import 'package:on_time_front/data/data_sources/token_local_data_source.dart' ;
44import 'package:on_time_front/core/di/di_setup.dart' ;
5+ import 'package:on_time_front/domain/entities/token_entity.dart' ;
56import 'package:on_time_front/domain/repositories/user_repository.dart' ;
67
78class TokenInterceptor implements InterceptorsWrapper {
9+ static const _refreshTokenPath = '/refresh-token' ;
10+ static const _retryAfterRefreshKey = 'tokenInterceptor.retryAfterRefresh' ;
11+
812 final Dio dio;
913 TokenInterceptor (this .dio);
1014 final TokenLocalDataSource tokenLocalDataSource =
@@ -35,96 +39,94 @@ class TokenInterceptor implements InterceptorsWrapper {
3539
3640 @override
3741 void onError (DioException err, ErrorInterceptorHandler handler) async {
38- final response = err.response;
39- if (response != null &&
40- // status code for unauthorized usually 401
41- response.statusCode == 401 &&
42- // refresh token call maybe fail by it self
43- // eg: when refreshToken also is expired -> can't get new accessToken
44- // usually server also return 401 unauthorized for this case
45- // need to exlude it to prevent loop infinite call
46- response.requestOptions.path != "/refresh-token" ) {
42+ if (_shouldRefreshToken (err)) {
43+ final response = err.response;
4744 // if hasn't not refreshing yet, let's start it
45+ _requestsNeedRetry.add ((options: err.requestOptions, handler: handler));
46+
4847 if (! _isRefreshing) {
4948 _isRefreshing = true ;
5049
51- // add request (requestOptions and handler) to queue and wait to retry later
52- _requestsNeedRetry
53- .add ((options: response.requestOptions, handler: handler));
54-
5550 // call api refresh token
5651 final isRefreshSuccess = await _refreshToken ();
5752
58- if (isRefreshSuccess) {
59- // refresh success, loop requests need retry
60- for (var requestNeedRetry in _requestsNeedRetry) {
61- // don't need set new accessToken to header here, because these retry
62- // will go through onRequest callback above (where new accessToken will be set to header)
63-
64- // won't use await because this loop will take longer -> maybe throw: Unhandled Exception: Concurrent modification during iteration
65- // because method _requestsNeedRetry.add() is called at the same time
66- // final response = await dio.fetch(requestNeedRetry.options);
67- // requestNeedRetry.handler.resolve(response);
68-
69- dio.fetch (requestNeedRetry.options).then ((response) {
70- requestNeedRetry.handler.resolve (response);
71- }).catchError ((_) {});
72- }
73-
74- _requestsNeedRetry.clear ();
75- _isRefreshing = false ;
76- } else {
77- for (final requestNeedRetry in _requestsNeedRetry) {
78- requestNeedRetry.handler.reject (
79- DioException (
80- requestOptions: requestNeedRetry.options,
81- response: response,
82- type: err.type,
83- error: err.error,
84- message: err.message,
85- ),
86- );
87- }
88- _requestsNeedRetry.clear ();
89- // Force a local logout when the refresh token is rejected. The full
90- // sign-out use case may make authenticated cleanup calls, which can
91- // deadlock while the interceptor is already refreshing.
92- try {
93- await getIt.get <UserRepository >().signOut ();
94- } catch (_) {
95- await tokenLocalDataSource.deleteToken ();
53+ try {
54+ if (isRefreshSuccess) {
55+ while (_requestsNeedRetry.isNotEmpty) {
56+ final requestsNeedRetry = List .of (_requestsNeedRetry);
57+ _requestsNeedRetry.clear ();
58+ await _retryRequests (requestsNeedRetry);
59+ }
60+ } else {
61+ for (final requestNeedRetry in _requestsNeedRetry) {
62+ requestNeedRetry.handler.reject (
63+ DioException (
64+ requestOptions: requestNeedRetry.options,
65+ response: response,
66+ type: err.type,
67+ error: err.error,
68+ message: err.message,
69+ ),
70+ );
71+ }
72+ _requestsNeedRetry.clear ();
73+ // Force a local logout when the refresh token is rejected. The full
74+ // sign-out use case may make authenticated cleanup calls, which can
75+ // deadlock while the interceptor is already refreshing.
76+ await _signOutLocally ();
9677 }
78+ } finally {
9779 _isRefreshing = false ;
9880 }
99- } else {
100- // if refresh flow is processing, add this request to queue and wait to retry later
101- _requestsNeedRetry
102- .add ((options: response.requestOptions, handler: handler));
10381 }
10482 } else {
10583 // ignore other error is not unauthorized
10684 return handler.next (err);
10785 }
10886 }
10987
88+ bool _shouldRefreshToken (DioException err) {
89+ final response = err.response;
90+ if (response? .statusCode != 401 ) {
91+ return false ;
92+ }
93+
94+ final requestOptions = err.requestOptions;
95+ return requestOptions.path != _refreshTokenPath &&
96+ requestOptions.extra[_retryAfterRefreshKey] != true ;
97+ }
98+
11099 Future <bool > _refreshToken () async {
111100 try {
112101 final tokenEntity = await tokenLocalDataSource.getToken ();
113102 final refreshToken = tokenEntity.refreshToken;
114103
115104 final res = await dio.get (
116- '/refresh-token' ,
105+ _refreshTokenPath ,
117106 options: Options (
107+ extra: {
108+ _retryAfterRefreshKey: true ,
109+ },
118110 headers: {
119111 'Authorization-refresh' : 'Bearer $refreshToken ' ,
120112 },
121113 ),
122114 );
123115 if (res.statusCode == 200 ) {
124116 debugPrint ("token refreshing success" );
125- final authToken = res.headers['authorization' ]! [0 ];
126- // save new access + refresh token to your local storage for using later
127- await tokenLocalDataSource.storeAuthToken (authToken);
117+ final accessToken = res.headers.value ('authorization' );
118+ final refreshedRefreshToken =
119+ res.headers.value ('authorization-refresh' );
120+ if (accessToken == null || refreshedRefreshToken == null ) {
121+ throw StateError (
122+ 'Refresh response must include authorization and authorization-refresh headers' );
123+ }
124+ await tokenLocalDataSource.storeTokens (
125+ TokenEntity (
126+ accessToken: accessToken,
127+ refreshToken: refreshedRefreshToken,
128+ ),
129+ );
128130 return true ;
129131 } else {
130132 debugPrint ("refresh token fail ${res .statusMessage ?? res .toString ()}" );
@@ -136,6 +138,39 @@ class TokenInterceptor implements InterceptorsWrapper {
136138 }
137139 }
138140
141+ Future <void > _retryRequests (
142+ List <({RequestOptions options, ErrorInterceptorHandler handler})> requests,
143+ ) async {
144+ await Future .wait (
145+ requests.map ((requestNeedRetry) async {
146+ final options = requestNeedRetry.options;
147+ options.extra[_retryAfterRefreshKey] = true ;
148+
149+ try {
150+ final response = await dio.fetch (options);
151+ requestNeedRetry.handler.resolve (response);
152+ } on DioException catch (error) {
153+ requestNeedRetry.handler.reject (error);
154+ } catch (error) {
155+ requestNeedRetry.handler.reject (
156+ DioException (
157+ requestOptions: options,
158+ error: error,
159+ ),
160+ );
161+ }
162+ }),
163+ );
164+ }
165+
166+ Future <void > _signOutLocally () async {
167+ try {
168+ await getIt.get <UserRepository >().signOut ();
169+ } catch (_) {
170+ await tokenLocalDataSource.deleteToken ();
171+ }
172+ }
173+
139174 @override
140175 void onResponse (Response response, ResponseInterceptorHandler handler) {
141176 handler.next (response);
0 commit comments