mirror of
https://github.com/inventree/InvenTree.git
synced 2026-08-10 15:36:17 +00:00
[API] Handle AppRegistryNotReady (#12571)
Explicit handling for AppRegistryNotReady error
This commit is contained in:
@@ -7,6 +7,7 @@ import traceback
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import AppRegistryNotReady
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@@ -121,6 +122,24 @@ def exception_handler(exc, context):
|
|||||||
# If sentry.io fails, we don't want to crash the server!
|
# If sentry.io fails, we don't want to crash the server!
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# The Django app registry can be transiently un-ready while the plugin
|
||||||
|
# registry is reloading apps (see plugin.registry.PluginsRegistry._reload_apps).
|
||||||
|
# Any request handled by another thread/worker during that window can trip
|
||||||
|
# this - it is not a real server error, so ask the client to retry shortly
|
||||||
|
# rather than surfacing a 500.
|
||||||
|
if isinstance(exc, AppRegistryNotReady):
|
||||||
|
response = Response(
|
||||||
|
{
|
||||||
|
'error': 'AppRegistryNotReady',
|
||||||
|
'detail': _(
|
||||||
|
'Server is temporarily reloading, please retry the request'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
status=503,
|
||||||
|
)
|
||||||
|
response['Retry-After'] = '1'
|
||||||
|
return response
|
||||||
|
|
||||||
# Catch any django validation error, and re-throw a DRF validation error
|
# Catch any django validation error, and re-throw a DRF validation error
|
||||||
if isinstance(exc, DjangoValidationError):
|
if isinstance(exc, DjangoValidationError):
|
||||||
exc = DRFValidationError(detail=serializers.as_serializer_error(exc))
|
exc = DRFValidationError(detail=serializers.as_serializer_error(exc))
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ from base64 import b64encode
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
from django.core.exceptions import AppRegistryNotReady
|
||||||
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from InvenTree.api import read_license_file
|
from InvenTree.api import read_license_file
|
||||||
from InvenTree.api_version import INVENTREE_API_VERSION
|
from InvenTree.api_version import INVENTREE_API_VERSION
|
||||||
|
from InvenTree.exceptions import exception_handler
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase
|
from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase
|
||||||
from InvenTree.version import inventreeApiText, parse_version_text
|
from InvenTree.version import inventreeApiText, parse_version_text
|
||||||
from users.ruleset import RULESET_NAMES
|
from users.ruleset import RULESET_NAMES
|
||||||
@@ -66,6 +69,25 @@ class HTMLAPITests(InvenTreeTestCase):
|
|||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
|
||||||
|
class ExceptionHandlerTests(TestCase):
|
||||||
|
"""Tests for the custom DRF exception handler."""
|
||||||
|
|
||||||
|
def test_app_registry_not_ready(self):
|
||||||
|
"""AppRegistryNotReady should be reported as a transient 503, not a 500.
|
||||||
|
|
||||||
|
Regression test: this can be raised on a request served by one thread while
|
||||||
|
the plugin registry is mid-reload on another (see
|
||||||
|
plugin.registry.PluginsRegistry._reload_apps, which briefly clears Django's
|
||||||
|
app registry) - it is not a genuine server error, so the client should be
|
||||||
|
told to retry rather than seeing a hard failure.
|
||||||
|
"""
|
||||||
|
response = exception_handler(AppRegistryNotReady(), {})
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 503)
|
||||||
|
self.assertEqual(response.data['error'], 'AppRegistryNotReady')
|
||||||
|
self.assertEqual(response['Retry-After'], '1')
|
||||||
|
|
||||||
|
|
||||||
class ApiAccessTests(InvenTreeAPITestCase):
|
class ApiAccessTests(InvenTreeAPITestCase):
|
||||||
"""Tests for various access scenarios with the InvenTree API."""
|
"""Tests for various access scenarios with the InvenTree API."""
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
"""Unit tests for base mixins for plugins."""
|
"""Unit tests for base mixins for plugins."""
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from django.core.exceptions import AppRegistryNotReady
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
@@ -86,6 +89,24 @@ class UserInterfaceMixinTests(InvenTreeAPITestCase):
|
|||||||
response = self.get(url)
|
response = self.get(url)
|
||||||
self.assertEqual(len(response.data), 3)
|
self.assertEqual(len(response.data), 3)
|
||||||
|
|
||||||
|
def test_ui_feature_list_app_registry_not_ready(self):
|
||||||
|
"""A mid-reload AppRegistryNotReady should surface as a 503, not a 500.
|
||||||
|
|
||||||
|
Regression test: the plugin registry can force-reload Django's app registry
|
||||||
|
(e.g. when ENABLE_PLUGINS_INTERFACE is toggled - see
|
||||||
|
common.setting.system.reload_plugin_registry) while other requests are still
|
||||||
|
being served, so this endpoint's own settings lookup can race that reload.
|
||||||
|
"""
|
||||||
|
url = reverse('api-plugin-ui-feature-list', kwargs={'feature': 'dashboard'})
|
||||||
|
|
||||||
|
with mock.patch(
|
||||||
|
'plugin.base.ui.api.get_global_setting', side_effect=AppRegistryNotReady()
|
||||||
|
):
|
||||||
|
response = self.get(url, expected_code=503)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['error'], 'AppRegistryNotReady')
|
||||||
|
self.assertEqual(response['Retry-After'], '1')
|
||||||
|
|
||||||
def test_ui_panels(self):
|
def test_ui_panels(self):
|
||||||
"""Test that the sample UI plugin provides custom panels."""
|
"""Test that the sample UI plugin provides custom panels."""
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
|
|||||||
Reference in New Issue
Block a user