I'm working on a Flutter app and making API calls to my ASP.NET Core backend running locally. However, I'm encountering the following error when running the app on the Android studio:
Handshake Exception: Handshake error in client (OS Error: CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate (handshake:393))
Flutter Code: Here's the part of my Flutter code that makes the HTTP request:
final url = Uri.parse(':7095/api/UserAccount/login');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({"email": "[email protected]", "password": "password"}),
);
Android Emulator:
Using Android Studio Emulator.
Accessing the backend using :7095.
Main Dart File: I added an HttpOverrides class to bypass SSL certificate verification:
class MyHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
}
}
void main() {
HttpOverrides.global = MyHttpOverrides();
runApp(MyApp());
}
ASP.NET Core Backend: My launchSettings.json includes the following:
"applicationUrl": "http://localhost:7095 ;https://localhost:5000",
What I've Tried:
-Verified the backend is running and accessible via http://localhost:7095
from the browser.
-Used 10.0.2.2:7095
for the API URL in Flutter to access localhost from the emulator.
-Added the HttpOverrides class to bypass SSL verification.
-Even disabled firewall
Issue: Despite the above, I continue to see the CERTIFICATE_VERIFY_FAILED error. The same API call works in Postman or directly from the browser, but not in the Flutter app.
Question: How can I resolve this handshake error when making HTTP requests from my Flutter app to my ASP.NET Core backend running locally? Are there additional configurations required for the Android emulator, Flutter, or ASP.NET Core backend to handle this scenario?
I'm working on a Flutter app and making API calls to my ASP.NET Core backend running locally. However, I'm encountering the following error when running the app on the Android studio:
Handshake Exception: Handshake error in client (OS Error: CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate (handshake.cc:393))
Flutter Code: Here's the part of my Flutter code that makes the HTTP request:
final url = Uri.parse('http://10.0.2.2:7095/api/UserAccount/login');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({"email": "[email protected]", "password": "password"}),
);
Android Emulator:
Using Android Studio Emulator.
Accessing the backend using http://10.0.2.2:7095.
Main Dart File: I added an HttpOverrides class to bypass SSL certificate verification:
class MyHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
}
}
void main() {
HttpOverrides.global = MyHttpOverrides();
runApp(MyApp());
}
ASP.NET Core Backend: My launchSettings.json includes the following:
"applicationUrl": "http://localhost:7095 ;https://localhost:5000",
What I've Tried:
-Verified the backend is running and accessible via http://localhost:7095
from the browser.
-Used 10.0.2.2:7095
for the API URL in Flutter to access localhost from the emulator.
-Added the HttpOverrides class to bypass SSL verification.
-Even disabled firewall
Issue: Despite the above, I continue to see the CERTIFICATE_VERIFY_FAILED error. The same API call works in Postman or directly from the browser, but not in the Flutter app.
Question: How can I resolve this handshake error when making HTTP requests from my Flutter app to my ASP.NET Core backend running locally? Are there additional configurations required for the Android emulator, Flutter, or ASP.NET Core backend to handle this scenario?
First of all: Note, that HTTPS and HTTP are different protocols. While HTTP sends a simple plain-text message to the server and awaits the response, HTTPS establishes a secure connection first. Said connection starts with a cryptographic key exchange, that we call a 'handshake'. It is important to keep that in mind.
Every URI starts with a protocol selector terminated by ://
. When you access your API with your browser, you use a http://
protocol selector (hence an HTTP connection is established - simple plain text). An URI starting with https://
however, expects a handshake first. The server and the client are exchanging the encryption details first, so they can understand each other. In short: Those encryption details depend on your certificate (and its authority chain). After a successful handshake, the actual payload data can be transmitted as encrypted bytes, only the receiver can understand. More on that can be found in RFC 2818.
When establishing a connection with HTTP, there is no encryption and no certificate to validate, so your override will never be used. That's the reason why this works, when you use your browser: Plain-Text communication, no certificates, no keys, no handshake and thus, no encryption. If you're developing for Android 9+ however, you have to explicitly allow plain text communication or the SDK will automatically assume, that you want to use a secure connection and attempts a handshake.
Network TLS enabled by default
If your app targets Android 9 or higher, the isCleartextTrafficPermitted() method returns false by default. If your app needs to enable cleartext for specific domains, you must explicitly set cleartextTrafficPermitted to true for those domains in your app's Network Security Configuration.
More information on that here.
ASP however has two endpoints configured. One for plain HTTP and one for HTTPS. Your request is directed to the HTTP endpoint. Since the server expects no key exchange, it does not understand the handshake attempt and terminates the connection immediately causing the following error:
Handshake Exception: Handshake error in client (OS Error: CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate (handshake.cc:393))
There are multiple possible solutions: