Coverage for aiocoap / error.py: 79%
175 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 12:28 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 12:28 +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
17from .util import DeprecationWarning
20class Error(Exception):
21 """
22 Base exception for all exceptions that indicate a failed request
23 """
26class HelpfulError(Error):
27 def __str__(self):
28 """User presentable string. This should start with "Error:", or with
29 "Something Error:", because the context will not show that this was an
30 error."""
31 return type(self).__name__
33 def extra_help(self, hints={}) -> Optional[str]:
34 """Information printed at aiocoap-client or similar occasions when the
35 error message itself may be insufficient to point the user in the right
36 direction
38 The `hints` dictionary may be populated with context that the caller
39 has; the implementation must tolerate their absence. Currently
40 established keys:
42 * original_uri (str): URI that was attempted to access
43 * request (Message): Request that was assembled to be sent
44 """
45 return None
48class RenderableError(Error, metaclass=abc.ABCMeta):
49 """
50 Exception that can meaningfully be represented in a CoAP response
51 """
53 @abc.abstractmethod
54 def to_message(self):
55 """Create a CoAP message that should be sent when this exception is
56 rendered"""
59class ResponseWrappingError(Error):
60 """
61 An exception that is raised due to an unsuccessful but received response.
63 A better relationship with :mod:`.numbers.codes` should be worked out to do
64 ``except UnsupportedMediaType`` (similar to the various ``OSError``
65 subclasses).
66 """
68 def __init__(self, coapmessage):
69 self.coapmessage = coapmessage
71 def to_message(self):
72 return self.coapmessage
74 def __repr__(self):
75 return "<%s: %s %r>" % (
76 type(self).__name__,
77 self.coapmessage.code,
78 self.coapmessage.payload,
79 )
82class ConstructionRenderableError(RenderableError):
83 """
84 RenderableError that is constructed from class attributes :attr:`code` and
85 :attr:`message` (where the can be overridden in the constructor).
86 """
88 def __init__(self, message=None):
89 if message is not None:
90 self.message = message
92 def to_message(self):
93 from .message import Message
95 return Message(code=self.code, payload=self.message.encode("utf8"))
97 code = codes.INTERNAL_SERVER_ERROR #: Code assigned to messages built from it
98 message = "" #: Text sent in the built message's payload
101# This block is code-generated to make the types available to static checkers.
102# The __debug__ check below ensures that it stays up to date.
103class BadRequest(ConstructionRenderableError):
104 code = codes.BAD_REQUEST
107class Unauthorized(ConstructionRenderableError):
108 code = codes.UNAUTHORIZED
111class BadOption(ConstructionRenderableError):
112 code = codes.BAD_OPTION
115class Forbidden(ConstructionRenderableError):
116 code = codes.FORBIDDEN
119class NotFound(ConstructionRenderableError):
120 code = codes.NOT_FOUND
123class MethodNotAllowed(ConstructionRenderableError):
124 code = codes.METHOD_NOT_ALLOWED
127class NotAcceptable(ConstructionRenderableError):
128 code = codes.NOT_ACCEPTABLE
131class RequestEntityIncomplete(ConstructionRenderableError):
132 code = codes.REQUEST_ENTITY_INCOMPLETE
135class Conflict(ConstructionRenderableError):
136 code = codes.CONFLICT
139class PreconditionFailed(ConstructionRenderableError):
140 code = codes.PRECONDITION_FAILED
143class RequestEntityTooLarge(ConstructionRenderableError):
144 code = codes.REQUEST_ENTITY_TOO_LARGE
147class UnsupportedContentFormat(ConstructionRenderableError):
148 code = codes.UNSUPPORTED_CONTENT_FORMAT
151class UnprocessableEntity(ConstructionRenderableError):
152 code = codes.UNPROCESSABLE_ENTITY
155class TooManyRequests(ConstructionRenderableError):
156 code = codes.TOO_MANY_REQUESTS
159class InternalServerError(ConstructionRenderableError):
160 code = codes.INTERNAL_SERVER_ERROR
163class NotImplemented(ConstructionRenderableError):
164 code = codes.NOT_IMPLEMENTED
167class BadGateway(ConstructionRenderableError):
168 code = codes.BAD_GATEWAY
171class ServiceUnavailable(ConstructionRenderableError):
172 code = codes.SERVICE_UNAVAILABLE
175class GatewayTimeout(ConstructionRenderableError):
176 code = codes.GATEWAY_TIMEOUT
179class ProxyingNotSupported(ConstructionRenderableError):
180 code = codes.PROXYING_NOT_SUPPORTED
183class HopLimitReached(ConstructionRenderableError):
184 code = codes.HOP_LIMIT_REACHED
187if __debug__:
188 _missing_codes = False
189 _full_code = ""
190 for code in codes.Code:
191 if code.is_successful() or not code.is_response():
192 continue
193 classname = "".join(w.title() for w in code.name.split("_"))
194 _full_code += f"""
195class {classname}(ConstructionRenderableError):
196 code = codes.{code.name}"""
197 if classname not in locals():
198 warnings.warn(f"Missing exception type: f{classname}")
199 _missing_codes = True
200 continue
201 if locals()[classname].code != code:
202 warnings.warn(
203 f"Mismatched code for {classname}: Should be {code}, is {locals()[classname].code}"
204 )
205 _missing_codes = True
206 continue
207 if _missing_codes:
208 warnings.warn(
209 "Generated exception list is out of sync, should be:\n" + _full_code
210 )
212# More detailed versions of code based errors
215class NoResource(NotFound):
216 """
217 Raised when resource is not found.
218 """
220 message = "Error: Resource not found!"
222 def __init__(self):
223 warnings.warn(
224 "NoResource is deprecated in favor of NotFound",
225 DeprecationWarning,
226 stacklevel=2,
227 )
230class UnallowedMethod(MethodNotAllowed):
231 """
232 Raised by a resource when request method is understood by the server
233 but not allowed for that particular resource.
234 """
236 message = "Error: Method not allowed!"
239class UnsupportedMethod(MethodNotAllowed):
240 """
241 Raised when request method is not understood by the server at all.
242 """
244 message = "Error: Method not recognized!"
247class NetworkError(HelpfulError):
248 """Base class for all "something went wrong with name resolution, sending
249 or receiving packages".
251 Errors of these kinds are raised towards client callers when things went
252 wrong network-side, or at context creation. They are often raised from
253 socket.gaierror or similar classes, but these are wrapped in order to make
254 catching them possible independently of the underlying transport."""
256 def __str__(self):
257 return f"Network error: {type(self).__name__}"
259 def extra_help(self, hints={}):
260 if isinstance(self.__cause__, OSError):
261 if self.__cause__.errno == errno.ECONNREFUSED:
262 # seen trying to reach any used address with the port closed
263 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."
264 if self.__cause__.errno == errno.EHOSTUNREACH:
265 # seen trying to reach any unused local address
266 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."
267 if self.__cause__.errno == errno.ENETUNREACH:
268 connectivity_text = ""
269 try:
270 host, _ = util.hostportsplit(hints["request"].remote.hostinfo)
271 ip = ipaddress.ip_address(host)
272 if isinstance(ip, ipaddress.IPv4Address):
273 connectivity_text = "IPv4 "
274 if isinstance(ip, ipaddress.IPv6Address):
275 connectivity_text = "IPv6 "
277 # Else, we can't help because we don't have access to the
278 # name resolution result. This could all still be enhanced
279 # by threading along the information somewhere.
280 except Exception:
281 # This is not the point to complain about bugs, just give a more generic error.
282 pass
284 # 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
285 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."
286 if self.__cause__.errno == errno.EACCES:
287 # seen trying to reach the broadcast address of a local network
288 return "The operating system refused to send the request. For example, this can occur when attempting to send broadcast requests instead of multicast requests."
291class NoRequestInterface(RuntimeError, ConstructionRenderableError, NetworkError):
292 code = codes.PROXYING_NOT_SUPPORTED
293 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
295 def extra_help(self, hints={}):
296 return "More transports may be enabled by installing extra optional dependencies. Alternatively, consider using a CoAP-CoAP proxy."
299class ResolutionError(NetworkError):
300 """Resolving the host component of a URI to a usable transport address was
301 not possible"""
303 def __str__(self):
304 return f"Name resolution error: {self.args[0]}"
307class MessageError(NetworkError):
308 """Received an error from the remote on the CoAP message level (typically a
309 RST)"""
312class RemoteServerShutdown(NetworkError):
313 """The peer a request was sent to in a stateful connection closed the
314 connection around the time the request was sent"""
317class TimeoutError(NetworkError):
318 """Base for all timeout-ish errors.
320 Like NetworkError, receiving this alone does not indicate whether the
321 request may have reached the server or not.
322 """
324 def extra_help(self, hints={}):
325 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."
328class ConRetransmitsExceeded(TimeoutError):
329 """A transport that retransmits CON messages has failed to obtain a response
330 within its retransmission timeout.
332 When this is raised in a transport, requests failing with it may or may
333 have been received by the server.
334 """
337class RequestTimedOut(TimeoutError):
338 """
339 Raised when request is timed out.
341 This error is currently not produced by aiocoap; it is deprecated. Users
342 can now catch error.TimeoutError, or newer more detailed subtypes
343 introduced later.
344 """
347class WaitingForClientTimedOut(TimeoutError):
348 """
349 Raised when server expects some client action:
351 - sending next PUT/POST request with block1 or block2 option
352 - sending next GET request with block2 option
354 but client does nothing.
356 This error is currently not produced by aiocoap; it is deprecated. Users
357 can now catch error.TimeoutError, or newer more detailed subtypes
358 introduced later.
359 """
362class ConToMulticast(ValueError, HelpfulError):
363 # This could become a RenderableError if a proxy gains the capability to
364 # influence whether or not a message is sent reliably strongly (ie. beyond
365 # hints), and reliable transport is required in a request sent to
366 # multicast.
367 """
368 Raised when attempting to send a confirmable message to a multicast address.
369 """
371 def __str__(self):
372 return "Refusing to send CON message to multicast address"
374 def extra_help(self, hints={}):
375 return "Requests over UDP can only be sent to unicast addresses as per RFC7252; pick a concrete peer or configure a NON request instead."
378class ResourceChanged(Error):
379 """
380 The requested resource was modified during the request and could therefore
381 not be received in a consistent state.
382 """
385class UnexpectedBlock1Option(Error):
386 """
387 Raised when a server responds with block1 options that just don't match.
388 """
391class UnexpectedBlock2(Error):
392 """
393 Raised when a server responds with another block2 than expected.
394 """
397class MissingBlock2Option(Error):
398 """
399 Raised when response with Block2 option is expected
400 (previous response had Block2 option with More flag set),
401 but response without Block2 option is received.
402 """
405class NotObservable(Error):
406 """
407 The server did not accept the request to observe the resource.
408 """
411class ObservationCancelled(Error):
412 """
413 The server claimed that it will no longer sustain the observation.
414 """
417class UnparsableMessage(Error):
418 """
419 An incoming message does not look like CoAP.
421 Note that this happens rarely -- the requirements are just two bit at the
422 beginning of the message, and a minimum length.
423 """
426class LibraryShutdown(Error):
427 """The library or a transport registered with it was requested to shut
428 down; this error is raised in all outstanding requests."""
431class AnonymousHost(Error):
432 """This is raised when it is attempted to express as a reference a (base)
433 URI of a host or a resource that can not be reached by any process other
434 than this.
436 Typically, this happens when trying to serialize a link to a resource that
437 is hosted on a CoAP-over-TCP or -WebSockets client: Such resources can be
438 accessed for as long as the connection is active, but can not be used any
439 more once it is closed or even by another system."""
442class MalformedUrlError(ValueError, HelpfulError):
443 def __str__(self):
444 if self.args:
445 return f"Malformed URL: {self.args[0]}"
446 else:
447 return f"Malformed URL: {self.__cause__}"
450class IncompleteUrlError(ValueError, HelpfulError):
451 def __str__(self):
452 return "URL incomplete: Must start with a scheme."
454 def extra_help(self, hints={}):
455 return "Most URLs in aiocoap need to be given with a scheme, eg. the 'coap' in 'coap://example.com/path'."
458class MissingRemoteError(HelpfulError):
459 """A request is sent without a .remote attribute"""
461 def __str__(self):
462 return "Error: No remote endpoint set for request."
464 def extra_help(self, hints={}):
465 original_uri = hints.get("original_uri", None)
466 requested_message = hints.get("request", None)
467 if requested_message and (
468 requested_message.opt.proxy_uri or requested_message.opt.proxy_scheme
469 ):
470 if original_uri:
471 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."
472 else:
473 return (
474 "The message is set up for use with a proxy, but no proxy was set."
475 )
476 # Nothing helpful otherwise: The message was probably constructed in a
477 # rather manual way.
480__getattr__ = util.deprecation_getattr(
481 {
482 "UnsupportedMediaType": "UnsupportedContentFormat",
483 "RequestTimedOut": "TimeoutError",
484 "WaitingForClientTimedOut": "TimeoutError",
485 },
486 globals(),
487)