exceptions.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. # Licensed to the Apache Software Foundation (ASF) under one
  2. # or more contributor license agreements. See the NOTICE file
  3. # distributed with this work for additional information
  4. # regarding copyright ownership. The ASF licenses this file
  5. # to you under the Apache License, Version 2.0 (the
  6. # "License"); you may not use this file except in compliance
  7. # with the License. You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing,
  12. # software distributed under the License is distributed on an
  13. # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14. # KIND, either express or implied. See the License for the
  15. # specific language governing permissions and limitations
  16. # under the License.
  17. from __future__ import annotations
  18. from http import HTTPStatus
  19. from typing import TYPE_CHECKING, Any
  20. import werkzeug
  21. from connexion import FlaskApi, ProblemException, problem
  22. from airflow.utils.docs import get_docs_url
  23. if TYPE_CHECKING:
  24. import flask
  25. doc_link = get_docs_url("stable-rest-api-ref.html")
  26. EXCEPTIONS_LINK_MAP = {
  27. 400: f"{doc_link}#section/Errors/BadRequest",
  28. 404: f"{doc_link}#section/Errors/NotFound",
  29. 405: f"{doc_link}#section/Errors/MethodNotAllowed",
  30. 401: f"{doc_link}#section/Errors/Unauthenticated",
  31. 409: f"{doc_link}#section/Errors/AlreadyExists",
  32. 403: f"{doc_link}#section/Errors/PermissionDenied",
  33. 500: f"{doc_link}#section/Errors/Unknown",
  34. }
  35. def common_error_handler(exception: BaseException) -> flask.Response:
  36. """Use to capture connexion exceptions and add link to the type field."""
  37. if isinstance(exception, ProblemException):
  38. link = EXCEPTIONS_LINK_MAP.get(exception.status)
  39. if link:
  40. response = problem(
  41. status=exception.status,
  42. title=exception.title,
  43. detail=exception.detail,
  44. type=link,
  45. instance=exception.instance,
  46. headers=exception.headers,
  47. ext=exception.ext,
  48. )
  49. else:
  50. response = problem(
  51. status=exception.status,
  52. title=exception.title,
  53. detail=exception.detail,
  54. type=exception.type,
  55. instance=exception.instance,
  56. headers=exception.headers,
  57. ext=exception.ext,
  58. )
  59. else:
  60. if not isinstance(exception, werkzeug.exceptions.HTTPException):
  61. exception = werkzeug.exceptions.InternalServerError()
  62. response = problem(title=exception.name, detail=exception.description, status=exception.code)
  63. return FlaskApi.get_response(response)
  64. class NotFound(ProblemException):
  65. """Raise when the object cannot be found."""
  66. def __init__(
  67. self,
  68. title: str = "Not Found",
  69. detail: str | None = None,
  70. headers: dict | None = None,
  71. **kwargs: Any,
  72. ) -> None:
  73. super().__init__(
  74. status=HTTPStatus.NOT_FOUND,
  75. type=EXCEPTIONS_LINK_MAP[404],
  76. title=title,
  77. detail=detail,
  78. headers=headers,
  79. **kwargs,
  80. )
  81. class BadRequest(ProblemException):
  82. """Raise when the server processes a bad request."""
  83. def __init__(
  84. self,
  85. title: str = "Bad Request",
  86. detail: str | None = None,
  87. headers: dict | None = None,
  88. **kwargs: Any,
  89. ) -> None:
  90. super().__init__(
  91. status=HTTPStatus.BAD_REQUEST,
  92. type=EXCEPTIONS_LINK_MAP[400],
  93. title=title,
  94. detail=detail,
  95. headers=headers,
  96. **kwargs,
  97. )
  98. class Unauthenticated(ProblemException):
  99. """Raise when the user is not authenticated."""
  100. def __init__(
  101. self,
  102. title: str = "Unauthorized",
  103. detail: str | None = None,
  104. headers: dict | None = None,
  105. **kwargs: Any,
  106. ):
  107. super().__init__(
  108. status=HTTPStatus.UNAUTHORIZED,
  109. type=EXCEPTIONS_LINK_MAP[401],
  110. title=title,
  111. detail=detail,
  112. headers=headers,
  113. **kwargs,
  114. )
  115. class PermissionDenied(ProblemException):
  116. """Raise when the user does not have the required permissions."""
  117. def __init__(
  118. self,
  119. title: str = "Forbidden",
  120. detail: str | None = None,
  121. headers: dict | None = None,
  122. **kwargs: Any,
  123. ) -> None:
  124. super().__init__(
  125. status=HTTPStatus.FORBIDDEN,
  126. type=EXCEPTIONS_LINK_MAP[403],
  127. title=title,
  128. detail=detail,
  129. headers=headers,
  130. **kwargs,
  131. )
  132. class AlreadyExists(ProblemException):
  133. """Raise when the object already exists."""
  134. def __init__(
  135. self,
  136. title="Conflict",
  137. detail: str | None = None,
  138. headers: dict | None = None,
  139. **kwargs: Any,
  140. ):
  141. super().__init__(
  142. status=HTTPStatus.CONFLICT,
  143. type=EXCEPTIONS_LINK_MAP[409],
  144. title=title,
  145. detail=detail,
  146. headers=headers,
  147. **kwargs,
  148. )
  149. class Unknown(ProblemException):
  150. """Returns a response body and status code for HTTP 500 exception."""
  151. def __init__(
  152. self,
  153. title: str = "Internal Server Error",
  154. detail: str | None = None,
  155. headers: dict | None = None,
  156. **kwargs: Any,
  157. ) -> None:
  158. super().__init__(
  159. status=HTTPStatus.INTERNAL_SERVER_ERROR,
  160. type=EXCEPTIONS_LINK_MAP[500],
  161. title=title,
  162. detail=detail,
  163. headers=headers,
  164. **kwargs,
  165. )