rxDart not calling onError - flutter

I am trying to make a simple request to backend using rxDart. But the problem I face is that when I get a http error such as 404, onError is not called, however, it is possible to extract it in onData.
I have a little experience with RxJava + retrofit and there it works as expected, when there is a response with error http status code onError is called and can be handled appropriately.
1. What am I doing wrong, or is it intended behavior?.
Object sendProfileData() {
Stream<Response> stream = onboardingRepository.createUser(User(name: 'name', surname: 'surname', lat: 1.0, lng: 2.0));
stream.listen((response) {
print(response.statusCode);
setAttributes();
}, onError: (e) {
print(e);
});
}
OnboardingRepository.dart:
class OnboardingRepository {
Observable<Response> createUser(User user) {
return Observable.fromFuture(TMApi.createUser(user));
}
}
TMApi.dart:
class TMApi {
static Future<http.Response> createUser(User user) async {
String url = '$baseUrl/create_user';
return await http.post(url, body: json.encode(user.toJson()));
}
}
What would be the best way to handle the event in the View? There should be an error displayed if error occurs, otherwise it should open a new screen. sendProfileData() method will return an Object, based on that I am going to perform actions in the view, but that doesn't sound like a very elegant solution...
Any suggestions on architecture are welcome :)

the http library in dart works a bit different than Retrofit.
The Future returned by http.post only throws an exception when there is an io error (socket error, no internet).
Server responses like 404 are reflected in the http.Response.
I created a simple convenience method that might help you:
void throwIfNoSuccess(http.Response response) {
if(response.statusCode < 200 || response.statusCode > 299) {
print('http error!');
print(response.body);
throw new HttpException(response);
}
}
class HttpException implements Exception {
HttpException(this.response);
http.Response response;
}
How to use:
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<UserProfile> getUserProfile(String userId) async {
final url = 'https://example.com/api/users/$userId';
final response = await http.get(url);
throwIfNoSuccess(response);
final jsonBody = json.decode(response.body);
return UserProfile.fromJson(jsonBody);
}

Related

Error [ERROR:flutter/lib/ui/ui_dart_state.cc(198)] after running json.decode(response.body)

Repository:
class PlanosRepository {
Future<List<PlanosModel>> load() async {
final client = http.Client();
try {
final uri = Uri.parse('http://localhost:3001/sales/lista_planos');
final response = await client.get(uri);
if (response.hashCode == 200) {
Error after running this line
final body = json.decode(response.body);
List<PlanosModel> planos =
body.map((json) => PlanosModel.fromJson(json)).toList();
return planos;
} else {
throw Exception('Failed to load post');
}
} finally {
client.close();
}
}
}
Error:
Image error
gives the error when I run json.decode, I've tried other methods but nothing worked
Few points i noticed
Import http like this
import 'package:http/http.dart' as http;
When you make an API call you can directly use
http.get(uri);
Http client is for multiple request at the same time.
Not sure if the local host is running and you are getting it's data.
You can check
response.statusCode == 200
Instead of hasCode

Dio Client: if request to protected route fails (401 code), then refresh the token and try again. Struggling to create

