Coverage for src/aiocoap/error.py: 0%

158 statements  

« prev     ^ index     » next       coverage.py v7.7.0, created at 2025-03-20 17:26 +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 

13 

14from .numbers import codes 

15from . import util 

16 

17 

18class Error(Exception): 

19 """ 

20 Base exception for all exceptions that indicate a failed request 

21 """ 

22 

23 

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__ 

30 

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 

35 

36 The `hints` dictonary may be populated with context that the caller 

37 has; the implementation must tolerate their absence. Currently 

38 established keys: 

39 

40 * original_uri (str): URI that was attemted to access 

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

42 """ 

43 return None 

44 

45 

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

47 """ 

48 Exception that can meaningfully be represented in a CoAP response 

49 """ 

50 

51 @abc.abstractmethod 

52 def to_message(self): 

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

54 rendered""" 

55 

56 

57class ResponseWrappingError(Error): 

58 """ 

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

60 

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

65 

66 def __init__(self, coapmessage): 

67 self.coapmessage = coapmessage 

68 

69 def to_message(self): 

70 return self.coapmessage 

71 

72 def __repr__(self): 

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

74 type(self).__name__, 

75 self.coapmessage.code, 

76 self.coapmessage.payload, 

77 ) 

78 

79 

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

85 

86 def __init__(self, message=None): 

87 if message is not None: 

88 self.message = message 

89 

90 def to_message(self): 

91 from .message import Message 

92 

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

94 

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

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

97 

98 

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 

103 

104 

105class Unauthorized(ConstructionRenderableError): 

106 code = codes.UNAUTHORIZED 

107 

108 

109class BadOption(ConstructionRenderableError): 

110 code = codes.BAD_OPTION 

111 

112 

113class Forbidden(ConstructionRenderableError): 

114 code = codes.FORBIDDEN 

115 

116 

117class NotFound(ConstructionRenderableError): 

118 code = codes.NOT_FOUND 

119 

120 

121class MethodNotAllowed(ConstructionRenderableError): 

122 code = codes.METHOD_NOT_ALLOWED 

123 

124 

125class NotAcceptable(ConstructionRenderableError): 

126 code = codes.NOT_ACCEPTABLE 

127 

128 

129class RequestEntityIncomplete(ConstructionRenderableError): 

130 code = codes.REQUEST_ENTITY_INCOMPLETE 

131 

132 

133class Conflict(ConstructionRenderableError): 

134 code = codes.CONFLICT 

135 

136 

137class PreconditionFailed(ConstructionRenderableError): 

138 code = codes.PRECONDITION_FAILED 

139 

140 

141class RequestEntityTooLarge(ConstructionRenderableError): 

142 code = codes.REQUEST_ENTITY_TOO_LARGE 

143 

144 

145class UnsupportedContentFormat(ConstructionRenderableError): 

146 code = codes.UNSUPPORTED_CONTENT_FORMAT 

147 

148 

149class UnprocessableEntity(ConstructionRenderableError): 

150 code = codes.UNPROCESSABLE_ENTITY 

151 

152 

153class TooManyRequests(ConstructionRenderableError): 

154 code = codes.TOO_MANY_REQUESTS 

155 

156 

157class InternalServerError(ConstructionRenderableError): 

158 code = codes.INTERNAL_SERVER_ERROR 

159 

160 

161class NotImplemented(ConstructionRenderableError): 

162 code = codes.NOT_IMPLEMENTED 

163 

164 

165class BadGateway(ConstructionRenderableError): 

166 code = codes.BAD_GATEWAY 

167 

168 

169class ServiceUnavailable(ConstructionRenderableError): 

170 code = codes.SERVICE_UNAVAILABLE 

171 

172 

173class GatewayTimeout(ConstructionRenderableError): 

174 code = codes.GATEWAY_TIMEOUT 

175 

176 

177class ProxyingNotSupported(ConstructionRenderableError): 

178 code = codes.PROXYING_NOT_SUPPORTED 

179 

180 

181class HopLimitReached(ConstructionRenderableError): 

182 code = codes.HOP_LIMIT_REACHED 

183 

184 

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 ) 

209 

210# More detailed versions of code based errors 

211 

212 

213class NoResource(NotFound): 

214 """ 

215 Raised when resource is not found. 

216 """ 

217 

218 message = "Error: Resource not found!" 

219 

220 def __init__(self): 

221 warnings.warn( 

222 "NoResource is deprecated in favor of NotFound", 

223 DeprecationWarning, 

224 stacklevel=2, 

225 ) 

226 

227 

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

233 

234 message = "Error: Method not allowed!" 

235 

236 

237class UnsupportedMethod(MethodNotAllowed): 

238 """ 

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

