Coverage for aiocoap/error.py: 84%
153 statements
« prev ^ index » next coverage.py v7.6.8, created at 2024-11-28 12:34 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2024-11-28 12:34 +0000
1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors
2#
3# SPDX-License-Identifier: MIT
5"""
6Common errors for the aiocoap library
7"""
9import warnings
10import abc
11import errno
12from typing import Optional
14from .numbers import codes
15from . import util
18class Error(Exception):
19 """
20 Base exception for all exceptions that indicate a failed request
21 """
24class HelpfulError(Error):
25 def __str__(self):
26 """User presentable string. This should start with "Error:", or with
27 "Something Error:", because the context will not show that this was an
28 error."""
29 return type(self).__name__
31 def extra_help(self, hints={}) -> Optional[str]:
32 """Information printed at aiocoap-client or similar occasions when the
33 error message itself may be insufficient to point the user in the right
34 direction
36 The `hints` dictonary may be populated with context that the caller
37 has; the implementation must tolerate their absence. Currently
38 established keys:
40 * original_uri (str): URI that was attemted to access
41 * request (Message): Request that was assembled to be sent
42 """
43 return None
46class RenderableError(Error, metaclass=abc.ABCMeta):
47 """
48 Exception that can meaningfully be represented in a CoAP response
49 """
51 @abc.abstractmethod
52 def to_message(self):
53 """Create a CoAP message that should be sent when this exception is
54 rendered"""
57class ResponseWrappingError(Error):
58 """
59 An exception that is raised due to an unsuccessful but received response.
61 A better relationship with :mod:`.numbers.codes` should be worked out to do
62 ``except UnsupportedMediaType`` (similar to the various ``OSError``
63 subclasses).
64 """
66 def __init__(self, coapmessage):
67 self.coapmessage = coapmessage
69 def to_message(self):
70 return self.coapmessage
72 def __repr__(self):
73 return "<%s: %s %r>" % (
74 type(self).__name__,
75 self.coapmessage.code,
76 self.coapmessage.payload,
77 )
80class ConstructionRenderableError(RenderableError):
81 """
82 RenderableError that is constructed from class attributes :attr:`code` and
83 :attr:`message` (where the can be overridden in the constructor).
84 """
86 def __init__(self, message=None):
87 if message is not None:
88 self.message = message
90 def to_message(self):
91 from .message import Message
93 return Message(code=self.code, payload=self.message.encode("utf8"))
95 code = codes.INTERNAL_SERVER_ERROR #: Code assigned to messages built from it
96 message = "" #: Text sent in the built message's payload
99# This block is code-generated to make the types available to static checkers.
100# The __debug__ check below ensures that it stays up to date.
101class BadRequest(ConstructionRenderableError):
102 code = codes.BAD_REQUEST
105class Unauthorized(ConstructionRenderableError):
106 code = codes.UNAUTHORIZED
109class BadOption(ConstructionRenderableError):
110 code = codes.BAD_OPTION
113class Forbidden(ConstructionRenderableError):
114 code = codes.FORBIDDEN
117class NotFound(ConstructionRenderableError):
118 code = codes.NOT_FOUND
121class MethodNotAllowed(ConstructionRenderableError):
122 code = codes.METHOD_NOT_ALLOWED
125class NotAcceptable(ConstructionRenderableError):
126 code = codes.NOT_ACCEPTABLE
129class RequestEntityIncomplete(ConstructionRenderableError):
130 code = codes.REQUEST_ENTITY_INCOMPLETE
133class Conflict(ConstructionRenderableError):
134 code = codes.CONFLICT
137class PreconditionFailed(ConstructionRenderableError):
138 code = codes.PRECONDITION_FAILED
141class RequestEntityTooLarge(ConstructionRenderableError):
142 code = codes.REQUEST_ENTITY_TOO_LARGE
145class UnsupportedContentFormat(ConstructionRenderableError):
146 code = codes.UNSUPPORTED_CONTENT_FORMAT
149class UnprocessableEntity(ConstructionRenderableError):
150 code = codes.UNPROCESSABLE_ENTITY
153class TooManyRequests(ConstructionRenderableError):
154 code = codes.TOO_MANY_REQUESTS
157class InternalServerError(ConstructionRenderableError):
158 code = codes.INTERNAL_SERVER_ERROR
161class NotImplemented(ConstructionRenderableError):
162 code = codes.NOT_IMPLEMENTED
165class BadGateway(ConstructionRenderableError):
166 code = codes.BAD_GATEWAY
169class ServiceUnavailable(ConstructionRenderableError):
170 code = codes.SERVICE_UNAVAILABLE
173class GatewayTimeout(ConstructionRenderableError):
174 code = codes.GATEWAY_TIMEOUT
177class ProxyingNotSupported(ConstructionRenderableError):
178 code = codes.PROXYING_NOT_SUPPORTED
181class HopLimitReached(ConstructionRenderableError):
182 code = codes.HOP_LIMIT_REACHED
185if __debug__:
186 _missing_codes = False
187 _full_code = ""
188 for code in codes.Code:
189 if code.is_successful() or not code.is_response():
190 continue
191 classname = "".join(w.title() for w in code.name.split("_"))
192 _full_code += f"""
193class {classname}(ConstructionRenderableError):
194 code = codes.{code.name}"""
195 if classname not in locals():
196 warnings.warn(f"Missing exception type: f{classname}")
197 _missing_codes = True
198 continue
199 if locals()[classname].code != code:
200 warnings.warn(
201 f"Mismatched code for {classname}: Should be {code}, is {locals()[classname].code}"
202 )
203 _missing_codes = True
204 continue
205 if _missing_codes:
206 warnings.warn(
207 "Generated exception list is out of sync, should be:\n" + _full_code
208 )
210# More detailed versions of code based errors
213class NoResource(NotFound):
214 """
215 Raised when resource is not found.
216 """
218 message = "Error: Resource not found!"
220 def __init__(self):
221 warnings.warn(
222 "NoResource is deprecated in favor of NotFound",
223 DeprecationWarning,
224 stacklevel=2,
225 )
228class UnallowedMethod(MethodNotAllowed):
229 """
230 Raised by a resource when request method is understood by the server
231 but not allowed for that particular resource.
232 """
234 message = "Error: Method not allowed!"
237class UnsupportedMethod(MethodNotAllowed):
238 """
239 Raised when request method is not understood by the server at all.
240 """
242 message = "Error: Method not recognized!"
245class NetworkError(HelpfulError):
246 """Base class for all "something went wrong with name resolution, sending
247 or receiving packages".
249 Errors of these kinds are raised towards client callers when things went
250 wrong network-side, or at context creation. They are often raised from
251 socket.gaierror or similar classes, but these are wrapped in order to make
252 catching them possible independently of the underlying transport."""
254 def __str__(self):
255 return f"Network error: {type(self).__name__}"
257 def extra_help(self, hints={}):
258 if isinstance(self.__cause__, OSError):
259 if self.__cause__.errno == errno.ECONNREFUSED:
260 # seen trying to reach any used address with the port closed
261 return "The remote host could be reached, but reported that the requested port is not open. Check whether a CoAP server is running at the address, or whether it is running on a different port."
262 if self.__cause__.errno == errno.EHOSTUNREACH:
263 # seen trying to reach any unused local address
264 return "No way of contacting the remote host could be found. This could be because a host on the local network is offline or firewalled. Tools for debugging in the next step could be ping or traceroute."
265 if self.__cause__.errno == errno.ENETUNREACH:
266 # seen trying to reach an IPv6 host through an IP literal from a v4-only system, or trying to reach 2001:db8::1
267 return "No way of contacting the remote network could be found. This may be due to lack of IPv6 connectivity, lack of a concrete route (eg. trying to reach a private use network which there is no route to). Tools for debugging in the next step could be ping or traceroute."
268 if self.__cause__.errno == errno.EACCES:
269 # seen trying to reach the broadcast address of a local network
270 return "The operating system refused to send the request. For example, this can occur when attempting to send broadcast requests instead of multicast requests."
273class ResolutionError(NetworkError):
274 """Resolving the host component of a URI to a usable transport address was
275 not possible"""
277 def __str__(self):
278 return f"Name resolution error: {self.args[0]}"
281class MessageError(NetworkError):
282 """Received an error from the remote on the CoAP message level (typically a
283 RST)"""
286class RemoteServerShutdown(NetworkError):
287 """The peer a request was sent to in a stateful connection closed the
288 connection around the time the request was sent"""
291class TimeoutError(NetworkError):
292 """Base for all timeout-ish errors.
294 Like NetworkError, receiving this alone does not indicate whether the
295 request may have reached the server or not.
296 """
298 def extra_help(self, hints={}):
299 return "Neither a response nor an error was received. This can have a wide range of causes, from the address being wrong to the server being stuck."
302class ConRetransmitsExceeded(TimeoutError):
303 """A transport that retransmits CON messages has failed to obtain a response
304 within its retransmission timeout.
306 When this is raised in a transport, requests failing with it may or may
307 have been received by the server.
308 """
311class RequestTimedOut(TimeoutError):
312 """
313 Raised when request is timed out.
315 This error is currently not produced by aiocoap; it is deprecated. Users
316 can now catch error.TimeoutError, or newer more detailed subtypes
317 introduced later.
318 """
321class WaitingForClientTimedOut(TimeoutError):
322 """
323 Raised when server expects some client action:
325 - sending next PUT/POST request with block1 or block2 option
326 - sending next GET request with block2 option
328 but client does nothing.
330 This error is currently not produced by aiocoap; it is deprecated. Users
331 can now catch error.TimeoutError, or newer more detailed subtypes
332 introduced later.
333 """
336class ResourceChanged(Error):
337 """
338 The requested resource was modified during the request and could therefore
339 not be received in a consistent state.
340 """
343class UnexpectedBlock1Option(Error):
344 """
345 Raised when a server responds with block1 options that just don't match.
346 """
349class UnexpectedBlock2(Error):
350 """
351 Raised when a server responds with another block2 than expected.
352 """
355class MissingBlock2Option(Error):
356 """
357 Raised when response with Block2 option is expected
358 (previous response had Block2 option with More flag set),
359 but response without Block2 option is received.
360 """
363class NotObservable(Error):
364 """
365 The server did not accept the request to observe the resource.
366 """
369class ObservationCancelled(Error):
370 """
371 The server claimed that it will no longer sustain the observation.
372 """
375class UnparsableMessage(Error):
376 """
377 An incoming message does not look like CoAP.
379 Note that this happens rarely -- the requirements are just two bit at the
380 beginning of the message, and a minimum length.
381 """
384class LibraryShutdown(Error):
385 """The library or a transport registered with it was requested to shut
386 down; this error is raised in all outstanding requests."""
389class AnonymousHost(Error):
390 """This is raised when it is attempted to express as a reference a (base)
391 URI of a host or a resource that can not be reached by any process other
392 than this.
394 Typically, this happens when trying to serialize a link to a resource that
395 is hosted on a CoAP-over-TCP or -WebSockets client: Such resources can be
396 accessed for as long as the connection is active, but can not be used any
397 more once it is closed or even by another system."""
400class MalformedUrlError(ValueError, HelpfulError):
401 def __str__(self):
402 if self.args:
403 return f"Malformed URL: {self.args[0]}"
404 else:
405 return f"Malformed URL: {self.__cause__}"
408class IncompleteUrlError(ValueError, HelpfulError):
409 def __str__(self):
410 return "URL incomplete: Must start with a scheme."
412 def extra_help(self, hints={}):
413 return "Most URLs in aiocoap need to be given with a scheme, eg. the 'coap' in 'coap://example.com/path'."
416class MissingRemoteError(HelpfulError):
417 """A request is sent without a .remote attribute"""
419 def __str__(self):
420 return "Error: No remote endpoint set for request."
422 def extra_help(self, hints={}):
423 original_uri = hints.get("original_uri", None)
424 requested_message = hints.get("request", None)
425 if requested_message and (
426 requested_message.opt.proxy_uri or requested_message.opt.proxy_scheme
427 ):
428 if original_uri:
429 return f"The message is set up for use with a proxy (because the scheme of {original_uri!r} is not supported), but no proxy was set."
430 else:
431 return (
432 "The message is set up for use with a proxy, but no proxy was set."
433 )
434 # Nothing helpful otherwise: The message was probably constructed in a
435 # rather manual way.
438__getattr__ = util.deprecation_getattr(
439 {
440 "UnsupportedMediaType": "UnsupportedContentFormat",
441 "RequestTimedOut": "TimeoutError",
442 "WaitingForClientTimedOut": "TimeoutError",
443 },
444 globals(),
445)