Coverage for aiocoap/error.py: 84%

153 statements  

« prev     ^ index     » next       coverage.py v7.6.3, created at 2024-10-15 22:10 +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 ResolutionError(NetworkError): 

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

275 not possible""" 

276 

277 def __str__(self): 

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

279 

280 

281class MessageError(NetworkError): 

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

283 RST)""" 

284 

285 

286class RemoteServerShutdown(NetworkError): 

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

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

289 

290 

291class TimeoutError(NetworkError): 

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

293 

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

295 request may have reached the server or not. 

296 """ 

297 

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

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

300 

301 

302class ConRetransmitsExceeded(TimeoutError): 

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

304 within its retransmission timeout. 

305 

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

307 have been received by the server. 

308 """ 

309 

310 

311class RequestTimedOut(TimeoutError): 

312 """ 

313 Raised when request is timed out. 

314 

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

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

317 introduced later. 

318 """ 

319 

320 

321class WaitingForClientTimedOut(TimeoutError): 

322 """ 

323 Raised when server expects some client action: 

324 

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

326 - sending next GET request with block2 option 

327 

328 but client does nothing. 

329 

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

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

332 introduced later. 

333 """ 

334 

335 

336class ResourceChanged(Error): 

337 """ 

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

339 not be received in a consistent state. 

340 """ 

341 

342 

343class UnexpectedBlock1Option(Error): 

344 """ 

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

346 """ 

347 

348 

349class UnexpectedBlock2(Error): 

350 """ 

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

352 """ 

353 

354 

355class MissingBlock2Option(Error): 

356 """ 

357 Raised when response with Block2 option is expected 

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

359 but response without Block2 option is received. 

360 """ 

361 

362 

363class NotObservable(Error): 

364 """ 

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

366 """ 

367 

368 

369class ObservationCancelled(Error): 

370 """ 

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

372 """ 

373 

374 

375class UnparsableMessage(Error): 

376 """ 

377 An incoming message does not look like CoAP. 

378 

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

380 beginning of the message, and a minimum length. 

381 """ 

382 

383 

384class LibraryShutdown(Error): 

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

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

387 

388 

389class AnonymousHost(Error): 

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

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

392 than this. 

393 

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

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

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

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

398 

399 

400class MalformedUrlError(ValueError, HelpfulError): 

401 def __str__(self): 

402 if self.args: 

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

404 else: 

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

406 

407 

408class IncompleteUrlError(ValueError, HelpfulError): 

409 def __str__(self): 

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

411 

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

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

414 

415 

416class MissingRemoteError(HelpfulError): 

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

418 

419 def __str__(self): 

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

421 

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

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

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

425 if requested_message and ( 

426 requested_message.opt.proxy_uri or requested_message.opt.proxy_scheme 

427 ): 

428 if original_uri: 

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

430 else: 

431 return ( 

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

433 ) 

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

435 # rather manual way. 

436 

437 

438__getattr__ = util.deprecation_getattr( 

439 { 

440 "UnsupportedMediaType": "UnsupportedContentFormat", 

441 "RequestTimedOut": "TimeoutError", 

442 "WaitingForClientTimedOut": "TimeoutError", 

443 }, 

444 globals(), 

445)