You start a debugging proxy, your browser and your iOS Simulator traffic show up fine, and then you run a Node script or a Python job and... nothing. Or you get UNABLE_TO_VERIFY_LEAF_SIGNATURE / CERTIFICATE_VERIFY_FAILED.
On macOS the proxy sets the system proxy, and apps built on Apple's networking follow it. Most things you launch from a terminal don't. A request only shows up in a proxy when the program:
- sends it to the proxy, and
- trusts the proxy's certificate authority.
Every runtime has its own switch for both. Examples below assume a proxy on 127.0.0.1:8080 that serves its CA as a PEM file.
Step 0: one bundle, not just the proxy's CA
cat /etc/ssl/cert.pem ~/proxy-ca.pem > ~/proxy-bundle.pem This one bit me. curl --cacert and Python's REQUESTS_CA_BUNDLE replace the trusted list. If you point them at the proxy CA alone, every host the proxy is only tunnelling (not decrypting) now fails verification, because those hosts present their real certificate. A bundle of the normal roots plus the proxy CA works for both.
curl
curl -x http://127.0.0.1:8080 --cacert ~/proxy-bundle.pem https://api.example.com/v1/me From the environment, curl reads https_proxy, but for plain HTTP it only reads lowercase http_proxy. That's deliberate (CGI HTTP_PROXY header injection). Export both spellings:
export http_proxy=http://127.0.0.1:8080 https_proxy=http://127.0.0.1:8080 export HTTP_PROXY=$http_proxy HTTPS_PROXY=$https_proxy And please don't reach for -k.
Node.js
Node's built-in http, https and fetch ignore HTTPS_PROXY. Node 24+ lets you opt in:
export HTTPS_PROXY=http://127.0.0.1:8080 HTTP_PROXY=http://127.0.0.1:8080 export NODE_USE_ENV_PROXY=1 export NODE_EXTRA_CA_CERTS=~/proxy-ca.pem node server.js NODE_EXTRA_CA_CERTS adds to Node's roots, so the CA file alone is right here (no bundle needed). It's read once at startup.
On older Node, use undici in code:
import { ProxyAgent, setGlobalDispatcher } from 'undici'; setGlobalDispatcher(new ProxyAgent('http://127.0.0.1:8080')); axios: pass an https-proxy-agent as httpsAgent and proxy: false, or axios layers its own proxy handling on top.
Next.js: export the variables before next dev. That covers server-side fetches only; browser calls follow the browser.
Python
requests and urllib read the proxy variables already. Point them at the bundle:
export HTTPS_PROXY=http://127.0.0.1:8080 HTTP_PROXY=http://127.0.0.1:8080 export REQUESTS_CA_BUNDLE=~/proxy-bundle.pem # requests export SSL_CERT_FILE=~/proxy-bundle.pem # ssl / urllib Not verify=False.
Go
The easy one. The default client reads HTTPS_PROXY, and on macOS Go verifies against the system keychain. Trust the proxy CA in Keychain Access, then:
HTTPS_PROXY=http://127.0.0.1:8080 go run . If you build your own http.Transport, set Proxy: http.ProxyFromEnvironment or the variable is ignored.
Still nothing?
- Calling localhost? Most clients skip the proxy for
localhost/127.0.0.1, andNO_PROXYoften lists them. Use the Mac's hostname or LAN IP. - Stale
NO_PROXY?env | grep -i proxy. - You see a CONNECT but no body? The proxy reached the host but isn't decrypting it. Turn on SSL decoding for that host in your proxy.
- The program has no proxy setting at all? A normal HTTP proxy can't see it. Nothing to do short of a transparent/VPN-style setup.
Disclosure: I build MinPin, a macOS debugging proxy. Its Tools menu has an "Open Terminal with Proxy" item that exports the variables above, but everything here works with any proxy.
