fix: 404 instead of a hollow 200 when a global view does not exist

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 <noreply@plane.so>
This commit is contained in:
Manish Gupta
2026-08-20 18:11:23 +05:30
parent 463cba8557
commit 999514fffc
2 changed files with 26 additions and 0 deletions

View File

@@ -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,

View File

@@ -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."""