]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
fix: retain pgconn on OperationalError 1247/head
authorErik Wienhold <ewie@ewie.name>
Wed, 14 Jan 2026 13:42:18 +0000 (14:42 +0100)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Sun, 25 Jan 2026 13:06:48 +0000 (13:06 +0000)
When multiple connection attempts fail, a new OperationalError with the
combined attempt errors is raised without retaining the pgconn attribute
of the original attempt errors.  Fix this by assigning the pgconn from
the last attempt error.

docs/news.rst
psycopg/psycopg/connection.py
psycopg/psycopg/connection_async.py
tests/test_connection.py
tests/test_connection_async.py

index 4ac9cc8635073ccfeea49f79906bc529024e0d0c..156f9edea7c01528ef57f19d94aa8c26182d63e2 100644 (file)
 Current release
 ---------------
 
+Psyocpg 3.3.3 (unreleased)
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Retain `Error.pgconn` when raising a single exception for multiple connection
+attempt errors (:ticket:`#1246`).
+
+
 Psycopg 3.3.2
 ^^^^^^^^^^^^^
 
index 0d74066f329d5fb2414774946aa50cee6587d5d9..c50a386f9cebcc527f3196f67a3f2d7385821d70 100644 (file)
@@ -126,7 +126,8 @@ class Connection(BaseConnection[Row]):
             lines = [str(last_ex)]
             lines.append("Multiple connection attempts failed. All failures were:")
             lines.extend((f"- {descr}: {error}" for error, descr in conn_errors))
-            raise type(last_ex)("\n".join(lines)).with_traceback(None)
+            new_ex = type(last_ex)("\n".join(lines), pgconn=last_ex.pgconn)
+            raise new_ex.with_traceback(None)
 
         if (
             capabilities.has_used_gssapi()
index 0a10ae24a183f58f7fdf167e1b0b1e71b5268942..3c8edc38329f857fc188d5b098fc56f580e839f8 100644 (file)
@@ -142,7 +142,8 @@ class AsyncConnection(BaseConnection[Row]):
             lines = [str(last_ex)]
             lines.append("Multiple connection attempts failed. All failures were:")
             lines.extend(f"- {descr}: {error}" for error, descr in conn_errors)
-            raise type(last_ex)("\n".join(lines)).with_traceback(None)
+            new_ex = type(last_ex)("\n".join(lines), pgconn=last_ex.pgconn)
+            raise new_ex.with_traceback(None)
 
         if (
             capabilities.has_used_gssapi()
index c30fb9bee1ec1ce6f1cc5b8b9cadb15ee9c7c3bd..8510afcc9b3c18e7d7ec26572940141d2694636a 100644 (file)
@@ -3,6 +3,7 @@
 # DO NOT CHANGE! Change the original file instead.
 from __future__ import annotations
 
+import os
 import sys
 import time
 import logging
@@ -15,7 +16,7 @@ import psycopg
 from psycopg import errors as e
 from psycopg import pq
 from psycopg.rows import tuple_row
-from psycopg.conninfo import conninfo_to_dict, timeout_from_conninfo
+from psycopg.conninfo import conninfo_to_dict, make_conninfo, timeout_from_conninfo
 from psycopg._conninfo_utils import get_param
 
 from .acompat import skip_async, skip_sync, sleep
@@ -80,6 +81,25 @@ def test_connect_error_multi_hosts_each_message_preserved(conn_cls):
     )
 
 
+def test_multi_attempt_pgconn(conn_cls, dsn, monkeypatch):
+    with conn_cls.connect(dsn) as conn:
+        if not conn.pgconn.used_password:
+            pytest.skip("test connection needs no password")
+
+    monkeypatch.delenv("PGPASSWORD", raising=False)
+    info = conninfo_to_dict(dsn)
+    info.pop("password", None)
+    info["host"] = ",".join([str(info.get("host", os.environ.get("PGHOST", "")))] * 2)
+    dsn = make_conninfo("", **info)
+
+    with pytest.raises(psycopg.OperationalError, match="connection failed:") as e:
+        conn_cls.connect(dsn)
+
+    msg = str(e.value)
+    assert MULTI_FAILURE_MESSAGE in msg
+    assert e.value.pgconn
+
+
 def test_connect_str_subclass(conn_cls, dsn):
 
     class MyString(str):
index 99a7ff41ef1c536cdf1d4be787c4aa137a3e0ade..122228a49cd96ca4bb953494f6ff3f19b03d70e3 100644 (file)
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import os
 import sys
 import time
 import logging
@@ -12,7 +13,7 @@ import psycopg
 from psycopg import errors as e
 from psycopg import pq
 from psycopg.rows import tuple_row
-from psycopg.conninfo import conninfo_to_dict, timeout_from_conninfo
+from psycopg.conninfo import conninfo_to_dict, make_conninfo, timeout_from_conninfo
 from psycopg._conninfo_utils import get_param
 
 from .acompat import asleep, skip_async, skip_sync
@@ -76,6 +77,25 @@ async def test_connect_error_multi_hosts_each_message_preserved(aconn_cls):
     assert any(expected_host2 in line and expected_error in line for line in msg_lines)
 
 
+async def test_multi_attempt_pgconn(aconn_cls, dsn, monkeypatch):
+    async with await aconn_cls.connect(dsn) as conn:
+        if not conn.pgconn.used_password:
+            pytest.skip("test connection needs no password")
+
+    monkeypatch.delenv("PGPASSWORD", raising=False)
+    info = conninfo_to_dict(dsn)
+    info.pop("password", None)
+    info["host"] = ",".join([str(info.get("host", os.environ.get("PGHOST", "")))] * 2)
+    dsn = make_conninfo("", **info)
+
+    with pytest.raises(psycopg.OperationalError, match="connection failed:") as e:
+        await aconn_cls.connect(dsn)
+
+    msg = str(e.value)
+    assert MULTI_FAILURE_MESSAGE in msg
+    assert e.value.pgconn
+
+
 async def test_connect_str_subclass(aconn_cls, dsn):
     class MyString(str):
         pass