How can I add customised header on http request for authentication when using flutter graphql library? - flutter

I am using this library https://pub.dev/packages/graphql_flutter for graphql in a flutter web application. Below code can be used to get authentication token:
import 'package:graphql_flutter/graphql_flutter.dart';
final HttpLink httpLink = HttpLink(
'https://api.github.com/graphql',
);
final AuthLink authLink = AuthLink(
getToken: () async => 'Bearer <YOUR_PERSONAL_ACCESS_TOKEN>',
// OR
// getToken: () => 'Bearer <YOUR_PERSONAL_ACCESS_TOKEN>',
);
but how can I put the token in the http header like x-api-key: xxxx when sending requests?
I have tried:
HttpLink link = HttpLink(
uri: 'https://api.github.com/graphql',
headers: <String, String>{
'x-api-key': 'xxxx',
},
);
but it gives me the error: The named parameter 'uri' isn't defined. Try correcting the name to an existing named parameter's name, or defining a named parameter with the name 'uri'.

Update: base on the answer from #Moaid
import 'package:graphql_flutter/graphql_flutter.dart';
typedef GetHeaders = FutureOr<Map<String, String>> Function();
class CustomAuthLink extends Link {
CustomAuthLink({
this.getHeaders,
});
final GetHeaders getHeaders;
#override
Stream<Response> request(Request request, [NextLink forward]) {
StreamController<Response> controller;
Future<void> onListen() async {
try {
final Map<String, String> headers = await getHeaders();
return request.updateContextEntry<HttpLinkHeaders>(
(_headers) => HttpLinkHeaders(
headers: <String, String>{
...headers,
},
),
);
} catch (error) {
controller.addError(error);
}
await controller.addStream(forward(request));
await controller.close();
}
controller = StreamController<Response>(onListen: onListen);
return controller.stream;
}
}

Base on the answer from #moaid-alrazhy and after checking how AuthLink is working
class CustomAuthLink extends Link {
CustomAuthLink();
#override
Stream<Response> request(Request request, [NextLink? forward]) async* {
// Some logic here
final AuthService authService = GetIt.I.get<AuthService>();
final String? token = authService.token;
final String deviceID = await DeviceInformation.deviceIMEINumber;
// TIP: do not forget getting new Request instance!
final Request req = request.updateContextEntry<HttpLinkHeaders>(
(HttpLinkHeaders? headers) => HttpLinkHeaders(
headers: <String, String>{
// put oldest headers
...headers?.headers ?? <String, String>{},
// and add a new headers
'Authorization': 'Bearer $token',
'x-device-id': deviceID,
},
),
);
// and "return" new Request with updated headers
yield* forward!(req);
}
}

Probably if you need to change only the name of the Authentication value you can edit the headerKey param
otherwise other parameters can be insert in the "defaultHeaders" fields of the HttpLink object. but I don't know if they can be use for authentication

you can add it to your HttpLink like this
HttpLink link = HttpLink(
'https://api.github.com/graphql',
headers: <String, String>{
'x-api-key': 'xxxx',
},
);
however this was in old versions .. now for more headers your have to write your own CustomAuthLink like
typedef GetHeaders = Future<Map<String, String>> Function();
class CustomAuthLink extends Link {
CustomAuthLink({
this.getHeaders,
}) : super(
request: (Operation operation, [NextLink forward]) {
StreamController<FetchResult> controller;
Future<void> onListen() async {
try {
final Map<String, String> headers = await getHeaders();
operation.setContext(<String, Map<String, String>>{
'headers': headers
});
} catch (error) {
controller.addError(error);
}
await controller.addStream(forward(operation));
await controller.close();
}
controller = StreamController<FetchResult>(onListen: onListen);
return controller.stream;
},
);
GetHeaders getHeaders;
}

Related

How to solve ssl certificate error with GetX and Get Connect in flutter

