Skip to content

Commit 0476816

Browse files
author
James Riley McHugh
committed
Added protections to AttributeErrors raised within properties
1 parent e9f3fd2 commit 0476816

File tree

2 files changed

+39
-16
lines changed

2 files changed

+39
-16
lines changed

rest_framework/request.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ def wrap_attributeerrors():
7878
raise exc.with_traceback(info[2])
7979

8080

81+
def safe_property(func):
82+
"""Property decorator to ensure AttributeErrors raised in properties are properly handled"""
83+
84+
@property
85+
def new_func(self):
86+
with wrap_attributeerrors():
87+
return func(self)
88+
89+
return new_func
90+
8191
class Empty:
8292
"""
8393
Placeholder for unset attributes.
@@ -193,12 +203,12 @@ def __class_getitem__(cls, *args, **kwargs):
193203
def _default_negotiator(self):
194204
return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS()
195205

196-
@property
206+
@safe_property
197207
def content_type(self):
198208
meta = self._request.META
199209
return meta.get('CONTENT_TYPE', meta.get('HTTP_CONTENT_TYPE', ''))
200210

201-
@property
211+
@safe_property
202212
def stream(self):
203213
"""
204214
Returns an object that may be used to stream the request content.
@@ -207,28 +217,27 @@ def stream(self):
207217
self._load_stream()
208218
return self._stream
209219

210-
@property
220+
@safe_property
211221
def query_params(self):
212222
"""
213223
More semantically correct name for request.GET.
214224
"""
215225
return self._request.GET
216226

217-
@property
227+
@safe_property
218228
def data(self):
219229
if not _hasattr(self, '_full_data'):
220230
self._load_data_and_files()
221231
return self._full_data
222232

223-
@property
233+
@safe_property
224234
def user(self):
225235
"""
226236
Returns the user associated with the current request, as authenticated
227237
by the authentication classes provided to the request.
228238
"""
229239
if not hasattr(self, '_user'):
230-
with wrap_attributeerrors():
231-
self._authenticate()
240+
self._authenticate()
232241
return self._user
233242

234243
@user.setter
@@ -244,15 +253,14 @@ def user(self, value):
244253
self._user = value
245254
self._request.user = value
246255

247-
@property
256+
@safe_property
248257
def auth(self):
249258
"""
250259
Returns any non-user authentication information associated with the
251260
request, such as an authentication token.
252261
"""
253262
if not hasattr(self, '_auth'):
254-
with wrap_attributeerrors():
255-
self._authenticate()
263+
self._authenticate()
256264
return self._auth
257265

258266
@auth.setter
@@ -264,15 +272,14 @@ def auth(self, value):
264272
self._auth = value
265273
self._request.auth = value
266274

267-
@property
275+
@safe_property
268276
def successful_authenticator(self):
269277
"""
270278
Return the instance of the authentication instance class that was used
271279
to authenticate the request, or `None`.
272280
"""
273281
if not hasattr(self, '_authenticator'):
274-
with wrap_attributeerrors():
275-
self._authenticate()
282+
self._authenticate()
276283
return self._authenticator
277284

278285
def _load_data_and_files(self):
@@ -420,9 +427,9 @@ def __getattr__(self, attr):
420427
_request = self.__getattribute__("_request")
421428
return getattr(_request, attr)
422429
except AttributeError:
423-
return self.__getattribute__(attr)
430+
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attr}'")
424431

425-
@property
432+
@safe_property
426433
def POST(self):
427434
# Ensure that request.POST uses our request parsing.
428435
if not _hasattr(self, '_data'):
@@ -431,7 +438,7 @@ def POST(self):
431438
return self._data
432439
return QueryDict('', encoding=self._request._encoding)
433440

434-
@property
441+
@safe_property
435442
def FILES(self):
436443
# Leave this one alone for backwards compat with Django's request.FILES
437444
# Different from the other two cases, which are not valid property

tests/test_request.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,22 @@ def test_duplicate_request_form_data_access(self):
347347
assert request.content_type.startswith('multipart/form-data')
348348
assert response.data == {'a': ['b']}
349349

350+
def test_parser_attribute_error(self):
351+
"""Ensure attribute errors raised when parsing are properly re-raised"""
352+
expected_message = "Internal error"
353+
354+
class BrokenParser:
355+
media_type = "application/json"
356+
357+
def parse(self, *args, **kwargs):
358+
raise AttributeError(expected_message)
359+
360+
http_request = factory.post('/', data={}, format="json")
361+
request = Request(http_request, parsers=[BrokenParser()])
362+
363+
with self.assertRaisesMessage(WrappedAttributeError, expected_message):
364+
request.data
365+
350366

351367
class TestDeepcopy(TestCase):
352368

0 commit comments

Comments
 (0)