From 999514fffc228f1097cc48447cc7fec60052b227 Mon Sep 17 00:00:00 2001 From: Manish Gupta Date: Thu, 20 Aug 2026 18:11:23 +0530 Subject: [PATCH] fix: 404 instead of a hollow 200 when a global view does not exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review catch, verified before fixing. WorkspaceViewViewSet.retrieve resolves the view with .first() and serialized the result unconditionally, so a member asking for an id that does not exist got 200 with every field null or empty (`{"name": "", "description": "", "filters": null, ...}`) and a recent-visit enqueued for a nonexistent entity. Because get_queryset() is scoped to the URL workspace, the same happened for a real view id belonging to a different workspace. Returns 404, matching the other retrieve endpoints. Noted while confirming this, not fixed here: the project-level sibling IssueViewViewSet.retrieve has the same .first() pattern and then dereferences `issue_view.owned_by`, which raises AttributeError on None rather than answering 404 — a 500 instead of a hollow 200. Different method, so it gets its own ticket rather than widening this one. Co-authored-by: Plane AI --- apps/api/plane/app/views/view/base.py | 12 ++++++++++++ .../app/test_undecorated_action_authz_app.py | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/apps/api/plane/app/views/view/base.py b/apps/api/plane/app/views/view/base.py index c56ea7657c..863b165eb0 100644 --- a/apps/api/plane/app/views/view/base.py +++ b/apps/api/plane/app/views/view/base.py @@ -129,6 +129,18 @@ class WorkspaceViewViewSet(BaseViewSet): # membership in the workspace could therefore read any global view in it # by id. issue_view = self.get_queryset().filter(pk=pk).first() + + # get_queryset() is scoped to the URL workspace, so None means either no + # such view or one belonging to a workspace this URL does not name. + # Serializing None yields a hollow object — every field null or empty — + # returned as 200, and enqueues a recent-visit for an entity that does + # not exist. Answer 404, as the other retrieve endpoints do. + if issue_view is None: + return Response( + {"error": "The required object does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + serializer = IssueViewSerializer(issue_view) recent_visited_task.delay( slug=slug, diff --git a/apps/api/plane/tests/contract/app/test_undecorated_action_authz_app.py b/apps/api/plane/tests/contract/app/test_undecorated_action_authz_app.py index 8f5b4495de..3f40932707 100644 --- a/apps/api/plane/tests/contract/app/test_undecorated_action_authz_app.py +++ b/apps/api/plane/tests/contract/app/test_undecorated_action_authz_app.py @@ -249,6 +249,20 @@ class TestWorkspaceViewRetrieveRequiresMembership: ) assert str(response.data["id"]) == str(workspace_view.id) + @pytest.mark.django_db + def test_missing_view_is_a_404_not_an_empty_200(self, session_client, workspace): + """A member asking for a view id that does not exist must get 404. + + get_queryset() is scoped to the URL workspace, so this also covers a real + view id belonging to a different workspace. + """ + url = WORKSPACE_VIEW_DETAIL_URL.format(slug=workspace.slug, pk=uuid4()) + response = session_client.get(url) + + assert response.status_code == status.HTTP_404_NOT_FOUND, ( + f"Got {response.status_code}: {getattr(response, 'data', None)!r}" + ) + @pytest.mark.django_db def test_workspace_guest_can_read_a_global_view(self, workspace, workspace_view): """Positive control: the role set matches list(), which permits guests."""