[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 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