philipn/django-rest-framework-filters

RecursionError for circular self relationship

dwoldenberg1 opened this issue · 1 comments

I am experiencing a Maximum Recursion Depth Exceeded Error when using this library to create filter classes for API views using django rest framework. I followed the documentation on how to create this type of filter relationship and still experience this recursion error: https://github.com/philipn/django-rest-framework-filters#recursive--circular-relationships . The error occurs specifically when trying to filter on objects by id only (that have a recursive filter).

I am fairly certain this is an issue with the library and not our implementation because this issue first occurred as a bug in an actual class with the fields modeled out but I was able to recreate the same issue by simply adding a RelatedFilter creating a circular reference to the class it is in without actually referencing a real field in the model the filter is for (the error occurs regardless of the fields in the model).

Code usage:

Data:

{
  'id' : 1,
  'name': 'fred',
  'best_friend': 2
}

{
  'id':2,
  'name':'daniel'
}

Filter Class Code:

# in filters.py
classNameFilter(rest_framework_filters.filterset.FilterSet):
    best_friend = RelatedFilter('people.filters.NameFilter', field_name='best_friend', queryset=Name.objects.all())
    ....

# in views_api.py
class NameViewSet(viewsets.ModelViewSet):
    serializer_class = NameSerializer
    filter_class = NameFilter
    filter_backends = (DjangoFilterBackend, )
    ...

Working API Call:

$curl "https://app.local/api/names/?best_friend__name=daniel"
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      'id':2,
      'name':'daniel'
    }
  ]
}

Broken API Call

$curl "https://app.local/api/names/?best_friend=2"
RecursionError at /api/names/
maximum recursion depth exceeded

Request Method: GET
Request URL: https://app.local/api/names/?best_friend=2
Django Version: 2.2.10
Python Executable: /usr/bin/python3
Python Version: 3.6.8
Python Path: ['/app', '/usr/local/bin', '/usr/lib64/python36.zip', '/usr/lib64/python3.6', '/usr/lib64/python3.6/lib-dynload', '/usr/local/lib64/python3.6/site-packages', '/usr/local/lib/python3.6/site-packages', '/usr/lib64/python3.6/site-packages', '/usr/lib/python3.6/site-packages', '/app']
...
Traceback:

File "/usr/local/lib/python3.6/site-packages/django/core/handlers/exception.py" in inner
  34.             response = get_response(request)

File "/usr/local/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response
  115.                 response = self.process_exception_by_middleware(e, request)

File "/usr/local/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response
  113.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/usr/lib64/python3.6/contextlib.py" in inner
  52.                 return func(*args, **kwds)

File "/usr/local/lib/python3.6/site-packages/django/views/decorators/csrf.py" in wrapped_view
  54.         return view_func(*args, **kwargs)

File "/usr/local/lib/python3.6/site-packages/rest_framework/viewsets.py" in view
  114.             return self.dispatch(request, *args, **kwargs)

File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py" in dispatch
  505.             response = self.handle_exception(exc)

File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py" in handle_exception
  465.             self.raise_uncaught_exception(exc)

File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py" in raise_uncaught_exception
  476.         raise exc

File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py" in dispatch
  502.             response = handler(request, *args, **kwargs)

File "/usr/local/lib/python3.6/site-packages/rest_framework/mixins.py" in list
  38.         queryset = self.filter_queryset(self.get_queryset())

File "/usr/local/lib/python3.6/site-packages/rest_framework/generics.py" in filter_queryset
  150.             queryset = backend().filter_queryset(self.request, queryset, self)

File "/usr/local/lib/python3.6/site-packages/django_filters/rest_framework/backends.py" in filter_queryset
  90.         filterset = self.get_filterset(request, queryset, view)

File "/usr/local/lib/python3.6/site-packages/django_filters/rest_framework/backends.py" in get_filterset
  36.         return filterset_class(**kwargs)

File "/usr/local/lib/python3.6/site-packages/rest_framework_filters/filterset.py" in __init__
  93.         self.related_filtersets = self.get_related_filtersets()

File "/usr/local/lib/python3.6/site-packages/rest_framework_filters/filterset.py" in get_related_filtersets
  235.                 prefix=self.form_prefix,

File "/usr/local/lib/python3.6/site-packages/rest_framework_filters/filterset.py" in __init__
  93.         self.related_filtersets = self.get_related_filtersets()

File "/usr/local/lib/python3.6/site-packages/rest_framework_filters/filterset.py" in get_related_filtersets
  235.                 prefix=self.form_prefix,

File "/usr/local/lib/python3.6/site-packages/rest_framework_filters/filterset.py" in __init__
  93.         self.related_filtersets = self.get_related_filtersets()

File "/usr/local/lib/python3.6/site-packages/rest_framework_filters/filterset.py" in get_related_filtersets
  235.                 prefix=self.form_prefix,
...

Relevant version information:

Django Version: 2.2.10
django-filter==2.2.0
django-filters==0.2.1
djangorestframework-filters==1.0.0.dev0

Great catch. The related param filtering works by matching on its related prefix. e.g., best_friend__name is correctly handled because it's prefixed with best_friend__. Of course, best_friend isn't prefixed by itself so the relationship check was failing. Adding a simple edge case check for when the param name is the relationship name fixed this.

Fixed in #343. I'll be issuing a 1.0.0.dev.1 prerelease shortly.