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.
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
-
Download Proxyman (macOS; similar flow works with Charles).
-
Open Proxyman before you run the app.
-
Note the proxy port: 9090 by default (Proxyman → Preferences → Advanced).
-
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’sHttpClientto send every request tohost:portinstead of connecting directly.badCertificateCallback— Proxyman terminates TLS with its own cert. Without this, HTTPS calls fail with certificate errors. Only use this behindkDebugMode+ 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
-
Start Proxyman.
-
Run the app with the flags above.
-
Trigger an API call (login, product list, etc.).
-
You should see requests appear in Proxyman’s left panel.
-
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)
badCertificateCallbackalways returningtruedisables 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
-
Add
*PROXY_**` — dart-definekeys inAppSettings`. -
Create
*ProxymanHttpClientAdapter*with*findProxy*+ cert callback. -
In
*ApiClient*, use it only when*kDebugMode && enabled*; otherwise keep*Http2Adapter*. -
Run with
*— dart-define=PROXY_ENABLED=true*and the correct host for your target. -
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