New Fluxzy 2.0 just shipped. Electron is out, Tauri is in. Fresh design, 3x smaller install. Learn more

How to Intercept HTTPS Traffic from Flutter Apps

Intercepting HTTP traffic from Flutter apps lets you debug API calls, inspect payloads, analyze performance, and troubleshoot network issues during development. This guide shows you how to configure a Flutter app to route traffic through Fluxzy, whether your app uses certificate pinning or not.

If you're targeting Android specifically, check out Fluxzy Connect — a companion Android app that routes traffic through a local VPN, eliminating manual proxy configuration on the device. See it in action in this video walkthrough.

What You'll Learn

  • Configure a Flutter app to route traffic through Fluxzy
  • Export and load the Fluxzy root CA certificate
  • Handle HTTPS interception for apps with and without certificate pinning
  • Code examples for dart:io, package:http, and package:dio

Prerequisites:

  • Fluxzy Desktop or CLI installed (see below)
  • A Flutter project

Install Fluxzy CLI

If you don't have Fluxzy installed yet, the CLI is the fastest way to get started. Pick the method for your platform:

Windows (winget):

winget install Fluxzy.Fluxzy

macOS (Homebrew):

brew tap haga-rak/fluxzy
brew install fluxzy

Linux (install script):

curl -sL https://fluxzy.io/install/script/linux/latest | bash

You can also download standalone binaries from the download page or directly from GitHub releases.

Why Flutter Requires Extra Configuration

Unlike browsers, Flutter's dart:io HttpClient doesn't pick up the system proxy automatically. You must explicitly set the proxy address in your code so that HTTP requests flow through Fluxzy. For HTTPS, Fluxzy intercepts and re-signs traffic with its own root CA, so your app must also be configured to trust that certificate.

Start Fluxzy

Before running your Flutter app, start Fluxzy to listen for connections.

Using Fluxzy Desktop:

  1. Open Fluxzy Desktop
  2. Click Start Capture (or press F5)
  3. Default proxy address: 127.0.0.1:44344

Using Fluxzy CLI:

fluxzy start -l 127.0.0.1:44344

Export the Fluxzy CA Certificate

To handle HTTPS properly, you need Fluxzy's root CA certificate. Export it with the CLI:

fluxzy cert export fluxzy-ca.pem

This creates a PEM file containing Fluxzy's root CA. You can then bundle it in your Flutter project's assets or load it from disk during development.

Fluxzy Desktop and CLI share the same default certificate. If you set a custom root CA with fluxzy cert default, or use the built-in one, Fluxzy Desktop will use the same certificate by default. This means a certificate exported with the CLI works seamlessly when you switch to Fluxzy Desktop for capture, and vice versa — no need to export or configure twice.

Other useful certificate commands:

Command Description
fluxzy cert export <file> Export the root CA to a file
fluxzy cert check Verify if the certificate is trusted on your machine
fluxzy cert install Install the certificate as a trusted root (requires elevation)
fluxzy cert list List all root certificates

Tip: If you install the Fluxzy certificate at the system level with fluxzy cert install, desktop Flutter apps will trust it automatically without any code changes. The code-level configuration below is still required for Android emulators and physical devices.

Determine the Proxy Address

The proxy address depends on where your Flutter app runs:

Platform Proxy Address Notes
Desktop (Windows, macOS, Linux) 127.0.0.1:44344 Direct localhost access
iOS Simulator 127.0.0.1:44344 Shares host network
Android Emulator 10.0.2.2:44344 10.0.2.2 maps to host loopback
Android Physical Device <your-computer-ip>:44344 Or use Fluxzy Connect to skip proxy config
iOS Physical Device <your-computer-ip>:44344 Device must be on the same network

Apps Without Certificate Pinning

If your Flutter app does not implement custom certificate pinning, you only need to configure the proxy and accept Fluxzy's certificate.

dart:io (HttpClient)

import 'dart:io';

HttpClient createProxiedClient({
  required String proxyHost,
  required int proxyPort,
}) {
  final client = HttpClient();

  client.findProxy = (uri) => 'PROXY $proxyHost:$proxyPort';

  // Accept Fluxzy's certificate during development
  client.badCertificateCallback = (cert, host, port) => true;

  return client;
}

// Usage
Future<void> main() async {
  final client = createProxiedClient(
    proxyHost: '127.0.0.1',
    proxyPort: 44344,
  );

  final request = await client.getUrl(Uri.parse('https://example.com'));
  final response = await request.close();
  print('Status: ${response.statusCode}');

  client.close();
}

package:http

import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';