I am trying to create a custom ApiClient class that I can inject as a dependency (with get_it package) to be used in the data layer of my application. In order not to worry about access tokens throughout the presentation/application/domain layers of my app, I'd like to have a field, accessToken, that keeps track of the accessToken inside the ApiClient (singleton) class.
The ApiClient class would be used all throughout my data layer to handle requests to my server for data. It should have a method that allows me to add my own requests to it for unique routes. Then, if those routes require access tokens, it will add the accessToken field from the class along with the request. If that access token is invalid (expired/tampered with), then I would use the refresh token from the device's storage and send a request to the server to get a new access token, then try the original request again. It would "retry" the request at maximum once. Then, if there's still an error, it just returns that to be handled.
I am really struggling with how to implement this. My current attempt is below. Any help would be amazing!
class ApiClient {
final String baseUrl;
final Dio dio;
final NetworkInfo networkInfo;
final FlutterSecureStorage secureStorage;
ApiClient(
{required this.baseUrl,
required this.dio,
required this.networkInfo,
required this.secureStorage}) {
dio.interceptors.add(RefreshInvalidTokenInterceptor(networkInfo, dio, secureStorage));
}
}
class RefreshInvalidTokenInterceptor extends QueuedInterceptor {
final NetworkInfo networkInfo;
final Dio dio;
final FlutterSecureStorage secureStorage;
String? accessToken;
RefreshInvalidTokenInterceptor(this.networkInfo, this.dio, this.secureStorage);
#override
Future onError(DioError err, ErrorInterceptorHandler handler) async {
if (_shouldRetry(err) && await networkInfo.isConnected) {
try {
// access token request (using refresh token from flutter_secure_storage)
final refreshToken = await secureStorage.read(key: "refreshToken");
final response = await dio.post(
"$kDomain/api/user/token",
queryParameters: {"token": refreshToken},
);
accessToken = response.data["accessToken"];
return err;
} on DioError catch (e) {
handler.next(e);
} catch (e) {
handler.next(err);
}
} else {
handler.next(err);
}
}
bool _shouldRetry(DioError err) =>
(err.response!.statusCode == 403 || err.response!.statusCode == 401);
}
There are similar questions online, but none seem to answer my question! :)
EDIT: I've gotten a working solution (almost), with just 1 error. This works (except in the function retryRequest() I'm hardcoding the request to be a post request):
<imports removed for simplicity>
class ApiClient {
final Dio dio;
final NetworkInfo networkInfo;
final FlutterSecureStorage secureStorage;
String? accessToken;
ApiClient({
required this.dio,
required this.networkInfo,
required this.secureStorage,
}) {
dio.options = BaseOptions(
connectTimeout: 5000,
receiveTimeout: 3000,
receiveDataWhenStatusError: true,
followRedirects: true,
headers: {"content-Type": "application/json"},
);
dio.interceptors.add(QueuedInterceptorsWrapper(
//! ON REQUEST
onRequest: (options, handler) {
handler.next(options);
},
//! ON RESPONSE
onResponse: (response, handler) {
print("onResponse...");
handler.next(response);
},
//! ON ERROR
onError: (error, handler) async {
print("onError...");
if (tokenInvalid(error)) {
print("token invalid: retrying");
print("header before: ${dio.options.headers}");
await getAccessTokenAndSetToHeader(dio);
print("header after: ${dio.options.headers}");
final response = await retryRequest(error, handler);
handler.resolve(response);
print("here-1");
} else {
handler.reject(error);
}
print("here-2");
print("here-3");
},
));
}
Future<String?> getRefreshToken() async => await secureStorage.read(key: "refreshToken");
Future<void> getAccessTokenAndSetToHeader(Dio dio) async {
final refreshToken = await secureStorage.read(key: "refreshToken");
if (refreshToken == null || refreshToken.isEmpty) {
print("NO REFRESH TOKEN ERROR; LOGOUT!!!");
throw ServerException();
} else {
final response = await dio.post(
"$kDomain/api/user/token",
data: {"token": refreshToken},
);
dio.options.headers["authorization"] = "Bearer ${response.data["accessToken"]}";
}
}
// This function has the hardcoded post
Future<Response> retryRequest(DioError error, ErrorInterceptorHandler handler) async {
print("retry called, headers: ${dio.options.headers}");
final retryResponse = await dio.post(error.requestOptions.path);
print("retry results: $retryResponse");
return retryResponse;
}
bool tokenInvalid(DioError error) =>
error.response?.statusCode == 403 || error.response?.statusCode == 401;
Future<void> refreshToken() async {}
bool validStatusCode(Response response) =>
response.statusCode == 200 || response.statusCode == 201;
}
However, if I change the hardcoded post request to:
final retryResponse =
await dio.request(error.requestOptions.path, data: error.requestOptions.data);
the code no longer works... anyone know why? Having it dynamic based on whatever the failed request was, lets me re-use this class.
package:dio already include the BaseOptions which you can use to add some basic configuration like the baseUrl.
After that, you could use interceptors to add the accessToken to every request. To do this depending on your state management solution you can update the accessToken when the user authentication state changes.
And finally regarding the token refresh you can checkout package:fresh_dio.
Figured it out! (code + how to use below)
Here is my entire ApiClient class (imports hidden for simplicity). It acts as an HTTP client using dio:
class ApiClient {
final Dio dio;
final NetworkInfo networkInfo;
final FlutterSecureStorage secureStorage;
String? accessToken;
/// The base options for all requests with this Dio client.
final BaseOptions baseOptions = BaseOptions(
connectTimeout: 5000,
receiveTimeout: 3000,
receiveDataWhenStatusError: true,
followRedirects: true,
headers: {"content-Type": "application/json"},
baseUrl: kDomain, // Domain constant (base path).
);
/// Is the current access token valid? Checks if it's null, empty, or expired.
bool get validToken {
if (accessToken == null || accessToken!.isEmpty || Jwt.isExpired(accessToken!)) return false;
return true;
}
ApiClient({
required this.dio,
required this.networkInfo,
required this.secureStorage,
}) {
dio.options = baseOptions;
dio.interceptors.add(QueuedInterceptorsWrapper(
// Runs before a request happens. If there's no valid access token, it'll
// get a new one before running the request.
onRequest: (options, handler) async {
if (!validToken) {
await getAndSetAccessTokenVariable(dio);
}
setHeader(options);
handler.next(options);
},
// Runs on an error. If this error is a token error (401 or 403), then the access token
// is refreshed and the request is re-run.
onError: (error, handler) async {
if (tokenInvalidResponse(error)) {
await refreshAndRedoRequest(error, handler);
} else {
// Other error occurs (non-token issue).
handler.reject(error);
}
},
));
}
/// Sets the current [accessToken] to request header.
void setHeader(RequestOptions options) =>
options.headers["authorization"] = "Bearer $accessToken";
/// Refreshes access token, sets it to header, and resolves cloned request of the original.
Future<void> refreshAndRedoRequest(DioError error, ErrorInterceptorHandler handler) async {
await getAndSetAccessTokenVariable(dio);
setHeader(error.requestOptions);
handler.resolve(await dio.post(error.requestOptions.path,
data: error.requestOptions.data, options: Options(method: error.requestOptions.method)));
}
/// Gets new access token using the device's refresh token and sets it to [accessToken] class field.
///
/// If the refresh token from the device's storage is null or empty, an [EmptyTokenException] is thrown.
/// This should be handled with care. This means the user has somehow been logged out!
Future<void> getAndSetAccessTokenVariable(Dio dio) async {
final refreshToken = await secureStorage.read(key: "refreshToken");
if (refreshToken == null || refreshToken.isEmpty) {
// User is no longer logged in!
throw EmptyTokenException();
} else {
// New DIO instance so it doesn't get blocked by QueuedInterceptorsWrapper.
// Refreshes token from endpoint.
try {
final response = await Dio(baseOptions).post(
"/api/user/token",
data: {"token": refreshToken},
);
// If refresh fails, throw a custom exception.
if (!validStatusCode(response)) {
throw ServerException();
}
accessToken = response.data["accessToken"];
} on DioError catch (e) {
// Based on the different dio errors, throw custom exception classes.
switch (e.type) {
case DioErrorType.sendTimeout:
throw ConnectionException();
case DioErrorType.connectTimeout:
throw ConnectionException();
case DioErrorType.receiveTimeout:
throw ConnectionException();
case DioErrorType.response:
throw ServerException();
default:
throw ServerException();
}
}
}
}
bool tokenInvalidResponse(DioError error) =>
error.response?.statusCode == 403 || error.response?.statusCode == 401;
bool validStatusCode(Response response) =>
response.statusCode == 200 || response.statusCode == 201;
}
It should be injected as a singleton to your project so there's one instance of it (for the sake of keeping the state of its accessToken field). I used get_it like so:
// Registers the custom ApiClient class.
sl.registerLazySingleton(() => ApiClient(dio: sl(), networkInfo: sl(), secureStorage: sl()));
Then, inside your data layer (or wherever you call APIs from), you can use it by passing it through the constructor:
class MyDatasource implements IMyDatasource {
final ApiClient apiClient;
late Dio api;
FeedDatasource({required this.client, required this.apiClient}) {
api = apiClient.dio;
}
// Logic for your class here.
}
I simplified it to api so I wouldn't have to go apiClient.dio... every call (optional).
Then, you can use it in one of your class' methods like so:
#override
Future<List<SomeData>> fetchSomeDataFromApi() async {
try {
final response = await api.post("/api/data/whatYouWant");
throw ServerException();
} catch (e) {
throw ServerException();
}
}
Now, for this request, if your class has a valid access token (non-null, non-empty, non-expired), it will call normally. However, if your token isn't valid, it'll refresh it first, then proceed with your call. Even if the call fails after the token originally passed the validation check (token somehow expires during the call for example), it will still be refreshed, and the call re-executed.
Note: I use a lot of custom exceptions, this is optional.
Hopefully this helps someone else!

