Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

spectacular_settings caching bug in enum overrides #1244

Open
fledman opened this issue May 23, 2024 · 0 comments
Open

spectacular_settings caching bug in enum overrides #1244

fledman opened this issue May 23, 2024 · 0 comments
Labels
bug Something isn't working

Comments

@fledman
Copy link

fledman commented May 23, 2024

Describe the bug
The load_enum_name_overrides function uses a decorator to cache function execution. However, the function internally uses the global variable spectacular_settings.ENUM_NAME_OVERRIDES, which is actually not constant. So whichever set of settings are loaded during the first execution of this function are implicitly baked into the cached result.

Specifically, if you are serving multiple schemas, each one can have its own custom settings. The way this works is that a context manager wraps the core view logic, patching the global settings object:

with patched_settings(self.custom_settings):
if settings.USE_I18N and request.GET.get('lang'):
with translation.override(request.GET.get('lang')):
return self._get_schema_response(request)
else:
return self._get_schema_response(request)

try:
spectacular_settings.apply_patches(patches)
yield
finally:
spectacular_settings.clear_patches()

The end result is that your expected enum overrides are not consistently applied, leading to unpredictable schema generation.

This bug is not new. It impacts both the historical implementation:

@cache
def load_enum_name_overrides():
overrides = {}
for name, choices in spectacular_settings.ENUM_NAME_OVERRIDES.items():

as well as the recent changes to fix translation:
def load_enum_name_overrides():
return _load_enum_name_overrides(get_language())
@functools.lru_cache()
def _load_enum_name_overrides(language: str):
overrides = {}
for name, choices in spectacular_settings.ENUM_NAME_OVERRIDES.items():

To Reproduce
Suppose your drf project has multiple installed applications. Each app can serve its own schema, with potentially unique drf-specatcular settings:

# in foo/urls.py
path("schema/", SpectacularAPIView.as_view(custom_settings=FOO_SPECTACULAR_SETTINGS), name="schema"),
# in bar/urls.py
path("schema/", SpectacularAPIView.as_view(custom_settings=BAR_SPECTACULAR_SETTINGS), name="schema"),
# in project_name/urls.py
path("foo/", include("foo.urls", namespace="foo")),
path("bar/", include("bar.urls", namespace="bar")),

Assuming that foo and bar have different enum overrides, loading foo first will break bar until the server restarts, and visa-versa.

Proposed fix
Make enum_name_overrides_setting an explicit argument to _load_enum_name_overrides, so it is cached properly:

def load_enum_name_overrides():
    return _load_enum_name_overrides(
      language=get_language(),
      enum_name_overrides_setting=spectacular_settings.ENUM_NAME_OVERRIDES,
    )

@functools.lru_cache()
def _load_enum_name_overrides(language: str, enum_name_overrides_setting: dict[str, Any]):
    overrides = {}
    for name, choices in enum_name_overrides_setting.items():
    ...
    # there is one more usage of the setting at the bottom of the function that also needs to be swapped

Note that using the full settings object itself as a cached function argument will NOT work, since its hash function is not impacted by the patching process.

@tfranzel tfranzel added the bug Something isn't working label May 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants