← Back to list

Debug Flutter API Calls with Proxyman and Dio (Step-by-Step)

A safe, opt-in proxy setup for Flutter apps using Dio — no release builds affected, no hardcoded secrets.

Moyeen · 2026-07-01 13:47 · 0 claps · 5.4 min read
#flutter-app-development #rest-api #debugging #dart #proxyman
Open on Medium ↗
Wiki topics: 💻 · Programming 📱 · Mobile Development

Debug Flutter API Calls with Proxyman and Dio (Step-by-Step)

A safe, opt-in proxy setup for Flutter apps using Dio — no release builds affected, no hardcoded secrets.

You shipped a screen. The API returns 200. The UI still looks wrong.

You add *print(response.data)* everywhere. You rebuild. You scroll. You lose the one request that mattered. You give up and blame the backend.

*There is a better way: route your app’s HTTP traffic through Proxyman (or Charles) and inspect every request and response in real time — headers, body, timing, SSL — without littering your codebase with logs.**

This guide shows the exact pattern we use in production Flutter apps with Dio: a custom IOHttpClientAdapter, compile-time flags via — dart-define, and a hard kDebugMode guard so proxy code never ships to users.

What you will build

flutter run --dart-define=PROXY_ENABLED=true \
            --dart-define=PROXY_HOST=127.0.0.1 \
            --dart-define=PROXY_PORT=9090

When those flags are set and the app runs in debug mode, all Dio traffic flows through Proxyman. In profile/release builds, or when flags are omitted, the app uses your normal HTTP/2 adapter — unchanged behavior.

Why Dio needs extra work

Dio is flexible. By default it uses dart:io’s HttpClient, which respects system proxy settings on some platforms — but not reliably on mobile emulators and physical devices.

Many teams also enable HTTP/2 via *dio_http2_adapter*’s *Http2Adapter* for performance. That adapter does not support HTTP proxies. So you cannot “just turn on Proxyman” if Dio is already on HTTP/2.

The fix: in debug + proxy mode, swap *Http2Adapter* for a custom *IOHttpClientAdapter* that points *HttpClient.findProxy* at Proxyman and accepts Proxyman’s MITM certificate.

Architecture (3 small pieces)

| Piece | Role |
|-------|------|
| `AppSettings` | Reads `PROXY_ENABLED`, `PROXY_HOST`, `PROXY_PORT` from `--dart-define` |
| `ProxymanHttpClientAdapter` | Custom Dio adapter: sets proxy + SSL bypass for local debugging |
| `ApiClient` | Chooses proxy adapter only when `kDebugMode && proxy enabled` |
┌─────────────┐     ┌──────────────────────────┐     ┌───────────┐
│  Flutter    │     │  ProxymanHttpClient      │     │ Proxyman  │
│  (Dio)      │────▶│  Adapter (debug only)    │────▶│ :9090     │
└─────────────┘     └──────────────────────────┘     └───────────┘
                              │
                    findProxy → PROXY host:port
                    badCertificateCallback → true

Step 1 — Install and start Proxyman

  1. Download Proxyman (macOS; similar flow works with Charles).

  2. Open Proxyman before you run the app.

  3. Note the proxy port: 9090 by default (Proxyman → Preferences → Advanced).

  4. Install Proxyman’s root certificate on your simulator/emulator/device if you need HTTPS decryption (Proxyman → Certificate → Install on iOS Simulator / Android Emulator).

Step 2 — Add compile-time proxy settings

Use String.fromEnvironment so proxy config is never baked into release builds unless you explicitly pass flags at compile time.

// lib/core/config/app_settings.dart

class AppSettings {
  static String get proxyEnabled =>
      const String.fromEnvironment('PROXY_ENABLED', defaultValue: '');

  static String get proxyHost =>
      const String.fromEnvironment('PROXY_HOST', defaultValue: '');

  static String get proxyPort =>
      const String.fromEnvironment('PROXY_PORT', defaultValue: '');
}

