From 1747ec64fcb6c7a3c40f1362fc86aa49087f1da2 Mon Sep 17 00:00:00 2001 From: Daniel Mursa Date: Fri, 3 Oct 2025 16:18:16 +0200 Subject: [PATCH] :recycle: [maykinmedia/open-api-framework#175] Improve handled_exception --- src/objects/tests/v2/test_filters.py | 7 ++++- src/objects/utils/views.py | 41 +++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/objects/tests/v2/test_filters.py b/src/objects/tests/v2/test_filters.py index 804a882f..5eacf147 100644 --- a/src/objects/tests/v2/test_filters.py +++ b/src/objects/tests/v2/test_filters.py @@ -1,3 +1,4 @@ +import os from datetime import date, timedelta from unittest.mock import patch @@ -1021,6 +1022,7 @@ def test_filter_with_nesting(self): f"http://testserver{reverse('object-detail', args=[record.object.uuid])}", ) + @patch.dict(os.environ, {"DEBUG": "false"}) @patch( "objects.core.query.ObjectRecordQuerySet._fetch_all", side_effect=ProgrammingError("'jsonpath' is not found"), @@ -1032,7 +1034,10 @@ def test_filter_db_error(self, mock_query): self.assertEqual( response.json(), { - "detail": "This search operation is not supported by the underlying data store." + "code": "error", + "title": "Internal Server Error", + "status": 500, + "detail": "This search operation is not supported by the underlying data store.", }, ) diff --git a/src/objects/utils/views.py b/src/objects/utils/views.py index cb384c64..8c3a4562 100644 --- a/src/objects/utils/views.py +++ b/src/objects/utils/views.py @@ -1,16 +1,21 @@ from django import http from django.db.utils import DatabaseError from django.template import TemplateDoesNotExist, loader +from django.utils.translation import gettext_lazy as _ from django.views.decorators.csrf import requires_csrf_token from django.views.defaults import ERROR_500_TEMPLATE_NAME import structlog +from open_api_framework.conf.utils import config from rest_framework import status from rest_framework.response import Response from rest_framework.views import exception_handler as drf_exception_handler logger = structlog.stdlib.get_logger(__name__) +DEFAULT_CODE = "invalid" +DEFAULT_DETAIL = _("Invalid input.") + @requires_csrf_token def server_error(request, template_name=ERROR_500_TEMPLATE_NAME): @@ -34,14 +39,42 @@ def server_error(request, template_name=ERROR_500_TEMPLATE_NAME): def exception_handler(exc, context): + """ + Transform 5xx errors into DSO-compliant shape. + """ response = drf_exception_handler(exc, context) + if not response: + if config("DEBUG", default=False): + return None - # provide user-friendly response if data_icontains was used but DB couldn't process it - if not response and isinstance(exc, DatabaseError) and "jsonpath" in exc.args[0]: data = { - "detail": "This search operation is not supported by the underlying data store." + "code": "error", + "title": "Internal Server Error", + "status": status.HTTP_500_INTERNAL_SERVER_ERROR, + "detail": _("A server error has occurred."), } + event = "api.uncaught_exception" + + if isinstance(exc, DatabaseError) and "jsonpath" in exc.args[0]: + # provide user-friendly response if data_icontains was used but DB couldn't process it + data["detail"] = ( + "This search operation is not supported by the underlying data store." + ) + event = "api.database_exception" + response = Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR, data=data) - logger.exception("search_failed_for_datastore", exc_info=exc) + logger.exception(event, exc_info=exc) + + return response + + # exception logger event + logger.exception( + "api.handled_exception", + title=getattr(exc, "default_detail", DEFAULT_DETAIL).strip("'"), + code=getattr(exc, "default_code", DEFAULT_CODE), + status=getattr(response, "status_code", status.HTTP_400_BAD_REQUEST), + data=getattr(response, "data", {}), + exc_info=False, + ) return response