Skip to content

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

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

    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

    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

    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.

Related: Flutter web “XMLHttpRequest error” · Android cleartext HTTP blocked · Node “self-signed certificate in certificate chain” · Flutter “version solving failed”

Questions

More on this error

It works in the browser but not in the app. Why?

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.

Could the phone’s clock cause it?

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.

Can you fix it for me?

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.

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.