How to Debug Encrypted API Traffic When Console.log Isn't Enough

dev.to

We've all been there. Your app is sending requests to a third-party API, something's going wrong, and all you can see in your browser's Network tab is a bunch of opaque responses that tell you absolutely nothing useful. Maybe the request is getting silently modified by a middleware layer. Maybe response headers are being stripped. Maybe the WebSocket connection keeps dropping and you have no idea why.

I spent an embarrassing amount of time last month debugging a payment integration where the API kept returning 400 Bad Request — and the browser DevTools showed me a perfectly valid-looking payload. Turns out, a reverse proxy was mutating my Content-Type header in a way that was invisible from the client side.

This is the kind of problem that makes you reach for something more powerful than console.log.

Why Browser DevTools Fall Short

Browser DevTools are fantastic for basic request inspection. But they have real limitations:

  • You only see the browser's perspective. If something between your client and the server is modifying traffic (CDN, reverse proxy, API gateway), you won't see it.
  • TLS termination hides everything. Once traffic leaves the browser, it's encrypted. You can't inspect what actually hits the wire.
  • WebSocket and streaming protocols are painful. The DevTools WebSocket inspector is bare-bones at best.
  • No replay or modification. You can't easily re-send a captured request with tweaked headers to isolate the issue.

The root cause of many "impossible" API bugs is that there's a gap between what you think you're sending and what actually arrives at the server.

Enter MITM Proxies: Seeing the Unseeable

A Man-in-the-Middle (MITM) proxy sits between your client and the destination server, intercepting and decrypting TLS traffic so you can inspect it in plain text. Before you panic about the name — this is a standard, legitimate debugging technique. Tools like mitmproxy have been used by developers for years.

Here's how the basic flow works:

Your App → MITM Proxy (decrypts, inspects, re-encrypts) → Target Server
                ↕
        You see everything
Enter fullscreen mode Exit fullscreen mode

The proxy generates its own TLS certificate on the fly. Your client trusts the proxy's CA cert, so the connection completes normally — but now you can see every byte.

Setting Up mitmproxy for API Debugging

Let's walk through a concrete debugging workflow. Say you've got a Node.js service that's hitting a REST API and getting unexpected responses.

First, install mitmproxy:

# macOS
brew install mitmproxy

# Or pip (works anywhere)
pip install mitmproxy

# Start the proxy on port 8080
mitmproxy --listen-port 8080
Enter fullscreen mode Exit fullscreen mode

Now configure your app to route traffic through it:

// Point your HTTP client at the proxy
const axios = require('axios');
const HttpsProxyAgent = require('https-proxy-agent');

const agent = new HttpsProxyAgent('http://127.0.0.1:8080');

// Trust the mitmproxy CA cert for this request
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // dev only!

const response = await axios.get('https://api.example.com/v2/orders', {
  httpsAgent: agent,
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  }
});
Enter fullscreen mode Exit fullscreen mode

Now every request flows through mitmproxy and you can see headers, bodies, timing — everything. The mitmproxy terminal UI lets you arrow through requests and drill into details.

Going Deeper: Scripting Your Proxy

The real power comes when you script the proxy. mitmproxy lets you write Python add-ons that can inspect, modify, or log traffic programmatically.

Here's an example that logs every request where the Content-Type header gets modified between your client and the server response:

# content_type_watcher.py
from mitmproxy import http
import json
from datetime import datetime

def response(flow: http.HTTPFlow) -> None:
    req_ct = flow.request.headers.get("content-type", "none")
    resp_ct = flow.response.headers.get("content-type", "none")

    # Flag mismatches between what we sent and what came back
    if "json" in req_ct and "json" not in resp_ct:
        print(f"[WARN] {datetime.now().isoformat()}")
        print(f"  URL: {flow.request.pretty_url}")
        print(f"  Sent Content-Type: {req_ct}")
        print(f"  Got Content-Type: {resp_ct}")
        print(f"  Status: {flow.response.status_code}")

        # Dump the response body for inspection
        try:
            body = json.loads(flow.response.content)
            print(f"  Body: {json.dumps(body, indent=2)[:500]}")
        except (json.JSONDecodeError, TypeError):
            print(f"  Body (raw): {flow.response.content[:200]}")
Enter fullscreen mode Exit fullscreen mode

Run it with:

mitmproxy -s content_type_watcher.py --listen-port 8080
Enter fullscreen mode Exit fullscreen mode

This is exactly how I found my payment integration bug. The CDN was normalizing application/json; charset=utf-8 to application/json, and the upstream API was strict about the charset parameter. Maddening, but instantly visible once you're looking at the actual wire traffic.

Browser-Based Capture for Frontend Debugging

Sometimes the problem isn't in your backend service — it's in the browser itself. Maybe a Chrome extension is injecting headers. Maybe a service worker is caching stale responses. Maybe CORS preflight is doing something unexpected.

For these cases, you want to intercept traffic at the browser level. You can configure your browser to use the MITM proxy:

# Launch Chrome with proxy settings (macOS)
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
  --proxy-server="http://127.0.0.1:8080" \
  --ignore-certificate-errors-spiffe-only \
  --user-data-dir="/tmp/chrome-proxy-debug"
Enter fullscreen mode Exit fullscreen mode

Now you get full visibility into what the browser is actually sending, not just what DevTools shows you. This is particularly useful for debugging:

  • CORS issues where preflight OPTIONS requests behave differently than you expect
  • Cookie handling where SameSite, Secure, or HttpOnly flags cause silent failures
  • Service worker interference where cached responses mask real API errors

Newer Tools in the Space

The protocol analysis landscape has been evolving. Projects like anything-analyzer are combining multiple approaches — browser capture, MITM proxying, and JS hooks — into unified toolkits. Some of these newer tools are also integrating with AI-powered analysis through MCP (Model Context Protocol) servers, which means you can point an AI assistant at your captured traffic and ask it to spot anomalies.

I haven't tested that particular tool in production yet, but the general trend of combining capture, analysis, and AI in one pipeline is genuinely exciting for debugging complex protocol issues.

Prevention: Making Future Debugging Easier

Once you've solved the immediate fire, here's how to prevent the next one:

  • Log the full request/response at your API boundary. Not just status codes — headers, content types, and (redacted) body snippets. You'll thank yourself later.
  • Add request ID headers. Pass a unique X-Request-ID through your entire chain so you can correlate client → proxy → server logs.
  • Test with strict header validation. If your API cares about Content-Type or Accept headers, add tests that verify the exact values — not just that they're present.
  • Document your proxy chain. If traffic flows through CDN → load balancer → API gateway → service, write that down. Future-you debugging at 2 AM needs that diagram.

The Takeaway

When you're stuck on an API bug that doesn't make sense from the client side, the answer is almost always "something is happening on the wire that you can't see." MITM proxies give you that visibility. Start with mitmproxy for quick inspection, script it for automated detection, and layer in browser-level capture when the problem is on the frontend.

The five minutes it takes to set up a proxy will save you hours of staring at console.log output wondering why your perfectly valid JSON is getting rejected. Trust me on this one — I've done both, and the proxy wins every time.

Source: dev.to

arrow_back Back to News