import re from collections.abc import Mapping from typing import Any, Protocol, override from flask import Blueprint, Flask, current_app, got_request_exception, request from flask.typing import ResponseReturnValue from flask_restx import Api from werkzeug.exceptions import HTTPException from werkzeug.http import HTTP_STATUS_CODES from configs import dify_config from core.errors.error import AppInvokeQuotaExceededError from core.plugin.impl.exc import PluginRuntimeError from extensions.ext_logging import get_request_id from libs.flask_restx_compat import install_swagger_compatibility from libs.token import build_force_logout_cookie_headers def http_status_message(code): return HTTP_STATUS_CODES.get(code, "") class ErrorBodyFormatter(Protocol): """Last-touch hook over an error body before it goes on the wire.""" def finalize(self, e: Exception, data: dict[str, Any], status_code: int) -> dict[str, Any]: ... def register_external_error_handlers(api: Api, body_formatter: ErrorBodyFormatter | None = None): def _finalize(e: Exception, data: dict[str, Any], status_code: int) -> dict[str, Any]: if body_formatter is None: return data return body_formatter.finalize(e, data, status_code) def handle_http_exception(e: HTTPException): got_request_exception.send(current_app, exception=e) # If Werkzeug already prepared a Response, just use it. This bypasses # body_formatter entirely — surfaces with a formatter must not raise # exceptions carrying a pre-built response. if e.response is not None: return e.response status_code = getattr(e, "code", 500) or 500 # Build a safe, dict-like payload default_data = { "code": re.sub(r"(? None: if self._add_specs and self._doc: app_or_blueprint.add_url_rule(self._doc, "doc", self.render_doc) # Api.base_path resolves the ``root`` endpoint. A caller that disables # Flask-RESTX's 404 root must register its own resource with that endpoint. if self._register_default_root: def render_default_root() -> ResponseReturnValue: self.render_root() return "", 404 app_or_blueprint.add_url_rule(self.prefix or "/", "root", render_default_root) @override def _should_use_fr_error_handler(self): # catch_all_404s makes flask-restx claim NotFound for ANY app path # (it wraps the app-level handle_exception), so scope the claim to # this blueprint's url prefix; other surfaces keep their own 404s. if self._error_body_formatter is not None and not self._request_under_own_prefix(): return False return super()._should_use_fr_error_handler() def _request_under_own_prefix(self) -> bool: prefix = self.blueprint.url_prefix if self.blueprint is not None else None if not prefix: return True return request.path == prefix or request.path.startswith(prefix.rstrip("/") + "/") @override def _help_on_404(self, message: str | None = None) -> str | None: # flask-restx appends route suggestions post-handler; with a canonical # formatter installed, that would corrupt the contract and enumerate # routes to unauthenticated callers. if self._error_body_formatter is not None: return message return super()._help_on_404(message)