LonamiWebs/Telethon

TypeError: ConnectionError() takes no keyword arguments

delobanov opened this issue · 5 comments

Code that causes the issue

I found problem in this patch of standard python_socks error.

if python_socks:
# python_socks internal errors are not inherited from
# builtin IOError (just from Exception). Instead of adding those
# in exceptions clauses everywhere through the code, we
# rather monkey-patch them in place.
python_socks._errors.ProxyError = ConnectionError
python_socks._errors.ProxyConnectionError = ConnectionError
python_socks._errors.ProxyTimeoutError = ConnectionError

Expected behavior

Error python_socks._protocols.errors.ReplyError: Host unreachable should be handled properly.

Actual behavior

But it causes TypeError: ConnectionError() takes no keyword arguments.

Traceback

  • Exception Group Traceback (most recent call last):
    | File "/opt/telegram_client/src/scripts/load_new_accounts.py", line 170, in
    | asyncio.run(main())
    | File "/usr/lib/python3.12/asyncio/runners.py", line 194, in run
    | return runner.run(main)
    | ^^^^^^^^^^^^^^^^
    | File "/usr/lib/python3.12/asyncio/runners.py", line 118, in run
    | return self.loop.run_until_complete(task)
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | File "/usr/lib/python3.12/asyncio/base_events.py", line 687, in run_until_complete
    | return future.result()
    | ^^^^^^^^^^^^^^^
    | File "/opt/telegram_client/src/scripts/load_new_accounts.py", line 161, in main
    | async with asyncio.TaskGroup() as tg:
    | File "/usr/lib/python3.12/asyncio/taskgroups.py", line 145, in aexit
    | raise me from None
    | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
    +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    | File "/opt/telegram_client/.venv/lib/python3.12/site-packages/python_socks/async
    /asyncio/_proxy.py", line 93, in _connect
    | await connector.connect(
    | File "/opt/telegram_client/.venv/lib/python3.12/site-packages/python_socks/_connectors/socks5_async.py", line 79, in connect
    | reply: socks5.ConnectReply = conn.receive(data)
    | ^^^^^^^^^^^^^^^^^^
    | File "/opt/telegram_client/.venv/lib/python3.12/site-packages/python_socks/_protocols/socks5.py", line 347, in receive
    | reply = ConnectReply.loads(data)
    | ^^^^^^^^^^^^^^^^^^^^^^^^
    | File "/opt/telegram_client/.venv/lib/python3.12/site-packages/python_socks/_protocols/socks5.py", line 209, in loads
    | raise ReplyError(msg, error_code=reply)
    | python_socks._protocols.errors.ReplyError: Host unreachable
    |
    | During handling of the above exception, another exception occurred:
    |
    | Traceback (most recent call last):
    | File "/opt/telegram_client/src/scripts/load_new_accounts.py", line 103, in prepare_account
    | await client.connect()
    | File "/opt/telegram_client/.venv/lib/python3.12/site-packages/telethon/client/telegrambaseclient.py", line 547, in connect
    | if not await self._sender.connect(self._connection(
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | File "/opt/telegram_client/.venv/lib/python3.12/site-packages/telethon/network/mtprotosender.py", line 134, in connect
    | await self._connect()
    | File "/opt/telegram_client/.venv/lib/python3.12/site-packages/telethon/network/mtprotosender.py", line 234, in _connect
    | connected = await self._try_connect(attempt)
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | File "/opt/telegram_client/.venv/lib/python3.12/site-packages/telethon/network/mtprotosender.py", line 284, in _try_connect
    | await self._connection.connect(timeout=self._connect_timeout)
    | File "/opt/telegram_client/.venv/lib/python3.12/site-packages/telethon/network/connection/connection.py", line 244, in connect
    | await self._connect(timeout=timeout, ssl=ssl)
    | File "/opt/telegram_client/.venv/lib/python3.12/site-packages/telethon/network/connection/connection.py", line 225, in _connect
    | sock = await self._proxy_connect(
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^
    | File "/opt/telegram_client/.venv/lib/python3.12/site-packages/telethon/network/connection/connection.py", line 135, in proxy_connect
    | sock = await proxy.connect(
    | ^^^^^^^^^^^^^^^^^^^^
    | File "/opt/telegram_client/.venv/lib/python3.12/site-packages/python_socks/async
    /asyncio/_proxy.py", line 59, in connect
    | return await self.connect(
    | ^^^^^^^^^^^^^^^^^^^^
    | File "/opt/telegram_client/.venv/lib/python3.12/site-packages/python_socks/async
    /asyncio/_proxy.py", line 108, in _connect
    | raise ProxyError(e, error_code=e.error_code)
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | TypeError: ConnectionError() takes no keyword arguments
    +------------------------------------

Telethon version

1.36.0

Python version

3.12

Operating system (including distribution name and version)

Ubuntu 22.04

Other details

No response

Checklist

  • The error is in the library's code, and not in my own.
  • I have searched for this issue before posting it and there isn't an open duplicate.
  • I ran pip install -U https://github.com/LonamiWebs/Telethon/archive/v1.zip and triggered the bug in the latest version.

Seems you've investigated the issue a fair bit. Feel free to submit a pull request, as I don't personally use proxies.

If I understand correctly, we need to handle ProxyError, ProxyConnectionError, ProxyTimeoutError errors in places where we already handle IOError in connection.py like in recv and send loops like this

async def _recv_loop(self):
"""
This loop is constantly putting items on the queue as they're read.
"""
try:
while self._connected:
try:
data = await self._recv()
except asyncio.CancelledError:
break
except (IOError, asyncio.IncompleteReadError) as e:
self._log.warning('Server closed the connection: %s', e)
await self._recv_queue.put((None, e))
await self.disconnect()
except InvalidChecksumError as e:
self._log.warning('Server response had invalid checksum: %s', e)
await self._recv_queue.put((None, e))
except InvalidBufferError as e:
self._log.warning('Server response had invalid buffer: %s', e)
await self._recv_queue.put((None, e))
except Exception as e:
self._log.exception('Unexpected exception in the receive loop')
await self._recv_queue.put((None, e))
await self.disconnect()
else:
await self._recv_queue.put((data, None))
finally:
await self.disconnect()

and in mtprotosender.py in _try_connect, _connect, _reconnect, _send_loop and _recv_loop.

But I don't understand, why IOError handles when it seems to be already handled, example:
in mtprotosender.py _reconnect:

for attempt in retry_range(retries, force_retry=False):
try:
await self._connect()
except (IOError, asyncio.TimeoutError) as e:
last_error = e
self._log.info('Failed reconnection attempt %d with %s',
attempt, e.__class__.__name__)
await asyncio.sleep(self._delay)

in mtprotosender.py _connect:
for attempt in retry_range(self._retries):
if not connected:
connected = await self._try_connect(attempt)
if not connected:
continue # skip auth key generation until we're connected
if not self.auth_key:
try:
if not await self._try_gen_auth_key(attempt):
continue # keep retrying until we have the auth key
except (IOError, asyncio.TimeoutError) as e:
# Sometimes, specially during user-DC migrations,
# Telegram may close the connection during auth_key
# generation. If that's the case, we will need to
# connect again.
self._log.warning('Connection error %d during auth_key gen: %s: %s',
attempt, type(e).__name__, e)
# Whatever the IOError was, make sure to disconnect so we can
# reconnect cleanly after.
await self._connection.disconnect()
connected = False
await asyncio.sleep(self._delay)
continue # next iteration we will try to reconnect

and then in mtprotosender.py _try_connect:
async def _try_connect(self, attempt):
try:
self._log.debug('Connection attempt %d...', attempt)
await self._connection.connect(timeout=self._connect_timeout)
self._log.debug('Connection success!')
return True
except (IOError, asyncio.TimeoutError) as e:
self._log.warning('Attempt %d at connecting failed: %s: %s',
attempt, type(e).__name__, e)
await asyncio.sleep(self._delay)
return False

Looks like IOError already handled in _try_connect method and handling in _reconnect seems useless?

Maybe I am missing something? Will handling proxy errors in this places be enough?

Too complicated. I'd make a subclass and use that:

class ConnectionErrorExtra(ConnectionError):
    def __init__(self, *args, **kwargs):
        super().__init__()

and then we can use this instead of the default ConnectionError.

Please look for my fix (#4440 ). Is it okay to define Error class right in function? As for error_code argument I make definition like in original python_socks repository.

https://github.com/romis2012/python-socks/blob/98462c52a5ea174c055c4393ffe64f1ac81f5842/python_socks/_errors.py#L1-L4

Is it okay to define Error class right in function?

That's fine. It means people won't import it (or shouldn't), but it's okay because it inherits ConnectionError.