From c9aaa9b41a6354d604ce8e93bd50803e952c161b Mon Sep 17 00:00:00 2001 From: Mauricio0129 Date: Mon, 6 Jul 2026 15:05:08 -0400 Subject: [PATCH 1/2] fix: do not retry on wrong password (28P01) with sslmode=allow/prefer --- asyncpg/connect_utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index 07c4fdde..4615f415 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -1103,9 +1103,14 @@ async def __connect_addr( except ( exceptions.InvalidAuthorizationSpecificationError, exceptions.ConnectionDoesNotExistError, # seen on Windows - ): + ) as exc: tr.close() - + + # Do not retry on wrong password (28P01) — the issue is credentials, + # not SSL negotiation. Only pg_hba.conf rejections (28000) warrant a retry. + if isinstance(exc, exceptions.InvalidPasswordError): + raise + # retry=True here is a redundant check because we don't want to # accidentally raise the internal _RetryConnectSignal to the user if retry and ( From 48d3f5aeabd717699d14e9383223064b9d50e3f7 Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Fri, 18 Sep 2026 23:02:14 -0700 Subject: [PATCH 2/2] Fix password error reporting during SSL fallback --- asyncpg/connect_utils.py | 31 ++++++++++++++++--------- tests/test_connect.py | 49 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index 4615f415..651bff58 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -1052,11 +1052,25 @@ async def _connect_addr( # first attempt try: return await __connect_addr(params, True, *args) - except _RetryConnectSignal: - pass + except _RetryConnectSignal as retry_exc: + first_error = retry_exc.__cause__ + assert first_error is not None # second attempt - return await __connect_addr(params_retry, False, *args) + try: + return await __connect_addr(params_retry, False, *args) + except ( + exceptions.InvalidAuthorizationSpecificationError, + exceptions.ConnectionDoesNotExistError, + ) as second_error: + # If the preferred attempt produced a useful authentication error, + # do not hide it behind a generic rejection from the fallback mode. + if ( + isinstance(first_error, exceptions.InvalidPasswordError) + and not isinstance(second_error, exceptions.InvalidPasswordError) + ): + raise first_error from None + raise class _RetryConnectSignal(Exception): @@ -1103,14 +1117,9 @@ async def __connect_addr( except ( exceptions.InvalidAuthorizationSpecificationError, exceptions.ConnectionDoesNotExistError, # seen on Windows - ) as exc: + ) as exc: tr.close() - - # Do not retry on wrong password (28P01) — the issue is credentials, - # not SSL negotiation. Only pg_hba.conf rejections (28000) warrant a retry. - if isinstance(exc, exceptions.InvalidPasswordError): - raise - + # retry=True here is a redundant check because we don't want to # accidentally raise the internal _RetryConnectSignal to the user if retry and ( @@ -1122,7 +1131,7 @@ async def __connect_addr( # 2. First attempt with sslmode=prefer, ssl=ctx failed while the # server claimed to support SSL (returning "S" for SSLRequest) # (likely because pg_hba.conf rejected the connection) - raise _RetryConnectSignal() + raise _RetryConnectSignal() from exc else: # but will NOT retry if: diff --git a/tests/test_connect.py b/tests/test_connect.py index 955fb825..fae32447 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1940,6 +1940,55 @@ async def verify_fails(sslmode, *, host='localhost', exn_type): await verify_fails('verify-full', exn_type=ssl.SSLError) + async def test_sslmode_preserves_password_error(self): + await self.con.execute( + "ALTER ROLE ssl_user PASSWORD 'correct_password'") + + cases = ( + ('prefer', 'hostssl', 'hostnossl', False), + ('allow', 'hostnossl', 'hostssl', True), + ) + for sslmode, first_type, fallback_type, fallback_is_ssl in cases: + with self.subTest(sslmode=sslmode): + self.cluster.reset_hba() + for address in ('127.0.0.0/24', '::1/128'): + self.cluster.add_hba_entry( + type=first_type, + address=ipaddress.ip_network(address), + database='postgres', user='ssl_user', + auth_method='password') + self.cluster.reload() + + connect_args = dict( + host='localhost', + database='postgres', + user='ssl_user', + password='wrong_password', + ssl=sslmode, + ) + + with self.assertRaisesRegex( + asyncpg.InvalidPasswordError, + 'password authentication failed', + ): + await self.connect(**connect_args) + + # A password failure in the preferred mode must not prevent + # a valid, differently-authenticated fallback connection. + for address in ('127.0.0.0/24', '::1/128'): + self.cluster.add_hba_entry( + type=fallback_type, + address=ipaddress.ip_network(address), + database='postgres', user='ssl_user', + auth_method='trust') + self.cluster.reload() + + con = await self.connect(**connect_args) + try: + self.assertEqual(con._protocol.is_ssl, fallback_is_ssl) + finally: + await con.close() + async def test_ssl_connection_default_context(self): # XXX: uvloop artifact old_handler = self.loop.get_exception_handler()