blueprint.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. """``/api/v1`` Blueprint及其局部HTTP基础设施。"""
  2. from __future__ import annotations
  3. import logging
  4. from flask import Blueprint, Flask, request
  5. from werkzeug.exceptions import RequestEntityTooLarge
  6. from dms.common.errors import DmsError
  7. from dms.common.request_context import REQUEST_ID_HEADER, get_request_id, initialize_request_id
  8. from dms.common.response import error_response
  9. logger = logging.getLogger(__name__)
  10. api_v1 = Blueprint("dms_api_v1", __name__, url_prefix="/api/v1")
  11. EXPOSED_RESPONSE_HEADERS = ("Content-Disposition", "X-Request-Id")
  12. @api_v1.before_request
  13. def _before_dms_request() -> None:
  14. initialize_request_id()
  15. @api_v1.after_request
  16. def _after_dms_request(response):
  17. response.headers[REQUEST_ID_HEADER] = get_request_id()
  18. return response
  19. @api_v1.errorhandler(DmsError)
  20. def _handle_dms_error(error: DmsError):
  21. return error_response(
  22. error.code,
  23. error.message,
  24. details=error.details,
  25. status=error.status_code,
  26. )
  27. @api_v1.errorhandler(RequestEntityTooLarge)
  28. def _handle_request_too_large(_error: RequestEntityTooLarge):
  29. return error_response(
  30. "PAYLOAD_TOO_LARGE",
  31. "请求内容过大",
  32. details=None,
  33. status=413,
  34. )
  35. @api_v1.errorhandler(Exception)
  36. def _handle_unexpected_error(error: Exception):
  37. logger.exception("未处理的DMS接口异常", exc_info=error)
  38. return error_response(
  39. "INTERNAL_ERROR",
  40. "服务器内部错误",
  41. details=None,
  42. status=500,
  43. )
  44. def register_dms_routing_error_handlers(app: Flask) -> None:
  45. """让未匹配到Blueprint的 ``/api/v1`` 404也使用DMS错误结构。
  46. 其他路径直接返回Werkzeug原始异常,保持既有 ``/api/*`` 行为。
  47. """
  48. @app.errorhandler(404)
  49. def _handle_routing_not_found(error):
  50. if request.path == "/api/v1" or request.path.startswith("/api/v1/"):
  51. try:
  52. initialize_request_id()
  53. except DmsError as request_id_error:
  54. return _handle_dms_error(request_id_error)
  55. return error_response(
  56. "RESOURCE_NOT_FOUND",
  57. "请求的资源不存在",
  58. details=None,
  59. status=404,
  60. )
  61. return error
  62. def _merge_exposed_headers(existing: str | None) -> str:
  63. """大小写无关地合并浏览器可读响应头,并保持已有声明。"""
  64. values: list[str] = []
  65. seen: set[str] = set()
  66. for value in (existing or "").split(","):
  67. normalized = value.strip()
  68. key = normalized.lower()
  69. if normalized and key not in seen:
  70. values.append(normalized)
  71. seen.add(key)
  72. for value in EXPOSED_RESPONSE_HEADERS:
  73. key = value.lower()
  74. if key not in seen:
  75. values.append(value)
  76. seen.add(key)
  77. return ", ".join(values)
  78. def register_dms_response_headers(app: Flask) -> None:
  79. """仅为DMS路径补充跨域可读响应头,覆盖路由级错误。"""
  80. @app.after_request
  81. def _expose_dms_response_headers(response):
  82. if request.path == "/api/v1" or request.path.startswith("/api/v1/"):
  83. response.headers["Access-Control-Expose-Headers"] = (
  84. _merge_exposed_headers(
  85. response.headers.get("Access-Control-Expose-Headers")
  86. )
  87. )
  88. return response
  89. from dms.api.v1 import health # noqa: E402,F401
  90. from dms.api.v1 import ( # noqa: E402,F401
  91. audit,
  92. auth,
  93. categories,
  94. documents,
  95. organizations,
  96. recycle_bin,
  97. runtime_config,
  98. ui_dictionaries,
  99. users,
  100. )