Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/api/plane/api/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def dispatch(self, request, *args, **kwargs):
return response
except Exception as exc:
response = self.handle_exception(exc)
return exc
return response

def finalize_response(self, request, response, *args, **kwargs):
# Call super to get the default response
Expand Down
4 changes: 2 additions & 2 deletions apps/api/plane/app/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def dispatch(self, request, *args, **kwargs):
return response
except Exception as exc:
response = self.handle_exception(exc)
return exc
return response

@property
def workspace_slug(self):
Expand Down Expand Up @@ -215,7 +215,7 @@ def dispatch(self, request, *args, **kwargs):

except Exception as exc:
response = self.handle_exception(exc)
return exc
return response

@property
def workspace_slug(self):
Expand Down
2 changes: 1 addition & 1 deletion apps/api/plane/license/api/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def dispatch(self, request, *args, **kwargs):

except Exception as exc:
response = self.handle_exception(exc)
return exc
return response

@property
def fields(self):
Expand Down
4 changes: 2 additions & 2 deletions apps/api/plane/space/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def dispatch(self, request, *args, **kwargs):
return response
except Exception as exc:
response = self.handle_exception(exc)
return exc
return response

@property
def workspace_slug(self):
Expand Down Expand Up @@ -197,7 +197,7 @@ def dispatch(self, request, *args, **kwargs):

except Exception as exc:
response = self.handle_exception(exc)
return exc
return response

@property
def workspace_slug(self):
Expand Down
3 changes: 3 additions & 0 deletions apps/api/plane/tests/unit/views/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
69 changes: 69 additions & 0 deletions apps/api/plane/tests/unit/views/test_base_dispatch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.

"""
Regression tests for the ``dispatch()`` exception handling on the shared
``BaseAPIView`` / ``BaseViewSet`` classes.

When ``super().dispatch()`` raises an unhandled exception, ``dispatch()`` must
return the HTTP ``Response`` produced by ``handle_exception()`` -- not the raw
exception object. Returning the exception causes Django's response pipeline to
fail with ``TypeError: 'Exception' object is not a valid HTTP response``.

See: https://github.com/makeplane/plane/issues/9157
"""

import pytest
from unittest.mock import patch

from rest_framework import status
from rest_framework.response import Response
from rest_framework.test import APIRequestFactory

from plane.api.views.base import BaseAPIView as ApiBaseAPIView, BaseViewSet as ApiBaseViewSet
from plane.app.views.base import BaseAPIView as AppBaseAPIView, BaseViewSet as AppBaseViewSet
from plane.license.api.views.base import BaseAPIView as LicenseBaseAPIView
from plane.space.views.base import BaseAPIView as SpaceBaseAPIView, BaseViewSet as SpaceBaseViewSet


# Every shared base view that wraps ``super().dispatch()`` in a try/except.
VIEW_CLASSES = [
ApiBaseAPIView,
ApiBaseViewSet,
AppBaseAPIView,
AppBaseViewSet,
LicenseBaseAPIView,
SpaceBaseAPIView,
SpaceBaseViewSet,
]


@pytest.mark.unit
@pytest.mark.parametrize(
"view_class",
VIEW_CLASSES,
ids=lambda c: f"{c.__module__}.{c.__name__}",
)
def test_dispatch_returns_response_when_super_dispatch_raises(view_class):
"""dispatch() must return handle_exception()'s Response, not the exception."""
request = APIRequestFactory().get("/api/test/")
view = view_class()

sentinel = Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

with (
patch("rest_framework.views.APIView.dispatch", side_effect=RuntimeError("boom")),
patch.object(view_class, "handle_exception", return_value=sentinel) as mock_handle,
):
result = view.dispatch(request)

mock_handle.assert_called_once()
assert isinstance(result, Response), (
f"{view_class.__module__}.{view_class.__name__}.dispatch() returned "
f"{type(result).__name__} instead of an HTTP Response"
)
assert result is sentinel
Loading