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    import urllib3.response
 13    from efro.terminal import ClrBase
 14
 15
 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            )
 54
 55
 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    """
 68
 69
 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}'
 94
 95
 96class IntegrityError(ValueError):
 97    """Data has been tampered with or corrupted in some form."""
 98
 99
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    """
107
108
109class _Urllib3HttpError(Exception):
110    """Exception raised for non-200 html codes."""
111
112    def __init__(self, code: int) -> None:
113        self.code = code
114
115    # So we can see code in tracebacks.
116    @override
117    def __str__(self) -> str:
118        from http import HTTPStatus
119
120        try:
121            desc = HTTPStatus(self.code).description
122        except ValueError:
123            desc = 'Unknown HTTP Status Code'
124        return f'{self.code}: {desc}'
125
126
127def raise_for_urllib3_status(
128    response: urllib3.response.BaseHTTPResponse,
129) -> None:
130    """Raise an exception for html error codes aside from 200."""
131    if response.status != 200:
132        raise _Urllib3HttpError(code=response.status)
133
134
135def is_urllib3_communication_error(exc: BaseException, url: str | None) -> bool:
136    """Is the provided exception from urllib3 a communication-related error?
137
138    Url, if provided, can provide extra context for when to treat an error
139    as such an error.
140
141    This should be passed an exception which resulted from making
142    requests with urllib3. It returns True for any errors that could
143    conceivably arise due to unavailable/poor network connections,
144    firewall/connectivity issues, or other issues out of our control.
145    These errors can often be safely ignored or presented to the user as
146    general 'network-unavailable' states.
147    """
148    # Need to start building these up. For now treat everything as a
149    # real error.
150    import urllib3.exceptions
151
152    # If this error is from hitting max-retries, look at the underlying
153    # error instead.
154    if isinstance(exc, urllib3.exceptions.MaxRetryError):
155        # Hmm; will a max-retry error ever not have an underlying error?
156        if exc.reason is None:
157            return False
158        exc = exc.reason
159
160    if isinstance(exc, _Urllib3HttpError):
161        # Special sub-case: appspot.com hosting seems to give 403 errors
162        # (forbidden) to some countries. I'm assuming for legal reasons?..
163        # Let's consider that a communication error since its out of our
164        # control so we don't fill up logs with it.
165        if exc.code == 403 and url is not None and '.appspot.com' in url:
166            return True
167
168    elif isinstance(exc, urllib3.exceptions.ReadTimeoutError):
169        return True
170
171    elif isinstance(exc, urllib3.exceptions.ProtocolError):
172        # Most protocol errors quality as CommunicationErrors, but some
173        # may be due to server misconfigurations or whatnot so let's
174        # take it on a case by case basis.
175        excstr = str(exc)
176        if 'Connection aborted.' in excstr:
177            return True
178
179    return False
180
181
182def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool:
183    """Is the provided exception from urllib a communication-related error?
184
185    Url, if provided, can provide extra context for when to treat an error
186    as such an error.
187
188    This should be passed an exception which resulted from opening or
189    reading a urllib Request. It returns True for any errors that could
190    conceivably arise due to unavailable/poor network connections,
191    firewall/connectivity issues, or other issues out of our control.
192    These errors can often be safely ignored or presented to the user
193    as general 'network-unavailable' states.
194    """
195    import urllib.error
196    import http.client
197    import socket
198
199    if isinstance(
200        exc,
201        (
202            urllib.error.URLError,
203            ConnectionError,
204            http.client.IncompleteRead,
205            http.client.BadStatusLine,
206            http.client.RemoteDisconnected,
207            socket.timeout,
208        ),
209    ):
210        # Special case: although an HTTPError is a subclass of URLError,
211        # we don't consider it a communication error. It generally means we
212        # have successfully communicated with the server but what we are asking
213        # for is not there/etc.
214        if isinstance(exc, urllib.error.HTTPError):
215            # Special sub-case: appspot.com hosting seems to give 403 errors
216            # (forbidden) to some countries. I'm assuming for legal reasons?..
217            # Let's consider that a communication error since its out of our
218            # control so we don't fill up logs with it.
219            if exc.code == 403 and url is not None and '.appspot.com' in url:
220                return True
221
222            return False
223
224        return True
225
226    if isinstance(exc, OSError):
227        if exc.errno == 10051:  # Windows unreachable network error.
228            return True
229        if exc.errno in {
230            errno.ETIMEDOUT,
231            errno.EHOSTUNREACH,
232            errno.ENETUNREACH,
233        }:
234            return True
235    return False
236
237
238def is_requests_communication_error(exc: BaseException) -> bool:
239    """Is the provided exception a communication-related error from requests?"""
240    import requests
241
242    # Looks like this maps pretty well onto requests' ConnectionError
243    return isinstance(exc, requests.ConnectionError)
244
245
246def is_udp_communication_error(exc: BaseException) -> bool:
247    """Should this udp-related exception be considered a communication error?
248
249    This should be passed an exception which resulted from creating and
250    using a socket.SOCK_DGRAM type socket. It should return True for any
251    errors that could conceivably arise due to unavailable/poor network
252    conditions, firewall/connectivity issues, etc. These issues can often
253    be safely ignored or presented to the user as general
254    'network-unavailable' states.
255    """
256    if isinstance(exc, ConnectionRefusedError | TimeoutError):
257        return True
258    if isinstance(exc, OSError):
259        if exc.errno == 10051:  # Windows unreachable network error.
260            return True
261        if exc.errno in {
262            errno.EADDRNOTAVAIL,
263            errno.ETIMEDOUT,
264            errno.EHOSTUNREACH,
265            errno.ENETUNREACH,
266            errno.EINVAL,
267            errno.EPERM,
268            errno.EACCES,
269            # Windows 'invalid argument' error.
270            10022,
271            # Windows 'a socket operation was attempted to'
272            #         'an unreachable network' error.
273            10051,
274        }:
275            return True
276    return False
277
278
279def is_asyncio_streams_communication_error(exc: BaseException) -> bool:
280    """Should this streams error be considered a communication error?
281
282    This should be passed an exception which resulted from creating and
283    using asyncio streams. It should return True for any errors that could
284    conceivably arise due to unavailable/poor network connections,
285    firewall/connectivity issues, etc. These issues can often be safely
286    ignored or presented to the user as general 'connection-lost' events.
287    """
288    # pylint: disable=too-many-return-statements
289    import ssl
290
291    if isinstance(
292        exc,
293        (
294            ConnectionError,
295            TimeoutError,
296            EOFError,
297        ),
298    ):
299        return True
300
301    # Also some specific errno ones.
302    if isinstance(exc, OSError):
303        if exc.errno == 10051:  # Windows unreachable network error.
304            return True
305        if exc.errno in {
306            errno.ETIMEDOUT,
307            errno.EHOSTUNREACH,
308            errno.ENETUNREACH,
309        }:
310            return True
311
312    # Am occasionally getting a specific SSL error on shutdown which I
313    # believe is harmless (APPLICATION_DATA_AFTER_CLOSE_NOTIFY).
314    # It sounds like it may soon be ignored by Python (as of March 2022).
315    # Let's still complain, however, if we get any SSL errors besides
316    # this one. https://bugs.python.org/issue39951
317    if isinstance(exc, ssl.SSLError):
318        excstr = str(exc)
319        if 'APPLICATION_DATA_AFTER_CLOSE_NOTIFY' in excstr:
320            return True
321
322        # Also occasionally am getting WRONG_VERSION_NUMBER ssl errors;
323        # Assuming this just means client is attempting to connect from some
324        # outdated browser or whatnot.
325        if 'SSL: WRONG_VERSION_NUMBER' in excstr:
326            return True
327
328        # Also getting this sometimes which sounds like corrupt SSL data
329        # or something.
330        if 'SSL: BAD_RECORD_TYPE' in excstr:
331            return True
332
333        # And seeing this very rarely; assuming its just data corruption?
334        if 'SSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC' in excstr:
335            return True
336
337    return False
class CleanError(builtins.Exception):
17class CleanError(Exception):
18    """An error that can be presented to the user as a simple message.
19
20    These errors should be completely self-explanatory, to the point where
21    a traceback or other context would not be useful.
22
23    A CleanError with no message can be used to inform a script to fail
24    without printing any message.
25
26    This should generally be limited to errors that will *always* be
27    presented to the user (such as those in high level tool code).
28    Exceptions that may be caught and handled by other code should use
29    more descriptive exception types.
30    """
31
32    def pretty_print(
33        self,
34        flush: bool = True,
35        prefix: str = 'Error',
36        file: Any = None,
37        clr: type[ClrBase] | None = None,
38    ) -> None:
39        """Print the error to stdout, using red colored output if available.
40
41        If the error has an empty message, prints nothing (not even a newline).
42        """
43        from efro.terminal import Clr
44
45        if clr is None:
46            clr = Clr
47
48        if prefix:
49            prefix = f'{prefix}: '
50        errstr = str(self)
51        if errstr:
52            print(
53                f'{clr.SRED}{prefix}{errstr}{clr.RST}', flush=flush, file=file
54            )

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

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

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

