external_api.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import re
  2. import sys
  3. from typing import Any
  4. from flask import current_app, got_request_exception
  5. from flask_restful import Api, http_status_message # type: ignore
  6. from werkzeug.datastructures import Headers
  7. from werkzeug.exceptions import HTTPException
  8. from core.errors.error import AppInvokeQuotaExceededError
  9. class ExternalApi(Api):
  10. def handle_error(self, e):
  11. """Error handler for the API transforms a raised exception into a Flask
  12. response, with the appropriate HTTP status code and body.
  13. :param e: the raised Exception object
  14. :type e: Exception
  15. """
  16. got_request_exception.send(current_app, exception=e)
  17. headers = Headers()
  18. if isinstance(e, HTTPException):
  19. if e.response is not None:
  20. resp = e.get_response()
  21. return resp
  22. status_code = e.code
  23. default_data = {
  24. "code": re.sub(r"(?<!^)(?=[A-Z])", "_", type(e).__name__).lower(),
  25. "message": getattr(e, "description", http_status_message(status_code)),
  26. "status": status_code,
  27. }
  28. if (
  29. default_data["message"]
  30. and default_data["message"] == "Failed to decode JSON object: Expecting value: line 1 column 1 (char 0)"
  31. ):
  32. default_data["message"] = "Invalid JSON payload received or JSON payload is empty."
  33. headers = e.get_response().headers
  34. elif isinstance(e, ValueError):
  35. status_code = 400
  36. default_data = {
  37. "code": "invalid_param",
  38. "message": str(e),
  39. "status": status_code,
  40. }
  41. elif isinstance(e, AppInvokeQuotaExceededError):
  42. status_code = 429
  43. default_data = {
  44. "code": "too_many_requests",
  45. "message": str(e),
  46. "status": status_code,
  47. }
  48. else:
  49. status_code = 500
  50. default_data = {
  51. "message": http_status_message(status_code),
  52. }
  53. # Werkzeug exceptions generate a content-length header which is added
  54. # to the response in addition to the actual content-length header
  55. # https://github.com/flask-restful/flask-restful/issues/534
  56. remove_headers = ("Content-Length",)
  57. for header in remove_headers:
  58. headers.pop(header, None)
  59. data = getattr(e, "data", default_data)
  60. error_cls_name = type(e).__name__
  61. if error_cls_name in self.errors:
  62. custom_data = self.errors.get(error_cls_name, {})
  63. custom_data = custom_data.copy()
  64. status_code = custom_data.get("status", 500)
  65. if "message" in custom_data:
  66. custom_data["message"] = custom_data["message"].format(
  67. message=str(e.description if hasattr(e, "description") else e)
  68. )
  69. data.update(custom_data)
  70. # record the exception in the logs when we have a server error of status code: 500
  71. if status_code and status_code >= 500:
  72. exc_info: Any = sys.exc_info()
  73. if exc_info[1] is None:
  74. exc_info = None
  75. current_app.log_exception(exc_info)
  76. if status_code == 406 and self.default_mediatype is None:
  77. # if we are handling NotAcceptable (406), make sure that
  78. # make_response uses a representation we support as the
  79. # default mediatype (so that make_response doesn't throw
  80. # another NotAcceptable error).
  81. supported_mediatypes = list(self.representations.keys()) # only supported application/json
  82. fallback_mediatype = supported_mediatypes[0] if supported_mediatypes else "text/plain"
  83. data = {"code": "not_acceptable", "message": data.get("message")}
  84. resp = self.make_response(data, status_code, headers, fallback_mediatype=fallback_mediatype)
  85. elif status_code == 400:
  86. if isinstance(data.get("message"), dict):
  87. param_key, param_value = list(data.get("message", {}).items())[0]
  88. data = {"code": "invalid_param", "message": param_value, "params": param_key}
  89. else:
  90. if "code" not in data:
  91. data["code"] = "unknown"
  92. resp = self.make_response(data, status_code, headers)
  93. else:
  94. if "code" not in data:
  95. data["code"] = "unknown"
  96. resp = self.make_response(data, status_code, headers)
  97. if status_code == 401:
  98. resp = self.unauthorized(resp)
  99. return resp