1# --------------------------------------------------------------------------
2#
3# Copyright (c) Microsoft Corporation. All rights reserved.
4#
5# The MIT License (MIT)
6#
7# Permission is hereby granted, free of charge, to any person obtaining a copy
8# of this software and associated documentation files (the ""Software""), to
9# deal in the Software without restriction, including without limitation the
10# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
11# sell copies of the Software, and to permit persons to whom the Software is
12# furnished to do so, subject to the following conditions:
13#
14# The above copyright notice and this permission notice shall be included in
15# all copies or substantial portions of the Software.
16#
17# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23# IN THE SOFTWARE.
24#
25# --------------------------------------------------------------------------
26from __future__ import annotations
27import json
28import logging
29import sys
30
31from types import TracebackType
32from typing import (
33 Callable,
34 Any,
35 Optional,
36 Union,
37 Type,
38 List,
39 Mapping,
40 TypeVar,
41 Generic,
42 Dict,
43 NoReturn,
44 TYPE_CHECKING,
45)
46from typing_extensions import Protocol, runtime_checkable
47
48_LOGGER = logging.getLogger(__name__)
49
50if TYPE_CHECKING:
51 from azure.core.pipeline.policies import RequestHistory
52
53HTTPResponseType = TypeVar("HTTPResponseType")
54HTTPRequestType = TypeVar("HTTPRequestType")
55KeyType = TypeVar("KeyType")
56ValueType = TypeVar("ValueType")
57# To replace when typing.Self is available in our baseline
58SelfODataV4Format = TypeVar("SelfODataV4Format", bound="ODataV4Format")
59
60
61__all__ = [
62 "AzureError",
63 "ServiceRequestError",
64 "ServiceResponseError",
65 "HttpResponseError",
66 "DecodeError",
67 "ResourceExistsError",
68 "ResourceNotFoundError",
69 "ClientAuthenticationError",
70 "ResourceModifiedError",
71 "ResourceNotModifiedError",
72 "TooManyRedirectsError",
73 "ODataV4Format",
74 "ODataV4Error",
75 "StreamConsumedError",
76 "StreamClosedError",
77 "ResponseNotReadError",
78 "SerializationError",
79 "DeserializationError",
80]
81
82
83def raise_with_traceback(exception: Callable, *args: Any, message: str = "", **kwargs: Any) -> NoReturn:
84 """Raise exception with a specified traceback.
85 This MUST be called inside a "except" clause.
86
87 .. note:: This method is deprecated since we don't support Python 2 anymore. Use raise/from instead.
88
89 :param Exception exception: Error type to be raised.
90 :param any args: Any additional args to be included with exception.
91 :keyword str message: Message to be associated with the exception. If omitted, defaults to an empty string.
92 """
93 exc_type, exc_value, exc_traceback = sys.exc_info()
94 # If not called inside an "except", exc_type will be None. Assume it will not happen
95 if exc_type is None:
96 raise ValueError("raise_with_traceback can only be used in except clauses")
97 exc_msg = "{}, {}: {}".format(message, exc_type.__name__, exc_value)
98 error = exception(exc_msg, *args, **kwargs)
99 try:
100 raise error.with_traceback(exc_traceback) # pylint: disable=raise-missing-from
101 except AttributeError: # Python 2
102 error.__traceback__ = exc_traceback
103 raise error # pylint: disable=raise-missing-from
104
105
106@runtime_checkable
107class _HttpResponseCommonAPI(Protocol):
108 """Protocol used by exceptions for HTTP response.
109
110 As HttpResponseError uses very few properties of HttpResponse, a protocol
111 is faster and simpler than import all the possible types (at least 6).
112 """
113
114 @property
115 def reason(self) -> Optional[str]: ...
116
117 @property
118 def status_code(self) -> Optional[int]: ...
119
120 def text(self) -> str: ...
121
122 @property
123 def request(self) -> object: # object as type, since all we need is str() on it
124 ...
125
126
127class ErrorMap(Generic[KeyType, ValueType]):
128 """Error Map class. To be used in map_error method, behaves like a dictionary.
129 It returns the error type if it is found in custom_error_map. Or return default_error
130
131 :param dict custom_error_map: User-defined error map, it is used to map status codes to error types.
132 :keyword error default_error: Default error type. It is returned if the status code is not found in custom_error_map
133 """
134
135 def __init__(
136 self, # pylint: disable=unused-argument
137 custom_error_map: Optional[Mapping[KeyType, ValueType]] = None,
138 *,
139 default_error: Optional[ValueType] = None,
140 **kwargs: Any,
141 ) -> None:
142 self._custom_error_map = custom_error_map or {}
143 self._default_error = default_error
144
145 def get(self, key: KeyType) -> Optional[ValueType]:
146 ret = self._custom_error_map.get(key)
147 if ret:
148 return ret
149 return self._default_error
150
151
152def map_error(
153 status_code: int, response: _HttpResponseCommonAPI, error_map: Mapping[int, Type[HttpResponseError]]
154) -> None:
155 if not error_map:
156 return
157 error_type = error_map.get(status_code)
158 if not error_type:
159 return
160 error = error_type(response=response)
161 raise error
162
163
164class ODataV4Format:
165 """Class to describe OData V4 error format.
166
167 http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091
168
169 Example of JSON:
170
171 .. code-block:: json
172
173 {
174 "error": {
175 "code": "ValidationError",
176 "message": "One or more fields contain incorrect values: ",
177 "details": [
178 {
179 "code": "ValidationError",
180 "target": "representation",
181 "message": "Parsing error(s): String '' does not match regex pattern '^[^{}/ :]+(?: :\\\\d+)?$'.
182 Path 'host', line 1, position 297."
183 },
184 {
185 "code": "ValidationError",
186 "target": "representation",
187 "message": "Parsing error(s): The input OpenAPI file is not valid for the OpenAPI specificate
188 https: //github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md
189 (schema https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v2.0/schema.json)."
190 }
191 ]
192 }
193 }
194
195 :param dict json_object: A Python dict representing a ODataV4 JSON
196 :ivar str ~.code: Its value is a service-defined error code.
197 This code serves as a sub-status for the HTTP error code specified in the response.
198 :ivar str message: Human-readable, language-dependent representation of the error.
199 :ivar str target: The target of the particular error (for example, the name of the property in error).
200 This field is optional and may be None.
201 :ivar list[ODataV4Format] details: Array of ODataV4Format instances that MUST contain name/value pairs
202 for code and message, and MAY contain a name/value pair for target, as described above.
203 :ivar dict innererror: An object. The contents of this object are service-defined.
204 Usually this object contains information that will help debug the service.
205 """
206
207 CODE_LABEL = "code"
208 MESSAGE_LABEL = "message"
209 TARGET_LABEL = "target"
210 DETAILS_LABEL = "details"
211 INNERERROR_LABEL = "innererror"
212
213 def __init__(self, json_object: Mapping[str, Any]) -> None:
214 if "error" in json_object:
215 json_object = json_object["error"]
216 cls: Type[ODataV4Format] = self.__class__
217
218 # Required fields, but assume they could be missing still to be robust
219 self.code: Optional[str] = json_object.get(cls.CODE_LABEL)
220 self.message: Optional[str] = json_object.get(cls.MESSAGE_LABEL)
221
222 if not (self.code or self.message):
223 raise ValueError("Impossible to extract code/message from received JSON:\n" + json.dumps(json_object))
224
225 # Optional fields
226 self.target: Optional[str] = json_object.get(cls.TARGET_LABEL)
227
228 # details is recursive of this very format
229 self.details: List[ODataV4Format] = []
230 for detail_node in json_object.get(cls.DETAILS_LABEL) or []:
231 try:
232 self.details.append(self.__class__(detail_node))
233 except Exception: # pylint: disable=broad-except
234 pass
235
236 self.innererror: Mapping[str, Any] = json_object.get(cls.INNERERROR_LABEL, {})
237
238 @property
239 def error(self: SelfODataV4Format) -> SelfODataV4Format:
240 import warnings
241
242 warnings.warn(
243 "error.error from azure exceptions is deprecated, just simply use 'error' once",
244 DeprecationWarning,
245 )
246 return self
247
248 def __str__(self) -> str:
249 return "({}) {}\n{}".format(self.code, self.message, self.message_details())
250
251 def message_details(self) -> str:
252 """Return a detailed string of the error.
253
254 :return: A string with the details of the error.
255 :rtype: str
256 """
257 error_str = "Code: {}".format(self.code)
258 error_str += "\nMessage: {}".format(self.message)
259 if self.target:
260 error_str += "\nTarget: {}".format(self.target)
261
262 if self.details:
263 error_str += "\nException Details:"
264 for error_obj in self.details:
265 # Indent for visibility
266 error_str += "\n".join("\t" + s for s in str(error_obj).splitlines())
267
268 if self.innererror:
269 error_str += "\nInner error: {}".format(json.dumps(self.innererror, indent=4))
270 return error_str
271
272
273class AzureError(Exception):
274 """Base exception for all errors.
275
276 :param object message: The message object stringified as 'message' attribute
277 :keyword error: The original exception if any
278 :paramtype error: Exception
279
280 :ivar inner_exception: The exception passed with the 'error' kwarg
281 :vartype inner_exception: Exception
282 :ivar exc_type: The exc_type from sys.exc_info()
283 :ivar exc_value: The exc_value from sys.exc_info()
284 :ivar exc_traceback: The exc_traceback from sys.exc_info()
285 :ivar exc_msg: A string formatting of message parameter, exc_type and exc_value
286 :ivar str message: A stringified version of the message parameter
287 :ivar str continuation_token: A token reference to continue an incomplete operation. This value is optional
288 and will be `None` where continuation is either unavailable or not applicable.
289 """
290
291 def __init__(self, message: Optional[object], *args: Any, **kwargs: Any) -> None:
292 self.inner_exception: Optional[BaseException] = kwargs.get("error")
293
294 exc_info = sys.exc_info()
295 self.exc_type: Optional[Type[Any]] = exc_info[0]
296 self.exc_value: Optional[BaseException] = exc_info[1]
297 self.exc_traceback: Optional[TracebackType] = exc_info[2]
298
299 self.exc_type = self.exc_type if self.exc_type else type(self.inner_exception)
300 self.exc_msg: str = "{}, {}: {}".format(message, self.exc_type.__name__, self.exc_value)
301 self.message: str = str(message)
302 self.continuation_token: Optional[str] = kwargs.get("continuation_token")
303 super(AzureError, self).__init__(self.message, *args)
304
305 def raise_with_traceback(self) -> None:
306 """Raise the exception with the existing traceback.
307
308 .. deprecated:: 1.22.0
309 This method is deprecated as we don't support Python 2 anymore. Use raise/from instead.
310 """
311 try:
312 raise super(AzureError, self).with_traceback(self.exc_traceback) # pylint: disable=raise-missing-from
313 except AttributeError:
314 self.__traceback__: Optional[TracebackType] = self.exc_traceback
315 raise self # pylint: disable=raise-missing-from
316
317
318class ServiceRequestError(AzureError):
319 """An error occurred while attempt to make a request to the service.
320 No request was sent.
321 """
322
323
324class ServiceResponseError(AzureError):
325 """The request was sent, but the client failed to understand the response.
326 The connection may have timed out. These errors can be retried for idempotent or
327 safe operations"""
328
329
330class ServiceRequestTimeoutError(ServiceRequestError):
331 """Error raised when timeout happens"""
332
333
334class ServiceResponseTimeoutError(ServiceResponseError):
335 """Error raised when timeout happens"""
336
337
338class HttpResponseError(AzureError):
339 """A request was made, and a non-success status code was received from the service.
340
341 :param object message: The message object stringified as 'message' attribute
342 :param response: The response that triggered the exception.
343 :type response: ~azure.core.pipeline.transport.HttpResponse or ~azure.core.pipeline.transport.AsyncHttpResponse
344
345 :ivar reason: The HTTP response reason
346 :vartype reason: str
347 :ivar status_code: HttpResponse's status code
348 :vartype status_code: int
349 :ivar response: The response that triggered the exception.
350 :vartype response: ~azure.core.pipeline.transport.HttpResponse or ~azure.core.pipeline.transport.AsyncHttpResponse
351 :ivar model: The request body/response body model
352 :vartype model: ~msrest.serialization.Model
353 :ivar error: The formatted error
354 :vartype error: ODataV4Format
355 """
356
357 def __init__(
358 self, message: Optional[object] = None, response: Optional[_HttpResponseCommonAPI] = None, **kwargs: Any
359 ) -> None:
360 # Don't want to document this one yet.
361 error_format = kwargs.get("error_format", ODataV4Format)
362
363 self.reason: Optional[str] = None
364 self.status_code: Optional[int] = None
365 self.response: Optional[_HttpResponseCommonAPI] = response
366 if response:
367 self.reason = response.reason
368 self.status_code = response.status_code
369
370 # old autorest are setting "error" before calling __init__, so it might be there already
371 # transferring into self.model
372 model: Optional[Any] = kwargs.pop("model", None)
373 self.model: Optional[Any]
374 if model is not None: # autorest v5
375 self.model = model
376 else: # autorest azure-core, for KV 1.0, Storage 12.0, etc.
377 self.model = getattr(self, "error", None)
378 self.error: Optional[ODataV4Format] = self._parse_odata_body(error_format, response)
379
380 # By priority, message is:
381 # - odatav4 message, OR
382 # - parameter "message", OR
383 # - generic meassage using "reason"
384 if self.error:
385 message = str(self.error)
386 else:
387 message = message or "Operation returned an invalid status '{}'".format(self.reason)
388
389 super(HttpResponseError, self).__init__(message=message, **kwargs)
390
391 @staticmethod
392 def _parse_odata_body(
393 error_format: Type[ODataV4Format], response: Optional[_HttpResponseCommonAPI]
394 ) -> Optional[ODataV4Format]:
395 try:
396 # https://github.com/python/mypy/issues/14743#issuecomment-1664725053
397 odata_json = json.loads(response.text()) # type: ignore
398 return error_format(odata_json)
399 except Exception: # pylint: disable=broad-except
400 # If the body is not JSON valid, just stop now
401 pass
402 return None
403
404 def __str__(self) -> str:
405 retval = super(HttpResponseError, self).__str__()
406 try:
407 # https://github.com/python/mypy/issues/14743#issuecomment-1664725053
408 body = self.response.text() # type: ignore
409 if body and not self.error:
410 return "{}\nContent: {}".format(retval, body)[:2048]
411 except Exception: # pylint: disable=broad-except
412 pass
413 return retval
414
415
416class DecodeError(HttpResponseError):
417 """Error raised during response deserialization."""
418
419
420class IncompleteReadError(DecodeError):
421 """Error raised if peer closes the connection before we have received the complete message body."""
422
423
424class ResourceExistsError(HttpResponseError):
425 """An error response with status code 4xx.
426 This will not be raised directly by the Azure core pipeline."""
427
428
429class ResourceNotFoundError(HttpResponseError):
430 """An error response, typically triggered by a 412 response (for update) or 404 (for get/post)"""
431
432
433class ClientAuthenticationError(HttpResponseError):
434 """An error response with status code 4xx.
435 This will not be raised directly by the Azure core pipeline."""
436
437
438class ResourceModifiedError(HttpResponseError):
439 """An error response with status code 4xx, typically 412 Conflict.
440 This will not be raised directly by the Azure core pipeline."""
441
442
443class ResourceNotModifiedError(HttpResponseError):
444 """An error response with status code 304.
445 This will not be raised directly by the Azure core pipeline."""
446
447
448class TooManyRedirectsError(HttpResponseError, Generic[HTTPRequestType, HTTPResponseType]):
449 """Reached the maximum number of redirect attempts.
450
451 :param history: The history of requests made while trying to fulfill the request.
452 :type history: list[~azure.core.pipeline.policies.RequestHistory]
453 """
454
455 def __init__(
456 self, history: "List[RequestHistory[HTTPRequestType, HTTPResponseType]]", *args: Any, **kwargs: Any
457 ) -> None:
458 self.history = history
459 message = "Reached maximum redirect attempts."
460 super(TooManyRedirectsError, self).__init__(message, *args, **kwargs)
461
462
463class ODataV4Error(HttpResponseError):
464 """An HTTP response error where the JSON is decoded as OData V4 error format.
465
466 http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091
467
468 :param ~azure.core.rest.HttpResponse response: The response object.
469 :ivar dict odata_json: The parsed JSON body as attribute for convenience.
470 :ivar str ~.code: Its value is a service-defined error code.
471 This code serves as a sub-status for the HTTP error code specified in the response.
472 :ivar str message: Human-readable, language-dependent representation of the error.
473 :ivar str target: The target of the particular error (for example, the name of the property in error).
474 This field is optional and may be None.
475 :ivar list[ODataV4Format] details: Array of ODataV4Format instances that MUST contain name/value pairs
476 for code and message, and MAY contain a name/value pair for target, as described above.
477 :ivar dict innererror: An object. The contents of this object are service-defined.
478 Usually this object contains information that will help debug the service.
479 """
480
481 _ERROR_FORMAT = ODataV4Format
482
483 def __init__(self, response: _HttpResponseCommonAPI, **kwargs: Any) -> None:
484 # Ensure field are declared, whatever can happen afterwards
485 self.odata_json: Optional[Dict[str, Any]] = None
486 try:
487 self.odata_json = json.loads(response.text())
488 odata_message = self.odata_json.setdefault("error", {}).get("message")
489 except Exception: # pylint: disable=broad-except
490 # If the body is not JSON valid, just stop now
491 odata_message = None
492
493 self.code: Optional[str] = None
494 message: Optional[str] = kwargs.get("message", odata_message)
495 self.target: Optional[str] = None
496 self.details: Optional[List[Any]] = []
497 self.innererror: Optional[Mapping[str, Any]] = {}
498
499 if message and "message" not in kwargs:
500 kwargs["message"] = message
501
502 super(ODataV4Error, self).__init__(response=response, **kwargs)
503
504 self._error_format: Optional[Union[str, ODataV4Format]] = None
505 if self.odata_json:
506 try:
507 error_node = self.odata_json["error"]
508 self._error_format = self._ERROR_FORMAT(error_node)
509 self.__dict__.update({k: v for k, v in self._error_format.__dict__.items() if v is not None})
510 except Exception: # pylint: disable=broad-except
511 _LOGGER.info("Received error message was not valid OdataV4 format.")
512 self._error_format = "JSON was invalid for format " + str(self._ERROR_FORMAT)
513
514 def __str__(self) -> str:
515 if self._error_format:
516 return str(self._error_format)
517 return super(ODataV4Error, self).__str__()
518
519
520class StreamConsumedError(AzureError):
521 """Error thrown if you try to access the stream of a response once consumed.
522
523 It is thrown if you try to read / stream an ~azure.core.rest.HttpResponse or
524 ~azure.core.rest.AsyncHttpResponse once the response's stream has been consumed.
525
526 :param response: The response that triggered the exception.
527 :type response: ~azure.core.rest.HttpResponse or ~azure.core.rest.AsyncHttpResponse
528 """
529
530 def __init__(self, response: _HttpResponseCommonAPI) -> None:
531 message = (
532 "You are attempting to read or stream the content from request {}. "
533 "You have likely already consumed this stream, so it can not be accessed anymore.".format(response.request)
534 )
535 super(StreamConsumedError, self).__init__(message)
536
537
538class StreamClosedError(AzureError):
539 """Error thrown if you try to access the stream of a response once closed.
540
541 It is thrown if you try to read / stream an ~azure.core.rest.HttpResponse or
542 ~azure.core.rest.AsyncHttpResponse once the response's stream has been closed.
543
544 :param response: The response that triggered the exception.
545 :type response: ~azure.core.rest.HttpResponse or ~azure.core.rest.AsyncHttpResponse
546 """
547
548 def __init__(self, response: _HttpResponseCommonAPI) -> None:
549 message = (
550 "The content for response from request {} can no longer be read or streamed, since the "
551 "response has already been closed.".format(response.request)
552 )
553 super(StreamClosedError, self).__init__(message)
554
555
556class ResponseNotReadError(AzureError):
557 """Error thrown if you try to access a response's content without reading first.
558
559 It is thrown if you try to access an ~azure.core.rest.HttpResponse or
560 ~azure.core.rest.AsyncHttpResponse's content without first reading the response's bytes in first.
561
562 :param response: The response that triggered the exception.
563 :type response: ~azure.core.rest.HttpResponse or ~azure.core.rest.AsyncHttpResponse
564 """
565
566 def __init__(self, response: _HttpResponseCommonAPI) -> None:
567 message = (
568 "You have not read in the bytes for the response from request {}. "
569 "Call .read() on the response first.".format(response.request)
570 )
571 super(ResponseNotReadError, self).__init__(message)
572
573
574class SerializationError(ValueError):
575 """Raised if an error is encountered during serialization."""
576
577
578class DeserializationError(ValueError):
579 """Raised if an error is encountered during deserialization."""