Documentation Python quickstart Blog Free tools Enterprise solutions hello@quanticdata.ioLog in

SSL: CERTIFICATE_VERIFY_FAILED behind a proxy: what the error tail tells you

A Python traceback reading SSL CERTIFICATE_VERIFY_FAILED with the last words of the message highlighted; the card maps each ending to its cause: self-signed certificate in certificate chain means an intercepting proxy, unable to get local issuer certificate means a missing or replaced CA bundle, self-signed certificate means a single self-signed leaf.
A Python traceback reading SSL CERTIFICATE_VERIFY_FAILED with the last words of the message highlighted; the card maps each ending to its cause: self-signed certificate in certificate chain means an intercepting proxy, unable to get local issuer certificate means a missing or replaced CA bundle, self-signed certificate means a single self-signed leaf.

A normal HTTP proxy cannot cause SSL: CERTIFICATE_VERIFY_FAILED. With a CONNECT tunnel, TLS runs from your client to the website and the proxy never sees a certificate. If you get this error behind a proxy, one of two things is true: something in the path is re-signing the site with its own certificate, or your client is checking against the wrong CA bundle. On 5 October 2026 we reproduced the error 42 times across seven clients. The last words of the message tell you which case you have.

Tunnel or interception: the one question that matters

An HTTPS request through a proxy normally starts with CONNECT example.com:443. The proxy answers 200 Connection established and then just copies bytes. The certificate your client checks is the website's own, signed by a public CA, so verification passes just as it would without a proxy. In our lab, a plain tunnel returned 200 to requests, urllib, curl and Node with no extra settings. Proxy credentials, rotation and country targeting change nothing here, because none of them touch TLS.

An intercepting proxy also accepts the CONNECT, but then finishes the TLS handshake itself. It shows your client a certificate for example.com that it issued on the fly from its own root CA. Corporate SSL inspection, antivirus "web shields", debugging proxies and web unlockers all work this way. Your client has never seen that root, so it refuses the connection. That refusal is the error you're looking at. It isn't a bug: it's TLS doing its job.

You can tell which case you have in one command. Ask for the certificate through the proxy and read the issuer:

openssl s_client -proxy PROXY_HOST:PORT -connect example.com:443 \
  -servername example.com </dev/null 2>/dev/null | grep -E " s:| i:|Verify return"

Through our plain tunnel, the issuer was a public CA and verification succeeded. Through the intercepting proxy, it was the lab's own root, and openssl printed Verify return code: 19 (self-signed certificate in certificate chain). If the issuer names your company, your antivirus or a debugging tool, you've found the cause.

What each error tail means

Python, curl and Node all start the message the same way and put the diagnosis at the end. These are the three endings we reproduced and what each one meant in our tests:

End of the messagecurl / Node wordingWhat happenedFix
self-signed certificate in certificate chaincurl (60) same text; Node SELF_SIGNED_CERT_IN_CHAINAn intercepting proxy sent its own root along with the forged certificate. Your trust store doesn't include that root.Trust that root CA for this client only
unable to get local issuer certificatecurl (60) same text; Node UNABLE_TO_VERIFY_LEAF_SIGNATUREEither the proxy sent only the leaf, or your CA bundle lacks the issuer. We also triggered it on a plain tunnel by pointing REQUESTS_CA_BUNDLE at a single CA file.Use a bundle that contains both the public roots and the proxy's CA
self-signed certificatecurl (60) same text; Node DEPTH_ZERO_SELF_SIGNED_CERTThe proxy presents one self-signed certificate per host, with no CA behind itFix the proxy, or trust that exact certificate

The middle row causes the most confusion. The same text appears when an interceptor sends an incomplete chain and when your own configuration has thrown away the public roots. If the error started right after you set a CA variable, suspect the variable first.

Python: which setting your library actually reads

This is where most of the time goes, because each library reads a different setting. We ran the same intercepted request with each setting, from a separate process so nothing leaked between runs:

