From 5c0de5f5d47adf575f0f4a37dd15939bf26ecc61 Mon Sep 17 00:00:00 2001
From: Samat K Jain <_@skj.io>
Date: Mon, 18 May 2026 14:20:51 -0700
Subject: [PATCH 1/4] gh-150101 Expose OpenSSL's error queue through
SSLError.error_queue
---
Doc/library/ssl.rst | 10 ++++++++-
Lib/test/test_ssl.py | 4 ++++
Modules/_ssl.c | 51 ++++++++++++++++++++++++++++++++++++++++++++
Modules/_ssl.h | 1 +
4 files changed, 65 insertions(+), 1 deletion(-)
diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst
index b180673f22973e..65a09c3a2b7fc8 100644
--- a/Doc/library/ssl.rst
+++ b/Doc/library/ssl.rst
@@ -262,7 +262,15 @@ Exceptions
example ``CERTIFICATE_VERIFY_FAILED``. The range of possible
values depends on the OpenSSL version.
- .. versionadded:: 3.3
+ .. attribute:: error_queue
+
+ The full OpenSSL error queue, as a list of strings, where the last
+ item of the list is the top of the queue. If :attr:`reason` is not enough
+ to diagnose a problem, OpenSSL may report more detail through this queue.
+ The format of the strings follows that of OpenSSL's
+ `ERR_error_string `_.
+
+ .. versionadded:: 3.16
.. exception:: SSLZeroReturnError
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
index f1f7a07701de16..a5a0e2822eba80 100644
--- a/Lib/test/test_ssl.py
+++ b/Lib/test/test_ssl.py
@@ -1909,6 +1909,7 @@ def test_lib_reason(self):
self.assertEqual(cm.exception.library, 'PEM')
regex = "(NO_START_LINE|UNSUPPORTED_PUBLIC_KEY_TYPE)"
self.assertRegex(cm.exception.reason, regex)
+ self.assertTrue(len(cm.exception.error_queue >= 1))
s = str(cm.exception)
self.assertIn("NO_START_LINE", s)
@@ -4688,6 +4689,7 @@ def cb_returning_alert(ssl_sock, server_name, initial_context):
chatty=False,
sni_name='supermessage')
self.assertEqual(cm.exception.reason, 'TLSV1_ALERT_ACCESS_DENIED')
+ self.assertTrue(len(cm.exception.error_queue >= 1))
def test_sni_callback_raising(self):
# Raising fails the connection with a TLS handshake failure alert.
@@ -4708,6 +4710,7 @@ def cb_raising(ssl_sock, server_name, initial_context):
"|SSLV3_ALERT_HANDSHAKE_FAILURE"
"|NO_PRIVATE_VALUE)")
self.assertRegex(cm.exception.reason, regex)
+ self.assertTrue(len(cm.exception.error_queue >= 1))
self.assertEqual(catch.unraisable.exc_type, ZeroDivisionError)
def test_sni_callback_wrong_return_type(self):
@@ -4727,6 +4730,7 @@ def cb_wrong_return_type(ssl_sock, server_name, initial_context):
self.assertEqual(cm.exception.reason, 'TLSV1_ALERT_INTERNAL_ERROR')
+ self.assertTrue(len(cm.exception.error_queue >= 1))
self.assertEqual(catch.unraisable.exc_type, TypeError)
def test_shared_ciphers(self):
diff --git a/Modules/_ssl.c b/Modules/_ssl.c
index 3224ca7d0f93b9..2151255a79904d 100644
--- a/Modules/_ssl.c
+++ b/Modules/_ssl.c
@@ -513,6 +513,8 @@ fill_and_set_sslerror(_sslmodulestate *state,
PyObject *verify_obj = NULL, *verify_code_obj = NULL;
PyObject *init_value, *msg, *key;
PyUnicodeWriter *writer = NULL;
+ PyObject *error_queue = PyList_New(0);
+ if (!error_queue) goto fail;
if (ssl_errno == PY_SSL_ERROR_EOF && sslsock != NULL) {
sslsock->got_eof_error = 1;
@@ -542,6 +544,45 @@ fill_and_set_sslerror(_sslmodulestate *state,
if (errstr == NULL) {
errstr = ERR_reason_error_string(errcode);
}
+
+ /* populate error_list from OpenSSL's error queue */
+ unsigned int q_pos = 0; /* Error queue position */
+ const char *eq_filename, *eq_func, *eq_data;
+ char eq_msg[256];
+ int eq_lineno;
+ int flags; /* Flags (discarded) */
+ unsigned long openssl_errorcode;
+
+ /* Presumably, we no longer need the OpenSSL error queue after this, so
+ we can call ERR_get_error (destructive) instead of ERR_peek_error */
+ while ((openssl_errorcode = ERR_get_error_all(&eq_filename, &eq_lineno, &eq_func,
+ &eq_data, &flags))) {
+ if (q_pos == 0) {
+ /* errcode should have come from a caller, and should have been
+ returned from ERR_peek_last_error() */
+ assert(openssl_errorcode == errcode);
+ }
+
+ ERR_error_string_n(openssl_errorcode, eq_msg, 256);
+
+ // Follows [lib reason] error_string extra_data (OpenSSL file:func:line)
+ PyObject *current_eq_msg = NULL;
+ if (eq_data != NULL) {
+ current_eq_msg = PyUnicode_FromFormat(
+ "%s %s (%s:%s:%d)",
+ eq_msg, eq_data, eq_filename, eq_func, eq_lineno
+ );
+ } else {
+ current_eq_msg = PyUnicode_FromFormat(
+ "%s (%s:%s:%d)",
+ eq_msg, eq_filename, eq_func, eq_lineno
+ );
+ }
+ if (PyList_Append(error_queue, current_eq_msg) != 0) {
+ goto fail;
+ }
+ q_pos++;
+ }
}
/* verify code for cert validation error */
@@ -655,6 +696,12 @@ fill_and_set_sslerror(_sslmodulestate *state,
goto fail;
}
+ /* Add the full OpenSSL error queue to exception */
+ if (PyObject_SetAttr(err_value, state->str_error_queue, error_queue) != 0) {
+ goto fail;
+ }
+ Py_DECREF(error_queue);
+
PyErr_SetObject(type, err_value);
fail:
Py_XDECREF(err_value);
@@ -7359,6 +7406,10 @@ sslmodule_init_strings(PyObject *module)
if (state->str_verify_code == NULL) {
return -1;
}
+ state->str_error_queue = PyUnicode_InternFromString("error_queue");
+ if (state->str_error_queue == NULL) {
+ return -1;
+ }
return 0;
}
diff --git a/Modules/_ssl.h b/Modules/_ssl.h
index 22d93ddcc6d6eb..5510b3644b07ef 100644
--- a/Modules/_ssl.h
+++ b/Modules/_ssl.h
@@ -33,6 +33,7 @@ typedef struct {
PyObject *str_reason;
PyObject *str_verify_code;
PyObject *str_verify_message;
+ PyObject *str_error_queue;
/* keylog lock */
PyThread_type_lock keylog_lock;
} _sslmodulestate;
From 690018757a75c47ca02748513199e247f56ff0a2 Mon Sep 17 00:00:00 2001
From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com>
Date: Tue, 19 May 2026 19:07:34 +0000
Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?=
=?UTF-8?q?rb=5Fit.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../next/Library/2026-05-19-19-07-32.gh-issue-150101.x62Rm6.rst | 1 +
1 file changed, 1 insertion(+)
create mode 100644 Misc/NEWS.d/next/Library/2026-05-19-19-07-32.gh-issue-150101.x62Rm6.rst
diff --git a/Misc/NEWS.d/next/Library/2026-05-19-19-07-32.gh-issue-150101.x62Rm6.rst b/Misc/NEWS.d/next/Library/2026-05-19-19-07-32.gh-issue-150101.x62Rm6.rst
new file mode 100644
index 00000000000000..eef7dad89b3978
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-05-19-19-07-32.gh-issue-150101.x62Rm6.rst
@@ -0,0 +1 @@
+Expose OpenSSL's error queue through SSLError.error_queue
From 83591f2f9449d8174f4458cc0f55c4c6dd89be09 Mon Sep 17 00:00:00 2001
From: Samat K Jain <_@skj.io>
Date: Tue, 19 May 2026 12:17:14 -0700
Subject: [PATCH 3/4] fix bad copy-pasta syntax error
---
Lib/test/test_ssl.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
index a5a0e2822eba80..176c8bad1760b8 100644
--- a/Lib/test/test_ssl.py
+++ b/Lib/test/test_ssl.py
@@ -1909,7 +1909,7 @@ def test_lib_reason(self):
self.assertEqual(cm.exception.library, 'PEM')
regex = "(NO_START_LINE|UNSUPPORTED_PUBLIC_KEY_TYPE)"
self.assertRegex(cm.exception.reason, regex)
- self.assertTrue(len(cm.exception.error_queue >= 1))
+ self.assertTrue(len(cm.exception.error_queue) >= 1)
s = str(cm.exception)
self.assertIn("NO_START_LINE", s)
@@ -4689,7 +4689,7 @@ def cb_returning_alert(ssl_sock, server_name, initial_context):
chatty=False,
sni_name='supermessage')
self.assertEqual(cm.exception.reason, 'TLSV1_ALERT_ACCESS_DENIED')
- self.assertTrue(len(cm.exception.error_queue >= 1))
+ self.assertTrue(len(cm.exception.error_queue) >= 1)
def test_sni_callback_raising(self):
# Raising fails the connection with a TLS handshake failure alert.
@@ -4710,7 +4710,7 @@ def cb_raising(ssl_sock, server_name, initial_context):
"|SSLV3_ALERT_HANDSHAKE_FAILURE"
"|NO_PRIVATE_VALUE)")
self.assertRegex(cm.exception.reason, regex)
- self.assertTrue(len(cm.exception.error_queue >= 1))
+ self.assertTrue(len(cm.exception.error_queue) >= 1)
self.assertEqual(catch.unraisable.exc_type, ZeroDivisionError)
def test_sni_callback_wrong_return_type(self):
@@ -4730,7 +4730,7 @@ def cb_wrong_return_type(ssl_sock, server_name, initial_context):
self.assertEqual(cm.exception.reason, 'TLSV1_ALERT_INTERNAL_ERROR')
- self.assertTrue(len(cm.exception.error_queue >= 1))
+ self.assertTrue(len(cm.exception.error_queue) >= 1)
self.assertEqual(catch.unraisable.exc_type, TypeError)
def test_shared_ciphers(self):
From 28e1f40fc5fe4b97cd237a79d82e00788750cf12 Mon Sep 17 00:00:00 2001
From: Samat K Jain <_@skj.io>
Date: Tue, 19 May 2026 12:25:49 -0700
Subject: [PATCH 4/4] qualify folks not rely on the wording, from in-person
review by gpshead
---
Doc/library/ssl.rst | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst
index 65a09c3a2b7fc8..4199ad9cfd9cbe 100644
--- a/Doc/library/ssl.rst
+++ b/Doc/library/ssl.rst
@@ -268,7 +268,9 @@ Exceptions
item of the list is the top of the queue. If :attr:`reason` is not enough
to diagnose a problem, OpenSSL may report more detail through this queue.
The format of the strings follows that of OpenSSL's
- `ERR_error_string `_.
+ `ERR_error_string `_,
+ and may change between OpenSSL versions (i.e. do not rely on the exact
+ wording of these messages).
.. versionadded:: 3.16