I tried refreshing my tokens when my calling api goes to error 401 (token expires) but when it calls dio interceptor is not triggered.
api.dart is definition of my dio
Dio _createHttpClient() {
final api = Dio(
new BaseOptions(
baseUrl: environments.api,
contentType: Headers.jsonContentType,
responseType: ResponseType.json,
),
);
api
..interceptors.clear()
..interceptors.add(new ErrorDialogInterceptor())
..interceptors.add(new AuthTokenInterceptor(api));
return api;
}
final api = _createHttpClient();
profil_provider.dart is the call of my api
Future<ProfilePicture?> getPictureProfile(String id) async {
String url = '/v1/users/$id/profile-picture';
try {
final response = await api.get(
url,
options: Options(
responseType: ResponseType.bytes,
headers: {
ErrorDialogInterceptor.skipHeader: true,
},
),
);
Uint8List avatar = Uint8List.fromList(response.data);
return ProfilePicture(image: avatar);
} catch (e) {
print('e');
return null;
}
}
and it go to the catch error
auth_interceptor.dart is for manage my error and request of my api
class AuthTokenInterceptor extends Interceptor {
static const skipHeader = 'skipAuthToken';
Dio api;
AuthTokenInterceptor(this.api);
#override
onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
final context = applicationKey.currentContext;
print("test");
final repository = context?.read<AuthRepository>();
if (repository == null) {
return;
}
final accessToken = await repository.getAccessToken();
print("access: $accessToken");
if (accessToken != null) {
print(accessToken);
options.headers['Authorization'] = 'Bearer $accessToken';
}
return super.onRequest(options, handler);
}
#override
onError(DioError err, ErrorInterceptorHandler handler) async {
final context = applicationKey.currentContext;
if (context == null) {
return;
}
final response = err.response?.data;
if (response == null) {
return super.onError(err, handler);
}
final repository = context.read<AuthRepository>();
if (err.response?.statusCode == 401)
if (err.response?.statusCode == 401 &&
await repository.getRefreshToken() != null) {
api.interceptors.clear();
return _handlerRefreshToken(context, repository, err, handler);
}
return super.onError(err, handler);
}
and it not go to the auth interceptor with my debug print, I don't know why it not go there with the error 401 from the catch error
you can do.
// returns the http request
Future<Response<ProfilePicture>> getPictureProfile(String id) {
String url = '/v1/users/$id/profile-picture';
return api.get(url)
}
//...
// and use a try/catch outside the method
try{
Response<ProfilePicture> respose = await getPictureProfile(555)
print(respose.data)
}catch(e){
print(e)
}
Related
hello I have a case where when the user token expires the user does not switch to the loginPage page, even though I have set it here.
how do i solve this problem thanks.
i set it on splashscreen if token is not null then go to main page and if token is null then go to login page.
but when the token expires it still remains on the main page
Future<void> toLogin() async {
Timer(
const Duration(seconds: 3),
() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? token = prefs.getString(Constant.token);
Navigator.pushReplacementNamed(
context,
token != null ? AppRoute.mainRoute : AppRoute.loginRoute,
arguments: token,
);
},
);
}
and function when user login
CustomButtonFilled(
title: 'Login',
onPressed: () async {
final prefs =
await SharedPreferences.getInstance();
prefs.setString(Constant.token, '');
if (nimController.text.isEmpty ||
passwordController.text.isEmpty) {
showError('NIM/Password harus diisi');
} else {
setState(() {
isLoading = true;
});
User? user = await userProvider.login(
nimController.text,
passwordController.text);
setState(() {
isLoading = false;
});
if (user == null) {
showError('NIM/Password tidak sesuai!');
} else {
userProvider.user = user;
Navigator.pushNamedAndRemoveUntil(
context,
'/main',
(route) => false,
);
}
}
},
),
and this call api
Future<User?> login(String nim, String password) async {
String url = Constant.baseURL;
try {
var body = {
'username': nim,
'password': password,
};
var response = await http.post(
Uri.parse(
'$url/login_mhs',
),
body: body,
);
if (response.statusCode == 200) {
final token = jsonDecode(response.body)['data']['access_token'];
//Ini mulai nyimpen token
await UtilSharedPreferences.setToken(token);
print(token);
// print(await UtilSharedPreferences.getToken());
return User.fromJson(jsonDecode(response.body));
} else {
return null;
}
} catch (e) {
print(e);
throw Exception();
}
}
you can just make your own HTTP client using Dio and add Interceptor to automatically regenerate idToken if expired using the refreshToken given.
Http client gives an error if the refreshToken also gets expired.
In that case, just navigate to the login screen.
Full code for adding interceptor and making own HTTP client is given below
import 'package:dio/dio.dart';
import '../utils/shared_preference.dart';
class Api {
static Dio? _client;
static Dio clientInstance() {
if (_client == null) {
_client = Dio();
_client!.interceptors
.add(InterceptorsWrapper(onRequest: (options, handler) async {
if (!options.path.contains('http')) {
options.path = 'your-server' + options.path;
}
options.headers['Authorization'] =
'Bearer ${PreferenceUtils.getString('IdToken')}';
return handler.next(options);
}, onError: (DioError error, handler) async {
if ((error.response?.statusCode == 401 &&
error.response?.data['message'] == "Invalid JWT")) {
if (PreferenceUtils.exists('refreshToken')) {
await _refreshToken();
return handler.resolve(await _retry(error.requestOptions));
}
}
return handler.next(error);
}));
}
return _client!;
}
static Future<void> _refreshToken() async {
final refreshToken = PreferenceUtils.getString('refreshToken');
final response = await _client!
.post('/auth/refresh', data: {'refreshToken': refreshToken});
if (response.statusCode == 201) {
// successfully got the new access token
PreferenceUtils.setString('accessToken', response.data);
} else {
// refresh token is wrong so log out user.
PreferenceUtils.deleteAll();
}
}
static Future<Response<dynamic>> _retry(RequestOptions requestOptions) async {
final options = Options(
method: requestOptions.method,
headers: requestOptions.headers,
);
return _client!.request<dynamic>(requestOptions.path,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters,
options: options);
}
}
Dio client = Api.clientInstance();
var resposne = (hit any request);
if(error in response is 401){
//it is sure that 401 is because of expired refresh token as we
//already handled idTokoen expiry case in 401 error while
//adding interceptor.
navigate to login screen for logging in again.
}
Please accept the solution if it solves your problem.
If your session expire feature has some predefine interval or logic than you have to implement it in splash screen and based on that you can navigate user further. Otherwise you want to handle it in API response only you have add condition for statusCode 401.
checkSessionExpire(BuildContext context)
if (response.statusCode == 200) {
//SuccessWork
} else if (response.statusCode == 401) {
//SessionExpire
} else {
return null
}
}
I am trying to implement a JWT Access/Refresh token flow with flutter. After my access token expires, my QueuedInterceptor gets a new access token with the refresh token. Everything works fine, but it is not retrying to get the requested ressource and returns a 401. After a refresh of that page, the resource loads. How do I implement a retry with QueuedInterceptor ?
class AuthInterceptor extends QueuedInterceptor {
final Dio _dio;
AuthInterceptor(this._dio);
#override
void onRequest(
RequestOptions options, RequestInterceptorHandler handler) async {
final accessToken = await storage.read(key: "accessToken");
final refreshToken = await storage.read(key: "refreshToken");
if (accessToken == null || refreshToken == null) {
const AuthState.unauthenticated();
final error = DioError(requestOptions: options, type: DioErrorType.other);
return handler.reject(error);
}
final accessTokenHasExpired = JwtDecoder.isExpired(accessToken);
final refreshTokenHasExpired = JwtDecoder.isExpired(refreshToken);
var _refreshed = true;
if (refreshTokenHasExpired) {
const AuthState.unauthenticated();
final error = DioError(requestOptions: options, type: DioErrorType.other);
return handler.reject(error);
} else if (accessTokenHasExpired) {
// regenerate new access token
_refreshed = await _regenerateAccessToken();
}
if (_refreshed) {
options.headers["Authorization"] = "Bearer $accessToken";
return handler.next(options);
} else {
final error = DioError(requestOptions: options, type: DioErrorType.other);
return handler.reject(error);
}
}
Future<bool> _regenerateAccessToken() async {
try {
var dio = Dio();
final refreshToken = await storage.read(key: "refreshToken");
final response = await dio.post(
"https://localhost:7104/api/Login/Token/Refresh",
options: Options(headers: {"Authorization": "Bearer $refreshToken"}),
);
if (response.statusCode == 200 || response.statusCode == 201) {
final newAccessToken = response.data["accessToken"];
storage.write(key: "accessToken", value: newAccessToken);
return true;
} else if (response.statusCode == 401 || response.statusCode == 403) {
const AuthState.unauthenticated();
return false;
} else {
return false;
}
} on DioError {
return false;
} catch (e) {
return false;
}
}
}
This is how I create the request with the interceptor. It throws a 401 if my access token is expired:
final dio = Dio();
dio.options.baseUrl = authenticationBackend;
dio.interceptors.addAll([
AuthInterceptor(dio),
]);
var response = await dio.get('$host/animals');
class RefreshTokenInterceptor extends Interceptor {
final Dio dio;
RefreshTokenInterceptor({
required this.dio,
});
#override
void onError(DioError err, ErrorInterceptorHandler handler) async {
if (err.response == null) {
return;
}
if (err.response!.statusCode == 401) {
var res = await refreshToken();
if (res != null && res) {
await _retry(err.requestOptions);
}
}
return handler.next(err);
}
/// Api to get new token from refresh token
///
Future<bool?> refreshToken() async {
///call your refesh token api here
}
/// For retrying request with new token
///
Future<Response<dynamic>> _retry(RequestOptions requestOptions) async {
final options = Options(
method: requestOptions.method,
headers: requestOptions.headers,
);
return dio.request<dynamic>(requestOptions.path,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters,
options: options);
}
}
And then use it
dio.interceptors.addAll(
[
/// interceptor for refreshing token
///
RefreshTokenInterceptor(dio: dio),
],
);
Flutter Custom Interceptors code here, it is showing error on onRequest Method as well as onResponse Method and onError Method. recently started flutter implementation.
class AppInterceptors extends Interceptor {
Dio _dio = Dio();
SharedPreferences _prefs;
TokenAnalyzer _tokenAnalyzer;
AppInterceptors(this._dio, this._tokenAnalyzer);
#override
Future<dynamic> onRequest(RequestOptions options) async {
_prefs = await _sharedPreferences;
var accessToken = _prefs.get("access_token") ?? '';
return options;
}
#override
Future<dynamic> onResponse(Response options) async {
return options;
}
#override
Future<dynamic> onError(DioError dioError) async {
if (dioError.type == DioErrorType.response &&
dioError.response.statusCode < 200 ||
dioError.response.statusCode > 400) {
if (dioError.response.statusCode == 500) {
prefs.setString("ErrorMessage", dioError.response.data);
errorService.showErrorMessage(dioError.response.data);
throw (dioError.response.data);
} else if (dioError.response.statusCode == 401) {
var _prefs = await _sharedPreferences;
prefs.setString("ErrorMessage", dioError.response.data);
}
}
return null;
}
Here it is another file implemented DIO instillation
class DioHttpClient {
Dio _dio = Dio();
BaseOptions options = new BaseOptions(
receiveTimeout: 1000 * 60,
headers: {'Content-Type': 'application/json', 'Authorization': ''});
DioHttpClient(TokenAnalyzer tokenAnalyzer) {
_dio = new Dio(options);
_dio.interceptors.add(AppInterceptors(_dio, tokenAnalyzer));
}
Dio get HttpClient {
return _dio;
}
}
API Call Code below:
Dio _httpClient1 = Dio();
Future<T> post<T>(url, body, {allowAnnoymous = false}) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
try {
print("called new post method $url");
clientService.isBusy = true;
String token = prefs.get("AccessToken");
Response response = await _httpClient1.post<T>(url,
data: jsonEncode(body),
options: Options(headers: {
"allowanonymous": allowAnnoymous.toString(),
'Authorization': 'Bearer $token',
}));
print("==================== success post");
throwError(response);
return response.data;
} catch (ex) {
throwErrorMessage();
throw ex;
}
}
I am getting below error and it is showing error Like this
'AppInterceptors.onRequest' ('Future<dynamic> Function(RequestOptions)') isn't a valid override of 'Interceptor.onRequest' ('void Function(RequestOptions, RequestInterceptorHandler)').
Could you please help me anyone !! thanks Advance !!!
Error says it all:
'AppInterceptors.onRequest' ('Future<dynamic> Function(RequestOptions)') isn't a valid override of 'Interceptor.onRequest' ('void Function(RequestOptions, RequestInterceptorHandler)').
You have
Future<dynamic> Function(RequestOptions)
Shoud be
void Function(RequestOptions, RequestInterceptorHandler)
See example.
You have missed RequestInterceptorHandler in arguments of onRequest method.
override
Future<dynamic> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
_prefs = await _sharedPreferences;
var accessToken = _prefs.get("access_token") ?? '';
return
options;
}
You will have to change all the three overridden method's declaration ( onError, onResponse, and onRequest), with like something below:
import 'package:dio/dio.dart';
class Intercept extends Interceptor {
#override // add errorInterceptorHandler param
Future<dynamic> onError(DioError error, errorInterceptorHandler) async {
// replace with your logic
if (error.type == DioErrorType.connectTimeout ||
error.type == DioErrorType.response) {
print(error.response);
print("CONNECTION TIMEOUT");
}
return error;
}
#override // add responseInterceptorHandler param
Future<dynamic> onResponse(Response response, responseInterceptorHandler) async {
// replace with your logic
if (response.statusCode == 200) {
print("RESPONSE--------->");
print(response.data);
print("<--------------------");
}
return response;
}
#override // add requestInterceptorHandler param
Future<dynamic> onRequest(RequestOptions request, requestInterceptorHandler) async {
// replace with your logic
print("REQUESTING--------->");
print(request.headers);
print("<--------------------");
return request;
}
}
I have declared a class to make api requests using flutter Dio as follows.
class DioUtil {
static Dio _instance;
static Dio getInstance() {
if (_instance == null) {
_instance = createDio();
}
return _instance;
}
static Dio createDio() {
var dio = Dio();
dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
// Do something before request is sent
return handler.next(options); //continue
}, onResponse: (response, handler) {
// Do something with response data
return handler.next(response); // continue
}, onError: (DioError e, handler) async {
if (e.response != null) {
if (e.response.statusCode == 401) {
var dio = DioUtil.getInstance();
dio.interceptors.requestLock.lock();
dio.interceptors.responseLock.lock();
RequestOptions requestOptions = e.requestOptions;
await refreshToken();
Repository repository = Repository();
var accessToken = await repository.readData("accessToken");
final opts = new Options(
method: requestOptions.method
);
dio.options.headers["Authorization"] = "Bearer " + accessToken;
dio.interceptors.requestLock.unlock();
dio.interceptors.responseLock.unlock();
dio.request(requestOptions.path,
options: opts,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters);
}//TODO: handle else clause
}
}));
return dio;
}
static refreshToken() async {
Response response;
Repository repository = Repository();
var dio = Dio();
final Uri apiUrl = Uri.parse(BASE_PATH + "auth/reIssueAccessToken");
var refreshToken = await repository.readData("refreshToken");
dio.options.headers["Authorization"] = "Bearer " + refreshToken;
response = await dio.postUri(apiUrl);
if (response.statusCode == 200) {
LoginResponse loginResponse =
LoginResponse.fromJson(jsonDecode(response.toString()));
repository.addValue('accessToken', loginResponse.data.accessToken);
repository.addValue('refreshToken', loginResponse.data.refreshToken);
} else {
print(response.toString());
}
}
}
and I use flutter bloc pattern and my bloc is as follows.
class OurClassBloc extends Bloc<OurClassEvent, OurClassState> {
OurClassBloc(OurClassState initialState) : super(initialState);
Repository repository = Repository();
#override
Stream<OurClassState> mapEventToState(
OurClassEvent event,
) async* {
if (event is GetClasses) {
yield* _getClassCategories(event);
}
}
Stream<OurClassState> _getClassCategories(GetClasses event) async* {
Response response;
var dio = DioUtil.getInstance();
final String apiUrl = (BASE_PATH + "classCategories");
var accessToken = await repository.readData("accessToken");
Map<String, dynamic> map = {"active": event.active};
dio.options.headers["Authorization"] = "Bearer " + accessToken;
dio.options.headers["Accept"] = "*/*";
try {
response = await dio.get(apiUrl, queryParameters: map);
if (response.statusCode == 200) {
OurClassResponse loginResponse =
OurClassResponse.fromJson(jsonDecode(response.toString()));
yield OurClassSuccess(loginResponse);
}
if (response.statusCode >= 400) {
yield OurClassFailed();
}
} catch (e) {
yield OurClassFailed();
}
}
}
When I make the requests with valid access token, I get 200 status code in bloc class and api works fine.when the token is expired, the dio class correctly gets the new token, make the same api call with new token successfully and inside the below callback I get the correct response also.
onResponse: (response, handler) {
return handler.next(response);
}
but response doesn't comes to bloc class. Though it returned the response by calling return handler.next(response);,it is not coming to response variable inside _getClassCategories method.I expect the correct response should come to the response variable in bloc class for both scenarios:
makes the api call with valid token.
makes the api call with expired token.
but only scenario 1 is working in my code and hope someone here can help me to fix this.
EDIT- this works fine with dio previous version(3.0.10) - code
dio.request(requestOptions.path,
options: opts,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters);
This line creates a new request with no relation to the original one. If the request succeeds, there is no code listening for a response. If you want the original caller to receive anything, you will need to forward the response to the original handler:
try {
final response = await dio.request(requestOptions.path,
options: opts,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters);
handler.resolve(response);
} on DioError catch (error) {
handler.next(error); // or handler.reject(error);
}
Also, be sure to forward the error to the handler in non-401 cases as well. Dio 4.0.0 interceptors don't automatically forward anything.
Now I try to transform my Andorid project to flutter. but I stucked on an api call.
here is my android code in Kotlin:
/**
* sendSms
*
* #return
*/
#Headers("Content-Type: application/json;charset=UTF-8")
#POST("uaa/sms/send/code")
fun sendSms(#Body params: Map<String, String?>): Observable<ApiResult<String>>
Now I want to implement this api call in flutter use dio, but I still got wrong, my flutter code
is :
class Req {
static Req _instance;
static const int connectTimeOut = 5 * 1000;
static const int receiveTimeOut = 7 * 1000;
static Req getInstance() {
if (_instance == null) {
_instance = Req._internal();
}
return _instance;
}
Dio _client;
Req._internal() {
if (_client == null) {
BaseOptions options = new BaseOptions();
options.connectTimeout = connectTimeOut;
options.receiveTimeout = receiveTimeOut;
_client = new Dio(BaseOptions(
baseUrl: 'https://gw.ec.iunicorn.com/',
));
// 添加缓存插件
_client.interceptors.add(Global.netCache);
//添加token
_client.interceptors.add(Global.tokenInterceptor);
_client.interceptors.add(Global.logInterceptor);
// dio.options.headers[HttpHeaders.authorizationHeader] = Global.profile.token;
_client.options.headers['source'] = 'ANDROID';
if (!Global.isRelease) {
(_client.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) {
// client.findProxy = (uri) {
// return "PROXY 10.1.10.250:8888";
// };
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
};
}
}
}
//post请求
void post(
String url,
OnData callBack, {
Map<String, String> params,
Options options,
FormData formData,
OnError errorCallBack,
CancelToken token,
}) async {
this._request(
url,
callBack,
method: RequestType.POST,
options: options,
formData: formData,
params: params,
errorCallBack: errorCallBack,
token: token,
);
}
void _request(
String url,
OnData callBack, {
RequestType method,
Map<String, String> params,
Options options,
FormData formData,
OnError errorCallBack,
ProgressCallback progressCallBack,
CancelToken token,
}) async {
final id = _id++;
int statusCode;
try {
Response response;
if (method == RequestType.GET) {
if (mapNoEmpty(params)) {
response = await _client.get(url,
queryParameters: params, cancelToken: token);
} else {
response = await _client.get(url, cancelToken: token);
}
} else {
if (mapNoEmpty(params) || formData != null) {
response = await _client.post(
url,
data: formData ?? params,
onSendProgress: progressCallBack,
cancelToken: token,
);
} else {
response = await _client.post(url, cancelToken: token);
}
}
statusCode = response.statusCode;
if (response != null) {
if (response.data is List) {
Map data = response.data[0];
callBack(data);
} else {
Map data = response.data;
callBack(data);
}
print('HTTP_REQUEST_URL::[$id]::$url');
print('HTTP_REQUEST_BODY::[$id]::${params ?? ' no'}');
print('HTTP_RESPONSE_BODY::[$id]::${response.data}');
}
if (statusCode < 0) {
_handError(errorCallBack, statusCode);
return;
}
} catch (e) {
_handError(errorCallBack, statusCode);
}
}
///处理异常
static void _handError(OnError errorCallback, int statusCode) {
String errorMsg = 'Network request error';
if (errorCallback != null) {
errorCallback(errorMsg, statusCode);
}
print("HTTP_RESPONSE_ERROR::$errorMsg code:$statusCode");
}
}
void sendSms(BuildContext context, Callback callback) async {
Req.getInstance().post(
ApiPath.SEND_SMS,
(t) {
SmsResponse r = SmsResponse.fromJson(t);
print(r);
if (callback != null) {
callback();
}
},
formData: FormData.fromMap({
'phoneNumber':'182********'
}),
options: RequestOptions(
headers: {
HttpHeaders.contentTypeHeader: 'application/json;charset=UTF-8',
}),
errorCallBack: (msg, code) {
Fluttertoast.showToast(
msg: AppLocalizations.of(context).send_sms_fail,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
backgroundColor: Colors.orangeAccent,
timeInSecForIosWeb: 1);
});
}
Now I want to know is the data in dio is equivalent to the #Body in java retrofit, if not, how can I do?
Doing this in plain Dio leads to a lot of boilerplate. There is also an equivalent retrofit package for Flutter inspired by the same package for Android. https://pub.dev/packages?q=retrofit
From there it's almost the same, you just add () after #Body. Here is an example
#POST('/auth/change-password')
Future<bool> changePassword({
#required #Body() Map<String, dynamic> params,
#required #Header('auth-token') String token,
});