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