Coverage for aiocoap / error.py: 79%

175 statements  

« 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 

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 

17from .util import DeprecationWarning 

18 

19 

20class Error(Exception): 

21 """ 

22 Base exception for all exceptions that indicate a failed request 

23 """ 

24 

25 

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__ 

32 

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 

37 

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

39 has; the implementation must tolerate their absence. Currently 

40 established keys: 

41 

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

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

44 """ 

45 return None 

46 

47 

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

49 """ 

50 Exception that can meaningfully be represented in a CoAP response 

51 """ 

52 

53 @abc.abstractmethod 

54 def to_message(self): 

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

56 rendered""" 

57 

58 

59class ResponseWrappingError(Error): 

60 """ 

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

62 

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

67 

68 def __init__(self, coapmessage): 

69 self.coapmessage = coapmessage 

70 

71 def to_message(self): 

72 return self.coapmessage 

73 

74 def __repr__(self): 

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

76 type(self).__name__, 

77 self.coapmessage.code, 

78 self.coapmessage.payload, 

79 ) 

80 

81 

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

87 

88 def __init__(self, message=None): 

89 if message is not None: 

90 self.message = message 

91 

92 def to_message(self): 

93 from .message import Message 

94 

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

96 

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

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

99 

100 

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 

105 

106 

107class Unauthorized(ConstructionRenderableError): 

108 code = codes.UNAUTHORIZED 

109 

110 

111class BadOption(ConstructionRenderableError): 

112 code = codes.BAD_OPTION 

113 

114 

115class Forbidden(ConstructionRenderableError): 

116 code = codes.FORBIDDEN 

117 

118 

119class NotFound(ConstructionRenderableError): 

120 code = codes.NOT_FOUND 

121 

122 

123class MethodNotAllowed(ConstructionRenderableError): 

124 code = codes.METHOD_NOT_ALLOWED 

125 

126 

127class NotAcceptable(ConstructionRenderableError): 

128 code = codes.NOT_ACCEPTABLE 

129 

130 

131class RequestEntityIncomplete(ConstructionRenderableError): 

132 code = codes.REQUEST_ENTITY_INCOMPLETE 

133 

134 

135class Conflict(ConstructionRenderableError): 

136 code = codes.CONFLICT 

137 

138 

139class PreconditionFailed(ConstructionRenderableError): 

140 code = codes.PRECONDITION_FAILED 

141 

142 

143class RequestEntityTooLarge(ConstructionRenderableError): 

144 code = codes.REQUEST_ENTITY_TOO_LARGE 

145 

146 

147class UnsupportedContentFormat(ConstructionRenderableError): 

148 code = codes.UNSUPPORTED_CONTENT_FORMAT 

149 

150 

151class UnprocessableEntity(ConstructionRenderableError): 

152 code = codes.UNPROCESSABLE_ENTITY 

153 

154 

155class TooManyRequests(ConstructionRenderableError): 

156 code = codes.TOO_MANY_REQUESTS 

157 

158 

159class InternalServerError(ConstructionRenderableError): 

160 code = codes.INTERNAL_SERVER_ERROR 

161 

162 

163class NotImplemented(ConstructionRenderableError): 

164 code = codes.NOT_IMPLEMENTED 

165 

166 

167class BadGateway(ConstructionRenderableError): 

168 code = codes.BAD_GATEWAY 

169 

170 

171class ServiceUnavailable(ConstructionRenderableError): 

172 code = codes.SERVICE_UNAVAILABLE 

173 

174 

175class GatewayTimeout(ConstructionRenderableError): 

176 code = codes.GATEWAY_TIMEOUT 

177 

178 

179class ProxyingNotSupported(ConstructionRenderableError): 

180 code = codes.PROXYING_NOT_SUPPORTED 

181 

182 

183class HopLimitReached(ConstructionRenderableError): 

184 code = codes.HOP_LIMIT_REACHED 

185 

186 

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 ) 

211 

212# More detailed versions of code based errors 

213 

214 

215class NoResource(NotFound): 

216 """ 

217 Raised when resource is not found. 

218 """ 

219 

220 message = "Error: Resource not found!" 

221 

222 def __init__(self): 

223 warnings.warn( 

224 "NoResource is deprecated in favor of NotFound", 

225 DeprecationWarning, 

226 stacklevel=2, 

227 ) 

228 

229 

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

235 

236 message = "Error: Method not allowed!" 

237 

238 

239class UnsupportedMethod(MethodNotAllowed): 

240 """ 

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

242 """ 

243 

244 message = "Error: Method not recognized!" 

245 

246 

247class NetworkError(HelpfulError): 

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

249 or receiving packages". 

250 

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

255 

256 def __str__(self): 

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

258 

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 " 

276 

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 

283 

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

289 

290 

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 

294 

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

297 

298 

299class ResolutionError(NetworkError): 

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

301 not possible""" 

302 

303 def __str__(self): 

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

305 

306 

307class MessageError(NetworkError): 

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

309 RST)""" 

310 

311 

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

315 

316 

317class TimeoutError(NetworkError): 

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

319 

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

321 request may have reached the server or not. 

322 """ 

323 

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

326 

327 

328class ConRetransmitsExceeded(TimeoutError): 

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

330 within its retransmission timeout. 

331 

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

333 have been received by the server. 

334 """ 

335 

336 

337class RequestTimedOut(TimeoutError): 

338 """ 

339 Raised when request is timed out. 

340 

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

345 

346 

347class WaitingForClientTimedOut(TimeoutError): 

348 """ 

349 Raised when server expects some client action: 

350 

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

352 - sending next GET request with block2 option 

353 

354 but client does nothing. 

355 

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

360 

361 

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

370 

371 def __str__(self): 

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

373 

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

376 

377 

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

383 

384 

385class UnexpectedBlock1Option(Error): 

386 """ 

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

388 """ 

389 

390 

391class UnexpectedBlock2(Error): 

392 """ 

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

394 """ 

395 

396 

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

403 

404 

405class NotObservable(Error): 

406 """ 

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

408 """ 

409 

410 

411class ObservationCancelled(Error): 

412 """ 

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

414 """ 

415 

416 

417class UnparsableMessage(Error): 

418 """ 

419 An incoming message does not look like CoAP. 

420 

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

422 beginning of the message, and a minimum length. 

423 """ 

424 

425 

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

429 

430 

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. 

435 

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

440 

441 

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

448 

449 

450class IncompleteUrlError(ValueError, HelpfulError): 

451 def __str__(self): 

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

453 

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

456 

457 

458class MissingRemoteError(HelpfulError): 

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

460 

461 def __str__(self): 

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

463 

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. 

478 

479 

480__getattr__ = util.deprecation_getattr( 

481 { 

482 "UnsupportedMediaType": "UnsupportedContentFormat", 

483 "RequestTimedOut": "TimeoutError", 

484 "WaitingForClientTimedOut": "TimeoutError", 

485 }, 

486 globals(), 

487)