Data has been tampered with or corrupted in some form.

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

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 raise_for_urllib3_status(response: urllib3.response.BaseHTTPResponse) -> None:
128def raise_for_urllib3_status(
129    response: urllib3.response.BaseHTTPResponse,
130) -> None:
131    """Raise an exception for html error codes aside from 200."""
132    if response.status != 200:
133        raise _Urllib3HttpError(code=response.status)

Raise an exception for html error codes aside from 200.

def is_urllib3_communication_error(exc: BaseException, url: str | None) -> bool:
136def is_urllib3_communication_error(exc: BaseException, url: str | None) -> bool:
137    """Is the provided exception from urllib3 a communication-related error?
138
139    Url, if provided, can provide extra context for when to treat an error
140    as such an error.
141
142    This should be passed an exception which resulted from making
143    requests with urllib3. It returns True for any errors that could
144    conceivably arise due to unavailable/poor network connections,
145    firewall/connectivity issues, or other issues out of our control.
146    These errors can often be safely ignored or presented to the user as
147    general 'network-unavailable' states.
148    """
149    # Need to start building these up. For now treat everything as a
150    # real error.
151    import urllib3.exceptions
152
153    # If this error is from hitting max-retries, look at the underlying
154    # error instead.
155    if isinstance(exc, urllib3.exceptions.MaxRetryError):
156        # Hmm; will a max-retry error ever not have an underlying error?
157        if exc.reason is None:
158            return False
159        exc = exc.reason
160
161    if isinstance(exc, _Urllib3HttpError):
162        # Special sub-case: appspot.com hosting seems to give 403 errors
163        # (forbidden) to some countries. I'm assuming for legal reasons?..
164        # Let's consider that a communication error since its out of our
165        # control so we don't fill up logs with it.
166        if exc.code == 403 and url is not None and '.appspot.com' in url:
167            return True
168
169    elif isinstance(exc, urllib3.exceptions.ReadTimeoutError):
170        return True
171
172    elif isinstance(exc, urllib3.exceptions.ProtocolError):
173        # Most protocol errors quality as CommunicationErrors, but some
174        # may be due to server misconfigurations or whatnot so let's
175        # take it on a case by case basis.
176        excstr = str(exc)
177        if 'Connection aborted.' in excstr:
178            return True
179
180    return False