Flutter how can i set Auth token from flutter secure storage to dio header?

After login i setting user token to my user Secure storage. Like :
Future<AuthResponseModel?> login(AuthRequstModel model) async {
try {
Response response = await _dio.post(loginPath, data: model);
if (response.statusCode == 200) {
final AuthResponseModel authResponseModel = AuthResponseModel.fromJson(response.data);
if (authResponseModel.success!) {
await UserSecureStorage.setField("token", authResponseModel.token);
}
return AuthResponseModel.fromJson(response.data);
}
return null;
} catch (e) {
return null;
}
}
User Secure Storage =>
class UserSecureStorage {
static const _storage = FlutterSecureStorage();
static Future setField(String key, value) async {
await _storage.write(key: key, value: value);
}
static Future<String?> getField(key) async {
return await _storage.read(key: key);
}
But problem is when i want to make apiservice and when i want to auth token inside header of dio, I cant access it becouse its a future<String?> function. But i cant use await coz its inside of baseoption. Like :
class ApiService {
final _dio = Dio(BaseOptions(headers: {
'authorization': 'Bearer ${UserSecureStorage.getField("token")}', //I cant access here its only giving instance.
}));
Future<Response?> get(String path) async {
try {
Response response = await _dio.get('${ApiConstants.BASE_URL}$path');
if (response.statusCode == 200) {
return response;
}
return null;
} on DioError catch (e) {
return null;
}
}
What can i do for solve that problem ? I tried use .then(value=>value) after tried get token but didnt work too. Thanks for responses!
I think token is not getting updated because _dio is already intitalized.
Try to request for token when dio request is made like :
class ApiService {
final _dio = Dio();
Future<Response?> get(String path) async {
try {
Response response = await _dio.get('${ApiConstants.BASE_URL}$path', options: Options(headers: {"authorization": "Bearer ${UserSecureStorage.getField("token")}"}));
if (response.statusCode == 200) {
return response;
}
return null;
} on DioError catch (e) {
return null;
}
}
Use options in get method to add headers for a single request or interceptors for all requests.
I think that it is not an issue easily solvable, I would try with two different methods, you can maintain the token in a state manager such as Provider so you don't have to rely on an async function to retrive it, but this of course add in the code the state manager structure that complicates thing a little.
A bit more naive way to solve this could be to include a async initializator in the ApiService class such this
class ApiService {
late final _dio;
Future<void> init() async {
_dio = Dio(BaseOptions(headers: {
'authorization': 'Bearer ${UserSecureStorage.getField("token")}', //I cant access here its only giving instance.
}));}
Future<Response?> get(String path) async {
try {
Response response = await _dio.get('${ApiConstants.BASE_URL}$path');
if (response.statusCode == 200) {
return response;
}
return null;
} on DioError catch (e) {
return null;
}
}
And this introduce us a new issue, we have to call init everytime the class ApiService is instantiated, to solve this you could use the package get_it which grants you the possibility to instatiate only once the class and access it from everywhere in your project.
I hope this will help you solve your problem
your are getting instance because UserSecureStorage.getField("token") is future so you can get token when you put await keyword
so try like this
await UserSecureStorage.getField("token")

How to put this dart class together so I can use it in different parts of my code

I am trying to implement a Dio interceptor so I will be able to use it with my code.
I will be using this interceptor in many places. So I thought it would make sense to put it into a class or an interface whichever is best and just extend or implement it my subclasses.
I have this so far:
class AppInterceptor {
Dio dio = Dio();
AppInterceptor() {
dio.interceptors
.add(InterceptorsWrapper(onRequest: (Options options) async {
var token = await getAuthorizationToken();
options.headers["Authorization"] = 'Bearer $token';
return options;
}, onResponse: (Response response) {
// Finally, the string is parsed into a JSON object.
//print(response.toString());
return response;
}, onError: (DioError e) {
print('somthing went wrong');
// Do something with response error
return e; //continue
}));
}
}
How do I use this in a subclass to make an http call?
I tried something like this when trying to do my http call:
Response response = await AppInterceptor.dio.post(Global.functionsUrl+'/auth/linkuseraccount/', data: {'hey': 'hello'});
print(response);
It fails each time with Unhandled Exception: DioError [DioErrorType.RESPONSE]: Http status error [403]
From my backend, I can tell that it fails because the interceptor didn't pass in the authentication header.
How should I go about this?

Retry Http Get request if there is no response in Flutter

getData() async {
http.Response response = await http.get('https://www.example.com/);
print(response.body);
}
The above function works to get the HTML code of a page but it fails in some cases. The function is sometimes never completed and it waits forever to get response( For example, if the app is opened while internet is off and even when its turned on, it never connects). In such situations is there any way to retry ?
I tried the http retry package but it gives me 15+ errors.
Example code for how this could be done:
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<List> loadData() async {
bool loadRemoteDatatSucceed = false;
var data;
try {
http.Response response = await http.post("https://www.example.com",
body: <String, String>{"username": "test"});
data = json.decode(response.body);
if (data.containsKey("success")) {
loadRemoteDatatSucceed = true;
}
} catch (e) {
if (loadRemoteDatatSucceed == false) retryFuture(loadData, 2000);
}
return data;
}
retryFuture(future, delay) {
Future.delayed(Duration(milliseconds: delay), () {
future();
});
}
You can use RetryPolicy from http package to retry your connection, just create your own class and inherit form RetryPolicy and override these function like the following example, then create a Client using HttpClientWithInterceptor.build and add your custom retryPolicy as a parameter, this will retry your request for a number of times until a condition is met, if not, it'll just stop retrying.
import 'package:http/http.dart';
class MyRetryPolicy extends RetryPolicy {
final url = 'https://www.example.com/';
#override
// how many times you want to retry your request.
int maxRetryAttempts = 5;
#override
Future<bool> shouldAttemptRetryOnResponse(ResponseData response) async {
//You can check if you got your response after certain timeout,
//or if you want to retry your request based on the status code,
//usually this is used for refreshing your expired token but you can check for what ever you want
//your should write a condition here so it won't execute this code on every request
//for example if(response == null)
// a very basic solution is that you can check
// for internet connection, for example
try {
final result = await InternetAddress.lookup('google.com');
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
return true;
}
return false;
} on SocketException catch (_) {
return false;
}
}
}
then create and use a client to make your requests.
it will automatically retry the request if the condition you wrote is met.
Client client = HttpClientWithInterceptor.build(
retryPolicy: ExpiredTokenRetryPolicy(),
);
final response = await client.get('https://www.example.com/);
there is also a package to check for internet connection if that your problem, see connectivity
You can use try-catch blocks inside async functions like you would in synchronous code. Perhaps you'd be able to add some sort of error handling mechanism in the function, and retry the function on error? Here's some documentation on that one.
Example from the docs:
try {
var order = await getUserOrder();
print('Awaiting user order...');
} catch (err) {
print('Caught error: $err');
}
You can also catch specific Exceptions, per this github issue.
doLogin(String username, String password) async {
try {
var user = await api.login(username, password);
_view.onLoginSuccess(user);
} on Exception catch(error) {
_view.onLoginError(error.toString());
}
}
EDIT: This may also help.
While we're at it, look here for a function that reattempts an async operation however many times you need.