Coverage for aiocoap/error.py: 82%

174 statements  

« 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 

4 

5""" 

6Common errors for the aiocoap library 

7""" 

8 

9import warnings 

10import abc 

11import errno 

12from typing import Optional 

13import ipaddress 

14 

15from .numbers import codes 

16from . import util 

17 

18 

19class Error(Exception): 

20 """ 

21 Base exception for all exceptions that indicate a failed request 

22 """ 

23 

24 

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__ 

31 

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 

36 

37 The `hints` dictionary may be populated with context that the caller 

38 has; the implementation must tolerate their absence. Currently 

39 established keys: 

40 

41 * original_uri (str): URI that was attempted to access 

42 * request (Message): Request that was assembled to be sent 

43 """ 

44 return None 

45 

46 

47class RenderableError(Error, metaclass=abc.ABCMeta): 

48 """ 

49 Exception that can meaningfully be represented in a CoAP response 

50 """ 

51 

52 @abc.abstractmethod 

53 def to_message(self): 

54 """Create a CoAP message that should be sent when this exception is 

55 rendered""" 

56 

57 

58class ResponseWrappingError(Error): 

59 """ 

60 An exception that is raised due to an unsuccessful but received response. 

61 

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 """ 

66 

67 def __init__(self, coapmessage): 

68 self.coapmessage = coapmessage 

69 

70 def to_message(self): 

71 return self.coapmessage 

72 

73 def __repr__(self): 

74 return "<%s: %s %r>" % ( 

75 type(self).__name__, 

76 self.coapmessage.code, 

77 self.coapmessage.payload, 

78 ) 

79 

80 

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 """ 

86 

87 def __init__(self, message=None): 

88 if message is not None: 

89 self.message = message 

90 

91 def to_message(self): 

92 from .message import Message 

93 

94 return Message(code=self.code, payload=self.message.encode("utf8")) 

95 

96 code = codes.INTERNAL_SERVER_ERROR #: Code assigned to messages built from it 

97 message = "" #: Text sent in the built message's payload 

98 

99 

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 

104 

105 

106class Unauthorized(ConstructionRenderableError): 

107 code = codes.UNAUTHORIZED 

108 

109 

110class BadOption(ConstructionRenderableError): 

111 code = codes.BAD_OPTION 

112 

113 

114class Forbidden(ConstructionRenderableError): 

115 code = codes.FORBIDDEN 

116 

117 

118class NotFound(ConstructionRenderableError): 

119 code = codes.NOT_FOUND 

120 

121 

122class MethodNotAllowed(ConstructionRenderableError): 

123 code = codes.METHOD_NOT_ALLOWED 

124 

125 

126class NotAcceptable(ConstructionRenderableError): 

127 code = codes.NOT_ACCEPTABLE 

128 

129 

130class RequestEntityIncomplete(ConstructionRenderableError): 

131 code = codes.REQUEST_ENTITY_INCOMPLETE 

132 

133 

134class Conflict(ConstructionRenderableError): 

135 code = codes.CONFLICT 

136 

137 

138class PreconditionFailed(ConstructionRenderableError): 

139 code = codes.PRECONDITION_FAILED 

140 

141 

142class RequestEntityTooLarge(ConstructionRenderableError): 

143 code = codes.REQUEST_ENTITY_TOO_LARGE 

144 

145 

146class UnsupportedContentFormat(ConstructionRenderableError): 

147 code = codes.UNSUPPORTED_CONTENT_FORMAT 

148 

149 

150class UnprocessableEntity(ConstructionRenderableError): 

151 code = codes.UNPROCESSABLE_ENTITY 

152 

153 

154class TooManyRequests(ConstructionRenderableError): 

155 code = codes.TOO_MANY_REQUESTS 

156 

157 

158class InternalServerError(ConstructionRenderableError): 

159 code = codes.INTERNAL_SERVER_ERROR 

160 

161 

162class NotImplemented(ConstructionRenderableError): 

163 code = codes.NOT_IMPLEMENTED 

164 

165 

166class BadGateway(ConstructionRenderableError): 

167 code = codes.BAD_GATEWAY 

168 

169 

170class ServiceUnavailable(ConstructionRenderableError): 

171 code = codes.SERVICE_UNAVAILABLE 

172 

173 

174class GatewayTimeout(ConstructionRenderableError): 

175 code = codes.GATEWAY_TIMEOUT 

176 

177 

178class ProxyingNotSupported(ConstructionRenderableError): 

