ScreenshotNeo

BlogHow-to

How to Fix an SSLError in Python Requests

Fix Python Requests SSLError errors by identifying the certificate, hostname, CA, proxy, or client-certificate problem and applying the right secure remedy.

By the ScreenshotNeo team29 September 20269 min read

How to Fix an SSLError in Python Requests

An SSLError in Python Requests means the HTTPS connection could not complete its TLS checks. The exact fix depends on the exception text: an untrusted certificate chain needs the correct CA bundle, a hostname mismatch needs the correct URL or server certificate, and mutual TLS needs a valid client certificate. Start by preserving verification, reading the complete traceback, and matching the error to its cause.

Requests verifies HTTPS certificates by default and raises SSLError when it cannot verify the server certificate. Setting verify=False accepts untrusted, expired, or mismatched certificates and exposes the connection to man-in-the-middle attacks, so it is not a production fix. See the Requests advanced usage documentation for the verification rules.

1. Identify the exact SSL failure

Save the full traceback, including the nested OpenSSL message. These common endings point to different repairs:

The right remedy depends on whether the server chain is public, private, or mismatched.
The right remedy depends on whether the server chain is public, private, or mismatched.
Error text Usually means First action
CERTIFICATE_VERIFY_FAILED The issuer or chain is not trusted, or the certificate is expired. Check the public CA chain and configure an approved private CA bundle if needed.
hostname 'x' doesn't match The certificate identity does not cover the hostname in the URL. Check the URL, redirects, proxy, and certificate SANs.
unknown ca or unable to get local issuer certificate A required issuing CA is missing from the trust store. Obtain the correct CA bundle through your organization’s trusted process.
WRONG_VERSION_NUMBER or handshake errors The endpoint, proxy, or TLS protocol configuration is incompatible. Confirm HTTPS versus HTTP and inspect proxy settings.
Errors loading cert or key The client certificate path, key, or pair is invalid. Check file paths, permissions, format, and certificate/key matching.

Also record the Python version, Requests version, operating system, URL hostname, whether a corporate proxy or TLS inspection device is present, and whether the failure occurs for every HTTPS site or only one endpoint. Without that environment information, a traceback cannot prove which component is responsible.

2. Verify the URL and the server identity

For a hostname mismatch, the server sent a certificate whose names do not include the hostname Requests believes it is contacting. Check for spelling mistakes, an IP address used instead of a DNS name, redirects to another host, and a proxy that replaces the origin certificate. Requests’ FAQ describes hostname mismatch as an endpoint identity problem.

A hostname mismatch is an identity problem involving the URL, certificate, or proxy.
A hostname mismatch is an identity problem involving the URL, certificate, or proxy.
import requests

url = 'https://api.example.com/health'
response = requests.get(url, timeout=30)
response.raise_for_status()
print(response.status_code)

Do not “solve” a mismatch by disabling verification. If you operate the server, issue a certificate containing the requested DNS name in its Subject Alternative Name field. If a proxy performs TLS inspection, install the organization’s documented inspection CA in the client trust configuration instead of trusting a certificate copied from an unverified connection.

3. Fix an untrusted or private certificate authority

Public websites normally chain to a CA already trusted by the system or Requests’ CA bundle. Internal services often use an enterprise or private CA. Obtain the approved PEM bundle from the service owner or network administrator, store it with appropriate file permissions, and pass its path to Requests.

Per-request CA bundle

import requests

ca_bundle = '/etc/company/pki/enterprise-ca-bundle.pem'
r = requests.get(
    'https://internal.example.com/report',
    verify=ca_bundle,
    timeout=30,
)
r.raise_for_status()
print(r.text)

Session-wide CA bundle

import requests

session = requests.Session()
session.verify = '/etc/company/pki/enterprise-ca-bundle.pem'
r = session.get('https://internal.example.com/report', timeout=30)
r.raise_for_status()

Environment variables

Requests honors REQUESTS_CA_BUNDLE. If it is unset, CURL_CA_BUNDLE is a fallback. This is useful for containers and deployment environments where code should remain unchanged.

export REQUESTS_CA_BUNDLE=/etc/company/pki/enterprise-ca-bundle.pem
python fetch_report.py

Confirm that the file is a CA bundle in PEM format, contains the issuing chain required by the server, and is readable by the process. Never download a bundle over the failing connection and trust it without independently confirming its origin.

4. Handle mutual TLS with the cert argument

Some servers require mutual TLS (mTLS): the server proves its identity to Requests, and Requests also presents a client certificate. The cert option is separate from verify: verify authenticates the server; cert supplies the client credential.

import requests

# Combined PEM containing the client certificate and private key
r = requests.get(
    'https://mtls.example.com/data',
    cert='/secure/client.pem',
    verify='/secure/ca-bundle.pem',
    timeout=30,
)
r.raise_for_status()

When the certificate and key are separate, pass a tuple:

r = requests.get(
    'https://mtls.example.com/data',
    cert=('/secure/client.crt', '/secure/client.key'),
    verify='/secure/ca-bundle.pem',
    timeout=30,
)

A client-certificate error can come from a missing file, unreadable permissions, an encrypted key that the process cannot unlock, an expired certificate, or a certificate and key that do not match. The Requests API reference documents the accepted certificate forms.

5. Make environment settings work with prepared requests

A normal requests.get() call applies environment settings. If you build a PreparedRequest manually and send it through a session, merge environment settings explicitly; otherwise REQUESTS_CA_BUNDLE and proxy variables may not be applied.

import requests

