← Back to Blog

How to Debug Flutter API Requests on macOS

· 7 min read

Flutter makes it easy to ship one UI across multiple platforms. It does not make it easy to debug the network layer when something goes wrong. The hard bugs are rarely "request failed" in the abstract. They are "the iOS Simulator works but the Android Emulator gets a timeout", "Dio sends the header you expected but the backend still returns 401", or "HTTPS works in one runtime and fails in another because the certificate trust path is incomplete."

If you need to debug Flutter API requests in a way that feels grounded in reality, logs are usually not enough. Logs show what your app thinks it is sending. A proxy shows what actually went over the wire: URL, headers, cookies, auth, body, response, and timing. That is the difference between guessing and knowing.

Why Flutter API requests are hard to debug

The pain usually starts once a simple REST call becomes a mobile-debugging problem. A request may work on macOS desktop Flutter and fail on an emulator. The same app may hit the wrong backend because of stale environment config. A token might be missing because the interceptor ran in a different order than you expected. HTTPS might fail because your client or runtime does not trust the debugging certificate yet.

Flutter adds one more wrinkle: traffic routing is not uniform across runtimes. A local Apple runtime can talk to Rockxy on 127.0.0.1. Android Emulator needs 10.0.2.2. A physical device needs the Mac's LAN-accessible proxy host. If you miss that one detail, nothing shows up and the debugging session goes sideways immediately.

What Rockxy gives you

Rockxy is a native macOS proxy tool for inspecting HTTP and HTTPS traffic. For Flutter work, that means you can inspect real request URLs, headers, response bodies, cache behavior, and status codes instead of trying to reconstruct them from print statements or partial logs. It is especially useful when the bug lives at the integration boundary between app code and backend behavior.

Once Flutter traffic is routed through Rockxy, you can inspect the exact payload, confirm whether auth headers are present, verify the host and path, compare the response body to what your app parsed, and spot slow or failing calls without inventing extra logging just for one debugging session.

The setup that actually works

The reliable path is simple, but it helps to say it plainly: do not assume your Flutter app will always inherit the system proxy in the way a browser does. The practical setup is to start Rockxy, copy the active Rockxy port from the app, and explicitly configure your Flutter HTTP client to route through that proxy.

Use the proxy host that matches the runtime where the app is running:

  • iOS Simulator / macOS desktop Flutter: 127.0.0.1:<Rockxy port>
  • Android Emulator: 10.0.2.2:<Rockxy port>
  • Physical iPhone, iPad, or Android device: <Device Proxy LAN host>:<Rockxy port>

That runtime split matters because many "Flutter HTTP proxy" problems are not really HTTP-client bugs at all. They are host-routing mistakes.

Flutter client setup for HttpClient, package:http, and Dio

The sample guidance for Rockxy takes the right approach: put the proxy logic in one debug-only helper, derive the proxy host from the runtime, and then create the exact HTTP client your app already uses. In the sample, that helper lives in lib/rockxy_debug_proxy.dart.

This is the core shape worth copying into a real Flutter app:

enum RockxyRuntime {
  localAppleRuntime,
  androidEmulator,
  physicalDevice,
}

final settings = RockxyDebugProxySettings(
  enabled: true,
  allowBadCertificates: true, // Debug builds only.
  runtime: RockxyRuntime.localAppleRuntime,
  port: 8888, // Copy the active Rockxy proxy port.
  physicalDeviceHost: '',
);

The important part is that the helper resolves the right host for the runtime, then feeds that into the client layer. In the sample implementation, 127.0.0.1 is used for iOS Simulator and macOS Flutter, 10.0.2.2 is used for Android Emulator, and the Device Proxy LAN host is used for physical devices.

For Dart HttpClient, the key line is findProxy:

HttpClient createHttpClient() {
  final client = HttpClient();

  if (enabled) {
    client.findProxy = proxyRuleFor;
  }

  if (allowBadCertificates) {
    client.badCertificateCallback = (certificate, host, port) => true;
  }

  return client;
}

final client = settings.createHttpClient();
final request = await client.getUrl(
  Uri.parse('http://127.0.0.1:43210/rockxy-demo/bootstrap'),
);
request.headers.set(HttpHeaders.cacheControlHeader, 'no-cache');
final response = await request.close();
client.close(force: true);

If your app uses package:http, keep the same socket configuration and wrap it with IOClient:

IOClient createPackageHttpClient() {
  return IOClient(createHttpClient());
}

final client = settings.createPackageHttpClient();
final response = await client.get(
  Uri.parse('http://127.0.0.1:43210/rockxy-demo/bootstrap'),
  headers: const {HttpHeaders.cacheControlHeader: 'no-cache'},
);
client.close();

For Dio 5, route it through the same proxy-aware HttpClient using IOHttpClientAdapter:

