From 3e2d735f676bdf268c7dc764ba5dc78db47e1c85 Mon Sep 17 00:00:00 2001 From: jtvhd6 Date: Tue, 16 Jun 2026 20:50:37 -0500 Subject: [PATCH] fix: improve SSL certificate error messages with human-readable output (#1549) --- httpie/ssl_errors.py | 39 ++++++++++++++++++++++++++++++++++ tests/test_ssl_errors.py | 45 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 httpie/ssl_errors.py create mode 100644 tests/test_ssl_errors.py diff --git a/httpie/ssl_errors.py b/httpie/ssl_errors.py new file mode 100644 index 0000000000..1cac55991d --- /dev/null +++ b/httpie/ssl_errors.py @@ -0,0 +1,39 @@ +import re +from typing import Optional + +_SSL_ERROR_MESSAGES = { + r'CERTIFICATE_VERIFY_FAILED.*unable to get local issuer': ( + 'SSL Error: The server certificate could not be verified because the\n' + 'issuing Certificate Authority (CA) is not trusted by your system.\n' + 'Possible fixes:\n' + ' 1. Specify a custom CA bundle: --verify=/path/to/ca-bundle.crt\n' + ' 2. If using a corporate proxy, install your organization CA cert.\n' + ' WARNING: Do NOT use --verify=no in production environments.' + ), + r'CERTIFICATE_VERIFY_FAILED.*certificate has expired': ( + 'SSL Error: The server certificate has EXPIRED.\n' + 'The remote host must renew its SSL certificate.\n' + 'Contact the server administrator if this is unexpected.' + ), + r'CERTIFICATE_VERIFY_FAILED.*self.signed': ( + 'SSL Error: The server is using a SELF-SIGNED certificate.\n' + 'If you trust this server, you may provide the cert:\n' + ' --verify=/path/to/server.crt' + ), + r'hostname.*does not match': ( + 'SSL Error: HOSTNAME MISMATCH — the certificate is valid, but\n' + 'it was issued for a different hostname than the one you requested.' + ), +} + + +def get_ssl_error_message(exc: Exception) -> Optional[str]: + """ + Inspect *exc* and return a human-readable description. + Returns None if the error is unrecognised. + """ + msg = str(exc) + for pattern, friendly in _SSL_ERROR_MESSAGES.items(): + if re.search(pattern, msg, re.IGNORECASE): + return friendly + return None \ No newline at end of file diff --git a/tests/test_ssl_errors.py b/tests/test_ssl_errors.py new file mode 100644 index 0000000000..19e4f76770 --- /dev/null +++ b/tests/test_ssl_errors.py @@ -0,0 +1,45 @@ +import pytest +from httpie.ssl_errors import get_ssl_error_message + + +class TestGetSslErrorMessage: + + def test_local_issuer_certificate(self): + exc = Exception( + '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: ' + 'unable to get local issuer certificate (_ssl.c:1000)' + ) + result = get_ssl_error_message(exc) + assert result is not None + assert 'Certificate Authority' in result + assert '--verify=' in result + + def test_expired_certificate(self): + exc = Exception( + '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: ' + 'certificate has expired' + ) + result = get_ssl_error_message(exc) + assert result is not None + assert 'EXPIRED' in result + + def test_self_signed_certificate(self): + exc = Exception( + '[SSL: CERTIFICATE_VERIFY_FAILED] self signed certificate' + ) + result = get_ssl_error_message(exc) + assert result is not None + assert 'SELF-SIGNED' in result + + def test_hostname_mismatch(self): + exc = Exception( + "hostname 'example.com' does not match 'other.com'" + ) + result = get_ssl_error_message(exc) + assert result is not None + assert 'HOSTNAME MISMATCH' in result + + def test_unrecognised_error_returns_none(self): + exc = Exception('Some completely unknown SSL error') + result = get_ssl_error_message(exc) + assert result is None \ No newline at end of file