Is the provided exception from urllib3 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 making requests with urllib3. 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_urllib_communication_error(exc: BaseException, url: str | None) -> bool:
183def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool:
184    """Is the provided exception from urllib a communication-related error?
185
186    Url, if provided, can provide extra context for when to treat an error
187    as such an error.
188
189    This should be passed an exception which resulted from opening or
190    reading a urllib Request. It returns True for any errors that could
191    conceivably arise due to unavailable/poor network connections,
192    firewall/connectivity issues, or other issues out of our control.
193    These errors can often be safely ignored or presented to the user
194    as general 'network-unavailable' states.
195    """
196    import urllib.error
197    import http.client
198    import socket
199
200    if isinstance(
201        exc,
202        (
203            urllib.error.URLError,
204            ConnectionError,
205            http.client.IncompleteRead,
206            http.client.BadStatusLine,
207            http.client.RemoteDisconnected,
208            socket.timeout,
209        ),
210    ):
211        # Special case: although an HTTPError is a subclass of URLError,
212        # we don't consider it a communication error. It generally means we
213        # have successfully communicated with the server but what we are asking
214        # for is not there/etc.
215        if isinstance(exc, urllib.error.HTTPError):
216            # Special sub-case: appspot.com hosting seems to give 403 errors
217            # (forbidden) to some countries. I'm assuming for legal reasons?..
218            # Let's consider that a communication error since its out of our
219            # control so we don't fill up logs with it.
220            if exc.code == 403 and url is not None and '.appspot.com' in url:
221                return True
222
223            return False
224
225        return True
226
227    if isinstance(exc, OSError):
228        if exc.errno == 10051:  # Windows unreachable network error.
229            return True
230        if exc.errno in {
231            errno.ETIMEDOUT,
232            errno.EHOSTUNREACH,
233            errno.ENETUNREACH,
234        }:
235            return True
236    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:
239def is_requests_communication_error(exc: BaseException) -> bool:
240    """Is the provided exception a communication-related error from requests?"""
241    import requests
242
243    # Looks like this maps pretty well onto requests' ConnectionError
244    return isinstance(exc, requests.ConnectionError)

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