Why *— dart-define*? Values are resolved at compile time. No .env file in the repo, no accidental commit of your machine’s IP, and zero runtime cost when unset.

Step 3 — Create the Proxyman adapter

Extend Dio’s IOHttpClientAdapter and override createHttpClient:

// lib/core/network/proxyman_client_adapter.dart

import 'dart:io';
import 'package:dio/io.dart';
import 'package:your_app/core/config/app_settings.dart';

class ProxymanHttpClientAdapter extends IOHttpClientAdapter {
  ProxymanHttpClientAdapter._(this.host, this.port);

  final String host;
  final int port;

  static ProxymanHttpClientAdapter? _adapter;

  static bool get enabled => adapter != null;

  static ProxymanHttpClientAdapter? get adapter {
    if (_adapter != null) return _adapter;

    final host = AppSettings.proxyHost;
    final port = AppSettings.proxyPort;
    final enabled = AppSettings.proxyEnabled;

    if ([host, port, enabled].any((e) => e.isEmpty)) return null;
    if (enabled.toLowerCase().trim() != 'true') return null;
    if (int.tryParse(port) == null) return null;

    return _adapter = ProxymanHttpClientAdapter._(host, int.parse(port));
  }

  @override
  CreateHttpClient? get createHttpClient => () {
        final client = HttpClient();

        // Route all traffic through Proxyman
        client.findProxy = (uri) => 'PROXY $host:$port';

        // Accept Proxyman's MITM certificate (debug only!)
        client.badCertificateCallback =
            (X509Certificate cert, String host, int port) => true;

        return client;
      };
}

What each line does

  • findProxy — Tells Dart’s HttpClient to send every request to host:port instead of connecting directly.
  • badCertificateCallback— Proxyman terminates TLS with its own cert. Without this, HTTPS calls fail with certificate errors. Only use this behind kDebugMode + explicit opt-in.

Step 4 — Wire it into ApiClient

In your Dio setup, branch on debug mode and proxy flags. When proxy is off, keep your normal adapter (e.g. HTTP/2):

// lib/core/network/api_client.dart

import 'package:dio/dio.dart';
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:flutter/foundation.dart';
import 'package:your_app/core/network/proxyman_client_adapter.dart';

class ApiClient {
  ApiClient(Dio dio, /* interceptors */) {
    dio.options = BaseOptions(
      connectTimeout: const Duration(seconds: 30),
      receiveTimeout: const Duration(seconds: 30),
    );

    // Enable Proxyman only while debugging.
    // Http2Adapter has no proxy support.
    if (kDebugMode && ProxymanHttpClientAdapter.enabled) {
      dio.httpClientAdapter = ProxymanHttpClientAdapter.adapter!;
    } else {
      dio.httpClientAdapter = Http2Adapter(
        ConnectionManager(
          onClientCreate: (_, ClientSetting clientSetting) {
            clientSetting.onBadCertificate = (_) => true;
          },
        ),
      );
    }

    // dio.interceptors.add(...);
  }
}

Safety checklist:

| Guard | Purpose |
|-------|---------|
| `kDebugMode` | Proxy adapter never used in profile/release |
| `PROXY_ENABLED=true` | Explicit opt-in per run |
| Empty defaults in `fromEnvironment` | Normal runs behave exactly as before |

Step 5 — Run the app with proxy flags

iOS Simulator (Mac + Proxyman on same machine)

flutter run \
  --dart-define=PROXY_ENABLED=true \
  --dart-define=PROXY_HOST=127.0.0.1 \
  --dart-define=PROXY_PORT=9090

*127.0.0.1* works because the simulator shares the Mac’s network stack.

Android Emulator

The emulator’s localhost is the emulator itself, not your Mac. Use the special alias:

flutter run \
  --dart-define=PROXY_ENABLED=true \
  --dart-define=PROXY_HOST=10.0.2.2 \
  --dart-define=PROXY_PORT=9090