240 """ 

241 

242 message = "Error: Method not recognized!" 

243 

244 

245class NetworkError(HelpfulError): 

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

247 or receiving packages". 

248 

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

253 

254 def __str__(self): 

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

256 

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

271 

272 

273class NoRequestInterface(RuntimeError, ConstructionRenderableError, NetworkError): 

274 code = codes.PROXYING_NOT_SUPPORTED 

275 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 

276 

277 def extra_help(self, hints={}): 

278 return "More transports may be enabled by installing extra optional dependencies. Alternatively, consider using a CoAP-CoAP proxy." 

279 

280 

281class ResolutionError(NetworkError): 

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

283 not possible""" 

284 

285 def __str__(self): 

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

287 

288 

289class MessageError(NetworkError): 

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

291 RST)""" 

292 

293 

294class RemoteServerShutdown(NetworkError): 

295 """The peer a request was sent to in a stateful connection closed the 

296 connection around the time the request was sent""" 

297 

298 

299class TimeoutError(NetworkError): 

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

301 

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

303 request may have reached the server or not. 

304 """ 

305 

306 def extra_help(self, hints={}): 

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

308 

309 

310class ConRetransmitsExceeded(TimeoutError): 

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

312 within its retransmission timeout. 

313 

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

315 have been received by the server. 

316 """ 

317 

318 

319class RequestTimedOut(TimeoutError): 

320 """ 

321 Raised when request is timed out. 

322 

323 This error is currently not produced by aiocoap; it is deprecated. Users 

324 can now catch error.TimeoutError, or newer more detailed subtypes 

325 introduced later. 

326 """ 

327 

328 

329class WaitingForClientTimedOut(TimeoutError): 

330 """ 

331 Raised when server expects some client action: 

332 

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

334 - sending next GET request with block2 option 

335 

336 but client does nothing. 

337 

338 This error is currently not produced by aiocoap; it is deprecated. Users 

339 can now catch error.TimeoutError, or newer more detailed subtypes 

340 introduced later. 

341 """ 

342 

343 

344class ResourceChanged(Error): 

345 """ 

346 The requested resource was modified during the request and could therefore 

347 not be received in a consistent state. 

348 """ 

349 

350 

351class UnexpectedBlock1Option(Error): 

352 """ 

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

354 """ 

355 

356 

357class UnexpectedBlock2(Error): 

358 """ 

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

360 """ 

361 

362 

363class MissingBlock2Option(Error): 

364 """ 

365 Raised when response with Block2 option is expected 

366 (previous response had Block2 option with More flag set), 

367 but response without Block2 option is received. 

368 """ 

369 

370 

371class NotObservable(Error): 

372 """ 

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

374 """ 

375 

376 

377class ObservationCancelled(Error): 

378 """ 

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

380 """ 

381 

382 

383class UnparsableMessage(Error): 

384 """ 

385 An incoming message does not look like CoAP. 

386 

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

388 beginning of the message, and a minimum length. 

389 """ 

390 

391 

392class LibraryShutdown(Error): 

393 """The library or a transport registered with it was requested to shut 

394 down; this error is raised in all outstanding requests.""" 

395 

396 

397class AnonymousHost(Error): 

398 """This is raised when it is attempted to express as a reference a (base) 

399 URI of a host or a resource that can not be reached by any process other 

400 than this. 

401 

402 Typically, this happens when trying to serialize a link to a resource that 

403 is hosted on a CoAP-over-TCP or -WebSockets client: Such resources can be 

404 accessed for as long as the connection is active, but can not be used any 

405 more once it is closed or even by another system.""" 

406 

407 

408class MalformedUrlError(ValueError, HelpfulError): 

409 def __str__(self): 

410 if self.args: 

411 return f"Malformed URL: {self.args[0]}" 

412 else: 

413 return f"Malformed URL: {self.__cause__}" 

414 

415 

416class IncompleteUrlError(ValueError, HelpfulError): 

417 def __str__(self): 

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

419 

420 def extra_help(self, hints={}): 

421 return "Most URLs in aiocoap need to be given with a scheme, eg. the 'coap' in 'coap://example.com/path'." 

422 

423 

424class MissingRemoteError(HelpfulError): 

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

426 

427 def __str__(self): 

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

429 

430 def extra_help(self, hints={}): 

431 original_uri = hints.get("original_uri", None) 

432 requested_message = hints.get("request", None) 

433 if requested_message and ( 

434 requested_message.opt.proxy_uri or requested_message.opt.proxy_scheme 

435 ): 

436 if original_uri: 

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

438 else: 

439 return ( 

440 "The message is set up for use with a proxy, but no proxy was set." 

441 ) 

442 # Nothing helpful otherwise: The message was probably constructed in a 

443 # rather manual way. 

444 

445 

446__getattr__ = util.deprecation_getattr( 

447 { 

448 "UnsupportedMediaType": "UnsupportedContentFormat", 

449 "RequestTimedOut": "TimeoutError", 

450 "WaitingForClientTimedOut": "TimeoutError", 

451 }, 

452 globals(), 

453)