session = requests.Session()
request = requests.Request('GET', 'https://internal.example.com/data')
prepared = session.prepare_request(request)
environment = session.merge_environment_settings(
    prepared.url,
    proxies={},
    stream=None,
    verify=None,
    cert=None,
)
response = session.send(prepared, timeout=30, **environment)
response.raise_for_status()

6. Complete diagnostic script

This script prints versions and distinguishes certificate failures without weakening verification.

import ssl
import sys
import requests
from requests.exceptions import SSLError, RequestException

url = 'https://api.example.com/health'
print('Python:', sys.version)
print('OpenSSL:', ssl.OPENSSL_VERSION)
print('Requests:', requests.__version__)

try:
    response = requests.get(url, timeout=(10, 30))
    response.raise_for_status()
    print('OK:', response.status_code)
except SSLError as exc:
    print('TLS verification or handshake failed:')
    print(exc)
    raise
except RequestException as exc:
    print('HTTP request failed:')
    print(exc)
    raise

7. cURL and Node.js equivalents

Using another client helps isolate whether the issue is Python-specific. Keep certificate verification enabled in every client.

cURL

curl --fail --show-error --location \
  --cacert /etc/company/pki/enterprise-ca-bundle.pem \
  https://internal.example.com/health

For mTLS with separate files:

curl --fail --show-error \
  --cacert /secure/ca-bundle.pem \
  --cert /secure/client.crt \
  --key /secure/client.key \
  https://mtls.example.com/data

Node.js

import fs from 'node:fs';

const response = await fetch('https://internal.example.com/health', {
  // Node’s fetch uses an undici dispatcher for custom TLS settings.
  // Configure the approved CA in your runtime or HTTP agent for your Node version.
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
console.log(await response.text());

Do not use a global “reject unauthorized: false” setting in Node or an equivalent insecure switch. Configure the trusted CA in the runtime or agent used by your deployment.

8. Troubleshooting checklist

Symptom Cause to check Secure fix
Works in a browser but not Python Browser has an enterprise root or uses a different proxy. Export the approved enterprise CA and configure verify or REQUESTS_CA_BUNDLE.
Only one hostname fails Bad certificate SAN, wrong DNS record, or redirect. Inspect the certificate for that hostname and correct DNS/server configuration.
Everything fails after a machine image update Outdated CA bundle, Python, or OpenSSL build. Update through your approved package channel and verify the resulting trust store.
Failure appears only behind a corporate network TLS inspection proxy presents an enterprise certificate. Configure the documented proxy and enterprise CA; do not trust an arbitrary leaf certificate.
verify path has no effect Wrong path, unreadable file, non-PEM file, or prepared-request environment not merged. Check the file and use merge_environment_settings for prepared requests.
Handshake fails before verification Protocol mismatch, proxy speaking HTTP on an HTTPS port, or incompatible cipher. Confirm scheme and proxy URL, then inspect server TLS policy and supported versions.
Intermittent failures Different load balancer nodes expose different chains or certificates. Check every node and certificate rotation path; use retries only for transient transport errors.

9. Reliability, performance, and cost considerations

Certificate verification adds a small amount of CPU work during a new TLS connection, but connection reuse through requests.Session avoids repeated handshakes and is usually the largest practical improvement.

import requests

with requests.Session() as session:
    session.verify = '/etc/company/pki/enterprise-ca-bundle.pem'
    for endpoint in ('/users', '/projects', '/usage'):
        response = session.get('https://api.example.com' + endpoint, timeout=30)
        response.raise_for_status()

Use explicit connect and read timeouts, and retry only idempotent requests when the failure is demonstrably transient. A retry cannot repair a wrong hostname or missing CA and can hide a persistent configuration defect. Monitor certificate expiration and CA rotation before production deploys. There is no license cost for Requests itself; operational cost comes from maintaining the server certificate, private CA process, proxy configuration, and observability.

10. What not to do

  • Do not ship verify=False to bypass the error.
  • Do not paste a certificate from an unverified connection into your trust store.
  • Do not confuse a server CA bundle (verify) with a client identity (cert).
  • Do not fix a hostname mismatch by changing the hostname to whatever certificate happens to be presented.
  • Do not suppress the traceback; its final OpenSSL message is often the fastest route to the correct fix.

11. Or skip the browser setup

If the task behind your Python request is producing website screenshots, ScreenshotNeo removes the browser and certificate-management setup from your application. Its API accepts one GET request and returns PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for all options.

import requests
r = requests.get('https://api.screenshotneo.com/v1/shot', params={'access_key': 'YOUR_API_KEY', 'url': 'https://stripe.com'}, timeout=90)
open('shot.webp', 'wb').write(r.content)

cURL

curl -G 'https://api.screenshotneo.com/v1/shot' -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan. Create a free ScreenshotNeo account.

12. FAQ

Should I update certifi?

Updating a dependency can help when a public CA has changed, but first confirm your environment and package policy. An enterprise CA still needs to be configured explicitly.

Can I use an IP address in the URL?

Only if the server certificate includes that IP address in its identity fields. DNS names are the usual and more reliable choice.

Does cert fix CERTIFICATE_VERIFY_FAILED?

No. cert is for your client identity in mTLS. Use verify or the CA environment variables to trust the server.

Why does a request succeed with curl but fail in Requests?

The clients may use different CA bundles, proxy variables, TLS libraries, or hostname handling. Compare their CA paths and proxy configuration before changing verification.

Is a self-signed certificate always unusable?

No. It can be used when you intentionally trust it by supplying the approved certificate or CA bundle with verify. Trusting it blindly defeats TLS identity checks.

Summary

Read the complete exception, verify the hostname, configure the approved CA bundle for private services, and use cert only for client authentication. Keep TLS verification enabled, apply environment settings correctly in prepared-request flows, and reuse sessions for reliable connections.