Short answer
Test the URL from a browser and an SSL checker first. If the checker reports a missing intermediate (chain) certificate, fix the server so it sends the full chain. If it only fails on old Android phones, the certificate chains to a root those phones don’t have. If it is your own dev server with a self-signed certificate, trust that certificate in debug builds only. Also check the device’s date and any proxy or antivirus that inspects HTTPS. Don’t ship badCertificateCallback returning true: it turns off the check for every request.
Why the device doesn’t trust the certificate, and the fix
01
The server doesn’t send its full certificate chain
Browsers often fill in a missing intermediate certificate from their cache, so the site looks fine there, while the app fails. Run the domain through an SSL checker. If it reports an incomplete chain, configure the server or host to serve the full chain (fullchain.pem on nginx, the CA bundle on Apache).
02
Old Android devices and an expired root
“over a third of Android devices were still running old versions of the OS which would suddenly stop trusting websites using our certificates.” Let’s Encrypt, Shortening the Let’s Encrypt Chain of Trust
Android keeps its own list of trusted roots, and older versions don’t get updates. When a certificate authority’s old root or cross-sign expires, phones on those versions stop trusting servers that chain to the newer root. If the error only shows on old phones, check which root your certificate uses. Either ask the host for a chain those phones trust, or ship the root certificate in the app as an asset and add it to SecurityContext.defaultContext at startup, or raise the app’s minimum Android version.
03
A self-signed certificate on a development server
“Add a certificate to the set of trusted X509 certificates used by SecureSocket client connections.” Dart, SecurityContext class
A local or staging server with its own certificate isn’t trusted by any device, and the same happens behind a corporate proxy, antivirus or a debugging proxy such as Charles that re-signs HTTPS traffic. Use a real certificate on staging, or add your development certificate to a SecurityContext in debug builds only. Dart’s HttpClient builds its trust list itself, so Android’s network security config (debug-overrides) only helps native clients such as a WebView, not dart:io.
final bytes = (await rootBundle.load('assets/certs/dev-ca.pem')).buffer.asUint8List(); if (kDebugMode) { SecurityContext.defaultContext.setTrustedCertificatesBytes(bytes); }04
badCertificateCallback left on in production
“If the callback returns true, the secure connection is accepted” Dart, HttpClient.badCertificateCallback
The common copied fix sets badCertificateCallback to return true. That accepts any certificate, so anyone on the same Wi-Fi can read and change the app’s traffic. If you use it while debugging, limit it to your dev host and to debug builds, and remove it before release. Packages with their own HTTP adapter, such as Dio, need the same limit in their own setup.
import 'dart:io'; import 'package:flutter/foundation.dart'; // debug builds only, one host only class DevHttpOverrides extends HttpOverrides { @override HttpClient createHttpClient(SecurityContext? context) { return super.createHttpClient(context) ..badCertificateCallback = (cert, host, port) => kDebugMode && host == 'dev.example.local'; } } void main() { if (kDebugMode) HttpOverrides.global = DevHttpOverrides(); runApp(const MyApp()); }
Checked against the vendor’s docs on : Dart API: HandshakeException class; Dart API: HttpClient.badCertificateCallback; Dart API: SecurityContext class; Let’s Encrypt: cross-sign expiration and older Android devices.
Rather hand it over?
Fixed scope, fixed price, paid upfront. The work happens in writing; no call needed.
- from $450App Fix TicketOne reproducible bug in an existing mobile or web app, diagnosed and fixed.See scope and start
- from $450AI-Built App RescueA Lovable, Bolt or Cursor app checked end to end in 3 days, then its worst problems fixed at a fixed price.No issues worth fixing, no charge. If the Launch Check finds nothing rated medium or higher, Simon refunds the full $450. You keep the report either way. This applies to the first 10 Launch Checks bought after October 30, 2026.See scope and start
Related: Flutter web “XMLHttpRequest error” · Android cleartext HTTP blocked · Node “self-signed certificate in certificate chain” · Flutter “version solving failed”