[API] Handle AppRegistryNotReady (#12571)

Explicit handling for AppRegistryNotReady error
This commit is contained in:
Oliver
2026-08-08 16:11:30 +10:00
committed by GitHub
parent 358c464349
commit 66b95cde5b
3 changed files with 62 additions and 0 deletions
@@ -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