Python network bugs often hide in places that logs flatten: a library silently reads HTTP_PROXY, a test worker talks to a different host than the one in your shell, or TLS fails because one client is using the system trust store while another expects a CA bundle path.
If you need to debug Python API requests on macOS, the goal is to make one known request visible first. Once Rockxy captures that request, you can inspect the real URL, headers, body, response, and timing instead of stitching the story together from partial logs.
Rockxy's Python setup covers the four clients teams reach for most often: requests, httpx, aiohttp, and urllib3. The Developer Setup Hub generates snippets for each one, and the Python guide in the docs walks through the same localhost demo shown here: Python setup guide.
If you want a runnable project instead of copying snippets from the article, use Rockxy-Python-Sample-Guidance. It includes the local demo server, library-specific examples, environment-variable setup, and smoke tests for the same flow.
Why Python traffic can disappear
The most common failure is not a broken proxy. It is ambiguity. One terminal has proxy variables and another does not. A test runner inherits a stale environment. requests reads shell proxy variables while httpx is configured explicitly. A local server runs on 8000, Rockxy listens on 8888, and those ports get mixed up.
That is why the best first demo uses plain HTTP to a local target. No certificate trust, no external dependency, no DNS. Just a Python script targeting http://127.0.0.1:8000/demo through Rockxy's active proxy port.
The setup that proves the path
Start Rockxy, enable capture, and copy the active proxy port from the toolbar. In the screenshot below, Rockxy is listening on 8888. Your app may use a different port, so read it from the toolbar instead of copying an old value.
Create a tiny local API, then run it in one terminal:
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -U pip
python -m pip install requests httpx aiohttp "urllib3<2"
cat > demo_server.py <<'PY'
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
class DemoHandler(BaseHTTPRequestHandler):
def do_GET(self):
payload = {
"message": "Hello from the Rockxy Python localhost demo.",
"path": self.path,
"host": self.headers.get("Host"),
}
body = json.dumps(payload, indent=2).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
ThreadingHTTPServer(("127.0.0.1", 8000), DemoHandler).serve_forever()
PY
python demo_server.py
In a second terminal, point the Python example at Rockxy and the local demo endpoint:
. .venv/bin/activate
export ROCKXY_PROXY_URL="http://127.0.0.1:8888"
export ROCKXY_TARGET_URL="http://127.0.0.1:8000/demo"
unset ROCKXY_CA_BUNDLE
python - <<'PY'
import requests
proxy_url = "http://127.0.0.1:8888"
target_url = "http://127.0.0.1:8000/demo"
session = requests.Session()
session.trust_env = False
response = session.get(
target_url,
proxies={"http": proxy_url, "https": proxy_url},
verify=True,
timeout=10,
)
print(f"status={response.status_code}")
print(response.text)
PY
The expected terminal result is deliberately boring:
status=200
{
"message": "Hello from the Rockxy Python localhost demo.",
"path": "/demo",
"host": "127.0.0.1:8000"
}
The useful part happens in Rockxy: the traffic list shows 127.0.0.1/demo, the inspector shows GET 200 OK http://127.0.0.1:8000/demo, and the headers show the Python client that sent it. The sample guidance project uses the same environment variable names and target URL, so this shell demo maps directly to reusable example files.
What success looks like in Rockxy
requests example sends GET /demo to 127.0.0.1:8000. The selected flow proves the script, proxy, and demo server are connected.This screenshot is the baseline. Before debugging auth, retries, request bodies, or TLS, get to this state. If this local HTTP request is not visible, fix proxy routing first.
Python client setup for requests, httpx, aiohttp, and urllib3
The core idea is the same across libraries: set the proxy explicitly, pass a CA bundle for HTTPS when needed, and avoid ambient shell behavior while you are proving the route.
For requests, use a session and turn off environment inheritance:
import requests
proxy_url = "http://127.0.0.1:8888"
target_url = "http://127.0.0.1:8000/demo"
session = requests.Session()
session.trust_env = False
response = session.get(
target_url,
proxies={"http": proxy_url, "https": proxy_url},
verify=True,
timeout=10,
)
print(response.status_code)
print(response.text)
For httpx, keep the same shape with trust_env=False:
import httpx
with httpx.Client(
proxy="http://127.0.0.1:8888",
verify=True,
trust_env=False,
timeout=10,
) as client:
response = client.get("http://127.0.0.1:8000/demo")
print(response.status_code)
print(response.text)
For aiohttp, pass the proxy per request. Add a custom SSL context only when the target is HTTPS:
import aiohttp
import asyncio
async def main():
timeout = aiohttp.ClientTimeout(total=10)
async with aiohttp.ClientSession(timeout=timeout, trust_env=False) as session:
async with session.get(
"http://127.0.0.1:8000/demo",
proxy="http://127.0.0.1:8888",
) as response:
print(response.status)
print(await response.text())
asyncio.run(main())
For urllib3, route through ProxyManager:
import urllib3
http = urllib3.ProxyManager(
"http://127.0.0.1:8888",
timeout=urllib3.Timeout(total=10),
)
response = http.request("GET", "http://127.0.0.1:8000/demo")
print(response.status)
print(response.data.decode("utf-8"))
If all four clients hit the same local endpoint and Rockxy captures them, you have proved that your Python environment can route traffic through Rockxy. The next debugging step can focus on your real application code.
When to use HTTPS instead
The localhost demo is intentionally HTTP. It removes certificate trust from the first check. Once proxy routing works, switch to HTTPS when you need to inspect TLS traffic:
export ROCKXY_PROXY_URL="http://127.0.0.1:8888"
export ROCKXY_CA_BUNDLE="/path/to/RockxyRootCA.pem"
export ROCKXY_TARGET_URL="https://example.com"
python examples/httpx_example.py
For HTTPS, export Rockxy's root certificate and point the Python client at that PEM file. Do not use verify=False as your normal answer. If TLS fails, the useful fixes are the CA bundle path, certificate trust, and whether SSL proxying is enabled for the host.
A realistic workflow for Python API debugging
- Start Rockxy and keep capture enabled.
- Copy the active Rockxy proxy port from the toolbar.
- Run one local HTTP request to
127.0.0.1:8000/demo. - Confirm Rockxy shows the request before touching your real API.
- Run the same route through the Python client your project uses.
- Only then move to HTTPS, auth headers, request bodies, retries, or test workers.
When the real request is captured, inspect in order: method, host, path, query, auth headers, cookies, body, status, response body, and timing. This catches wrong base URLs, missing tokens, stale cookies, JSON shape mistakes, and backend errors without adding temporary print statements across the codebase.
Common mistakes that waste time
Confusing the backend port with the proxy port. In the local demo, 8000 is the demo API and 8888 is Rockxy. The Python client connects to the API through the proxy.
Seeing status=200 but no Rockxy flow. That usually means the script reached the target directly or another process owns the proxy port. Re-export ROCKXY_PROXY_URL with Rockxy's active toolbar port.
Expecting example.com to prove the localhost demo. It proves only that your target URL is still example.com. For the showcase demo, set ROCKXY_TARGET_URL=http://127.0.0.1:8000/demo.
Treating a LibreSSL warning as a proxy failure. Some macOS system Python builds pair older LibreSSL with newer urllib3. Use a Python.org or Homebrew Python build, or pin urllib3 to a compatible range for that demo environment.
Disabling TLS verification too early. Certificate bypasses can hide the exact trust issue you need to fix. Prefer an exported Rockxy CA bundle and explicit client configuration.
Make the first captured request boring
The best Python debugging session starts with a boring request: local host, known path, known status, visible in Rockxy. After that, your real bug has fewer places to hide.
If you are setting this up for a team, start with the Python setup guide, then share one library-specific snippet for the client your codebase uses. The same Rockxy capture view works whether the request came from a script, a web worker, a Celery task, a pytest run, or a desktop app embedding Python.
For the underlying HTTPS mechanics, read How to Debug HTTPS Traffic on macOS and How Rockxy Intercepts HTTPS Without Compromising Security. If you also debug mobile clients, the companion article on Flutter API requests on macOS covers simulator, emulator, and physical-device routing.