179 code = codes.PROXYING_NOT_SUPPORTED 

180 

181 

182class HopLimitReached(ConstructionRenderableError): 

183 code = codes.HOP_LIMIT_REACHED 

184 

185 

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 ) 

210 

211# More detailed versions of code based errors 

212 

213 

214class NoResource(NotFound): 

215 """ 

216 Raised when resource is not found. 

217 """ 

218 

219 message = "Error: Resource not found!" 

220 

221 def __init__(self): 

222 warnings.warn( 

223 "NoResource is deprecated in favor of NotFound", 

224 DeprecationWarning, 

225 stacklevel=2, 

226 ) 

227 

228 

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 """ 

234 

235 message = "Error: Method not allowed!" 

236 

237 

238class UnsupportedMethod(MethodNotAllowed): 

239 """ 

240 Raised when request method is not understood by the server at all. 

241 """ 

242 

243 message = "Error: Method not recognized!" 

244 

245 

246class NetworkError(HelpfulError): 

247 """Base class for all "something went wrong with name resolution, sending 

248 or receiving packages". 

249 

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.""" 

254 

255 def __str__(self): 

256 return f"Network error: {type(self).__name__}" 

257 

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 " 

275 

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 

282 

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." 

288 

289 

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 

293 

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." 

296 

297 

298class ResolutionError(NetworkError): 

299 """Resolving the host component of a URI to a usable transport address was 

300 not possible""" 

301 

302 def __str__(self): 

303 return f"Name resolution error: {self.args[0]}" 

304 

305 

306class MessageError(NetworkError): 

307 """Received an error from the remote on the CoAP message level (typically a 

308 RST)""" 

309 

310 

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""" 

314 

315 

316class TimeoutError(NetworkError): 

317 """Base for all timeout-ish errors. 

318 

319 Like NetworkError, receiving this alone does not indicate whether the 

320 request may have reached the server or not. 

321 """ 

322 

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." 

325 

326 

327class ConRetransmitsExceeded(TimeoutError): 

328 """A transport that retransmits CON messages has failed to obtain a response 

329 within its retransmission timeout. 

330 

331 When this is raised in a transport, requests failing with it may or may 

332 have been received by the server. 

333 """ 

334 

335 

336class RequestTimedOut(TimeoutError): 

337 """ 

338 Raised when request is timed out. 

339 

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 """ 

344 

345 

346class WaitingForClientTimedOut(TimeoutError): 

347 """ 

348 Raised when server expects some client action: 

349 

350 - sending next PUT/POST request with block1 or block2 option 

351 - sending next GET request with block2 option 

352 

353 but client does nothing. 

354 

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 """ 

359 

360 

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 """ 

369 

370 def __str__(self): 

371 return "Refusing to send CON message to multicast address" 

372 

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." 

375 

376 

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 """ 

382 

383 

384class UnexpectedBlock1Option(Error): 

385 """ 

386 Raised when a server responds with block1 options that just don't match. 

387 """ 

388 

389 

390class UnexpectedBlock2(Error): 

391 """ 

392 Raised when a server responds with another block2 than expected. 

393 """ 

394 

395 

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 """ 

402 

403 

404class NotObservable(Error): 

405 """ 

406 The server did not accept the request to observe the resource. 

407 """ 

408 

409 

410class ObservationCancelled(Error): 

411 """ 

412 The server claimed that it will no longer sustain the observation. 

413 """ 

414 

415 

416class UnparsableMessage(Error): 

417 """ 

418 An incoming message does not look like CoAP. 

419 

420 Note that this happens rarely -- the requirements are just two bit at the 

421 beginning of the message, and a minimum length. 

422 """ 

423 

424 

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.""" 

428 

429 

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. 

434 

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.""" 

439 

440 

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__}" 

447 

448 

449class IncompleteUrlError(ValueError, HelpfulError): 

450 def __str__(self): 

451 return "URL incomplete: Must start with a scheme." 

452 

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'." 

455 

456 

457class MissingRemoteError(HelpfulError): 

458 """A request is sent without a .remote attribute""" 

459 

460 def __str__(self): 

461 return "Error: No remote endpoint set for request." 

462 

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. 

477 

478 

479__getattr__ = util.deprecation_getattr( 

480 { 

481 "UnsupportedMediaType": "UnsupportedContentFormat", 

482 "RequestTimedOut": "TimeoutError", 

483 "WaitingForClientTimedOut": "TimeoutError", 

484 }, 

485 globals(), 

486)