I am trying to use Getx service.
here is my api client class as am trying to get data from internet using getx
import 'package:flutter_application_shop/utilis/app_constance.dart';
import 'package:get/get.dart';
class ApiClient extends GetConnect implements GetxService {
late String token;
final String appBaseUrl;
late Map<String, String> _mainHeaders;
ApiClient({required this.appBaseUrl}) {
baseUrl = appBaseUrl;
timeout = const Duration(seconds: 30);
token = AppConstance.TOKEN;
_mainHeaders = {
'Content-type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer $token',
};
}
Future<Response> getData(String url) async {
try {
Response response = await get(url);
return response;
} catch (e) {
return Response(statusCode: 1, statusText: e.toString());
}
}
///end
}
and when I run debug, I get this error.
I/flutter ( 6967): HandshakeException: Handshake error in client (OS Error:
I/flutter ( 6967): CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate(handshake.cc:393))
How can I solve this?
This is because the request is coming from an untrusted source, in order to bypass the error, Add allowAutoSignedCert = true; to your request in the class that extends GetConnet.
Example:
import 'package:flutter_application_shop/utilis/app_constance.dart';
import 'package:get/get.dart';
class ApiClient extends GetConnect implements GetxService {
late String token;
final String appBaseUrl;
late Map<String, String> _mainHeaders;
ApiClient({required this.appBaseUrl}) {
baseUrl = appBaseUrl;
timeout = const Duration(seconds: 30);
token = AppConstance.TOKEN;
allowAutoSignedCert = true; // the solution
_mainHeaders = {
'Content-type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer $token',
};
}
Future<Response> getData(String url) async {
try {
Response response = await get(url);
return response;
} catch (e) {
return Response(statusCode: 1, statusText: e.toString());
}
}
}

how to resend multipart request