Settingrequests 2.34.2httpx 0.28.1Effect on a normal site
REQUESTS_CA_BUNDLE=ca.pemIntercepted request passesIgnored, still failsrequests now fails on a plain tunnel with "unable to get local issuer certificate"
SSL_CERT_FILE=ca.pemIgnored, still failsIntercepted request passesNot tested for httpx
verify="ca.pem" (requests) / SSL context with cafile (httpx, aiohttp)PassesPassesScoped to that call or client
verify= a bundle of public roots plus the proxy CAPassesnot runPlain tunnel also passes: 200
verify=FalsePasses, emits InsecureRequestWarningnot runVerification is off for everything

Two results from this table are worth remembering. First, REQUESTS_CA_BUNDLE replaces the trust store rather than adding to it. Point it at your proxy's CA alone and every site you reach through a normal tunnel breaks with a different error. Build a combined file instead:

import certifi, requests

# public roots + the intercepting proxy's root, in one file
with open("combined.pem", "w") as out:
    out.write(open(certifi.where()).read())
    out.write(open("proxy-ca.pem").read())

proxies = {"https": "http://USER:PASS@PROXY_HOST:PORT"}
r = requests.get("https://example.com/", proxies=proxies,
                 verify="combined.pem", timeout=30)
print(r.status_code)

Second, httpx and requests don't share environment variables. A fix that works in a requests script won't carry over when you switch libraries. With httpx or aiohttp, pass an explicit context and you won't depend on either variable:

import ssl, httpx

ctx = ssl.create_default_context(cafile="combined.pem")
with httpx.Client(proxy="http://USER:PASS@PROXY_HOST:PORT", verify=ctx) as client:
    print(client.get("https://example.com/").status_code)

aiohttp takes the same context through its ssl= argument. With the default settings, aiohttp raised ClientConnectorCertificateError wrapping the same "self-signed certificate in certificate chain" text. Python's built-in urllib behaved like the others: 200 through the plain tunnel, the same CERTIFICATE_VERIFY_FAILED through the interceptor.

pip behind a proxy

"pip proxy ssl certificate_verify_failed" is the top autocomplete for this error, and pip 25.2 behaves differently from the libraries above. It checks certificates against the operating system's trust store. On macOS, our intercepted install failed with a system message saying the lab CA "is not trusted", not the usual OpenSSL wording. With --use-deprecated=legacy-certs, pip went back to its bundled roots and printed the classic self-signed certificate in certificate chain.

That has two practical consequences. If your company already pushes its inspection CA into the system store, current pip usually works with no flags. If it doesn't, give pip the CA explicitly and keep verification on:

pip install --proxy http://USER:PASS@PROXY_HOST:PORT --cert proxy-ca.pem package-name
# or once, in pip.conf / pip.ini:
# [global]
# cert = /path/to/proxy-ca.pem

In our run, --cert got pip past the handshake. Avoid --trusted-host as a permanent fix, because it turns off verification for that host.

curl and Node: two traps

curl --proxy-insecure does not help. It skips checking the certificate of an HTTPS proxy, meaning the proxy's own TLS. It doesn't affect the certificate presented for the target. Through our intercepting proxy, curl 8.7.1 still exited with code 60 when given --proxy-insecure. --cacert proxy-ca.pem returned 200, as did -k, which turns verification off entirely.

curl --proxy http://PROXY_HOST:PORT --proxy-user 'USER:PASS' \
  --cacert ./proxy-ca.pem 'https://example.com/'

Node appends, Python replaces. Starting Node with NODE_EXTRA_CA_CERTS=proxy-ca.pem fixed the intercepted request and left the plain tunnel working: both returned OK in the same process. That's the behaviour most people expect from REQUESTS_CA_BUNDLE, and it's exactly what that variable does not do.

When the error is not about certificates at all

A common mistake is writing the proxy URL as https:// when the proxy speaks plain HTTP. It looks like a TLS problem but it isn't one. With proxies={"https": "https://PROXY_HOST:PORT"}, requests opened TLS to the proxy itself. When our test proxy replied with a plain-text 400, requests raised a ProxyError saying the proxy "appears to only use HTTP and not HTTPS". When the proxy stayed silent instead, the call hung until the 15-second read timeout. Neither case produces CERTIFICATE_VERIFY_FAILED. The fix is the scheme: the key is "https" because the target is HTTPS, and the value stays http:// because the proxy is HTTP. We cover how to read that error in requests.exceptions.ProxyError, explained, and the tunnel-level failures in ERR_TUNNEL_CONNECTION_FAILED.

