efro.error

Common errors and related functionality.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Common errors and related functionality."""
  4from __future__ import annotations
  5
  6from typing import TYPE_CHECKING, override
  7import errno
  8
  9if TYPE_CHECKING:
 10    from typing import Any
 11
 12    from efro.terminal import ClrBase
 13
 14
 15class CleanError(Exception):
 16    """An error that can be presented to the user as a simple message.
 17
 18    These errors should be completely self-explanatory, to the point where
 19    a traceback or other context would not be useful.
 20
 21    A CleanError with no message can be used to inform a script to fail
 22    without printing any message.
 23
 24    This should generally be limited to errors that will *always* be
 25    presented to the user (such as those in high level tool code).
 26    Exceptions that may be caught and handled by other code should use
 27    more descriptive exception types.
 28    """
 29
 30    def pretty_print(
 31        self,
 32        flush: bool = True,
 33        prefix: str = 'Error',
 34        file: Any = None,
 35        clr: type[ClrBase] | None = None,
 36    ) -> None:
 37        """Print the error to stdout, using red colored output if available.
 38
 39        If the error has an empty message, prints nothing (not even a newline).
 40        """
 41        from efro.terminal import Clr
 42
 43        if clr is None:
 44            clr = Clr
 45
 46        if prefix:
 47            prefix = f'{prefix}: '
 48        errstr = str(self)
 49        if errstr:
 50            print(
 51                f'{clr.SRED}{prefix}{errstr}{clr.RST}', flush=flush, file=file
 52            )
 53
 54
 55class CommunicationError(Exception):
 56    """A communication related error has occurred.
 57
 58    This covers anything network-related going wrong in the sending
 59    of data or receiving of a response. Basically anything that is out
 60    of our control should get lumped in here. This error does not imply
 61    that data was not received on the other end; only that a full
 62    acknowledgement round trip was not completed.
 63
 64    These errors should be gracefully handled whenever possible, as
 65    occasional network issues are unavoidable.
 66    """
 67
 68
 69class RemoteError(Exception):
 70    """An error occurred on the other end of some connection.
 71
 72    This occurs when communication succeeds but another type of error
 73    occurs remotely. The error string can consist of a remote stack
 74    trace or a simple message depending on the context.
 75
 76    Communication systems should aim to communicate specific errors
 77    gracefully as standard message responses when specific details are
 78    needed; this is intended more as a catch-all.
 79    """
 80
 81    def __init__(self, msg: str, peer_desc: str):
 82        super().__init__(msg)
 83        self._peer_desc = peer_desc
 84
 85    @override
 86    def __str__(self) -> str:
 87        s = ''.join(str(arg) for arg in self.args)
 88        # Indent so we can more easily tell what is the remote part when
 89        # this is in the middle of a long exception chain.
 90        padding = '  '
 91        s = ''.join(padding + line for line in s.splitlines(keepends=True))
 92        return f'The following occurred on {self._peer_desc}:\n{s}'
 93
 94
 95class IntegrityError(ValueError):
 96    """Data has been tampered with or corrupted in some form."""
 97
 98
 99class AuthenticationError(Exception):
100    """Authentication has failed for some operation.
101
102    This can be raised if server-side-verification does not match
103    client-supplied credentials, if an invalid password is supplied
104    for a sign-in attempt, etc.
105    """
106
107
108def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool:
109    """Is the provided exception from urllib a communication-related error?
110
111    Url, if provided, can provide extra context for when to treat an error
112    as such an error.
113
114    This should be passed an exception which resulted from opening or
115    reading a urllib Request. It returns True for any errors that could
116    conceivably arise due to unavailable/poor network connections,
117    firewall/connectivity issues, or other issues out of our control.
118    These errors can often be safely ignored or presented to the user
119    as general 'network-unavailable' states.
120    """
121    import urllib.error
122    import http.client
123    import socket
124
125    if isinstance(
126        exc,
127        (
128            urllib.error.URLError,
129            ConnectionError,
130            http.client.IncompleteRead,
131            http.client.BadStatusLine,
132            http.client.RemoteDisconnected,
133            socket.timeout,
134        ),
135    ):
136        # Special case: although an HTTPError is a subclass of URLError,
137        # we don't consider it a communication error. It generally means we
138        # have successfully communicated with the server but what we are asking
139        # for is not there/etc.
140        if isinstance(exc, urllib.error.HTTPError):
141            # Special sub-case: appspot.com hosting seems to give 403 errors
142            # (forbidden) to some countries. I'm assuming for legal reasons?..
143            # Let's consider that a communication error since its out of our
144            # control so we don't fill up logs with it.
145            if exc.code == 403 and url is not None and '.appspot.com' in url:
146                return True
147
148            return False
149
150        return True
151
152    if isinstance(exc, OSError):
153        if exc.errno == 10051:  # Windows unreachable network error.
154            return True
155        if exc.errno in {
156            errno.ETIMEDOUT,
157            errno.EHOSTUNREACH,
158            errno.ENETUNREACH,
159        }:
160            return True
161    return False
162
163
164def is_requests_communication_error(exc: BaseException) -> bool:
165    """Is the provided exception a communication-related error from requests?"""
166    import requests
167
168    # Looks like this maps pretty well onto requests' ConnectionError
169    return isinstance(exc, requests.ConnectionError)
170
171
172def is_udp_communication_error(exc: BaseException) -> bool:
173    """Should this udp-related exception be considered a communication error?
174
175    This should be passed an exception which resulted from creating and
176    using a socket.SOCK_DGRAM type socket. It should return True for any
177    errors that could conceivably arise due to unavailable/poor network
178    conditions, firewall/connectivity issues, etc. These issues can often
179    be safely ignored or presented to the user as general
180    'network-unavailable' states.
181    """
182    if isinstance(exc, ConnectionRefusedError | TimeoutError):
183        return True
184    if isinstance(exc, OSError):
185        if exc.errno == 10051:  # Windows unreachable network error.
186            return True
187        if exc.errno in {
188            errno.EADDRNOTAVAIL,
189            errno.ETIMEDOUT,
190            errno.EHOSTUNREACH,
191            errno.ENETUNREACH,
192            errno.EINVAL,
193            errno.EPERM,
194            errno.EACCES,
195            # Windows 'invalid argument' error.
196            10022,
197            # Windows 'a socket operation was attempted to'
198            #         'an unreachable network' error.
199            10051,
200        }:
201            return True
202    return False
203
204
205def is_asyncio_streams_communication_error(exc: BaseException) -> bool:
206    """Should this streams error be considered a communication error?
207
208    This should be passed an exception which resulted from creating and
209    using asyncio streams. It should return True for any errors that could
210    conceivably arise due to unavailable/poor network connections,
211    firewall/connectivity issues, etc. These issues can often be safely
212    ignored or presented to the user as general 'connection-lost' events.
213    """
214    # pylint: disable=too-many-return-statements
215    import ssl
216
217    if isinstance(
218        exc,
219        (
220            ConnectionError,
221            TimeoutError,
222            EOFError,
223        ),
224    ):
225        return True
226
227    # Also some specific errno ones.
228    if isinstance(exc, OSError):
229        if exc.errno == 10051:  # Windows unreachable network error.
230            return True
231        if exc.errno in {
232            errno.ETIMEDOUT,
233            errno.EHOSTUNREACH,
234            errno.ENETUNREACH,
235        }:
236            return True
237
238    # Am occasionally getting a specific SSL error on shutdown which I
239    # believe is harmless (APPLICATION_DATA_AFTER_CLOSE_NOTIFY).
240    # It sounds like it may soon be ignored by Python (as of March 2022).
241    # Let's still complain, however, if we get any SSL errors besides
242    # this one. https://bugs.python.org/issue39951
243    if isinstance(exc, ssl.SSLError):
244        excstr = str(exc)
245        if 'APPLICATION_DATA_AFTER_CLOSE_NOTIFY' in excstr:
246            return True
247
248        # Also occasionally am getting WRONG_VERSION_NUMBER ssl errors;
249        # Assuming this just means client is attempting to connect from some
250        # outdated browser or whatnot.
251        if 'SSL: WRONG_VERSION_NUMBER' in excstr:
252            return True
253
254        # And seeing this very rarely; assuming its just data corruption?
255        if 'SSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC' in excstr:
256            return True
257
258    return False
class CleanError(builtins.Exception):
16class CleanError(Exception):
17    """An error that can be presented to the user as a simple message.
18
19    These errors should be completely self-explanatory, to the point where
20    a traceback or other context would not be useful.
21
22    A CleanError with no message can be used to inform a script to fail
23    without printing any message.
24
25    This should generally be limited to errors that will *always* be
26    presented to the user (such as those in high level tool code).
27    Exceptions that may be caught and handled by other code should use
28    more descriptive exception types.
29    """
30
31    def pretty_print(
32        self,
33        flush: bool = True,
34        prefix: str = 'Error',
35        file: Any = None,
36        clr: type[ClrBase] | None = None,
37    ) -> None:
38        """Print the error to stdout, using red colored output if available.
39
40        If the error has an empty message, prints nothing (not even a newline).
41        """
42        from efro.terminal import Clr
43
44        if clr is None:
45            clr = Clr
46
47        if prefix:
48            prefix = f'{prefix}: '
49        errstr = str(self)
50        if errstr:
51            print(
52                f'{clr.SRED}{prefix}{errstr}{clr.RST}', flush=flush, file=file
53            )

An error that can be presented to the user as a simple message.

These errors should be completely self-explanatory, to the point where a traceback or other context would not be useful.

A CleanError with no message can be used to inform a script to fail without printing any message.

This should generally be limited to errors that will always be presented to the user (such as those in high level tool code). Exceptions that may be caught and handled by other code should use more descriptive exception types.

def pretty_print( self, flush: bool = True, prefix: str = 'Error', file: Any = None, clr: type[efro.terminal.ClrBase] | None = None) -> None:
31    def pretty_print(
32        self,
33        flush: bool = True,
34        prefix: str = 'Error',
35        file: Any = None,
36        clr: type[ClrBase] | None = None,
37    ) -> None:
38        """Print the error to stdout, using red colored output if available.
39
40        If the error has an empty message, prints nothing (not even a newline).
41        """
42        from efro.terminal import Clr
43
44        if clr is None:
45            clr = Clr
46
47        if prefix:
48            prefix = f'{prefix}: '
49        errstr = str(self)
50        if errstr:
51            print(
52                f'{clr.SRED}{prefix}{errstr}{clr.RST}', flush=flush, file=file
53            )

Print the error to stdout, using red colored output if available.

If the error has an empty message, prints nothing (not even a newline).

class CommunicationError(builtins.Exception):
56class CommunicationError(Exception):
57    """A communication related error has occurred.
58
59    This covers anything network-related going wrong in the sending
60    of data or receiving of a response. Basically anything that is out
61    of our control should get lumped in here. This error does not imply
62    that data was not received on the other end; only that a full
63    acknowledgement round trip was not completed.
64
65    These errors should be gracefully handled whenever possible, as
66    occasional network issues are unavoidable.
67    """

A communication related error has occurred.

This covers anything network-related going wrong in the sending of data or receiving of a response. Basically anything that is out of our control should get lumped in here. This error does not imply that data was not received on the other end; only that a full acknowledgement round trip was not completed.

These errors should be gracefully handled whenever possible, as occasional network issues are unavoidable.

class RemoteError(builtins.Exception):
70class RemoteError(Exception):
71    """An error occurred on the other end of some connection.
72
73    This occurs when communication succeeds but another type of error
74    occurs remotely. The error string can consist of a remote stack
75    trace or a simple message depending on the context.
76
77    Communication systems should aim to communicate specific errors
78    gracefully as standard message responses when specific details are
79    needed; this is intended more as a catch-all.
80    """
81
82    def __init__(self, msg: str, peer_desc: str):
83        super().__init__(msg)
84        self._peer_desc = peer_desc
85
86    @override
87    def __str__(self) -> str:
88        s = ''.join(str(arg) for arg in self.args)
89        # Indent so we can more easily tell what is the remote part when
90        # this is in the middle of a long exception chain.
91        padding = '  '
92        s = ''.join(padding + line for line in s.splitlines(keepends=True))
93        return f'The following occurred on {self._peer_desc}:\n{s}'

An error occurred on the other end of some connection.

This occurs when communication succeeds but another type of error occurs remotely. The error string can consist of a remote stack trace or a simple message depending on the context.

Communication systems should aim to communicate specific errors gracefully as standard message responses when specific details are needed; this is intended more as a catch-all.

RemoteError(msg: str, peer_desc: str)
82    def __init__(self, msg: str, peer_desc: str):
83        super().__init__(msg)
84        self._peer_desc = peer_desc
class IntegrityError(builtins.ValueError):
96class IntegrityError(ValueError):
97    """Data has been tampered with or corrupted in some form."""

Data has been tampered with or corrupted in some form.

class AuthenticationError(builtins.Exception):
100class AuthenticationError(Exception):
101    """Authentication has failed for some operation.
102
103    This can be raised if server-side-verification does not match
104    client-supplied credentials, if an invalid password is supplied
105    for a sign-in attempt, etc.
106    """

Authentication has failed for some operation.

This can be raised if server-side-verification does not match client-supplied credentials, if an invalid password is supplied for a sign-in attempt, etc.

def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool:
109def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool:
110    """Is the provided exception from urllib a communication-related error?
111
112    Url, if provided, can provide extra context for when to treat an error
113    as such an error.
114
115    This should be passed an exception which resulted from opening or
116    reading a urllib Request. It returns True for any errors that could
117    conceivably arise due to unavailable/poor network connections,
118    firewall/connectivity issues, or other issues out of our control.
119    These errors can often be safely ignored or presented to the user
120    as general 'network-unavailable' states.
121    """
122    import urllib.error
123    import http.client
124    import socket
125
126    if isinstance(
127        exc,
128        (
129            urllib.error.URLError,
130            ConnectionError,
131            http.client.IncompleteRead,
132            http.client.BadStatusLine,
133            http.client.RemoteDisconnected,
134            socket.timeout,
135        ),
136    ):
137        # Special case: although an HTTPError is a subclass of URLError,
138        # we don't consider it a communication error. It generally means we
139        # have successfully communicated with the server but what we are asking
140        # for is not there/etc.
141        if isinstance(exc, urllib.error.HTTPError):
142            # Special sub-case: appspot.com hosting seems to give 403 errors
143            # (forbidden) to some countries. I'm assuming for legal reasons?..
144            # Let's consider that a communication error since its out of our
145            # control so we don't fill up logs with it.
146            if exc.code == 403 and url is not None and '.appspot.com' in url:
147                return True
148
149            return False
150
151        return True
152
153    if isinstance(exc, OSError):
154        if exc.errno == 10051:  # Windows unreachable network error.
155            return True
156        if exc.errno in {
157            errno.ETIMEDOUT,
158            errno.EHOSTUNREACH,
159            errno.ENETUNREACH,
160        }:
161            return True
162    return False

Is the provided exception from urllib a communication-related error?

Url, if provided, can provide extra context for when to treat an error as such an error.

This should be passed an exception which resulted from opening or reading a urllib Request. It returns True for any errors that could conceivably arise due to unavailable/poor network connections, firewall/connectivity issues, or other issues out of our control. These errors can often be safely ignored or presented to the user as general 'network-unavailable' states.

def is_requests_communication_error(exc: BaseException) -> bool:
165def is_requests_communication_error(exc: BaseException) -> bool:
166    """Is the provided exception a communication-related error from requests?"""
167    import requests
168
169    # Looks like this maps pretty well onto requests' ConnectionError
170    return isinstance(exc, requests.ConnectionError)

Is the provided exception a communication-related error from requests?

def is_udp_communication_error(exc: BaseException) -> bool:
173def is_udp_communication_error(exc: BaseException) -> bool:
174    """Should this udp-related exception be considered a communication error?
175
176    This should be passed an exception which resulted from creating and
177    using a socket.SOCK_DGRAM type socket. It should return True for any
178    errors that could conceivably arise due to unavailable/poor network
179    conditions, firewall/connectivity issues, etc. These issues can often
180    be safely ignored or presented to the user as general
181    'network-unavailable' states.
182    """
183    if isinstance(exc, ConnectionRefusedError | TimeoutError):
184        return True
185    if isinstance(exc, OSError):
186        if exc.errno == 10051:  # Windows unreachable network error.
187            return True
188        if exc.errno in {
189            errno.EADDRNOTAVAIL,
190            errno.ETIMEDOUT,
191            errno.EHOSTUNREACH,
192            errno.ENETUNREACH,
193            errno.EINVAL,
194            errno.EPERM,
195            errno.EACCES,
196            # Windows 'invalid argument' error.
197            10022,
198            # Windows 'a socket operation was attempted to'
199            #         'an unreachable network' error.
200            10051,
201        }:
202            return True
203    return False

Should this udp-related exception be considered a communication error?

This should be passed an exception which resulted from creating and using a socket.SOCK_DGRAM type socket. It should return True for any errors that could conceivably arise due to unavailable/poor network conditions, firewall/connectivity issues, etc. These issues can often be safely ignored or presented to the user as general 'network-unavailable' states.

def is_asyncio_streams_communication_error(exc: BaseException) -> bool:
206def is_asyncio_streams_communication_error(exc: BaseException) -> bool:
207    """Should this streams error be considered a communication error?
208
209    This should be passed an exception which resulted from creating and
210    using asyncio streams. It should return True for any errors that could
211    conceivably arise due to unavailable/poor network connections,
212    firewall/connectivity issues, etc. These issues can often be safely
213    ignored or presented to the user as general 'connection-lost' events.
214    """
215    # pylint: disable=too-many-return-statements
216    import ssl
217
218    if isinstance(
219        exc,
220        (
221            ConnectionError,
222            TimeoutError,
223            EOFError,
224        ),
225    ):
226        return True
227
228    # Also some specific errno ones.
229    if isinstance(exc, OSError):
230        if exc.errno == 10051:  # Windows unreachable network error.
231            return True
232        if exc.errno in {
233            errno.ETIMEDOUT,
234            errno.EHOSTUNREACH,
235            errno.ENETUNREACH,
236        }:
237            return True
238
239    # Am occasionally getting a specific SSL error on shutdown which I
240    # believe is harmless (APPLICATION_DATA_AFTER_CLOSE_NOTIFY).
241    # It sounds like it may soon be ignored by Python (as of March 2022).
242    # Let's still complain, however, if we get any SSL errors besides
243    # this one. https://bugs.python.org/issue39951
244    if isinstance(exc, ssl.SSLError):
245        excstr = str(exc)
246        if 'APPLICATION_DATA_AFTER_CLOSE_NOTIFY' in excstr:
247            return True
248
249        # Also occasionally am getting WRONG_VERSION_NUMBER ssl errors;
250        # Assuming this just means client is attempting to connect from some
251        # outdated browser or whatnot.
252        if 'SSL: WRONG_VERSION_NUMBER' in excstr:
253            return True
254
255        # And seeing this very rarely; assuming its just data corruption?
256        if 'SSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC' in excstr:
257            return True
258
259    return False

Should this streams error be considered a communication error?

This should be passed an exception which resulted from creating and using asyncio streams. It should return True for any errors that could conceivably arise due to unavailable/poor network connections, firewall/connectivity issues, etc. These issues can often be safely ignored or presented to the user as general 'connection-lost' events.