From c778d836cbe1868d18ecf3baec22a1fa5379a136 Mon Sep 17 00:00:00 2001 From: Program2113 Date: Tue, 22 Sep 2026 17:11:37 +0530 Subject: [PATCH 1/2] fix(api): return 404 for a malformed work item identifier The route `workspaces//issues/-/` splits a path segment on its last hyphen. When a caller passes a UUID there, `issue_identifier` ends up as a hex fragment, and filtering the integer `sequence_id` column on it raises ValueError: Field 'sequence_id' expected a number but got 'e4c5320be796'. `BaseAPIView.handle_exception` maps `ObjectDoesNotExist` to 404 but lets `ValueError` fall through to a 500, so a simple bad URL looked like a server fault. Validate the identifier up front and return the same 404 body a genuinely missing work item produces. This also fixes a latent bug in the same handler: when either identifier was falsy the method fell off the end and returned `None`, which Django reports as "view didn't return an HttpResponse object" - another 500. Fixes PLANE-API-7YJ Fixes PLANE-API-95M Co-Authored-By: Claude Opus 5 --- apps/api/plane/api/views/issue.py | 37 ++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/apps/api/plane/api/views/issue.py b/apps/api/plane/api/views/issue.py index da9edc66d66a..13564fea4404 100644 --- a/apps/api/plane/api/views/issue.py +++ b/apps/api/plane/api/views/issue.py @@ -236,22 +236,33 @@ def get(self, request, slug, project_identifier=None, issue_identifier=None): Retrieve a specific work item using workspace slug, project identifier, and issue identifier. This endpoint provides workspace-level access to work items. """ - if issue_identifier and project_identifier: - issue = Issue.issue_objects.annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ).get( - workspace__slug=slug, - project__identifier=project_identifier, - sequence_id=issue_identifier, - ) + # `-` splits a path segment on its + # last hyphen, so a UUID in that position leaves a non-numeric + # `issue_identifier`. Filtering `sequence_id` on it raises ValueError, + # which surfaces as a 500 rather than a 404. isdecimal() is used over + # isdigit() because the latter also accepts superscript digits, which + # int() then rejects. + if not (project_identifier and issue_identifier and issue_identifier.isdecimal()): return Response( - IssueSerializer(issue, fields=self.fields, expand=self.expand).data, - status=status.HTTP_200_OK, + {"error": "The requested resource does not exist."}, + status=status.HTTP_404_NOT_FOUND, ) + issue = Issue.issue_objects.annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ).get( + workspace__slug=slug, + project__identifier=project_identifier, + sequence_id=issue_identifier, + ) + return Response( + IssueSerializer(issue, fields=self.fields, expand=self.expand).data, + status=status.HTTP_200_OK, + ) + class IssueListCreateAPIEndpoint(BaseAPIView): """ From 04635b1f9c1fd93b1be26490d8065d349982329b Mon Sep 17 00:00:00 2001 From: Program2113 Date: Wed, 23 Sep 2026 12:27:50 +0530 Subject: [PATCH 2/2] fix(api): also 404 identifiers past CPython's int conversion limit isdecimal() alone left one residual 500. CPython refuses to convert decimal strings longer than 4300 digits, so a path segment of 4301+ digits passes the isdecimal() guard, and Django's IntegerField.get_prep_value then re-raises the ValueError that this change set out to eliminate. Convert the identifier up front and treat a failed conversion as a 404, the same as any other malformed identifier. Co-Authored-By: Claude Opus 5 --- apps/api/plane/api/views/issue.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/api/plane/api/views/issue.py b/apps/api/plane/api/views/issue.py index 13564fea4404..b21474d1bb68 100644 --- a/apps/api/plane/api/views/issue.py +++ b/apps/api/plane/api/views/issue.py @@ -239,10 +239,19 @@ def get(self, request, slug, project_identifier=None, issue_identifier=None): # `-` splits a path segment on its # last hyphen, so a UUID in that position leaves a non-numeric # `issue_identifier`. Filtering `sequence_id` on it raises ValueError, - # which surfaces as a 500 rather than a 404. isdecimal() is used over - # isdigit() because the latter also accepts superscript digits, which - # int() then rejects. - if not (project_identifier and issue_identifier and issue_identifier.isdecimal()): + # which surfaces as a 500 rather than a 404. + sequence_id = None + if project_identifier and issue_identifier: + try: + # isdecimal() rather than isdigit(), which also accepts + # superscript digits that int() then rejects. int() covers the + # remaining case: CPython refuses to convert decimal strings + # longer than 4300 digits. + sequence_id = int(issue_identifier) if issue_identifier.isdecimal() else None + except ValueError: + sequence_id = None + + if sequence_id is None: return Response( {"error": "The requested resource does not exist."}, status=status.HTTP_404_NOT_FOUND, @@ -256,7 +265,7 @@ def get(self, request, slug, project_identifier=None, issue_identifier=None): ).get( workspace__slug=slug, project__identifier=project_identifier, - sequence_id=issue_identifier, + sequence_id=sequence_id, ) return Response( IssueSerializer(issue, fields=self.fields, expand=self.expand).data,