http.Client createProxiedHttpClient({
  required String proxyHost,
  required int proxyPort,
}) {
  final inner = HttpClient();
  inner.findProxy = (uri) => 'PROXY $proxyHost:$proxyPort';
  inner.badCertificateCallback = (cert, host, port) => true;
  return IOClient(inner);
}

// Usage
Future<void> main() async {
  final client = createProxiedHttpClient(
    proxyHost: '127.0.0.1',
    proxyPort: 44344,
  );

  final response = await client.get(Uri.parse('https://example.com'));
  print('Status: ${response.statusCode}');

  client.close();
}

package:dio

import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';

Dio createProxiedDio({
  required String proxyHost,
  required int proxyPort,
}) {
  final dio = Dio();
  dio.httpClientAdapter = IOHttpClientAdapter(
    createHttpClient: () {
      final client = HttpClient();
      client.findProxy = (uri) => 'PROXY $proxyHost:$proxyPort';
      client.badCertificateCallback = (cert, host, port) => true;
      return client;
    },
  );
  return dio;
}

// Usage
Future<void> main() async {
  final dio = createProxiedDio(
    proxyHost: '127.0.0.1',
    proxyPort: 44344,
  );

  final response = await dio.get('https://example.com');
  print('Status: ${response.statusCode}');

  dio.close();
}

Security Note: Setting badCertificateCallback to always return true disables certificate validation entirely. Use a compile-time flag or environment check to ensure this only runs in debug builds. Never ship this to production.

Apps with Certificate Pinning

If your Flutter app uses SSL certificate pinning, Fluxzy's intercepted traffic will be rejected because the proxy re-signs requests with its own CA. You need to add Fluxzy's root CA alongside your existing pinning configuration.

This section covers two pinning strategies:

Pinning strategy What is pinned What to add for the proxy
Leaf certificate Exact DER bytes of the server's leaf cert The proxy's root CA as a trusted issuer
Root / intermediate CA The CA certificate that issued the server cert The proxy's root CA as an additional trusted issuer

Important: Only add the proxy certificate in debug/development builds. Use a compile-time flag or environment check to ensure it is never included in production.

Loading the Proxy Certificate as DER Bytes

Before modifying your pinning code, load Fluxzy's root CA as Uint8List DER bytes.

Load from a file exported with fluxzy cert export:

import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

Uint8List loadCertificateFromFile(String path) {
  final data = File(path).readAsBytesSync();

  // If starts with 0x30 (ASN.1 SEQUENCE), it's already raw DER
  if (data.isNotEmpty && data[0] == 0x30) {
    return Uint8List.fromList(data);
  }

  // Otherwise, decode as PEM / base64 text
  final text = utf8.decode(data);
  final cleaned = text
      .replaceAll('-----BEGIN CERTIFICATE-----', '')
      .replaceAll('-----END CERTIFICATE-----', '')
      .replaceAll(RegExp(r'\s+'), '');
  return Uint8List.fromList(base64.decode(cleaned));
}

Decode from a PEM string (e.g. hardcoded in a debug config):

import 'dart:convert';
import 'dart:typed_data';

Uint8List decodePemOrBase64(String input) {
  final cleaned = input
      .replaceAll('-----BEGIN CERTIFICATE-----', '')
      .replaceAll('-----END CERTIFICATE-----', '')
      .replaceAll(RegExp(r'\s+'), '');
  return Uint8List.fromList(base64.decode(cleaned));
}

Case 1: Leaf Certificate Pinning

With leaf pinning, the app rejects any certificate whose DER bytes don't match the pinned leaf. A debugging proxy generates a different leaf for each domain, so a simple byte comparison always fails.

Strategy:

  1. Add the proxy's root CA to the SecurityContext via setTrustedCertificatesBytes
  2. Keep withTrustedRoots: false so only explicitly trusted certificates are accepted
  3. In badCertificateCallback, accept the certificate if its DER bytes match the original pinned leaf

How it works:

  • Through the proxy: the proxy-signed leaf chains to the trusted proxy CA → chain validation succeeds
  • Direct connection: the real server cert fails chain validation → badCertificateCallback fires → DER bytes match the pinned leaf → connection allowed
  • Attacker cert: fails chain validation → badCertificateCallback fires → DER bytes don't match → connection rejected

Shared helper

import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

bool _derEquals(Uint8List a, Uint8List b) {
  if (a.length != b.length) return false;
  for (var i = 0; i < a.length; i++) {
    if (a[i] != b[i]) return false;
  }
  return true;
}

