"""``/api/v1`` Blueprint及其局部HTTP基础设施。""" from __future__ import annotations import logging from flask import Blueprint, Flask, request from werkzeug.exceptions import RequestEntityTooLarge from dms.common.errors import DmsError from dms.common.request_context import REQUEST_ID_HEADER, get_request_id, initialize_request_id from dms.common.response import error_response logger = logging.getLogger(__name__) api_v1 = Blueprint("dms_api_v1", __name__, url_prefix="/api/v1") EXPOSED_RESPONSE_HEADERS = ("Content-Disposition", "X-Request-Id") @api_v1.before_request def _before_dms_request() -> None: initialize_request_id() @api_v1.after_request def _after_dms_request(response): response.headers[REQUEST_ID_HEADER] = get_request_id() return response @api_v1.errorhandler(DmsError) def _handle_dms_error(error: DmsError): return error_response( error.code, error.message, details=error.details, status=error.status_code, ) @api_v1.errorhandler(RequestEntityTooLarge) def _handle_request_too_large(_error: RequestEntityTooLarge): return error_response( "PAYLOAD_TOO_LARGE", "请求内容过大", details=None, status=413, ) @api_v1.errorhandler(Exception) def _handle_unexpected_error(error: Exception): logger.exception("未处理的DMS接口异常", exc_info=error) return error_response( "INTERNAL_ERROR", "服务器内部错误", details=None, status=500, ) def register_dms_routing_error_handlers(app: Flask) -> None: """让未匹配到Blueprint的 ``/api/v1`` 404也使用DMS错误结构。 其他路径直接返回Werkzeug原始异常,保持既有 ``/api/*`` 行为。 """ @app.errorhandler(404) def _handle_routing_not_found(error): if request.path == "/api/v1" or request.path.startswith("/api/v1/"): try: initialize_request_id() except DmsError as request_id_error: return _handle_dms_error(request_id_error) return error_response( "RESOURCE_NOT_FOUND", "请求的资源不存在", details=None, status=404, ) return error def _merge_exposed_headers(existing: str | None) -> str: """大小写无关地合并浏览器可读响应头,并保持已有声明。""" values: list[str] = [] seen: set[str] = set() for value in (existing or "").split(","): normalized = value.strip() key = normalized.lower() if normalized and key not in seen: values.append(normalized) seen.add(key) for value in EXPOSED_RESPONSE_HEADERS: key = value.lower() if key not in seen: values.append(value) seen.add(key) return ", ".join(values) def register_dms_response_headers(app: Flask) -> None: """仅为DMS路径补充跨域可读响应头,覆盖路由级错误。""" @app.after_request def _expose_dms_response_headers(response): if request.path == "/api/v1" or request.path.startswith("/api/v1/"): response.headers["Access-Control-Expose-Headers"] = ( _merge_exposed_headers( response.headers.get("Access-Control-Expose-Headers") ) ) return response from dms.api.v1 import health # noqa: E402,F401 from dms.api.v1 import ( # noqa: E402,F401 audit, auth, categories, documents, organizations, recycle_bin, runtime_config, ui_dictionaries, users, )