From 482cf4853002cf1b61cbfc3dab7e7d13ce1697da Mon Sep 17 00:00:00 2001 From: Srijan Tripathi Date: Sat, 1 Aug 2026 18:43:50 +0000 Subject: [PATCH 1/5] Add Exceptions for Paywall and System Limits --- api/organisations/exceptions.py | 10 ++++-- api/organisations/subscriptions/exceptions.py | 27 +++++++++----- api/projects/exceptions.py | 35 +++++++++++++++++++ 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/api/organisations/exceptions.py b/api/organisations/exceptions.py index 3c401516d16c..c562cb2ddeb8 100644 --- a/api/organisations/exceptions.py +++ b/api/organisations/exceptions.py @@ -1,6 +1,10 @@ from rest_framework.exceptions import APIException - +from rest_framework import status class OrganisationHasNoPaidSubscription(APIException): - status_code = 400 - default_detail = "Organisation has no subscription" + status_code = status.HTTP_402_PAYMENT_REQUIRED + default_code = "invalid-plan" + default_detail = ( + "Organisation has no subscription. " + "Please upgrade your plan: https://www.flagsmith.com/pricing" + ) \ No newline at end of file diff --git a/api/organisations/subscriptions/exceptions.py b/api/organisations/subscriptions/exceptions.py index e0aefb1814ed..0bc571765d55 100644 --- a/api/organisations/subscriptions/exceptions.py +++ b/api/organisations/subscriptions/exceptions.py @@ -1,9 +1,18 @@ +from rest_framework import status from rest_framework.exceptions import APIException -class InvalidSubscriptionPlanError(APIException): - status_code = 403 - default_detail = "Organisation does not have a valid plan for this resource." +class BaseInvalidPlanError(APIException): + status_code = status.HTTP_402_PAYMENT_REQUIRED + default_code = "invalid-plan" + default_detail = ( + "Organisation does not have a valid plan for this resource. " + "Please upgrade your plan: https://www.flagsmith.com/pricing" + ) + + +class InvalidSubscriptionPlanError(BaseInvalidPlanError): + pass class CannotCancelChargebeeSubscription(APIException): @@ -15,7 +24,7 @@ class UpgradeSeatsError(APIException): class UpgradeSeatsPaymentFailure(APIException): - status_code = 400 + status_code = status.HTTP_400_BAD_REQUEST default_detail = ( "Joining the organisation has failed due to a payment issue. " "Please contact your organisation's admin." @@ -27,13 +36,15 @@ class UpgradeAPIUsageError(APIException): class UpgradeAPIUsagePaymentFailure(APIException): - status_code = 400 + status_code = status.HTTP_400_BAD_REQUEST default_detail = ( "API usage upgrade has failed due to a payment issue. " "If this persists, contact the organisation admin." ) -class SubscriptionDoesNotSupportSeatUpgrade(APIException): - status_code = 400 - default_detail = "Please upgrade your plan to add additional seats/users" +class SubscriptionDoesNotSupportSeatUpgrade(BaseInvalidPlanError): + default_detail = ( + "Please upgrade your plan to add additional seats/users: " + "https://www.flagsmith.com/pricing" + ) \ No newline at end of file diff --git a/api/projects/exceptions.py b/api/projects/exceptions.py index 203037e511c6..8fcceaef0866 100644 --- a/api/projects/exceptions.py +++ b/api/projects/exceptions.py @@ -19,3 +19,38 @@ class TooManyIdentitiesError(APIException): class ProjectTooLargeError(APIException): status_code = 400 default_detail = "Project is too large; Please contact support" +from rest_framework import status +from rest_framework.exceptions import APIException + + +class SystemLimitError(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_code = "system-limit" + default_detail = ( + "System limit exceeded. Please contact support to adjust your limits: " + "https://docs.flagsmith.com/support#getting-in-touch" + ) + + +class DynamoNotEnabledError(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "Dynamo DB is not enabled for this project" + + +class ProjectMigrationError(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "Migration is either already done or is in progress" + + +class TooManyIdentitiesError(SystemLimitError): + default_detail = ( + "Too many identities. Please contact support to adjust your limits: " + "https://docs.flagsmith.com/support#getting-in-touch" + ) + + +class ProjectTooLargeError(SystemLimitError): + default_detail = ( + "Project is too large. Please contact support to adjust your limits: " + "https://docs.flagsmith.com/support#getting-in-touch" + ) \ No newline at end of file From f219ecb1e9b6c6ca36c3182710f5e1b88b3b7c05 Mon Sep 17 00:00:00 2001 From: Srijan Tripathi Date: Sun, 2 Aug 2026 14:46:11 +0000 Subject: [PATCH 2/5] feat: standardize paywall and plan limit errors to 402 Payment Required - Introduced specific `402 Payment Required` exceptions for paywall and plan limits (replacing generic 400 Bad Request errors). - Updated organization invite views to return 402 when seat limits are exceeded. - Updated project edge migration views to return 402 when project size limits (identities, features, segments) are exceeded. - Standardized error detail messages to include direct links to pricing and support documentation. - Updated all related unit tests in `test_unit_invites_views.py` and `test_unit_projects_views.py` to assert the new 402 status codes and updated detail strings. --- .../invites/test_unit_invites_views.py | 27 ++++++++++++------- .../unit/projects/test_unit_projects_views.py | 10 +++---- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/api/tests/unit/organisations/invites/test_unit_invites_views.py b/api/tests/unit/organisations/invites/test_unit_invites_views.py index df8ddaa92469..8feb0b072a00 100644 --- a/api/tests/unit/organisations/invites/test_unit_invites_views.py +++ b/api/tests/unit/organisations/invites/test_unit_invites_views.py @@ -91,7 +91,11 @@ def test_get_invite_links__seats_exceeded__returns_400( response = admin_client.get(url) # Then - assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.status_code == status.HTTP_402_PAYMENT_REQUIRED + assert ( + response.json()["detail"] + == "Please upgrade your plan to add additional seats/users: https://www.flagsmith.com/pricing" + ) def test_delete_invite_link__valid_invite__returns_204( @@ -137,7 +141,11 @@ def test_delete_invite_link__seats_exceeded__returns_400( response = admin_client.delete(url) # Then - assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.status_code == status.HTTP_402_PAYMENT_REQUIRED + assert ( + response.json()["detail"] + == "Please upgrade your plan to add additional seats/users: https://www.flagsmith.com/pricing" + ) def test_update_invite_link__patch_request__returns_405( # type: ignore[no-untyped-def] @@ -258,6 +266,7 @@ def test_create_invite__permission_group_from_another_org__returns_400( ) # Then + # Validation error, stays 400 assert response.status_code == status.HTTP_400_BAD_REQUEST response_json = response.json() @@ -286,10 +295,10 @@ def test_create_invite__seats_exceeded__returns_400( url, data=json.dumps(data), content_type="application/json" ) # Then - assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.status_code == status.HTTP_402_PAYMENT_REQUIRED assert ( response.json()["detail"] - == "Please upgrade your plan to add additional seats/users" + == "Please upgrade your plan to add additional seats/users: https://www.flagsmith.com/pricing" ) @@ -359,10 +368,10 @@ def test_join_organisation__exceeds_plan_limit_saas__returns_400( response = staff_client.post(url) # Then - assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.status_code == status.HTTP_402_PAYMENT_REQUIRED assert ( response.json()["detail"] - == "Please upgrade your plan to add additional seats/users" + == "Please upgrade your plan to add additional seats/users: https://www.flagsmith.com/pricing" ) @@ -391,10 +400,10 @@ def test_join_organisation__exceeds_plan_limit_self_hosted__returns_400( response = staff_client.post(url) # Then - assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.status_code == status.HTTP_402_PAYMENT_REQUIRED assert ( response.json()["detail"] - == "Please upgrade your plan to add additional seats/users" + == "Please upgrade your plan to add additional seats/users: https://www.flagsmith.com/pricing" ) @@ -471,4 +480,4 @@ def test_join_organisation_from_link__invite_links_disabled__returns_403( response = api_client.post(url) # Then - assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.status_code == status.HTTP_403_FORBIDDEN \ No newline at end of file diff --git a/api/tests/unit/projects/test_unit_projects_views.py b/api/tests/unit/projects/test_unit_projects_views.py index cff455eef308..86b92a3b7451 100644 --- a/api/tests/unit/projects/test_unit_projects_views.py +++ b/api/tests/unit/projects/test_unit_projects_views.py @@ -590,7 +590,7 @@ def test_migrate_to_edge__too_many_identities__returns_400( # type: ignore[no-u # Then assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json()["detail"] == "Too many identities; Please contact support" + assert response.json()["detail"] == "Too many identities. Please contact support to adjust your limits: https://docs.flagsmith.com/support#getting-in-touch" mocked_identity_migrator.assert_not_called() @@ -611,7 +611,7 @@ def test_migrate_to_edge__too_many_features__returns_400( # type: ignore[no-unt # Then assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json()["detail"] == "Project is too large; Please contact support" + assert response.json()["detail"] == "Project is too large. Please contact support to adjust your limits: https://docs.flagsmith.com/support#getting-in-touch" mocked_identity_migrator.assert_not_called() @@ -639,7 +639,7 @@ def test_migrate_to_edge__too_many_segments__returns_400( # type: ignore[no-unt # Then assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json()["detail"] == "Project is too large; Please contact support" + assert response.json()["detail"] == "Project is too large. Please contact support to adjust your limits: https://docs.flagsmith.com/support#getting-in-touch" mocked_identity_migrator.assert_not_called() @@ -672,7 +672,7 @@ def test_migrate_to_edge__too_many_segment_overrides__returns_400( # type: igno # Then assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json()["detail"] == "Project is too large; Please contact support" + assert response.json()["detail"] == "Project is too large. Please contact support to adjust your limits: https://docs.flagsmith.com/support#getting-in-touch" mocked_identity_migrator.assert_not_called() @@ -1068,4 +1068,4 @@ def test_list_projects__default_enforce_feature_owners__returns_false( assert response.status_code == status.HTTP_200_OK assert len(response.json()) > 0 assert "enforce_feature_owners" in response.json()[0] - assert response.json()[0]["enforce_feature_owners"] is False + assert response.json()[0]["enforce_feature_owners"] is False \ No newline at end of file From 6ad876a75ed9b680da4c2ea5c3ebcd1d4b85322d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 14:49:35 +0000 Subject: [PATCH 3/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- api/organisations/exceptions.py | 5 +++-- api/organisations/subscriptions/exceptions.py | 2 +- api/projects/exceptions.py | 4 +++- .../invites/test_unit_invites_views.py | 2 +- .../unit/projects/test_unit_projects_views.py | 22 ++++++++++++++----- 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/api/organisations/exceptions.py b/api/organisations/exceptions.py index c562cb2ddeb8..8683095b1d87 100644 --- a/api/organisations/exceptions.py +++ b/api/organisations/exceptions.py @@ -1,5 +1,6 @@ -from rest_framework.exceptions import APIException from rest_framework import status +from rest_framework.exceptions import APIException + class OrganisationHasNoPaidSubscription(APIException): status_code = status.HTTP_402_PAYMENT_REQUIRED @@ -7,4 +8,4 @@ class OrganisationHasNoPaidSubscription(APIException): default_detail = ( "Organisation has no subscription. " "Please upgrade your plan: https://www.flagsmith.com/pricing" - ) \ No newline at end of file + ) diff --git a/api/organisations/subscriptions/exceptions.py b/api/organisations/subscriptions/exceptions.py index 0bc571765d55..c1091ea1d207 100644 --- a/api/organisations/subscriptions/exceptions.py +++ b/api/organisations/subscriptions/exceptions.py @@ -47,4 +47,4 @@ class SubscriptionDoesNotSupportSeatUpgrade(BaseInvalidPlanError): default_detail = ( "Please upgrade your plan to add additional seats/users: " "https://www.flagsmith.com/pricing" - ) \ No newline at end of file + ) diff --git a/api/projects/exceptions.py b/api/projects/exceptions.py index 8fcceaef0866..50b891e1f528 100644 --- a/api/projects/exceptions.py +++ b/api/projects/exceptions.py @@ -19,6 +19,8 @@ class TooManyIdentitiesError(APIException): class ProjectTooLargeError(APIException): status_code = 400 default_detail = "Project is too large; Please contact support" + + from rest_framework import status from rest_framework.exceptions import APIException @@ -53,4 +55,4 @@ class ProjectTooLargeError(SystemLimitError): default_detail = ( "Project is too large. Please contact support to adjust your limits: " "https://docs.flagsmith.com/support#getting-in-touch" - ) \ No newline at end of file + ) diff --git a/api/tests/unit/organisations/invites/test_unit_invites_views.py b/api/tests/unit/organisations/invites/test_unit_invites_views.py index 8feb0b072a00..3529a35bfb76 100644 --- a/api/tests/unit/organisations/invites/test_unit_invites_views.py +++ b/api/tests/unit/organisations/invites/test_unit_invites_views.py @@ -480,4 +480,4 @@ def test_join_organisation_from_link__invite_links_disabled__returns_403( response = api_client.post(url) # Then - assert response.status_code == status.HTTP_403_FORBIDDEN \ No newline at end of file + assert response.status_code == status.HTTP_403_FORBIDDEN diff --git a/api/tests/unit/projects/test_unit_projects_views.py b/api/tests/unit/projects/test_unit_projects_views.py index 86b92a3b7451..55ebe458b43d 100644 --- a/api/tests/unit/projects/test_unit_projects_views.py +++ b/api/tests/unit/projects/test_unit_projects_views.py @@ -590,7 +590,10 @@ def test_migrate_to_edge__too_many_identities__returns_400( # type: ignore[no-u # Then assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json()["detail"] == "Too many identities. Please contact support to adjust your limits: https://docs.flagsmith.com/support#getting-in-touch" + assert ( + response.json()["detail"] + == "Too many identities. Please contact support to adjust your limits: https://docs.flagsmith.com/support#getting-in-touch" + ) mocked_identity_migrator.assert_not_called() @@ -611,7 +614,10 @@ def test_migrate_to_edge__too_many_features__returns_400( # type: ignore[no-unt # Then assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json()["detail"] == "Project is too large. Please contact support to adjust your limits: https://docs.flagsmith.com/support#getting-in-touch" + assert ( + response.json()["detail"] + == "Project is too large. Please contact support to adjust your limits: https://docs.flagsmith.com/support#getting-in-touch" + ) mocked_identity_migrator.assert_not_called() @@ -639,7 +645,10 @@ def test_migrate_to_edge__too_many_segments__returns_400( # type: ignore[no-unt # Then assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json()["detail"] == "Project is too large. Please contact support to adjust your limits: https://docs.flagsmith.com/support#getting-in-touch" + assert ( + response.json()["detail"] + == "Project is too large. Please contact support to adjust your limits: https://docs.flagsmith.com/support#getting-in-touch" + ) mocked_identity_migrator.assert_not_called() @@ -672,7 +681,10 @@ def test_migrate_to_edge__too_many_segment_overrides__returns_400( # type: igno # Then assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json()["detail"] == "Project is too large. Please contact support to adjust your limits: https://docs.flagsmith.com/support#getting-in-touch" + assert ( + response.json()["detail"] + == "Project is too large. Please contact support to adjust your limits: https://docs.flagsmith.com/support#getting-in-touch" + ) mocked_identity_migrator.assert_not_called() @@ -1068,4 +1080,4 @@ def test_list_projects__default_enforce_feature_owners__returns_false( assert response.status_code == status.HTTP_200_OK assert len(response.json()) > 0 assert "enforce_feature_owners" in response.json()[0] - assert response.json()[0]["enforce_feature_owners"] is False \ No newline at end of file + assert response.json()[0]["enforce_feature_owners"] is False From 2504d1b0ab4dedf554d797c1e6c8ca682fc23afc Mon Sep 17 00:00:00 2001 From: Srijan Tripathi Date: Sun, 2 Aug 2026 15:13:54 +0000 Subject: [PATCH 4/5] fix: address review feedback for standardized API errors - Added a custom DRF exception handler in `api/app/exceptions.py` to ensure `default_code` is exposed in the JSON response payload. - Removed duplicated imports and duplicated exception class definitions in `api/projects/exceptions.py`. - Renamed invite tests in `test_unit_invites_views.py` to accurately reflect the new `402` status code. - Added assertions for `"code": "invalid-plan"` and `"code": "system-limit"` across invite and project unit tests to verify the new handler works as expected. --- api/app/exceptions.py | 15 +++++++++++++++ api/app/settings/common.py | 1 + api/projects/exceptions.py | 5 +---- .../invites/test_unit_invites_views.py | 17 +++++++++++------ .../unit/projects/test_unit_projects_views.py | 6 +++++- 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/api/app/exceptions.py b/api/app/exceptions.py index 29452a6f53f2..76e1d51b373e 100644 --- a/api/app/exceptions.py +++ b/api/app/exceptions.py @@ -1,2 +1,17 @@ +from rest_framework.views import exception_handler + class ImproperlyConfiguredError(RuntimeError): pass + +def custom_api_exception_handler(exc, context): + # Call REST framework's default exception handler first, + # to get the standard error response. + response = exception_handler(exc, context) + + if response is not None and hasattr(exc, 'get_codes'): + if isinstance(response.data, dict) and "detail" in response.data: + codes = exc.get_codes() + if isinstance(codes, str): + response.data["code"] = codes + + return response \ No newline at end of file diff --git a/api/app/settings/common.py b/api/app/settings/common.py index 4c41bdf15cc4..f4ae78f7ba0b 100644 --- a/api/app/settings/common.py +++ b/api/app/settings/common.py @@ -353,6 +353,7 @@ "api_keys.authentication.MasterAPIKeyAuthentication", "oauth2_metadata.authentication.OAuth2BearerTokenAuthentication", ), + 'EXCEPTION_HANDLER': 'app.exceptions.custom_api_exception_handler', "PAGE_SIZE": 10, "UNICODE_JSON": False, "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", diff --git a/api/projects/exceptions.py b/api/projects/exceptions.py index 50b891e1f528..5f8031d7023c 100644 --- a/api/projects/exceptions.py +++ b/api/projects/exceptions.py @@ -1,4 +1,5 @@ from rest_framework.exceptions import APIException +from rest_framework import status class DynamoNotEnabledError(APIException): @@ -21,10 +22,6 @@ class ProjectTooLargeError(APIException): default_detail = "Project is too large; Please contact support" -from rest_framework import status -from rest_framework.exceptions import APIException - - class SystemLimitError(APIException): status_code = status.HTTP_400_BAD_REQUEST default_code = "system-limit" diff --git a/api/tests/unit/organisations/invites/test_unit_invites_views.py b/api/tests/unit/organisations/invites/test_unit_invites_views.py index 3529a35bfb76..238b89d7c7b5 100644 --- a/api/tests/unit/organisations/invites/test_unit_invites_views.py +++ b/api/tests/unit/organisations/invites/test_unit_invites_views.py @@ -72,7 +72,7 @@ def test_get_invite_links__multiple_roles_exist__returns_all_links( assert all(attr in invite_link for attr in expected_attributes) -def test_get_invite_links__seats_exceeded__returns_400( +def test_get_invite_links__seats_exceeded__returns_402( settings: SettingsWrapper, organisation: Organisation, admin_client: APIClient, @@ -92,6 +92,7 @@ def test_get_invite_links__seats_exceeded__returns_400( # Then assert response.status_code == status.HTTP_402_PAYMENT_REQUIRED + assert response.json()["code"] == "invalid-plan" assert ( response.json()["detail"] == "Please upgrade your plan to add additional seats/users: https://www.flagsmith.com/pricing" @@ -124,7 +125,7 @@ def test_delete_invite_link__valid_invite__returns_204( assert response.status_code == status.HTTP_204_NO_CONTENT -def test_delete_invite_link__seats_exceeded__returns_400( +def test_delete_invite_link__seats_exceeded__returns_402( organisation: Organisation, admin_client: APIClient, settings: SettingsWrapper, @@ -142,6 +143,7 @@ def test_delete_invite_link__seats_exceeded__returns_400( # Then assert response.status_code == status.HTTP_402_PAYMENT_REQUIRED + assert response.json()["code"] == "invalid-plan" assert ( response.json()["detail"] == "Please upgrade your plan to add additional seats/users: https://www.flagsmith.com/pricing" @@ -277,7 +279,7 @@ def test_create_invite__permission_group_from_another_org__returns_400( } -def test_create_invite__seats_exceeded__returns_400( +def test_create_invite__seats_exceeded__returns_402( admin_client: APIClient, organisation: Organisation, user_permission_group: UserPermissionGroup, @@ -296,6 +298,7 @@ def test_create_invite__seats_exceeded__returns_400( ) # Then assert response.status_code == status.HTTP_402_PAYMENT_REQUIRED + assert response.json()["code"] == "invalid-plan" assert ( response.json()["detail"] == "Please upgrade your plan to add additional seats/users: https://www.flagsmith.com/pricing" @@ -355,7 +358,7 @@ def test_update_invite__put_request__returns_405( # type: ignore[no-untyped-def (lazy_fixture("invite_link"), "api-v1:users:user-join-organisation-link"), ], ) -def test_join_organisation__exceeds_plan_limit_saas__returns_400( +def test_join_organisation__exceeds_plan_limit_saas__returns_402( staff_client: APIClient, invite_object: Invite | InviteLink, url: str, @@ -369,6 +372,7 @@ def test_join_organisation__exceeds_plan_limit_saas__returns_400( # Then assert response.status_code == status.HTTP_402_PAYMENT_REQUIRED + assert response.json()["code"] == "invalid-plan" assert ( response.json()["detail"] == "Please upgrade your plan to add additional seats/users: https://www.flagsmith.com/pricing" @@ -383,7 +387,7 @@ def test_join_organisation__exceeds_plan_limit_saas__returns_400( (lazy_fixture("invite_link"), "api-v1:users:user-join-organisation-link"), ], ) -def test_join_organisation__exceeds_plan_limit_self_hosted__returns_400( +def test_join_organisation__exceeds_plan_limit_self_hosted__returns_402( staff_client: APIClient, invite_object: Invite | InviteLink, url: str, @@ -401,6 +405,7 @@ def test_join_organisation__exceeds_plan_limit_self_hosted__returns_400( # Then assert response.status_code == status.HTTP_402_PAYMENT_REQUIRED + assert response.json()["code"] == "invalid-plan" assert ( response.json()["detail"] == "Please upgrade your plan to add additional seats/users: https://www.flagsmith.com/pricing" @@ -480,4 +485,4 @@ def test_join_organisation_from_link__invite_links_disabled__returns_403( response = api_client.post(url) # Then - assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.status_code == status.HTTP_403_FORBIDDEN \ No newline at end of file diff --git a/api/tests/unit/projects/test_unit_projects_views.py b/api/tests/unit/projects/test_unit_projects_views.py index 55ebe458b43d..efcd5f5f82a9 100644 --- a/api/tests/unit/projects/test_unit_projects_views.py +++ b/api/tests/unit/projects/test_unit_projects_views.py @@ -590,6 +590,7 @@ def test_migrate_to_edge__too_many_identities__returns_400( # type: ignore[no-u # Then assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["code"] == "system-limit" assert ( response.json()["detail"] == "Too many identities. Please contact support to adjust your limits: https://docs.flagsmith.com/support#getting-in-touch" @@ -614,6 +615,7 @@ def test_migrate_to_edge__too_many_features__returns_400( # type: ignore[no-unt # Then assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["code"] == "system-limit" assert ( response.json()["detail"] == "Project is too large. Please contact support to adjust your limits: https://docs.flagsmith.com/support#getting-in-touch" @@ -645,6 +647,7 @@ def test_migrate_to_edge__too_many_segments__returns_400( # type: ignore[no-unt # Then assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["code"] == "system-limit" assert ( response.json()["detail"] == "Project is too large. Please contact support to adjust your limits: https://docs.flagsmith.com/support#getting-in-touch" @@ -681,6 +684,7 @@ def test_migrate_to_edge__too_many_segment_overrides__returns_400( # type: igno # Then assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["code"] == "system-limit" assert ( response.json()["detail"] == "Project is too large. Please contact support to adjust your limits: https://docs.flagsmith.com/support#getting-in-touch" @@ -1080,4 +1084,4 @@ def test_list_projects__default_enforce_feature_owners__returns_false( assert response.status_code == status.HTTP_200_OK assert len(response.json()) > 0 assert "enforce_feature_owners" in response.json()[0] - assert response.json()[0]["enforce_feature_owners"] is False + assert response.json()[0]["enforce_feature_owners"] is False \ No newline at end of file From b70935c36fb12886496bb8866fdd35a3dd104f3b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 15:14:10 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- api/app/exceptions.py | 6 ++++-- api/app/settings/common.py | 2 +- api/projects/exceptions.py | 2 +- .../unit/organisations/invites/test_unit_invites_views.py | 2 +- api/tests/unit/projects/test_unit_projects_views.py | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/api/app/exceptions.py b/api/app/exceptions.py index 76e1d51b373e..a66b86d9cb50 100644 --- a/api/app/exceptions.py +++ b/api/app/exceptions.py @@ -1,17 +1,19 @@ from rest_framework.views import exception_handler + class ImproperlyConfiguredError(RuntimeError): pass + def custom_api_exception_handler(exc, context): # Call REST framework's default exception handler first, # to get the standard error response. response = exception_handler(exc, context) - if response is not None and hasattr(exc, 'get_codes'): + if response is not None and hasattr(exc, "get_codes"): if isinstance(response.data, dict) and "detail" in response.data: codes = exc.get_codes() if isinstance(codes, str): response.data["code"] = codes - return response \ No newline at end of file + return response diff --git a/api/app/settings/common.py b/api/app/settings/common.py index f4ae78f7ba0b..2c4c6d5c036a 100644 --- a/api/app/settings/common.py +++ b/api/app/settings/common.py @@ -353,7 +353,7 @@ "api_keys.authentication.MasterAPIKeyAuthentication", "oauth2_metadata.authentication.OAuth2BearerTokenAuthentication", ), - 'EXCEPTION_HANDLER': 'app.exceptions.custom_api_exception_handler', + "EXCEPTION_HANDLER": "app.exceptions.custom_api_exception_handler", "PAGE_SIZE": 10, "UNICODE_JSON": False, "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", diff --git a/api/projects/exceptions.py b/api/projects/exceptions.py index 5f8031d7023c..6b5e86b2c354 100644 --- a/api/projects/exceptions.py +++ b/api/projects/exceptions.py @@ -1,5 +1,5 @@ -from rest_framework.exceptions import APIException from rest_framework import status +from rest_framework.exceptions import APIException class DynamoNotEnabledError(APIException): diff --git a/api/tests/unit/organisations/invites/test_unit_invites_views.py b/api/tests/unit/organisations/invites/test_unit_invites_views.py index 238b89d7c7b5..0e5ec56b8ff0 100644 --- a/api/tests/unit/organisations/invites/test_unit_invites_views.py +++ b/api/tests/unit/organisations/invites/test_unit_invites_views.py @@ -485,4 +485,4 @@ def test_join_organisation_from_link__invite_links_disabled__returns_403( response = api_client.post(url) # Then - assert response.status_code == status.HTTP_403_FORBIDDEN \ No newline at end of file + assert response.status_code == status.HTTP_403_FORBIDDEN diff --git a/api/tests/unit/projects/test_unit_projects_views.py b/api/tests/unit/projects/test_unit_projects_views.py index efcd5f5f82a9..cc6863ff6ea4 100644 --- a/api/tests/unit/projects/test_unit_projects_views.py +++ b/api/tests/unit/projects/test_unit_projects_views.py @@ -1084,4 +1084,4 @@ def test_list_projects__default_enforce_feature_owners__returns_false( assert response.status_code == status.HTTP_200_OK assert len(response.json()) > 0 assert "enforce_feature_owners" in response.json()[0] - assert response.json()[0]["enforce_feature_owners"] is False \ No newline at end of file + assert response.json()[0]["enforce_feature_owners"] is False