Proxy error · TLS

SSL certificate errors

With an HTTP proxy and an HTTPS site, your client asks the proxy for a tunnel and then does TLS directly with the site through it. The proxy never sees the certificate. So a certificate error is between you and the site: the certificate it sent, and the list of CAs your client trusts.
Whose fault is it?

Yours or the site’s. The proxy does not touch TLS on a tunnelled HTTPS request.

Usually from
Your CA bundle, or the site’s certificate
Try first
Update your CA bundle, then check which CA your tool trusts
With a proxy in the path

What a certificate error means through a proxy

Verification errors (CERTIFICATE_VERIFY_FAILED, ERR_CERT_AUTHORITY_INVALID, unable to get local issuer certificate) mean your client did not trust the certificate it was shown. Each tool checks against its own CA bundle: Requests uses certifi, Node ships its own list, curl uses whichever bundle its build points at, and Chrome uses its own root store.

An error on one site only is that site’s certificate: expired, missing an intermediate, or issued for another name. Browsers can fetch a missing intermediate on their own, which is why a site can look fine in Chrome and fail in a script.

An error on every site, with an issuer you do not recognise, means something on your side re-signs TLS: a company firewall or an antivirus that inspects HTTPS. Your tool needs that CA added to the list it trusts.

WRONG_VERSION_NUMBER is a different error, a handshake with the wrong kind of server, usually an https:// proxy URL. It has its own page.

The key question

Your credentials, the proxy, or the target?

Yours or the site’s. The proxy does not touch TLS on a tunnelled HTTPS request.

  • Your credentials

    Not the cause. Credentials are checked on the CONNECT, before any TLS.

  • The proxy

    Not the cause: it passes TLS through untouched and never presents a certificate of its own on a tunnelled request.

  • The target site

    An expired certificate, a missing intermediate or a name mismatch. It fails the same way without the proxy.

How to tell

  • The issuer in the error or in curl -v is your employer or your antivirus, not a public CA: something on your side is inspecting TLS.
  • The same error with and without the proxy: it is the site or your CA bundle.
  • openssl s_client against the site, without the proxy, shows its chain and expiry directly.
  • Only one tool on the machine fails: that tool carries its own, older CA bundle.
Cheapest first

Fixes, in the order to try them

  1. Update the CA bundle

    pip install -U certifi for Python, a current Node release for Node, and your OS updates or update-ca-certificates for curl.

    Costs nothing
  2. Check which bundle your tool loads

    Requests reads REQUESTS_CA_BUNDLE and CURL_CA_BUNDLE before certifi, so a variable left pointing at an old file overrides an update. On OpenSSL builds, curl -v prints the CAfile it used.

    Costs nothing
  3. Look at the site’s certificate

    Check its expiry, the names it covers and whether the chain is complete. A broken chain is the site owner’s to fix; you can tell them.

    Costs nothing
  4. Disable verification for one test run only

    -k, verify=False and ignore_https_errors confirm a diagnosis. Left in a script, they remove the only check that the page came from the real site.

    Costs nothing
  5. Point your tool at your network’s CA

    If your machine sits behind a firewall that inspects TLS, your tool needs that firewall’s CA: REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS or --cacert.

    Costs some time
Per tool

See the real status and headers

Each sample shows the handshake with the site, or the exact certificate error, with verification left on.

curl
terminal
# -v shows the tunnel opening, then the TLS handshake with the site
curl -sv -o /dev/null -x http://USER:[email protected]:8000 https://example.com/ 2>&1 \
  | grep -E 'CONNECT|HTTP/1.1 200|CAfile|SSL|TLS|subject|expire|issuer'

# the site's certificate, checked without the proxy
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -enddate
Python Requests
tls.py
import os

import certifi
import requests

PROXY = "http://USER:[email protected]:8000"

print("CA bundle:", os.environ.get("REQUESTS_CA_BUNDLE") or os.environ.get("CURL_CA_BUNDLE") or certifi.where())
try:
    r = requests.get("https://example.com/", proxies={"http": PROXY, "https": PROXY}, timeout=30)
    print(r.status_code)
except requests.exceptions.SSLError as err:
    print(err)

The first line prints the bundle Requests checks the site against, in the order Requests looks for it.

Scrapy
settings.py + spider
# settings.py: Scrapy skips certificate checks unless this is on (2.15+)
DOWNLOAD_VERIFY_CERTIFICATES = True


# in the spider
from scrapy.exceptions import DownloadFailedError

async def start(self):
    for url in self.start_urls:
        yield scrapy.Request(url, errback=self.on_error)

