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 django.conf import settings
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
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!
|
||||
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
|
||||
if isinstance(exc, DjangoValidationError):
|
||||
exc = DRFValidationError(detail=serializers.as_serializer_error(exc))
|
||||
|
||||
@@ -4,12 +4,15 @@ from base64 import b64encode
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework import status
|
||||
|
||||
from InvenTree.api import read_license_file
|
||||
from InvenTree.api_version import INVENTREE_API_VERSION
|
||||
from InvenTree.exceptions import exception_handler
|
||||
from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase
|
||||
from InvenTree.version import inventreeApiText, parse_version_text
|
||||
from users.ruleset import RULESET_NAMES
|
||||
@@ -66,6 +69,25 @@ class HTMLAPITests(InvenTreeTestCase):
|
||||
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):
|
||||
"""Tests for various access scenarios with the InvenTree API."""
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"""Unit tests for base mixins for plugins."""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from django.urls import reverse
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
@@ -86,6 +89,24 @@ class UserInterfaceMixinTests(InvenTreeAPITestCase):
|
||||
response = self.get(url)
|
||||
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):
|
||||
"""Test that the sample UI plugin provides custom panels."""
|
||||
from part.models import Part
|
||||
|
||||
Reference in New Issue
Block a user