Here is my MultiPartRequest code
var request =
http.MultipartRequest("POST", Uri.parse(EMPLOYEE_PUNCH_IN_URL));
request.fields['uid'] = userId;
request.fields['location'] = location;
request.fields['punchin_time'] = punchInTime;
request.fields['punchin_location_name'] = address;
var multiPartFile = await http.MultipartFile.fromPath(
"photo", imageFile.path,
contentType: MediaType("image", "$extension"));
request.files.add(multiPartFile);
http.StreamedResponse response = await request.send();
var responseByteArray = await response.stream.toBytes();
employeePunchInModel = standardSerializers.deserializeWith(
EmployeePunchInModel.serializer,
json.decode(utf8.decode(responseByteArray)));
......
I know how to set timeout to a normal http request. I have followed this link
Set timeout for HTTPClient get() request
I have tried adding timeout function in following ways but it won't work and my request gets completed
1.
var multiPartFile = await http.MultipartFile.fromPath(
"photo", imageFile.path,
contentType: MediaType("image", "$extension")).timeout(const Duration(seconds: 1));
2.
http.StreamedResponse response = await request.send().timeout(const Duration(seconds: 1));
3.
var responseByteArray = await response.stream.toBytes().timeout(const Duration(seconds: 15));
But none of the above timeout works.
Using http package, this is my approach :
Create a Streamed Response that we're going to use for onTimeOut callback
StreamedResponse timeOutResponse({
#required String httpMethod,
#required dynamic error,
#required String url,
}) {
Map<String, dynamic> body = {
'any': 'value',
'you': 'want for $error',
};
int statusCode = 404;
Uri destination = Uri.parse(url);
String json = jsonEncode(body);
return StreamedResponse(
Stream.value(json.codeUnits),
statusCode,
request: Request(httpMethod, destination),
);
}
Use the modified http multipart function from Mahesh Jamdade answer
Future<http.Response> makeAnyHttpRequest(String url,
Map<String, dynamic> body,
{Function onTimeout,
Duration duration = const Duration(seconds: 10)}) async {
final request = http.MultipartRequest(
'POST',
Uri.parse('$url'),
);
final res = await request.send().timeout(
duration,
onTimeout: () {
return timeOutResponse(
httpMethod: 'MULTIPART POST',
error: 'Request Time Out',
url: url,
);
},
);
return await http.Response.fromStream(res);
}
this way, instead of timeout exception, you can return the onTimeOut Http Response.
Use Dio package with following code:
try {
final response = await Dio().post(requestFinal.item1, data:formData, options: option,
onSendProgress: (sent, total) {
print("uploadFile ${sent / total}");
});
print("Response Status code:: ${response.statusCode}");
if (response.statusCode >= 200 && response.statusCode < 299) {
dynamic jsonResponse = response.data;
print("response body :: $jsonResponse");
final message = jsonResponse["msg"] ?? '';
final status = jsonResponse["status"] ?? 400;
final data = jsonResponse["data"];
return HttpResponse(status: status, errMessage: message, json: data);
}
else {
dynamic jsonResponse = response.data;
print('*********************************************************');
print("response body :: $jsonResponse");
print('*********************************************************');
var errMessage = jsonResponse["msg"];
return HttpResponse(status: response.statusCode, errMessage: errMessage, json: jsonResponse);
}
}
on DioError catch(error) {
print('*********************************************************');
print('Error Details :: ${error.message}');
print('*********************************************************');
dynamic jsonResponse = error.response.data;
print('*********************************************************');
print("response body :: $jsonResponse");
print('*********************************************************');
var errMessage = jsonResponse["message"] ?? "Something went wrong";
return HttpResponse(status: jsonResponse["status"] , errMessage: errMessage, json: null);
}
Hope this helps!
I suggest that
var request = http.MultipartRequest("POST", Uri.parse(EMPLOYEE_PUNCH_IN_URL));
request.fields['uid'] = userId;
request.fields['location'] = location;
request.fields['punchin_time'] = punchInTime;
request.fields['punchin_location_name'] = address;
var multiPartFile = await http.MultipartFile.fromPath(
"photo", imageFile.path,
contentType: MediaType("image", "$extension"));
request.files.add(multiPartFile);
await request.send().timeout(Duration(seconds: 1), onTimeout: () {
throw "TimeOut";
}).then((onValue) {
var responseByteArray = await onValue.stream.toBytes();
employeePunchInModel = standardSerializers.deserializeWith(
EmployeePunchInModel.serializer,
json.decode(utf8.decode(responseByteArray)));
}).catchError((){ throw "TimeOut";});
hey you can also use dio 3.0.4
A powerful Http client for Dart, which supports Interceptors, Global configuration, FormData, Request Cancellation, File downloading, Timeout etc.
Here is the link :Http client for Dart
You can try this which uses http package
declare your multipart function like this with your desired arguments
Future<http.Response> makeAnyHttpRequest(String url,
Map<String, dynamic> body,
{Function onTimeout,
Duration duration = const Duration(seconds: 10)}) async {
final request = http.MultipartRequest(
'POST',
Uri.parse('$url'),
);
final res = await request.send().timeout(duration, onTimeout: onTimeout);
return await http.Response.fromStream(res);
}
and then call it within a try catch block and you can catch the timeout exception by throwing the desired value on Timeout.
try{
final res = makeAnyHttpRequest("<url>",{"body":"here"},onTimeout:(){
throw 'TIME_OUT'; // Throw anything
});
}catch(_){
if (_.toString() == 'TIME_OUT') { // catch the thrown value to detect TIMEOUT
/// DO SOMETHING ON TIMEOUT
debugPrint('The request Timeout');
}
}
}
The above approach would work for any http request as long as you have a onTimeout call back
Related
I'm doing a post request to an ASP.Net Web API to acquire a token. I am able to do this successfully with the dart HTTP package as follow:
Uri address = Uri.parse('https://myaddress:myport/token');
var response = await http.post(
address,
body: {
'username': 'MyUsername',
'password': 'MyPassword',
'grant_type': 'password'
},
).timeout(Duration(seconds: 20));
return response.body;
No problem with Postman either:
Now I want to do the same with the base dart:io class, as the testing server has a self signed certificate which I found the HTTP package has no bypass for (might be wrong), but for the life of me I cannot figure out where I am going wrong as when I debug the server the requests never get hit with the following code:
Uri address = Uri.parse('https://myaddress:myport/token');
HttpClient httpClient = HttpClient();
httpClient.connectionTimeout = Duration(seconds: 20);
httpClient.badCertificateCallback = ((X509Certificate cert, String host, int port) => true); // Allow self signed certificates
HttpClientRequest request = await httpClient.postUrl(address);
final Map<String, String> payLoad = {
'username': 'MyUsername',
'password': 'MyPassword',
'grant_type': 'password'
};
request.headers.contentType = new ContentType("application", "x-www-form-urlencoded", charset: "utf-8");
request.add(utf8.encode(json.encode(payLoad)));
// request.write(payLoad);
HttpClientResponse response = await request.close();
String responseBody = await response.transform(utf8.decoder).join();
httpClient.close();
responseBody is always:
"{"error":"unsupported_grant_type"}"
So I assume my encoding or structure is wrong, but with all possibilities I have tried, nothing works, any help would be appreciated.
i did the same but in my case i am requesting a soap web service, the bellow code do the job for me i hope it will for you
Future<XmlDocument> sendSoapRequest(String dataRequest) async {
final startTime = Stopwatch()..start();
_attemptsRequest = 0;
bool successful = false;
String dataResponse;
try {
Uri uri = Uri.parse('https://address:port/ADService');
var httpClient = HttpClient();
httpClient.connectionTimeout = Duration(milliseconds: 5000);
httpClient.idleTimeout = Duration(milliseconds: 5000);
httpClient.badCertificateCallback = ((X509Certificate cert, String host, int port) => true); // Allow self signed certificates
await httpClient
.openUrl('POST', uri)
.then((HttpClientRequest request) async {
request.headers.contentType =
new ContentType('application', 'text/xml', charset: 'UTF-8');
_attemptsRequest++;
request.write(dataRequest);
await request.close().then((HttpClientResponse response) async {
// var data = await response.transform(utf8.decoder).join();
// i didn't use this method cause it disorganize the response when there is high level of data, -i get binary data from the server-
var data = await utf8.decoder.bind(response).toList();
dataResponse = data.join();
successful = true;
httpClient.close();
});
_timeRequest = startTime.elapsed.inMilliseconds;
});
} catch (e) {
if (_attemptsRequest >= getAttempts) {
_timeRequest = startTime.elapsed.inMilliseconds;
if (e is SocketException)
throw Exception('Timeout exception, operation has expired: $e');
throw Exception('Error sending request: $e');
} else {
sleep(const Duration(milliseconds: 500));
}
}
try {
if (successful) {
XmlDocument doc;
doc = parse(dataResponse);
return doc;
} else {
return null;
}
} catch (e) {
throw Exception('Error converting response to Document: $e');
}
}
Im using this api to upload a mp3 file
using this method
Future<void> uploadRecord(String matchId, String filePath) async {
Uri url = Uri.parse(
Urls.baseurl + EndPoints.uploadRecordEndPoint + '${auth.token}');
final request = http.MultipartRequest('POST', url)
..fields['match_id'] = matchId
..files.add(http.MultipartFile.fromBytes(
'file', await File.fromUri(Uri(path: filePath)).readAsBytes(),
contentType: MediaType('audio', 'mpeg')));
final response = await request.send();
final responseStr = await response.stream.bytesToString();
print(responseStr);
}
but it doesn't work, it seems that no file uploading, am i missing something ? or is there any better solution ?
Please use flutter_upload package for uploading file
Or use below code for uploading the file using multipart :
static Future<String> fileUploadMultipart(
{File file, OnUploadProgressCallback onUploadProgress}) async {
assert(file != null);
final url = '$baseUrl/api/file';
final httpClient = getHttpClient();
final request = await httpClient.postUrl(Uri.parse(url));
int byteCount = 0;
var multipart = await http.MultipartFile.fromPath(fileUtil.basename(file.path), file.path);
// final fileStreamFile = file.openRead();
// var multipart = MultipartFile("file", fileStreamFile, file.lengthSync(),
// filename: fileUtil.basename(file.path));
var requestMultipart = http.MultipartRequest("", Uri.parse("uri"));
requestMultipart.files.add(multipart);
var msStream = requestMultipart.finalize();
var totalByteLength = requestMultipart.contentLength;
request.contentLength = totalByteLength;
request.headers.set(
HttpHeaders.contentTypeHeader, requestMultipart.headers[HttpHeaders.contentTypeHeader]);
Stream<List<int>> streamUpload = msStream.transform(
new StreamTransformer.fromHandlers(
handleData: (data, sink) {
sink.add(data);
byteCount += data.length;
if (onUploadProgress != null) {
onUploadProgress(byteCount, totalByteLength);
// CALL STATUS CALLBACK;
}
},
handleError: (error, stack, sink) {
throw error;
},
handleDone: (sink) {
sink.close();
// UPLOAD DONE;
},
),
);
await request.addStream(streamUpload);
final httpResponse = await request.close();
//
var statusCode = httpResponse.statusCode;
if (statusCode ~/ 100 != 2) {
throw Exception('Error uploading file, Status code: ${httpResponse.statusCode}');
} else {
return await readResponseAsString(httpResponse);
}
}
Try to add filename to the
http.MultipartFile.fromBytes()
I'm trying to do a http post request and I need to specify the body as form-data, because the server don't take the request as raw.
This is what I'm doing:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
postTest() async {
final uri = 'https://na57.salesforce.com/services/oauth2/token';
var requestBody = {
'grant_type':'password',
'client_id':'3MVG9dZJodJWITSviqdj3EnW.LrZ81MbuGBqgIxxxdD6u7Mru2NOEs8bHFoFyNw_nVKPhlF2EzDbNYI0rphQL',
'client_secret':'42E131F37E4E05313646E1ED1D3788D76192EBECA7486D15BDDB8408B9726B42',
'username':'example#mail.com.us',
'password':'ABC1234563Af88jesKxPLVirJRW8wXvj3D'
};
http.Response response = await http.post(
uri,
body: json.encode(requestBody),
);
print(response.body);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Container(
child: Center(
child: RaisedButton(
child: Text('Press Here'),
onPressed: (){
postTest();
},
),
),
),
);
}
}
This is the actual response:
{
"error": "unsupported_grant_type",
"error_description": "grant type not supported"
}
This is the expected response:
{
"access_token": "00D0b000000Bb08!AR8AQO.s8mAGXCbwV77FXNLQqc2vtl8g6_16miVbgWlQMsuNHsaf2IGLUwnMVXBOfAj19iznhqhwlPOi4tagvf7FFgiJJgoi",
"instance_url": "https://na57.salesforce.com",
"id": "https://login.salesforce.com/id/00D0b000000Bb08EAC/0050b000005nstiAAA",
"token_type": "Bearer",
"issued_at": "1567993324968",
"signature": "1+Zd/dSh9i7Moh2U0nFJLdXkVHqPlPVU6emwdYzXDPk="
}
You can test this on postman switching the body between raw (you get the actual response) and form-data (you get the expected response)
PS: The headers are temporary headers created by the client tool.
Use Map instead, because body in http package only has 3 types: String, List or Map. Try this:
final uri = 'https://na57.salesforce.com/services/oauth2/token';
var map = new Map<String, dynamic>();
map['grant_type'] = 'password';
map['client_id'] = '3MVG9dZJodJWITSviqdj3EnW.LrZ81MbuGBqgIxxxdD6u7Mru2NOEs8bHFoFyNw_nVKPhlF2EzDbNYI0rphQL';
map['client_secret'] = '42E131F37E4E05313646E1ED1D3788D76192EBECA7486D15BDDB8408B9726B42';
map['username'] = 'example#mail.com.us';
map['password'] = 'ABC1234563Af88jesKxPLVirJRW8wXvj3D';
http.Response response = await http.post(
uri,
body: map,
);
There is a dart package dio
it works like a charm, am using it as a standard to do http requests.
Please read the docs too on sending form data with dio package
import 'package:dio/dio.dart';
postData(Map<String, dynamic> body)async{
var dio = Dio();
try {
FormData formData = new FormData.fromMap(body);
var response = await dio.post(url, data: formData);
return response.data;
} catch (e) {
print(e);
}
}
Use MultipartRequest class
A multipart/form-data request automatically sets the Content-Type header to multipart/form-data.
This value will override any value set by the user.
refer pub.dev doc here
For example:
Map<String, String> requestBody = <String,String>{
'field1':value1
};
Map<String, String> headers= <String,String>{
'Authorization':'Basic ${base64Encode(utf8.encode('user:password'))}'
};
var uri = Uri.parse('http://localhost.com');
var request = http.MultipartRequest('POST', uri)
..headers.addAll(headers) //if u have headers, basic auth, token bearer... Else remove line
..fields.addAll(requestBody);
var response = await request.send();
final respStr = await response.stream.bytesToString();
return jsonDecode(respStr);
Hope this helps
So, you wanna send the body as form-data right? maybe you can try this? for me it's work
postTest() async {
final uri = 'https://na57.salesforce.com/services/oauth2/token';
var requestBody = {
'grant_type':'password',
'client_id':'3MVG9dZJodJWITSviqdj3EnW.LrZ81MbuGBqgIxxxdD6u7Mru2NOEs8bHFoFyNw_nVKPhlF2EzDbNYI0rphQL',
'client_secret':'42E131F37E4E05313646E1ED1D3788D76192EBECA7486D15BDDB8408B9726B42',
'username':'example#mail.com.us',
'password':'ABC1234563Af88jesKxPLVirJRW8wXvj3D'
};
http.Response response = await http.post(
uri,
body: requestBody,
);
print(response.body);
}
Or
postTest() async {
final uri = 'https://na57.salesforce.com/services/oauth2/token';
http.Response response = await http.post(
uri, body: {
'grant_type':'password',
'client_id':'3MVG9dZJodJWITSviqdj3EnW.LrZ81MbuGBqgIxxxdD6u7Mru2NOEs8bHFoFyNw_nVKPhlF2EzDbNYI0rphQL',
'client_secret':'42E131F37E4E05313646E1ED1D3788D76192EBECA7486D15BDDB8408B9726B42',
'username':'example#mail.com.us',
'password':'ABC1234563Af88jesKxPLVirJRW8wXvj3D'
});
print(response.body);
}
Edit 1 (this worked for code login flow):
String url = "https://login.salesforce.com/services/oauth2/token";
http.post(url, body: {
"grant_type": "authorization_code",
"client_id": "some_client_id",
"redirect_uri": "some_redirect_uri",
"code": "some_code_generated_by_salesforce_login",
"client_secret": "some_client_secret",
}).then((response) {
//--handle response
});
give 'FormData' a try from:
import 'package:dio/dio.dart';
FormData formData = new FormData.fromMap(dataMap);
retrofitClient.getToken(formData).then((response){//--handle respnse--});
'retrofitClient' is from package
retrofit: ^1.0.1+1
Can you try this;
String url = 'https://myendpoint.com';
Map<String, String> headers = {
"Content-Type": "application/x-www-form-urlencoded"
"Content-type": "application/json"};
String json = '{"grant_type":"password",
"username":"myuser#mail.com",
"password":"123456"}';
// make POST request
Response response = await post(url, headers: headers, body: json);
// check the status code for the result
int statusCode = response.statusCode;
// this API passes back the id of the new item added to the body
String body = response.body;
This is my example with form data function
Future<ResponseModel> postWithFormData(String url, List<File> files,
{Map<String, String> body = const {}, bool throwAlert = false}) async {
var request = http.MultipartRequest("POST", Uri.parse(localApiHost + url));
request.headers
.addAll({"Authorization": "Bearer ${Storage.getString(token)}"});
request.fields.addAll(body);
for (var file in files) {
request.files.add(await http.MultipartFile.fromPath("files", file.path));
}
var sendRequest = await request.send();
var response = await http.Response.fromStream(sendRequest);
final responseData = json.decode(response.body);
if (response.statusCode >= 400 && throwAlert) {
showErrorDialog(responseData["message"]);
}
return ResponseModel(body: responseData, statusCode: response.statusCode);
}
HTTP does not support form-data yet!
Use DIO instead. It will handle everything on its own!
This code snippets successfully executes a POST api call which expect an authorization token and form-data.
final headers = {'Authorization': 'Bearer $authToken'};
var requestBody = {
'shopId': '5',
'fromDate': '01/01/2021',
'toDate': '01/10/2022',
};
final response = await http.post(
Uri.parse(
'https://api.sample.com/mobile/dashboard/getdetails'),
headers: headers,
body: requestBody,
);
print("RESPONSE ${response.body}");
Using POSTMAN to test the query and get the format is quite useful. This is allow you to see if you really need to set Headers. See my example below. I hope it helps and it is not too much
import 'dart:convert';
import 'package:http/http.dart';
class RegisterUser{
String fullname;
String phonenumber;
String emailaddress;
String password;
Map data;
RegisterUser({this.fullname, this.phonenumber, this.emailaddress, this.password});
Future<void> registeruseraction() async {
String url = 'https://api.url.com/';
Response response = await post(url, body: {
'fullname' : fullname,
'phonenumber' : phonenumber,
'emailaddress' : emailaddress,
'password' : password
});
print(response.body);
data = jsonDecode(response.body);
}
}
You can also use MultiPartRequest, it will work for sure
var request = new
http.MultipartRequest("POST",Uri.parse("$baseUrl/example/"));
request.headers.addAll(baseHeader);
request.fields['id'] = params.id.toString();
request.fields['regionId'] = params.regionId.toString();
request.fields['districtId'] = params.districtId.toString();
http.Response response = await http.Response.fromStream(await
request.send());
print('Uploaded! ${response.body} ++ ${response.statusCode}');
import 'package:http/http.dart' as http;
// Function to make the POST request
Future<http.Response> post(String url, Map<String, String> body) async {
// Encode the body of the request as JSON
var encodedBody = json.encode(body);
// Make the POST request
var response = await http.post(url,
headers: {"Content-Type": "application/json"}, body: encodedBody);
// Return the response
return response;
}
I am trying to use Interceptor with Dio in flutter, I have to handle Token expire.
following is my code
Future<Dio> getApiClient() async {
token = await storage.read(key: USER_TOKEN);
_dio.interceptors.clear();
_dio.interceptors
.add(InterceptorsWrapper(onRequest: (RequestOptions options) {
// Do something before request is sent
options.headers["Authorization"] = "Bearer " + token;
return options;
},onResponse:(Response response) {
// Do something with response data
return response; // continue
}, onError: (DioError error) async {
// Do something with response error
if (error.response?.statusCode == 403) {
// update token and repeat
// Lock to block the incoming request until the token updated
_dio.interceptors.requestLock.lock();
_dio.interceptors.responseLock.lock();
RequestOptions options = error.response.request;
FirebaseUser user = await FirebaseAuth.instance.currentUser();
token = await user.getIdToken(refresh: true);
await writeAuthKey(token);
options.headers["Authorization"] = "Bearer " + token;
_dio.interceptors.requestLock.unlock();
_dio.interceptors.responseLock.unlock();
_dio.request(options.path, options: options);
} else {
return error;
}
}));
_dio.options.baseUrl = baseUrl;
return _dio;
}
problem is instead of repeating the network call with the new token, Dio is returning the error object to the calling method, which in turn is rendering the wrong widget, any leads on how to handle token refresh with dio?
I have found a simple solution that looks like the following:
this.api = Dio();
this.api.interceptors.add(InterceptorsWrapper(
onError: (error) async {
if (error.response?.statusCode == 403 ||
error.response?.statusCode == 401) {
await refreshToken();
return _retry(error.request);
}
return error.response;
}));
Basically what is going on is it checks to see if the error is a 401 or 403, which are common auth errors, and if so, it will refresh the token and retry the response. My implementation of refreshToken() looks like the following, but this may vary based on your api:
Future<void> refreshToken() async {
final refreshToken = await this._storage.read(key: 'refreshToken');
final response =
await this.api.post('/users/refresh', data: {'token': refreshToken});
if (response.statusCode == 200) {
this.accessToken = response.data['accessToken'];
}
}
I use Flutter Sercure Storage to store the accessToken. My retry method looks like the following:
Future<Response<dynamic>> _retry(RequestOptions requestOptions) async {
final options = new Options(
method: requestOptions.method,
headers: requestOptions.headers,
);
return this.api.request<dynamic>(requestOptions.path,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters,
options: options);
}
If you want to easily allows add the access_token to the request I suggest adding the following function when you declare your dio router with the onError callback:
onRequest: (options) async {
options.headers['Authorization'] = 'Bearer: $accessToken';
return options;
},
I solved it using interceptors in following way :-
Future<Dio> getApiClient() async {
token = await storage.read(key: USER_TOKEN);
_dio.interceptors.clear();
_dio.interceptors
.add(InterceptorsWrapper(onRequest: (RequestOptions options) {
// Do something before request is sent
options.headers["Authorization"] = "Bearer " + token;
return options;
},onResponse:(Response response) {
// Do something with response data
return response; // continue
}, onError: (DioError error) async {
// Do something with response error
if (error.response?.statusCode == 403) {
_dio.interceptors.requestLock.lock();
_dio.interceptors.responseLock.lock();
RequestOptions options = error.response.request;
FirebaseUser user = await FirebaseAuth.instance.currentUser();
token = await user.getIdToken(refresh: true);
await writeAuthKey(token);
options.headers["Authorization"] = "Bearer " + token;
_dio.interceptors.requestLock.unlock();
_dio.interceptors.responseLock.unlock();
return _dio.request(options.path,options: options);
} else {
return error;
}
}));
_dio.options.baseUrl = baseUrl;
return _dio;
}
Dio 4.0.0 Support
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (request, handler) {
if (token != null && token != '')
request.headers['Authorization'] = 'Bearer $token';
return handler.next(request);
},
onError: (e, handler) async {
if (e.response?.statusCode == 401) {
try {
await dio
.post(
"https://refresh.api",
data: jsonEncode(
{"refresh_token": refreshtoken}))
.then((value) async {
if (value?.statusCode == 201) {
//get new tokens ...
print("access token" + token);
print("refresh token" + refreshtoken);
//set bearer
e.requestOptions.headers["Authorization"] =
"Bearer " + token;
//create request with new access token
final opts = new Options(
method: e.requestOptions.method,
headers: e.requestOptions.headers);
final cloneReq = await dio.request(e.requestOptions.path,
options: opts,
data: e.requestOptions.data,
queryParameters: e.requestOptions.queryParameters);
return handler.resolve(cloneReq);
}
return e;
});
return dio;
} catch (e, st) {
}
}
},
),
);
I modify John Anderton's answer. I agree that it is better approach to check the token(s) before you actually make the request. we have to check if the tokens are expired or not, instead of making request and check the error 401 and 403.
I modify it to add some functionalities, so this interceptor can be used
to add access token to the header if it is still valid
to regenerate access token if it has expired
to navigate back to Login Page if refresh token has expired
to navigate back to Login Page if there is an error because of invalidated token (for example, revoked by the backend)
and it also work for multiple concurrent requests, and if you don't need to add token to the header (like in login endpoint), this interceptor can handle it as well. here is the interceptor
class AuthInterceptor extends Interceptor {
final Dio _dio;
final _localStorage = LocalStorage.instance; // helper class to access your local storage
AuthInterceptor(this._dio);
#override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
if (options.headers["requiresToken"] == false) {
// if the request doesn't need token, then just continue to the next interceptor
options.headers.remove("requiresToken"); //remove the auxiliary header
return handler.next(options);
}
// get tokens from local storage, you can use Hive or flutter_secure_storage
final accessToken = _localStorage.getAccessToken();
final refreshToken = _localStorage.getRefreshToken();
if (accessToken == null || refreshToken == null) {
_performLogout(_dio);
// create custom dio error
options.extra["tokenErrorType"] = TokenErrorType.tokenNotFound; // I use enum type, you can chage it to string
final error = DioError(requestOptions: options, type: DioErrorType.other);
return handler.reject(error);
}
// check if tokens have already expired or not
// I use jwt_decoder package
// Note: ensure your tokens has "exp" claim
final accessTokenHasExpired = JwtDecoder.isExpired(accessToken);
final refreshTokenHasExpired = JwtDecoder.isExpired(refreshToken);
var _refreshed = true;
if (refreshTokenHasExpired) {
_performLogout(_dio);
// create custom dio error
options.extra["tokenErrorType"] = TokenErrorType.refreshTokenHasExpired;
final error = DioError(requestOptions: options, type: DioErrorType.other);
return handler.reject(error);
} else if (accessTokenHasExpired) {
// regenerate access token
_dio.interceptors.requestLock.lock();
_refreshed = await _regenerateAccessToken();
_dio.interceptors.requestLock.unlock();
}
if (_refreshed) {
// add access token to the request header
options.headers["Authorization"] = "Bearer $accessToken";
return handler.next(options);
} else {
// create custom dio error
options.extra["tokenErrorType"] = TokenErrorType.failedToRegenerateAccessToken;
final error = DioError(requestOptions: options, type: DioErrorType.other);
return handler.reject(error);
}
}
#override
void onError(DioError err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 403 || err.response?.statusCode == 401) {
// for some reasons the token can be invalidated before it is expired by the backend.
// then we should navigate the user back to login page
_performLogout(_dio);
// create custom dio error
err.type = DioErrorType.other;
err.requestOptions.extra["tokenErrorType"] = TokenErrorType.invalidAccessToken;
}
return handler.next(err);
}
void _performLogout(Dio dio) {
_dio.interceptors.requestLock.clear();
_dio.interceptors.requestLock.lock();
_localStorage.removeTokens(); // remove token from local storage
// back to login page without using context
// check this https://stackoverflow.com/a/53397266/9101876
navigatorKey.currentState?.pushReplacementNamed(LoginPage.routeName);
_dio.interceptors.requestLock.unlock();
}
/// return true if it is successfully regenerate the access token
Future<bool> _regenerateAccessToken() async {
try {
var dio = Dio(); // should create new dio instance because the request interceptor is being locked
// get refresh token from local storage
final refreshToken = _localStorage.getRefreshToken();
// make request to server to get the new access token from server using refresh token
final response = await dio.post(
"https://yourDomain.com/api/refresh",
options: Options(headers: {"Authorization": "Bearer $refreshToken"}),
);
if (response.statusCode == 200 || response.statusCode == 201) {
final newAccessToken = response.data["accessToken"]; // parse data based on your JSON structure
_localStorage.saveAccessToken(newAccessToken); // save to local storage
return true;
} else if (response.statusCode == 401 || response.statusCode == 403) {
// it means your refresh token no longer valid now, it may be revoked by the backend
_performLogout(_dio);
return false;
} else {
print(response.statusCode);
return false;
}
} on DioError {
return false;
} catch (e) {
return false;
}
}
}
usage
final dio = Dio();
dio.options.baseUrl = "https://yourDomain.com/api";
dio.interceptors.addAll([
AuthInterceptor(dio), // add this line before LogInterceptor
LogInterceptor(),
]);
if your request doesn't need token in the header (like in the login endpoint), then you should make request like this
await dio.post(
"/login",
data: loginData,
options: Options(headers: {"requiresToken": false}), // add this line
);
otherwise, just make a regular request without adding token to the header option, the interceptor will automatically handle it.
await dio.get("/user", data: myData);
I think that a better approach is to check the token(s) before you actually make the request. That way you have less network traffic and the response is faster.
EDIT: Another important reason to follow this approach is because it is a safer one, as X.Y. pointed out in the comment section
In my example I use:
http: ^0.13.3
dio: ^4.0.0
flutter_secure_storage: ^4.2.0
jwt_decode: ^0.3.1
flutter_easyloading: ^3.0.0
The idea is to first check the expiration of tokens (both access and refresh).
If the refresh token is expired then clear the storage and redirect to LoginPage.
If the access token is expired then (before submit the actual request) refresh it by using the refresh token, and then use the refreshed credentials to submit the original request. In that way you minimize the network traffic and you take the response way faster.
I did this:
AuthService appAuth = new AuthService();
class AuthService {
Future<void> logout() async {
token = '';
refresh = '';
await Future.delayed(Duration(milliseconds: 100));
Navigator.of(cnt).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => LoginPage()),
(_) => false,
);
}
Future<bool> login(String username, String password) async {
var headers = {'Accept': 'application/json'};
var request = http.MultipartRequest('POST', Uri.parse(baseURL + 'token/'));
request.fields.addAll({'username': username, 'password': password});
request.headers.addAll(headers);
http.StreamedResponse response = await request.send();
if (response.statusCode == 200) {
var resp = await response.stream.bytesToString();
final data = jsonDecode(resp);
token = data['access'];
refresh = data['refresh'];
secStore.secureWrite('token', token);
secStore.secureWrite('refresh', refresh);
return true;
} else {
return (false);
}
}
Future<bool> refreshToken() async {
var headers = {'Accept': 'application/json'};
var request =
http.MultipartRequest('POST', Uri.parse(baseURL + 'token/refresh/'));
request.fields.addAll({'refresh': refresh});
request.headers.addAll(headers);
http.StreamedResponse response = await request.send();
if (response.statusCode == 200) {
final data = jsonDecode(await response.stream.bytesToString());
token = data['access'];
refresh = data['refresh'];
secStore.secureWrite('token', token);
secStore.secureWrite('refresh', refresh);
return true;
} else {
print(response.reasonPhrase);
return false;
}
}
}
After that create the interceptor
import 'package:dio/dio.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import '../settings/globals.dart';
class AuthInterceptor extends Interceptor {
static bool isRetryCall = false;
#override
void onRequest(
RequestOptions options, RequestInterceptorHandler handler) async {
bool _token = isTokenExpired(token);
bool _refresh = isTokenExpired(refresh);
bool _refreshed = true;
if (_refresh) {
appAuth.logout();
EasyLoading.showInfo(
'Expired session');
DioError _err;
handler.reject(_err);
} else if (_token) {
_refreshed = await appAuth.refreshToken();
}
if (_refreshed) {
options.headers["Authorization"] = "Bearer " + token;
options.headers["Accept"] = "application/json";
handler.next(options);
}
}
#override
void onResponse(Response response, ResponseInterceptorHandler handler) async {
handler.next(response);
}
#override
void onError(DioError err, ErrorInterceptorHandler handler) async {
handler.next(err);
}
}
The secure storage functionality is from:
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
SecureStorage secStore = new SecureStorage();
class SecureStorage {
final _storage = FlutterSecureStorage();
void addNewItem(String key, String value) async {
await _storage.write(
key: key,
value: value,
iOptions: _getIOSOptions(),
);
}
IOSOptions _getIOSOptions() => IOSOptions(
accountName: _getAccountName(),
);
String _getAccountName() => 'blah_blah_blah';
Future<String> secureRead(String key) async {
String value = await _storage.read(key: key);
return value;
}
Future<void> secureDelete(String key) async {
await _storage.delete(key: key);
}
Future<void> secureWrite(String key, String value) async {
await _storage.write(key: key, value: value);
}
}
check expiration with:
bool isTokenExpired(String _token) {
DateTime expiryDate = Jwt.getExpiryDate(_token);
bool isExpired = expiryDate.compareTo(DateTime.now()) < 0;
return isExpired;
}
and then the original request
var dio = Dio();
Future<Null> getTasks() async {
EasyLoading.show(status: 'Wait ...');
Response response = await dio
.get(baseURL + 'tasks/?task={"foo":"1","bar":"30"}');
if (response.statusCode == 200) {
print('success');
} else {
print(response?.statusCode);
}}
As you can see the Login and refreshToken request use http package (they don't need the interceptor). The getTasks use dio and it's interceptor in order to get its response in one and only request
Dio 4.0.0
dio.interceptors.clear();
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (request, handler) {
if (token != null && token != '')
request.headers['Authorization'] = 'Bearer $token';
return handler.next(request);
},
onError: (err, handler) async {
if (err.response?.statusCode == 401) {
try {
await dio
.post(
"https://refresh.api",
data: jsonEncode(
{"refresh_token": refreshtoken}))
.then((value) async {
if (value?.statusCode == 201) {
//get new tokens ...
print("acces token" + token);
print("refresh token" + refreshtoken);
//set bearer
err.requestOptions.headers["Authorization"] =
"Bearer " + token;
//create request with new access token
final opts = new Options(
method: err.requestOptions.method,
headers: err.requestOptions.headers);
final cloneReq = await dio.request(err.requestOptions.path,
options: opts,
data: err.requestOptions.data,
queryParameters: err.requestOptions.queryParameters);
return handler.resolve(cloneReq);
}
return err;
});
return dio;
} catch (err, st) {
}
}
},
),
);
Dio 4.0.2 deprecates Interceptor locks. QueuedInterceptor should be used instead.
From the docs:
Locks of interceptors were originally designed to synchronize interceptor execution, but locks have a problem that once it becomes unlocked all of the requests run at once, rather than executing sequentially. Now QueuedInterceptor can do it better.
QueuedInterceptor provides a mechanism for sequential access(one by one) to interceptors.
An example of AuthInterceptor implemented using QueuedInterceptor:
/// Adds Authorization header with a non-expired bearer token.
///
/// Logic:
/// 1. Check if the endpoint requires authentication
/// - If not, bypass interceptor
/// 2. Get a non-expired access token
/// - AuthRepository takes care of refreshing the token if it is expired
/// 3. Make API call (attaching token in Authorization header)
/// 4. If response if 401 (e.g. a not expired access token that was revoked by backend),
/// force refresh access token and retry call.
///
/// For non-authenticated endpoints add the following header to bypass this interceptor:
/// `Authorization: None`
///
/// For endpoints with optional authentication provide the following header:
/// `Authorization: Optional`
/// - If user is not authenticated: the Authorization header will be removed
/// and the call will be performed without it.
/// - If the user is authenticated: the authentication token will be attached in the
/// Authorization header.
class AuthInterceptor extends QueuedInterceptor {
AuthInterceptor({
required this.dio,
required this.authRepository,
this.retries = 3,
});
/// The original dio
final Dio dio;
final AuthRepository authRepository;
/// The number of retries in case of 401
final int retries;
#override
Future<void> onRequest(
final RequestOptions options,
final RequestInterceptorHandler handler,
) async {
// Non-authenticated endpoint -> bypass this interceptor
if (options._requiresNoAuthentication()) {
options._removeAuthenticationHeader();
return handler.next(options);
}
// Get auth token
final authTokenRes = await authRepository.getAuthToken();
authTokenRes.fold(
success: (final authToken) {
// Add auth token in Authorization header
options._setAuthenticationHeader(authToken.token);
handler.next(options);
},
failure: (final e) async {
// Skip authentication header if it is optional and user is not authenticated
if (e is UserNoAuthenticatedException && options._hasOptionalAuthentication()) {
options._removeAuthenticationHeader();
return handler.next(options);
}
// Handle auth token errors
await _onErrorRefreshingToken();
final error = DioError(requestOptions: options, error: e);
handler.reject(error);
},
);
}
#override
Future<void> onError(final DioError err, final ErrorInterceptorHandler handler) async {
if (err.response?.statusCode != 401) {
return super.onError(err, handler);
}
// Check retry attempt
final attempt = err.requestOptions._retryAttempt + 1;
if (attempt > retries) {
return super.onError(err, handler);
}
err.requestOptions._retryAttempt = attempt;
await Future<void>.delayed(const Duration(seconds: 1));
// Force refresh auth token
final authTokenRes = await authRepository.getAuthToken(forceRefresh: true);
authTokenRes.fold(
success: (final authToken) async {
// Add new auth token in Authorization header and retry call
try {
final options = err.requestOptions.._setAuthenticationHeader(authToken.token);
final response = await dio.fetch<void>(options);
handler.resolve(response);
} on DioError catch (e) {
if (e.response?.statusCode == 401) {
await _onErrorRefreshingToken();
}
super.onError(e, handler);
}
},
failure: (final e) async {
// Handle auth token errors
await _onErrorRefreshingToken();
final error = DioError(requestOptions: err.requestOptions, error: authTokenRes.error);
return handler.next(error);
},
);
}
Future<void> _onErrorRefreshingToken() async {
await authRepository.signOut();
}
}
extension AuthRequestOptionsX on RequestOptions {
bool _requiresNoAuthentication() => headers['Authorization'] == 'None';
bool _hasOptionalAuthentication() => headers['Authorization'] == 'Optional';
void _setAuthenticationHeader(final String token) => headers['Authorization'] = 'Bearer $token';
void _removeAuthenticationHeader() => headers.remove('Authorization');
int get _retryAttempt => (extra['auth_retry_attempt'] as int?) ?? 0;
set _retryAttempt(final int attempt) => extra['auth_retry_attempt'] = attempt;
}
Notes:
In my case AuthRepository is a wrapper of FirebaseAuth. The Firebase SDK takes care of providing a non-expired token when getAuthToken() is called.
AuthRepository.getAuthToken() returns a Future<Result<AuthToken, AuthException>>. My Result object is similar to the one provided in Result package.
You would get a response status code as 401 for token expiration. In order to request new access token, you need to use post method along with form data and required Dio's options (content-type and headers). Below is the code shows how to request new token.
After successful request, if you get the response status code as 200, then you will get new access token value along with refresh token value and save them in any storage you prefer to use. For example, Shared preferences.
Once you have new access token saved, you can use it to fetch data using get method shown in the same code below.
onError(DioError error) async {
if (error.response?.statusCode == 401) {
Response response;
var authToken = base64
.encode(utf8.encode("username_value" + ":" + "password_value"));
FormData formData = new FormData.from(
{"grant_type": "refresh_token", "refresh_token": refresh_token_value});
response = await dio.post(
url,
data: formData,
options: new Options(
contentType: ContentType.parse("application/x-www-form-urlencoded"),
headers: {HttpHeaders.authorizationHeader: 'Basic $authToken'}),
);
if (response.statusCode == 200) {
response = await dio.get(
url,
options: new Options(headers: {
HttpHeaders.authorizationHeader: 'Bearer access_token_value'
}),
);
return response;
} else {
print(response.data);
return null;
}
}
return error;
}
Below is a snippet from my interceptor
dio.interceptors
.add(InterceptorsWrapper(onRequest: (RequestOptions options) async {
/* Write your request logic setting your Authorization header from prefs*/
String token = await prefs.accessToken;
if (token != null) {
options.headers["Authorization"] = "Bearer " + token;
return options; //continue
}, onResponse: (Response response) async {
// Write your response logic
return response; // continue
}, onError: (DioError dioError) async {
// Refresh Token
if (dioError.response?.statusCode == 401) {
Response response;
var data = <String, dynamic>{
"grant_type": "refresh_token",
"refresh_token": await prefs.refreshToken,
'email': await prefs.userEmail
};
response = await dio
.post("api/url/for/refresh/token", data: data);
if (response.statusCode == 200) {
var newRefreshToken = response.data["data"]["refresh_token"]; // get new refresh token from response
var newAccessToken = response.data["data"]["access_token"]; // get new access token from response
prefs.refreshToken = newRefreshToken;
prefs.accessToken = newAccessToken; // to be used in the request section of the interceptor
return dio.request(dioError.request.baseUrl + dioError.request.path,
options: dioError.request);
}
}
return dioError;
}));
return dio;
}
}
it is working 100%
RestClient client;
static BaseOptions options = new BaseOptions(
connectTimeout: 5000,
receiveTimeout: 3000,
);
RemoteService() {
// or new Dio with a BaseOptions instance.
final dio = Dio(options);
dio.interceptors
.add(InterceptorsWrapper(onRequest: (RequestOptions options) async {
// Do something before request is sent
return options; //continue
}, onResponse: (Response response) async {
// Do something with response data
return response; // continue
}, onError: (DioError error) async {
// Do something with response error
if (error.response.statusCode == 401) {
Response response =
await dio.post("http://addrees-server/oauth/token",
options: Options(
headers: {
'Authorization': ApiUtils.BASIC_TOKEN,
'Content-Type': ApiUtils.CONTENT_TYPE,
},
),
queryParameters: {
"grant_type": ApiUtils.GRANT_TYPE,
"username": AppConstants.LOGIN,
"password": AppConstants.PASSWORD
});
Sessions.access_token = response.data['access_token'];
error.response.request.queryParameters
.update('access_token', (value) => Sessions.access_token);
RequestOptions options = error.response.request;
return dio.request(options.path, options: options); //continue
} else {
return error;
}
}));
client = RestClient(dio);
}
The post request is throwing an error while setting the header map.
Here is my code
Future<GenericResponse> makePostCall(
GenericRequest genericRequest) {String URL = "$BASE_URL/api/";
Map data = {
"name": "name",
"email": "email",
"mobile": "mobile",
"transportationRequired": false,
"userId": 5,
};
Map userHeader = {"Content-type": "application/json", "Accept": "application/json"};
return _netUtil.post(URL, body: data, headers:userHeader).then((dynamic res) {
print(res);
if (res["code"] != 200) throw new Exception(res["message"][0]);
return GenericResponse.fromJson(res);
});
}
but I'm getting this exception with headers.
══╡ EXCEPTION CAUGHT BY GESTURE ╞═
flutter: The following assertion was thrown while handling a gesture:
flutter: type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, String>'
flutter:
flutter: Either the assertion indicates an error in the framework itself, or we should provide substantially
flutter: more information in this error message to help you determine and fix the underlying cause.
flutter: In either case, please report this assertion by filing a bug on GitHub:
flutter: https://github.com/flutter/flutter/issues/new?template=BUG.md
flutter:
flutter: When the exception was thrown, this was the stack:
flutter: #0 NetworkUtil.post1 (package:saranam/network/network_util.dart:50:41)
flutter: #1 RestDatasource.bookPandit (package:saranam/network/rest_data_source.dart:204:21)
Anybody facing this issue? I didn't find any clue with the above log.
Try
Map<String, String> requestHeaders = {
'Content-type': 'application/json',
'Accept': 'application/json',
'Authorization': '<Your token>'
};
You can try this:
Map<String, String> get headers => {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": "Bearer $_token",
};
and then along with your http request for header just pass header as header
example:
Future<AvatarResponse> getAvatar() async {
var url = "$urlPrefix/api/v1/a/me/avatar";
print("fetching $url");
var response = await http.get(url, headers: headers);
if (response.statusCode != 200) {
throw Exception(
"Request to $url failed with status ${response.statusCode}: ${response.body}");
}
var avatar = AvatarResponse()
..mergeFromProto3Json(json.decode(response.body),
ignoreUnknownFields: true);
print(avatar);
return avatar;
}
I have done it this way passing a private key within the headers. This will also answer #Jaward:
class URLS {
static const String BASE_URL = 'https://location.to.your/api';
static const String USERNAME = 'myusername';
static const String PASSWORD = 'mypassword';
}
In the same .dart file:
class ApiService {
Future<UserInfo> getUserInfo() async {
var headers = {
'pk': 'here_a_private_key',
'authorization': 'Basic ' +
base64Encode(utf8.encode('${URLS.USERNAME}:${URLS.PASSWORD}')),
"Accept": "application/json"
};
final response = await http.get('${URLS.BASE_URL}/UserInfo/v1/GetUserInfo',
headers: headers);
if (response.statusCode == 200) {
final jsonResponse = json.decode(response.body);
return new UserInfo.fromJson(jsonResponse);
} else {
throw Exception('Failed to load data!');
}
}
}
Try this
Future<String> createPost(String url, Map newPost) async {
String collection;
try{
Map<String, String> headers = {"Content-type": "application/json"};
Response response =
await post(url, headers: headers, body: json.encode(newPost));
String responsebody = response.body;
final int statusCode = response.statusCode;
if (statusCode == 200 || statusCode == 201) {
final jsonResponse = json.decode(responsebody);
collection = jsonResponse["token"];
}
return collection;
}
catch(e){
print("catch");
}
}
Future<String> loginApi(String url) async {
Map<String, String> header = new Map();
header["content-type"] = "application/x-www-form-urlencoded";
header["token"] = "token from device";
try {
final response = await http.post("$url",body:{
"email":"test#test.com",
"password":"3efeyrett"
},headers: header);
Map<String,dynamic> output = jsonDecode(response.body);
if (output["status"] == 200) {
return "success";
}else{
return "error";
} catch (e) {
print("catch--------$e");
return "error";
}
return "";
}
void getApi() async {
SharedPreferences prefsss = await SharedPreferences.getInstance();
String tokennn = prefsss.get("k_token");
String url = 'http://yourhost.com/services/Default/Places/List';
Map<String, String> mainheader = {
"Content-type": "application/json",
"Cookie": tokennn
};
String requestBody =
'{"Take":100,"IncludeColumns":["Id","Name","Address","PhoneNumber","WebSite","Username","ImagePath","ServiceName","ZoneID","GalleryImages","Distance","ServiceTypeID"],"EqualityFilter":{"ServiceID":${_radioValue2 != null ? _radioValue2 : '""'},"ZoneID":"","Latitude":"${fav_lat != null ? fav_lat : 0.0}","Longitude":"${fav_long != null ? fav_long : 0.0}","SearchDistance":"${distanceZone}"},"ContainsText":"${_txtSearch}"}';
Response response = await post(url , headers: mainheader ,body:requestBody);
String parsedata = response.body;
var data = jsonDecode(parsedata);
var getval = data['Entities'] as List;
setState(() {
list = getval.map<Entities>((json) => Entities.fromJson(json)).toList();
});
}