From 0f27fe298c1684a231d2117980c6471f937a15d7 Mon Sep 17 00:00:00 2001 From: Syed Ali Abbas Zaidi Date: Thu, 16 Jul 2026 17:23:49 +0500 Subject: [PATCH] fix(api): return 400 for non-positive per_page instead of 500 --- .../plane/tests/contract/api/test_issues.py | 47 +++++++++++++++++++ apps/api/plane/utils/paginator.py | 3 ++ 2 files changed, 50 insertions(+) diff --git a/apps/api/plane/tests/contract/api/test_issues.py b/apps/api/plane/tests/contract/api/test_issues.py index cda57b64800..2c7328cd93d 100644 --- a/apps/api/plane/tests/contract/api/test_issues.py +++ b/apps/api/plane/tests/contract/api/test_issues.py @@ -94,3 +94,50 @@ def test_legitimate_order_by_still_works(self, api_key_client, workspace, projec assert response.status_code == status.HTTP_200_OK, ( f"order_by={value!r} got {response.status_code}: {response.data!r}" ) + + +@pytest.mark.contract +class TestIssueListPerPageValidation: + """Regression tests for the work-item list endpoint's ``per_page`` handling: + GET /api/v1/workspaces/{slug}/projects/{project_id}/issues/. + + Non-numeric and oversized values already returned a clear 400, but zero and + negative values fell through validation and crashed downstream with a 500. + They should return a 400 like the other invalid cases. + """ + + def get_url(self, workspace_slug, project_id): + return f"/api/v1/workspaces/{workspace_slug}/projects/{project_id}/issues/" + + @pytest.mark.django_db + @pytest.mark.parametrize("per_page", ["0", "-5"]) + def test_non_positive_per_page_returns_400(self, api_key_client, workspace, project, issue, per_page): + """``per_page=0`` and negative values used to raise a 500; now a 400.""" + url = self.get_url(workspace.slug, project.id) + response = api_key_client.get(url, {"per_page": per_page}) + + assert response.status_code == status.HTTP_400_BAD_REQUEST, ( + f"per_page={per_page!r} got {response.status_code}: {response.data!r}" + ) + assert "positive integer" in str(response.data).lower() + + @pytest.mark.django_db + def test_non_numeric_per_page_returns_400(self, api_key_client, workspace, project, issue): + url = self.get_url(workspace.slug, project.id) + response = api_key_client.get(url, {"per_page": "abc"}) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @pytest.mark.django_db + def test_oversized_per_page_returns_400(self, api_key_client, workspace, project, issue): + url = self.get_url(workspace.slug, project.id) + response = api_key_client.get(url, {"per_page": "100000"}) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @pytest.mark.django_db + def test_valid_per_page_still_works(self, api_key_client, workspace, project, issue): + url = self.get_url(workspace.slug, project.id) + response = api_key_client.get(url, {"per_page": "50"}) + + assert response.status_code == status.HTTP_200_OK, f"Got {response.status_code}: {response.data!r}" diff --git a/apps/api/plane/utils/paginator.py b/apps/api/plane/utils/paginator.py index 2082041f1ac..930b03ebd75 100644 --- a/apps/api/plane/utils/paginator.py +++ b/apps/api/plane/utils/paginator.py @@ -646,6 +646,9 @@ def get_per_page(self, request, default_per_page=1000, max_per_page=1000): except ValueError: raise ParseError(detail="Invalid per_page parameter.") + if per_page <= 0: + raise ParseError(detail="Invalid per_page value. Must be a positive integer.") + max_per_page = max(max_per_page, default_per_page) if per_page > max_per_page: raise ParseError(detail=f"Invalid per_page value. Cannot exceed {max_per_page}.")