def is_udp_communication_error(exc: BaseException) -> bool:
247def is_udp_communication_error(exc: BaseException) -> bool:
248    """Should this udp-related exception be considered a communication error?
249
250    This should be passed an exception which resulted from creating and
251    using a socket.SOCK_DGRAM type socket. It should return True for any
252    errors that could conceivably arise due to unavailable/poor network
253    conditions, firewall/connectivity issues, etc. These issues can often
254    be safely ignored or presented to the user as general
255    'network-unavailable' states.
256    """
257    if isinstance(exc, ConnectionRefusedError | TimeoutError):
258        return True
259    if isinstance(exc, OSError):
260        if exc.errno == 10051:  # Windows unreachable network error.
261            return True
262        if exc.errno in {
263            errno.EADDRNOTAVAIL,
264            errno.ETIMEDOUT,
265            errno.EHOSTUNREACH,
266            errno.ENETUNREACH,
267            errno.EINVAL,
268            errno.EPERM,
269            errno.EACCES,
270            # Windows 'invalid argument' error.
271            10022,
272            # Windows 'a socket operation was attempted to'
273            #         'an unreachable network' error.
274            10051,
275        }:
276            return True
277    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:
280def is_asyncio_streams_communication_error(exc: BaseException) -> bool:
281    """Should this streams error be considered a communication error?
282
283    This should be passed an exception which resulted from creating and
284    using asyncio streams. It should return True for any errors that could
285    conceivably arise due to unavailable/poor network connections,
286    firewall/connectivity issues, etc. These issues can often be safely
287    ignored or presented to the user as general 'connection-lost' events.
288    """
289    # pylint: disable=too-many-return-statements
290    import ssl
291
292    if isinstance(
293        exc,
294        (
295            ConnectionError,
296            TimeoutError,
297            EOFError,
298        ),
299    ):
300        return True
301
302    # Also some specific errno ones.
303    if isinstance(exc, OSError):
304        if exc.errno == 10051:  # Windows unreachable network error.
305            return True
306        if exc.errno in {
307            errno.ETIMEDOUT,
308            errno.EHOSTUNREACH,
309            errno.ENETUNREACH,
310        }:
311            return True
312
313    # Am occasionally getting a specific SSL error on shutdown which I
314    # believe is harmless (APPLICATION_DATA_AFTER_CLOSE_NOTIFY).
315    # It sounds like it may soon be ignored by Python (as of March 2022).
316    # Let's still complain, however, if we get any SSL errors besides
317    # this one. https://bugs.python.org/issue39951
318    if isinstance(exc, ssl.SSLError):
319        excstr = str(exc)
320        if 'APPLICATION_DATA_AFTER_CLOSE_NOTIFY' in excstr:
321            return True
322
323        # Also occasionally am getting WRONG_VERSION_NUMBER ssl errors;
324        # Assuming this just means client is attempting to connect from some
325        # outdated browser or whatnot.
326        if 'SSL: WRONG_VERSION_NUMBER' in excstr:
327            return True
328
329        # Also getting this sometimes which sounds like corrupt SSL data
330        # or something.
331        if 'SSL: BAD_RECORD_TYPE' in excstr:
332            return True
333
334        # And seeing this very rarely; assuming its just data corruption?
335        if 'SSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC' in excstr:
336            return True
337
338    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.