# Released under the MIT License. See LICENSE for details.
#
"""Common errors and related functionality."""
from __future__ import annotations
from typing import TYPE_CHECKING, override
import errno
if TYPE_CHECKING:
from typing import Any
import urllib3.response
from efro.terminal import ClrBase
[docs]
class CleanError(Exception):
"""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.
"""
[docs]
def pretty_print(
self,
flush: bool = True,
prefix: str = 'Error',
file: Any = None,
clr: type[ClrBase] | None = None,
) -> None:
"""Print the error to stdout, using red colored output if available.
If the error has an empty message, prints nothing (not even a newline).
"""
from efro.terminal import Clr
if clr is None:
clr = Clr
if prefix:
prefix = f'{prefix}: '
errstr = str(self)
if errstr:
print(
f'{clr.SRED}{prefix}{errstr}{clr.RST}', flush=flush, file=file
)
[docs]
class CommunicationError(Exception):
"""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.
"""
[docs]
class RemoteError(Exception):
"""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.
"""
def __init__(self, msg: str, peer_desc: str):
super().__init__(msg)
self._peer_desc = peer_desc
@override
def __str__(self) -> str:
s = ''.join(str(arg) for arg in self.args)
# Indent so we can more easily tell what is the remote part when
# this is in the middle of a long exception chain.
padding = ' '
s = ''.join(padding + line for line in s.splitlines(keepends=True))
return f'The following occurred on {self._peer_desc}:\n{s}'
[docs]
class IntegrityError(ValueError):
"""Data has been tampered with or corrupted in some form."""
[docs]
class AuthenticationError(Exception):
"""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.
"""
class _Urllib3HttpError(Exception):
"""Exception raised for non-200 html codes."""
def __init__(self, code: int) -> None:
self.code = code
# So we can see code in tracebacks.
@override
def __str__(self) -> str:
from http import HTTPStatus
try:
desc = HTTPStatus(self.code).description
except ValueError:
desc = 'Unknown HTTP Status Code'
return f'{self.code}: {desc}'
[docs]
def raise_for_urllib3_status(
response: urllib3.response.BaseHTTPResponse,
) -> None:
"""Raise an exception for html error codes aside from 200."""
if response.status != 200:
raise _Urllib3HttpError(code=response.status)
[docs]
def is_urllib3_communication_error(exc: BaseException, url: str | None) -> bool:
"""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.
"""
# Need to start building these up. For now treat everything as a
# real error.
import urllib3.exceptions
# If this error is from hitting max-retries, look at the underlying
# error instead.
if isinstance(exc, urllib3.exceptions.MaxRetryError):
# Hmm; will a max-retry error ever not have an underlying error?
if exc.reason is None:
return False
exc = exc.reason
if isinstance(exc, _Urllib3HttpError):
# Special sub-case: appspot.com hosting seems to give 403 errors
# (forbidden) to some countries. I'm assuming for legal reasons?..
# Let's consider that a communication error since its out of our
# control so we don't fill up logs with it.
if exc.code == 403 and url is not None and '.appspot.com' in url:
return True
elif isinstance(exc, urllib3.exceptions.ReadTimeoutError):
return True
elif isinstance(exc, urllib3.exceptions.ProtocolError):
# Most protocol errors quality as CommunicationErrors, but some
# may be due to server misconfigurations or whatnot so let's
# take it on a case by case basis.
excstr = str(exc)
if 'Connection aborted.' in excstr:
return True
return False
[docs]
def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool:
"""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.
"""
import urllib.error
import http.client
import socket
if isinstance(
exc,
(
urllib.error.URLError,
ConnectionError,
http.client.IncompleteRead,
http.client.BadStatusLine,
http.client.RemoteDisconnected,
socket.timeout,
),
):
# Special case: although an HTTPError is a subclass of URLError,
# we don't consider it a communication error. It generally means we
# have successfully communicated with the server but what we are asking
# for is not there/etc.
if isinstance(exc, urllib.error.HTTPError):
# Special sub-case: appspot.com hosting seems to give 403 errors
# (forbidden) to some countries. I'm assuming for legal reasons?..
# Let's consider that a communication error since its out of our
# control so we don't fill up logs with it.
if exc.code == 403 and url is not None and '.appspot.com' in url:
return True
return False
return True
if isinstance(exc, OSError):
if exc.errno == 10051: # Windows unreachable network error.
return True
if exc.errno in {
errno.ETIMEDOUT,
errno.EHOSTUNREACH,
errno.ENETUNREACH,
}:
return True
return False
[docs]
def is_requests_communication_error(exc: BaseException) -> bool:
"""Is the provided exception a communication-related error from requests?"""
import requests
# Looks like this maps pretty well onto requests' ConnectionError
return isinstance(exc, requests.ConnectionError)
[docs]
def is_udp_communication_error(exc: BaseException) -> bool:
"""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.
"""
if isinstance(exc, ConnectionRefusedError | TimeoutError):
return True
if isinstance(exc, OSError):
if exc.errno == 10051: # Windows unreachable network error.
return True
if exc.errno in {
errno.EADDRNOTAVAIL,
errno.ETIMEDOUT,
errno.EHOSTUNREACH,
errno.ENETUNREACH,
errno.EINVAL,
errno.EPERM,
errno.EACCES,
# Windows 'invalid argument' error.
10022,
# Windows 'a socket operation was attempted to'
# 'an unreachable network' error.
10051,
}:
return True
return False
[docs]
def is_asyncio_streams_communication_error(exc: BaseException) -> bool:
"""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.
"""
# pylint: disable=too-many-return-statements
import ssl
if isinstance(
exc,
(
ConnectionError,
TimeoutError,
EOFError,
),
):
return True
# Also some specific errno ones.
if isinstance(exc, OSError):
if exc.errno == 10051: # Windows unreachable network error.
return True
if exc.errno in {
errno.ETIMEDOUT,
errno.EHOSTUNREACH,
errno.ENETUNREACH,
}:
return True
# Am occasionally getting a specific SSL error on shutdown which I
# believe is harmless (APPLICATION_DATA_AFTER_CLOSE_NOTIFY).
# It sounds like it may soon be ignored by Python (as of March 2022).
# Let's still complain, however, if we get any SSL errors besides
# this one. https://bugs.python.org/issue39951
if isinstance(exc, ssl.SSLError):
excstr = str(exc)
if 'APPLICATION_DATA_AFTER_CLOSE_NOTIFY' in excstr:
return True
# Also occasionally am getting WRONG_VERSION_NUMBER ssl errors;
# Assuming this just means client is attempting to connect from some
# outdated browser or whatnot.
if 'SSL: WRONG_VERSION_NUMBER' in excstr:
return True
# Also getting this sometimes which sounds like corrupt SSL data
# or something.
if 'SSL: BAD_RECORD_TYPE' in excstr:
return True
# And seeing this very rarely; assuming its just data corruption?
if 'SSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC' in excstr:
return True
return False