Coverage for aiocoap/error.py: 82%
174 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-05 18:37 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-05 18:37 +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
13import ipaddress
15from .numbers import codes
16from . import util
19class Error(Exception):
20 """
21 Base exception for all exceptions that indicate a failed request
22 """
25class HelpfulError(Error):
26 def __str__(self):
27 """User presentable string. This should start with "Error:", or with
28 "Something Error:", because the context will not show that this was an
29 error."""
30 return type(self).__name__
32 def extra_help(self, hints={}) -> Optional[str]:
33 """Information printed at aiocoap-client or similar occasions when the
34 error message itself may be insufficient to point the user in the right
35 direction
37 The `hints` dictionary may be populated with context that the caller
38 has; the implementation must tolerate their absence. Currently
39 established keys:
41 * original_uri (str): URI that was attempted to access
42 * request (Message): Request that was assembled to be sent
43 """
44 return None
47class RenderableError(Error, metaclass=abc.ABCMeta):
48 """
49 Exception that can meaningfully be represented in a CoAP response
50 """
52 @abc.abstractmethod
53 def to_message(self):
54 """Create a CoAP message that should be sent when this exception is
55 rendered"""
58class ResponseWrappingError(Error):
59 """
60 An exception that is raised due to an unsuccessful but received response.
62 A better relationship with :mod:`.numbers.codes` should be worked out to do
63 ``except UnsupportedMediaType`` (similar to the various ``OSError``
64 subclasses).
65 """
67 def __init__(self, coapmessage):
68 self.coapmessage = coapmessage
70 def to_message(self):
71 return self.coapmessage
73 def __repr__(self):
74 return "<%s: %s %r>" % (
75 type(self).__name__,
76 self.coapmessage.code,
77 self.coapmessage.payload,
78 )
81class ConstructionRenderableError(RenderableError):
82 """
83 RenderableError that is constructed from class attributes :attr:`code` and
84 :attr:`message` (where the can be overridden in the constructor).
85 """
87 def __init__(self, message=None):
88 if message is not None:
89 self.message = message
91 def to_message(self):
92 from .message import Message
94 return Message(code=self.code, payload=self.message.encode("utf8"))
96 code = codes.INTERNAL_SERVER_ERROR #: Code assigned to messages built from it
97 message = "" #: Text sent in the built message's payload
100# This block is code-generated to make the types available to static checkers.
101# The __debug__ check below ensures that it stays up to date.
102class BadRequest(ConstructionRenderableError):
103 code = codes.BAD_REQUEST
106class Unauthorized(ConstructionRenderableError):
107 code = codes.UNAUTHORIZED
110class BadOption(ConstructionRenderableError):
111 code = codes.BAD_OPTION
114class Forbidden(ConstructionRenderableError):
115 code = codes.FORBIDDEN
118class NotFound(ConstructionRenderableError):
119 code = codes.NOT_FOUND
122class MethodNotAllowed(ConstructionRenderableError):
123 code = codes.METHOD_NOT_ALLOWED
126class NotAcceptable(ConstructionRenderableError):
127 code = codes.NOT_ACCEPTABLE
130class RequestEntityIncomplete(ConstructionRenderableError):
131 code = codes.REQUEST_ENTITY_INCOMPLETE
134class Conflict(ConstructionRenderableError):
135 code = codes.CONFLICT
138class PreconditionFailed(ConstructionRenderableError):
139 code = codes.PRECONDITION_FAILED
142class RequestEntityTooLarge(ConstructionRenderableError):
143 code = codes.REQUEST_ENTITY_TOO_LARGE
146class UnsupportedContentFormat(ConstructionRenderableError):
147 code = codes.UNSUPPORTED_CONTENT_FORMAT
150class UnprocessableEntity(ConstructionRenderableError):
151 code = codes.UNPROCESSABLE_ENTITY
154class TooManyRequests(ConstructionRenderableError):
155 code = codes.TOO_MANY_REQUESTS
158class InternalServerError(ConstructionRenderableError):
159 code = codes.INTERNAL_SERVER_ERROR
162class NotImplemented(ConstructionRenderableError):
163 code = codes.NOT_IMPLEMENTED
166class BadGateway(ConstructionRenderableError):
167 code = codes.BAD_GATEWAY
170class ServiceUnavailable(ConstructionRenderableError):
171 code = codes.SERVICE_UNAVAILABLE
174class GatewayTimeout(ConstructionRenderableError):
175 code = codes.GATEWAY_TIMEOUT
178class ProxyingNotSupported(ConstructionRenderableError):
179 code = codes.PROXYING_NOT_SUPPORTED
182class HopLimitReached(ConstructionRenderableError):
183 code = codes.HOP_LIMIT_REACHED
186if __debug__:
187 _missing_codes = False
188 _full_code = ""
189 for code in codes.Code:
190 if code.is_successful() or not code.is_response():
191 continue
192 classname = "".join(w.title() for w in code.name.split("_"))
193 _full_code += f"""
194class {classname}(ConstructionRenderableError):
195 code = codes.{code.name}"""
196 if classname not in locals():
197 warnings.warn(f"Missing exception type: f{classname}")
198 _missing_codes = True
199 continue
200 if locals()[classname].code != code:
201 warnings.warn(
202 f"Mismatched code for {classname}: Should be {code}, is {locals()[classname].code}"
203 )
204 _missing_codes = True
205 continue
206 if _missing_codes:
207 warnings.warn(
208 "Generated exception list is out of sync, should be:\n" + _full_code
209 )
211# More detailed versions of code based errors
214class NoResource(NotFound):
215 """
216 Raised when resource is not found.
217 """
219 message = "Error: Resource not found!"
221 def __init__(self):
222 warnings.warn(
223 "NoResource is deprecated in favor of NotFound",
224 DeprecationWarning,
225 stacklevel=2,
226 )
229class UnallowedMethod(MethodNotAllowed):
230 """
231 Raised by a resource when request method is understood by the server
232 but not allowed for that particular resource.
233 """
235 message = "Error: Method not allowed!"
238class UnsupportedMethod(MethodNotAllowed):
239 """
240 Raised when request method is not understood by the server at all.
241 """
243 message = "Error: Method not recognized!"
246class NetworkError(HelpfulError):
247 """Base class for all "something went wrong with name resolution, sending
248 or receiving packages".
250 Errors of these kinds are raised towards client callers when things went
251 wrong network-side, or at context creation. They are often raised from
252 socket.gaierror or similar classes, but these are wrapped in order to make
253 catching them possible independently of the underlying transport."""
255 def __str__(self):
256 return f"Network error: {type(self).__name__}"
258 def extra_help(self, hints={}):
259 if isinstance(self.__cause__, OSError):
260 if self.__cause__.errno == errno.ECONNREFUSED:
261 # seen trying to reach any used address with the port closed
262 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."
263 if self.__cause__.errno == errno.EHOSTUNREACH:
264 # seen trying to reach any unused local address
265 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."
266 if self.__cause__.errno == errno.ENETUNREACH:
267 connectivity_text = ""
268 try:
269 host, _ = util.hostportsplit(hints["request"].remote.hostinfo)
270 ip = ipaddress.ip_address(host)
271 if isinstance(ip, ipaddress.IPv4Address):
272 connectivity_text = "IPv4 "
273 if isinstance(ip, ipaddress.IPv6Address):
274 connectivity_text = "IPv6 "
276 # Else, we can't help because we don't have access to the
277 # name resolution result. This could all still be enhanced
278 # by threading along the information somewhere.
279 except Exception:
280 # This is not the point to complain about bugs, just give a more generic error.
281 pass
283 # seen trying to reach an IPv6 host through an IP literal from a v4-only system, or trying to reach 2001:db8::1, or 192.168.254.254
284 return f"No way of contacting the remote network could be found. This may be due to lack of {connectivity_text}connectivity, or 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."
285 if self.__cause__.errno == errno.EACCES:
286 # seen trying to reach the broadcast address of a local network
287 return "The operating system refused to send the request. For example, this can occur when attempting to send broadcast requests instead of multicast requests."
290class NoRequestInterface(RuntimeError, ConstructionRenderableError, NetworkError):
291 code = codes.PROXYING_NOT_SUPPORTED
292 message = "Error: No CoAP transport available for this scheme on any request interface." # or address, but we don't take transport-indication in full yet
294 def extra_help(self, hints={}):
295 return "More transports may be enabled by installing extra optional dependencies. Alternatively, consider using a CoAP-CoAP proxy."
298class ResolutionError(NetworkError):
299 """Resolving the host component of a URI to a usable transport address was
300 not possible"""
302 def __str__(self):
303 return f"Name resolution error: {self.args[0]}"
306class MessageError(NetworkError):
307 """Received an error from the remote on the CoAP message level (typically a
308 RST)"""
311class RemoteServerShutdown(NetworkError):
312 """The peer a request was sent to in a stateful connection closed the
313 connection around the time the request was sent"""
316class TimeoutError(NetworkError):
317 """Base for all timeout-ish errors.
319 Like NetworkError, receiving this alone does not indicate whether the
320 request may have reached the server or not.
321 """
323 def extra_help(self, hints={}):
324 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."
327class ConRetransmitsExceeded(TimeoutError):
328 """A transport that retransmits CON messages has failed to obtain a response
329 within its retransmission timeout.
331 When this is raised in a transport, requests failing with it may or may
332 have been received by the server.
333 """
336class RequestTimedOut(TimeoutError):
337 """
338 Raised when request is timed out.
340 This error is currently not produced by aiocoap; it is deprecated. Users
341 can now catch error.TimeoutError, or newer more detailed subtypes
342 introduced later.
343 """
346class WaitingForClientTimedOut(TimeoutError):
347 """
348 Raised when server expects some client action:
350 - sending next PUT/POST request with block1 or block2 option
351 - sending next GET request with block2 option
353 but client does nothing.
355 This error is currently not produced by aiocoap; it is deprecated. Users
356 can now catch error.TimeoutError, or newer more detailed subtypes
357 introduced later.
358 """
361class ConToMulticast(ValueError, HelpfulError):
362 # This could become a RenderableError if a proxy gains the capability to
363 # influence whether or not a message is sent reliably strongly (ie. beyond
364 # hints), and reliable transport is required in a request sent to
365 # multicast.
366 """
367 Raised when attempting to send a confirmable message to a multicast address.
368 """
370 def __str__(self):
371 return "Refusing to send CON message to multicast address"
373 def extra_help(self, hints={}):
374 return "Requests over UDP can only be sent to unicast addresses as per RFC7252; pick a concrete peer or configure a NON request instead."
377class ResourceChanged(Error):
378 """
379 The requested resource was modified during the request and could therefore
380 not be received in a consistent state.
381 """
384class UnexpectedBlock1Option(Error):
385 """
386 Raised when a server responds with block1 options that just don't match.
387 """
390class UnexpectedBlock2(Error):
391 """
392 Raised when a server responds with another block2 than expected.
393 """
396class MissingBlock2Option(Error):
397 """
398 Raised when response with Block2 option is expected
399 (previous response had Block2 option with More flag set),
400 but response without Block2 option is received.
401 """
404class NotObservable(Error):
405 """
406 The server did not accept the request to observe the resource.
407 """
410class ObservationCancelled(Error):
411 """
412 The server claimed that it will no longer sustain the observation.
413 """
416class UnparsableMessage(Error):
417 """
418 An incoming message does not look like CoAP.
420 Note that this happens rarely -- the requirements are just two bit at the
421 beginning of the message, and a minimum length.
422 """
425class LibraryShutdown(Error):
426 """The library or a transport registered with it was requested to shut
427 down; this error is raised in all outstanding requests."""
430class AnonymousHost(Error):
431 """This is raised when it is attempted to express as a reference a (base)
432 URI of a host or a resource that can not be reached by any process other
433 than this.
435 Typically, this happens when trying to serialize a link to a resource that
436 is hosted on a CoAP-over-TCP or -WebSockets client: Such resources can be
437 accessed for as long as the connection is active, but can not be used any
438 more once it is closed or even by another system."""
441class MalformedUrlError(ValueError, HelpfulError):
442 def __str__(self):
443 if self.args:
444 return f"Malformed URL: {self.args[0]}"
445 else:
446 return f"Malformed URL: {self.__cause__}"
449class IncompleteUrlError(ValueError, HelpfulError):
450 def __str__(self):
451 return "URL incomplete: Must start with a scheme."
453 def extra_help(self, hints={}):
454 return "Most URLs in aiocoap need to be given with a scheme, eg. the 'coap' in 'coap://example.com/path'."
457class MissingRemoteError(HelpfulError):
458 """A request is sent without a .remote attribute"""
460 def __str__(self):
461 return "Error: No remote endpoint set for request."
463 def extra_help(self, hints={}):
464 original_uri = hints.get("original_uri", None)
465 requested_message = hints.get("request", None)
466 if requested_message and (
467 requested_message.opt.proxy_uri or requested_message.opt.proxy_scheme
468 ):
469 if original_uri:
470 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."
471 else:
472 return (
473 "The message is set up for use with a proxy, but no proxy was set."
474 )
475 # Nothing helpful otherwise: The message was probably constructed in a
476 # rather manual way.
479__getattr__ = util.deprecation_getattr(
480 {
481 "UnsupportedMediaType": "UnsupportedContentFormat",
482 "RequestTimedOut": "TimeoutError",
483 "WaitingForClientTimedOut": "TimeoutError",
484 },
485 globals(),
486)