From f231cbce3b42308536fbb82ab95d7b3e44d7205c Mon Sep 17 00:00:00 2001 From: lotte-amsterdam Date: Mon, 18 May 2026 10:31:11 +0200 Subject: [PATCH 1/3] added invalid crs transform error --- src/dso_api/dynamic_api/views/api.py | 22 +++++++++---- .../views/test_api_filters.py | 32 +++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/dso_api/dynamic_api/views/api.py b/src/dso_api/dynamic_api/views/api.py index 7df1cd834..9c1c6e8fd 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,13 @@ def _handle_db_error(self, e: ProgrammingError) -> None: code="db_permission_denied", ) from e + if "Invalid coordinate" in str(e): + logger.warning("Invalid CRS transform requested: %s", e) + raise ValidationError( + "Invalid coordinate reference system or transform.", + 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..dc7fe1581 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,38 @@ def test_syntax_error(param, api_client, afval_dataset, filled_router): reason = response.data["invalid-params"][0]["reason"] assert param in reason + @staticmethod + @pytest.mark.parametrize( + ["query", "expect_code"], + [ + ( + "geometry[intersects]=" + "POLYGON((119814 485931," + "121814 485931," + "121814 487931," + "119814 487931," + "119814 485931))", + 400, + ), + ], + ) + def test_crs_transform( + api_client, + parkeervakken_dataset, + filled_router, + query, + expect_code, + ): + response = api_client.get(f"/v1/parkeervakken/parkeervakken/?{query}") + data = read_response_json(response) + assert response.status_code == expect_code, data + assert response.data == { + "type": "urn:apiexception:invalid", + "title": "Invalid input.", + "detail": ("Invalid coordinate reference system or transform."), + "status": 400, + } + @pytest.mark.django_db class TestFilterFieldTypes: From f416470747766e232ce2b064910945c67aef746b Mon Sep 17 00:00:00 2001 From: lotte-amsterdam Date: Mon, 18 May 2026 12:17:00 +0200 Subject: [PATCH 2/3] made validationerror more general + added test --- src/dso_api/dynamic_api/views/api.py | 5 +- .../views/test_api_filters.py | 49 ++++++++++++++++--- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/dso_api/dynamic_api/views/api.py b/src/dso_api/dynamic_api/views/api.py index 9c1c6e8fd..5aa4dafa1 100644 --- a/src/dso_api/dynamic_api/views/api.py +++ b/src/dso_api/dynamic_api/views/api.py @@ -95,10 +95,11 @@ def _handle_db_error(self, e: DatabaseError) -> None: code="db_permission_denied", ) from e - if "Invalid coordinate" in str(e): + if str(e).startswith("transform"): logger.warning("Invalid CRS transform requested: %s", e) raise ValidationError( - "Invalid coordinate reference system or transform.", + "Invalid coordinate reference system or transform." + "You may want to add an Accept-Crs header.", code="invalid_crs", ) from e 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 dc7fe1581..fe131691b 100644 --- a/src/tests/test_dynamic_api/views/test_api_filters.py +++ b/src/tests/test_dynamic_api/views/test_api_filters.py @@ -93,22 +93,55 @@ def test_syntax_error(param, api_client, afval_dataset, filled_router): ), ], ) - def test_crs_transform( + def test_transform_crs_without_header( api_client, parkeervakken_dataset, filled_router, query, expect_code, ): - response = api_client.get(f"/v1/parkeervakken/parkeervakken/?{query}") + response = api_client.get( + f"/v1/parkeervakken/parkeervakken/?{query}", + ) + data = read_response_json(response) + assert response.status_code == expect_code, 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 + @pytest.mark.parametrize( + ["query", "expect_code"], + [ + ( + "geometry[intersects]=" + "POLYGON((119814 485931," + "121814 485931," + "121814 487931," + "119814 487931," + "119814 485931))", + 200, + ), + ], + ) + def test_transform_crs_with_header( + api_client, + parkeervakken_dataset, + filled_router, + query, + expect_code, + ): + response = api_client.get( + f"/v1/parkeervakken/parkeervakken/?{query}", + headers={"Accept-Crs": "EPSG:28992"}, + ) data = read_response_json(response) assert response.status_code == expect_code, data - assert response.data == { - "type": "urn:apiexception:invalid", - "title": "Invalid input.", - "detail": ("Invalid coordinate reference system or transform."), - "status": 400, - } @pytest.mark.django_db From d5cd9fb3c3db5c03983833938d817caad947d21f Mon Sep 17 00:00:00 2001 From: lotte-amsterdam Date: Mon, 18 May 2026 13:16:22 +0200 Subject: [PATCH 3/3] removed pytest mark parametrize from tests --- .../views/test_api_filters.py | 50 ++++++------------- 1 file changed, 14 insertions(+), 36 deletions(-) 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 fe131691b..22e80dc9f 100644 --- a/src/tests/test_dynamic_api/views/test_api_filters.py +++ b/src/tests/test_dynamic_api/views/test_api_filters.py @@ -79,32 +79,21 @@ def test_syntax_error(param, api_client, afval_dataset, filled_router): assert param in reason @staticmethod - @pytest.mark.parametrize( - ["query", "expect_code"], - [ - ( - "geometry[intersects]=" - "POLYGON((119814 485931," - "121814 485931," - "121814 487931," - "119814 487931," - "119814 485931))", - 400, - ), - ], - ) def test_transform_crs_without_header( api_client, parkeervakken_dataset, filled_router, - query, - expect_code, ): response = api_client.get( - f"/v1/parkeervakken/parkeervakken/?{query}", + "/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 == expect_code, data + assert response.status_code == 400, data assert response.data["invalid-params"] == [ { "type": "urn:apiexception:invalid:invalid_crs", @@ -115,33 +104,22 @@ def test_transform_crs_without_header( ] @staticmethod - @pytest.mark.parametrize( - ["query", "expect_code"], - [ - ( - "geometry[intersects]=" - "POLYGON((119814 485931," - "121814 485931," - "121814 487931," - "119814 487931," - "119814 485931))", - 200, - ), - ], - ) def test_transform_crs_with_header( api_client, parkeervakken_dataset, filled_router, - query, - expect_code, ): response = api_client.get( - f"/v1/parkeervakken/parkeervakken/?{query}", + "/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 == expect_code, data + assert response.status_code == 200, data @pytest.mark.django_db