*10.0.2.2* maps to the host machine from the Android emulator.

Physical device (iPhone / Android phone)

Use your Mac’s LAN IP (e.g. 192.168.1.42). Phone and Mac must be on the same Wi‑Fi.

flutter run \
  --dart-define=PROXY_ENABLED=true \
  --dart-define=PROXY_HOST=192.168.1.42 \
  --dart-define=PROXY_PORT=9090

Find your IP: System Settings → Network, or run *ipconfig getifaddr en0* on macOS.

Also install Proxyman’s certificate on the physical device (Proxyman → Certificate → Install on iOS/Android Device).

Step 6 — Verify in Proxyman

  1. Start Proxyman.

  2. Run the app with the flags above.

  3. Trigger an API call (login, product list, etc.).

  4. You should see requests appear in Proxyman’s left panel.

  5. Click a request → Request / Response tabs for full inspection.

If nothing shows up, see Troubleshooting below.

Optional — VS Code launch configuration

Add a debug configuration so the team does not memorize flags:

{
  "name": "Flutter (Proxyman)",
  "request": "launch",
  "type": "dart",
  "toolArgs": [
    "--dart-define=PROXY_ENABLED=true",
    "--dart-define=PROXY_HOST=127.0.0.1",
    "--dart-define=PROXY_PORT=9090"
  ]
}

Swap PROXY_HOST for 10.0.2.2 or your LAN IP when needed.

Troubleshooting

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| No traffic in Proxyman | Wrong `PROXY_HOST` | Simulator: `127.0.0.1`. Emulator: `10.0.2.2`. Device: Mac LAN IP. |
| SSL / certificate errors | Cert not installed | Install Proxyman cert on simulator/emulator/device. |
| Still no proxy | Not in debug mode | `kDebugMode` is false in profile/release — use `flutter run` (debug). |
| `PROXY_ENABLED` ignored | Typo or missing flag | Must be exactly `true` (case-insensitive). |
| App works, Proxyman empty | Http2Adapter still active | Confirm `ProxymanHttpClientAdapter.enabled` is true and branch runs. |
| Only some requests visible | Non-Dio HTTP | WebViews, `http` package, or native SDKs bypass Dio — proxy those separately. |

Security notes (read this)

  • badCertificateCallback always returning true disables TLS verification. That is acceptable only for local debugging with an explicit flag. Never ship this to production.
  • The *kDebugMode* check is your main safety net. Do not remove it.
  • Do not commit machine-specific IPs. Pass them via *— dart-define* or local launch configs (gitignored if personal).

What this does **not capture**

  • WebView traffic — uses the platform WebView stack, not Dio.

  • Third-party SDKs that open their own HTTP clients.

  • Firebase / gRPC unless they route through your Dio client.

For those, use Proxyman’s system proxy or platform-specific setup guides.

Recap

  1. Add *PROXY_** ` — dart-definekeys inAppSettings`.

  2. Create *ProxymanHttpClientAdapter* with *findProxy* + cert callback.

  3. In *ApiClient*, use it only when *kDebugMode && enabled*; otherwise keep *Http2Adapter*.

  4. Run with *— dart-define=PROXY_ENABLED=true* and the correct host for your target.

  5. Inspect traffic in Proxyman instead of *print()* debugging.

You get full visibility into API traffic, zero impact on release builds, and a pattern your whole team can copy in under 30 minutes.


메타데이터
post_id
ece2b63e73d0
slug
debug-flutter-api-calls-with-proxyman-and-dio-step-by-step-ece2b63e73d0
url
https://medium.com/@moyeenadds/debug-flutter-api-calls-with-proxyman-and-dio-step-by-step-ece2b63e73d0
canonical_url
https://medium.com/@moyeenadds/debug-flutter-api-calls-with-proxyman-and-dio-step-by-step-ece2b63e73d0
author_url
https://medium.com/@moyeenadds
status
ok
fetched_at
2026-09-12 03:12:22