I am retrying my api call if get 401 response but when Retrying I am ending with an following exception
following is my code for retrying multipart I had used http_interceptor package for retrying Api Calls
interceptor.dart
class AuthorizationInterceptor extends InterceptorContract {
#override
Future<BaseRequest> interceptRequest({required BaseRequest request}) async {
final prefs = await SharedPreferences.getInstance();
final extractData =
json.decode(prefs.getString('userData')!) as Map<String, dynamic>;
final Map<String, String> headers = Map.from(request.headers);
headers['Authorization'] = await extractData['accessToken'];
print(
'this is from AuthorizationInterceptor: ${extractData['accessToken']}');
// TODO: implement interceptRequest
return request.copyWith(
headers: headers,
);
}
retry.dart
class ExpiredTokenRetryPolicy extends RetryPolicy {
BuildContext context;
ExpiredTokenRetryPolicy(this.context);
#override
// TODO: implement maxRetryAttempts
int get maxRetryAttempts => 2;
#override
Future<bool> shouldAttemptRetryOnResponse(BaseResponse response) async {
if (response.statusCode == 401) {
print('retry token started');
//perform token refresh,get the new token and update it in the secure storage
await Provider.of<Auth>(context, listen: false).restoreAccessToken();
return true;
}
return false;
}
I am using interceptors in my widget following is my code where I am using interceptors and using retry policy
#override
Widget build(BuildContext context) {
var flutterFunctions = Provider.of<FlutterFunctions>(context);
// print('this is from insert package${token.token}');
ApiCalls repository = ApiCalls(
client: InterceptedClient.build(
retryPolicy: ExpiredTokenRetryPolicy(context),
interceptors: [
AuthorizationInterceptor(),
],
),
);
following is my restore access token method
Future<void> restoreAccessToken() async {
print('restoreAccessToken started');
//print(token);
final url = '${Ninecabsapi().urlHost}${Ninecabsapi().login}/$sessionId';
var response = await http.patch(
Uri.parse(url),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': accessToken!
},
body: json.encode(
{"refresh_token": refreshtoken},
),
);
var userDetails = json.decode(response.body);
if (response.statusCode == 401) {
print(userDetails['messages']);
}
sessionId = userDetails['data']['session_id'];
accessToken = userDetails['data']['access_token'];
accessTokenExpiryDate = DateTime.now().add(
Duration(seconds: userDetails['data']['access_token_expiry']),
);
refreshToken = userDetails['data']['refresh_token'];
refreshTokenExpiryDate = DateTime.now().add(
Duration(seconds: userDetails['data']['refresh_token_expiry']),
);
final userData = json.encode({
'sessionId': sessionId,
'refreshToken': refreshToken,
'refreshExpiry': refreshTokenExpiryDate!.toIso8601String(),
'accessToken': accessToken,
'accessTokenExpiry': accessTokenExpiryDate!.toIso8601String()
});
//print(userDetails);
notifyListeners();
final prefs = await SharedPreferences.getInstance();
prefs.setString('userData', userData);
print("this is from restoreAcessToken :$userDetails");
final extractData =
json.decode(prefs.getString('userData')!) as Map<String, dynamic>;
print('restore access token: ${extractData['accessToken']}');
reset();
}
As a rule. You must NOT write using the same Stream/MultipartFile more than once. If you need to retry sending to the same destination, you have to use a new MultipartFile each time you retry.

Flutter Test for API Helper class using Dio

I am using a helper class to make request to server for that I am using Dio.
I want to test the helper class
this is my helper class
import 'package:dio/dio.dart';
import '../error/exceptions.dart';
enum HttpMethod { GET, POST, PUT, DELETE }
abstract class ApiClient {
Future<Response> request(
HttpMethod method,
String path, {
Map<String, dynamic>? headers,
Map<String, dynamic>? queryParams,
dynamic body,
});
}
/// This class provides http calls using dio package
class ApiClientImpl implements ApiClient {
final Dio dio;
ApiClientImpl({required this.dio});
#override
Future<Response> request(
HttpMethod method,
String path, {
Map<String, dynamic>? headers,
Map<String, dynamic>? queryParams,
dynamic body,
}) async {
// we can add headers here which are common for every restapi call
// headers = {'content-type': 'application/json'};
try {
final _response = await dio.request(
path,
options: Options(
method: _getApiMethodString(method),
headers: headers,
),
queryParameters: queryParams,
data: body,
);
return _response;
} on DioError {
throw FetchDataException('Dio Error Occurred');
}
}
String _getApiMethodString(HttpMethod method) {
switch (method) {
case HttpMethod.GET:
return 'GET';
case HttpMethod.POST:
return 'POST';
case HttpMethod.PUT:
return 'PUT';
case HttpMethod.DELETE:
return 'DELETE';
}
}
}
The test I wrote for it is
#GenerateMocks([Dio])
void main() {
late MockDio dio;
late ApiClientImpl apiClient;
setUp(() {
dio = MockDio();
apiClient = ApiClientImpl(dio: dio);
});
group('ApiService class methods test', () {
test('Should return response when request to server is made', () async {
// arrange
var successMessage = {'message': 'Success'};
const baseUrl = 'https://example.com/';
final options = Options(method: 'GET', headers: null);
when(dio.request(baseUrl,
options: options, queryParameters: anyNamed('queryParameters')))
.thenAnswer((_) async => Response(
requestOptions: RequestOptions(path: baseUrl),
data: successMessage,
statusCode: 200));
// act
final response = await apiClient.request(HttpMethod.GET, baseUrl);
// assert
expect(response.data, successMessage);
});
});
}
I am getting an error
MissingStubError: 'request'
No stub was found which matches the arguments of this method call:
request('https://example.com/', {data: null, queryParameters: null, cancelToken: null, options: Instance of 'Options', onSendProgress: null, onReceiveProgress: null})
How Can I write a correct test for the helper class.

mocking Dio is not working to test my app in Flutter

I am trying to write unit tests for my request with Dio but I keep getting this error:
type 'Null' is not a subtype of type 'BaseOptions'
I tried adding the base options on DioMock in a lot of different ways but the test remains the same.
How can I fix it?
Bellow are my Network class and the test class.
class NetworkService {
final Dio dio;
NetworkService(this.dio){
dio.options.baseUrl = "https://food2fork.ca/api/recipe/search";
dio.options.headers['Authorization'] = 'Token 9c8b06d329136da358c2d00e76946b0111ce2c48';
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler){
print('base ${options.baseUrl}');
print("PATH: ${options.path}");
return handler.next(options);
},
onResponse: (response, handler){
print("RESPONSE: ${response.statusCode} PATH: ${response.requestOptions.path}");
return handler.next(response);
},
onError: (DioError e, handler){
print("ERROR: ${e.response?.statusCode} => PATH: ${e.requestOptions.path}");
return handler.next(e);
}
));
}
Future<List<Recipe>> getRecipe() async {
var response = await dio.get('/?page=1&query=beef');
print("response ${response.data}");
if(response.statusCode == 200){
final List<Recipe> recipeList = [];
for(Map<String, dynamic> recipe in response.data['results']){
recipeList.add(Recipe.fromJson(recipe));
}
return recipeList;
} else {
throw Exception('sss');
}
// ONBOARDING
}
}
class DioMock extends Mock implements DioForNative {}
class RecipeMock extends Mock implements Recipe {}
main() {
final dio = DioMock();
final service = NetworkService(dio);
dio.options.baseUrl = "https://food2fork.ca/api/recipe/search";
dio.options.headers = { 'Content-type': 'application/json', 'Accept': 'application/json' };
test("should return Onboarding Model", () async {
final response = Response(
requestOptions: RequestOptions(
path: 'gfh',
baseUrl: "fgh"
),
data: RecipeMock()
);
when(dio.get(
"https://food2fork.ca/api/recipe/search"))
.thenAnswer((_) async => response);
final result = await service.getRecipe();
expect(result, isA<Recipe>());
});
}
You should use https://pub.dev/packages/http_mock_adapter package to help you mock your DIO requests
See its example https://github.com/lomsa-dev/http-mock-adapter/blob/main/example/main.dart :
void main() async {
late Dio dio;
late DioAdapter dioAdapter;
Response<dynamic> response;
group('Accounts', () {
const baseUrl = 'https://example.com';
const userCredentials = <String, dynamic>{
'email': 'test#example.com',
'password': 'password',
};
setUp(() {
dio = Dio(BaseOptions(baseUrl: baseUrl));
dioAdapter = DioAdapter(dio: dio);
});
test('signs up user', () async {
const route = '/signup';
dioAdapter.onPost(
route,
(server) => server.reply(201, null),
data: userCredentials,
);
// Returns a response with 201 Created success status response code.
response = await dio.post(route, data: userCredentials);
expect(response.statusCode, 201);
});
...
final dioError = DioError(
error: {'message': 'Some beautiful error!'},
requestOptions: RequestOptions(path: path),
response: Response(
statusCode: 500,
requestOptions: RequestOptions(path: path),
),
type: DioErrorType.response,
);
test("should return a DioError", () async {
dioAdapter.onGet(
path,
(server) {
server.throws(404, dioError );
});
final result = await service.getOnboardingAnswer("lastAnswerId");
expect(result, throwsA(isA<DioError>()));
});
}
});