def on_error(self, failure):
    if failure.check(DownloadFailedError) and "certificate verify failed" in str(failure.value):
        self.logger.error("%s: the certificate did not verify: %s", failure.request.url, failure.value)
    else:
        self.logger.error("%s: %r", failure.request.url, failure.value)

With verification on, Scrapy checks against the CA paths of its OpenSSL, not certifi. SSL_CERT_FILE points it at another bundle.

Playwright
tls.py
from playwright.sync_api import Error, sync_playwright

PROXY = {"server": "http://resi.proxymonkey.io:8000", "username": "USER", "password": "PASS"}

with sync_playwright() as p:
    browser = p.chromium.launch(proxy=PROXY)
    page = browser.new_page()
    try:
        page.goto("https://example.com/")
    except Error as err:
        print(err.message.splitlines()[0])
    browser.close()

# net::ERR_CERT_DATE_INVALID          the certificate has expired
# net::ERR_CERT_COMMON_NAME_INVALID   issued for a different name
# net::ERR_CERT_AUTHORITY_INVALID     not signed by a CA Chromium trusts
Node.js
tls.mjs
import { fetch, ProxyAgent } from "undici";

const dispatcher = new ProxyAgent("http://USER:[email protected]:8000");

try {
  await fetch("https://example.com/", { dispatcher });
} catch (err) {
  console.error(err.cause?.code, err.cause?.message);
}

// CERT_HAS_EXPIRED                   the certificate has expired
// ERR_TLS_CERT_ALTNAME_INVALID       issued for a different name
// UNABLE_TO_VERIFY_LEAF_SIGNATURE    the site's chain is incomplete
// behind a TLS-inspecting firewall:  NODE_EXTRA_CA_CERTS=./ca.pem node tls.mjs

The samples use the residential gateway, resi.proxymonkey.io:8000. For an ISP or datacenter IP, use USER:PASS@IP:PORT for the address you rented. Your dashboard lists the host and port for every order, and where it differs from this page, the dashboard is right.

Before you buy anything

Will a different proxy line fix it?

When switching helps

Switching line does not change which certificate your client checks. On an HTTPS site it is always the site’s own.

When it will not

Buying ISP or datacenter to get rid of a certificate error changes the address the site sees. The certificate check still happens on your machine, the same way.

The meter

Is a failed request billed?

We bill for request bytes and response bytes, including headers and protocol overhead on the tunnelled connection. Connections that fail before transferring data are not billed. Retries that you initiate are billed and appear as separate entries in your usage log.

From the metering section of our terms of service.

The terms count protocol overhead on the tunnelled connection as billed bytes, and say connections that fail before transferring data are not billed. A handshake that fails inside an open tunnel has moved some bytes, and the terms do not describe that case on its own. Check the row in your usage log and ask in Discord if it looks wrong.

Residential is billed per GB of that traffic. ISP and datacenter addresses are charged per IP for their term, and where a plan includes a traffic allowance, traffic past it is billed per GB under the same rule. The usage log in your dashboard has one row per request with bytes in, bytes out and cost, so you can look up the failed request yourself.

TLS, asked often

Questions people ask about a certificate error

Can the proxy see my HTTPS traffic?

No. For an HTTPS site the proxy opens a tunnel and passes encrypted bytes through. It sees the host and port you asked for and how many bytes moved, which is what the meter counts. It cannot read the pages or swap the certificate.

Why does the site work in my browser and fail in Python?

Browsers ship their own trust store and can fetch a missing intermediate certificate by themselves. Python uses the certifi bundle and fails where the site’s chain is incomplete or the bundle is old. Update certifi, then check the site’s chain.

Is verify=False safe?

For one test run to confirm the diagnosis, it does no harm. Left in a scraper, it means anyone in the path could hand you a fake page and your code would accept it.

Why does every site fail on one network and work on another?

Something on that network is re-signing TLS, usually a company firewall or an antivirus that scans HTTPS. Your browser may trust its CA because it was installed on the machine, while your script uses its own bundle and does not. Ask for the CA file and point your tool at it rather than turning verification off.

The community layer

Still stuck on a certificate error?

Paste your error in the Discord: the full message plus the command or the few lines that set up the proxy, with the password taken out. Someone there has seen it before.

Join the Discord

4,200+monkeys in the Discord

  • Help from humans

    Post your error, get an answer. Usually in minutes, usually from someone who has hit the same wall.

  • A status bot that tells on us

    Pool health, incidents and maintenance posted automatically. Including the bad days.

  • Deals and free traffic

    Bonus GB drops, early access to new pools, and the occasional giveaway for a good bug report.

Join the Discord4,200+ monkeys, free to lurk