diff --git a/src/dso_api/dynamic_api/views/api.py b/src/dso_api/dynamic_api/views/api.py index 7df1cd834..5aa4dafa1 100644 --- a/src/dso_api/dynamic_api/views/api.py +++ b/src/dso_api/dynamic_api/views/api.py @@ -15,12 +15,12 @@ class (namely the :class:`~dso_api.dynamic_api.views.DynamicApiViewSet` base cla from functools import cached_property from django.db import models -from django.db.utils import ProgrammingError +from django.db.utils import DatabaseError, InternalError, ProgrammingError from django.utils.decorators import method_decorator from django.utils.translation import gettext as _ from django.views.decorators.cache import never_cache from rest_framework import viewsets -from rest_framework.exceptions import NotFound, PermissionDenied +from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError from schematools.contrib.django.models import DynamicModel from dso_api.dynamic_api import filters, permissions, serializers @@ -70,7 +70,7 @@ class DynamicApiViewSet(NestedViewSetMixin, DSOViewMixin, viewsets.ReadOnlyModel def list(self, request, *args, **kwargs): try: return super().list(request, *args, **kwargs) - except ProgrammingError as e: + except (ProgrammingError, InternalError) as e: self._handle_db_error(e) raise @@ -81,11 +81,12 @@ def retrieve(self, request, *args, **kwargs): self._handle_db_error(e) raise - def _handle_db_error(self, e: ProgrammingError) -> None: - """Make sure database permission errors are gratefully handled. - This is a common source of 500 error issues, - and giving a better response to the user helps. + def _handle_db_error(self, e: DatabaseError) -> None: + """Make sure database permission errors and invalid coordinate + errors are gratefully handled. These are a common source of + 500 error issues, and giving a better response to the user helps. """ + if str(e).startswith("permission denied for "): logger.exception("Database role has no access (while application allowed): %s", e) raise PermissionDenied( @@ -94,6 +95,14 @@ def _handle_db_error(self, e: ProgrammingError) -> None: code="db_permission_denied", ) from e + if str(e).startswith("transform"): + logger.warning("Invalid CRS transform requested: %s", e) + raise ValidationError( + "Invalid coordinate reference system or transform." + "You may want to add an Accept-Crs header.", + code="invalid_crs", + ) from e + def initial(self, request, *args, **kwargs): super().initial(request, *args, **kwargs) table_schema = self.model.table_schema() diff --git a/src/tests/test_dynamic_api/views/test_api_filters.py b/src/tests/test_dynamic_api/views/test_api_filters.py index ba67f2712..22e80dc9f 100644 --- a/src/tests/test_dynamic_api/views/test_api_filters.py +++ b/src/tests/test_dynamic_api/views/test_api_filters.py @@ -78,6 +78,49 @@ def test_syntax_error(param, api_client, afval_dataset, filled_router): reason = response.data["invalid-params"][0]["reason"] assert param in reason + @staticmethod + def test_transform_crs_without_header( + api_client, + parkeervakken_dataset, + filled_router, + ): + response = api_client.get( + "/v1/parkeervakken/parkeervakken/?geometry[intersects]=" + "POLYGON((119814 485931," + "121814 485931," + "121814 487931," + "119814 487931," + "119814 485931))", + ) + data = read_response_json(response) + assert response.status_code == 400, data + assert response.data["invalid-params"] == [ + { + "type": "urn:apiexception:invalid:invalid_crs", + "name": "invalid_crs", + "reason": "Invalid coordinate reference system or transform.You may want to add " + "an Accept-Crs header.", + } + ] + + @staticmethod + def test_transform_crs_with_header( + api_client, + parkeervakken_dataset, + filled_router, + ): + response = api_client.get( + "/v1/parkeervakken/parkeervakken/?geometry[intersects]=" + "POLYGON((119814 485931," + "121814 485931," + "121814 487931," + "119814 487931," + "119814 485931))", + headers={"Accept-Crs": "EPSG:28992"}, + ) + data = read_response_json(response) + assert response.status_code == 200, data + @pytest.mark.django_db class TestFilterFieldTypes: