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
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.
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).
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.
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.
Data has been tampered with or corrupted in some form.
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.
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.
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.
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.
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?
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.
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.