Description
Discussed in #9426
Originally posted by james-mchugh June 3, 2024
Hello.
First, thank you for the hard work on DRF. It has been great workin with it.
There seems to be an issue where an AttributeError
raised in a parser is being swallowed by the Request
class's __getattr__
method. I've noticed a similar issue reported previously (#4896), but it was closed as the reporter associated with an error from a separate package.
I encountered this because I wrote the server and tested using Python 3.11, and another developer attempted to test using 3.10. My custom parser used a function that was not available in Python 3.10 (hashlib.file_digest), which caused an AttributeError
when hitting certain endpoints. Instead of seeing a 500 response and a traceback in the server logs, we were getting a 200 response with an empty dictionary in the response body. We were scratching our heads for a while, as there was no indication anything was going wrong.
Environment
OS: Darwin 23.4.0 Darwin Kernel Version 23.4.0 x86_64
Python: 3.10 + 3.11
DRF: 3.15.1
Django: 5.0.6
Expected Behavior
500 Internal server error response and traceback in server logs.
Actual Behavior
200 response and no errors in server logs.
$ curl --json '{"foo": "bar"}' http://localhost:8000/foos/
{}
[04/Jun/2024 02:59:15] "POST /foos/ HTTP/1.1" 200 2
Code to Reproduce
from rest_framework import viewsets, response, parsers, routers
class BrokenParser(parsers.JSONParser):
def parse(self, stream, media_type=None, parser_context=None):
raise AttributeError
class TestViewSet(viewsets.ViewSet):
parser_classes = (BrokenParser,)
def create(self, request, **kwargs):
return response.Response(request.data)
router = routers.DefaultRouter()
router.register("foos", TestViewSet, "foo")
urlpatterns = router.urls
Investigation
This appears to be happening because accessing the data
property lazily parses data. If the parsing raises an AttributeError
, this is raised up to https://github.com/encode/django-rest-framework/blob/master/rest_framework/request.py#L359 where self._full_data
is set to an empty dictionary before the error is re-raised.
This error then raises up and causes the attribute access to fallback to the Request.__getattr__
method via Python's data model. Here, the data
attribute is attempted to be retrieved from the WSGIRequest
stored in Request._request
, and once again, this raises an AttributeError
which is caught by https://github.com/encode/django-rest-framework/blob/master/rest_framework/request.py#L423. From here, the original Request.__getattribute__
handling runs again. Now when the data
getter runs, self._full_data
is already set to an empty dictionary which is then returned.
In the end, an empty response is returned and the error is silently ignored.
Noob question, but why does Request.__getattr__
call self.__getattribute__
when an AttributeError
occurs? Based on the Python Data model linked above, __getattr__
should only run if __getattribute__
fails in the first place, so by the time __getattr__
runs, __getattribute__
already tried and failed.