Where this applies on QuanticData

The residential, mobile, ISP and datacenter gateways are CONNECT tunnels: TLS stays between your client and the site. If you see CERTIFICATE_VERIFY_FAILED on one of them, look for an interceptor on your side (office network, antivirus, a debugging proxy left running) or a CA variable you set yourself. The proxy tester tells you whether the gateway itself answers.

The Web Unlocker is different, and deliberately so. It ends your TLS session so it can read and rewrite the request, then opens its own connection to the site. Every HTTPS request through it fails verification until your client trusts its CA. Download the certificate with your API key and pass it to the client. Don't turn verification off:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  https://app.quanticdata.io/api/v1/scraper/unlock/ca -o qp-unlocker-ca.pem

curl --proxy http://unlock.quanticdata.io:9000 \
  --proxy-user 'unlock-YOUR_ID-country-us:YOUR_PROXY_PASSWORD' \
  --cacert ./qp-unlocker-ca.pem 'https://example.com/'

If you'd rather not manage certificates at all, the web scraping API takes a URL over a normal HTTPS call to our API and returns the page. There's no proxy in your client and no CA to trust, and failed requests are not billed.

A five-step checklist

  1. Read the end of the error message and match it to the table above.
  2. Run openssl s_client -proxy and check who issued the certificate.
  3. If it's an interceptor you trust, get its root CA as a PEM file.
  4. Add that CA for this client only: a combined bundle for requests, an SSL context for httpx and aiohttp, --cert for pip, --cacert for curl, NODE_EXTRA_CA_CERTS for Node.
  5. Re-run against both an intercepted site and a normal one, so a replaced bundle can't hide behind a fix that only works for one of them.

Our full checklist for proxy errors that aren't about certificates is in proxy not working: the checklist.

Sources & further reading

FAQ

Quick answers on ssl certificate_verify_failed proxy.

Something else? Ask us

Can a normal proxy cause SSL CERTIFICATE_VERIFY_FAILED?

No. With a CONNECT tunnel, TLS runs end to end and the client checks the website's own certificate. In our 5 October 2026 tests, a plain tunnel returned 200 to requests, urllib, curl and Node. The error means something is intercepting TLS or your CA bundle is wrong.

What does "self-signed certificate in certificate chain" mean behind a proxy?

An intercepting proxy, such as corporate SSL inspection, an antivirus or a debugging tool, re-signed the site with its own root CA, and your client does not trust that root. Trust that CA for the client you are using instead of disabling verification.

Why did REQUESTS_CA_BUNDLE break other sites?

Because it replaces the trust store instead of adding to it. Pointing it at a single proxy CA made a normal site fail with "unable to get local issuer certificate". Use a file that contains the certifi roots plus the proxy CA.

Is verify=False an acceptable fix?

It makes the request pass, and requests warns with InsecureRequestWarning, but it turns off verification for everything that call touches. Use it only to confirm the diagnosis, then switch to verify pointing at a bundle that includes the proxy CA.

How do I fix pip SSL CERTIFICATE_VERIFY_FAILED behind a corporate proxy?

pip 25.2 checks the operating system trust store, so it works when IT has installed the inspection CA there. Otherwise pass the CA with --cert proxy-ca.pem, or set cert in pip.conf. Avoid --trusted-host as a permanent setting.

Do I need a CA certificate for QuanticData proxies?

Not for the residential, mobile, ISP or datacenter gateways, which are plain CONNECT tunnels. Yes for the Web Unlocker, which terminates TLS by design: download its CA with your API key and pass it to your client.

Proxies that leave your TLS alone

QuanticData residential, mobile, ISP and datacenter gateways are plain CONNECT tunnels: no certificate to install, country targeting in the username. Failed API requests are never billed.

Related reading