/// Creates an HttpClient that accepts:
/// - certificates signed by [proxyCaDer] (proxy-intercepted traffic), AND
/// - the exact leaf certificate matching [pinnedLeafDer] (direct traffic).
HttpClient createLeafPinningClient({
  required Uint8List pinnedLeafDer,
  Uint8List? proxyCaDer,
}) {
  final context = SecurityContext(withTrustedRoots: false);

  // Trust the proxy CA so proxy-signed certificates pass chain validation
  if (proxyCaDer != null) {
    final proxyPem = '-----BEGIN CERTIFICATE-----\n'
        '${base64.encode(proxyCaDer)}\n'
        '-----END CERTIFICATE-----\n';
    context.setTrustedCertificatesBytes(utf8.encode(proxyPem));
  }

  final client = HttpClient(context: context);

  // badCertificateCallback fires only when chain validation fails.
  // If we're NOT going through the proxy, the real server cert won't
  // chain to any trusted CA, so we verify it against the pinned leaf.
  client.badCertificateCallback = (cert, host, port) {
    return _derEquals(cert.der, pinnedLeafDer);
  };

  return client;
}

With package:http

import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';

Future<http.Response> httpLeafPinnedGet(
  String url, {
  required Uint8List pinnedLeafDer,
  Uint8List? proxyCaDer,
}) async {
  final inner = createLeafPinningClient(
    pinnedLeafDer: pinnedLeafDer,
    proxyCaDer: proxyCaDer,
  );
  final client = IOClient(inner);
  try {
    return await client.get(Uri.parse(url));
  } finally {
    client.close();
  }
}

With package:dio

import 'package:dio/dio.dart';
import 'package:dio/io.dart';

Future<Response<List<int>>> dioLeafPinnedGet(
  String url, {
  required Uint8List pinnedLeafDer,
  Uint8List? proxyCaDer,
}) async {
  final dio = Dio();
  dio.options.responseType = ResponseType.bytes;
  dio.httpClientAdapter = IOHttpClientAdapter(
    createHttpClient: () => createLeafPinningClient(
      pinnedLeafDer: pinnedLeafDer,
      proxyCaDer: proxyCaDer,
    ),
  );
  return await dio.get<List<int>>(url);
}

Case 2: Root / Intermediate CA Pinning

With CA pinning, the app trusts only a specific CA as root. The debugging proxy signs traffic with a different CA, so the connection fails.

Strategy:

Call setTrustedCertificatesBytes twice — once for the original pinned CA, and once for the proxy's root CA. The SecurityContext accumulates trusted certificates, so both CAs are accepted during chain validation.

How it works:

  • Through the proxy: the proxy-signed cert chains to the proxy CA → chain validation succeeds
  • Direct connection: the real server cert chains to the original pinned CA → chain validation succeeds
  • Attacker cert: chains to neither CA → connection rejected

Shared helper

import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

/// Creates an HttpClient that trusts [pinnedCaDer] (the original pinned CA)
/// and optionally [proxyCaDer] (the debugging proxy's root CA).
HttpClient createCaPinningClient({
  required Uint8List pinnedCaDer,
  Uint8List? proxyCaDer,
}) {
  final context = SecurityContext(withTrustedRoots: false);

  // Original pinned CA
  final originalPem = '-----BEGIN CERTIFICATE-----\n'
      '${base64.encode(pinnedCaDer)}\n'
      '-----END CERTIFICATE-----\n';
  context.setTrustedCertificatesBytes(utf8.encode(originalPem));

  // Proxy CA — only in debug builds
  if (proxyCaDer != null) {
    final proxyPem = '-----BEGIN CERTIFICATE-----\n'
        '${base64.encode(proxyCaDer)}\n'
        '-----END CERTIFICATE-----\n';
    context.setTrustedCertificatesBytes(utf8.encode(proxyPem));
  }

  final client = HttpClient(context: context);
  client.badCertificateCallback = (cert, host, port) {
    return false; // Reject anything that doesn't chain to a trusted CA
  };
  return client;
}

With package:http

import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';

Future<http.Response> httpCaPinnedGet(
  String url, {
  required Uint8List pinnedCaDer,
  Uint8List? proxyCaDer,
}) async {
  final inner = createCaPinningClient(
    pinnedCaDer: pinnedCaDer,
    proxyCaDer: proxyCaDer,
  );
  final client = IOClient(inner);
  try {
    return await client.get(Uri.parse(url));
  } finally {
    client.close();
  }
}

With package:dio

import 'package:dio/dio.dart';
import 'package:dio/io.dart';

