After setting up my project with a local Firebase functions emulator as my backend, and calling my Firebase onCall function from my Android emulator, I get this very non-informative error message PlatformException(functionsError, Cloud function failed with exception., {code: INTERNAL, details: null, message: INTERNAL})
. Full error message below:
E/flutter (20862): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: PlatformException(functionsError, Cloud function failed with exception., {code: INTERNAL, details: null, message: INTERNAL})
E/flutter (21445): #0 StandardMethodCodec.decodeEnvelope (package:flutter/src/services/message_codecs.dart:569:7)
E/flutter (21445): #1 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:156:18)
E/flutter (21445): <asynchronous suspension>
E/flutter (21445): #2 MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:329:12)
E/flutter (21445): #3 MethodChannelCloudFunctions.callCloudFunction (package:cloud_functions_platform_interface/src/method_channel_cloud_functions.dart:43:15)
E/flutter (21445): #4 HttpsCallable.call (package:cloud_functions/src/https_callable.dart:33:12)
E/flutter (21445): #5 ApiService.loadUserLessonsByLessonIds (package:kim/services/api.dart:28:21)
E/flutter (21445): #6 MapScreen.build.<anonymous closure> (package:kim/screens/map.dart:170:34)
E/flutter (21445): #7 _InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:779:19)
E/flutter (21445): #8 _InkResponseState.build.<anonymous closure> (package:flutter/src/material/ink_well.dart:862:36)
E/flutter (21445): #9 GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:182:24)
E/flutter (21445): #10 TapGestureRecognizer.handleTapUp (package:flutter/src/gestures/tap.dart:504:11)
E/flutter (21445): #11 BaseTapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:282:5)
E/flutter (21445): #12 BaseTapGestureRecognizer.handlePrimaryPointer (package:flutter/src/gestures/tap.dart:217:7)
E/flutter (21445): #13 PrimaryPointerGestureRecognizer.handleEvent (package:flutter/src/gestures/recognizer.dart:475:9)
E/flutter (21445): #14 PointerRouter._dispatch (package:flutter/src/gestures/pointer_router.dart:76:12)
E/flutter (21445): #15 PointerRouter._dispatchEventToRoutes.<anonymous closure> (package:flutter/src/gestures/pointer_router.dart:122:9)
E/flutter (21445): #16 _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:379:8)
E/flutter (21445): #17 PointerRouter._dispatchEventToRoutes (package:flutter/src/gestures/pointer_router.dart:120:18)
E/flutter (21445): #18 PointerRouter.route (package:flutter/src/gestures/pointer_router.dart:106:7)
E/flutter (21445): #19 GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:218:19)
E/flutter (21445): #20 GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:198:22)
E/flutter (21445): #21 GestureBinding._handlePointerEvent (package:flutter/src/gestures/binding.dart:156:7)
E/flutter (21445): #22 GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:102:7)
E/flutter (21445): #23 GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:86:7)
E/flutter (21445): #24 _rootRunUnary (dart:async/zone.dart:1196:13)
E/flutter (21445): #25 _CustomZone.runUnary (dart:async/zone.dart:1085:19)
E/flutter (21445): #26 _CustomZone.runUnaryGuarded (dart:async/zone.dart:987:7)
E/flutter (21445): #27 _invoke1 (dart:ui/hooks.dart:275:10)
E/flutter (21445): #28 _dispatchPointerDataPacket (dart:ui/hooks.dart:184:5)
E/flutter (21445):
The server code index.ts
:
import * as functions from 'firebase-functions'
interface LoadUserLessonsData {
lessonIds: string[]
}
export const loadUserLessons = functions.https.onCall((data: LoadUserLessonsData, context) => {
const uid = context.auth?.uid
const ids = data.lessonIds
console.log(uid, ids)
})
The server console window after running firebase emulators:start --only functions
:
...
+ functions[loadUserLessons]: http function initialized (http://localhost:5001/project-name/us-central1/loadUserLessons).
...
┌───────────┬────────────────┬─────────────────────────────────┐
│ Emulator │ Host:Port │ View in Emulator UI │
├───────────┼────────────────┼─────────────────────────────────┤
│ Functions │ localhost:5001 │ http://localhost:4000/functions │
└───────────┴────────────────┴─────────────────────────────────┘
The client app code api.dart
:
import 'dart:io';
import 'package:cloud_functions/cloud_functions.dart';
import 'package:myapp/services/config.dart';
class ApiService {
static final ApiService _apiService = ApiService._internal();
static final _functions = CloudFunctions.instance;
ApiService._internal();
factory ApiService() {
init();
return _apiService;
}
static void init() {
// 10.0.2.2 is the special IP address to connect to the 'localhost' of the host computer from an Android emulator.
final origin = Platform.isAndroid ? 'http://10.0.2.2:5001' : 'http://localhost:5001';
_functions.useFunctionsEmulator(origin: origin);
}
static Future<dynamic> loadUserLessons(List<String> lessonIds) {
final HttpsCallable callable = _functions.getHttpsCallable(
functionName: 'loadUserLessons',
);
return callable.call({
'lessonIds': lessonIds,
});
}
}
What can I do to correctly setup my Flutter app to connect to my local emulator?
It should be noted that the error message is the same regardless if the emulator is running or not. The emulator doesn't register any events in the console logs either. However, when the emulator is running, sending an custom request (e.g. through an Android app like "REST Api Client") to the function endpoint does make the emulator react.
This means that the problem is solely in the Flutter code (mine or cloud_functions
Flutter package). At least, that's my conclusion so far.
Luckily I had set up Crashlytics that had been given more detailed error messages, one of which was:
CLEARTEXT communication to 10.0.2.2 not permitted by network security policy
which pointed me to this Stackoverflow question and the comment made by Ashish John:
You can you 'android:usesCleartextTraffic="true"' in 'Application' tag in manifest file. This problem occurs if your API/Link doe not support https & you are using 'Android P' or above.
Reading up more on android:usesCleartextTraffic
https://developer.android.com/guide/topics/manifest/application-element
Indicates whether the app intends to use cleartext network traffic, such as cleartext HTTP. The default value for apps that target API level 27 or lower is "true". Apps that target API level 28 or higher default to "false".
In other words, if your Android emulator is running Android API level 28 or higher you need to add android:usesCleartextTraffic="true"
to the <application>
tag in your app's AndroidManifest.xml
Now we need to permit the traffic between Android emulator and the Firebase functions emulator, which Ahmed Ghrib solved.
Create a file network_security_config.xml
and place it in [PROJECT]/android/app/src/main/res/xml
with the following code:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain>10.0.2.2</domain>
</domain-config>
</network-security-config>
This is a modified version made to Ahmed Ghrib's answer, to limit clear text traffic to only be used between the Android emulator and the Firebase functions emulator.