Dio createDio() {
  final dio = Dio();
  dio.httpClientAdapter = IOHttpClientAdapter(
    createHttpClient: createHttpClient,
    validateCertificate:
        allowBadCertificates ? (certificate, host, port) => true : null,
  );
  return dio;
}

final dio = settings.createDio();
final response = await dio.getUri(
  Uri.parse('http://127.0.0.1:43210/rockxy-demo/bootstrap'),
  options: Options(
    headers: const {HttpHeaders.cacheControlHeader: 'no-cache'},
    responseType: ResponseType.plain,
  ),
);
dio.close(force: true);

If you want one extra guardrail from the sample, copy its small reachability check before sending requests. It opens a short socket to the Rockxy host and port first, which helps catch a wrong proxy host or port before you waste time blaming your API:

await Socket.connect(
  settings.proxyHost,
  settings.port,
  timeout: const Duration(seconds: 2),
);

The sample also includes debug-only certificate bypass behavior for local learning and HTTPS inspection. That is fine for a local debugging build. It is not fine for production. Do not ship badCertificateCallback, permissive Dio certificate validation, or Android user-CA trust in release builds.

What success looks like when you inspect Flutter network traffic

Rockxy capturing Flutter sample traffic on macOS with the sample app targeting a local proxy port and a request visible in the request list.
Rockxy listening locally while a Flutter sample app routes traffic through 127.0.0.1:8888. The captured request and visible request/response headers confirm that the proxy path is working.

This is the moment you want to get to quickly. The sample app is clearly targeting Rockxy, Rockxy is clearly listening, and the request shows up in the flow list with visible request and response details. Once you have that, debugging gets much less mysterious.

A realistic workflow to debug Flutter API requests

Here is the workflow I would actually use on a real project:

  1. Start Rockxy and keep capture enabled.
  2. Copy the active Rockxy port from the toolbar.
  3. Run the Flutter app in the runtime you care about.
  4. Configure the client with the correct proxy host for that runtime.
  5. Trigger one known API request.
  6. Confirm the request appears in Rockxy before investigating anything else.

Once the request is visible, inspect the basics in order: URL, method, request headers, response status, and response body. A surprising number of bugs collapse right there. You might find a 401 because the auth header was never attached. You might find a 404 because the app hit the wrong base URL. You might find a 400 because the JSON body is structurally wrong even though the model looked fine in code. You might find a slow call that is really a backend latency problem rather than a Flutter rendering issue.

This is also where "inspect Flutter network traffic" becomes more useful than app-only logging. In Rockxy you can compare what the app sent with what the backend answered, which is exactly what you need for stale config, malformed payloads, retry storms, cache misses, and response-shape surprises.

Flutter HTTPS debugging across simulator, emulator, and real devices

HTTP capture is the easy part. Flutter HTTPS debugging requires certificate trust on the runtime that is making the request. Without that trust path, the request may fail before you ever get useful traffic in the proxy.

  • iOS Simulator: install and trust the Rockxy certificate in the simulator.
  • Android Emulator: install the user certificate and use a debug build that trusts user CAs.
  • Physical iPhone or iPad: install the certificate profile and enable full trust in iOS settings.
  • Physical Android device: set the Wi-Fi proxy to the Device Proxy LAN host, install the certificate, and keep user-CA trust limited to debug builds.

If you only remember one safety rule, make it this: certificate shortcuts are for local debug loops only. Production builds should remove permissive trust overrides and go back to normal TLS validation.

Common mistakes that waste an hour

Using the wrong port. The Rockxy port is the active proxy port shown by Rockxy. It is not your backend port. If your demo API runs on 43210 and Rockxy listens on 8888, those two ports do different jobs.

Using 10.0.2.2 outside Android Emulator. That address is only valid from inside Android Emulator. If your app is running on macOS or iOS Simulator, use 127.0.0.1 instead.

Assuming localhost works the same on real devices. It does not. A physical device cannot use its own 127.0.0.1 to reach the Mac. Use the Device Proxy LAN host from Rockxy and keep the device on the same network.

Expecting traffic to appear without explicit client routing. If Flutter traffic is not showing up, do not start by blaming Rockxy. First confirm that your client actually applied the proxy rule.

Stopping at "request captured." A captured request proves the proxy path worked. It does not prove which device, isolate, or process produced it. That distinction matters when multiple runtimes are active.

The shorter feedback loop is the real win

The value of a good macOS proxy tool for Flutter is not just that you can see packets. It is that you can collapse the debug loop. Trigger the request, inspect the traffic, confirm the real headers and body, fix the client or backend, and try again without guessing what happened in between.

If you are working through backend integration issues in Flutter, Rockxy gives you a practical local workflow for that loop. Start with the proxy path, make one known request visible, and the rest of the debugging session gets much more honest.

For the underlying HTTPS mechanics, read How to Debug HTTPS Traffic on macOS and How Rockxy Intercepts HTTPS Without Compromising Security. If you are evaluating tools more broadly, the shorter Proxyman alternative guide is a useful follow-up.