# Flutter: “HandshakeException: Handshake error in client (OS Error: CERTIFICATE_VERIFY_FAILED)”

> Flutter or Dart fails with HandshakeException: CERTIFICATE_VERIFY_FAILED. The app doesn’t trust the server’s certificate. The causes, and the fix.

Canonical: https://app369.com/fix/flutter-handshakeexception-certificate-verify-failed/
Last updated: 2026-11-08

An HTTP call from a Flutter or Dart app fails with HandshakeException and CERTIFICATE_VERIFY_FAILED, often with unable to get local issuer certificate or certificate has expired. The secure connection was refused before any data was sent, because the device couldn’t verify the server’s certificate. It is usually the server’s setup, an old Android device, or a development server with a self-signed certificate.

**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**

1. 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).
2. 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**](https://letsencrypt.org/2023/07/10/cross-sign-expiration) 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.
3. 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**](https://api.dart.dev/dart-io/SecurityContext-class.html) 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);
}
```
4. 04 ### **badCertificateCallback left on in production**
  > “If the callback returns true, the secure connection is accepted” [**Dart, HttpClient.badCertificateCallback**](https://api.dart.dev/dart-io/HttpClient/badCertificateCallback.html) 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 November 8, 2026: [Dart API: HandshakeException class](https://api.dart.dev/dart-io/HandshakeException-class.html); [Dart API: HttpClient.badCertificateCallback](https://api.dart.dev/dart-io/HttpClient/badCertificateCallback.html); [Dart API: SecurityContext class](https://api.dart.dev/dart-io/SecurityContext-class.html); [Let’s Encrypt: cross-sign expiration and older Android devices](https://letsencrypt.org/2023/07/10/cross-sign-expiration).

## **Rather hand it over?**

Fixed scope, fixed price, paid upfront. The work happens in writing; no call needed.

- [from $450**App Fix Ticket**One reproducible bug in an existing mobile or web app, diagnosed and fixed.**See scope and start**](https://app369.com/fixed-price/flutter-firebase-bug-fix/)
- [from $450**AI-Built App Rescue**A 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**](https://app369.com/fixed-price/ai-app-rescue/)

Related: [**Flutter web “XMLHttpRequest error”**](https://app369.com/fix/flutter-web-xmlhttprequest-error/) · [**Android cleartext HTTP blocked**](https://app369.com/fix/android-http-requests-blocked-cleartext/) · [**Node “self-signed certificate in certificate chain”**](https://app369.com/fix/node-self-signed-certificate-in-chain/) · [**Flutter “version solving failed”**](https://app369.com/fix/flutter-pub-version-solving-failed/)

Questions

## **More on this error**

<details>

<summary>**It works in the browser but not in the app. Why? **+</summary>

Browsers can fetch a missing intermediate certificate on their own and keep their own up-to-date list of roots. The app relies on the device’s list and on what the server sends, so a gap the browser hides shows up in the app.

</details>

<details>

<summary>**Could the phone’s clock cause it? **+</summary>

Yes. A device whose date is far off sees a valid certificate as not yet valid or expired. Check the date on the test device or emulator before changing anything else.

</details>

<details>

<summary>**Can you fix it for me? **+</summary>

If one bug is the whole problem, the App Fix Ticket covers it: a written diagnosis first, then the fix. If the app has more going on, the Launch Check is a written check of the whole app. Each issue found is rated critical, high, medium or low, with where it is and how it was confirmed. 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.

</details>

A different error?

## **Describe it and get a written quote**

Send the error and what you were doing. You get a written scope, a fixed price and a timeline; Simon replies by email, usually within one business day.

[**Describe your problem **→](https://app369.com/contact/) [**All fixes**](https://app369.com/fix/)