Future<Response<List<int>>> dioCaPinnedGet(
  String url, {
  required Uint8List pinnedCaDer,
  Uint8List? proxyCaDer,
}) async {
  final dio = Dio();
  dio.options.responseType = ResponseType.bytes;
  dio.httpClientAdapter = IOHttpClientAdapter(
    createHttpClient: () => createCaPinningClient(
      pinnedCaDer: pinnedCaDer,
      proxyCaDer: proxyCaDer,
    ),
  );
  return await dio.get<List<int>>(url);
}

Pinning Summary

Pinning type What changes Mechanism
Leaf Add proxy CA to SecurityContext + keep leaf check in badCertificateCallback Proxy traffic passes chain validation via proxy CA; direct traffic passes via leaf DER match
Root / intermediate CA Call setTrustedCertificatesBytes twice (original CA + proxy CA) Both CAs are trusted; chain validation accepts either

In both cases, the proxy certificate is passed as an optional Uint8List? proxyCaDer parameter. When null (production), only the original pinning is active. When provided (debug), proxy-intercepted traffic is also accepted.

Platform-Specific Notes

Android Emulator

Android 7+ (API 24) restricts user-installed certificates by default. For debug builds, add a network security configuration:

android/app/src/debug/res/xml/network_security_config.xml:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <debug-overrides>
    <trust-anchors>
      <certificates src="system" />
      <certificates src="user" />
    </trust-anchors>
  </debug-overrides>
</network-security-config>

Reference it in android/app/src/debug/AndroidManifest.xml:

<application android:networkSecurityConfig="@xml/network_security_config">

Note: This configuration only applies to debug builds. Release builds are not affected.

Android Physical Device

For physical Android devices, the easiest approach is Fluxzy Connect — a companion Android app that creates a local VPN tunnel to route traffic through Fluxzy without any proxy configuration in your code. It auto-discovers your Fluxzy instance via mDNS and handles certificate installation on the device. See the video walkthrough.

If you prefer manual proxy configuration instead, set the proxy address to your computer's local IP (e.g. 192.168.1.100:44344) and start Fluxzy bound to all interfaces:

fluxzy start -l 0.0.0.0:44344

iOS Simulator

The iOS Simulator shares the host machine's network, so 127.0.0.1:44344 works directly. No additional platform configuration is needed if you handle certificates in Dart code as shown above.

iOS Physical Device

Fluxzy Connect is not available on iOS. To debug on a physical iOS device, configure the proxy address in your Dart code to point to your computer's local IP (e.g. 192.168.1.100:44344), and start Fluxzy bound to all interfaces:

fluxzy start -l 0.0.0.0:44344

The device must be on the same network as your computer.

Quick Reference

Library Proxy Configuration Certificate Handling
dart:io client.findProxy = (uri) => 'PROXY host:port' badCertificateCallback
package:http Wrap HttpClient with IOClient Same as dart:io (via inner client)
package:dio IOHttpClientAdapter(createHttpClient: ...) Same as dart:io (via factory)

Troubleshooting

No Traffic in Fluxzy

Symptoms: App makes requests but Fluxzy shows no exchanges.

Solutions:

  1. Verify Fluxzy is running and listening on the correct port
  2. Check the proxy address matches your platform (use 10.0.2.2 for Android emulator)
  3. Ensure findProxy is set on the HttpClient — Flutter doesn't use the system proxy by default
  4. Try with an http:// URL first to verify basic proxy connectivity

CERTIFICATE_VERIFY_FAILED Errors

Symptoms: HandshakeException: CERTIFICATE_VERIFY_FAILED in the console.

Solutions:

  1. Verify badCertificateCallback is set on the HttpClient
  2. If using certificate pinning, ensure Fluxzy's CA is added to the trusted certificates
  3. On Android, check that network_security_config.xml is in place for debug builds
  4. Export a fresh certificate with fluxzy cert export and reload it in the app

Connection Refused on Android Emulator

Symptoms: SocketException: Connection refused when using 127.0.0.1.

Solutions:

  1. Use 10.0.2.2 instead of 127.0.0.1 on Android emulators
  2. Ensure Fluxzy is listening on the expected interface
  3. For physical devices, use the computer's network IP and start Fluxzy with -l 0.0.0.0:44344

Proxy Works but Pinned Requests Fail

Symptoms: Non-pinned requests work through the proxy, but pinned requests are rejected.

Solutions:

  1. Verify the Fluxzy CA certificate is loaded correctly as DER bytes
  2. For leaf pinning, ensure proxyCaDer is passed to createLeafPinningClient
  3. For CA pinning, ensure setTrustedCertificatesBytes is called for both the original CA and the proxy CA
  4. Check that withTrustedRoots: false is set on the SecurityContext

Next Steps

ESC