Flutter - How to send a POST request using HTTP in Dart?

I am using an API that converts HTTP to JSON and to get a response from the server I need to send a post request of HTML however i'm not sure how to do that?
This is my current implementation -
Future<String> test() async {
var link =
await http.get('https://example.com');
var body = parse(link.body);
return body.outerHtml;
}
Future<Album> createAlbum(String html) async {
final http.Response response = await http.post(
'https://www.html2json.com/api/v1',
headers: <String, String>{
'Content-Type': 'text/html; charset=UTF-8',
},
body: html,
);
if (response.statusCode == 200) {
return Album.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to create album.');
}
}
I call this is when my app starts like so,
#ovveride
void initState() {
test().then((body) => createAlbum(body)); //Output returns HTTP error 301, am I doing something wrong?
super.initState();
}
Checked the following code which worked for me.
Future<Album> createAlbum(String html) async {
final http.Response response = await http.post(
'https://www.html2json.com/api/v1',
headers: <String, String>{
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
body: html,
);
You need to change the Content-type to application/x-www-form-urlencoded; charset=UTF-8. This should do the trick.
Thanks to DARSH SHAH, I solved this using dio (https://pub.dev/packages/dio).
dynamic response;
Dio dio = Dio();
Future<dynamic> test() async {
dynamic link = await dio.get(
'https://example.com');
return link;
}
Future<dynamic> post(dynamic body) async {
String _baseUrl = "https://html2json.com/api/v1";
var options = Options(
contentType: "application/x-www-form-urlencoded; charset=UTF-8",
followRedirects: false,
);
final response = await dio.post(
_baseUrl,
data: body,
options: options,
);
return response;
}
FloatingActionButton(
onPressed: () async {
dynamic responseHTML = await test();
response = await post(responseHTML.data); //This will contain the JSON response
},
);