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)
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,
154 response: _HttpResponseCommonAPI,
155 error_map: Mapping[int, Type[HttpResponseError]],
156) -> None:
157 if not error_map:
158 return
159 error_type = error_map.get(status_code)
160 if not error_type:
161 return
162 error = error_type(response=response)
163 raise error
164
165
166class ODataV4Format:
167 """Class to describe OData V4 error format.
168
169 http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091
170
171 Example of JSON:
172
173 .. code-block:: json
174
175 {
176 "error": {
177 "code": "ValidationError",
178 "message": "One or more fields contain incorrect values: ",
179 "details": [
180 {
181 "code": "ValidationError",
182 "target": "representation",
183 "message": "Parsing error(s): String '' does not match regex pattern '^[^{}/ :]+(?: :\\\\d+)?$'.
184 Path 'host', line 1, position 297."
185 },
186 {
187 "code": "ValidationError",
188 "target": "representation",
189 "message": "Parsing error(s): The input OpenAPI file is not valid for the OpenAPI specificate
190 https: //github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md
191 (schema https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v2.0/schema.json)."
192 }
193 ]
194 }
195 }
196
197 :param dict json_object: A Python dict representing a ODataV4 JSON
198 :ivar str code: Its value is a service-defined error code.
199 This code serves as a sub-status for the HTTP error code specified in the response.
200 :ivar str message: Human-readable, language-dependent representation of the error.
201 :ivar str target: The target of the particular error (for example, the name of the property in error).
202 This field is optional and may be None.
203 :ivar list[ODataV4Format] details: Array of ODataV4Format instances that MUST contain name/value pairs
204 for code and message, and MAY contain a name/value pair for target, as described above.
205 :ivar dict innererror: An object. The contents of this object are service-defined.
206 Usually this object contains information that will help debug the service.
207 """
208
209 CODE_LABEL = "code"
210 MESSAGE_LABEL = "message"
211 TARGET_LABEL = "target"
212 DETAILS_LABEL = "details"
213 INNERERROR_LABEL = "innererror"
214
215 def __init__(self, json_object: Mapping[str, Any]) -> None:
216 if "error" in json_object:
217 json_object = json_object["error"]
218 cls: Type[ODataV4Format] = self.__class__
219
220 # Required fields, but assume they could be missing still to be robust
221 self.code: Optional[str] = json_object.get(cls.CODE_LABEL)
222 self.message: Optional[str] = json_object.get(cls.MESSAGE_LABEL)
223
224 if not (self.code or self.message):
225 raise ValueError("Impossible to extract code/message from received JSON:\n" + json.dumps(json_object))
226
227 # Optional fields
228 self.target: Optional[str] = json_object.get(cls.TARGET_LABEL)
229
230 # details is recursive of this very format
231 self.details: List[ODataV4Format] = []
232 for detail_node in json_object.get(cls.DETAILS_LABEL) or []:
233 try:
234 self.details.append(self.__class__(detail_node))
235 except Exception: # pylint: disable=broad-except
236 pass
237
238 self.innererror: Mapping[str, Any] = json_object.get(cls.INNERERROR_LABEL, {})
239
240 def as_dict(self, *, exclude_readonly: bool = False) -> Dict[str, Any]:
241 """Return the parsed error as a JSON-compatible dictionary.
242
243 :keyword bool exclude_readonly: Whether to exclude readonly fields.
244 :return: A dictionary representing the error.
245 :rtype: dict[str, Any]
246 """
247
248 def as_dict_value(value: Any) -> Any:
249 as_dict = getattr(value, "as_dict", None)
250 if callable(as_dict):
251 return as_dict(exclude_readonly=exclude_readonly)
252 if isinstance(value, Mapping):
253 return {key: as_dict_value(item) for key, item in value.items()}
254 if isinstance(value, list):
255 return [as_dict_value(item) for item in value]
256 return value
257
258 return {
259 self.CODE_LABEL: self.code,
260 self.MESSAGE_LABEL: self.message,
261 self.TARGET_LABEL: self.target,
262 self.DETAILS_LABEL: as_dict_value(self.details),
263 self.INNERERROR_LABEL: as_dict_value(self.innererror),
264 }
265
266 @property
267 def error(self: SelfODataV4Format) -> SelfODataV4Format:
268 import warnings
269
270 warnings.warn(
271 "error.error from azure exceptions is deprecated, just simply use 'error' once",
272 DeprecationWarning,
273 )
274 return self
275
276 def __str__(self) -> str:
277 return "({}) {}\n{}".format(self.code, self.message, self.message_details())
278
279 def message_details(self) -> str:
280 """Return a detailed string of the error.
281
282 :return: A string with the details of the error.
283 :rtype: str
284 """
285 error_str = "Code: {}".format(self.code)
286 error_str += "\nMessage: {}".format(self.message)
287 if self.target:
288 error_str += "\nTarget: {}".format(self.target)
289
290 if self.details:
291 error_str += "\nException Details:"
292 for error_obj in self.details:
293 # Indent for visibility
294 error_str += "\n".join("\t" + s for s in str(error_obj).splitlines())
295
296 if self.innererror:
297 error_str += "\nInner error: {}".format(json.dumps(self.innererror, indent=4))
298 return error_str
299
300
301class AzureError(Exception):
302 """Base exception for all errors.
303
304 :param object message: The message object stringified as 'message' attribute
305 :keyword error: The original exception if any
306 :paramtype error: Exception
307
308 :ivar inner_exception: The exception passed with the 'error' kwarg
309 :vartype inner_exception: Exception
310 :ivar exc_type: The exc_type from sys.exc_info()
311 :ivar exc_value: The exc_value from sys.exc_info()
312 :ivar exc_traceback: The exc_traceback from sys.exc_info()
313 :ivar exc_msg: A string formatting of message parameter, exc_type and exc_value
314 :ivar str message: A stringified version of the message parameter
315 :ivar str continuation_token: A token reference to continue an incomplete operation. This value is optional
316 and will be `None` where continuation is either unavailable or not applicable.
317 """
318
319 def __init__(self, message: Optional[object], *args: Any, **kwargs: Any) -> None:
320 self.inner_exception: Optional[BaseException] = kwargs.get("error")
321
322 exc_info = sys.exc_info()
323 self.exc_type: Optional[Type[Any]] = exc_info[0]
324 self.exc_value: Optional[BaseException] = exc_info[1]
325 self.exc_traceback: Optional[TracebackType] = exc_info[2]
326
327 self.exc_type = self.exc_type if self.exc_type else type(self.inner_exception)
328 self.exc_msg: str = "{}, {}: {}".format(message, self.exc_type.__name__, self.exc_value)
329 self.message: str = str(message)
330 self.continuation_token: Optional[str] = kwargs.get("continuation_token")
331 super(AzureError, self).__init__(self.message, *args)
332
333 def raise_with_traceback(self) -> None:
334 """Raise the exception with the existing traceback.
335
336 .. deprecated:: 1.22.0
337 This method is deprecated as we don't support Python 2 anymore. Use raise/from instead.
338 """
339 try:
340 raise super(AzureError, self).with_traceback(self.exc_traceback)
341 except AttributeError:
342 self.__traceback__: Optional[TracebackType] = self.exc_traceback
343 raise self # pylint: disable=raise-missing-from
344
345
346class ServiceRequestError(AzureError):
347 """An error occurred while attempt to make a request to the service.
348 No request was sent.
349 """
350
351
352class ServiceResponseError(AzureError):
353 """The request was sent, but the client failed to understand the response.
354 The connection may have timed out. These errors can be retried for idempotent or
355 safe operations"""
356
357
358class ServiceRequestTimeoutError(ServiceRequestError):
359 """Error raised when timeout happens"""
360
361
362class ServiceResponseTimeoutError(ServiceResponseError):
363 """Error raised when timeout happens"""
364
365
366class HttpResponseError(AzureError):
367 """A request was made, and a non-success status code was received from the service.
368
369 :param object message: The message object stringified as 'message' attribute
370 :param response: The response that triggered the exception.
371 :type response: ~azure.core.pipeline.transport.HttpResponse or ~azure.core.pipeline.transport.AsyncHttpResponse
372
373 :ivar reason: The HTTP response reason
374 :vartype reason: str
375 :ivar status_code: HttpResponse's status code
376 :vartype status_code: int
377 :ivar response: The response that triggered the exception.
378 :vartype response: ~azure.core.pipeline.transport.HttpResponse or ~azure.core.pipeline.transport.AsyncHttpResponse
379 :ivar model: The request body/response body model
380 :vartype model: ~msrest.serialization.Model
381 :ivar error: The formatted error
382 :vartype error: ODataV4Format
383 """
384
385 def __init__(
386 self,
387 message: Optional[object] = None,
388 response: Optional[_HttpResponseCommonAPI] = None,
389 **kwargs: Any,
390 ) -> None:
391 # Don't want to document this one yet.
392 error_format = kwargs.get("error_format", ODataV4Format)
393
394 self.reason: Optional[str] = None
395 self.status_code: Optional[int] = None
396 self.response: Optional[_HttpResponseCommonAPI] = response
397 if response:
398 self.reason = response.reason
399 self.status_code = response.status_code
400
401 # old autorest are setting "error" before calling __init__, so it might be there already
402 # transferring into self.model
403 model: Optional[Any] = kwargs.pop("model", None)
404 self.model: Optional[Any]
405 if model is not None: # autorest v5
406 self.model = model
407 else: # autorest azure-core, for KV 1.0, Storage 12.0, etc.
408 self.model = getattr(self, "error", None)
409 self.error: Optional[ODataV4Format] = self._parse_odata_body(error_format, response)
410
411 # By priority, message is:
412 # - odatav4 message, OR
413 # - parameter "message", OR
414 # - generic meassage using "reason"
415 if self.error:
416 message = str(self.error)
417 else:
418 message = message or "Operation returned an invalid status '{}'".format(self.reason)
419
420 super(HttpResponseError, self).__init__(message=message, **kwargs)
421
422 @staticmethod
423 def _parse_odata_body(
424 error_format: Type[ODataV4Format], response: Optional[_HttpResponseCommonAPI]
425 ) -> Optional[ODataV4Format]:
426 try:
427 # https://github.com/python/mypy/issues/14743#issuecomment-1664725053
428 odata_json = json.loads(response.text()) # type: ignore
429 return error_format(odata_json)
430 except Exception: # pylint: disable=broad-except
431 # If the body is not JSON valid, just stop now
432 pass
433 return None
434
435 def __str__(self) -> str:
436 retval = super(HttpResponseError, self).__str__()
437 try:
438 # https://github.com/python/mypy/issues/14743#issuecomment-1664725053
439 body = self.response.text() # type: ignore
440 if body and not self.error:
441 return "{}\nContent: {}".format(retval, body)[:2048]
442 except Exception: # pylint: disable=broad-except
443 pass
444 return retval
445
446
447class DecodeError(HttpResponseError):
448 """Error raised during response deserialization."""
449
450
451class IncompleteReadError(DecodeError):
452 """Error raised if peer closes the connection before we have received the complete message body."""
453
454
455class ResourceExistsError(HttpResponseError):
456 """An error response with status code 4xx.
457 This will not be raised directly by the Azure core pipeline."""
458
459
460class ResourceNotFoundError(HttpResponseError):
461 """An error response, typically triggered by a 412 response (for update) or 404 (for get/post)"""
462
463
464class ClientAuthenticationError(HttpResponseError):
465 """An error response with status code 4xx.
466 This will not be raised directly by the Azure core pipeline."""
467
468
469class ResourceModifiedError(HttpResponseError):
470 """An error response with status code 4xx, typically 412 Conflict.
471 This will not be raised directly by the Azure core pipeline."""
472
473
474class ResourceNotModifiedError(HttpResponseError):
475 """An error response with status code 304.
476 This will not be raised directly by the Azure core pipeline."""
477
478
479class TooManyRedirectsError(HttpResponseError, Generic[HTTPRequestType, HTTPResponseType]):
480 """Reached the maximum number of redirect attempts.
481
482 :param history: The history of requests made while trying to fulfill the request.
483 :type history: list[~azure.core.pipeline.policies.RequestHistory]
484 """
485
486 def __init__(
487 self,
488 history: "List[RequestHistory[HTTPRequestType, HTTPResponseType]]",
489 *args: Any,
490 **kwargs: Any,
491 ) -> None:
492 self.history = history
493 message = "Reached maximum redirect attempts."
494 super(TooManyRedirectsError, self).__init__(message, *args, **kwargs)
495
496
497class ODataV4Error(HttpResponseError):
498 """An HTTP response error where the JSON is decoded as OData V4 error format.
499
500 http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091
501
502 :param ~azure.core.rest.HttpResponse response: The response object.
503 :ivar dict odata_json: The parsed JSON body as attribute for convenience.
504 :ivar str ~.code: Its value is a service-defined error code.
505 This code serves as a sub-status for the HTTP error code specified in the response.
506 :ivar str message: Human-readable, language-dependent representation of the error.
507 :ivar str target: The target of the particular error (for example, the name of the property in error).
508 This field is optional and may be None.
509 :ivar list[ODataV4Format] details: Array of ODataV4Format instances that MUST contain name/value pairs
510 for code and message, and MAY contain a name/value pair for target, as described above.
511 :ivar dict innererror: An object. The contents of this object are service-defined.
512 Usually this object contains information that will help debug the service.
513 """
514
515 _ERROR_FORMAT = ODataV4Format
516
517 def __init__(self, response: _HttpResponseCommonAPI, **kwargs: Any) -> None:
518 # Ensure field are declared, whatever can happen afterwards
519 self.odata_json: Optional[Dict[str, Any]] = None
520 try:
521 self.odata_json = json.loads(response.text())
522 odata_message = self.odata_json.setdefault("error", {}).get("message")
523 except Exception: # pylint: disable=broad-except
524 # If the body is not JSON valid, just stop now
525 odata_message = None
526
527 self.code: Optional[str] = None
528 message: Optional[str] = kwargs.get("message", odata_message)
529 self.target: Optional[str] = None
530 self.details: Optional[List[Any]] = []
531 self.innererror: Optional[Mapping[str, Any]] = {}
532
533 if message and "message" not in kwargs:
534 kwargs["message"] = message
535
536 super(ODataV4Error, self).__init__(response=response, **kwargs)
537
538 self._error_format: Optional[Union[str, ODataV4Format]] = None
539 if self.odata_json:
540 try:
541 error_node = self.odata_json["error"]
542 self._error_format = self._ERROR_FORMAT(error_node)
543 self.__dict__.update({k: v for k, v in self._error_format.__dict__.items() if v is not None})
544 except Exception: # pylint: disable=broad-except
545 _LOGGER.info("Received error message was not valid OdataV4 format.")
546 self._error_format = "JSON was invalid for format " + str(self._ERROR_FORMAT)
547
548 def __str__(self) -> str:
549 if self._error_format:
550 return str(self._error_format)
551 return super(ODataV4Error, self).__str__()
552
553
554class StreamConsumedError(AzureError):
555 """Error thrown if you try to access the stream of a response once consumed.
556
557 It is thrown if you try to read / stream an ~azure.core.rest.HttpResponse or
558 ~azure.core.rest.AsyncHttpResponse once the response's stream has been consumed.
559
560 :param response: The response that triggered the exception.
561 :type response: ~azure.core.rest.HttpResponse or ~azure.core.rest.AsyncHttpResponse
562 """
563
564 def __init__(self, response: _HttpResponseCommonAPI) -> None:
565 message = (
566 "You are attempting to read or stream the content from request {}. "
567 "You have likely already consumed this stream, so it can not be accessed anymore.".format(response.request)
568 )
569 super(StreamConsumedError, self).__init__(message)
570
571
572class StreamClosedError(AzureError):
573 """Error thrown if you try to access the stream of a response once closed.
574
575 It is thrown if you try to read / stream an ~azure.core.rest.HttpResponse or
576 ~azure.core.rest.AsyncHttpResponse once the response's stream has been closed.
577
578 :param response: The response that triggered the exception.
579 :type response: ~azure.core.rest.HttpResponse or ~azure.core.rest.AsyncHttpResponse
580 """
581
582 def __init__(self, response: _HttpResponseCommonAPI) -> None:
583 message = (
584 "The content for response from request {} can no longer be read or streamed, since the "
585 "response has already been closed.".format(response.request)
586 )
587 super(StreamClosedError, self).__init__(message)
588
589
590class ResponseNotReadError(AzureError):
591 """Error thrown if you try to access a response's content without reading first.
592
593 It is thrown if you try to access an ~azure.core.rest.HttpResponse or
594 ~azure.core.rest.AsyncHttpResponse's content without first reading the response's bytes in first.
595
596 :param response: The response that triggered the exception.
597 :type response: ~azure.core.rest.HttpResponse or ~azure.core.rest.AsyncHttpResponse
598 """
599
600 def __init__(self, response: _HttpResponseCommonAPI) -> None:
601 message = (
602 "You have not read in the bytes for the response from request {}. "
603 "Call .read() on the response first.".format(response.request)
604 )
605 super(ResponseNotReadError, self).__init__(message)
606
607
608class SerializationError(ValueError):
609 """Raised if an error is encountered during serialization."""
610
611
612class DeserializationError(ValueError):
613 """Raised if an error is encountered during deserialization."""