psf/requests · error · OSError

Could not find the TLS key file, invalid path

Error message

Could not find the TLS key file, invalid path: {conn.key_file}

What it means

Raised during cert_verify when a client certificate is supplied as a (cert, key) tuple and the resolved private key file path does not exist. After setting conn.key_file from the second tuple element, the code checks os.path.exists(conn.key_file); a missing key file aborts before the TLS handshake. The single-string cert form sets conn.key_file=None and so never triggers this branch.

Solutions

  1. Verify the key path exists with os.path.exists and that it is the second element of the cert tuple.
  2. Check file permissions/ownership for the process running the request.
  3. Ensure the secret volume mounts both files at the expected paths.
  4. If cert and key are combined in one PEM, pass the single string form instead of a tuple.
  5. In deployment, validate both halves of the cert pair in a startup probe.

Example fix

# before
session.cert = ("/etc/secrets/client.crt", "/etc/secrets/client.key")  # key missing -> OSError

# after
import os
crt, key = "/etc/secrets/client.crt", "/etc/secrets/client.key"
assert os.path.exists(crt) and os.path.exists(key), "cert or key missing"
session.cert = (crt, key)
Defensive patterns

Strategy: validation

Validate before calling

import os

def resolve_client_cert_tuple(cert):
    """For the (cert, key) tuple form, validate both files exist."""
    if isinstance(cert, (tuple, list)):
        crt, key = cert
        if not (os.path.exists(crt) and os.path.exists(key)):
            raise FileNotFoundError(
                f"cert/key missing: cert={crt} key={key}"
            )
        return (crt, key)
    return cert  # string or None handled elsewhere

session.cert = resolve_client_cert_tuple(config["client_cert"])

Type guard

import os

def has_valid_key_file(cert) -> bool:
    """True when cert is a tuple whose key file (2nd element) exists."""
    if isinstance(cert, (tuple, list)) and len(cert) == 2:
        return os.path.exists(cert[1]) and os.access(cert[1], os.R_OK)
    # string form sets key_file=None, so this branch is N/A
    return True

Try / catch

try:
    resp = session.get(url)
except OSError as e:
    if "TLS key file" in str(e):
        raise RuntimeError(
            f"client key missing/unreadable: {session.cert}"
        ) from e
    raise

Prevention

When it happens

Trigger: Produced when cert=("/path/client.crt", "/path/client.key") is passed to a request or session and the key file (second element) does not exist. Only reachable via the tuple form, since the string form leaves key_file as None.

Common situations: Seen when the cert and key are stored in separate files and only one was deployed, when secret rotation updated the cert but not the key path, when the key file has restrictive permissions that effectively hide it, or when a path typo affects only the key element.

Understand the failure class

Related errors


AI-assisted analysis of psf/requests@8068356288 (2026-08-11). Data as JSON: /api/errors/b4a8a0373582a990. Report an issue: GitHub.

Appendix: source

Thrown at src/requests/adapters.py:361

        else:
            conn.cert_reqs = "CERT_NONE"
            conn.ca_certs = None
            conn.ca_cert_dir = None

        if cert:
            if not isinstance(cert, basestring):
                conn.cert_file = cert[0]
                conn.key_file = cert[1]
            else:
                conn.cert_file = cert
                conn.key_file = None
            if conn.cert_file and not os.path.exists(conn.cert_file):
                raise OSError(
                    f"Could not find the TLS certificate file, "
                    f"invalid path: {conn.cert_file}"
                )
            if conn.key_file and not os.path.exists(conn.key_file):
                raise OSError(
                    f"Could not find the TLS key file, invalid path: {conn.key_file}"
                )

    def build_response(self, req: PreparedRequest, resp: Any) -> Response:
        """Builds a :class:`Response <requests.Response>` object from a urllib3
        response. This should not be called from user code, and is only exposed
        for use when subclassing the
        :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`

        :param req: The :class:`PreparedRequest <PreparedRequest>` used to generate the response.
        :param resp: The urllib3 response object.
        :rtype: requests.Response
        """
        assert _is_prepared(req)
        response = Response()

        # Fallback to None if there's no status_code, for whatever reason.
        response.status_code = getattr(resp, "status", None)  # type: ignore[assignment]

View on GitHub (pinned to 8068356288)